This commit is contained in:
Marshal
2026-08-08 14:02:58 +00:00
104 changed files with 8507 additions and 962 deletions

View File

@@ -149,6 +149,8 @@ import {
import { containerTypesService } from "./container-types.service";
import { containerService, type Container } from "./containerService";
import { customersService } from "./customers.service";
import { eimsService } from "./eims.service";
import type { EimsInvoiceStatusView, EimsVerifyResult } from "@/types/eims";
import { invoicesService } from "./invoices.service";
import { dropdownSettingsService } from "./dropdownSettings.service";
import { fileUploadSettingsService } from "./fileUploadSettings.service";
@@ -2938,6 +2940,36 @@ export const api = {
// Settling the invoice also advances the booking, so refresh both trees.
() => [QUERY_KEYS.INVOICES.ROOT, QUERY_KEYS.BOOKINGS.ROOT],
),
eimsStatus: endpoint<{ id: string }, EimsInvoiceStatusView>(
"invoices",
"eimsStatus",
({ id }) => eimsService.status(id),
({ id }) => QUERY_KEYS.INVOICES.eimsStatus(id),
),
// Both mutations refresh the filing panel; register also moves the invoice's own row.
eimsRegister: endpoint<{ id: string }, EimsInvoiceStatusView>(
"invoices",
"eimsRegister",
({ id }) => eimsService.register(id),
undefined,
({ id }) => [QUERY_KEYS.INVOICES.eimsStatus(id), QUERY_KEYS.INVOICES.byId(id)],
),
eimsVerify: endpoint<{ id: string }, EimsVerifyResult>(
"invoices",
"eimsVerify",
({ id }) => eimsService.verify(id),
),
eimsResolve: endpoint<{ id: string; irn?: string; discard?: boolean }, EimsInvoiceStatusView>(
"invoices",
"eimsResolve",
({ id, irn, discard }) => eimsService.resolve(id, { irn, discard }),
undefined,
({ id }) => [QUERY_KEYS.INVOICES.eimsStatus(id), QUERY_KEYS.INVOICES.byId(id)],
),
},
overview: {

View File

@@ -0,0 +1,39 @@
import { api as apiClient } from "@/auth/http";
import { URL_CONSTANTS } from "@/constants/URLS";
import type { EimsInvoiceStatusView, EimsVerifyResult } from "@/types/eims";
/**
* MoR EIMS filing actions on an invoice.
*
* Registration is irreversible at the tax authority, so these are admin actions rather than part
* of the ordinary invoice screen: the normal production path is the API's cron sweep.
*/
export const eimsService = {
status(invoiceId: string): Promise<EimsInvoiceStatusView> {
return apiClient
.get<EimsInvoiceStatusView>(URL_CONSTANTS.EIMS.STATUS(invoiceId))
.then((r) => r.data);
},
register(invoiceId: string): Promise<EimsInvoiceStatusView> {
return apiClient
.post<EimsInvoiceStatusView>(URL_CONSTANTS.EIMS.REGISTER(invoiceId))
.then((r) => r.data);
},
verify(invoiceId: string): Promise<EimsVerifyResult> {
return apiClient
.post<EimsVerifyResult>(URL_CONSTANTS.EIMS.VERIFY(invoiceId))
.then((r) => r.data);
},
/** Record an IRN confirmed with MoR, or discard the attempt. Clears the system-wide block. */
resolve(
invoiceId: string,
input: { irn?: string; discard?: boolean },
): Promise<EimsInvoiceStatusView> {
return apiClient
.post<EimsInvoiceStatusView>(URL_CONSTANTS.EIMS.RESOLVE(invoiceId), input)
.then((r) => r.data);
},
};

View File

@@ -0,0 +1,98 @@
import type {
PortalMediaKind,
SupportDocPayload,
SupportDocSlug,
SupportDocumentDetail,
SupportDocVersionDetail,
SupportDocVersionSummary,
} from "@edr/types";
import { api as client } from "../auth/http";
const ROOT = "/support-content";
const BASE = `${ROOT}/documents`;
/**
* Customer-facing help/FAQ/legal copy for the freight portal. The client's
* response interceptor already unwraps the `{ success, data }` envelope, so
* every method is a one-liner.
*/
export const portalContentService = {
async getBySlug(slug: SupportDocSlug): Promise<SupportDocumentDetail> {
const { data } = await client.get<SupportDocumentDetail>(`${BASE}/${slug}`);
return data;
},
/**
* Replaces the document's whole payload. Whole-payload rather than per-field
* on purpose: one Save becomes exactly one version, which is what keeps the
* history list readable.
*/
async update(
slug: SupportDocSlug,
payload: SupportDocPayload,
note?: string,
): Promise<SupportDocumentDetail> {
const { data } = await client.patch<SupportDocumentDetail>(
`${BASE}/${slug}`,
{ payload, note },
);
return data;
},
async listVersions(slug: SupportDocSlug): Promise<SupportDocVersionSummary[]> {
const { data } = await client.get<SupportDocVersionSummary[]>(
`${BASE}/${slug}/versions`,
);
return data;
},
async getVersion(
slug: SupportDocSlug,
version: number,
): Promise<SupportDocVersionDetail> {
const { data } = await client.get<SupportDocVersionDetail>(
`${BASE}/${slug}/versions/${version}`,
);
return data;
},
/**
* Uploads an image or video and returns its object *key*. The key is what
* gets saved in the document; `url` is only for showing the editor a preview
* right now, and expires.
*/
async uploadMedia(
file: File,
): Promise<{ key: string; kind: PortalMediaKind; url: string }> {
const form = new FormData();
form.append("file", file);
const { data } = await client.post<{
key: string;
kind: PortalMediaKind;
url: string;
}>(`${ROOT}/media`, form);
return data;
},
/** Resolves one stored key to a temporary URL, for editor previews. */
async mediaUrl(key: string): Promise<string> {
const { data } = await client.get<{ url: string }>(`${ROOT}/media-url`, {
params: { key },
});
return data.url;
},
/** Re-saves an old payload as a new version — never destructive. */
async restore(
slug: SupportDocSlug,
version: number,
): Promise<SupportDocumentDetail> {
const { data } = await client.post<SupportDocumentDetail>(
`${BASE}/${slug}/versions/${version}/restore`,
{},
);
return data;
},
};