import React from "react"; import { useParams } from "react-router-dom"; 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 { DeliveryStateNotice } from "../components/client/DeliveryStateNotice"; import { Panel } from "../components/UI/Panel"; import { Skeleton } from "../components/UI/Loading"; import { formatDeliveryDate } from "../components/client/deliveryDateFormatting"; import { confirmDeliveryChoice, fetchDeliveryInvitation, } from "../services/deliveryInvitationApi"; 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 isSundayKey = (dateKey) => { if (!dateKey) return true; const d = new Date(`${dateKey}T12:00:00Z`); return d.getUTCDay() === 0; }; const getNextWorkdayKey = (dateKey) => { let next = addDaysToDateKey(dateKey, 1); while (isSundayKey(next)) { next = addDaysToDateKey(next, 1); } return next; }; const getAllowedDeliveryDateKeys = (referenceDate = new Date()) => { const todayKey = getBusinessTodayKey(referenceDate); const firstWorkday = getNextWorkdayKey(todayKey); const secondWorkday = getNextWorkdayKey(firstWorkday); return new Set([firstWorkday, secondWorkday].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 []; } const rawSlots = Array.isArray(invitation.availableSlots) ? invitation.availableSlots : []; const deliveryDate = invitation.deliveryDate; const deliveryTime = invitation.deliveryTime; if (!rawSlots.length && !deliveryDate) { // Fallback: generate default delivery slots (next 2 workdays, both halves, skip Sunday) const todayKey = getBusinessTodayKey(referenceDate); const firstWorkday = getNextWorkdayKey(todayKey); const secondWorkday = getNextWorkdayKey(firstWorkday); return [ { id: `slot-${firstWorkday}-first`, date: firstWorkday, time: "Первая половина дня" }, { id: `slot-${firstWorkday}-second`, date: firstWorkday, time: "Вторая половина дня" }, { id: `slot-${secondWorkday}-first`, date: secondWorkday, time: "Первая половина дня" }, { id: `slot-${secondWorkday}-second`, date: secondWorkday, time: "Вторая половина дня" }, ].filter((s) => s.date); } if (!rawSlots.length && deliveryDate) { return [ { id: `slot-${deliveryDate}-${deliveryTime || "default"}`, date: deliveryDate, time: deliveryTime || "Половина дня", }, ]; } return rawSlots .map((raw, index) => { if (typeof raw === "string") { const parts = raw.split(","); const datePart = parts[0]?.trim() || ""; const timePart = parts.slice(1).join(",").trim() || ""; const parsedDate = datePart.replace(/[а-яё]+/gi, "").trim() || deliveryDate || ""; if (!isAllowedDeliverySlotDate(parsedDate, referenceDate)) { return null; } return { id: `slot-${index}-${raw}`, date: parsedDate || deliveryDate || "", time: timePart || deliveryTime || raw, }; } if (raw && typeof raw === "object") { const slotId = typeof raw.id === "string" ? raw.id : `slot-${index}-${deliveryDate || "custom"}`; const slotDate = typeof raw.date === "string" ? raw.date : deliveryDate || ""; const slotTime = typeof raw.time === "string" ? raw.time : typeof raw.label === "string" ? raw.label : deliveryTime || ""; if (!slotDate && !slotTime) { return null; } if (!isAllowedDeliverySlotDate(slotDate, referenceDate)) { return null; } return { id: slotId, date: slotDate, time: slotTime, }; } return null; }) .filter(Boolean); }; export const buildDeliveryConfirmationPayload = ({ slot, invitation, searchDate, deliveryType = "delivery", pickupDate, pickupTimeSlot, }) => { if (deliveryType === "pickup") { return { deliveryType: "pickup", pickupDate: pickupDate || slot?.date || undefined, pickupTimeSlot: pickupTimeSlot || slot?.time || undefined, deliveryDate: pickupDate || slot?.date || searchDate || invitation?.deliveryDate || undefined, deliveryTime: pickupTimeSlot || slot?.time || undefined, }; } return { deliveryType: "delivery", deliveryDate: slot?.date || searchDate || invitation?.deliveryDate || undefined, deliveryTime: slot?.time || invitation?.deliveryTime || undefined, }; }; export const buildSelectedSlotFromInvitation = (invitation, slots = []) => { if (!invitation?.deliveryDate) { return null; } const matchingSlot = slots.find( (slot) => slot.date === invitation.deliveryDate && (!invitation.deliveryTime || slot.time === invitation.deliveryTime), ); return matchingSlot || { id: `slot-${invitation.deliveryDate}-${invitation.deliveryTime || "default"}`, date: invitation.deliveryDate, time: invitation.deliveryTime || "Половина дня", }; }; export const getClientDeliveryHeroDescription = (isActiveState, isChoiceSaved) => { if (isChoiceSaved) { return ""; } return isActiveState ? "Вам предложены варианты доставки. Выберите удобную дату и время." : "По этому заказу согласование доставки завершено или передано логисту."; }; const TAB_DELIVERY = "delivery"; const TAB_PICKUP = "pickup"; export const ClientDeliveryPage = () => { const { token } = useParams(); const [invitation, setInvitation] = React.useState(null); const [loading, setLoading] = React.useState(Boolean(token)); const [error, setError] = React.useState(""); const [actionMessage, setActionMessage] = React.useState(""); const [selectedSlotId, setSelectedSlotId] = React.useState(null); const [selectedSlot, setSelectedSlot] = React.useState(null); const [choiceSaved, setChoiceSaved] = React.useState(false); const [activeTab, setActiveTab] = React.useState(TAB_DELIVERY); const [deliveryAddress, setDeliveryAddress] = React.useState(""); const referenceDate = React.useMemo( () => (invitation?.smsSentAt ? new Date(invitation.smsSentAt) : new Date()), [token, invitation?.smsSentAt], ); React.useEffect(() => { let cancelled = false; const loadInvitation = async () => { if (!token) { setLoading(false); setError("Не передан токен приглашения."); return; } setLoading(true); setError(""); setActionMessage(""); setSelectedSlotId(null); setSelectedSlot(null); setChoiceSaved(false); try { const loadedInvitation = await fetchDeliveryInvitation(token); if (!cancelled) { setInvitation(loadedInvitation); // If invitation already has deliveryType=pickup, pre-select pickup tab if (loadedInvitation?.deliveryType === "pickup") { setActiveTab(TAB_PICKUP); } } } catch (fetchError) { if (!cancelled) { setInvitation(null); setError(fetchError instanceof Error ? fetchError.message : "Не удалось загрузить приглашение"); } } finally { if (!cancelled) { setLoading(false); } } }; loadInvitation(); return () => { cancelled = true; }; }, [token]); const slots = groupSlotsFromInvitation(invitation, referenceDate); const invitationState = invitation?.state || "awaiting_choice"; const isActiveState = ["awaiting_choice", "opened", "reminder_sent"].includes(invitationState); const invitationSelectedSlot = isActiveState ? null : buildSelectedSlotFromInvitation(invitation, slots); const effectiveSelectedSlot = selectedSlot || invitationSelectedSlot; const isChoiceSaved = choiceSaved || (!isActiveState && Boolean(invitationSelectedSlot)); const savedChoiceLabel = effectiveSelectedSlot ? `${formatDeliveryDate(effectiveSelectedSlot.date)} / ${effectiveSelectedSlot.time}` : ""; const heroDescription = getClientDeliveryHeroDescription(isActiveState, isChoiceSaved); const handleSaveChoice = async () => { if (!token) { return; } if (!effectiveSelectedSlot) { setError("Сначала выберите дату и половину дня."); return; } setActionMessage("Сохраняем выбор..."); setChoiceSaved(false); setError(""); try { await confirmDeliveryChoice({ token, deliveryTime: effectiveSelectedSlot.time, deliveryDate: effectiveSelectedSlot.date, deliveryType: activeTab, ...(activeTab === TAB_PICKUP ? { pickupDate: effectiveSelectedSlot.date, pickupTimeSlot: effectiveSelectedSlot.time, } : {}), ...(activeTab === TAB_DELIVERY && deliveryAddress.trim() ? { deliveryAddress: deliveryAddress.trim(), } : {}), }); const loadedInvitation = await fetchDeliveryInvitation(token); setInvitation(loadedInvitation); setSelectedSlot( buildSelectedSlotFromInvitation( loadedInvitation, groupSlotsFromInvitation(loadedInvitation, referenceDate), ) || effectiveSelectedSlot, ); setChoiceSaved(true); setActionMessage("Выбор сохранен, спасибо."); } catch (confirmError) { setActionMessage(""); setError(confirmError instanceof Error ? confirmError.message : "Не удалось сохранить выбор"); } }; const handleSlotSelect = (slot) => { setSelectedSlotId(slot.id); setSelectedSlot(slot); setChoiceSaved(false); setActionMessage( `Выбрано: ${slot.date ? `${formatDeliveryDate(slot.date)} / ${slot.time}` : slot.time}`, ); setError(""); }; if (loading) { return (

Доставка заказа

Загрузка страницы

); } if (error && !invitation) { return (

Доставка заказа

Не удалось открыть страницу

{error}

); } return (
{!isChoiceSaved ? (

Доставка заказа

Согласование доставки

{heroDescription ? (

{heroDescription}

) : null}
) : null} {isChoiceSaved && savedChoiceLabel ? (

Ваш выбор

{invitation?.deliveryType === "pickup" ? "Самовывоз" : "Доставка"}: {savedChoiceLabel}

{getInvitationReferenceLabel(invitation)}

Статус: {invitation?.deliveryType === "pickup" ? "самовывоз" : "доставка"} уже согласован. При повторном открытии этой ссылки будет показан тот же выбор.

{(invitation?.pickupCode || invitation?.pickup_code) ? (

Код выдачи

{invitation.pickupCode || invitation.pickup_code}

Покажите этот код при получении заказа

) : null}
) : null} {isActiveState && !isChoiceSaved ? ( <> {/* Tab switcher */}
{activeTab === TAB_DELIVERY && !invitation?.deliveryAddress && !invitation?.customerAddress && (
📍

Укажите адрес доставки

Адрес доставки отсутствует в заказе. Пожалуйста, введите полный адрес, куда нужно привезти заказ.

setDeliveryAddress(e.target.value)} placeholder="Город, улица, дом, квартира" className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] px-4 py-3 text-sm text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)] focus:outline-none" />
)} {activeTab === TAB_DELIVERY && !invitation?.isPayedShip ? (
💰

Стоимость доставки не оплачена

Стоимость доставки будет рассчитана отдельно и согласована с логистом после выбора даты и времени.

) : null} {activeTab === TAB_DELIVERY && slots.length ? ( ) : null} {activeTab === TAB_PICKUP ? ( ) : null} {activeTab === TAB_PICKUP && (invitation?.pickupCode || invitation?.pickup_code) ? (

Код выдачи

{invitation.pickupCode || invitation.pickup_code}

Покажите этот код при получении заказа

) : null} {activeTab === TAB_DELIVERY && !slots.length ? (

Нет доступных слотов для выбора доставки.

) : null} ) : null} {isActiveState && !isChoiceSaved ? ( ) : !isActiveState && !isChoiceSaved ? ( ) : null} {actionMessage ? ( {actionMessage} ) : null} {!loading && error && invitation ? (

Не удалось сохранить

Проверьте выбор еще раз

{error}

) : null}
); };