feat(client): номер доставки и код выдачи + «Доставка» вместо «Счета»; в SMS — {order} и {code}
Клиентская страница согласования:
- приглашение теперь отдаёт deliveryType и pickupCode (public view) — до этого
страница не знала, что заказ самовывозный, и говорила «Доставка заказа /
Согласование доставки» даже для самовывоза; в шапке и hero-тексте выбор по типу
- в блоке с кодом: «Ваш номер доставки 010796» + «Ваш код выдачи 7598»
(номер берём из состава заказа, «ТП\-010796» → «010796»)
- ссылка на заказ теперь «Доставка ТП-010796» вместо «Счет ТП\-010796»,
экранированный дефис из обмена с 1С больше не показывается
SMS: в шаблоны добавились переменные {order}/{номер}/{номер_доставки} (номер
доставки) и {code}/{код}/{код_выдачи} (код выдачи) — работают и в шаблоне
кампании, и в тексте профиля города. Дефолтные тексты самовывоза (и значения в
БД, см. supabase/sms_pickup_code_placeholders.sql) теперь содержат номер и код;
в админке у полей появилась памятка по переменным и предпросмотр с подстановкой.
service-worker v118 → v119
This commit is contained in:
parent
b69e5172d6
commit
d98c44c9fd
|
|
@ -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-v118";
|
||||
const RUNTIME_CACHE = "construction-delivery-runtime-v118";
|
||||
const STATIC_CACHE = "construction-delivery-static-v119";
|
||||
const RUNTIME_CACHE = "construction-delivery-runtime-v119";
|
||||
const APP_SHELL_URLS = ["/", "/client", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"];
|
||||
|
||||
// Only these are treated as cacheable static assets. Everything else
|
||||
|
|
|
|||
|
|
@ -68,10 +68,16 @@ LIMIT_ERROR_CODES = {"131", "132", "230", "231", "232"}
|
|||
# Шаблоны живут в админке (sms_campaign_settings):
|
||||
# sms_text_template — текст для ДОСТАВКИ
|
||||
# sms_text_template_pickup — текст для САМОВЫВОЗА
|
||||
# {link} подставляется ссылкой согласования. Пустой шаблон → DEFAULT_TEXT_*.
|
||||
# Подстановки: {link}/{ссылка} — ссылка согласования,
|
||||
# {order}/{номер}/{номер_доставки} — номер доставки (010796),
|
||||
# {code}/{код}/{код_выдачи} — код выдачи (7598).
|
||||
# Пустой шаблон → DEFAULT_TEXT_*.
|
||||
|
||||
DEFAULT_TEXT_DELIVERY = "Ваш заказ готов. Согласуйте дату доставки по ссылке: {link}"
|
||||
DEFAULT_TEXT_PICKUP = "Ваш заказ готов. Согласуйте дату самовывоза по ссылке: {link}"
|
||||
DEFAULT_TEXT_PICKUP = (
|
||||
"Ваш заказ готов. Номер доставки {order}, код выдачи {code}. "
|
||||
"Согласуйте дату самовывоза по ссылке: {link}"
|
||||
)
|
||||
DEFAULT_TEXT_NOTIFICATION = "Ваш заказ готов."
|
||||
|
||||
|
||||
|
|
@ -79,6 +85,35 @@ def is_pickup_group(group):
|
|||
return (group.get("delivery_type") or "delivery").strip().lower() == "pickup"
|
||||
|
||||
|
||||
def first_order_digits(value):
|
||||
"""Номер доставки из order_numbers («ТП\\-010722» → «010722»)."""
|
||||
items = value if isinstance(value, (list, tuple)) else [value]
|
||||
for item in items:
|
||||
digits = "".join(ch for ch in str(item or "").replace("\\", "") if ch.isdigit())
|
||||
if digits:
|
||||
return digits
|
||||
return ""
|
||||
|
||||
|
||||
def apply_sms_placeholders(text, group, link):
|
||||
"""Подстановка переменных шаблона в текст SMS."""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
order = first_order_digits(group.get("order_numbers"))
|
||||
code = "".join(ch for ch in str(group.get("pickup_code") or "") if ch.isdigit())
|
||||
replacements = (
|
||||
("{link}", link), ("{ссылка}", link),
|
||||
("{номер_доставки}", order), ("{номер}", order), ("{order}", order),
|
||||
("{код_выдачи}", code), ("{код}", code), ("{code}", code),
|
||||
)
|
||||
|
||||
result = text
|
||||
for token, value in replacements:
|
||||
result = result.replace(token, value)
|
||||
return result.strip()
|
||||
|
||||
|
||||
def build_sms_text(group, settings):
|
||||
"""Текст SMS по типу отгрузки: самовывоз → pickup-шаблон, доставка → обычный.
|
||||
|
||||
|
|
@ -99,13 +134,13 @@ def build_sms_text(group, settings):
|
|||
|
||||
if mode == "notification":
|
||||
if profile_text:
|
||||
return profile_text
|
||||
return apply_sms_placeholders(profile_text, group, link)
|
||||
# уведомление без ссылки: шаблон годится, только если в нём нет {link}
|
||||
if template and "{link}" not in template:
|
||||
return template
|
||||
return DEFAULT_TEXT_NOTIFICATION
|
||||
return apply_sms_placeholders(template, group, link)
|
||||
return apply_sms_placeholders(DEFAULT_TEXT_NOTIFICATION, group, link)
|
||||
|
||||
return template.replace("{link}", link).strip()
|
||||
return apply_sms_placeholders(template, group, link)
|
||||
|
||||
# ─── Логирование ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -227,6 +262,7 @@ def get_groups_to_send(conn, max_attempts):
|
|||
SELECT og.id, og.group_key, og.customer_name, og.customer_phone,
|
||||
og.customer_phone_normalized, og.delivery_link, og.notification_status,
|
||||
og.delivery_type, og.town, og.sms_attempts,
|
||||
og.order_numbers, og.pickup_code,
|
||||
-- Профиль города (fallback на дефолтные значения если нет записи)
|
||||
COALESCE(scp.delivery_mode, 'link') as delivery_mode,
|
||||
COALESCE(scp.delivery_text, '') as delivery_text,
|
||||
|
|
|
|||
|
|
@ -55,18 +55,50 @@ LIMIT_ERROR_CODES = {"131", "132", "230", "231", "232"}
|
|||
# Шаблоны живут в админке (sms_campaign_settings):
|
||||
# sms_text_template — текст для ДОСТАВКИ
|
||||
# sms_text_template_pickup — текст для САМОВЫВОЗА
|
||||
# {link} подставляется ссылкой согласования. Пустой шаблон → DEFAULT_TEXT_*.
|
||||
# Подстановки: {link}/{ссылка} — ссылка согласования,
|
||||
# {order}/{номер}/{номер_доставки} — номер доставки (010796),
|
||||
# {code}/{код}/{код_выдачи} — код выдачи (7598).
|
||||
# Пустой шаблон → DEFAULT_TEXT_*.
|
||||
|
||||
DEFAULT_TEXT_DELIVERY = (
|
||||
"Ваш заказ переведён на платное хранение. Стоимость: 300 ₽/день. "
|
||||
"Заберите заказ или согласуйте доставку: {link}"
|
||||
)
|
||||
DEFAULT_TEXT_PICKUP = (
|
||||
"Ваш заказ переведён на платное хранение. Стоимость: 300 ₽/день. "
|
||||
"Заберите заказ со склада или согласуйте дату самовывоза: {link}"
|
||||
"Заказ переведён на платное хранение. Номер доставки {order}, код выдачи {code}. "
|
||||
"Заберите со склада или согласуйте дату самовывоза: {link}"
|
||||
)
|
||||
|
||||
|
||||
def first_order_digits(value):
|
||||
"""Номер доставки из order_numbers («ТП\\-010722» → «010722»)."""
|
||||
items = value if isinstance(value, (list, tuple)) else [value]
|
||||
for item in items:
|
||||
digits = "".join(ch for ch in str(item or "").replace("\\", "") if ch.isdigit())
|
||||
if digits:
|
||||
return digits
|
||||
return ""
|
||||
|
||||
|
||||
def apply_sms_placeholders(text, group, link):
|
||||
"""Подстановка переменных шаблона в текст SMS."""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
order = first_order_digits(group.get("order_numbers"))
|
||||
code = "".join(ch for ch in str(group.get("pickup_code") or "") if ch.isdigit())
|
||||
replacements = (
|
||||
("{link}", link), ("{ссылка}", link),
|
||||
("{номер_доставки}", order), ("{номер}", order), ("{order}", order),
|
||||
("{код_выдачи}", code), ("{код}", code), ("{code}", code),
|
||||
)
|
||||
|
||||
result = text
|
||||
for token, value in replacements:
|
||||
result = result.replace(token, value)
|
||||
return result.strip()
|
||||
|
||||
|
||||
def build_sms_text(group, settings):
|
||||
"""Текст SMS о платном хранении по типу отгрузки."""
|
||||
link = group.get("delivery_link") or ""
|
||||
|
|
@ -76,7 +108,7 @@ def build_sms_text(group, settings):
|
|||
) or "").strip()
|
||||
if not template:
|
||||
template = DEFAULT_TEXT_PICKUP if pickup else DEFAULT_TEXT_DELIVERY
|
||||
return template.replace("{link}", link).strip()
|
||||
return apply_sms_placeholders(template, group, link)
|
||||
|
||||
# ─── Логирование ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -222,7 +254,8 @@ def get_groups_to_send(conn):
|
|||
cur.execute("""
|
||||
SELECT og.id, og.group_key, og.customer_name, og.customer_phone,
|
||||
og.customer_phone_normalized, og.delivery_link, og.notification_status,
|
||||
og.paid_storage_at, og.delivery_type, og.town
|
||||
og.paid_storage_at, og.delivery_type, og.town,
|
||||
og.order_numbers, og.pickup_code
|
||||
FROM order_groups og
|
||||
LEFT JOIN sms_city_profiles scp ON scp.city = og.town
|
||||
WHERE og.delivery_status = 'paid_storage'
|
||||
|
|
|
|||
|
|
@ -67,10 +67,45 @@ LIMIT_ERROR_CODES = {"131", "132", "230", "231", "232"}
|
|||
# Шаблоны живут в админке (sms_campaign_settings):
|
||||
# sms_text_template — текст для ДОСТАВКИ
|
||||
# sms_text_template_pickup — текст для САМОВЫВОЗА
|
||||
# {link} подставляется ссылкой согласования. Пустой шаблон → DEFAULT_TEXT_*.
|
||||
# Подстановки: {link}/{ссылка} — ссылка согласования,
|
||||
# {order}/{номер}/{номер_доставки} — номер доставки (010796),
|
||||
# {code}/{код}/{код_выдачи} — код выдачи (7598).
|
||||
# Пустой шаблон → DEFAULT_TEXT_*.
|
||||
|
||||
DEFAULT_TEXT_DELIVERY = "Ваш заказ готов к доставке. Выберите дату доставки по ссылке: {link}"
|
||||
DEFAULT_TEXT_PICKUP = "Ваш заказ готов к самовывозу. Выберите дату самовывоза по ссылке: {link}"
|
||||
DEFAULT_TEXT_PICKUP = (
|
||||
"Напоминаем: номер доставки {order}, код выдачи {code}. "
|
||||
"Выберите дату самовывоза по ссылке: {link}"
|
||||
)
|
||||
|
||||
|
||||
def first_order_digits(value):
|
||||
"""Номер доставки из order_numbers («ТП\\-010722» → «010722»)."""
|
||||
items = value if isinstance(value, (list, tuple)) else [value]
|
||||
for item in items:
|
||||
digits = "".join(ch for ch in str(item or "").replace("\\", "") if ch.isdigit())
|
||||
if digits:
|
||||
return digits
|
||||
return ""
|
||||
|
||||
|
||||
def apply_sms_placeholders(text, group, link):
|
||||
"""Подстановка переменных шаблона в текст SMS."""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
order = first_order_digits(group.get("order_numbers"))
|
||||
code = "".join(ch for ch in str(group.get("pickup_code") or "") if ch.isdigit())
|
||||
replacements = (
|
||||
("{link}", link), ("{ссылка}", link),
|
||||
("{номер_доставки}", order), ("{номер}", order), ("{order}", order),
|
||||
("{код_выдачи}", code), ("{код}", code), ("{code}", code),
|
||||
)
|
||||
|
||||
result = text
|
||||
for token, value in replacements:
|
||||
result = result.replace(token, value)
|
||||
return result.strip()
|
||||
|
||||
|
||||
def build_sms_text(group, settings):
|
||||
|
|
@ -82,7 +117,7 @@ def build_sms_text(group, settings):
|
|||
) or "").strip()
|
||||
if not template:
|
||||
template = DEFAULT_TEXT_PICKUP if pickup else DEFAULT_TEXT_DELIVERY
|
||||
return template.replace("{link}", link).strip()
|
||||
return apply_sms_placeholders(template, group, link)
|
||||
|
||||
# ─── Логирование ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -237,7 +272,7 @@ def get_groups_to_send(conn, max_attempts):
|
|||
cur.execute("""
|
||||
SELECT og.id, og.group_key, og.customer_name, og.customer_phone,
|
||||
og.customer_phone_normalized, og.delivery_link, og.notification_status,
|
||||
og.delivery_type, og.town
|
||||
og.delivery_type, og.town, og.order_numbers, og.pickup_code
|
||||
FROM order_groups og
|
||||
LEFT JOIN sms_city_profiles scp ON scp.city = og.town
|
||||
WHERE og.delivery_status = 'pending_confirmation'
|
||||
|
|
|
|||
|
|
@ -24,13 +24,23 @@ const HAS_SETTINGS = ["first_sms", "second_sms", "manual", "paid_storage"];
|
|||
|
||||
// ── SMS text templates (доставка / самовывоз) ────────────────────────────────
|
||||
const SMS_LINK_PREVIEW = "https://dost.supersamsev.ru/d/XXXXXX";
|
||||
// Образцы значений для предпросмотра (как будет выглядеть SMS у клиента)
|
||||
const SMS_PREVIEW_VALUES = [
|
||||
["{номер_доставки}", "010796"], ["{номер}", "010796"], ["{order}", "010796"],
|
||||
["{код_выдачи}", "7598"], ["{код}", "7598"], ["{code}", "7598"],
|
||||
["{ссылка}", SMS_LINK_PREVIEW], ["{link}", SMS_LINK_PREVIEW],
|
||||
];
|
||||
|
||||
const renderSmsPreview = (template, emptyLabel) => {
|
||||
const text = (template || "").trim();
|
||||
if (!text) return emptyLabel;
|
||||
return text.replace("{link}", SMS_LINK_PREVIEW);
|
||||
const smsPreviewText = (template) => {
|
||||
let text = (template || "").trim();
|
||||
SMS_PREVIEW_VALUES.forEach(([token, value]) => {
|
||||
text = text.split(token).join(value);
|
||||
});
|
||||
return text;
|
||||
};
|
||||
|
||||
const renderSmsPreview = (template, emptyLabel) => smsPreviewText(template) || emptyLabel;
|
||||
|
||||
// ── Days of week ─────────────────────────────────────────────────────────────
|
||||
const DAYS = [
|
||||
{ num: 1, short: "Пн" },
|
||||
|
|
@ -512,7 +522,7 @@ export const SmsCampaignPanel = () => {
|
|||
placeholder="Ваш заказ готов. Согласуйте дату доставки по ссылке: {link}"
|
||||
/>
|
||||
<div className="mt-1 text-[10px] text-[var(--color-text-muted)]">
|
||||
Текущая длина: {(settings.sms_text_template || "").replace("{link}", SMS_LINK_PREVIEW).length} символов
|
||||
Текущая длина: {smsPreviewText(settings.sms_text_template).length} символов
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -525,12 +535,15 @@ export const SmsCampaignPanel = () => {
|
|||
placeholder="Ваш заказ готов. Согласуйте дату самовывоза по ссылке: {link}"
|
||||
/>
|
||||
<div className="mt-1 text-[10px] text-[var(--color-text-muted)]">
|
||||
Текущая длина: {(settings.sms_text_template_pickup || "").replace("{link}", SMS_LINK_PREVIEW).length} символов
|
||||
Текущая длина: {smsPreviewText(settings.sms_text_template_pickup).length} символов
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 rounded-lg bg-[var(--color-surface-strong)] px-3 py-2 text-[10px] text-[var(--color-text-muted)]">
|
||||
Текст выбирается по типу отгрузки: самовывоз → правый шаблон, доставка → левый. {"{link}"} заменяется ссылкой согласования.
|
||||
Текст выбирается по типу отгрузки: самовывоз → правый шаблон, доставка → левый.
|
||||
<div className="mt-1">
|
||||
Переменные: {"{link}"} — ссылка согласования, {"{order}"} — номер доставки (можно {"{номер}"}), {"{code}"} — код выдачи (можно {"{код}"}).
|
||||
</div>
|
||||
<div className="mt-1">📦 Доставка: {renderSmsPreview(settings.sms_text_template, "— пусто, уйдёт стандартный текст кампании")}</div>
|
||||
<div>🏪 Самовывоз: {renderSmsPreview(settings.sms_text_template_pickup, "— пусто, уйдёт стандартный текст кампании")}</div>
|
||||
<div className="mt-1">Если у города в «SMS по городам» задан свой текст в режиме «Уведомление» — уходит он, и тогда шаблон применяется только если в нём нет {"{link}"}.</div>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ describe("DeliveryChoiceFlow", () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain("Счет CD-240032");
|
||||
expect(markup).toContain("Доставка CD-240032");
|
||||
expect(markup).not.toContain("Александр Савин");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,20 +20,42 @@ const getOrderItemNames = (invitation) => {
|
|||
.filter(Boolean);
|
||||
};
|
||||
|
||||
// Обмен с 1С экранирует дефис («ТП\-010796») — клиенту показываем «ТП-010796».
|
||||
const unescapeOncDashes = (value) => String(value || "").replace(/\\/g, "");
|
||||
|
||||
export const getInvitationReferenceLabel = (invitation) => {
|
||||
const invoiceNumbers = [...new Set(getOrderItemNames(invitation))];
|
||||
const invoiceNumbers = [...new Set(getOrderItemNames(invitation))].map(unescapeOncDashes);
|
||||
if (invoiceNumbers.length === 1) {
|
||||
return `Счет ${invoiceNumbers[0]}`;
|
||||
return `Доставка ${invoiceNumbers[0]}`;
|
||||
}
|
||||
|
||||
if (invoiceNumbers.length > 1) {
|
||||
return `Счета: ${invoiceNumbers.join(", ")}`;
|
||||
return `Доставки: ${invoiceNumbers.join(", ")}`;
|
||||
}
|
||||
|
||||
const orderNumber = typeof invitation?.orderNumber === "string" ? invitation.orderNumber.trim() : "";
|
||||
const orderNumber = typeof invitation?.orderNumber === "string"
|
||||
? unescapeOncDashes(invitation.orderNumber.trim())
|
||||
: "";
|
||||
if (orderNumber) {
|
||||
return `Счет ${orderNumber}`;
|
||||
return `Доставка ${orderNumber}`;
|
||||
}
|
||||
|
||||
return "Счет —";
|
||||
return "Доставка —";
|
||||
};
|
||||
|
||||
/**
|
||||
* Номер доставки для клиента: «ТП\-010796» → «010796».
|
||||
* Сначала берём номер заказа из состава, затем orderNumber/orderNumbers приглашения.
|
||||
*/
|
||||
export const getDeliveryNumberDigits = (invitation) => {
|
||||
const fromItems = getOrderItemNames(invitation)[0] || "";
|
||||
const fromOrderNumbers = Array.isArray(invitation?.orderNumbers)
|
||||
? invitation.orderNumbers[0] || ""
|
||||
: "";
|
||||
const fromOrderNumber = typeof invitation?.orderNumber === "string"
|
||||
? invitation.orderNumber
|
||||
: "";
|
||||
const source = fromItems || fromOrderNumbers || fromOrderNumber;
|
||||
|
||||
return unescapeOncDashes(source).replace(/[^0-9]/g, "");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { DeliveryChoiceFlow } from "../components/client/DeliveryChoiceFlow";
|
|||
import { DeliverySlotsPicker } from "../components/client/DeliverySlotsPicker";
|
||||
import { PickupSlotsPicker } from "../components/client/PickupSlotsPicker";
|
||||
import { OrderCompositionPanel } from "../components/client/OrderCompositionPanel";
|
||||
import { getInvitationReferenceLabel } from "../components/client/invitationReference";
|
||||
import { getDeliveryNumberDigits, getInvitationReferenceLabel } from "../components/client/invitationReference";
|
||||
import { DeliveryStateNotice } from "../components/client/DeliveryStateNotice";
|
||||
import { PushSubscriptionBanner } from "../components/client/PushSubscriptionBanner";
|
||||
import { PwaInstallCard } from "../components/client/PwaInstallCard";
|
||||
|
|
@ -299,13 +299,25 @@ export const buildSelectedSlotFromInvitation = (invitation, slots = []) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const getClientDeliveryHeroDescription = (isActiveState, isChoiceSaved) => {
|
||||
export const getClientDeliveryHeroDescription = (
|
||||
isActiveState,
|
||||
isChoiceSaved,
|
||||
deliveryType = "delivery",
|
||||
) => {
|
||||
if (isChoiceSaved) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return isActiveState
|
||||
? "Вам предложены варианты доставки. Выберите удобную дату и время."
|
||||
const isPickup = deliveryType === "pickup";
|
||||
|
||||
if (isActiveState) {
|
||||
return isPickup
|
||||
? "Вам предложены варианты самовывоза. Выберите удобную дату и время."
|
||||
: "Вам предложены варианты доставки. Выберите удобную дату и время.";
|
||||
}
|
||||
|
||||
return isPickup
|
||||
? "По этому заказу согласование самовывоза завершено или передано логисту."
|
||||
: "По этому заказу согласование доставки завершено или передано логисту.";
|
||||
};
|
||||
|
||||
|
|
@ -399,6 +411,10 @@ export const ClientDeliveryPage = () => {
|
|||
|
||||
const invitationState = invitation?.state || "awaiting_choice";
|
||||
const isActiveState = ["awaiting_choice", "opened", "reminder_sent"].includes(invitationState);
|
||||
// Тип отгрузки приходит из приглашения: для самовывоза страница говорит «самовывоз».
|
||||
const orderIsPickup = invitation?.deliveryType === "pickup";
|
||||
const deliveryNumberDigits = getDeliveryNumberDigits(invitation);
|
||||
const orderTypeEyebrow = orderIsPickup ? "Самовывоз заказа" : "Доставка заказа";
|
||||
|
||||
const invitationSelectedSlot = isActiveState ? null : buildSelectedSlotFromInvitation(invitation, slots);
|
||||
|
||||
|
|
@ -407,7 +423,11 @@ export const ClientDeliveryPage = () => {
|
|||
const savedChoiceLabel = effectiveSelectedSlot
|
||||
? `${formatDeliveryDate(effectiveSelectedSlot.date)} / ${effectiveSelectedSlot.time}`
|
||||
: "";
|
||||
const heroDescription = getClientDeliveryHeroDescription(isActiveState, isChoiceSaved);
|
||||
const heroDescription = getClientDeliveryHeroDescription(
|
||||
isActiveState,
|
||||
isChoiceSaved,
|
||||
invitation?.deliveryType,
|
||||
);
|
||||
|
||||
const handleSaveChoice = async () => {
|
||||
if (!token) {
|
||||
|
|
@ -558,8 +578,10 @@ export const ClientDeliveryPage = () => {
|
|||
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4">
|
||||
{!isChoiceSaved ? (
|
||||
<Panel className="space-y-3 p-5 sm:p-6">
|
||||
<p className="text-sm uppercase tracking-[0.24em] text-[var(--color-text-muted)]">Доставка заказа</p>
|
||||
<h1 className="text-2xl font-semibold leading-tight sm:text-3xl">Согласование доставки</h1>
|
||||
<p className="text-sm uppercase tracking-[0.24em] text-[var(--color-text-muted)]">{orderTypeEyebrow}</p>
|
||||
<h1 className="text-2xl font-semibold leading-tight sm:text-3xl">
|
||||
{orderIsPickup ? "Согласование самовывоза" : "Согласование доставки"}
|
||||
</h1>
|
||||
{heroDescription ? (
|
||||
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
|
||||
{heroDescription}
|
||||
|
|
@ -591,7 +613,10 @@ export const ClientDeliveryPage = () => {
|
|||
</p>
|
||||
{(invitation?.pickupCode || invitation?.pickup_code) ? (
|
||||
<div className="mt-3 rounded-2xl border-2 border-[var(--color-accent)] bg-[var(--color-accent-soft)] p-4 text-center">
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-[var(--color-text-muted)]">Код выдачи</p>
|
||||
{deliveryNumberDigits ? (
|
||||
<p className="text-sm font-semibold text-[var(--color-text)]">Ваш номер доставки {deliveryNumberDigits}</p>
|
||||
) : null}
|
||||
<p className="mt-2 text-xs uppercase tracking-[0.2em] text-[var(--color-text-muted)]">Ваш код выдачи</p>
|
||||
<p className="mt-1 text-3xl font-bold tracking-[0.4em] text-[var(--color-accent)]">{invitation.pickupCode || invitation.pickup_code}</p>
|
||||
<p className="mt-2 text-xs text-[var(--color-text-muted)]">Покажите этот код при получении заказа</p>
|
||||
</div>
|
||||
|
|
@ -694,7 +719,10 @@ export const ClientDeliveryPage = () => {
|
|||
|
||||
{activeTab === TAB_PICKUP && (invitation?.pickupCode || invitation?.pickup_code) ? (
|
||||
<div className="rounded-2xl border-2 border-[var(--color-accent)] bg-[var(--color-accent-soft)] p-4 text-center">
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-[var(--color-text-muted)]">Код выдачи</p>
|
||||
{deliveryNumberDigits ? (
|
||||
<p className="text-sm font-semibold text-[var(--color-text)]">Ваш номер доставки {deliveryNumberDigits}</p>
|
||||
) : null}
|
||||
<p className="mt-2 text-xs uppercase tracking-[0.2em] text-[var(--color-text-muted)]">Ваш код выдачи</p>
|
||||
<p className="mt-1 text-3xl font-bold tracking-[0.4em] text-[var(--color-accent)]">{invitation.pickupCode || invitation.pickup_code}</p>
|
||||
<p className="mt-2 text-xs text-[var(--color-text-muted)]">Покажите этот код при получении заказа</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { getInvitationReferenceLabel } from "../components/client/invitationReference";
|
||||
import { getDeliveryNumberDigits, getInvitationReferenceLabel } from "../components/client/invitationReference";
|
||||
import {
|
||||
buildDeliveryConfirmationPayload,
|
||||
buildSelectedSlotFromInvitation,
|
||||
|
|
@ -176,7 +176,16 @@ describe("ClientDeliveryPage helpers", () => {
|
|||
{ name: "СФ Т\\ЕА-28700", quantity: "" },
|
||||
],
|
||||
}),
|
||||
).toBe("Счета: СФ Т\\ЕА-28687, СФ Т\\ЕА-28700");
|
||||
).toBe("Доставки: СФ ТЕА-28687, СФ ТЕА-28700");
|
||||
});
|
||||
|
||||
it("берёт номер доставки из состава заказа: «ТП\\-010796» → «010796»", () => {
|
||||
expect(
|
||||
getDeliveryNumberDigits({ orderItems: [{ name: "ТП\\-010796", quantity: "" }] }),
|
||||
).toBe("010796");
|
||||
expect(getDeliveryNumberDigits({ orderNumbers: ["ТП\\-010557", "ТП\\-010599"] })).toBe("010557");
|
||||
expect(getDeliveryNumberDigits({ orderNumber: "9788324113|28.09.2026" })).toBe("978832411328092026");
|
||||
expect(getDeliveryNumberDigits({})).toBe("");
|
||||
});
|
||||
|
||||
it("hides the hero helper text after the client saves the choice", () => {
|
||||
|
|
@ -189,6 +198,16 @@ describe("ClientDeliveryPage helpers", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("говорит про самовывоз, если отгрузка самовывозом", () => {
|
||||
expect(getClientDeliveryHeroDescription(true, false, "pickup")).toBe(
|
||||
"Вам предложены варианты самовывоза. Выберите удобную дату и время.",
|
||||
);
|
||||
expect(getClientDeliveryHeroDescription(false, false, "pickup")).toBe(
|
||||
"По этому заказу согласование самовывоза завершено или передано логисту.",
|
||||
);
|
||||
expect(getClientDeliveryHeroDescription(true, true, "pickup")).toBe("");
|
||||
});
|
||||
|
||||
it("skips Sunday when generating fallback delivery slots from Friday", () => {
|
||||
// 2026-04-17 is Friday → next workday = Saturday 18, then Monday 20 (skip Sunday 19)
|
||||
const slots = groupSlotsFromInvitation(
|
||||
|
|
|
|||
|
|
@ -242,6 +242,8 @@ export type OrderGroupInvitationSource = {
|
|||
delivery_status?: string | null;
|
||||
delivery_link?: string | null;
|
||||
source_orders?: unknown[] | null;
|
||||
delivery_type?: string | null;
|
||||
pickup_code?: string | null;
|
||||
};
|
||||
export const isInvitationExpired = (invitation: DeliveryInvitationRecord, now = new Date()) => {
|
||||
if (invitation.revoked_at) {
|
||||
|
|
@ -325,6 +327,11 @@ export const buildPublicOrderGroupInvitationView = (
|
|||
customerName: maskCustomerName(customerName),
|
||||
customerPhone: maskPhoneNumber(customerPhone),
|
||||
orderItems,
|
||||
// Тип отгрузки и код выдачи — клиентская страница показывает «самовывоз»
|
||||
// и номер доставки с кодом выдачи по этим полям.
|
||||
deliveryType: group.delivery_type || null,
|
||||
pickupCode: group.pickup_code || null,
|
||||
orderNumbers,
|
||||
availableSlots: invitation.available_slots || [],
|
||||
deliveryDate: invitation.delivery_date || null,
|
||||
deliveryTime: invitation.delivery_time || null,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
-- Код выдачи и номер доставки в SMS для самовывоза
|
||||
-- 2026-09-28
|
||||
--
|
||||
-- Переменные шаблона (sms_text_template / sms_text_template_pickup):
|
||||
-- {link} / {ссылка} — ссылка согласования
|
||||
-- {order} / {номер} — номер доставки без префикса (010796)
|
||||
-- {code} / {код} — код выдачи (7598)
|
||||
-- Подстановку делает скрипт кампании (apply_sms_placeholders).
|
||||
|
||||
UPDATE sms_campaign_settings SET
|
||||
sms_text_template_pickup = 'Ваш заказ готов. Номер доставки {order}, код выдачи {code}. Согласуйте дату самовывоза по ссылке: {link}'
|
||||
WHERE campaign_type = 'first_sms';
|
||||
|
||||
UPDATE sms_campaign_settings SET
|
||||
sms_text_template_pickup = 'Напоминаем: номер доставки {order}, код выдачи {code}. Выберите дату самовывоза по ссылке: {link}'
|
||||
WHERE campaign_type = 'second_sms';
|
||||
|
||||
UPDATE sms_campaign_settings SET
|
||||
sms_text_template_pickup = 'Заказ переведён на платное хранение. Номер доставки {order}, код выдачи {code}. Заберите со склада или согласуйте дату самовывоза: {link}'
|
||||
WHERE campaign_type = 'paid_storage';
|
||||
|
||||
-- Текст самовывоза для городов в режиме «Уведомление» (сейчас Ялта):
|
||||
-- переменные работают и в профиле города.
|
||||
UPDATE sms_city_profiles SET
|
||||
pickup_text = 'Ваш заказ готов. Номер доставки {order}, код выдачи {code}. Заберите со склада.',
|
||||
updated_at = NOW()
|
||||
WHERE pickup_text IS NOT NULL
|
||||
AND pickup_text <> ''
|
||||
AND pickup_text NOT LIKE '%{code}%';
|
||||
|
||||
-- Проверка:
|
||||
-- SELECT campaign_type, sms_text_template_pickup FROM sms_campaign_settings ORDER BY 1;
|
||||
-- SELECT city, pickup_text FROM sms_city_profiles;
|
||||
Loading…
Reference in New Issue