import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Button, Center, Icon, Sheet, Spinner } from '@barev/ui';
import { confirmDialog, dateKeyToDate, errorText, haptic, navigate, nextDays, paramsExtras, useI18n, useLocation, useToast, useTRPC } from '@barev/web';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Stars, Timeline, toneClass, useOrderTimeline, 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 timeline = useOrderTimeline();
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 arrived = o.history.some((h) => h.event === 'arrived');
const extras = paramsExtras(t, o.params);
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 (
#{o.number}
{t(`status.${o.flow}.${o.status}`)}
{arrived && o.status === 'EN_ROUTE' ? ` · ${t('client.detail.arrived')}` : ''}
{t(`services.${o.serviceCode}.name`)}
{summary(o.serviceCode, o.params)}
{extras.length > 0 && ` + ${extras.map((e) => e.label).join(', ')}`}
-
{t('client.detail.when')}
- {dt(o.scheduledAt, { weekday: 'long', day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit' })}
{o.addressText && (
-
{t('client.detail.where')}
-
{o.districtCode ? `${t(`districts.${o.districtCode}`)}, ` : ''}
{o.addressText}
)}
-
{t(`client.detail.providerLabel.${o.flow}`)}
- {o.providerName ?? {t('client.detail.whoPending')}}
{o.done && }
{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)}
)}
{o.pointsUsed > 0 && (
{t('client.points.paidLine')}
−{amd(o.pointsUsed)}
)}
{t('client.order.total')}
{amd(o.totalPrice - o.pointsUsed)}
{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 && o.status !== 'CANCELLED' && (
)}
{o.status === 'CANCELLED' && (
)}
{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 })}
)}
setReschedule(false)} order={o} />
);
}
/** Оценка после выполнения: звёзды и комментарий; уже оценённый заказ показывает оценку с кнопкой «Изменить» */
function RateBlock({ id, rating }: { id: string; rating: { score: number; comment: string | null } | null }) {
const { t } = useI18n();
const trpc = useTRPC();
const qc = useQueryClient();
const toast = useToast();
const { query } = useLocation();
const ref = useRef(null);
const [editing, setEditing] = useState(!rating);
const [score, setScore] = useState(rating?.score ?? 0);
const [comment, setComment] = useState(rating?.comment ?? '');
useEffect(() => {
if (query.get('rate') && ref.current) ref.current.scrollIntoView({ block: 'center' });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const rate = useMutation(
trpc.client.orders.rate.mutationOptions({
onSuccess: () => {
haptic.ok();
toast(t('client.rate.thanks'));
setEditing(false);
void qc.invalidateQueries({ queryKey: trpc.client.orders.pathKey() });
},
onError: (e) => toast(errorText(t, e), 'error'),
}),
);
if (!editing && rating)
return (
{t('client.rate.yours')}
{rating.comment && {rating.comment}
}
);
return (
{t('client.rate.title')}
{t('client.rate.text')}
{score > 0 && (
<>
);
}
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')}
)}
);
}