103 lines
3.7 KiB
JavaScript
103 lines
3.7 KiB
JavaScript
import React from 'react';
|
|
import { useMutation } from '@tanstack/react-query';
|
|
import { Gauge, RefreshCw } from 'lucide-react';
|
|
import { Button, EmptyState, SectionHeader, Stack, Surface } from '@/components/ds';
|
|
import { fetchBatchEfficiency } from '@/api/doormile';
|
|
import { messageOf } from '@/api/doormile/notify';
|
|
import { number } from '@/lib/doormileFormat';
|
|
|
|
/**
|
|
* The solver's own read on how well a wave was packed.
|
|
*
|
|
* This calls the dispatch optimiser, not the Doormile API, and its response has
|
|
* no agreed schema — so nothing here assumes a field name. Whatever the service
|
|
* returns is rendered: numbers get formatted, nested objects are shown as JSON,
|
|
* and a shape nobody has seen before is still readable rather than blank.
|
|
*
|
|
* It is behind a button rather than fetched with the board because it is an
|
|
* analysis an operator asks for, and running it automatically would put a call
|
|
* to an external service on every date and wave change.
|
|
*/
|
|
|
|
const titleise = (key) =>
|
|
String(key)
|
|
.replace(/[_-]+/g, ' ')
|
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
|
|
const renderValue = (value) => {
|
|
if (value === null || value === undefined || value === '') return '—';
|
|
if (typeof value === 'number') return number(value);
|
|
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
|
|
if (typeof value === 'object') return JSON.stringify(value);
|
|
return String(value);
|
|
};
|
|
|
|
/** @param {any} props */
|
|
export function BatchEfficiency({ batch, orders = [], tenantId }) {
|
|
const analyse = useMutation({
|
|
mutationFn: () => fetchBatchEfficiency({ batch, tenantId }),
|
|
});
|
|
|
|
const result = analyse.data?.data ?? analyse.data;
|
|
const entries =
|
|
result && typeof result === 'object' && !Array.isArray(result) ? Object.entries(result) : [];
|
|
|
|
return (
|
|
<Surface radius="xl" padding="lg">
|
|
<SectionHeader
|
|
title="Batch efficiency"
|
|
subtitle="How well the optimiser packed this wave. Runs against the solver, on request."
|
|
icon={Gauge}
|
|
actions={
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => analyse.mutate()}
|
|
loading={analyse.isPending}
|
|
disabled={orders.length === 0}
|
|
>
|
|
<RefreshCw className="mr-1.5 h-3.5 w-3.5" /> Analyse
|
|
</Button>
|
|
}
|
|
className="mb-4"
|
|
/>
|
|
|
|
{orders.length === 0 ? (
|
|
<EmptyState
|
|
icon={Gauge}
|
|
size="sm"
|
|
title="Nothing in this wave"
|
|
description="Pick a wave with orders in it to analyse."
|
|
/>
|
|
) : analyse.isError ? (
|
|
<p className="text-body-sm text-destructive">
|
|
{messageOf(analyse.error, 'The solver did not answer')}
|
|
</p>
|
|
) : !analyse.data ? (
|
|
<p className="text-body-sm text-ink-3">
|
|
{orders.length} order{orders.length === 1 ? '' : 's'} in this wave. Run the analysis to see how the solver
|
|
rates it.
|
|
</p>
|
|
) : entries.length === 0 ? (
|
|
/* A response that is not a flat object — an array, a scalar, an error
|
|
envelope. Shown raw rather than dropped. */
|
|
<pre className="overflow-x-auto rounded-lg bg-surface-sunken p-3 text-caption text-ink-2">
|
|
{JSON.stringify(result, null, 2)}
|
|
</pre>
|
|
) : (
|
|
<Stack space="xs">
|
|
{entries.map(([key, value]) => (
|
|
<div
|
|
key={key}
|
|
className="flex items-center justify-between gap-4 rounded-lg bg-surface-subtle px-3 py-2"
|
|
>
|
|
<span className="text-caption uppercase tracking-wide text-ink-3">{titleise(key)}</span>
|
|
<span className="text-body-sm font-semibold text-ink-1">{renderValue(value)}</span>
|
|
</div>
|
|
))}
|
|
</Stack>
|
|
)}
|
|
</Surface>
|
|
);
|
|
}
|