Files
doormilxpress_astryx/src/components/doormile/BatchEfficiency.jsx

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