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";
|
const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1";
|
||||||
|
|
||||||
if (!isLocalhost) {
|
if (!isLocalhost) {
|
||||||
const STATIC_CACHE = "construction-delivery-static-v98";
|
const STATIC_CACHE = "construction-delivery-static-v99";
|
||||||
const RUNTIME_CACHE = "construction-delivery-runtime-v98";
|
const RUNTIME_CACHE = "construction-delivery-runtime-v99";
|
||||||
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) => {
|
||||||
|
|
|
||||||
|
|
@ -392,6 +392,7 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
|
||||||
const [smsLogs, setSmsLogs] = useState([]);
|
const [smsLogs, setSmsLogs] = useState([]);
|
||||||
const [pushLogs, setPushLogs] = useState([]);
|
const [pushLogs, setPushLogs] = useState([]);
|
||||||
const [pushSubscriptions, setPushSubscriptions] = useState([]);
|
const [pushSubscriptions, setPushSubscriptions] = useState([]);
|
||||||
|
const [cityProfile, setCityProfile] = useState(null);
|
||||||
const [invitation, setInvitation] = useState(null);
|
const [invitation, setInvitation] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -413,7 +414,7 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
|
||||||
try {
|
try {
|
||||||
const { data: smsData, error: smsError } = await supabase
|
const { data: smsData, error: smsError } = await supabase
|
||||||
.from("sms_campaign_log")
|
.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)
|
.eq("order_group_id", order.id)
|
||||||
.order("created_at", { ascending: false })
|
.order("created_at", { ascending: false })
|
||||||
.limit(50);
|
.limit(50);
|
||||||
|
|
@ -449,6 +450,19 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
|
||||||
setPushSubscriptions([]);
|
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 {
|
try {
|
||||||
const { data: invData, error: invError } = await supabase
|
const { data: invData, error: invError } = await supabase
|
||||||
.from("delivery_invitations")
|
.from("delivery_invitations")
|
||||||
|
|
@ -478,6 +492,16 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
|
||||||
if (specificLabel) label = specificLabel;
|
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({
|
events.push({
|
||||||
id: `hist-${h.id}`,
|
id: `hist-${h.id}`,
|
||||||
created_at: h.created_at,
|
created_at: h.created_at,
|
||||||
|
|
@ -491,6 +515,9 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
|
||||||
if (smsLogs && smsLogs.length > 0) {
|
if (smsLogs && smsLogs.length > 0) {
|
||||||
for (const s of smsLogs) {
|
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 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({
|
events.push({
|
||||||
id: `sms-${s.id}`,
|
id: `sms-${s.id}`,
|
||||||
created_at: s.created_at,
|
created_at: s.created_at,
|
||||||
|
|
@ -499,7 +526,20 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
|
||||||
smsStatus: SMS_STATUS_LABELS[s.status] ?? s.status ?? "—",
|
smsStatus: SMS_STATUS_LABELS[s.status] ?? s.status ?? "—",
|
||||||
smsText: text,
|
smsText: text,
|
||||||
smsError: s.error_message || null,
|
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));
|
events.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||||
return events;
|
return events;
|
||||||
}, [history, smsLogs, pushLogs, invitation]);
|
}, [history, smsLogs, pushLogs, invitation, cityProfile]);
|
||||||
|
|
||||||
const activePushSubscriptions = pushSubscriptions.filter((sub) => sub.is_active);
|
const activePushSubscriptions = pushSubscriptions.filter((sub) => sub.is_active);
|
||||||
const latestPush = pushLogs[0] || null;
|
const latestPush = pushLogs[0] || null;
|
||||||
|
|
@ -601,11 +660,29 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
|
||||||
{ev.smsStatus}
|
{ev.smsStatus}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</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.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.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" && (
|
{ev.type === "push" && (
|
||||||
<>
|
<>
|
||||||
<p className="mt-0.5 text-sm">
|
<p className="mt-0.5 text-sm">
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,7 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
|
||||||
customerPhone,
|
customerPhone,
|
||||||
customerPhoneNormalized: parsedKey.phone || normalizePhone(customerPhone),
|
customerPhoneNormalized: parsedKey.phone || normalizePhone(customerPhone),
|
||||||
customerDate,
|
customerDate,
|
||||||
|
town: row.town || null,
|
||||||
deliveryAddress: resolvedDeliveryAddress,
|
deliveryAddress: resolvedDeliveryAddress,
|
||||||
originalDeliveryAddress,
|
originalDeliveryAddress,
|
||||||
customerAddress,
|
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_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, 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, 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