disputes only before acknowledgement, registered disputes immutable (view-only), cancel action removed end-to-end, and internal UUIDs removed from the document, detail view, and list.

This commit is contained in:
Hagernesh
2026-07-23 12:02:57 +00:00
parent 73f36fe46e
commit cee3a4f429
5 changed files with 13 additions and 62 deletions

View File

@@ -54,11 +54,4 @@ export class InterchangeDocumentsController {
dispute(@Param('id', ParseUUIDPipe) id: string, @Body() dto: DisputeInterchangeDocumentDto) {
return this.service.dispute(id, dto);
}
@Patch(':id/cancel')
@BookingStaff(FREIGHT_PERMS.interchangeDocuments.cancel)
@ApiOperation({ summary: 'Cancel a draft/generated interchange document' })
cancel(@Param('id', ParseUUIDPipe) id: string) {
return this.service.cancel(id);
}
}

View File

@@ -203,11 +203,12 @@ export class InterchangeDocumentsService {
async dispute(id: string, dto: DisputeInterchangeDocumentDto): Promise<InterchangeDocument> {
const document = await this.findOne(id);
// A dispute can only be raised on a live handover — a GENERATED or already
// ACKNOWLEDGED document. CANCELLED and already-DISPUTED are terminal here.
if (!['GENERATED', 'ACKNOWLEDGED'].includes(document.status)) {
// A dispute can only be raised BEFORE the handover is acknowledged — an
// acknowledged document is settled. DISPUTED itself is terminal and
// read-only: the registered dispute cannot be re-raised or overwritten.
if (document.status !== 'GENERATED') {
throw new BadRequestException(
`Interchange document in ${document.status} status cannot be disputed (must be GENERATED or ACKNOWLEDGED)`,
`Interchange document in ${document.status} status cannot be disputed (must be GENERATED — an acknowledged handover is settled, a registered dispute is read-only)`,
);
}
await this.dataSource.getRepository(InterchangeDocument).update(id, {
@@ -217,15 +218,6 @@ export class InterchangeDocumentsService {
return this.findOne(id);
}
async cancel(id: string): Promise<InterchangeDocument> {
const document = await this.findOne(id);
if (!['DRAFT', 'GENERATED'].includes(document.status)) {
throw new BadRequestException(`Interchange document ${document.status} cannot be cancelled`);
}
await this.dataSource.getRepository(InterchangeDocument).update(id, { status: 'CANCELLED' });
return this.findOne(id);
}
private async getScheduleSnapshot(scheduleId: string): Promise<ScheduleSnapshot> {
const [schedule] = await this.dataSource.query(
`SELECT ts.id,

View File

@@ -67,10 +67,3 @@ export function useDisputeInterchangeDocument() {
});
}
export function useCancelInterchangeDocument() {
const onSuccess = useInterchangeInvalidation();
return useMutation({
mutationFn: (id: string) => interchangeDocumentsService.cancel(id),
onSuccess,
});
}

View File

@@ -12,7 +12,7 @@ import {
Text,
TextInput,
} from '@mantine/core';
import { CheckCircle2, Download, Eye, FileText, Printer, Search, XCircle } from 'lucide-react';
import { CheckCircle2, Download, Eye, FileText, Printer, Search } from 'lucide-react';
import type { ReactNode } from 'react';
import { DataTable, type ColumnDef } from '@edr/ui-common';
@@ -21,7 +21,6 @@ import { PageContainer, PageHeader } from '@/components/page';
import { VisualEmptyState, formatDate, formatNumber } from '@/components/warehouses';
import {
useAcknowledgeInterchangeDocument,
useCancelInterchangeDocument,
useDisputeInterchangeDocument,
useInterchangeDocument,
useInterchangeDocuments,
@@ -66,7 +65,7 @@ const buildPrintableInterchangeHtml = (document: InterchangeDocument) => {
(item, index) => `
<tr>
<td>${index + 1}</td>
<td>${escapeHtml(item.bookingReference ?? item.bookingId?.slice(0, 8))}</td>
<td>${escapeHtml(item.bookingReference)}</td>
<td>${escapeHtml(item.itemType)}</td>
<td>${escapeHtml(item.containerNumber)}</td>
<td>${escapeHtml(item.sealNumber)}</td>
@@ -118,7 +117,6 @@ const buildPrintableInterchangeHtml = (document: InterchangeDocument) => {
<div class="grid">
<div class="field"><div class="label">Direction</div><div class="value">${escapeHtml(document.direction)}</div></div>
<div class="field"><div class="label">Train No</div><div class="value">${escapeHtml(document.trainNo)}</div></div>
<div class="field"><div class="label">Schedule</div><div class="value">${escapeHtml(document.scheduleId)}</div></div>
<div class="field"><div class="label">Handover Location</div><div class="value">${escapeHtml(document.handoverLocation)}</div></div>
<div class="field"><div class="label">Handover From</div><div class="value">${escapeHtml(document.handoverFrom)}</div></div>
<div class="field"><div class="label">Handover To</div><div class="value">${escapeHtml(document.handoverTo)}</div></div>
@@ -191,7 +189,6 @@ function InterchangeDocumentDetail({ id }: { id: string }) {
<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} />
@@ -232,7 +229,7 @@ function InterchangeDocumentDetail({ id }: { id: string }) {
<Table.Tbody>
{items.map((item) => (
<Table.Tr key={item.id}>
<Table.Td>{item.bookingReference ?? item.bookingId?.slice(0, 8) ?? '-'}</Table.Td>
<Table.Td>{item.bookingReference ?? '-'}</Table.Td>
<Table.Td>{item.itemType}</Table.Td>
<Table.Td>{item.containerNumber ?? '-'}</Table.Td>
<Table.Td>{item.sealNumber ?? '-'}</Table.Td>
@@ -268,7 +265,6 @@ export default function InterchangeDocumentsPage() {
const { data: documents = [], isLoading } = useInterchangeDocuments(filter);
const acknowledge = useAcknowledgeInterchangeDocument();
const dispute = useDisputeInterchangeDocument();
const cancel = useCancelInterchangeDocument();
const getPrintableDocument = async (interchangeDocument: InterchangeDocument) => {
if (interchangeDocument.items?.length) return interchangeDocument;
@@ -336,19 +332,7 @@ export default function InterchangeDocumentsPage() {
),
},
{ id: 'direction', header: 'Direction', cell: ({ row }) => row.original.direction },
{
id: 'train',
header: 'Train No / Schedule',
cell: ({ row }) => (
<Stack gap={0}>
<Text size="sm">{row.original.trainNo ?? '-'}</Text>
<Text size="xs" c="dimmed">
{row.original.scheduleId?.slice(0, 8) ?? '-'}
</Text>
</Stack>
),
},
{ id: 'route', header: 'Route', cell: ({ row }) => row.original.routeId?.slice(0, 8) ?? '-' },
{ id: 'train', header: 'Train No', cell: ({ row }) => row.original.trainNo ?? '-' },
{ id: 'handoverLocation', header: 'Handover Location', cell: ({ row }) => row.original.handoverLocation },
{ id: 'handoverFrom', header: 'Handover From', cell: ({ row }) => row.original.handoverFrom },
{ id: 'handoverTo', header: 'Handover To', cell: ({ row }) => row.original.handoverTo },
@@ -390,7 +374,7 @@ export default function InterchangeDocumentsPage() {
>
View
</Button>
{doc.status !== 'ACKNOWLEDGED' && doc.status !== 'CANCELLED' ? (
{doc.status === 'GENERATED' ? (
<Button
size="compact-xs"
color="green"
@@ -423,7 +407,9 @@ export default function InterchangeDocumentsPage() {
</Button>
</>
) : null}
{doc.status !== 'CANCELLED' ? (
{/* Disputes are raised BEFORE acknowledgement; a registered dispute
(DISPUTED) is read-only — its row offers View only. */}
{doc.status === 'GENERATED' ? (
<Button
size="compact-xs"
color="orange"
@@ -434,17 +420,6 @@ export default function InterchangeDocumentsPage() {
Dispute
</Button>
) : null}
{doc.status === 'DRAFT' || doc.status === 'GENERATED' ? (
<Button
size="compact-xs"
color="red"
variant="light"
leftSection={<XCircle size={14} />}
onClick={() => run(() => cancel.mutateAsync(doc.id), 'Interchange document cancelled')}
>
Cancel
</Button>
) : null}
</Group>
);
},

View File

@@ -28,6 +28,4 @@ export const interchangeDocumentsService = {
apiClient.patch<InterchangeDocument>(URL_CONSTANTS.INTERCHANGE_DOCUMENTS.ACKNOWLEDGE(id), payload),
dispute: (id: string, payload: { remarks: string }) =>
apiClient.patch<InterchangeDocument>(URL_CONSTANTS.INTERCHANGE_DOCUMENTS.DISPUTE(id), payload),
cancel: (id: string) =>
apiClient.patch<InterchangeDocument>(URL_CONSTANTS.INTERCHANGE_DOCUMENTS.CANCEL(id), {}),
};