fix(delivery): tighten public flow
Filter client delivery slots to tomorrow and the day after,\nkeep invalid delivery times out of the UI, and show a clearer\nerror state when saving fails.
This commit is contained in:
parent
633973142d
commit
7e399f2517
|
|
@ -10,7 +10,47 @@ import {
|
|||
fetchDeliveryInvitation,
|
||||
} from "../services/deliveryInvitationApi";
|
||||
|
||||
export const groupSlotsFromInvitation = (invitation) => {
|
||||
const DELIVERY_TIMEZONE = "Europe/Simferopol";
|
||||
|
||||
const getBusinessTodayKey = (referenceDate = new Date()) => {
|
||||
const parts = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: DELIVERY_TIMEZONE,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}).formatToParts(referenceDate);
|
||||
|
||||
const year = parts.find((part) => part.type === "year")?.value || "";
|
||||
const month = parts.find((part) => part.type === "month")?.value || "";
|
||||
const day = parts.find((part) => part.type === "day")?.value || "";
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
const addDaysToDateKey = (dateKey, amount) => {
|
||||
const baseDate = new Date(`${dateKey}T12:00:00Z`);
|
||||
if (Number.isNaN(baseDate.getTime())) {
|
||||
return "";
|
||||
}
|
||||
|
||||
baseDate.setUTCDate(baseDate.getUTCDate() + amount);
|
||||
return baseDate.toISOString().slice(0, 10);
|
||||
};
|
||||
|
||||
const getAllowedDeliveryDateKeys = (referenceDate = new Date()) => {
|
||||
const todayKey = getBusinessTodayKey(referenceDate);
|
||||
return new Set([addDaysToDateKey(todayKey, 1), addDaysToDateKey(todayKey, 2)].filter(Boolean));
|
||||
};
|
||||
|
||||
const isAllowedDeliverySlotDate = (dateKey, referenceDate = new Date()) => {
|
||||
if (!dateKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getAllowedDeliveryDateKeys(referenceDate).has(dateKey);
|
||||
};
|
||||
|
||||
export const groupSlotsFromInvitation = (invitation, referenceDate = new Date()) => {
|
||||
if (!invitation) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -44,6 +84,10 @@ export const groupSlotsFromInvitation = (invitation) => {
|
|||
|| deliveryDate
|
||||
|| "";
|
||||
|
||||
if (!isAllowedDeliverySlotDate(parsedDate, referenceDate)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: `slot-${index}-${raw}`,
|
||||
date: parsedDate || deliveryDate || "",
|
||||
|
|
@ -64,6 +108,10 @@ export const groupSlotsFromInvitation = (invitation) => {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (!isAllowedDeliverySlotDate(slotDate, referenceDate)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: slotId,
|
||||
date: slotDate,
|
||||
|
|
@ -112,6 +160,7 @@ export const ClientDeliveryPage = () => {
|
|||
const [selectedSlotId, setSelectedSlotId] = React.useState(null);
|
||||
const [selectedSlot, setSelectedSlot] = React.useState(null);
|
||||
const [choiceSaved, setChoiceSaved] = React.useState(false);
|
||||
const referenceDate = React.useMemo(() => new Date(), [token]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
|
@ -154,7 +203,7 @@ export const ClientDeliveryPage = () => {
|
|||
};
|
||||
}, [token]);
|
||||
|
||||
const slots = groupSlotsFromInvitation(invitation);
|
||||
const slots = groupSlotsFromInvitation(invitation, referenceDate);
|
||||
|
||||
const invitationState = invitation?.state || "awaiting_choice";
|
||||
const isActiveState = ["awaiting_choice", "opened", "reminder_sent"].includes(invitationState);
|
||||
|
|
@ -189,7 +238,12 @@ export const ClientDeliveryPage = () => {
|
|||
});
|
||||
const loadedInvitation = await fetchDeliveryInvitation(token);
|
||||
setInvitation(loadedInvitation);
|
||||
setSelectedSlot(buildSelectedSlotFromInvitation(loadedInvitation, groupSlotsFromInvitation(loadedInvitation)) || effectiveSelectedSlot);
|
||||
setSelectedSlot(
|
||||
buildSelectedSlotFromInvitation(
|
||||
loadedInvitation,
|
||||
groupSlotsFromInvitation(loadedInvitation, referenceDate),
|
||||
) || effectiveSelectedSlot,
|
||||
);
|
||||
setChoiceSaved(true);
|
||||
setActionMessage("Выбор сохранен, спасибо.");
|
||||
} catch (confirmError) {
|
||||
|
|
@ -286,7 +340,17 @@ export const ClientDeliveryPage = () => {
|
|||
<Panel className="p-5 text-sm leading-6 text-[var(--color-text-muted)] sm:p-6">{actionMessage}</Panel>
|
||||
) : null}
|
||||
|
||||
{!loading && error && invitation ? <DeliveryStateNotice state="default" /> : null}
|
||||
{!loading && error && invitation ? (
|
||||
<Panel className="space-y-2 border-[rgba(204,112,0,0.28)] bg-[var(--color-surface)] p-5 sm:p-6">
|
||||
<p className="text-sm uppercase tracking-[0.24em] text-[var(--color-text-muted)]">
|
||||
Не удалось сохранить
|
||||
</p>
|
||||
<h2 className="text-2xl font-semibold leading-tight">
|
||||
Проверьте выбор еще раз
|
||||
</h2>
|
||||
<p className="text-sm leading-6 text-[var(--color-text-muted)]">{error}</p>
|
||||
</Panel>
|
||||
) : null}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ describe("ClientDeliveryPage helpers", () => {
|
|||
"2026-04-15, До обеда",
|
||||
"2026-04-15, После обеда",
|
||||
],
|
||||
}),
|
||||
}, new Date("2026-04-14T09:00:00Z")),
|
||||
).toEqual([
|
||||
{
|
||||
id: "slot-0-2026-04-15, До обеда",
|
||||
|
|
@ -58,7 +58,7 @@ describe("ClientDeliveryPage helpers", () => {
|
|||
"2026-04-15, До обеда",
|
||||
"2026-04-15, После обеда",
|
||||
],
|
||||
}),
|
||||
}, new Date("2026-04-13T09:00:00Z")),
|
||||
).toEqual([
|
||||
{
|
||||
id: "slot-0-2026-04-14, До обеда",
|
||||
|
|
@ -114,7 +114,7 @@ describe("ClientDeliveryPage helpers", () => {
|
|||
{ id: "slot-object", date: "2026-04-15", time: "Первая половина дня" },
|
||||
42,
|
||||
],
|
||||
}),
|
||||
}, new Date("2026-04-14T09:00:00Z")),
|
||||
).toEqual([
|
||||
{
|
||||
id: "slot-object",
|
||||
|
|
@ -123,4 +123,43 @@ describe("ClientDeliveryPage helpers", () => {
|
|||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps only tomorrow and the day after tomorrow", () => {
|
||||
expect(
|
||||
groupSlotsFromInvitation(
|
||||
{
|
||||
availableSlots: [
|
||||
"2026-04-15, Первая половина дня",
|
||||
"2026-04-15, Вторая половина дня",
|
||||
"2026-04-16, Первая половина дня",
|
||||
"2026-04-16, Вторая половина дня",
|
||||
"2026-04-17, Первая половина дня",
|
||||
"2026-04-17, Вторая половина дня",
|
||||
],
|
||||
},
|
||||
new Date("2026-04-14T09:00:00Z"),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
id: "slot-0-2026-04-15, Первая половина дня",
|
||||
date: "2026-04-15",
|
||||
time: "Первая половина дня",
|
||||
},
|
||||
{
|
||||
id: "slot-1-2026-04-15, Вторая половина дня",
|
||||
date: "2026-04-15",
|
||||
time: "Вторая половина дня",
|
||||
},
|
||||
{
|
||||
id: "slot-2-2026-04-16, Первая половина дня",
|
||||
date: "2026-04-16",
|
||||
time: "Первая половина дня",
|
||||
},
|
||||
{
|
||||
id: "slot-3-2026-04-16, Вторая половина дня",
|
||||
date: "2026-04-16",
|
||||
time: "Вторая половина дня",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const requireSupabase = () => {
|
|||
};
|
||||
|
||||
const normalizeText = (value) => (value == null ? "" : String(value)).trim();
|
||||
const ALLOWED_DELIVERY_TIMES = new Set(["Первая половина дня", "Вторая половина дня"]);
|
||||
|
||||
const normalizePhone = (value) => normalizeText(value).replace(/[\s\-()]/g, "");
|
||||
|
||||
|
|
@ -69,7 +70,13 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
|
|||
);
|
||||
const deliveryStatus = normalizeText(row.delivery_status) || "pending_confirmation";
|
||||
const deliveryDate = normalizeText(row.delivery_date);
|
||||
const deliveryTime = normalizeText(row.delivery_time);
|
||||
const rawDeliveryTime = normalizeText(row.delivery_time);
|
||||
const rawDeliveryHalfDay = normalizeText(row.delivery_half_day);
|
||||
const deliveryTime = ALLOWED_DELIVERY_TIMES.has(rawDeliveryTime)
|
||||
? rawDeliveryTime
|
||||
: ALLOWED_DELIVERY_TIMES.has(rawDeliveryHalfDay)
|
||||
? rawDeliveryHalfDay
|
||||
: "";
|
||||
const deliveryAddress = normalizeText(row.delivery_address);
|
||||
|
||||
return {
|
||||
|
|
@ -114,8 +121,8 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
|
|||
deliveryDate,
|
||||
deliveryTime,
|
||||
deliveryHalfDay: getOrderGroupDeliveryHalfDay({
|
||||
deliveryHalfDay: row.delivery_half_day,
|
||||
deliveryTime,
|
||||
deliveryHalfDay: rawDeliveryHalfDay,
|
||||
deliveryTime: rawDeliveryTime,
|
||||
deliveryWindow: row.delivery_window,
|
||||
sourceOrders: row.source_orders,
|
||||
}),
|
||||
|
|
@ -126,8 +133,8 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
|
|||
customerPhone,
|
||||
customerDate,
|
||||
deliveryAddress,
|
||||
row.delivery_half_day,
|
||||
deliveryTime,
|
||||
rawDeliveryHalfDay,
|
||||
rawDeliveryTime,
|
||||
row.delivery_window,
|
||||
deliveryStatus,
|
||||
getOrderGroupDeliveryStatusLabel(deliveryStatus),
|
||||
|
|
@ -135,8 +142,8 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
|
|||
row.status,
|
||||
getOrderGroupStatusLabel(row.status),
|
||||
getOrderGroupDeliveryHalfDay({
|
||||
deliveryHalfDay: row.delivery_half_day,
|
||||
deliveryTime,
|
||||
deliveryHalfDay: rawDeliveryHalfDay,
|
||||
deliveryTime: rawDeliveryTime,
|
||||
deliveryWindow: row.delivery_window,
|
||||
sourceOrders: row.source_orders,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -86,6 +86,26 @@ describe("mapOrderGroupRowToDeliveryGroup", () => {
|
|||
expect(group.notReadyCount).toBe(0);
|
||||
expect(group.deliveryDate).toBe("");
|
||||
});
|
||||
|
||||
it("drops invalid delivery time values instead of showing them", () => {
|
||||
const group = mapOrderGroupRowToDeliveryGroup({
|
||||
id: "group-with-bad-time",
|
||||
group_key: "9781632663|08.05.26",
|
||||
customer_name: "Зиновьев Алексей Гаврилович",
|
||||
customer_phone: "9781632663",
|
||||
customer_date: "08.05.26",
|
||||
delivery_time: "Зиновьев Алексей Гаврилович",
|
||||
delivery_half_day: null,
|
||||
order_numbers: ["СФ Т\\ЕА-26979"],
|
||||
status: "ready_for_notification",
|
||||
delivery_status: "pending_confirmation",
|
||||
created_at: "2026-05-05 09:43:53.750061+00",
|
||||
updated_at: "2026-05-05 09:43:53.750061+00",
|
||||
});
|
||||
|
||||
expect(group.deliveryTime).toBe("");
|
||||
expect(group.searchText).not.toContain("зиновьев алексей гаврилович зиновьев алексей гаврилович");
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateOrderGroupDeliveryChoice", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue