feat: show client order items in delivery flow
This commit is contained in:
parent
ca72a4e662
commit
c28c826601
|
|
@ -18,6 +18,36 @@ const STATE_LABELS = {
|
|||
|
||||
const DEFAULT_SLOTS = ["Первая половина дня", "Вторая половина дня"];
|
||||
|
||||
const splitOrderItem = (item) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof item === "string") {
|
||||
const [name, quantity] = item.split("|").map((part) => part.trim());
|
||||
return {
|
||||
name: name || item.trim(),
|
||||
quantity: quantity || "",
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof item === "object") {
|
||||
const name = typeof item.name === "string" ? item.name.trim() : typeof item.label === "string" ? item.label.trim() : "";
|
||||
const quantity = typeof item.quantity === "string"
|
||||
? item.quantity.trim()
|
||||
: typeof item.quantity === "number"
|
||||
? String(item.quantity)
|
||||
: "";
|
||||
|
||||
return {
|
||||
name: name || "Позиция",
|
||||
quantity,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const DeliveryChoiceFlow = ({
|
||||
invitation = {},
|
||||
onConfirmChoice = () => {},
|
||||
|
|
@ -28,6 +58,9 @@ export const DeliveryChoiceFlow = ({
|
|||
const slots = invitation.availableSlots?.length ? invitation.availableSlots : DEFAULT_SLOTS;
|
||||
const orderNumber = invitation.orderNumber || "—";
|
||||
const customerName = invitation.customerName || "Клиент";
|
||||
const orderItems = (invitation.orderItems || invitation.items || [])
|
||||
.map(splitOrderItem)
|
||||
.filter(Boolean);
|
||||
|
||||
if (!isActive) {
|
||||
return <DeliveryStateNotice state={state} />;
|
||||
|
|
@ -36,16 +69,33 @@ export const DeliveryChoiceFlow = ({
|
|||
return (
|
||||
<Panel className="space-y-5 p-5 sm:p-6">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm uppercase tracking-[0.24em] text-[var(--color-text-muted)]">Клиентская ссылка</p>
|
||||
<p className="text-sm uppercase tracking-[0.24em] text-[var(--color-text-muted)]">Согласование доставки</p>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="text-2xl font-semibold leading-tight sm:text-3xl">Выберите время доставки</h1>
|
||||
<Badge tone="warning">{STATE_LABELS[state]}</Badge>
|
||||
</div>
|
||||
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
|
||||
Заказ {orderNumber} для {customerName}. Выберите подходящую половину дня и подтвердите выбор.
|
||||
Заказ {orderNumber} для {customerName}. Проверьте состав заказа и выберите удобную половину дня.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{orderItems.length ? (
|
||||
<div className="space-y-3 rounded-[22px] border border-[var(--color-border)] bg-[var(--color-surface)] p-4">
|
||||
<p className="text-sm uppercase tracking-[0.18em] text-[var(--color-text-muted)]">Состав заказа</p>
|
||||
<div className="space-y-2">
|
||||
{orderItems.map((item) => (
|
||||
<div
|
||||
key={`${item.name}-${item.quantity || "item"}`}
|
||||
className="flex items-center justify-between gap-3 rounded-[18px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] px-4 py-3 text-sm"
|
||||
>
|
||||
<span className="leading-6">{item.name}</span>
|
||||
{item.quantity ? <Badge tone="neutral">{item.quantity}</Badge> : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{slots.map((slot) => (
|
||||
<Button key={slot} className="w-full" onClick={() => onConfirmChoice(slot)}>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,33 @@ describe("DeliveryChoiceFlow", () => {
|
|||
expect(markup).toContain("Ожидает ответа клиента");
|
||||
});
|
||||
|
||||
it("renders order items with quantities when they are provided", () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<DeliveryChoiceFlow
|
||||
invitation={{
|
||||
state: "awaiting_choice",
|
||||
orderNumber: "CD-240031",
|
||||
customerName: "Мария Волкова",
|
||||
orderItems: [
|
||||
{ name: "Кухонный гарнитур", quantity: "1 комплект" },
|
||||
{ name: "Фурнитура Blum", quantity: "12 шт" },
|
||||
{ name: "Монтажный комплект" },
|
||||
],
|
||||
availableSlots: ["Первая половина дня", "Вторая половина дня"],
|
||||
}}
|
||||
onConfirmChoice={() => {}}
|
||||
onRequestNewLink={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain("Состав заказа");
|
||||
expect(markup).toContain("Кухонный гарнитур");
|
||||
expect(markup).toContain("1 комплект");
|
||||
expect(markup).toContain("Фурнитура Blum");
|
||||
expect(markup).toContain("12 шт");
|
||||
expect(markup).toContain("Монтажный комплект");
|
||||
});
|
||||
|
||||
it("renders a logistics notice when the order is transferred", () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<DeliveryChoiceFlow
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ export const DeliveryStateNotice = ({ state = "default" }) => {
|
|||
|
||||
return (
|
||||
<Panel className="space-y-3 p-5 sm:p-6">
|
||||
<p className="text-sm uppercase tracking-[0.24em] text-[var(--color-text-muted)]">Статус ссылки</p>
|
||||
<p className="text-sm uppercase tracking-[0.24em] text-[var(--color-text-muted)]">Статус заказа</p>
|
||||
<h2 className="text-2xl font-semibold leading-tight">{copy.title}</h2>
|
||||
<p className="text-sm leading-6 text-[var(--color-text-muted)]">{copy.description}</p>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -159,9 +159,9 @@ export const ClientDeliveryPage = () => {
|
|||
<main className="min-h-screen bg-[var(--color-bg)] px-3 py-4 sm:px-6 sm:py-8">
|
||||
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4">
|
||||
<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 leading-6 text-[var(--color-text-muted)]">Подтягиваем актуальный статус заказа.</p>
|
||||
<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 leading-6 text-[var(--color-text-muted)]">Подтягиваем актуальные данные по заказу.</p>
|
||||
</Panel>
|
||||
</div>
|
||||
</main>
|
||||
|
|
@ -173,8 +173,8 @@ export const ClientDeliveryPage = () => {
|
|||
<main className="min-h-screen bg-[var(--color-bg)] px-3 py-4 sm:px-6 sm:py-8">
|
||||
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4">
|
||||
<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)]">Доставка заказа</p>
|
||||
<h1 className="text-2xl font-semibold leading-tight sm:text-3xl">Не удалось открыть страницу</h1>
|
||||
<p className="text-sm leading-6 text-[var(--color-text-muted)]">{error}</p>
|
||||
</Panel>
|
||||
</div>
|
||||
|
|
@ -188,7 +188,7 @@ export const ClientDeliveryPage = () => {
|
|||
<main className="min-h-screen bg-[var(--color-bg)] px-3 py-4 sm:px-6 sm:py-8">
|
||||
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4">
|
||||
<Panel className="space-y-3 p-5 sm:p-6">
|
||||
<p className="text-sm uppercase tracking-[0.24em] text-[var(--color-text-muted)]">Публичная ссылка</p>
|
||||
<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 leading-6 text-[var(--color-text-muted)]">
|
||||
{isActiveState
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ export const buildShowcaseInvitation = (token = SHOWCASE_TOKEN, now = new Date()
|
|||
state: "awaiting_choice",
|
||||
deliveryDate: firstDay,
|
||||
deliveryTime: "До обеда",
|
||||
orderItems: [
|
||||
{ name: "Кухонный гарнитур", quantity: "1 комплект" },
|
||||
{ name: "Фурнитура Blum", quantity: "12 шт" },
|
||||
{ name: "Монтажный комплект", quantity: "1 набор" },
|
||||
],
|
||||
availableSlots: [
|
||||
`${firstDay}, До обеда`,
|
||||
`${firstDay}, После обеда`,
|
||||
|
|
|
|||
|
|
@ -73,6 +73,16 @@ describe("deliveryInvitationApi", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("includes readable order items in the showcase invitation", () => {
|
||||
const invitation = buildShowcaseInvitation("showcase", new Date("2026-04-14T09:00:00Z"));
|
||||
|
||||
expect(invitation.orderItems).toEqual([
|
||||
{ name: "Кухонный гарнитур", quantity: "1 комплект" },
|
||||
{ name: "Фурнитура Blum", quantity: "12 шт" },
|
||||
{ name: "Монтажный комплект", quantity: "1 набор" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("confirms a delivery choice with the chosen slot", async () => {
|
||||
invoke.mockResolvedValueOnce({
|
||||
data: {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,50 @@ import {
|
|||
} from "../_shared/delivery-invitations.ts";
|
||||
import { createServiceClient } from "../_shared/chatbot.ts";
|
||||
|
||||
const normalizeOrderItems = (
|
||||
items: unknown,
|
||||
): Array<{ name: string; quantity?: string }> => {
|
||||
if (!Array.isArray(items)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return items
|
||||
.map((item) => {
|
||||
if (typeof item === "string") {
|
||||
const [namePart, quantityPart] = item.split("|").map((part) => part.trim());
|
||||
const name = namePart || item.trim();
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return quantityPart ? { name, quantity: quantityPart } : { name };
|
||||
}
|
||||
|
||||
if (item && typeof item === "object") {
|
||||
const typedItem = item as { name?: unknown; quantity?: unknown; label?: unknown };
|
||||
const name = typeof typedItem.name === "string"
|
||||
? typedItem.name.trim()
|
||||
: typeof typedItem.label === "string"
|
||||
? typedItem.label.trim()
|
||||
: "";
|
||||
const quantity = typeof typedItem.quantity === "string"
|
||||
? typedItem.quantity.trim()
|
||||
: typeof typedItem.quantity === "number"
|
||||
? String(typedItem.quantity)
|
||||
: "";
|
||||
|
||||
if (!name && !quantity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return quantity ? { name: name || "Позиция", quantity } : { name: name || "Позиция" };
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((item): item is { name: string; quantity?: string } => Boolean(item));
|
||||
};
|
||||
|
||||
const corsHeaders = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
|
||||
|
|
@ -97,6 +141,7 @@ Deno.serve(async (request) => {
|
|||
orderNumber: order.order_number,
|
||||
customerName: order.customer?.name || invitation.customer_name || null,
|
||||
customerPhone: order.customer?.phone || invitation.customer_phone || null,
|
||||
orderItems: normalizeOrderItems(order.customer?.items),
|
||||
availableSlots: invitation.available_slots || [],
|
||||
deliveryDate: invitation.delivery_date || null,
|
||||
deliveryTime: invitation.delivery_time || null,
|
||||
|
|
|
|||
Loading…
Reference in New Issue