feat(orders): show SMS delivery confirmation and 2nd-SMS skip reason
This commit is contained in:
parent
fefb8065be
commit
c321353e8f
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue