feat: show full order composition on delivery page
- Extract OrderCompositionPanel as standalone component - Support nested order items with sub-positions from source_orders - Enrich order items from Edge Function when RPC only has invoice refs - Update RPC SQL to extract product items from source_orders - Update Edge Function to include source_orders items in invitation - Add detail rows (account, customer) when no order items available
This commit is contained in:
parent
5a5636c738
commit
e6dc1972fb
|
|
@ -84,17 +84,49 @@ begin
|
||||||
nullif(v_group.customer_phone_normalized, ''),
|
nullif(v_group.customer_phone_normalized, ''),
|
||||||
nullif(v_invitation.customer_phone, '')
|
nullif(v_invitation.customer_phone, '')
|
||||||
);
|
);
|
||||||
select coalesce(
|
-- Build orderItems from source_orders if available (real product lines),
|
||||||
jsonb_agg(jsonb_build_object('name', order_number, 'quantity', '')),
|
-- otherwise fall back to invoice numbers from order_numbers.
|
||||||
'[]'::jsonb
|
v_order_items := case
|
||||||
)
|
when v_group.source_orders is not null
|
||||||
into v_order_items
|
and jsonb_typeof(v_group.source_orders) = 'array'
|
||||||
from jsonb_array_elements_text(
|
and jsonb_array_length(v_group.source_orders) > 0
|
||||||
case
|
then
|
||||||
when jsonb_typeof(to_jsonb(v_group.order_numbers)) = 'array' then to_jsonb(v_group.order_numbers)
|
coalesce(
|
||||||
else '[]'::jsonb
|
(select jsonb_agg(
|
||||||
end
|
jsonb_build_object(
|
||||||
) as order_number;
|
'name', coalesce(src ->> 'nom', src ->> 'name', onum),
|
||||||
|
'quantity', '',
|
||||||
|
'items', coalesce(
|
||||||
|
src -> 'orderList',
|
||||||
|
src -> 'items',
|
||||||
|
'[]'::jsonb
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
from jsonb_array_elements(v_group.source_orders) with ordinality as t(src, idx)
|
||||||
|
cross join lateral (
|
||||||
|
select coalesce(
|
||||||
|
nullif(v_group.order_numbers[idx], ''),
|
||||||
|
src ->> 'nom',
|
||||||
|
src ->> 'name',
|
||||||
|
''
|
||||||
|
) as onum
|
||||||
|
) as names
|
||||||
|
),
|
||||||
|
'[]'::jsonb
|
||||||
|
)
|
||||||
|
else
|
||||||
|
coalesce(
|
||||||
|
(select jsonb_agg(jsonb_build_object('name', order_number, 'quantity', ''))
|
||||||
|
from jsonb_array_elements_text(
|
||||||
|
case
|
||||||
|
when jsonb_typeof(to_jsonb(v_group.order_numbers)) = 'array' then to_jsonb(v_group.order_numbers)
|
||||||
|
else '[]'::jsonb
|
||||||
|
end
|
||||||
|
) as order_number),
|
||||||
|
'[]'::jsonb
|
||||||
|
)
|
||||||
|
end;
|
||||||
|
|
||||||
return jsonb_build_object(
|
return jsonb_build_object(
|
||||||
'ok', true,
|
'ok', true,
|
||||||
|
|
|
||||||
|
|
@ -18,36 +18,6 @@ const STATE_LABELS = {
|
||||||
agreed: "Доставка согласована",
|
agreed: "Доставка согласована",
|
||||||
};
|
};
|
||||||
|
|
||||||
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 = ({
|
export const DeliveryChoiceFlow = ({
|
||||||
invitation = {},
|
invitation = {},
|
||||||
selectedSlot = null,
|
selectedSlot = null,
|
||||||
|
|
@ -56,9 +26,6 @@ export const DeliveryChoiceFlow = ({
|
||||||
const state = invitation.state || "awaiting_choice";
|
const state = invitation.state || "awaiting_choice";
|
||||||
const isActive = ACTIVE_STATES.has(state);
|
const isActive = ACTIVE_STATES.has(state);
|
||||||
const invitationReference = getInvitationReferenceLabel(invitation);
|
const invitationReference = getInvitationReferenceLabel(invitation);
|
||||||
const orderItems = (invitation.orderItems || invitation.items || [])
|
|
||||||
.map(splitOrderItem)
|
|
||||||
.filter(Boolean);
|
|
||||||
const slotSummary = selectedSlot ? formatDeliverySlotLabel(selectedSlot) : "";
|
const slotSummary = selectedSlot ? formatDeliverySlotLabel(selectedSlot) : "";
|
||||||
|
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
|
|
@ -78,27 +45,10 @@ export const DeliveryChoiceFlow = ({
|
||||||
<Badge tone="warning">{STATE_LABELS[state]}</Badge>
|
<Badge tone="warning">{STATE_LABELS[state]}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
|
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
|
||||||
{invitationReference}. Проверьте состав заказа и выберите удобную половину дня.
|
{invitationReference}. Выберите удобную половину дня.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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="flex flex-col gap-3 sm:flex-row">
|
<div className="flex flex-col gap-3 sm:flex-row">
|
||||||
<Button
|
<Button
|
||||||
className="w-full sm:w-auto"
|
className="w-full sm:w-auto"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React from "react";
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { DeliveryChoiceFlow } from "./DeliveryChoiceFlow";
|
import { DeliveryChoiceFlow } from "./DeliveryChoiceFlow";
|
||||||
|
import { OrderCompositionPanel } from "./OrderCompositionPanel";
|
||||||
|
|
||||||
describe("DeliveryChoiceFlow", () => {
|
describe("DeliveryChoiceFlow", () => {
|
||||||
it("renders the active delivery choice with half-day actions", () => {
|
it("renders the active delivery choice with half-day actions", () => {
|
||||||
|
|
@ -38,32 +39,6 @@ describe("DeliveryChoiceFlow", () => {
|
||||||
expect(markup).toContain("disabled");
|
expect(markup).toContain("disabled");
|
||||||
});
|
});
|
||||||
|
|
||||||
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={() => {}}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
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", () => {
|
it("renders a logistics notice when the order is transferred", () => {
|
||||||
const markup = renderToStaticMarkup(
|
const markup = renderToStaticMarkup(
|
||||||
<DeliveryChoiceFlow
|
<DeliveryChoiceFlow
|
||||||
|
|
@ -113,3 +88,59 @@ describe("DeliveryChoiceFlow", () => {
|
||||||
expect(markup).not.toContain("Александр Савин");
|
expect(markup).not.toContain("Александр Савин");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("OrderCompositionPanel", () => {
|
||||||
|
it("renders order items with quantities when they are provided", () => {
|
||||||
|
const markup = renderToStaticMarkup(
|
||||||
|
<OrderCompositionPanel
|
||||||
|
invitation={{
|
||||||
|
orderNumber: "CD-240031",
|
||||||
|
orderItems: [
|
||||||
|
{ name: "Кухонный гарнитур", quantity: "1 комплект" },
|
||||||
|
{ name: "Фурнитура Blum", quantity: "12 шт" },
|
||||||
|
{ name: "Монтажный комплект" },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(markup).toContain("Состав заказа");
|
||||||
|
expect(markup).toContain("Кухонный гарнитур");
|
||||||
|
expect(markup).toContain("1 комплект");
|
||||||
|
expect(markup).toContain("Фурнитура Blum");
|
||||||
|
expect(markup).toContain("12 шт");
|
||||||
|
expect(markup).toContain("Монтажный комплект");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders reference and customer info when there are no order items", () => {
|
||||||
|
const markup = renderToStaticMarkup(
|
||||||
|
<OrderCompositionPanel invitation={{ orderNumber: "CD-240031", customerName: "Мария Волкова" }} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(markup).toContain("Состав заказа");
|
||||||
|
expect(markup).toContain("CD-240031");
|
||||||
|
expect(markup).toContain("Мария Волкова");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders nothing when there are no order items and no reference info", () => {
|
||||||
|
const markup = renderToStaticMarkup(
|
||||||
|
<OrderCompositionPanel invitation={{}} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(markup).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles string items with pipe separator", () => {
|
||||||
|
const markup = renderToStaticMarkup(
|
||||||
|
<OrderCompositionPanel
|
||||||
|
invitation={{
|
||||||
|
orderNumber: "CD-240031",
|
||||||
|
orderItems: ["Плинтус|5 шт"],
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(markup).toContain("Плинтус");
|
||||||
|
expect(markup).toContain("5 шт");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Badge } from "../UI/Badge";
|
||||||
|
import { Panel } from "../UI/Panel";
|
||||||
|
import { getInvitationReferenceLabel } from "./invitationReference";
|
||||||
|
|
||||||
|
const splitOrderItem = (item) => {
|
||||||
|
if (!item) return null;
|
||||||
|
|
||||||
|
if (typeof item === "string") {
|
||||||
|
const [name, quantity] = item.split("|").map((p) => p.trim());
|
||||||
|
return { name: name || item.trim(), quantity: quantity || "", items: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
: "";
|
||||||
|
const nestedItems = Array.isArray(item.items)
|
||||||
|
? item.items.map((sub) => {
|
||||||
|
if (!sub || typeof sub !== "object") return null;
|
||||||
|
return {
|
||||||
|
name: String(sub.product_name || sub.name || sub.title || "").trim(),
|
||||||
|
quantity: String(sub.product_quantity || sub.quantity || sub.count || sub.amount || "").trim(),
|
||||||
|
};
|
||||||
|
}).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
return { name: name || "Позиция", quantity, items: nestedItems };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildDetailRows = (invitation) => {
|
||||||
|
const rows = [];
|
||||||
|
const reference = getInvitationReferenceLabel(invitation);
|
||||||
|
if (reference && reference !== "Счет —") {
|
||||||
|
rows.push({ label: "Счет", value: reference.replace(/^Счета?:\s*/, "") });
|
||||||
|
}
|
||||||
|
if (invitation.customerName) {
|
||||||
|
rows.push({ label: "Клиент", value: invitation.customerName });
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OrderCompositionPanel = ({ invitation = {} }) => {
|
||||||
|
const orderItems = (invitation.orderItems || invitation.items || [])
|
||||||
|
.map(splitOrderItem)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const detailRows = buildDetailRows(invitation);
|
||||||
|
const hasContent = orderItems.length > 0 || detailRows.length > 0;
|
||||||
|
|
||||||
|
if (!hasContent) return null;
|
||||||
|
|
||||||
|
const reference = getInvitationReferenceLabel(invitation);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel className="space-y-3 border shadow-soft p-5 sm:p-6">
|
||||||
|
<p className="text-sm uppercase tracking-[0.18em] text-[var(--color-text-muted)]">
|
||||||
|
Состав заказа {reference !== "Счет —" ? reference : ""}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{detailRows.length > 0 && !orderItems.length ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{detailRows.map((row) => (
|
||||||
|
<div
|
||||||
|
key={row.label}
|
||||||
|
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 text-[var(--color-text-muted)]">{row.label}</span>
|
||||||
|
<span className="font-medium leading-6">{row.value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{orderItems.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{orderItems.map((item) => (
|
||||||
|
<div key={`${item.name}-${item.quantity || "item"}`}>
|
||||||
|
<div 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="font-medium leading-6">{item.name}</span>
|
||||||
|
{item.quantity ? <Badge tone="neutral">{item.quantity}</Badge> : null}
|
||||||
|
</div>
|
||||||
|
{item.items && item.items.length > 0 ? (
|
||||||
|
<div className="mt-1 space-y-1 pl-3">
|
||||||
|
{item.items.map((sub, idx) => (
|
||||||
|
<div
|
||||||
|
key={`${sub.name}-${idx}`}
|
||||||
|
className="flex items-center justify-between gap-3 rounded-[14px] border border-[var(--color-border)] bg-[var(--color-bg)] px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<span className="leading-6">{sub.name}</span>
|
||||||
|
{sub.quantity ? <Badge tone="neutral">{sub.quantity}</Badge> : null}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -2,6 +2,7 @@ import React from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { DeliveryChoiceFlow } from "../components/client/DeliveryChoiceFlow";
|
import { DeliveryChoiceFlow } from "../components/client/DeliveryChoiceFlow";
|
||||||
import { DeliverySlotsPicker } from "../components/client/DeliverySlotsPicker";
|
import { DeliverySlotsPicker } from "../components/client/DeliverySlotsPicker";
|
||||||
|
import { OrderCompositionPanel } from "../components/client/OrderCompositionPanel";
|
||||||
import { getInvitationReferenceLabel } from "../components/client/invitationReference";
|
import { getInvitationReferenceLabel } from "../components/client/invitationReference";
|
||||||
import { DeliveryStateNotice } from "../components/client/DeliveryStateNotice";
|
import { DeliveryStateNotice } from "../components/client/DeliveryStateNotice";
|
||||||
import { Panel } from "../components/UI/Panel";
|
import { Panel } from "../components/UI/Panel";
|
||||||
|
|
@ -315,6 +316,8 @@ export const ClientDeliveryPage = () => {
|
||||||
) : null}
|
) : null}
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
|
<OrderCompositionPanel invitation={invitation} />
|
||||||
|
|
||||||
{isChoiceSaved && savedChoiceLabel ? (
|
{isChoiceSaved && savedChoiceLabel ? (
|
||||||
<Panel className="space-y-2 p-5 sm:p-6">
|
<Panel className="space-y-2 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>
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,60 @@ export const __resetLocalDeliveryInvitationCache = () => {
|
||||||
localDeliveryInvitationCache.clear();
|
localDeliveryInvitationCache.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const enrichOrderItemsFromEdgeFunction = (invitation, token) => {
|
||||||
|
// Fire-and-forget: try the Edge Function in the background to get richer
|
||||||
|
// order item data (with source_orders). On success, update the cache.
|
||||||
|
// The initial RPC response is returned immediately so the page renders
|
||||||
|
// without delay; the richer data arrives shortly after.
|
||||||
|
if (!token || isLocalClientInvitationToken(token)) {
|
||||||
|
return invitation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasOnlyInvoiceItems = Array.isArray(invitation.orderItems)
|
||||||
|
&& invitation.orderItems.every(
|
||||||
|
(item) => typeof item === "object" && item !== null
|
||||||
|
&& (!Array.isArray(item.items) || item.items.length === 0)
|
||||||
|
&& (!item.quantity || item.quantity === ""),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasOnlyInvoiceItems) {
|
||||||
|
return invitation;
|
||||||
|
}
|
||||||
|
|
||||||
|
invokeDeliveryFunction("get-delivery-invitation", { token })
|
||||||
|
.then((response) => {
|
||||||
|
if (response?.invitation?.orderItems) {
|
||||||
|
const enriched = {
|
||||||
|
...invitation,
|
||||||
|
orderItems: response.invitation.orderItems,
|
||||||
|
};
|
||||||
|
cacheInvitation(enriched);
|
||||||
|
const storage = getLocalStorage();
|
||||||
|
if (storage) {
|
||||||
|
try {
|
||||||
|
storage.setItem(
|
||||||
|
getLocalStorageKey(token),
|
||||||
|
JSON.stringify(enriched),
|
||||||
|
);
|
||||||
|
window.dispatchEvent(
|
||||||
|
new StorageEvent("storage", {
|
||||||
|
key: getLocalStorageKey(token),
|
||||||
|
newValue: JSON.stringify(enriched),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Ignore storage errors.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Silently ignore — the initial RPC data is already shown.
|
||||||
|
});
|
||||||
|
|
||||||
|
return invitation;
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchDeliveryInvitation = async (token) => {
|
export const fetchDeliveryInvitation = async (token) => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error("Token is required");
|
throw new Error("Token is required");
|
||||||
|
|
@ -208,7 +262,8 @@ export const fetchDeliveryInvitation = async (token) => {
|
||||||
p_token: token,
|
p_token: token,
|
||||||
});
|
});
|
||||||
if (response?.invitation) {
|
if (response?.invitation) {
|
||||||
return cacheInvitation(response.invitation);
|
const enriched = enrichOrderItemsFromEdgeFunction(response.invitation, token);
|
||||||
|
return cacheInvitation(enriched);
|
||||||
}
|
}
|
||||||
if (isLocalClientInvitationToken(token)) {
|
if (isLocalClientInvitationToken(token)) {
|
||||||
return cacheInvitation(buildFallbackInvitation(token));
|
return cacheInvitation(buildFallbackInvitation(token));
|
||||||
|
|
|
||||||
|
|
@ -190,8 +190,8 @@ export type OrderGroupInvitationSource = {
|
||||||
order_numbers?: string[] | null;
|
order_numbers?: string[] | null;
|
||||||
delivery_status?: string | null;
|
delivery_status?: string | null;
|
||||||
delivery_link?: string | null;
|
delivery_link?: string | null;
|
||||||
|
source_orders?: unknown[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isInvitationExpired = (invitation: DeliveryInvitationRecord, now = new Date()) => {
|
export const isInvitationExpired = (invitation: DeliveryInvitationRecord, now = new Date()) => {
|
||||||
if (invitation.revoked_at) {
|
if (invitation.revoked_at) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -212,6 +212,45 @@ const parseGroupKey = (groupKey?: string | null) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extractOrderItemsFromSourceOrders = (sourceOrders: unknown): Array<{ name: string; quantity: string; items?: unknown[] }> => {
|
||||||
|
if (!Array.isArray(sourceOrders) || sourceOrders.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const items: Array<{ name: string; quantity: string; items?: unknown[] }> = [];
|
||||||
|
|
||||||
|
for (const source of sourceOrders) {
|
||||||
|
if (!source || typeof source !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = source as Record<string, unknown>;
|
||||||
|
const nom = typeof record.nom === "string" ? record.nom : typeof record.name === "string" ? record.name : "";
|
||||||
|
const orderList = Array.isArray(record.orderList) ? record.orderList : Array.isArray(record.items) ? record.items : [];
|
||||||
|
|
||||||
|
if (orderList.length > 0) {
|
||||||
|
items.push({
|
||||||
|
name: nom || "Позиция",
|
||||||
|
quantity: "",
|
||||||
|
items: orderList.map((item: unknown) => {
|
||||||
|
if (!item || typeof item !== "object") {
|
||||||
|
return { name: String(item), quantity: "" };
|
||||||
|
}
|
||||||
|
const row = item as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
name: String(row.product_name || row.name || row.title || ""),
|
||||||
|
quantity: String(row.product_quantity || row.quantity || row.count || row.amount || ""),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} else if (nom) {
|
||||||
|
items.push({ name: nom, quantity: "" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
export const buildPublicOrderGroupInvitationView = (
|
export const buildPublicOrderGroupInvitationView = (
|
||||||
invitation: DeliveryInvitationRecord,
|
invitation: DeliveryInvitationRecord,
|
||||||
group: OrderGroupInvitationSource,
|
group: OrderGroupInvitationSource,
|
||||||
|
|
@ -221,6 +260,11 @@ export const buildPublicOrderGroupInvitationView = (
|
||||||
const customerPhone = group.customer_phone || group.customer?.phone || invitation.customer_phone || parsedKey.phone || null;
|
const customerPhone = group.customer_phone || group.customer?.phone || invitation.customer_phone || parsedKey.phone || null;
|
||||||
const orderNumbers = Array.isArray(group.order_numbers) ? group.order_numbers : [];
|
const orderNumbers = Array.isArray(group.order_numbers) ? group.order_numbers : [];
|
||||||
|
|
||||||
|
const orderItemsFromSource = extractOrderItemsFromSourceOrders(group.source_orders);
|
||||||
|
const orderItems = orderItemsFromSource.length > 0
|
||||||
|
? orderItemsFromSource
|
||||||
|
: orderNumbers.map((number) => ({ name: number, quantity: "" }));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
orderId: invitation.order_group_id || group.id,
|
orderId: invitation.order_group_id || group.id,
|
||||||
orderGroupId: invitation.order_group_id || group.id,
|
orderGroupId: invitation.order_group_id || group.id,
|
||||||
|
|
@ -229,7 +273,7 @@ export const buildPublicOrderGroupInvitationView = (
|
||||||
orderNumber: invitation.order_number || orderNumbers[0] || group.group_key || null,
|
orderNumber: invitation.order_number || orderNumbers[0] || group.group_key || null,
|
||||||
customerName: maskCustomerName(customerName),
|
customerName: maskCustomerName(customerName),
|
||||||
customerPhone: maskPhoneNumber(customerPhone),
|
customerPhone: maskPhoneNumber(customerPhone),
|
||||||
orderItems: orderNumbers.map((number) => ({ name: number, quantity: "" })),
|
orderItems,
|
||||||
availableSlots: invitation.available_slots || [],
|
availableSlots: invitation.available_slots || [],
|
||||||
deliveryDate: invitation.delivery_date || null,
|
deliveryDate: invitation.delivery_date || null,
|
||||||
deliveryTime: invitation.delivery_time || null,
|
deliveryTime: invitation.delivery_time || null,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue