feat(logistics): highlight and filter orders with SMS problems

This commit is contained in:
root 2026-08-06 15:12:23 +00:00
parent c321353e8f
commit d5849f0ab2
5 changed files with 52 additions and 5 deletions

View File

@ -1,8 +1,8 @@
const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1";
if (!isLocalhost) {
const STATIC_CACHE = "construction-delivery-static-v99";
const RUNTIME_CACHE = "construction-delivery-runtime-v99";
const STATIC_CACHE = "construction-delivery-static-v100";
const RUNTIME_CACHE = "construction-delivery-runtime-v100";
const APP_SHELL_URLS = ["/", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"];
self.addEventListener("install", (event) => {

View File

@ -18,6 +18,7 @@ import {
getOrderGroupDisplayStatusLabel,
getOrderGroupDisplayStatusValue,
getOrderGroupStatusTone,
getSmsProblemTone,
ORDER_GROUP_DISPLAY_STATUS_OPTIONS,
} from "../../services/orderGroupViews";
import { Badge } from "../UI/Badge";
@ -159,13 +160,19 @@ const renderRow = (g, onSelectSet) => {
const statusTone = getOrderGroupStatusTone(g);
const stale = isStale(g);
const databaseAge = getDatabaseAge(g);
const smsTone = getSmsProblemTone(g);
const smsRowClass = smsTone === "not_sent"
? "bg-[rgba(239,68,68,0.08)]"
: smsTone === "not_delivered"
? "bg-[rgba(245,158,11,0.08)]"
: "";
return (
<button
type="button"
key={g.id}
onClick={() => onSelectSet(g.id)}
className={`grid ${COLS} ${MIN_W} w-full border-t border-[var(--color-border)] text-left transition hover:bg-[var(--color-accent-soft)] ${
databaseAge.rowClass || (stale ? "bg-[rgba(245,158,11,0.06)]" : "")
databaseAge.rowClass || (stale ? "bg-[rgba(245,158,11,0.06)]" : "") || smsRowClass
}`}
>
<div className="min-w-0 px-3 py-2.5">
@ -263,6 +270,7 @@ const isFilterActive = (filters) => {
if (filters.city && filters.city.trim()) return true;
if (filters.deliveryStatus && filters.deliveryStatus !== "all") return true;
if (filters.deliveryHalfDay && filters.deliveryHalfDay !== "all") return true;
if (filters.smsStatus && filters.smsStatus !== "all") return true;
if (filters.dateFrom) return true;
if (filters.dateTo) return true;
return false;
@ -368,7 +376,7 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
// Reset to page 1 when filters change
React.useEffect(() => {
setPage(1);
}, [filters.query, filters.displayStatus, filters.city, filters.deliveryStatus, filters.deliveryHalfDay, filters.dateFrom, filters.dateTo]);
}, [filters.query, filters.displayStatus, filters.city, filters.deliveryStatus, filters.deliveryHalfDay, filters.smsStatus, filters.dateFrom, filters.dateTo]);
const totalGroups = rankedGroups.length;
const totalPages = Math.max(1, Math.ceil(totalGroups / PAGE_SIZE));

View File

@ -120,6 +120,12 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
{ value: "pickup", label: "Самовывоз" },
];
const smsProblemOptions = [
{ value: "all", label: "Проблемы SMS" },
{ value: "problem_not_sent", label: "SMS не отправлено" },
{ value: "problem_not_delivered", label: "SMS не доставлено" },
];
return (
<Panel className="p-4">
<div className="flex flex-col gap-3">
@ -190,6 +196,13 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
placeholder="Тип получения"
/>
<DropdownSelect
value={filters.smsStatus || "all"}
options={smsProblemOptions}
onChange={(v) => updateFilter("smsStatus", v)}
placeholder="Проблемы SMS"
/>
<Input
className="h-[46px] py-0"
placeholder="Поиск по группе, клиенту или телефону"

View File

@ -244,6 +244,23 @@ export const parseGroupDate = (value) => {
return Number.isNaN(parsed.getTime()) ? null : parsed;
};
/**
* SMS delivery problem tone used for both table highlighting and filtering.
* Returns "ok" | "not_sent" | "not_delivered".
*/
export const getSmsProblemTone = (group) => {
if (!group) return "ok";
const firstSms = group.firstSmsSentAt || group.first_sms_sent_at;
const secondSms = group.secondSmsSentAt || group.second_sms_sent_at;
const lastError = group.lastSmsError;
// Sent an SMS (1st or 2nd) and it has a recorded delivery error → problem with delivery.
if ((firstSms || secondSms) && lastError) return "not_delivered";
// SMS should be expected but not sent at all yet.
if (!firstSms && !secondSms) return "not_sent";
// Sent but no error recorded — assume delivered / in progress.
return "ok";
};
export const filterOrderGroups = (groups, filters = {}) => {
const query = normalizeDate(filters.query).trim().toLowerCase();
const status = filters.status || "all";
@ -252,6 +269,7 @@ export const filterOrderGroups = (groups, filters = {}) => {
const dateFrom = normalizeDate(filters.dateFrom);
const dateTo = normalizeDate(filters.dateTo);
const deliveryHalfDay = normalizeDate(filters.deliveryHalfDay || filters.timeSlot || "all");
const smsStatus = filters.smsStatus || "all";
const isWithinDateRange = (group) => {
const deliveryDate = parseGroupDate(getDeliveryDate(group));
@ -343,6 +361,12 @@ export const filterOrderGroups = (groups, filters = {}) => {
return false;
}
if (smsStatus !== "all") {
const tone = getSmsProblemTone(group);
if (smsStatus === "problem_not_sent" && tone !== "not_sent") return false;
if (smsStatus === "problem_not_delivered" && tone !== "not_delivered") return false;
}
if (!query) {
return true;
}

View File

@ -208,6 +208,8 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
manualConfirmationAt: row.manual_confirmation_at || null,
paidStorageAt: row.paid_storage_at || null,
notificationStatus: normalizeText(row.notification_status),
lastSmsError: row.last_sms_error || null,
smsAttempts: row.sms_attempts || 0,
createdFromExchangeAt: row.created_from_exchange_at || null,
sourceKey: row.source_key || null,
legacyCustomerName: row.legacy_customer_name || null,
@ -287,7 +289,7 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
};
const ORDER_GROUP_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, town, customer_name, customer_phone, customer_phone_normalized, customer_date, orders_total, orders_ready, orders_not_ready, source_orders, order_list, order_list_structured, delivery_invitation_id, delivery_link, delivery_link_code, notification_status, sms_attempts, first_sms_sent_at, second_sms_sent_at, last_sms_error, next_notification_check_at, delivery_date, delivery_time, delivery_address, customer_address, delivery_date_source, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, has_delivery_problem, delivery_problem_note, pickup_code, synced_to_1c_at, manager_name, manager_email, manager_tel, payed_ship, is_payed_ship, delivery_invitation:delivery_invitations!order_groups_delivery_invitation_id_fkey(opened_at, access_count, last_accessed_at), called, called_at, called_comment, order_history(id, action, old_status, new_status, user_id, metadata, created_at)`;
const ORDER_GROUP_LIST_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, town, customer_name, customer_phone, customer_phone_normalized, customer_date, orders_total, orders_ready, orders_not_ready, notification_status, first_sms_sent_at, second_sms_sent_at, delivery_date, delivery_time, delivery_address, customer_address, delivery_date_source, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, has_delivery_problem, delivery_problem_note, pickup_code, synced_to_1c_at, manager_name, manager_email, manager_tel, payed_ship, is_payed_ship, called, called_at, called_comment`;
const ORDER_GROUP_LIST_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, town, customer_name, customer_phone, customer_phone_normalized, customer_date, orders_total, orders_ready, orders_not_ready, notification_status, sms_attempts, last_sms_error, first_sms_sent_at, second_sms_sent_at, delivery_date, delivery_time, delivery_address, customer_address, delivery_date_source, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, has_delivery_problem, delivery_problem_note, pickup_code, synced_to_1c_at, manager_name, manager_email, manager_tel, payed_ship, is_payed_ship, called, called_at, called_comment`;
export const updateOrderGroupDeliveryChoice = async ({
orderGroupId,