mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 05:18:11 +00:00
198 lines
6.1 KiB
TypeScript
198 lines
6.1 KiB
TypeScript
import { useMemo } from 'react';
|
|
import type { ReactNode } from 'react';
|
|
import { ActionIcon, Group, Text } from '@mantine/core';
|
|
import {
|
|
Building2,
|
|
Eye,
|
|
MapPin,
|
|
Package,
|
|
Pencil,
|
|
Scale,
|
|
Warehouse as WarehouseIcon,
|
|
} from 'lucide-react';
|
|
import { DataTable, type ColumnDef } from '@edr/ui-common';
|
|
|
|
import { useQuery } from '@tanstack/react-query';
|
|
|
|
import { bookingTable } from '@/components/bookings/booking-ui.styles';
|
|
import { api } from '@/services/api';
|
|
import type { Warehouse } from '@/types/warehouse';
|
|
import { WarehouseStatusBadge, WarehouseTypeBadge } from './badges';
|
|
import { formatCapacity } from './options';
|
|
|
|
interface WarehouseTableProps {
|
|
warehouses: Warehouse[];
|
|
onView: (warehouse: Warehouse) => void;
|
|
onEdit: (warehouse: Warehouse) => void;
|
|
}
|
|
|
|
const HEADER = bookingTable.headerCell;
|
|
|
|
function CapacityCell({
|
|
current,
|
|
capacity,
|
|
icon,
|
|
}: {
|
|
current?: number | null;
|
|
capacity?: number | null;
|
|
icon: ReactNode;
|
|
}) {
|
|
const numericCurrent = Number(current) || 0;
|
|
const numericCapacity = Number(capacity) || 0;
|
|
const hasCapacity = numericCapacity > 0;
|
|
const ratio = hasCapacity ? Math.min(100, Math.max(0, (numericCurrent / numericCapacity) * 100)) : 0;
|
|
const isOverCapacity = hasCapacity && numericCurrent > numericCapacity;
|
|
|
|
return (
|
|
<div className="min-w-[8rem] space-y-2 py-1">
|
|
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
|
<span className="flex size-7 shrink-0 items-center justify-center rounded-lg border border-border/50 bg-background/70 text-muted-foreground">
|
|
{icon}
|
|
</span>
|
|
<span className="whitespace-nowrap">{formatCapacity(numericCurrent, capacity)}</span>
|
|
</div>
|
|
{hasCapacity ? (
|
|
<div className="h-1.5 overflow-hidden rounded-full bg-muted/50">
|
|
<div
|
|
className={isOverCapacity ? 'h-full rounded-full bg-red-500' : 'h-full rounded-full bg-edr-green'}
|
|
style={{ width: `${ratio}%` }}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function WarehouseTable({ warehouses, onView, onEdit }: WarehouseTableProps) {
|
|
const { data: stations } = useQuery(
|
|
api.stations.list.queryOptions({ staleTime: 5 * 60 * 1000 }),
|
|
);
|
|
const stationNameById = useMemo(
|
|
() => new Map((stations ?? []).map((s) => [s.id, s.name])),
|
|
[stations],
|
|
);
|
|
|
|
const columns: ColumnDef<Warehouse>[] = [
|
|
{
|
|
id: 'code',
|
|
header: () => <span className={HEADER}>Warehouse</span>,
|
|
cell: ({ row }) => (
|
|
<div className="flex min-w-[11rem] items-center gap-3 py-1.5">
|
|
<div className={bookingTable.rowIcon}>
|
|
<WarehouseIcon className="size-4" strokeWidth={1.75} />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<button
|
|
type="button"
|
|
className="block max-w-full truncate text-left text-sm font-semibold text-edr-green transition-colors hover:text-edr-green/80"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onView(row.original);
|
|
}}
|
|
>
|
|
{row.original.code}
|
|
</button>
|
|
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
{row.original.name}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'facility',
|
|
header: () => <span className={HEADER}>Facility</span>,
|
|
cell: ({ row }) => {
|
|
const name = row.original.stationId
|
|
? stationNameById.get(row.original.stationId)
|
|
: undefined;
|
|
return name ? (
|
|
<div className="flex min-w-[9rem] items-center gap-2 py-1 text-sm font-medium text-foreground">
|
|
<Building2 className="size-4 shrink-0 text-muted-foreground" />
|
|
<span className="truncate">{name}</span>
|
|
</div>
|
|
) : (
|
|
<Text size="sm" c="dimmed">
|
|
-
|
|
</Text>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'type',
|
|
header: () => <span className={HEADER}>Type</span>,
|
|
cell: ({ row }) => (
|
|
<div className="py-1">
|
|
<WarehouseTypeBadge type={row.original.type} />
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'location',
|
|
header: () => <span className={HEADER}>Location</span>,
|
|
cell: ({ row }) => (
|
|
<div className="flex min-w-[10rem] items-center gap-2 py-1 text-sm text-foreground">
|
|
<MapPin className="size-4 shrink-0 text-muted-foreground" />
|
|
<span className="truncate">{row.original.locationName ?? '-'}</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'weight',
|
|
header: () => <span className={HEADER}>Weight</span>,
|
|
cell: ({ row }) => (
|
|
<CapacityCell
|
|
current={row.original.currentWeight}
|
|
capacity={row.original.capacityWeight}
|
|
icon={<Scale className="size-3.5" />}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
id: 'containers',
|
|
header: () => <span className={HEADER}>Containers</span>,
|
|
cell: ({ row }) => (
|
|
<CapacityCell
|
|
current={row.original.currentContainers}
|
|
capacity={row.original.capacityContainers}
|
|
icon={<Package className="size-3.5" />}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
id: 'status',
|
|
header: () => <span className={HEADER}>Status</span>,
|
|
cell: ({ row }) => (
|
|
<div className="py-1">
|
|
<WarehouseStatusBadge status={row.original.status} />
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'actions',
|
|
header: '',
|
|
cell: ({ row }) => (
|
|
<Group gap="xs" justify="flex-end" wrap="nowrap" onClick={(e) => e.stopPropagation()}>
|
|
<ActionIcon variant="subtle" color="gray" onClick={() => onView(row.original)} title="View">
|
|
<Eye size={16} />
|
|
</ActionIcon>
|
|
<ActionIcon variant="subtle" color="gray" onClick={() => onEdit(row.original)} title="Edit">
|
|
<Pencil size={16} />
|
|
</ActionIcon>
|
|
</Group>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<DataTable
|
|
columns={columns}
|
|
data={warehouses}
|
|
status="success"
|
|
onRowClick={(warehouse) => onView(warehouse)}
|
|
emptyMessage="No warehouses found."
|
|
containerClassName="border-0 bg-transparent shadow-none"
|
|
/>
|
|
);
|
|
}
|