supersam/src/pages/ClientDeliveryPage.jsx

227 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
};