Merge pull request 'fix(delivery): simplify public choice flow' (#2) from codex/delivery-rpc-deploy into main

Reviewed-on: https://git.supersamsev.ru/mihail/supersam/pulls/2
This commit is contained in:
mihail 2026-05-14 18:17:59 +00:00
commit 488e478841
6 changed files with 65 additions and 28 deletions

View File

@ -52,7 +52,6 @@ export const DeliveryChoiceFlow = ({
invitation = {}, invitation = {},
selectedSlot = null, selectedSlot = null,
onConfirmChoice = () => {}, onConfirmChoice = () => {},
onRequestNewLink = () => {},
}) => { }) => {
const state = invitation.state || "awaiting_choice"; const state = invitation.state || "awaiting_choice";
const isActive = ACTIVE_STATES.has(state); const isActive = ACTIVE_STATES.has(state);
@ -108,9 +107,6 @@ export const DeliveryChoiceFlow = ({
> >
Сохранить Сохранить
</Button> </Button>
<Button variant="secondary" className="w-full sm:w-auto" onClick={onRequestNewLink}>
Запросить новую ссылку
</Button>
</div> </div>
</Panel> </Panel>
); );

View File

@ -13,13 +13,13 @@ describe("DeliveryChoiceFlow", () => {
customerName: "Мария Волкова", customerName: "Мария Волкова",
}} }}
onConfirmChoice={() => {}} onConfirmChoice={() => {}}
onRequestNewLink={() => {}}
/>, />,
); );
expect(markup).toContain("Выберите время доставки"); expect(markup).toContain("Выберите время доставки");
expect(markup).toContain("Сохранить"); expect(markup).toContain("Сохранить");
expect(markup).toContain("Ожидает ответа клиента"); expect(markup).toContain("Ожидает ответа клиента");
expect(markup).not.toContain("Запросить новую ссылку");
}); });
it("renders a disabled save action when nothing is selected", () => { it("renders a disabled save action when nothing is selected", () => {
@ -31,7 +31,6 @@ describe("DeliveryChoiceFlow", () => {
customerName: "Мария Волкова", customerName: "Мария Волкова",
}} }}
onConfirmChoice={() => {}} onConfirmChoice={() => {}}
onRequestNewLink={() => {}}
/>, />,
); );
@ -54,7 +53,6 @@ describe("DeliveryChoiceFlow", () => {
availableSlots: ["Первая половина дня", "Вторая половина дня"], availableSlots: ["Первая половина дня", "Вторая половина дня"],
}} }}
onConfirmChoice={() => {}} onConfirmChoice={() => {}}
onRequestNewLink={() => {}}
/>, />,
); );
@ -75,7 +73,6 @@ describe("DeliveryChoiceFlow", () => {
customerName: "Мария Волкова", customerName: "Мария Волкова",
}} }}
onConfirmChoice={() => {}} onConfirmChoice={() => {}}
onRequestNewLink={() => {}}
/>, />,
); );
@ -92,7 +89,6 @@ describe("DeliveryChoiceFlow", () => {
customerName: "Мария Волкова", customerName: "Мария Волкова",
}} }}
onConfirmChoice={() => {}} onConfirmChoice={() => {}}
onRequestNewLink={() => {}}
/>, />,
); );
@ -110,7 +106,6 @@ describe("DeliveryChoiceFlow", () => {
availableSlots: ["15 апреля, первая половина дня"], availableSlots: ["15 апреля, первая половина дня"],
}} }}
onConfirmChoice={() => {}} onConfirmChoice={() => {}}
onRequestNewLink={() => {}}
/>, />,
); );

View File

@ -6,6 +6,20 @@ import { formatDeliverySlotGroupLabel } from "./deliveryDateFormatting";
const groupSlotsByDate = (slots) => { const groupSlotsByDate = (slots) => {
const groups = new Map(); const groups = new Map();
const getSlotPriority = (slot) => {
const time = String(slot?.time || "").toLowerCase();
if (time.includes("первая") || time.includes("до обеда")) {
return 0;
}
if (time.includes("вторая") || time.includes("после обеда")) {
return 1;
}
return 2;
};
for (const slot of slots) { for (const slot of slots) {
if (!groups.has(slot.date)) { if (!groups.has(slot.date)) {
groups.set(slot.date, []); groups.set(slot.date, []);
@ -14,7 +28,12 @@ const groupSlotsByDate = (slots) => {
groups.get(slot.date).push(slot); groups.get(slot.date).push(slot);
} }
return Array.from(groups.entries()).sort(([a], [b]) => a.localeCompare(b)); return Array.from(groups.entries())
.map(([date, dateSlots]) => [
date,
[...dateSlots].sort((left, right) => getSlotPriority(left) - getSlotPriority(right)),
])
.sort(([a], [b]) => a.localeCompare(b));
}; };
export { formatDeliverySlotGroupLabel } from "./deliveryDateFormatting"; export { formatDeliverySlotGroupLabel } from "./deliveryDateFormatting";
@ -37,13 +56,6 @@ export const DeliverySlotsPicker = ({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<Panel className="p-5 sm:p-6">
<h3 className="text-lg font-semibold">Выберите день и половину дня доставки</h3>
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
Раскройте нужный день, выберите подходящую половину и затем сохраните выбор ниже.
</p>
</Panel>
{grouped.map(([date, dateSlots]) => ( {grouped.map(([date, dateSlots]) => (
<details key={date} className="group rounded-[28px] border border-[var(--color-border)] bg-[var(--color-surface)] shadow-soft backdrop-blur" open> <details key={date} className="group rounded-[28px] border border-[var(--color-border)] bg-[var(--color-surface)] shadow-soft backdrop-blur" open>
<summary className="cursor-pointer list-none p-5 sm:p-6"> <summary className="cursor-pointer list-none p-5 sm:p-6">

View File

@ -37,6 +37,23 @@ describe("DeliverySlotsPicker", () => {
expect(markup).toContain("послезавтра · 15.04.2026"); expect(markup).toContain("послезавтра · 15.04.2026");
expect(markup).toContain("первая половина дня"); expect(markup).toContain("первая половина дня");
expect(markup).toContain("вторая половина дня"); expect(markup).toContain("вторая половина дня");
expect(markup).not.toContain("выберите день и половину дня доставки");
});
it("renders the first half of day before the second half", () => {
const markup = renderToStaticMarkup(
<DeliverySlotsPicker
slots={[
{ date: "2026-04-14", time: "Вторая половина дня", id: "slot-2" },
{ date: "2026-04-14", time: "Первая половина дня", id: "slot-1" },
]}
onSelectSlot={() => {}}
selectedSlotId={null}
referenceDate={new Date("2026-04-13T09:00:00Z")}
/>,
).toLowerCase();
expect(markup.indexOf("первая половина дня")).toBeLessThan(markup.indexOf("вторая половина дня"));
}); });
it("marks the selected slot", () => { it("marks the selected slot", () => {

View File

@ -152,6 +152,16 @@ export const buildSelectedSlotFromInvitation = (invitation, slots = []) => {
}; };
}; };
export const getClientDeliveryHeroDescription = (isActiveState, isChoiceSaved) => {
if (isChoiceSaved) {
return "";
}
return isActiveState
? "Вам предложены варианты доставки. Выберите удобную дату и время."
: "По этому заказу согласование доставки завершено или передано логисту.";
};
export const ClientDeliveryPage = () => { export const ClientDeliveryPage = () => {
const { token } = useParams(); const { token } = useParams();
const [invitation, setInvitation] = React.useState(null); const [invitation, setInvitation] = React.useState(null);
@ -216,6 +226,7 @@ export const ClientDeliveryPage = () => {
const savedChoiceLabel = effectiveSelectedSlot const savedChoiceLabel = effectiveSelectedSlot
? `${formatDeliveryDate(effectiveSelectedSlot.date)} / ${effectiveSelectedSlot.time}` ? `${formatDeliveryDate(effectiveSelectedSlot.date)} / ${effectiveSelectedSlot.time}`
: ""; : "";
const heroDescription = getClientDeliveryHeroDescription(isActiveState, isChoiceSaved);
const handleSaveChoice = async () => { const handleSaveChoice = async () => {
if (!token) { if (!token) {
@ -263,10 +274,6 @@ export const ClientDeliveryPage = () => {
setError(""); setError("");
}; };
const handleRequestNewLink = () => {
setActionMessage("Если ссылка больше не работает, логист передаст новую ссылку вручную.");
};
if (loading) { if (loading) {
return ( return (
<main className="min-h-screen bg-[var(--color-bg)] px-3 py-4 sm:px-6 sm:py-8"> <main className="min-h-screen bg-[var(--color-bg)] px-3 py-4 sm:px-6 sm:py-8">
@ -301,11 +308,11 @@ export const ClientDeliveryPage = () => {
<Panel className="space-y-3 p-5 sm:p-6"> <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> <h1 className="text-2xl font-semibold leading-tight sm:text-3xl">Согласование доставки</h1>
<p className="text-sm leading-6 text-[var(--color-text-muted)]"> {heroDescription ? (
{isActiveState <p className="text-sm leading-6 text-[var(--color-text-muted)]">
? "Вам предложены варианты доставки. Выберите удобную дату и время." {heroDescription}
: "По этому заказу согласование доставки завершено или передано логисту."} </p>
</p> ) : null}
</Panel> </Panel>
{isChoiceSaved && savedChoiceLabel ? ( {isChoiceSaved && savedChoiceLabel ? (
@ -334,7 +341,6 @@ export const ClientDeliveryPage = () => {
invitation={invitation} invitation={invitation}
selectedSlot={effectiveSelectedSlot} selectedSlot={effectiveSelectedSlot}
onConfirmChoice={handleSaveChoice} onConfirmChoice={handleSaveChoice}
onRequestNewLink={handleRequestNewLink}
/> />
) : !isChoiceSaved ? ( ) : !isChoiceSaved ? (
<DeliveryStateNotice state={invitationState} /> <DeliveryStateNotice state={invitationState} />

View File

@ -3,6 +3,7 @@ import { getInvitationReferenceLabel } from "../components/client/invitationRefe
import { import {
buildDeliveryConfirmationPayload, buildDeliveryConfirmationPayload,
buildSelectedSlotFromInvitation, buildSelectedSlotFromInvitation,
getClientDeliveryHeroDescription,
groupSlotsFromInvitation, groupSlotsFromInvitation,
} from "./ClientDeliveryPage"; } from "./ClientDeliveryPage";
@ -176,4 +177,14 @@ describe("ClientDeliveryPage helpers", () => {
}), }),
).toBe("Счета: СФ Т\\ЕА-28687, СФ Т\\ЕА-28700"); ).toBe("Счета: СФ Т\\ЕА-28687, СФ Т\\ЕА-28700");
}); });
it("hides the hero helper text after the client saves the choice", () => {
expect(getClientDeliveryHeroDescription(true, true)).toBe("");
expect(getClientDeliveryHeroDescription(true, false)).toBe(
"Вам предложены варианты доставки. Выберите удобную дату и время.",
);
expect(getClientDeliveryHeroDescription(false, false)).toBe(
"По этому заказу согласование доставки завершено или передано логисту.",
);
});
}); });