import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Button, Card, Center, Icon, Sheet, Spinner, Steps } from '@barev/ui';
import { confirmDialog, dateKeyToDate, errorText, haptic, navigate, nextDays, paramsExtras, useI18n, useToast, useTRPC } from '@barev/web';
import { useMemo, useState } from 'react';
import { PageHead, toneClass, useParamsSummary } from '../lib.tsx';
export function OrderDetail({ id }: { id: string }) {
const { t, amd, dt, locale } = useI18n();
const trpc = useTRPC();
const qc = useQueryClient();
const toast = useToast();
const summary = useParamsSummary();
const order = useQuery({ ...trpc.client.orders.get.queryOptions({ id }), refetchInterval: 15_000 });
const preview = useQuery({ ...trpc.client.orders.cancelPreview.queryOptions({ id }), enabled: !!order.data?.active });
const [reschedule, setReschedule] = useState(false);
const cancel = useMutation(
trpc.client.orders.cancel.mutationOptions({
onSuccess: (r) => {
haptic.ok();
toast(r.fee > 0 ? t('client.detail.cancelledWithFee', { fee: amd(r.fee) }) : t('client.detail.cancelled'));
void qc.invalidateQueries();
},
onError: (e) => toast(errorText(t, e), 'error'),
}),
);
if (!order.data)
return (
);
const o = order.data;
// шкала прогресса: пройденные статусы по истории, текущий, остальные
const passed = new Set(o.history.map((h) => h.to));
const currentIdx = o.steps.indexOf(o.status);
const steps =
o.status === 'CANCELLED'
? [...o.steps.filter((s) => passed.has(s)).map((s) => ({ label: t(`status.${o.flow}.${s}`), state: 'done' as const })), { label: t(`status.${o.flow}.CANCELLED`), state: 'current' as const }]
: o.steps.map((s, i) => {
const at = o.history.find((h) => h.to === s && !h.event)?.at ?? o.history.find((h) => h.to === s)?.at;
return {
label: t(`status.${o.flow}.${s}`),
state: i < currentIdx ? ('done' as const) : i === currentIdx ? ('current' as const) : ('todo' as const),
meta: at && i <= currentIdx ? dt(at, { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }) : undefined,
};
});
const arrived = o.history.some((h) => h.event === 'arrived');
async function onCancel() {
const p = preview.data?.policy;
if (!p) return;
const text = p.kind === 'free' ? t('client.detail.confirmCancelFree') : p.kind === 'late' ? t('client.detail.confirmCancelLate', { fee: amd(p.fee), pct: p.feePct }) : '';
if (await confirmDialog(text)) cancel.mutate({ id });
}
return (
{t(`services.${o.serviceCode}.name`)}
{t(`status.${o.flow}.${o.status}`)}
{summary(o.serviceCode, o.params)}
{paramsExtras(t, o.params).length > 0 &&
+ {paramsExtras(t, o.params).map((e) => e.label).join(', ')}
}
{dt(o.scheduledAt, { weekday: 'long', day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit' })}
{o.addressText && (
{o.districtCode ? `${t(`districts.${o.districtCode}`)}, ` : ''}
{o.addressText}
)}
{o.providerName && (
{t(`client.detail.providerLabel.${o.flow}`)}: {o.providerName}
{arrived && o.status === 'EN_ROUTE' && {t('client.detail.arrived')}}
)}
{t('client.detail.progress')}
{o.photos.length > 0 && }
{t('client.order.price')}
{amd(o.basePrice)}
{o.discountAmount > 0 && (
{t('client.order.discount', { pct: o.discountPct })}
−{amd(o.discountAmount)}
)}
{t('client.order.total')}
{amd(o.totalPrice)}
{o.cancelFee > 0 && (
{t('client.detail.cancelFee')}
{amd(o.cancelFee)}
)}
{t(`payment.${o.paymentStatus}`)}
{o.paymentMethod ? ` · ${o.paymentMethod.name[locale] || o.paymentMethod.name.ru}` : ''}
{o.note && (
{t('client.order.note')}
{o.note}
)}
{o.active && preview.data?.canReschedule && (
)}
{o.active && preview.data?.cancellable && preview.data.policy.kind !== 'operator_only' && (
)}
{o.active && preview.data && (!preview.data.cancellable || preview.data.policy.kind === 'operator_only' || !preview.data.canReschedule) && (
{t('client.detail.viaOperator', { hours: preview.data.freeHours })}
)}
{!o.active && o.status !== 'CANCELLED' && (
)}
{o.status === 'CANCELLED' && (
)}
setReschedule(false)} order={o} />
);
}
function PhotoReport({ photos }: { photos: { code: string; fileId: string }[] }) {
const { t } = useI18n();
const trpc = useTRPC();
return (
{t('client.detail.photoReport')}
);
}
function Photo({ fileId, alt, trpc }: { fileId: string; alt: string; trpc: ReturnType }) {
const url = useQuery({ ...trpc.client.fileUrl.queryOptions({ fileId }), staleTime: 4 * 60_000 });
return url.data ? (
) : (
);
}
/** ORD-6: перенос — выбор нового слота */
function RescheduleSheet({ open, onClose, order }: { open: boolean; onClose: () => void; order: { id: string; serviceCode: string; params: Record } }) {
const { t, dt } = useI18n();
const trpc = useTRPC();
const qc = useQueryClient();
const toast = useToast();
const days = useMemo(() => nextDays(14), []);
const [day, setDay] = useState(days[1] ?? days[0]!);
const [slot, setSlot] = useState(null);
const slots = useQuery({
...trpc.client.slots.queryOptions({ serviceCode: order.serviceCode, params: order.params as Record, dateKey: day, excludeOrderId: order.id }),
enabled: open,
});
const mutate = useMutation(
trpc.client.orders.reschedule.mutationOptions({
onSuccess: () => {
toast(t('client.detail.rescheduled'));
void qc.invalidateQueries();
onClose();
},
onError: (e) => toast(errorText(t, e), 'error'),
}),
);
return (
{t('client.detail.reschedule')}
{days.map((d) => (
))}
{slots.isPending ? (
) : slots.data?.length ? (
{slots.data.map((s) => (
))}
) : (
{t('client.order.noSlots')}
)}
);
}