feat(logistics): highlight and filter orders with SMS problems
This commit is contained in:
parent
c321353e8f
commit
d5849f0ab2
|
|
@ -1,8 +1,8 @@
|
||||||
const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1";
|
const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1";
|
||||||
|
|
||||||
if (!isLocalhost) {
|
if (!isLocalhost) {
|
||||||
const STATIC_CACHE = "construction-delivery-static-v99";
|
const STATIC_CACHE = "construction-delivery-static-v100";
|
||||||
const RUNTIME_CACHE = "construction-delivery-runtime-v99";
|
const RUNTIME_CACHE = "construction-delivery-runtime-v100";
|
||||||
const APP_SHELL_URLS = ["/", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"];
|
const APP_SHELL_URLS = ["/", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"];
|
||||||
|
|
||||||
self.addEventListener("install", (event) => {
|
self.addEventListener("install", (event) => {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
getOrderGroupDisplayStatusLabel,
|
getOrderGroupDisplayStatusLabel,
|
||||||
getOrderGroupDisplayStatusValue,
|
getOrderGroupDisplayStatusValue,
|
||||||
getOrderGroupStatusTone,
|
getOrderGroupStatusTone,
|
||||||
|
getSmsProblemTone,
|
||||||
ORDER_GROUP_DISPLAY_STATUS_OPTIONS,
|
ORDER_GROUP_DISPLAY_STATUS_OPTIONS,
|
||||||
} from "../../services/orderGroupViews";
|
} from "../../services/orderGroupViews";
|
||||||
import { Badge } from "../UI/Badge";
|
import { Badge } from "../UI/Badge";
|
||||||
|
|
@ -159,13 +160,19 @@ const renderRow = (g, onSelectSet) => {
|
||||||
const statusTone = getOrderGroupStatusTone(g);
|
const statusTone = getOrderGroupStatusTone(g);
|
||||||
const stale = isStale(g);
|
const stale = isStale(g);
|
||||||
const databaseAge = getDatabaseAge(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 (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
key={g.id}
|
key={g.id}
|
||||||
onClick={() => onSelectSet(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)] ${
|
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">
|
<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.city && filters.city.trim()) return true;
|
||||||
if (filters.deliveryStatus && filters.deliveryStatus !== "all") return true;
|
if (filters.deliveryStatus && filters.deliveryStatus !== "all") return true;
|
||||||
if (filters.deliveryHalfDay && filters.deliveryHalfDay !== "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.dateFrom) return true;
|
||||||
if (filters.dateTo) return true;
|
if (filters.dateTo) return true;
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -368,7 +376,7 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
||||||
// Reset to page 1 when filters change
|
// Reset to page 1 when filters change
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setPage(1);
|
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 totalGroups = rankedGroups.length;
|
||||||
const totalPages = Math.max(1, Math.ceil(totalGroups / PAGE_SIZE));
|
const totalPages = Math.max(1, Math.ceil(totalGroups / PAGE_SIZE));
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,12 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
|
||||||
{ value: "pickup", label: "Самовывоз" },
|
{ value: "pickup", label: "Самовывоз" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const smsProblemOptions = [
|
||||||
|
{ value: "all", label: "Проблемы SMS" },
|
||||||
|
{ value: "problem_not_sent", label: "SMS не отправлено" },
|
||||||
|
{ value: "problem_not_delivered", label: "SMS не доставлено" },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel className="p-4">
|
<Panel className="p-4">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
|
|
@ -190,6 +196,13 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
|
||||||
placeholder="Тип получения"
|
placeholder="Тип получения"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DropdownSelect
|
||||||
|
value={filters.smsStatus || "all"}
|
||||||
|
options={smsProblemOptions}
|
||||||
|
onChange={(v) => updateFilter("smsStatus", v)}
|
||||||
|
placeholder="Проблемы SMS"
|
||||||
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
className="h-[46px] py-0"
|
className="h-[46px] py-0"
|
||||||
placeholder="Поиск по группе, клиенту или телефону"
|
placeholder="Поиск по группе, клиенту или телефону"
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,23 @@ export const parseGroupDate = (value) => {
|
||||||
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
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 = {}) => {
|
export const filterOrderGroups = (groups, filters = {}) => {
|
||||||
const query = normalizeDate(filters.query).trim().toLowerCase();
|
const query = normalizeDate(filters.query).trim().toLowerCase();
|
||||||
const status = filters.status || "all";
|
const status = filters.status || "all";
|
||||||
|
|
@ -252,6 +269,7 @@ export const filterOrderGroups = (groups, filters = {}) => {
|
||||||
const dateFrom = normalizeDate(filters.dateFrom);
|
const dateFrom = normalizeDate(filters.dateFrom);
|
||||||
const dateTo = normalizeDate(filters.dateTo);
|
const dateTo = normalizeDate(filters.dateTo);
|
||||||
const deliveryHalfDay = normalizeDate(filters.deliveryHalfDay || filters.timeSlot || "all");
|
const deliveryHalfDay = normalizeDate(filters.deliveryHalfDay || filters.timeSlot || "all");
|
||||||
|
const smsStatus = filters.smsStatus || "all";
|
||||||
|
|
||||||
const isWithinDateRange = (group) => {
|
const isWithinDateRange = (group) => {
|
||||||
const deliveryDate = parseGroupDate(getDeliveryDate(group));
|
const deliveryDate = parseGroupDate(getDeliveryDate(group));
|
||||||
|
|
@ -343,6 +361,12 @@ export const filterOrderGroups = (groups, filters = {}) => {
|
||||||
return false;
|
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) {
|
if (!query) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,8 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
|
||||||
manualConfirmationAt: row.manual_confirmation_at || null,
|
manualConfirmationAt: row.manual_confirmation_at || null,
|
||||||
paidStorageAt: row.paid_storage_at || null,
|
paidStorageAt: row.paid_storage_at || null,
|
||||||
notificationStatus: normalizeText(row.notification_status),
|
notificationStatus: normalizeText(row.notification_status),
|
||||||
|
lastSmsError: row.last_sms_error || null,
|
||||||
|
smsAttempts: row.sms_attempts || 0,
|
||||||
createdFromExchangeAt: row.created_from_exchange_at || null,
|
createdFromExchangeAt: row.created_from_exchange_at || null,
|
||||||
sourceKey: row.source_key || null,
|
sourceKey: row.source_key || null,
|
||||||
legacyCustomerName: row.legacy_customer_name || 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_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 ({
|
export const updateOrderGroupDeliveryChoice = async ({
|
||||||
orderGroupId,
|
orderGroupId,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue