Files
daily_console_web/src/features/store-admin/DeliveryProgress.tsx
2026-09-03 19:13:04 +05:30

102 lines
3.6 KiB
TypeScript

import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Text } from '@astryxdesign/core/Text';
import { VStack } from '@astryxdesign/core/VStack';
import { errorMessage } from '@/api/client';
import { deliveriesApi } from '@/api/deliveries';
import type { DeliveryRow } from '@/api/types';
import { queryKeys } from '@/queries/keys';
import { stampNow } from './assignDelivery';
/**
* Moving a delivery along from the back office.
*
* The rider's app drives this ladder in the ordinary case. This is for when it
* cannot: a rider whose phone died, a job cancelled by phone, a delivery that
* was completed but never marked. Without it the only way to correct a stuck
* job is a database write.
*
* ── Two things the backend does that shape this ─────────────────────────────
*
* `updatedelivery` mirrors the status onto the parent order for `pending`,
* `delivered` and `cancelled` — but NOT for `picked`, which updates the
* delivery and leaves the order where it was. So the order and its delivery
* legitimately disagree in the middle of a journey, and this does not try to
* paper over that.
*
* The lifecycle timestamp is sent explicitly. `Updates()` skips zero-valued
* struct fields, so a status sent without its stamp moved the status and left
* the column blank — which is how a delivery ends up marked delivered with no
* delivery time against it.
*/
/** What can be set from here, in the order a job actually walks. */
const MOVES = [
{ to: 'picked', label: 'Picked up', stamp: 'pickuptime' },
{ to: 'delivered', label: 'Delivered', stamp: 'deliverytime' },
{ to: 'cancelled', label: 'Cancelled', stamp: 'canceltime' },
] as const;
/** The end states. Nothing follows them, so nothing is offered. */
const SETTLED = ['delivered', 'cancelled'];
export function DeliveryProgress({ job }: { job: DeliveryRow }) {
const client = useQueryClient();
const [problem, setProblem] = useState<string | null>(null);
const status = (job.orderstatus ?? '').trim().toLowerCase();
const isSettled = SETTLED.includes(status);
const move = useMutation({
mutationFn: ({ to, stamp }: { to: string; stamp: string }) =>
deliveriesApi.update({
deliveryid: job.deliveryid,
orderheaderid: job.orderheaderid ?? 0,
orderstatus: to,
[stamp]: stampNow(),
}),
onSuccess: () => client.invalidateQueries({ queryKey: queryKeys.insights.all }),
onError: (error) => setProblem(errorMessage(error)),
});
if (isSettled) {
return (
<Text type="body" size="xsm" color="secondary">
This job is {status}. Nothing further to record.
</Text>
);
}
return (
<VStack gap={1}>
<div className="progress-moves">
{MOVES.map((step) => (
<button
key={step.to}
type="button"
className="progress-move"
data-danger={step.to === 'cancelled'}
disabled={move.isPending || step.to === status}
onClick={() => {
setProblem(null);
move.mutate({ to: step.to, stamp: step.stamp });
}}
>
{step.label}
</button>
))}
</div>
{problem ? (
<Text type="body" size="xsm" role="alert" style={{ color: 'var(--color-error, #d64545)' }}>
{problem}
</Text>
) : (
<Text type="body" size="xsm" color="secondary">
Use these only when the rider's app has not. Marking a job delivered here also closes the
order.
</Text>
)}
</VStack>
);
}