102 lines
3.6 KiB
TypeScript
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>
|
|
);
|
|
}
|