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

{photos.map((p) => ( ))}
); } 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 ? ( {alt} ) : (
); } /** 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')}

)}
); }