227 lines
7.4 KiB
JavaScript
227 lines
7.4 KiB
JavaScript
import React from "react";
|
||
import { useParams, useSearchParams } from "react-router-dom";
|
||
import { DeliveryChoiceFlow } from "../components/client/DeliveryChoiceFlow";
|
||
import { DeliverySlotsPicker } from "../components/client/DeliverySlotsPicker";
|
||
import { DeliveryStateNotice } from "../components/client/DeliveryStateNotice";
|
||
import { Panel } from "../components/UI/Panel";
|
||
import {
|
||
confirmDeliveryChoice,
|
||
fetchDeliveryInvitation,
|
||
} from "../services/deliveryInvitationApi";
|
||
|
||
export const groupSlotsFromInvitation = (invitation) => {
|
||
if (!invitation) {
|
||
return [];
|
||
}
|
||
|
||
const rawSlots = invitation.availableSlots || [];
|
||
const deliveryDate = invitation.deliveryDate;
|
||
const deliveryTime = invitation.deliveryTime;
|
||
|
||
if (!rawSlots.length && !deliveryDate) {
|
||
return [];
|
||
}
|
||
|
||
if (!rawSlots.length && deliveryDate) {
|
||
return [
|
||
{
|
||
id: `slot-${deliveryDate}-${deliveryTime || "default"}`,
|
||
date: deliveryDate,
|
||
time: deliveryTime || "Половина дня",
|
||
},
|
||
];
|
||
}
|
||
|
||
return rawSlots.map((raw, index) => {
|
||
const parts = raw.split(",");
|
||
const datePart = parts[0]?.trim() || "";
|
||
const timePart = parts.slice(1).join(",").trim() || "";
|
||
|
||
const parsedDate = datePart.replace(/[а-яё]+/gi, "").trim()
|
||
|| deliveryDate
|
||
|| "";
|
||
|
||
return {
|
||
id: `slot-${index}-${raw}`,
|
||
date: deliveryDate || parsedDate,
|
||
time: timePart || deliveryTime || raw,
|
||
};
|
||
});
|
||
};
|
||
|
||
export const buildDeliveryConfirmationPayload = ({
|
||
slot,
|
||
invitation,
|
||
searchDate,
|
||
}) => ({
|
||
deliveryDate: slot?.date || searchDate || invitation?.deliveryDate || undefined,
|
||
deliveryTime: slot?.time || invitation?.deliveryTime || undefined,
|
||
});
|
||
|
||
export const ClientDeliveryPage = () => {
|
||
const { token } = useParams();
|
||
const [searchParams] = useSearchParams();
|
||
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);
|
||
|
||
React.useEffect(() => {
|
||
let cancelled = false;
|
||
|
||
const loadInvitation = async () => {
|
||
if (!token) {
|
||
setLoading(false);
|
||
setError("Не передан токен приглашения.");
|
||
return;
|
||
}
|
||
|
||
setLoading(true);
|
||
setError("");
|
||
|
||
try {
|
||
const loadedInvitation = await fetchDeliveryInvitation(token);
|
||
if (!cancelled) {
|
||
setInvitation(loadedInvitation);
|
||
}
|
||
} catch (fetchError) {
|
||
if (!cancelled) {
|
||
setInvitation(null);
|
||
setError(fetchError instanceof Error ? fetchError.message : "Не удалось загрузить приглашение");
|
||
}
|
||
} finally {
|
||
if (!cancelled) {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
};
|
||
|
||
loadInvitation();
|
||
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [token]);
|
||
|
||
const slots = React.useMemo(
|
||
() => groupSlotsFromInvitation(invitation),
|
||
[invitation],
|
||
);
|
||
|
||
const invitationState = invitation?.state || "awaiting_choice";
|
||
|
||
const handleConfirmChoice = React.useCallback(
|
||
async ({ deliveryDate, deliveryTime }) => {
|
||
if (!token) {
|
||
return;
|
||
}
|
||
|
||
setActionMessage("Сохраняем выбор...");
|
||
|
||
try {
|
||
await confirmDeliveryChoice({
|
||
token,
|
||
deliveryTime,
|
||
deliveryDate,
|
||
});
|
||
const loadedInvitation = await fetchDeliveryInvitation(token);
|
||
setInvitation(loadedInvitation);
|
||
setActionMessage("Выбор сохранен, спасибо.");
|
||
} catch (confirmError) {
|
||
setActionMessage("");
|
||
setError(confirmError instanceof Error ? confirmError.message : "Не удалось сохранить выбор");
|
||
}
|
||
},
|
||
[token, invitation],
|
||
);
|
||
|
||
const handleSlotSelect = React.useCallback(
|
||
(slot) => {
|
||
setSelectedSlotId(slot.id);
|
||
handleConfirmChoice(
|
||
buildDeliveryConfirmationPayload({
|
||
slot,
|
||
invitation,
|
||
searchDate: searchParams.get("date"),
|
||
}),
|
||
);
|
||
},
|
||
[handleConfirmChoice, invitation, searchParams],
|
||
);
|
||
|
||
const handleRequestNewLink = React.useCallback(() => {
|
||
setActionMessage("Если ссылка больше не работает, логист передаст новую ссылку вручную.");
|
||
}, []);
|
||
|
||
if (loading) {
|
||
return (
|
||
<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>
|
||
</Panel>
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|
||
|
||
if (error && !invitation) {
|
||
return (
|
||
<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)]">{error}</p>
|
||
</Panel>
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|
||
|
||
const isActiveState = ["awaiting_choice", "opened", "reminder_sent"].includes(invitationState);
|
||
|
||
return (
|
||
<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)]">
|
||
{isActiveState
|
||
? "Вам предложены варианты доставки. Выберите удобную дату и время."
|
||
: "По этому заказу согласование доставки завершено или передано логисту."}
|
||
</p>
|
||
</Panel>
|
||
|
||
{isActiveState && slots.length ? (
|
||
<DeliverySlotsPicker
|
||
slots={slots}
|
||
onSelectSlot={handleSlotSelect}
|
||
selectedSlotId={selectedSlotId}
|
||
/>
|
||
) : null}
|
||
|
||
{isActiveState ? (
|
||
<DeliveryChoiceFlow
|
||
invitation={invitation}
|
||
onConfirmChoice={handleConfirmChoice}
|
||
onRequestNewLink={handleRequestNewLink}
|
||
/>
|
||
) : (
|
||
<DeliveryStateNotice state={invitationState} />
|
||
)}
|
||
|
||
{actionMessage ? (
|
||
<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}
|
||
</div>
|
||
</main>
|
||
);
|
||
};
|