Merge pull request #939 from Tria-plc/Truckmaintenance

disputes only before acknowledgement, registered disputes immutable (…
This commit is contained in:
Hagernesh Tadesse
2026-07-23 15:07:05 +03:00
committed by GitHub
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) { dispute(@Param('id', ParseUUIDPipe) id: string, @Body() dto: DisputeInterchangeDocumentDto) {
return this.service.dispute(id, dto); 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> { async dispute(id: string, dto: DisputeInterchangeDocumentDto): Promise<InterchangeDocument> {
const document = await this.findOne(id); const document = await this.findOne(id);
// A dispute can only be raised on a live handover — a GENERATED or already // A dispute can only be raised BEFORE the handover is acknowledged — an
// ACKNOWLEDGED document. CANCELLED and already-DISPUTED are terminal here. // acknowledged document is settled. DISPUTED itself is terminal and
if (!['GENERATED', 'ACKNOWLEDGED'].includes(document.status)) { // read-only: the registered dispute cannot be re-raised or overwritten.
if (document.status !== 'GENERATED') {
throw new BadRequestException( 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, { await this.dataSource.getRepository(InterchangeDocument).update(id, {
@@ -217,15 +218,6 @@ export class InterchangeDocumentsService {
return this.findOne(id); 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> { private async getScheduleSnapshot(scheduleId: string): Promise<ScheduleSnapshot> {
const [schedule] = await this.dataSource.query( const [schedule] = await this.dataSource.query(
`SELECT ts.id, `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, Text,
TextInput, TextInput,
} from '@mantine/core'; } 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 type { ReactNode } from 'react';
import { DataTable, type ColumnDef } from '@edr/ui-common'; 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 { VisualEmptyState, formatDate, formatNumber } from '@/components/warehouses';
import { import {
useAcknowledgeInterchangeDocument, useAcknowledgeInterchangeDocument,
useCancelInterchangeDocument,
useDisputeInterchangeDocument, useDisputeInterchangeDocument,
useInterchangeDocument, useInterchangeDocument,
useInterchangeDocuments, useInterchangeDocuments,
@@ -66,7 +65,7 @@ const buildPrintableInterchangeHtml = (document: InterchangeDocument) => {
(item, index) => ` (item, index) => `
<tr> <tr>
<td>${index + 1}</td> <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.itemType)}</td>
<td>${escapeHtml(item.containerNumber)}</td> <td>${escapeHtml(item.containerNumber)}</td>
<td>${escapeHtml(item.sealNumber)}</td> <td>${escapeHtml(item.sealNumber)}</td>
@@ -118,7 +117,6 @@ const buildPrintableInterchangeHtml = (document: InterchangeDocument) => {
<div class="grid"> <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">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">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 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 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> <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 }}> <SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }}>
<DetailField label="Document No" value={document.documentNo} /> <DetailField label="Document No" value={document.documentNo} />
<DetailField label="Direction" value={document.direction} /> <DetailField label="Direction" value={document.direction} />
<DetailField label="Schedule" value={document.scheduleId?.slice(0, 8)} />
<DetailField label="Train No" value={document.trainNo} /> <DetailField label="Train No" value={document.trainNo} />
<DetailField label="Handover Location" value={document.handoverLocation} /> <DetailField label="Handover Location" value={document.handoverLocation} />
<DetailField label="Handover From" value={document.handoverFrom} /> <DetailField label="Handover From" value={document.handoverFrom} />
@@ -232,7 +229,7 @@ function InterchangeDocumentDetail({ id }: { id: string }) {
<Table.Tbody> <Table.Tbody>
{items.map((item) => ( {items.map((item) => (
<Table.Tr key={item.id}> <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.itemType}</Table.Td>
<Table.Td>{item.containerNumber ?? '-'}</Table.Td> <Table.Td>{item.containerNumber ?? '-'}</Table.Td>
<Table.Td>{item.sealNumber ?? '-'}</Table.Td> <Table.Td>{item.sealNumber ?? '-'}</Table.Td>
@@ -268,7 +265,6 @@ export default function InterchangeDocumentsPage() {
const { data: documents = [], isLoading } = useInterchangeDocuments(filter); const { data: documents = [], isLoading } = useInterchangeDocuments(filter);
const acknowledge = useAcknowledgeInterchangeDocument(); const acknowledge = useAcknowledgeInterchangeDocument();
const dispute = useDisputeInterchangeDocument(); const dispute = useDisputeInterchangeDocument();
const cancel = useCancelInterchangeDocument();
const getPrintableDocument = async (interchangeDocument: InterchangeDocument) => { const getPrintableDocument = async (interchangeDocument: InterchangeDocument) => {
if (interchangeDocument.items?.length) return 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: 'direction', header: 'Direction', cell: ({ row }) => row.original.direction },
{ { id: 'train', header: 'Train No', cell: ({ row }) => row.original.trainNo ?? '-' },
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: 'handoverLocation', header: 'Handover Location', cell: ({ row }) => row.original.handoverLocation }, { id: 'handoverLocation', header: 'Handover Location', cell: ({ row }) => row.original.handoverLocation },
{ id: 'handoverFrom', header: 'Handover From', cell: ({ row }) => row.original.handoverFrom }, { id: 'handoverFrom', header: 'Handover From', cell: ({ row }) => row.original.handoverFrom },
{ id: 'handoverTo', header: 'Handover To', cell: ({ row }) => row.original.handoverTo }, { id: 'handoverTo', header: 'Handover To', cell: ({ row }) => row.original.handoverTo },
@@ -390,7 +374,7 @@ export default function InterchangeDocumentsPage() {
> >
View View
</Button> </Button>
{doc.status !== 'ACKNOWLEDGED' && doc.status !== 'CANCELLED' ? ( {doc.status === 'GENERATED' ? (
<Button <Button
size="compact-xs" size="compact-xs"
color="green" color="green"
@@ -423,7 +407,9 @@ export default function InterchangeDocumentsPage() {
</Button> </Button>
</> </>
) : null} ) : 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 <Button
size="compact-xs" size="compact-xs"
color="orange" color="orange"
@@ -434,17 +420,6 @@ export default function InterchangeDocumentsPage() {
Dispute Dispute
</Button> </Button>
) : null} ) : 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> </Group>
); );
}, },

View File

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