feat(eims): resolve buyer geography from the MoR location master

BuyerDetails Country/Region/City/Wereda now resolve from the Ministry's own
EIMS_COUNTRY_REGION_VW master instead of the EIMS_BUYER_*_CODES env maps and
the ethiopia-geo-codes table. Both invented their codes and looked names up
globally, so KERSA/GORO/BABILE/BURE — each present in several zones with
different LOCALITY_NOs — could be filed against the wrong jurisdiction.

Resolution is hierarchical and refuses to guess: an unknown or ambiguous
address raises a local validation error naming the level that failed, and
never selects the first matching row. Spelling differences between EDR and
MoR live in a reviewed, parent-scoped alias layer; the dataset itself stays
verbatim so it remains traceable to the Ministry sheet.

Resolution now runs before the counter reservation in both the single and
bulk paths, so a bad company address no longer burns an EIMS sequence number.

Adds eims:import-locations to regenerate the dataset from a future workbook,
reporting duplicate rows and same-hierarchy code conflicts.
This commit is contained in:
Hagernesh
2026-08-24 04:23:47 +00:00
parent d285006114
commit c937290019
25 changed files with 1150 additions and 566 deletions

View File

@@ -780,6 +780,8 @@ export const URL_CONSTANTS = {
`/import-operations/empty-container-returns/${id}/status`,
EMPTY_CONTAINER_RETURNS_LOAD_ON_TRAIN:
"/import-operations/empty-container-returns/load-on-train",
EMPTY_CONTAINER_RETURN_DOCUMENT: (id: string) =>
`/import-operations/empty-container-returns/${id}/document`,
},
VEHICLES: {

View File

@@ -19,12 +19,14 @@ import {
Select,
Checkbox,
} from "@mantine/core";
import { ChevronDown, ChevronRight, History } from "lucide-react";
import { ChevronDown, ChevronRight, FileText, History } from "lucide-react";
import { DataTable, type ColumnDef } from "@edr/ui-common";
import { PageContainer, PageHeader } from "@/components/page";
import ListControls from "@/components/common/ListControls";
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
import { extractDownloadErrorMessage } from "@/components/warehouses/options";
import { openPdfBlob } from "@/components/warehouses/pdf";
import { OverviewHorizontalBarChart } from "@/components/overview/OverviewHorizontalBarChart";
import { OverviewStackedBarChart } from "@/components/overview/OverviewStackedBarChart";
import { useListControls, toDayString } from "@/hooks/useListControls";
@@ -103,6 +105,25 @@ export default function ContainerReturnsPage() {
const [activeKey, setActiveKey] = useState<string | null>(null);
const [historyRow, setHistoryRow] = useState<any | null>(null);
const [allocateRow, setAllocateRow] = useState<EmptyContainerReturn | null>(null);
const [documentBusyId, setDocumentBusyId] = useState<string | null>(null);
const viewInterchangeDocument = async (ret: EmptyContainerReturn) => {
setDocumentBusyId(ret.id);
const pdfWindow = window.open("", "_blank");
try {
const response = await importOperationsService.downloadEquipmentInterchangeDocument(ret.id);
openPdfBlob(response.data, `equipment-interchange-${ret.containerNumber}.pdf`, pdfWindow);
} catch (error) {
pdfWindow?.close();
toast({
variant: "destructive",
title: "Could not open interchange receipt",
description: await extractDownloadErrorMessage(error),
});
} finally {
setDocumentBusyId(null);
}
};
const { data: unloadedQueue = [], isLoading: queueLoading } = useQuery({
queryKey: ["import-unloaded-queue"],
@@ -416,6 +437,15 @@ export default function ContainerReturnsPage() {
const nextStatus = RETURN_STATUS_ORDER[RETURN_STATUS_ORDER.indexOf(ret.status) + 1];
return (
<Group gap="xs" justify="flex-end" wrap="nowrap">
<ActionIcon
variant="subtle"
color="gray"
onClick={() => void viewInterchangeDocument(ret)}
loading={documentBusyId === ret.id}
title="View equipment interchange receipt"
>
<FileText size={14} />
</ActionIcon>
<ActionIcon
variant="subtle"
color="gray"

View File

@@ -144,4 +144,10 @@ export const importOperationsService = {
);
return unwrap(response.data);
},
/** Equipment interchange receipt — the doc handed to the customer at handover. */
downloadEquipmentInterchangeDocument: (id: string) =>
client.get<Blob>(URL_CONSTANTS.IMPORT_OPERATIONS.EMPTY_CONTAINER_RETURN_DOCUMENT(id), {
responseType: 'blob',
}),
};