feat(warehouse): add warehouse management module (batch 1)

Backend (edr-freight-api):
- Warehouse, WarehouseYard, WarehouseZone, WarehouseInventory entities
- CRUD for warehouses/yards/zones with scoped code uniqueness
- Inventory receive with location-hierarchy + capacity validation and
  transactional capacity-counter updates
- inspect / ready-for-loading status transitions
- inventory inquiry (booking/customer/container/cargo-type joins)
- migration creating freight.warehouse* tables

Frontend (freight backoffice):
- types, service, react-query hooks, URL constants
- reusable components: badges, filters, table/card views, inventory and
  inquiry tables, create/receive modals
- pages: Warehouse list, detail (overview/yards/zones/inventory tabs),
  inventory, inventory inquiry
- Warehouse Information card + Receive At Warehouse on booking detail
- routes + sidebar nav; app-wide ErrorBoundary

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hagernesh
2026-06-11 15:54:56 +00:00
parent 7facbeda22
commit c86118cd8d
54 changed files with 4257 additions and 1 deletions

View File

@@ -25,6 +25,7 @@ import {
BookingCargoCard,
BookingContractSummaryCard,
} from "@/components/bookings/detail";
import { WarehouseInfoCard } from "@/components/warehouses";
import { getStatusMeta } from "@/features/bookings/booking-status.config";
import { toBookingListRow } from "@/features/bookings/mapBookingListRow";
import { useBookingDetail, useBookingMutations } from "@/hooks/bookings/useBookings";
@@ -150,6 +151,7 @@ export default function BookingRequestDetailPage() {
<Box style={{ position: "sticky", top: 24 }}>
<Stack gap="lg">
<BookingPricingSummary booking={booking} />
<WarehouseInfoCard bookingId={booking.id} bookingReference={booking.reference} />
<BookingActionsToolbar booking={booking} mutations={mutations} />
{showContractButton && (
<Button

View File

@@ -0,0 +1,149 @@
import { useMemo, useState } from 'react';
import { Button, Card, Center, Container, Group, Loader, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { Search } from 'lucide-react';
import Breadcrumbs from '@/components/ui/Breadcrumbs';
import { WarehouseInquiryTable, inventoryStatusOptions } from '@/components/warehouses';
import {
useInventoryInquiry,
useWarehouseYards,
useWarehouseZones,
useWarehouses,
} from '@/hooks/useWarehouses';
import type { InventoryInquiryFilter, InventoryStatus } from '@/types/warehouse';
export default function InventoryInquiryPage() {
const [draft, setDraft] = useState<InventoryInquiryFilter>({});
const [applied, setApplied] = useState<InventoryInquiryFilter>({});
const warehousesQuery = useWarehouses();
const yardsQuery = useWarehouseYards(draft.warehouseId);
const zonesQuery = useWarehouseZones(draft.yardId);
const { data, isFetching } = useInventoryInquiry(applied);
const results = data ?? [];
const warehouseOptions = useMemo(
() => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })),
[warehousesQuery.data],
);
const yardOptions = useMemo(
() => (yardsQuery.data ?? []).map((y) => ({ value: y.id, label: `${y.name} (${y.code})` })),
[yardsQuery.data],
);
const zoneOptions = useMemo(
() => (zonesQuery.data ?? []).map((z) => ({ value: z.id, label: `${z.name} (${z.code})` })),
[zonesQuery.data],
);
const runSearch = () => setApplied(draft);
const reset = () => {
setDraft({});
setApplied({});
};
return (
<Container size="xxl" py="lg">
<Breadcrumbs items={[{ label: 'Inventory inquiry' }]} />
<Stack gap="lg" mt="sm">
<div>
<Title order={2}>Inventory Inquiry</Title>
<Text c="dimmed" size="sm">
Locate any cargo, container or goods inside the warehouse network.
</Text>
</div>
<Card withBorder radius="md" padding="lg">
<Stack gap="md">
<Group gap="sm" wrap="wrap">
<TextInput
label="Booking number"
placeholder="e.g. BKG-00123"
value={draft.bookingNumber ?? ''}
onChange={(e) => setDraft((f) => ({ ...f, bookingNumber: e.currentTarget.value || undefined }))}
w={200}
/>
<TextInput
label="Container number"
placeholder="e.g. MSKU1234567"
value={draft.containerNumber ?? ''}
onChange={(e) => setDraft((f) => ({ ...f, containerNumber: e.currentTarget.value || undefined }))}
w={200}
/>
<TextInput
label="Goods name"
placeholder="e.g. Coffee"
value={draft.goodsName ?? ''}
onChange={(e) => setDraft((f) => ({ ...f, goodsName: e.currentTarget.value || undefined }))}
w={180}
/>
<Select
label="Warehouse"
placeholder="Any"
clearable
searchable
data={warehouseOptions}
value={draft.warehouseId ?? null}
onChange={(value) =>
setDraft((f) => ({ ...f, warehouseId: value ?? undefined, yardId: undefined, zoneId: undefined }))
}
w={200}
/>
<Select
label="Yard"
placeholder="Any"
clearable
searchable
disabled={!draft.warehouseId}
data={yardOptions}
value={draft.yardId ?? null}
onChange={(value) => setDraft((f) => ({ ...f, yardId: value ?? undefined, zoneId: undefined }))}
w={180}
/>
<Select
label="Zone"
placeholder="Any"
clearable
searchable
disabled={!draft.yardId}
data={zoneOptions}
value={draft.zoneId ?? null}
onChange={(value) => setDraft((f) => ({ ...f, zoneId: value ?? undefined }))}
w={180}
/>
<Select
label="Status"
placeholder="Any"
clearable
data={inventoryStatusOptions}
value={draft.status ?? null}
onChange={(value) => setDraft((f) => ({ ...f, status: (value as InventoryStatus) ?? undefined }))}
w={180}
/>
</Group>
<Group>
<Button leftSection={<Search size={16} />} onClick={runSearch}>
Search
</Button>
<Button variant="default" onClick={reset}>
Reset
</Button>
</Group>
</Stack>
</Card>
<Card withBorder radius="md" padding="lg">
{isFetching ? (
<Center py="xl">
<Loader />
</Center>
) : (
<WarehouseInquiryTable results={results} />
)}
</Card>
</Stack>
</Container>
);
}

View File

@@ -0,0 +1,375 @@
import { useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import {
ActionIcon,
Button,
Card,
Center,
Container,
Group,
Loader,
SimpleGrid,
Stack,
Select,
Table,
Tabs,
Text,
Title,
} from '@mantine/core';
import { ArrowLeft, Boxes, LayoutGrid, Package, Pencil, Plus } from 'lucide-react';
import Breadcrumbs from '@/components/ui/Breadcrumbs';
import { useToast } from '@/hooks/use-toast';
import {
CreateYardModal,
CreateZoneModal,
WarehouseInventoryTable,
WarehouseStatusBadge,
WarehouseTypeBadge,
formatCapacity,
humanizeEnum,
} from '@/components/warehouses';
import {
useInspectInventory,
useMarkReadyForLoading,
useWarehouse,
useWarehouseInventory,
useWarehouseYards,
useWarehouseZones,
} from '@/hooks/useWarehouses';
import { extractErrorMessage } from '@/components/warehouses/options';
import type { WarehouseInventoryItem, WarehouseYard, WarehouseZone } from '@/types/warehouse';
function StatCard({ label, value }: { label: string; value: string }) {
return (
<Card withBorder radius="md" padding="md">
<Text size="xs" c="dimmed" tt="uppercase" fw={600}>
{label}
</Text>
<Text fw={700} size="lg" mt={4}>
{value}
</Text>
</Card>
);
}
export default function WarehouseDetailPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { toast } = useToast();
const { data: warehouse, isLoading } = useWarehouse(id);
const yardsQuery = useWarehouseYards(id);
const [yardModalOpen, setYardModalOpen] = useState(false);
const [editingYard, setEditingYard] = useState<WarehouseYard | null>(null);
const [zoneModalOpen, setZoneModalOpen] = useState(false);
const [editingZone, setEditingZone] = useState<WarehouseZone | null>(null);
const [selectedYardId, setSelectedYardId] = useState<string | null>(null);
const zonesQuery = useWarehouseZones(selectedYardId ?? undefined);
const inventoryQuery = useWarehouseInventory(id ? { warehouseId: id } : undefined);
const inspectMutation = useInspectInventory();
const readyMutation = useMarkReadyForLoading();
const [busyId, setBusyId] = useState<string | null>(null);
const yards = yardsQuery.data ?? [];
const yardOptions = useMemo(
() => yards.map((y) => ({ value: y.id, label: `${y.name} (${y.code})` })),
[yards],
);
const handleInspect = async (item: WarehouseInventoryItem) => {
setBusyId(item.id);
try {
await inspectMutation.mutateAsync(item.id);
toast({ title: 'Inventory under inspection' });
} catch (error) {
toast({ variant: 'destructive', title: 'Action failed', description: extractErrorMessage(error) });
} finally {
setBusyId(null);
}
};
const handleReady = async (item: WarehouseInventoryItem) => {
setBusyId(item.id);
try {
await readyMutation.mutateAsync(item.id);
toast({ title: 'Inventory ready for loading' });
} catch (error) {
toast({ variant: 'destructive', title: 'Action failed', description: extractErrorMessage(error) });
} finally {
setBusyId(null);
}
};
if (isLoading) {
return (
<Center mih="60vh">
<Loader />
</Center>
);
}
if (!warehouse) {
return (
<Container size="sm" py="xl">
<Stack align="center" gap="md">
<Text fw={700}>Warehouse not found</Text>
<Button variant="default" leftSection={<ArrowLeft size={16} />} onClick={() => navigate('/dashboard/warehouses')}>
Back to warehouses
</Button>
</Stack>
</Container>
);
}
return (
<Container size="xxl" py="lg">
<Breadcrumbs
items={[
{ label: 'Warehouses', href: '/dashboard/warehouses' },
{ label: warehouse.name },
]}
/>
<Stack gap="lg" mt="sm">
<Group justify="space-between" align="flex-start">
<Group gap="md" align="center">
<ActionIcon variant="subtle" color="gray" onClick={() => navigate('/dashboard/warehouses')}>
<ArrowLeft size={18} />
</ActionIcon>
<div>
<Group gap="sm">
<Title order={2}>{warehouse.name}</Title>
<WarehouseTypeBadge type={warehouse.type} />
<WarehouseStatusBadge status={warehouse.status} />
</Group>
<Text c="dimmed" size="sm">
{warehouse.code}
{warehouse.locationName ? ` · ${warehouse.locationName}` : ''}
</Text>
</div>
</Group>
</Group>
<Tabs defaultValue="overview">
<Tabs.List>
<Tabs.Tab value="overview" leftSection={<LayoutGrid size={16} />}>
Overview
</Tabs.Tab>
<Tabs.Tab value="yards" leftSection={<Boxes size={16} />}>
Yards
</Tabs.Tab>
<Tabs.Tab value="zones" leftSection={<LayoutGrid size={16} />}>
Zones
</Tabs.Tab>
<Tabs.Tab value="inventory" leftSection={<Package size={16} />}>
Inventory
</Tabs.Tab>
</Tabs.List>
{/* OVERVIEW */}
<Tabs.Panel value="overview" pt="lg">
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="md">
<StatCard label="Type" value={humanizeEnum(warehouse.type)} />
<StatCard label="Yards" value={String(yards.length)} />
<StatCard
label="Weight (cur / cap)"
value={formatCapacity(warehouse.currentWeight, warehouse.capacityWeight)}
/>
<StatCard
label="Containers (cur / cap)"
value={formatCapacity(warehouse.currentContainers, warehouse.capacityContainers)}
/>
</SimpleGrid>
</Tabs.Panel>
{/* YARDS */}
<Tabs.Panel value="yards" pt="lg">
<Card withBorder radius="md" padding="lg">
<Stack gap="md">
<Group justify="space-between">
<Text fw={600}>Yards</Text>
<Button
size="sm"
leftSection={<Plus size={16} />}
onClick={() => {
setEditingYard(null);
setYardModalOpen(true);
}}
>
Create Yard
</Button>
</Group>
{yards.length === 0 ? (
<Text c="dimmed" ta="center" py="lg">
No yards yet.
</Text>
) : (
<Table highlightOnHover verticalSpacing="sm" striped>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Code</Table.Th>
<Table.Th>Type</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>
{yards.map((yard) => (
<Table.Tr key={yard.id}>
<Table.Td>{yard.name}</Table.Td>
<Table.Td>{yard.code}</Table.Td>
<Table.Td>{humanizeEnum(yard.type)}</Table.Td>
<Table.Td>{formatCapacity(yard.currentWeight, yard.capacityWeight)}</Table.Td>
<Table.Td>{formatCapacity(yard.currentContainers, yard.capacityContainers)}</Table.Td>
<Table.Td>
<WarehouseStatusBadge status={yard.status} />
</Table.Td>
<Table.Td ta="right">
<ActionIcon
variant="subtle"
color="gray"
onClick={() => {
setEditingYard(yard);
setYardModalOpen(true);
}}
>
<Pencil size={16} />
</ActionIcon>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
)}
</Stack>
</Card>
</Tabs.Panel>
{/* ZONES */}
<Tabs.Panel value="zones" pt="lg">
<Card withBorder radius="md" padding="lg">
<Stack gap="md">
<Group justify="space-between" align="flex-end">
<Select
label="Yard"
placeholder="Select a yard"
data={yardOptions}
value={selectedYardId}
onChange={setSelectedYardId}
w={280}
searchable
/>
<Button
size="sm"
leftSection={<Plus size={16} />}
disabled={!selectedYardId}
onClick={() => {
setEditingZone(null);
setZoneModalOpen(true);
}}
>
Create Zone
</Button>
</Group>
{!selectedYardId ? (
<Text c="dimmed" ta="center" py="lg">
Select a yard to view its zones.
</Text>
) : (zonesQuery.data ?? []).length === 0 ? (
<Text c="dimmed" ta="center" py="lg">
No zones in this yard yet.
</Text>
) : (
<Table highlightOnHover verticalSpacing="sm" striped>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Code</Table.Th>
<Table.Th>Type</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>
{(zonesQuery.data ?? []).map((zone) => (
<Table.Tr key={zone.id}>
<Table.Td>{zone.name}</Table.Td>
<Table.Td>{zone.code}</Table.Td>
<Table.Td>{humanizeEnum(zone.type)}</Table.Td>
<Table.Td>{formatCapacity(zone.currentWeight, zone.capacityWeight)}</Table.Td>
<Table.Td>{formatCapacity(zone.currentContainers, zone.capacityContainers)}</Table.Td>
<Table.Td>
<WarehouseStatusBadge status={zone.status} />
</Table.Td>
<Table.Td ta="right">
<ActionIcon
variant="subtle"
color="gray"
onClick={() => {
setEditingZone(zone);
setZoneModalOpen(true);
}}
>
<Pencil size={16} />
</ActionIcon>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
)}
</Stack>
</Card>
</Tabs.Panel>
{/* INVENTORY */}
<Tabs.Panel value="inventory" pt="lg">
<Card withBorder radius="md" padding="lg">
{inventoryQuery.isLoading ? (
<Center py="xl">
<Loader />
</Center>
) : (
<WarehouseInventoryTable
items={inventoryQuery.data ?? []}
onInspect={handleInspect}
onReadyForLoading={handleReady}
busyId={busyId}
/>
)}
</Card>
</Tabs.Panel>
</Tabs>
</Stack>
{id && (
<CreateYardModal
opened={yardModalOpen}
onClose={() => setYardModalOpen(false)}
warehouseId={id}
yard={editingYard}
/>
)}
{selectedYardId && (
<CreateZoneModal
opened={zoneModalOpen}
onClose={() => setZoneModalOpen(false)}
yardId={selectedYardId}
zone={editingZone}
/>
)}
</Container>
);
}

View File

@@ -0,0 +1,169 @@
import { useMemo, useState } from 'react';
import { Button, Card, Center, Container, Group, Loader, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { PackagePlus, Search } from 'lucide-react';
import Breadcrumbs from '@/components/ui/Breadcrumbs';
import { useToast } from '@/hooks/use-toast';
import {
ReceiveInventoryModal,
WarehouseInventoryTable,
inventoryStatusOptions,
} from '@/components/warehouses';
import { extractErrorMessage } from '@/components/warehouses/options';
import {
useInspectInventory,
useMarkReadyForLoading,
useWarehouseInventory,
useWarehouseYards,
useWarehouseZones,
useWarehouses,
} from '@/hooks/useWarehouses';
import type { InventoryFilter, InventoryStatus, WarehouseInventoryItem } from '@/types/warehouse';
export default function WarehouseInventoryPage() {
const { toast } = useToast();
const [filter, setFilter] = useState<InventoryFilter>({});
const [search, setSearch] = useState('');
const [modalOpen, setModalOpen] = useState(false);
const [busyId, setBusyId] = useState<string | null>(null);
const [debouncedSearch] = useDebouncedValue(search, 300);
const queryFilter = useMemo<InventoryFilter>(
() => ({ ...filter, search: debouncedSearch || undefined }),
[filter, debouncedSearch],
);
const warehousesQuery = useWarehouses();
const yardsQuery = useWarehouseYards(filter.warehouseId);
const zonesQuery = useWarehouseZones(filter.yardId);
const inventoryQuery = useWarehouseInventory(queryFilter);
const inspectMutation = useInspectInventory();
const readyMutation = useMarkReadyForLoading();
const warehouseOptions = useMemo(
() => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })),
[warehousesQuery.data],
);
const yardOptions = useMemo(
() => (yardsQuery.data ?? []).map((y) => ({ value: y.id, label: `${y.name} (${y.code})` })),
[yardsQuery.data],
);
const zoneOptions = useMemo(
() => (zonesQuery.data ?? []).map((z) => ({ value: z.id, label: `${z.name} (${z.code})` })),
[zonesQuery.data],
);
const handleInspect = async (item: WarehouseInventoryItem) => {
setBusyId(item.id);
try {
await inspectMutation.mutateAsync(item.id);
toast({ title: 'Inventory under inspection' });
} catch (error) {
toast({ variant: 'destructive', title: 'Action failed', description: extractErrorMessage(error) });
} finally {
setBusyId(null);
}
};
const handleReady = async (item: WarehouseInventoryItem) => {
setBusyId(item.id);
try {
await readyMutation.mutateAsync(item.id);
toast({ title: 'Inventory ready for loading' });
} catch (error) {
toast({ variant: 'destructive', title: 'Action failed', description: extractErrorMessage(error) });
} finally {
setBusyId(null);
}
};
return (
<Container size="xxl" py="lg">
<Breadcrumbs items={[{ label: 'Warehouse inventory' }]} />
<Stack gap="lg" mt="sm">
<Group justify="space-between" align="flex-end">
<div>
<Title order={2}>Warehouse Inventory</Title>
<Text c="dimmed" size="sm">
Track received items and move them through inspection to loading.
</Text>
</div>
<Button leftSection={<PackagePlus size={16} />} onClick={() => setModalOpen(true)}>
Receive Inventory
</Button>
</Group>
<Card withBorder radius="md" padding="lg">
<Stack gap="md">
<Group gap="sm" wrap="wrap">
<TextInput
placeholder="Search notes"
leftSection={<Search size={16} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
w={220}
/>
<Select
placeholder="All warehouses"
clearable
searchable
data={warehouseOptions}
value={filter.warehouseId ?? null}
onChange={(value) =>
setFilter((f) => ({ ...f, warehouseId: value ?? undefined, yardId: undefined, zoneId: undefined }))
}
w={220}
/>
<Select
placeholder="All yards"
clearable
searchable
disabled={!filter.warehouseId}
data={yardOptions}
value={filter.yardId ?? null}
onChange={(value) => setFilter((f) => ({ ...f, yardId: value ?? undefined, zoneId: undefined }))}
w={200}
/>
<Select
placeholder="All zones"
clearable
searchable
disabled={!filter.yardId}
data={zoneOptions}
value={filter.zoneId ?? null}
onChange={(value) => setFilter((f) => ({ ...f, zoneId: value ?? undefined }))}
w={200}
/>
<Select
placeholder="All statuses"
clearable
data={inventoryStatusOptions}
value={filter.status ?? null}
onChange={(value) => setFilter((f) => ({ ...f, status: (value as InventoryStatus) ?? undefined }))}
w={200}
/>
</Group>
{inventoryQuery.isLoading ? (
<Center py="xl">
<Loader />
</Center>
) : (
<WarehouseInventoryTable
items={inventoryQuery.data ?? []}
onInspect={handleInspect}
onReadyForLoading={handleReady}
busyId={busyId}
/>
)}
</Stack>
</Card>
</Stack>
<ReceiveInventoryModal opened={modalOpen} onClose={() => setModalOpen(false)} />
</Container>
);
}

View File

@@ -0,0 +1,85 @@
import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button, Card, Center, Container, Group, Loader, Stack, Text, Title } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { Plus } from 'lucide-react';
import Breadcrumbs from '@/components/ui/Breadcrumbs';
import {
CreateWarehouseModal,
WarehouseCardView,
WarehouseFilters,
WarehouseTable,
type WarehouseView,
} from '@/components/warehouses';
import { useWarehouses } from '@/hooks/useWarehouses';
import type { Warehouse, WarehouseFilter } from '@/types/warehouse';
export default function WarehouseListPage() {
const navigate = useNavigate();
const [filter, setFilter] = useState<WarehouseFilter>({});
const [view, setView] = useState<WarehouseView>('table');
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<Warehouse | null>(null);
const [debouncedSearch] = useDebouncedValue(filter.search, 300);
const queryFilter = useMemo<WarehouseFilter>(
() => ({ ...filter, search: debouncedSearch }),
[filter, debouncedSearch],
);
const { data, isLoading, isError } = useWarehouses(queryFilter);
const warehouses = data ?? [];
const openCreate = () => {
setEditing(null);
setModalOpen(true);
};
const openEdit = (warehouse: Warehouse) => {
setEditing(warehouse);
setModalOpen(true);
};
const openDetail = (warehouse: Warehouse) => navigate(`/dashboard/warehouses/${warehouse.id}`);
return (
<Container size="xxl" py="lg">
<Breadcrumbs items={[{ label: 'Warehouses' }]} />
<Stack gap="lg" mt="sm">
<Group justify="space-between" align="flex-end">
<div>
<Title order={2}>Warehouses</Title>
<Text c="dimmed" size="sm">
Manage warehouses, yards and zones.
</Text>
</div>
<Button leftSection={<Plus size={16} />} onClick={openCreate}>
Create Warehouse
</Button>
</Group>
<Card withBorder radius="md" padding="lg">
<Stack gap="md">
<WarehouseFilters filter={filter} onChange={setFilter} view={view} onViewChange={setView} />
{isLoading ? (
<Center py="xl">
<Loader />
</Center>
) : isError ? (
<Text c="red" ta="center" py="xl">
Failed to load warehouses.
</Text>
) : view === 'table' ? (
<WarehouseTable warehouses={warehouses} onView={openDetail} onEdit={openEdit} />
) : (
<WarehouseCardView warehouses={warehouses} onView={openDetail} onEdit={openEdit} />
)}
</Stack>
</Card>
</Stack>
<CreateWarehouseModal opened={modalOpen} onClose={() => setModalOpen(false)} warehouse={editing} />
</Container>
);
}