mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 07:38:10 +00:00
api
This commit is contained in:
@@ -12,7 +12,6 @@ import {
|
||||
} from '@mantine/core';
|
||||
import { ChevronDown, ChevronRight, PackageOpen, Truck } from 'lucide-react';
|
||||
|
||||
import { PageHeader } from '@/components/page';
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs';
|
||||
import {
|
||||
VisualEmptyState,
|
||||
@@ -79,17 +78,13 @@ function ImportTrainDetailRows({ scheduleId }: { scheduleId: string }) {
|
||||
{item.bookingReference ?? item.bookingId.slice(0, 8)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{item.customerName ?? '-'}</Table.Td>
|
||||
<Table.Td>{item.containerNumber ?? '-'}</Table.Td>
|
||||
<Table.Td>{item.cargoType ?? '-'}</Table.Td>
|
||||
<Table.Td>{item.customerName ?? '—'}</Table.Td>
|
||||
<Table.Td>{item.containerNumber ?? '—'}</Table.Td>
|
||||
<Table.Td>{item.cargoType ?? '—'}</Table.Td>
|
||||
<Table.Td>{formatNumber(item.weight)}</Table.Td>
|
||||
<Table.Td>{formatDate(item.arrivalTime)}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
variant="light"
|
||||
color={item.currentStatus === 'UNLOADED' ? 'green' : 'orange'}
|
||||
size="sm"
|
||||
>
|
||||
<Badge variant="light" color={item.currentStatus === 'UNLOADED' ? 'green' : 'orange'} size="sm">
|
||||
{item.currentStatus ?? 'PENDING'}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
@@ -112,9 +107,7 @@ export default function ArrivalQueuePage() {
|
||||
const unloadTrain = async (train: ImportTrain) => {
|
||||
setBusyScheduleId(train.scheduleId);
|
||||
try {
|
||||
const res = (await autoUnload.mutateAsync(train.scheduleId)) as {
|
||||
data: AutoUnloadArrivedResult;
|
||||
};
|
||||
const res = (await autoUnload.mutateAsync(train.scheduleId)) as { data: AutoUnloadArrivedResult };
|
||||
const result = res.data;
|
||||
const details = [
|
||||
result.skippedCount ? `${result.skippedCount} skipped` : '',
|
||||
@@ -143,11 +136,6 @@ export default function ArrivalQueuePage() {
|
||||
<Breadcrumbs items={[{ label: 'Arrival queue' }]} />
|
||||
|
||||
<Stack gap="lg" mt="sm">
|
||||
<PageHeader
|
||||
title="Arrival / Unloading Queue"
|
||||
subtitle="Arrived import trains ready to unload assigned bookings into warehouse inventory."
|
||||
/>
|
||||
|
||||
<WarehouseHero
|
||||
variant="container"
|
||||
secondaryVariant="warehouse"
|
||||
@@ -199,16 +187,16 @@ export default function ArrivalQueuePage() {
|
||||
<Table.Td>
|
||||
<Stack gap={0}>
|
||||
<Text size="sm" fw={700}>
|
||||
{train.trainNumber ?? '-'}
|
||||
{train.trainNumber ?? '—'}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{train.scheduleId.slice(0, 8)}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Table.Td>
|
||||
<Table.Td>{train.route ?? '-'}</Table.Td>
|
||||
<Table.Td>{train.origin ?? '-'}</Table.Td>
|
||||
<Table.Td>{train.destination ?? '-'}</Table.Td>
|
||||
<Table.Td>{train.route ?? '—'}</Table.Td>
|
||||
<Table.Td>{train.origin ?? '—'}</Table.Td>
|
||||
<Table.Td>{train.destination ?? '—'}</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs">{formatDate(train.arrivalTime)}</Text>
|
||||
</Table.Td>
|
||||
@@ -225,9 +213,7 @@ export default function ArrivalQueuePage() {
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
leftSection={
|
||||
isOpen ? <ChevronDown size={14} /> : <ChevronRight size={14} />
|
||||
}
|
||||
leftSection={isOpen ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
onClick={() => setOpenScheduleId(isOpen ? null : train.scheduleId)}
|
||||
>
|
||||
Open
|
||||
@@ -235,13 +221,7 @@ export default function ArrivalQueuePage() {
|
||||
<Button
|
||||
size="compact-xs"
|
||||
color="orange"
|
||||
leftSection={
|
||||
busyScheduleId === train.scheduleId ? (
|
||||
<PackageOpen size={14} />
|
||||
) : (
|
||||
<Truck size={14} />
|
||||
)
|
||||
}
|
||||
leftSection={busyScheduleId === train.scheduleId ? <PackageOpen size={14} /> : <Truck size={14} />}
|
||||
loading={busyScheduleId === train.scheduleId}
|
||||
onClick={() => unloadTrain(train)}
|
||||
>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Text,
|
||||
} from '@mantine/core';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ChevronDown, ChevronRight, Eye, History, PackageOpen, Truck } from 'lucide-react';
|
||||
import { ChevronDown, ChevronRight, Eye, FileText, History, PackageOpen, Truck } from 'lucide-react';
|
||||
|
||||
import { PageHeader } from '@/components/page';
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs';
|
||||
@@ -30,6 +30,10 @@ import {
|
||||
useExportDjiboutiArrivalQueue,
|
||||
useExportDjiboutiTrainItems,
|
||||
} from '@/hooks/useWarehouses';
|
||||
import {
|
||||
useGenerateInterchangeDocument,
|
||||
useInterchangeDocuments,
|
||||
} from '@/hooks/useInterchangeDocuments';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import type {
|
||||
AutoUnloadExportDjiboutiResult,
|
||||
@@ -162,13 +166,22 @@ function ExportTrainDetailRows({
|
||||
}
|
||||
|
||||
export default function ExportDjiboutiUnloadingQueuePage() {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const { data: trains = [], isLoading } = useExportDjiboutiArrivalQueue();
|
||||
const { data: interchangeDocuments = [] } = useInterchangeDocuments({ direction: 'EXPORT' });
|
||||
const autoUnload = useAutoUnloadExportAtDjibouti();
|
||||
const generateInterchange = useGenerateInterchangeDocument();
|
||||
const [openScheduleId, setOpenScheduleId] = useState<string | null>(null);
|
||||
const [busyScheduleId, setBusyScheduleId] = useState<string | null>(null);
|
||||
const [historyInventoryId, setHistoryInventoryId] = useState<string | null>(null);
|
||||
|
||||
const interchangeBySchedule = new Map(
|
||||
interchangeDocuments
|
||||
.filter((doc) => doc.scheduleId && doc.status !== 'CANCELLED')
|
||||
.map((doc) => [doc.scheduleId as string, doc]),
|
||||
);
|
||||
|
||||
const unloadTrain = async (train: ExportTrain) => {
|
||||
setBusyScheduleId(train.scheduleId);
|
||||
try {
|
||||
@@ -179,6 +192,9 @@ export default function ExportDjiboutiUnloadingQueuePage() {
|
||||
const details = [
|
||||
result.skippedCount ? `${result.skippedCount} skipped` : '',
|
||||
result.failedCount ? `${result.failedCount} failed` : '',
|
||||
result.interchangeDocument
|
||||
? `Interchange document ${result.interchangeDocument.documentNo} generated`
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
@@ -198,6 +214,34 @@ export default function ExportDjiboutiUnloadingQueuePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const generateInterchangeDocument = async (train: ExportTrain) => {
|
||||
setBusyScheduleId(train.scheduleId);
|
||||
try {
|
||||
const res = await generateInterchange.mutateAsync({
|
||||
scheduleId: train.scheduleId,
|
||||
direction: 'EXPORT',
|
||||
handoverLocation: train.destination ?? 'Djibouti Port',
|
||||
handoverFrom: 'EDR',
|
||||
handoverTo: 'Djibouti Port Operator',
|
||||
portOperatorName: 'Doraleh Multipurpose Port',
|
||||
remarks: 'Generated after export unloading at Djibouti Port',
|
||||
});
|
||||
toast({
|
||||
title: 'Interchange document generated',
|
||||
description: res.data.documentNo,
|
||||
});
|
||||
navigate('/dashboard/interchange-documents');
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Interchange document generation failed',
|
||||
description: getErrorMessage(error),
|
||||
});
|
||||
} finally {
|
||||
setBusyScheduleId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container size="xxl" py="lg">
|
||||
<Breadcrumbs items={[{ label: 'Djibouti Arrival / Unloading Queue' }]} />
|
||||
@@ -255,6 +299,7 @@ export default function ExportDjiboutiUnloadingQueuePage() {
|
||||
<Table.Tbody>
|
||||
{trains.map((train: ExportTrain) => {
|
||||
const isOpen = openScheduleId === train.scheduleId;
|
||||
const interchangeDocument = interchangeBySchedule.get(train.scheduleId);
|
||||
return (
|
||||
<Fragment key={train.scheduleId}>
|
||||
<Table.Tr>
|
||||
@@ -304,6 +349,28 @@ export default function ExportDjiboutiUnloadingQueuePage() {
|
||||
>
|
||||
Auto Unload Export Items
|
||||
</Button>
|
||||
{interchangeDocument ? (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<Eye size={14} />}
|
||||
onClick={() => navigate('/dashboard/interchange-documents')}
|
||||
>
|
||||
View Document
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
color="green"
|
||||
leftSection={<FileText size={14} />}
|
||||
loading={busyScheduleId === train.scheduleId && generateInterchange.isPending}
|
||||
onClick={() => generateInterchangeDocument(train)}
|
||||
>
|
||||
Generate Interchange Document
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Loader,
|
||||
Modal,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { CheckCircle2, Eye, FileText, Search, XCircle } from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { PageContainer, PageHeader } from '@/components/page';
|
||||
import { VisualEmptyState, formatDate, formatNumber } from '@/components/warehouses';
|
||||
import {
|
||||
useAcknowledgeInterchangeDocument,
|
||||
useCancelInterchangeDocument,
|
||||
useDisputeInterchangeDocument,
|
||||
useInterchangeDocument,
|
||||
useInterchangeDocuments,
|
||||
} from '@/hooks/useInterchangeDocuments';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import type { InterchangeDocument, InterchangeDocumentStatus } from '@/types/interchangeDocument';
|
||||
|
||||
const statusColor: Record<InterchangeDocumentStatus, string> = {
|
||||
DRAFT: 'gray',
|
||||
GENERATED: 'blue',
|
||||
ACKNOWLEDGED: 'green',
|
||||
DISPUTED: 'red',
|
||||
CANCELLED: 'gray',
|
||||
};
|
||||
|
||||
const getErrorMessage = (error: unknown) => {
|
||||
if (error && typeof error === 'object' && 'response' in error) {
|
||||
const response = (error as { response?: { data?: { message?: unknown } } }).response;
|
||||
const message = response?.data?.message;
|
||||
if (Array.isArray(message)) return message.join(', ');
|
||||
if (typeof message === 'string') return message;
|
||||
}
|
||||
return error instanceof Error ? error.message : undefined;
|
||||
};
|
||||
|
||||
function DetailField({ label, value }: { label: string; value: ReactNode }) {
|
||||
return (
|
||||
<Stack gap={2}>
|
||||
<Text size="xs" c="dimmed">
|
||||
{label}
|
||||
</Text>
|
||||
<Text size="sm" fw={600}>
|
||||
{value || '-'}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function InterchangeDocumentDetail({ id }: { id: string }) {
|
||||
const { data: document, isLoading } = useInterchangeDocument(id);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Group justify="center" py="xl">
|
||||
<Loader />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
if (!document) return null;
|
||||
|
||||
const items = document.items ?? [];
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }}>
|
||||
<DetailField label="Document No" value={document.documentNo} />
|
||||
<DetailField label="Direction" value={document.direction} />
|
||||
<DetailField label="Schedule" value={document.scheduleId?.slice(0, 8)} />
|
||||
<DetailField label="Train No" value={document.trainNo} />
|
||||
<DetailField label="Handover Location" value={document.handoverLocation} />
|
||||
<DetailField label="Handover From" value={document.handoverFrom} />
|
||||
<DetailField label="Handover To" value={document.handoverTo} />
|
||||
<DetailField
|
||||
label="Status"
|
||||
value={
|
||||
<Badge variant="light" color={statusColor[document.status]}>
|
||||
{document.status}
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
<DetailField label="Generated At" value={formatDate(document.generatedAt)} />
|
||||
<DetailField label="Acknowledged At" value={formatDate(document.acknowledgedAt)} />
|
||||
<DetailField label="Customs Ref" value={document.customsReference} />
|
||||
<DetailField label="Manifest Ref" value={document.manifestReference} />
|
||||
</SimpleGrid>
|
||||
|
||||
<Table.ScrollContainer minWidth={980}>
|
||||
<Table striped highlightOnHover verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Booking Reference</Table.Th>
|
||||
<Table.Th>Item Type</Table.Th>
|
||||
<Table.Th>Container Number</Table.Th>
|
||||
<Table.Th>Seal Number</Table.Th>
|
||||
<Table.Th>Cargo Type</Table.Th>
|
||||
<Table.Th>Weight</Table.Th>
|
||||
<Table.Th>Quantity</Table.Th>
|
||||
<Table.Th>Wagon Number</Table.Th>
|
||||
<Table.Th>Condition</Table.Th>
|
||||
<Table.Th>Damage Description</Table.Th>
|
||||
<Table.Th>Remarks</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{items.map((item) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td>{item.bookingReference ?? item.bookingId?.slice(0, 8) ?? '-'}</Table.Td>
|
||||
<Table.Td>{item.itemType}</Table.Td>
|
||||
<Table.Td>{item.containerNumber ?? '-'}</Table.Td>
|
||||
<Table.Td>{item.sealNumber ?? '-'}</Table.Td>
|
||||
<Table.Td>{item.cargoType ?? item.cargoDescription ?? '-'}</Table.Td>
|
||||
<Table.Td>{formatNumber(item.weight)}</Table.Td>
|
||||
<Table.Td>{formatNumber(item.quantity)}</Table.Td>
|
||||
<Table.Td>{item.wagonNumber ?? '-'}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
variant="light"
|
||||
color={item.conditionStatus === 'GOOD' ? 'green' : item.conditionStatus === 'UNKNOWN' ? 'gray' : 'orange'}
|
||||
size="sm"
|
||||
>
|
||||
{item.conditionStatus}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{item.damageDescription ?? '-'}</Table.Td>
|
||||
<Table.Td>{item.remarks ?? '-'}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InterchangeDocumentsPage() {
|
||||
const { toast } = useToast();
|
||||
const [search, setSearch] = useState('');
|
||||
const [viewId, setViewId] = useState<string | null>(null);
|
||||
const filter = useMemo(() => ({ search: search.trim() || undefined }), [search]);
|
||||
const { data: documents = [], isLoading } = useInterchangeDocuments(filter);
|
||||
const acknowledge = useAcknowledgeInterchangeDocument();
|
||||
const dispute = useDisputeInterchangeDocument();
|
||||
const cancel = useCancelInterchangeDocument();
|
||||
|
||||
const run = async (fn: () => Promise<unknown>, title: string) => {
|
||||
try {
|
||||
await fn();
|
||||
toast({ title });
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Action failed', description: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
const acknowledgeDocument = (document: InterchangeDocument) => {
|
||||
const acknowledgedBy = window.prompt('Acknowledged by');
|
||||
if (!acknowledgedBy) return;
|
||||
run(
|
||||
() => acknowledge.mutateAsync({ id: document.id, acknowledgedBy }),
|
||||
'Interchange document acknowledged',
|
||||
);
|
||||
};
|
||||
|
||||
const disputeDocument = (document: InterchangeDocument) => {
|
||||
const remarks = window.prompt('Dispute reason');
|
||||
if (!remarks) return;
|
||||
run(() => dispute.mutateAsync({ id: document.id, remarks }), 'Interchange document disputed');
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="Interchange Documents"
|
||||
subtitle="Official freight handover documents with booking, container and cargo line items."
|
||||
/>
|
||||
|
||||
<Card withBorder radius="md" padding="lg">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Text fw={600}>{documents.length} document(s)</Text>
|
||||
<TextInput
|
||||
w={{ base: '100%', sm: 320 }}
|
||||
leftSection={<Search size={16} />}
|
||||
placeholder="Search documents"
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.currentTarget.value)}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{isLoading ? (
|
||||
<Group justify="center" py="xl">
|
||||
<Loader />
|
||||
</Group>
|
||||
) : documents.length === 0 ? (
|
||||
<VisualEmptyState
|
||||
variant="container"
|
||||
title="No interchange documents"
|
||||
description="Generated freight handover documents appear here."
|
||||
/>
|
||||
) : (
|
||||
<Table.ScrollContainer minWidth={1060}>
|
||||
<Table striped highlightOnHover verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Document No</Table.Th>
|
||||
<Table.Th>Direction</Table.Th>
|
||||
<Table.Th>Train No / Schedule</Table.Th>
|
||||
<Table.Th>Route</Table.Th>
|
||||
<Table.Th>Handover Location</Table.Th>
|
||||
<Table.Th>Handover From</Table.Th>
|
||||
<Table.Th>Handover To</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Generated At</Table.Th>
|
||||
<Table.Th ta="right">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{documents.map((document) => (
|
||||
<Table.Tr key={document.id}>
|
||||
<Table.Td>
|
||||
<Text fw={700} size="sm">
|
||||
{document.documentNo}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{document.direction}</Table.Td>
|
||||
<Table.Td>
|
||||
<Stack gap={0}>
|
||||
<Text size="sm">{document.trainNo ?? '-'}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{document.scheduleId?.slice(0, 8) ?? '-'}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Table.Td>
|
||||
<Table.Td>{document.routeId?.slice(0, 8) ?? '-'}</Table.Td>
|
||||
<Table.Td>{document.handoverLocation}</Table.Td>
|
||||
<Table.Td>{document.handoverFrom}</Table.Td>
|
||||
<Table.Td>{document.handoverTo}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge variant="light" color={statusColor[document.status]}>
|
||||
{document.status}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{formatDate(document.generatedAt)}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap">
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
leftSection={<Eye size={14} />}
|
||||
onClick={() => setViewId(document.id)}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
{document.status !== 'ACKNOWLEDGED' && document.status !== 'CANCELLED' ? (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
color="green"
|
||||
variant="light"
|
||||
leftSection={<CheckCircle2 size={14} />}
|
||||
onClick={() => acknowledgeDocument(document)}
|
||||
>
|
||||
Acknowledge
|
||||
</Button>
|
||||
) : null}
|
||||
{document.status !== 'CANCELLED' ? (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
color="orange"
|
||||
variant="light"
|
||||
leftSection={<FileText size={14} />}
|
||||
onClick={() => disputeDocument(document)}
|
||||
>
|
||||
Dispute
|
||||
</Button>
|
||||
) : null}
|
||||
{document.status === 'DRAFT' || document.status === 'GENERATED' ? (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
color="red"
|
||||
variant="light"
|
||||
leftSection={<XCircle size={14} />}
|
||||
onClick={() =>
|
||||
run(() => cancel.mutateAsync(document.id), 'Interchange document cancelled')
|
||||
}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
) : null}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Modal opened={Boolean(viewId)} onClose={() => setViewId(null)} title="Interchange Document" size="90%">
|
||||
{viewId ? <InterchangeDocumentDetail id={viewId} /> : null}
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user