feat(orders): show SMS delivery confirmation and 2nd-SMS skip reason

This commit is contained in:
root 2026-08-06 13:55:42 +00:00
parent fefb8065be
commit c321353e8f
3 changed files with 84 additions and 6 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-v98";
const RUNTIME_CACHE = "construction-delivery-runtime-v98";
const STATIC_CACHE = "construction-delivery-static-v99";
const RUNTIME_CACHE = "construction-delivery-runtime-v99";
const APP_SHELL_URLS = ["/", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"];
self.addEventListener("install", (event) => {

View File

@ -392,6 +392,7 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
const [smsLogs, setSmsLogs] = useState([]);
const [pushLogs, setPushLogs] = useState([]);
const [pushSubscriptions, setPushSubscriptions] = useState([]);
const [cityProfile, setCityProfile] = useState(null);
const [invitation, setInvitation] = useState(null);
useEffect(() => {
@ -413,7 +414,7 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
try {
const { data: smsData, error: smsError } = await supabase
.from("sms_campaign_log")
.select("id, campaign_type, status, sms_text, error_message, created_at, sent_to")
.select("id, campaign_type, status, sms_text, error_message, created_at, updated_at, sent_to")
.eq("order_group_id", order.id)
.order("created_at", { ascending: false })
.limit(50);
@ -449,6 +450,19 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
setPushSubscriptions([]);
}
try {
const town = order?.town || order?.city || null;
if (town) {
const { data: cpData, error: cpError } = await supabase
.from("sms_city_profiles")
.select("city, second_sms_enabled, sms_enabled")
.eq("city", town)
.maybeSingle();
if (cancelled) return;
if (!cpError) setCityProfile(cpData || null);
}
} catch { if (!cancelled) setCityProfile(null); }
try {
const { data: invData, error: invError } = await supabase
.from("delivery_invitations")
@ -478,6 +492,16 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
if (specificLabel) label = specificLabel;
}
// For field_change: skip noisy system rewrites of sms timestamps (they are
// already shown as separate "SMS доставлена" events). Other changes keep their label.
const smsRewriteFields = ["first_sms_sent_at", "second_sms_sent_at"];
if (h.action === "field_change") {
const keys = Object.keys(h.metadata?.changes || {}).filter((k) => !SKIP_FIELDS.has(k));
if (keys.length === 1 && smsRewriteFields.includes(keys[0])) {
continue; // system update of delivery timestamp redundant
}
}
events.push({
id: `hist-${h.id}`,
created_at: h.created_at,
@ -491,6 +515,9 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
if (smsLogs && smsLogs.length > 0) {
for (const s of smsLogs) {
const text = s.sms_text ? (s.sms_text.length > 100 ? s.sms_text.slice(0, 100) + "…" : s.sms_text) : "";
const isDelivered = s.status === "delivered";
const deliveryTime = isDelivered ? (s.updated_at || s.created_at) : null;
// sent time is created_at
events.push({
id: `sms-${s.id}`,
created_at: s.created_at,
@ -499,7 +526,20 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
smsStatus: SMS_STATUS_LABELS[s.status] ?? s.status ?? "—",
smsText: text,
smsError: s.error_message || null,
smsSentAt: s.created_at,
smsDeliveredAt: deliveryTime,
isDelivered,
});
// Separate delivery-confirmation event with the actual delivery time
if (isDelivered) {
events.push({
id: `sms-delivered-${s.id}`,
created_at: deliveryTime,
type: "sms_delivered",
label: "SMS доставлена (подтверждение)",
campaignType: s.campaign_type,
});
}
}
}
@ -530,9 +570,28 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
}
}
// Reason why the 2nd SMS was not sent (e.g. city profile disables it)
const secondSmsSent = order?.secondSmsSentAt || order?.second_sms_sent_at;
const hasSecondSmsLog = smsLogs.some((s) => s.campaign_type === "second_sms");
if (!secondSmsSent && !hasSecondSmsLog) {
const firstSmsDelivered = smsLogs.some((s) => s.campaign_type === "first_sms" && s.status === "delivered");
const town = order?.town || order?.city || null;
const cityDisables = cityProfile && cityProfile.second_sms_enabled === false;
if (firstSmsDelivered && cityDisables && town) {
events.push({
id: `second-sms-reason`,
created_at: order?.firstSmsSentAt || order?.first_sms_sent_at || new Date().toISOString(),
type: "reason",
label: "2-е SMS не отправлено",
reason: `Для города ${town} вторая SMS отключена в настройках.`,
tone: "warning",
});
}
}
events.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
return events;
}, [history, smsLogs, pushLogs, invitation]);
}, [history, smsLogs, pushLogs, invitation, cityProfile]);
const activePushSubscriptions = pushSubscriptions.filter((sub) => sub.is_active);
const latestPush = pushLogs[0] || null;
@ -601,11 +660,29 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
{ev.smsStatus}
</span>
</p>
<p className="mt-0.5 text-xs text-[var(--color-text-muted)]">
Отправлено: {fmtTimestamp(ev.smsSentAt)}
{ev.isDelivered ? ` · Доставлено: ${fmtTimestamp(ev.smsDeliveredAt)}` : ""}
</p>
{ev.smsText && <p className="mt-0.5 text-xs text-[var(--color-text-muted)]">«{ev.smsText}»</p>}
{ev.smsError && <p className="mt-0.5 text-xs text-[var(--color-danger)]">Ошибка: {ev.smsError}</p>}
</>
)}
{ev.type === "sms_delivered" && (
<p className="mt-0.5 text-xs text-[var(--color-accent)]">
Подтверждение доставки в {fmtTimestamp(ev.created_at)}
</p>
)}
{ev.type === "reason" && (
<p className={`mt-0.5 text-xs ${
ev.tone === "warning" ? "text-[var(--color-warning)]" : "text-[var(--color-text-muted)]"
}`}>
{ev.reason}
</p>
)}
{ev.type === "push" && (
<>
<p className="mt-0.5 text-sm">

View File

@ -189,6 +189,7 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
customerPhone,
customerPhoneNormalized: parsedKey.phone || normalizePhone(customerPhone),
customerDate,
town: row.town || null,
deliveryAddress: resolvedDeliveryAddress,
originalDeliveryAddress,
customerAddress,
@ -285,8 +286,8 @@ 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, 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, 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_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`;
export const updateOrderGroupDeliveryChoice = async ({
orderGroupId,