merge conflict fix

This commit is contained in:
hagiye
2026-06-24 12:31:59 +03:00
494 changed files with 21885 additions and 14336 deletions

View File

@@ -9,7 +9,9 @@ import {
Warehouse,
} from 'lucide-react';
import { useInventoryActivity } from '@/hooks/useWarehouses';
import { useQuery } from '@tanstack/react-query';
import { api } from '@/services/api';
import type { ActivityType } from '@/types/warehouse';
import { formatDate, humanizeEnum } from './options';
@@ -24,7 +26,12 @@ const activityIcon: Record<ActivityType, React.ReactNode> = {
};
export function ActivityTimeline({ inventoryId }: { inventoryId: string }) {
const { data, isLoading } = useInventoryActivity(inventoryId);
const { data, isLoading } = useQuery(
api.warehouses.activity.queryOptions({
input: { id: inventoryId },
enabled: Boolean(inventoryId),
}),
);
const items = data ?? [];
if (isLoading) {

View File

@@ -9,9 +9,10 @@ import {
TextInput,
} from '@mantine/core';
import { useMutation, useQuery } from '@tanstack/react-query';
import { api } from '@/services/api';
import { useToast } from '@/hooks/use-toast';
import { useStations } from '@/hooks/useStations';
import { useCreateWarehouse, useUpdateWarehouse } from '@/hooks/useWarehouses';
import type { SaveWarehousePayload, Warehouse, WarehouseType } from '@/types/warehouse';
import { extractErrorMessage, statusOptions, warehouseTypeOptions } from './options';
@@ -48,9 +49,11 @@ const emptyForm = (): FormState => ({
export function CreateWarehouseModal({ opened, onClose, warehouse }: CreateWarehouseModalProps) {
const isEdit = Boolean(warehouse);
const { toast } = useToast();
const createMutation = useCreateWarehouse();
const updateMutation = useUpdateWarehouse();
const { data: stations } = useStations();
const createMutation = useMutation(api.warehouses.create.mutationOptions());
const updateMutation = useMutation(api.warehouses.update.mutationOptions());
const { data: stations } = useQuery(
api.stations.list.queryOptions({ staleTime: 5 * 60 * 1000 }),
);
const [form, setForm] = useState<FormState>(emptyForm());
const stationOptions = (stations ?? []).map((s) => ({ value: s.id, label: `${s.name} (${s.code})` }));

View File

@@ -1,8 +1,10 @@
import { useEffect, useState } from 'react';
import { Button, Group, Modal, NumberInput, Select, Stack, TextInput } from '@mantine/core';
import { useMutation } from '@tanstack/react-query';
import { api } from '@/services/api';
import { useToast } from '@/hooks/use-toast';
import { useCreateYard, useUpdateYard } from '@/hooks/useWarehouses';
import type { SaveYardPayload, WarehouseYard, WarehouseYardType } from '@/types/warehouse';
import { extractErrorMessage, statusOptions, yardTypeOptions } from './options';
@@ -36,8 +38,8 @@ const emptyForm = (): FormState => ({
export function CreateYardModal({ opened, onClose, warehouseId, yard }: CreateYardModalProps) {
const isEdit = Boolean(yard);
const { toast } = useToast();
const createMutation = useCreateYard();
const updateMutation = useUpdateYard();
const createMutation = useMutation(api.warehouses.createYard.mutationOptions());
const updateMutation = useMutation(api.warehouses.updateYard.mutationOptions());
const [form, setForm] = useState<FormState>(emptyForm());
useEffect(() => {

View File

@@ -1,8 +1,10 @@
import { useEffect, useState } from 'react';
import { Button, Group, Modal, NumberInput, Select, Stack, TextInput } from '@mantine/core';
import { useMutation } from '@tanstack/react-query';
import { api } from '@/services/api';
import { useToast } from '@/hooks/use-toast';
import { useCreateZone, useUpdateZone } from '@/hooks/useWarehouses';
import type { SaveZonePayload, WarehouseZone, WarehouseZoneType } from '@/types/warehouse';
import { extractErrorMessage, statusOptions, zoneTypeOptions } from './options';
@@ -36,8 +38,8 @@ const emptyForm = (): FormState => ({
export function CreateZoneModal({ opened, onClose, yardId, zone }: CreateZoneModalProps) {
const isEdit = Boolean(zone);
const { toast } = useToast();
const createMutation = useCreateZone();
const updateMutation = useUpdateZone();
const createMutation = useMutation(api.warehouses.createZone.mutationOptions());
const updateMutation = useMutation(api.warehouses.updateZone.mutationOptions());
const [form, setForm] = useState<FormState>(emptyForm());
useEffect(() => {

View File

@@ -2,8 +2,10 @@ import { useEffect, useState } from 'react';
import { Alert, Button, Group, Modal, Stack, Text, Textarea, TextInput } from '@mantine/core';
import { Info } from 'lucide-react';
import { useMutation } from '@tanstack/react-query';
import { api } from '@/services/api';
import { useToast } from '@/hooks/use-toast';
import { useDeliverInventory } from '@/hooks/useWarehouses';
import type { WarehouseInventoryItem } from '@/types/warehouse';
import { extractErrorMessage } from './options';
@@ -15,7 +17,7 @@ interface DeliverInventoryModalProps {
export function DeliverInventoryModal({ opened, onClose, item }: DeliverInventoryModalProps) {
const { toast } = useToast();
const deliverMutation = useDeliverInventory();
const deliverMutation = useMutation(api.warehouses.deliver.mutationOptions());
const [receiverName, setReceiverName] = useState('');
const [remarks, setRemarks] = useState('');

View File

@@ -1,6 +1,9 @@
import { Badge, Button, Card, Divider, Group, Loader, Modal, Stack, Text } from '@mantine/core';
import { CalendarClock, Coins, DoorOpen, FileText } from 'lucide-react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { api } from '@/services/api';
import { useToast } from '@/hooks/use-toast';
import {
useFeePreview,
@@ -17,7 +20,7 @@ const INVOICE_STATUS_COLOR: Record<WarehouseInvoiceStatus, string> = {
DRAFT: 'gray',
ISSUED: 'orange',
PARTIALLY_PAID: 'yellow',
PAID: 'green',
PAID: 'edr-green',
CANCELLED: 'gray',
};
@@ -93,10 +96,20 @@ function Row({ label, value }: { label: string; value: string }) {
export function FeePreviewModal({ opened, onClose, inventoryId }: FeePreviewModalProps) {
const { toast } = useToast();
const enabledId = opened ? inventoryId ?? undefined : undefined;
const { data, isLoading } = useFeePreview(enabledId);
const { data: invoices } = useInvoicesForInventory(enabledId);
const generate = useGenerateInvoice();
const gateClear = useGateClearance();
const { data, isLoading } = useQuery(
api.warehouses.feePreview.queryOptions({
input: { inventoryId: enabledId ?? '' },
enabled: Boolean(enabledId),
}),
);
const { data: invoices } = useQuery(
api.warehouses.invoicesForInventory.queryOptions({
input: { inventoryId: enabledId ?? '' },
enabled: Boolean(enabledId),
}),
);
const generate = useMutation(api.warehouses.generateInvoice.mutationOptions());
const gateClear = useMutation(api.warehouses.gateClearance.mutationOptions());
const activeInvoice = (invoices ?? []).find((i) => i.status !== 'CANCELLED');
@@ -191,7 +204,7 @@ export function FeePreviewModal({ opened, onClose, inventoryId }: FeePreviewModa
<Button
variant="light"
color="green"
color="edr-green"
leftSection={<DoorOpen size={16} />}
loading={gateClear.isPending}
onClick={handleGateClearance}

View File

@@ -12,6 +12,9 @@ import {
} from '@mantine/core';
import { Upload } from 'lucide-react';
import { useMutation } from '@tanstack/react-query';
import { api } from '@/services/api';
import { useToast } from '@/hooks/use-toast';
import { useCreateInspectionReport, useInspectionReports, useUploadInspectionAttachments } from '@/hooks/useWarehouses';
import {
@@ -227,7 +230,7 @@ export function InspectionReportModal({ opened, onClose, inventoryId }: Inspecti
<Button variant="default" onClick={onClose} disabled={submitting}>
Cancel
</Button>
<Button color="green" onClick={handleSubmit} loading={submitting} disabled={!inventoryId}>
<Button color="edr-green" onClick={handleSubmit} loading={submitting} disabled={!inventoryId}>
Save report
</Button>
</Group>

View File

@@ -1,12 +1,19 @@
import { Center, Loader, Table, Text } from '@mantine/core';
import { useInventoryMovements } from '@/hooks/useWarehouses';
import { useQuery } from '@tanstack/react-query';
import { api } from '@/services/api';
import { formatDate } from './options';
const shortId = (id?: string | null) => (id ? `${id.slice(0, 8)}` : '—');
export function InventoryMovementHistoryTable({ inventoryId }: { inventoryId: string }) {
const { data, isLoading } = useInventoryMovements(inventoryId);
const { data, isLoading } = useQuery(
api.warehouses.movements.queryOptions({
input: { id: inventoryId },
enabled: Boolean(inventoryId),
}),
);
const movements = data ?? [];
if (isLoading) {

View File

@@ -2,6 +2,9 @@ import { useState } from 'react';
import { Button, Center, Group, Loader, Stack, Text } from '@mantine/core';
import { ClipboardCheck } from 'lucide-react';
import { useMutation } from '@tanstack/react-query';
import { api } from '@/services/api';
import { useToast } from '@/hooks/use-toast';
import {
useBulkMarkInspected,
@@ -46,11 +49,17 @@ export function InventoryWorkbench({ items, isLoading, onLastMile }: InventoryWo
const [releaseItem, setReleaseItem] = useState<WarehouseInventoryItem | null>(null);
const [deliverItem, setDeliverItem] = useState<WarehouseInventoryItem | null>(null);
const storeMutation = useStoreInventory();
const readyMutation = useMarkReadyForLoading();
const pickupMutation = useMarkReadyForPickup();
const dispatchMutation = useDispatchInventory();
const inspectMutation = useBulkMarkInspected();
const storeMutation = useMutation(api.warehouses.store.mutationOptions());
const readyMutation = useMutation(
api.warehouses.markReadyForLoading.mutationOptions(),
);
const pickupMutation = useMutation(
api.warehouses.markReadyForPickup.mutationOptions(),
);
const dispatchMutation = useMutation(api.warehouses.dispatch.mutationOptions());
const inspectMutation = useMutation(
api.warehouses.bulkMarkInspected.mutationOptions(),
);
const [selected, setSelected] = useState<Set<string>>(new Set());
const allSelected = items.length > 0 && selected.size === items.length;
@@ -70,10 +79,7 @@ export function InventoryWorkbench({ items, isLoading, onLastMile }: InventoryWo
return;
}
try {
const res = (await inspectMutation.mutateAsync({ inventoryIds: [...selected] })) as {
data: { inspectedCount: number; skippedCount: number };
};
const r = res.data;
const r = await inspectMutation.mutateAsync({ inventoryIds: [...selected] });
toast({
title: `${r.inspectedCount} marked inspected`,
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,

View File

@@ -2,8 +2,10 @@ import { useEffect, useState } from 'react';
import { Alert, Button, Group, Modal, NumberInput, Stack, Text, Textarea } from '@mantine/core';
import { Info } from 'lucide-react';
import { useMutation } from '@tanstack/react-query';
import { api } from '@/services/api';
import { useToast } from '@/hooks/use-toast';
import { useLoadInventory } from '@/hooks/useWarehouses';
import type { WarehouseInventoryItem } from '@/types/warehouse';
import { WagonSelect } from './WagonSelect';
import { extractErrorMessage } from './options';
@@ -17,7 +19,7 @@ interface LoadInventoryModalProps {
/** Load READY_FOR_LOADING inventory onto a wagon (creates a loading record). */
export function LoadInventoryModal({ opened, onClose, item }: LoadInventoryModalProps) {
const { toast } = useToast();
const loadMutation = useLoadInventory();
const loadMutation = useMutation(api.warehouses.load.mutationOptions());
const [wagonId, setWagonId] = useState('');
const [loadedWeight, setLoadedWeight] = useState<number | ''>('');
const [notes, setNotes] = useState('');

View File

@@ -1,8 +1,10 @@
import { useEffect, useMemo, useState } from 'react';
import { Button, Group, Modal, Select, Stack, Textarea } from '@mantine/core';
import { useMutation, useQuery } from '@tanstack/react-query';
import { api } from '@/services/api';
import { useToast } from '@/hooks/use-toast';
import { useMoveInventory, useWarehouseYards, useWarehouseZones, useWarehouses } from '@/hooks/useWarehouses';
import type { WarehouseInventoryItem } from '@/types/warehouse';
import { extractErrorMessage } from './options';
@@ -14,7 +16,7 @@ interface MoveInventoryModalProps {
export function MoveInventoryModal({ opened, onClose, item }: MoveInventoryModalProps) {
const { toast } = useToast();
const moveMutation = useMoveInventory();
const moveMutation = useMutation(api.warehouses.move.mutationOptions());
const [warehouseId, setWarehouseId] = useState('');
const [yardId, setYardId] = useState('');
const [zoneId, setZoneId] = useState('');
@@ -29,9 +31,21 @@ export function MoveInventoryModal({ opened, onClose, item }: MoveInventoryModal
}
}, [opened]);
const warehousesQuery = useWarehouses({ status: 'ACTIVE' });
const yardsQuery = useWarehouseYards(warehouseId || undefined);
const zonesQuery = useWarehouseZones(yardId || undefined);
const warehousesQuery = useQuery(
api.warehouses.list.queryOptions({ input: { filter: { status: 'ACTIVE' } } }),
);
const yardsQuery = useQuery(
api.warehouses.listYards.queryOptions({
input: { warehouseId },
enabled: Boolean(warehouseId),
}),
);
const zonesQuery = useQuery(
api.warehouses.listZones.queryOptions({
input: { yardId },
enabled: Boolean(yardId),
}),
);
const warehouseOptions = useMemo(
() => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })),

View File

@@ -18,34 +18,14 @@ import {
} from '@mantine/core';
import { ChevronDown, ChevronRight, ClipboardCheck, Info, PackageSearch, Train, Truck } from 'lucide-react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { api } from '@/services/api';
import { useToast } from '@/hooks/use-toast';
import {
useAutoUnloadArrivedBookings,
useBulkDispatchExport,
useBulkMarkInspected,
useBulkReceive,
useEligibleBookings,
useImportArriveQueue,
useImportTrainItems,
useImportUnloadedQueue,
useLoadPassedExport,
useLoadedExport,
useReadyToLoadExport,
useReceiveInventory,
useWarehouseInventory,
useWarehouseYards,
useWarehouseZones,
useWarehouses,
} from '@/hooks/useWarehouses';
import type {
AutoUnloadArrivedResult,
BulkDispatchResult,
BulkInspectResult,
BulkReceiveResult,
ImportTrain,
ImportTrainItem,
ImportUnloadedItem,
LoadPassedExportResult,
ReadyToLoadRow,
ReceiveInventoryPayload,
} from '@/types/warehouse';
@@ -77,9 +57,21 @@ function LocationSelects({
value: Location;
onChange: (next: Location) => void;
}) {
const warehousesQuery = useWarehouses({ status: 'ACTIVE' });
const yardsQuery = useWarehouseYards(value.warehouseId || undefined);
const zonesQuery = useWarehouseZones(value.yardId || undefined);
const warehousesQuery = useQuery(
api.warehouses.list.queryOptions({ input: { filter: { status: 'ACTIVE' } } }),
);
const yardsQuery = useQuery(
api.warehouses.listYards.queryOptions({
input: { warehouseId: value.warehouseId ?? '' },
enabled: Boolean(value.warehouseId),
}),
);
const zonesQuery = useQuery(
api.warehouses.listZones.queryOptions({
input: { yardId: value.yardId ?? '' },
enabled: Boolean(value.yardId),
}),
);
const warehouseOptions = useMemo(
() => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })),
@@ -148,10 +140,12 @@ function EligibleTab({
onChanged?: () => void;
}) {
const { toast } = useToast();
const { data: allRows = [], isLoading } = useEligibleBookings(enabled);
const { data: allRows = [], isLoading } = useQuery(
api.warehouses.eligibleBookings.queryOptions({ enabled }),
);
const rows = useMemo(() => allRows.filter((r) => r.direction === direction), [allRows, direction]);
const bulkReceive = useBulkReceive();
const loadPassed = useLoadPassedExport();
const bulkReceive = useMutation(api.warehouses.bulkReceive.mutationOptions());
const loadPassed = useMutation(api.warehouses.loadPassedExport.mutationOptions());
const [selected, setSelected] = useState<Set<string>>(new Set());
const locationReady = Boolean(location.warehouseId && location.yardId && location.zoneId);
@@ -177,10 +171,7 @@ function EligibleTab({
return;
}
try {
const res = (await bulkReceive.mutateAsync({ direction, ...location, bookingIds })) as {
data: BulkReceiveResult;
};
const r = res.data;
const r = await bulkReceive.mutateAsync({ direction, ...location, bookingIds });
toast({
title: `${r.receivedCount} received at ${direction === 'EXPORT' ? 'facility' : 'warehouse'}`,
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
@@ -194,8 +185,7 @@ function EligibleTab({
const loadPassedExport = async () => {
try {
const res = (await loadPassed.mutateAsync(undefined)) as { data: LoadPassedExportResult };
const r = res.data;
const r = await loadPassed.mutateAsync(undefined);
toast({
title: `${r.loadedCount} loaded`,
description: r.skippedCount ? `${r.skippedCount} skipped — inspection not passed` : undefined,
@@ -353,8 +343,10 @@ function EligibleTab({
/** Export items that passed inspection and are queued to be loaded onto a train. */
function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: () => void }) {
const { toast } = useToast();
const { data: rows = [], isLoading } = useReadyToLoadExport(enabled);
const loadPassed = useLoadPassedExport();
const { data: rows = [], isLoading } = useQuery(
api.warehouses.readyToLoadExport.queryOptions({ enabled }),
);
const loadPassed = useMutation(api.warehouses.loadPassedExport.mutationOptions());
const [selected, setSelected] = useState<Set<string>>(new Set());
const allSelected = rows.length > 0 && selected.size === rows.length;
@@ -369,8 +361,7 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
const autoLoad = async () => {
try {
const res = (await loadPassed.mutateAsync(undefined)) as { data: LoadPassedExportResult };
const r = res.data;
const r = await loadPassed.mutateAsync(undefined);
toast({
title: `${r.loadedCount} items loaded`,
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
@@ -494,8 +485,12 @@ function LoadedExportTab({
onChanged?: () => void;
}) {
const { toast } = useToast();
const { data: rows = [], isLoading } = useLoadedExport(enabled);
const bulkDispatch = useBulkDispatchExport();
const { data: rows = [], isLoading } = useQuery(
api.warehouses.loadedExport.queryOptions({ enabled }),
);
const bulkDispatch = useMutation(
api.warehouses.bulkDispatchExport.mutationOptions(),
);
const [selected, setSelected] = useState<Set<string>>(new Set());
const allSelected = rows.length > 0 && selected.size === rows.length;
@@ -514,8 +509,7 @@ function LoadedExportTab({
return;
}
try {
const res = (await bulkDispatch.mutateAsync(inventoryIds)) as { data: BulkDispatchResult };
const r = res.data;
const r = await bulkDispatch.mutateAsync(inventoryIds);
toast({
title: `${r.dispatchedCount} dispatched`,
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
@@ -659,7 +653,12 @@ function LoadedExportTab({
/** Assigned bookings/items for an arrived import train (read-only detail view). */
function ImportTrainDetailTable({ scheduleId }: { scheduleId: string }) {
const { data: items = [], isLoading } = useImportTrainItems(scheduleId);
const { data: items = [], isLoading } = useQuery(
api.warehouses.importTrainItems.queryOptions({
input: { scheduleId },
enabled: Boolean(scheduleId),
}),
);
if (isLoading) {
return (
@@ -735,18 +734,19 @@ function ImportArriveQueueTab({
onChanged?: () => void;
}) {
const { toast } = useToast();
const { data: trains = [], isLoading } = useImportArriveQueue(enabled);
const autoUnloadMutation = useAutoUnloadArrivedBookings();
const { data: trains = [], isLoading } = useQuery(
api.warehouses.importArriveQueue.queryOptions({ enabled }),
);
const autoUnloadMutation = useMutation(
api.warehouses.autoUnloadArrivedBookings.mutationOptions(),
);
const [openId, setOpenId] = useState<string | null>(null);
const [busyId, setBusyId] = useState<string | null>(null);
const autoUnload = async (train: ImportTrain) => {
setBusyId(train.scheduleId);
try {
const res = (await autoUnloadMutation.mutateAsync(train.scheduleId)) as {
data: AutoUnloadArrivedResult;
};
const r = res.data;
const r = await autoUnloadMutation.mutateAsync(train.scheduleId);
const extra = [
r.skippedCount ? `${r.skippedCount} skipped` : '',
r.failedCount ? `${r.failedCount} failed` : '',
@@ -866,8 +866,12 @@ function ImportArriveQueueTab({
*/
function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
const { toast } = useToast();
const { data: rows = [], isLoading } = useImportUnloadedQueue(enabled);
const inspectMutation = useBulkMarkInspected();
const { data: rows = [], isLoading } = useQuery(
api.warehouses.importUnloadedQueue.queryOptions({ enabled }),
);
const inspectMutation = useMutation(
api.warehouses.bulkMarkInspected.mutationOptions(),
);
const [selected, setSelected] = useState<Set<string>>(new Set());
const [inspectId, setInspectId] = useState<string | null>(null);
@@ -888,10 +892,7 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
return;
}
try {
const res = (await inspectMutation.mutateAsync({ inventoryIds: [...selected] })) as {
data: BulkInspectResult;
};
const r = res.data;
const r = await inspectMutation.mutateAsync({ inventoryIds: [...selected] });
toast({
title: `${r.inspectedCount} marked inspected`,
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
@@ -1033,8 +1034,10 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
*/
function ImportDispatchQueueTab({ enabled }: { enabled: boolean }) {
const { toast } = useToast();
const { data: items = [], isLoading } = useWarehouseInventory(
enabled ? { status: 'READY_FOR_PICKUP' } : undefined,
const { data: items = [], isLoading } = useQuery(
api.warehouses.listInventory.queryOptions({
input: { filter: enabled ? { status: 'READY_FOR_PICKUP' } : undefined },
}),
);
return (
@@ -1155,7 +1158,9 @@ function SingleBookingReceiveModal({
onReceived,
}: ReceiveInventoryModalProps) {
const { toast } = useToast();
const receiveMutation = useReceiveInventory();
const receiveMutation = useMutation(
api.warehouses.receiveInventory.mutationOptions(),
);
const [selectedBooking, setSelectedBooking] = useState(bookingId ?? '');
const [form, setForm] = useState<SingleFormState>({
warehouseId: '',

View File

@@ -2,6 +2,9 @@ import { useEffect, useState } from 'react';
import { Alert, Button, Group, Modal, Stack, Text, TextInput } from '@mantine/core';
import { Info } from 'lucide-react';
import { useMutation } from '@tanstack/react-query';
import { api } from '@/services/api';
import { useToast } from '@/hooks/use-toast';
import { useReleaseInventory } from '@/hooks/useWarehouses';
import { warehouseService } from '@/services/warehouse.service';
@@ -17,7 +20,7 @@ interface ReleaseOrderModalProps {
export function ReleaseOrderModal({ opened, onClose, item }: ReleaseOrderModalProps) {
const { toast } = useToast();
const releaseMutation = useReleaseInventory();
const releaseMutation = useMutation(api.warehouses.release.mutationOptions());
const [reference, setReference] = useState('');
const [downloading, setDownloading] = useState(false);

View File

@@ -2,8 +2,10 @@ import { useEffect, useState } from 'react';
import { Alert, Button, Group, Modal, Stack, Text } from '@mantine/core';
import { Info } from 'lucide-react';
import { useMutation } from '@tanstack/react-query';
import { api } from '@/services/api';
import { useToast } from '@/hooks/use-toast';
import { useReserveInventory } from '@/hooks/useWarehouses';
import type { WarehouseInventoryItem } from '@/types/warehouse';
import { BookingSelect } from './BookingSelect';
import { extractErrorMessage } from './options';
@@ -16,7 +18,7 @@ interface ReserveInventoryModalProps {
export function ReserveInventoryModal({ opened, onClose, item }: ReserveInventoryModalProps) {
const { toast } = useToast();
const reserveMutation = useReserveInventory();
const reserveMutation = useMutation(api.warehouses.reserve.mutationOptions());
const [bookingId, setBookingId] = useState('');
useEffect(() => {

View File

@@ -1,6 +1,7 @@
import { Select } from '@mantine/core';
import { useQuery } from '@tanstack/react-query';
import { useLoadableWagons } from '@/hooks/useWarehouses';
import { api } from '@/services/api';
interface WagonSelectProps {
value: string;
@@ -11,7 +12,9 @@ interface WagonSelectProps {
/** Searchable wagon picker. Lists wagons that are loadable (read-only from scheduling). */
export function WagonSelect({ value, onChange, label = 'Wagon', required }: WagonSelectProps) {
const { data, isLoading } = useLoadableWagons();
const { data, isLoading } = useQuery(
api.warehouses.loadableWagons.queryOptions(),
);
const options = (data ?? []).map((w) => ({
value: w.id,

View File

@@ -2,7 +2,9 @@ import { useMemo } from 'react';
import { ActionIcon, Box, Card, Divider, Group, Progress, SimpleGrid, Stack, Text, Tooltip } from '@mantine/core';
import { Building2, Eye, MapPin, Package, Pencil, Weight } from 'lucide-react';
import { useStations } from '@/hooks/useStations';
import { useQuery } from '@tanstack/react-query';
import { api } from '@/services/api';
import type { Warehouse } from '@/types/warehouse';
import { WarehouseStatusBadge, WarehouseTypeBadge } from './badges';
import { formatCapacity } from './options';
@@ -14,7 +16,9 @@ interface WarehouseCardViewProps {
}
export function WarehouseCardView({ warehouses, onView, onEdit }: WarehouseCardViewProps) {
const { data: stations } = useStations();
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],

View File

@@ -15,7 +15,9 @@ import {
YAxis,
} from 'recharts';
import { useWarehouseInventory } from '@/hooks/useWarehouses';
import { useQuery } from '@tanstack/react-query';
import { api } from '@/services/api';
import type { WarehouseDashboard, WarehouseInventoryItem } from '@/types/warehouse';
interface WarehouseDashboardChartsProps {
@@ -38,7 +40,9 @@ type Granularity = 'week' | 'month' | 'year';
export function WarehouseDashboardCharts({ data }: WarehouseDashboardChartsProps) {
const [granularity, setGranularity] = useState<Granularity>('month');
const { data: inventory } = useWarehouseInventory();
const { data: inventory } = useQuery(
api.warehouses.listInventory.queryOptions({ input: {} }),
);
const statusData = STATUS_SERIES.map((s) => ({
name: s.label,

View File

@@ -2,7 +2,9 @@ import { useState } from 'react';
import { Badge, Button, Card, Divider, Group, Stack, Text } from '@mantine/core';
import { PackagePlus, Train as TrainIcon, Warehouse as WarehouseIcon } from 'lucide-react';
import { useBookingSchedule, useWarehouseInventory } from '@/hooks/useWarehouses';
import { useQuery } from '@tanstack/react-query';
import { api } from '@/services/api';
import { InventoryStatusBadge } from './badges';
import { FreightVisual } from './FreightVisual';
import { formatDate } from './options';
@@ -28,8 +30,15 @@ function Row({ label, value }: { label: string; value: React.ReactNode }) {
export function WarehouseInfoCard({ bookingId, bookingReference }: WarehouseInfoCardProps) {
const [modalOpen, setModalOpen] = useState(false);
const { data, isLoading } = useWarehouseInventory({ bookingId });
const { data: scheduleView } = useBookingSchedule(bookingId);
const { data, isLoading } = useQuery(
api.warehouses.listInventory.queryOptions({ input: { filter: { bookingId } } }),
);
const { data: scheduleView } = useQuery(
api.warehouses.bookingSchedule.queryOptions({
input: { bookingId },
enabled: Boolean(bookingId),
}),
);
const items = data ?? [];
const latest = items[0];

View File

@@ -10,12 +10,14 @@ interface WarehouseInquiryTableProps {
onView?: (result: InventoryInquiryResult) => void;
}
const dash = '-';
const itemDescriptor = (result: InventoryInquiryResult) => {
if (result.containerNumber) return `Container ${result.containerNumber}`;
if (result.cargoType) return `Cargo · ${result.cargoType}`;
if (result.cargoDescription) return `Cargo · ${result.cargoDescription}`;
if (result.cargoType) return `Cargo - ${result.cargoType}`;
if (result.cargoDescription) return `Cargo - ${result.cargoDescription}`;
if (result.goodsId) return 'Goods';
return '—';
return dash;
};
export function WarehouseInquiryTable({ results, onView }: WarehouseInquiryTableProps) {
@@ -44,7 +46,7 @@ export function WarehouseInquiryTable({ results, onView }: WarehouseInquiryTable
<Table.Th>Weight</Table.Th>
<Table.Th>Arrived</Table.Th>
<Table.Th>Ready</Table.Th>
{onView && <Table.Th ta="right">Actions</Table.Th>}
{onView ? <Table.Th ta="right">Actions</Table.Th> : null}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
@@ -52,32 +54,32 @@ export function WarehouseInquiryTable({ results, onView }: WarehouseInquiryTable
<Table.Tr key={result.id}>
<Table.Td>
<Text size="sm" fw={600}>
{result.bookingReference ?? result.bookingNumber ?? result.bookingId?.slice(0, 8) ?? '—'}
{result.bookingReference ?? result.bookingNumber ?? result.bookingId?.slice(0, 8) ?? dash}
</Text>
</Table.Td>
<Table.Td>{result.customerName ?? '—'}</Table.Td>
<Table.Td>{result.customerName ?? dash}</Table.Td>
<Table.Td>{itemDescriptor(result)}</Table.Td>
<Table.Td>
<Stack gap={0}>
<Text size="sm">{result.warehouse?.name ?? '—'}</Text>
{result.warehouse?.code && (
<Text size="sm">{result.warehouse?.name ?? dash}</Text>
{result.warehouse?.code ? (
<Text size="xs" c="dimmed">
{result.warehouse.code}
</Text>
)}
) : null}
</Stack>
</Table.Td>
<Table.Td>{result.yard?.name ?? '—'}</Table.Td>
<Table.Td>{result.zone?.name ?? '—'}</Table.Td>
<Table.Td>{result.yard?.name ?? dash}</Table.Td>
<Table.Td>{result.zone?.name ?? dash}</Table.Td>
<Table.Td>
<Stack gap={0}>
<Text size="sm">{result.locationSummary ?? '-'}</Text>
{result.trainNumber && (
<Text size="sm">{result.locationSummary ?? dash}</Text>
{result.trainNumber ? (
<Text size="xs" c="dimmed">
{result.trainNumber}
{result.route ? ` - ${result.route}` : ''}
</Text>
)}
) : null}
</Stack>
</Table.Td>
<Table.Td>
@@ -97,7 +99,7 @@ export function WarehouseInquiryTable({ results, onView }: WarehouseInquiryTable
<Table.Td>
<Text size="xs">{formatDate(result.readyForLoadingAt)}</Text>
</Table.Td>
{onView && (
{onView ? (
<Table.Td>
<Tooltip label="View details" withArrow>
<ActionIcon variant="subtle" color="gray" onClick={() => onView(result)} ml="auto">
@@ -105,7 +107,7 @@ export function WarehouseInquiryTable({ results, onView }: WarehouseInquiryTable
</ActionIcon>
</Tooltip>
</Table.Td>
)}
) : null}
</Table.Tr>
))}
</Table.Tbody>

View File

@@ -1,8 +1,11 @@
import { ActionIcon, Badge, Button, Checkbox, Group, Table, Text, Tooltip } from '@mantine/core';
import { ArrowRightLeft, ClipboardList, Coins, Eye, FileText, History, MapPin } from 'lucide-react';
import type { InventoryAction, WarehouseInventoryItem } from '@/types/warehouse';
import { getNextInventoryAction } from '@/types/warehouse';
import {
getNextInventoryAction,
type InventoryAction,
type WarehouseInventoryItem,
} from '@/types/warehouse';
import { InventoryStatusBadge } from './badges';
import { formatDate, formatNumber, humanizeEnum } from './options';
@@ -16,9 +19,7 @@ interface WarehouseInventoryTableProps {
onInspect?: (item: WarehouseInventoryItem) => void;
onFeePreview?: (item: WarehouseInventoryItem) => void;
onReleaseDocument?: (item: WarehouseInventoryItem) => void;
// Optional Last Mile action — only rendered for items whose booking requested door delivery.
onLastMile?: (item: WarehouseInventoryItem) => void;
// Optional row selection (used for bulk Mark-as-Inspected).
selectedIds?: Set<string>;
onToggleSelect?: (id: string) => void;
onToggleSelectAll?: () => void;
@@ -38,7 +39,7 @@ const actionColor: Record<InventoryAction, string> = {
reserve: 'grape',
'ready-for-loading': 'cyan',
load: 'teal',
dispatch: 'green',
dispatch: 'edr-green',
'ready-for-pickup': 'orange',
release: 'yellow',
deliver: 'green',
@@ -62,9 +63,10 @@ export function WarehouseInventoryTable({
someSelected,
}: WarehouseInventoryTableProps) {
const selectable = Boolean(onToggleSelect);
if (items.length === 0) {
return (
<Text c="dimmed" ta="center" py="xl">
<Text size="sm" c="dimmed" ta="center" py="xl">
No inventory items found.
</Text>
);
@@ -103,6 +105,7 @@ export function WarehouseInventoryTable({
const kind = itemKind(item);
const busy = busyId === item.id;
const nextAction = getNextInventoryAction(item);
return (
<Table.Tr key={item.id}>
{selectable && (
@@ -118,19 +121,19 @@ export function WarehouseInventoryTable({
{item.bookingId ? (
<Tooltip label={item.bookingId} withArrow>
<Text size="sm" fw={600}>
{item.bookingId.slice(0, 8)}
{item.bookingId.slice(0, 8)}...
</Text>
</Tooltip>
) : (
<Text size="sm" c="dimmed">
-
</Text>
)}
</Table.Td>
<Table.Td>{item.warehouse?.facility?.name ?? ''}</Table.Td>
<Table.Td>{item.warehouse?.code ?? ''}</Table.Td>
<Table.Td>{item.yard?.code ?? ''}</Table.Td>
<Table.Td>{item.zone?.code ?? ''}</Table.Td>
<Table.Td>{item.warehouse?.facility?.name ?? '-'}</Table.Td>
<Table.Td>{item.warehouse?.code ?? '-'}</Table.Td>
<Table.Td>{item.yard?.code ?? '-'}</Table.Td>
<Table.Td>{item.zone?.code ?? '-'}</Table.Td>
<Table.Td>
<Badge color={kind.color} variant="light" size="sm" radius="md">
{kind.label}
@@ -164,8 +167,6 @@ export function WarehouseInventoryTable({
{humanizeEnum(nextAction.replace(/-/g, '_'))}
</Button>
)}
{/* Batch 10 — a PICKUP_READY import item can also be stored or dispatched,
kept separate from the customer-pickup (release/deliver) next action. */}
{item.status === 'READY_FOR_PICKUP' && (
<>
<Button
@@ -211,7 +212,11 @@ export function WarehouseInventoryTable({
)}
{onReleaseDocument && item.releaseDate && (
<Tooltip label="View release exit paper" withArrow>
<ActionIcon variant="subtle" color="orange" onClick={() => onReleaseDocument(item)}>
<ActionIcon
variant="subtle"
color="orange"
onClick={() => onReleaseDocument(item)}
>
<FileText size={16} />
</ActionIcon>
</Tooltip>

View File

@@ -1,8 +1,11 @@
import { useMemo } from 'react';
import { ActionIcon, Anchor, Group, Table, Text } from '@mantine/core';
import { ActionIcon, Group, Text } from '@mantine/core';
import { Eye, Pencil } from 'lucide-react';
import { DataTable, type ColumnDef } from '@edr/ui-common';
import { useStations } from '@/hooks/useStations';
import { useQuery } from '@tanstack/react-query';
import { api } from '@/services/api';
import type { Warehouse } from '@/types/warehouse';
import { WarehouseStatusBadge, WarehouseTypeBadge } from './badges';
import { formatCapacity } from './options';
@@ -14,79 +17,99 @@ interface WarehouseTableProps {
}
export function WarehouseTable({ warehouses, onView, onEdit }: WarehouseTableProps) {
const { data: stations } = useStations();
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],
);
if (warehouses.length === 0) {
return (
<Text c="dimmed" ta="center" py="xl">
No warehouses found.
</Text>
);
}
const columns: ColumnDef<Warehouse>[] = [
{
id: 'code',
header: 'Code',
cell: ({ row }) => (
<Text
fw={600}
size="sm"
c="edr-green.7"
style={{ cursor: 'pointer' }}
onClick={() => onView(row.original)}
>
{row.original.code}
</Text>
),
},
{ id: 'name', header: 'Name', cell: ({ row }) => row.original.name },
{
id: 'facility',
header: 'Facility',
cell: ({ row }) => {
const name = row.original.stationId
? stationNameById.get(row.original.stationId)
: undefined;
return name ? (
<Text size="sm" fw={500}>
{name}
</Text>
) : (
<Text size="sm" c="dimmed">
</Text>
);
},
},
{
id: 'type',
header: 'Type',
cell: ({ row }) => <WarehouseTypeBadge type={row.original.type} />,
},
{
id: 'location',
header: 'Location',
cell: ({ row }) => row.original.locationName ?? '—',
},
{
id: 'weight',
header: 'Weight (cur / cap)',
cell: ({ row }) => formatCapacity(row.original.currentWeight, row.original.capacityWeight),
},
{
id: 'containers',
header: 'Containers (cur / cap)',
cell: ({ row }) =>
formatCapacity(row.original.currentContainers, row.original.capacityContainers),
},
{
id: 'status',
header: 'Status',
cell: ({ row }) => <WarehouseStatusBadge status={row.original.status} />,
},
{
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 (
<Table.ScrollContainer minWidth={900}>
<Table highlightOnHover verticalSpacing="sm" striped>
<Table.Thead>
<Table.Tr>
<Table.Th>Code</Table.Th>
<Table.Th>Name</Table.Th>
<Table.Th>Facility</Table.Th>
<Table.Th>Type</Table.Th>
<Table.Th>Location</Table.Th>
<Table.Th>Weight (cur / cap)</Table.Th>
<Table.Th>Containers (cur / cap)</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th ta="right">Actions</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{warehouses.map((warehouse) => (
<Table.Tr key={warehouse.id}>
<Table.Td>
<Anchor fw={600} size="sm" onClick={() => onView(warehouse)}>
{warehouse.code}
</Anchor>
</Table.Td>
<Table.Td>{warehouse.name}</Table.Td>
<Table.Td>
{warehouse.stationId && stationNameById.get(warehouse.stationId) ? (
<Text size="sm" fw={500}>
{stationNameById.get(warehouse.stationId)}
</Text>
) : (
<Text size="sm" c="dimmed">
</Text>
)}
</Table.Td>
<Table.Td>
<WarehouseTypeBadge type={warehouse.type} />
</Table.Td>
<Table.Td>{warehouse.locationName ?? '—'}</Table.Td>
<Table.Td>{formatCapacity(warehouse.currentWeight, warehouse.capacityWeight)}</Table.Td>
<Table.Td>{formatCapacity(warehouse.currentContainers, warehouse.capacityContainers)}</Table.Td>
<Table.Td>
<WarehouseStatusBadge status={warehouse.status} />
</Table.Td>
<Table.Td>
<Group gap="xs" justify="flex-end" wrap="nowrap">
<ActionIcon variant="subtle" color="gray" onClick={() => onView(warehouse)} title="View">
<Eye size={16} />
</ActionIcon>
<ActionIcon variant="subtle" color="gray" onClick={() => onEdit(warehouse)} title="Edit">
<Pencil size={16} />
</ActionIcon>
</Group>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
<DataTable
columns={columns}
data={warehouses}
status="success"
onRowClick={(warehouse) => onView(warehouse)}
emptyMessage="No warehouses found."
containerClassName="border-0 shadow-none"
/>
);
}

View File

@@ -1,54 +1,80 @@
import { Badge } from '@mantine/core';
import { Badge } from "@mantine/core";
import type { InventoryStatus, WarehouseStatus, WarehouseType } from '@/types/warehouse';
import type {
InventoryStatus,
WarehouseStatus,
WarehouseType,
} from "@/types/warehouse";
const humanize = (value: string) =>
value
.toLowerCase()
.split('_')
.split("_")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
.join(" ");
const badgeStyle = {
fontSize: '0.7rem',
letterSpacing: '0.04em',
whiteSpace: 'nowrap' as const,
fontSize: "0.7rem",
letterSpacing: "0.04em",
whiteSpace: "nowrap" as const,
};
export function WarehouseTypeBadge({ type }: { type: WarehouseType }) {
const color = type === 'CLOSED_WAREHOUSE' ? 'indigo' : 'teal';
const color = type === "CLOSED_WAREHOUSE" ? "indigo" : "teal";
return (
<Badge color={color} variant="light" size="sm" radius="md" fw={600} style={badgeStyle}>
<Badge
color={color}
variant="light"
size="sm"
radius="md"
fw={600}
style={badgeStyle}
>
{humanize(type)}
</Badge>
);
}
export function WarehouseStatusBadge({ status }: { status: WarehouseStatus }) {
const color = status === 'ACTIVE' ? 'green' : 'gray';
const color = status === "ACTIVE" ? "edr-green" : "gray";
return (
<Badge color={color} variant="light" size="sm" radius="md" tt="uppercase" fw={600} style={badgeStyle}>
<Badge
color={color}
variant="light"
size="sm"
radius="md"
tt="uppercase"
fw={600}
style={badgeStyle}
>
{status}
</Badge>
);
}
const inventoryStatusColor: Record<InventoryStatus, string> = {
UNLOADED: 'indigo',
RECEIVED: 'yellow',
STORED: 'blue',
RESERVED: 'grape',
READY_FOR_LOADING: 'cyan',
LOADED: 'teal',
DISPATCHED: 'green',
READY_FOR_PICKUP: 'orange',
DELIVERED: 'green',
UNLOADED: "indigo",
RECEIVED: "yellow",
STORED: "blue",
RESERVED: "grape",
READY_FOR_LOADING: "cyan",
LOADED: "teal",
READY_FOR_PICKUP: "teal",
DISPATCHED: "edr-green",
DELIVERED: "edr-green",
};
export function InventoryStatusBadge({ status }: { status: InventoryStatus }) {
const color = inventoryStatusColor[status] ?? 'gray';
const color = inventoryStatusColor[status] ?? "gray";
return (
<Badge color={color} variant="light" size="sm" radius="md" fw={600} style={badgeStyle}>
<Badge
color={color}
variant="light"
size="sm"
radius="md"
fw={600}
style={badgeStyle}
>
{humanize(status)}
</Badge>
);