fix issue

This commit is contained in:
Marshal
2026-08-20 18:10:29 +00:00
183 changed files with 14241 additions and 1265 deletions

View File

@@ -200,6 +200,11 @@ export enum InvoiceSource {
* per-booking link.
*/
ShippingLineCredit = "shipping_line_credit",
/**
* Ad-hoc extra charge finance raises against a booking (e.g. a fee not
* covered by an existing fee rule). `sourceId` is the charge id.
*/
AdditionalCharge = "additional_charge",
}
export enum SchedulingStatus {
@@ -887,6 +892,36 @@ export interface ClearanceDocRequest {
at: string;
}
// ── Additional charges (ad-hoc finance billing) ──────────────────────────────
/**
* DRAFT: staff is still editing, nothing sent. SENT: invoice issued to the
* customer (in-app + SMS + email). PAID: the invoice settled. CANCELLED:
* withdrawn before payment.
*/
export type AdditionalChargeStatus = "DRAFT" | "SENT" | "PAID" | "CANCELLED";
/** One ad-hoc extra charge finance raised against a booking. */
export interface AdditionalCharge {
id: string;
bookingId: string;
reason: string;
status: AdditionalChargeStatus;
amount: number;
currency: string;
file: { id: string; name: string; url: string } | null;
invoiceId: string | null;
invoiceNumber: string | null;
paymentReference: string | null;
createdByName: string | null;
createdAt: string;
sentByName: string | null;
sentAt: string | null;
paidAt: string | null;
cancelledAt: string | null;
cancelReason: string | null;
}
/** One entry of a clearance document's audit trail, oldest first. */
export interface ClearanceDocumentEvent {
type: "UPLOADED" | "RESUBMITTED" | "QUERIED" | "APPROVED";

View File

@@ -1,6 +1,24 @@
import { useEffect, useMemo, useState } from "react";
import { Autocomplete } from "@mantine/core";
import { Button } from "../button";
import { DataTableFooterProps } from "./types";
/**
* Upper bound on a hand-typed page size. Not a free choice — the server has to
* accept it. It is kept in step with `@Max` on the freight API's
* `PaginationQueryDto.pageSize` and with `MAX_PAGE_SIZE` in its
* `common/utils/pagination.util.ts`; raising it here alone turns the top preset
* into a 400 on every endpoint that validates against those.
*/
export const MAX_PAGE_SIZE = 500;
/** Clamp a typed page size to a whole number in [1, MAX_PAGE_SIZE]; null if unusable. */
export function clampPageSize(raw: string | number): number | null {
const parsed = Math.trunc(Number(raw));
if (!Number.isFinite(parsed) || parsed < 1) return null;
return Math.min(parsed, MAX_PAGE_SIZE);
}
export interface DataTableFooterOptions {
pageSizeOptions?: number[];
showPageSizeSelector?: boolean;
@@ -25,7 +43,7 @@ interface DataTableFooterComponentProps<
}
const defaultOptions: DataTableFooterOptions = {
pageSizeOptions: [5, 10, 25, 50],
pageSizeOptions: [5, 10, 25, 50, 100, 200, 500],
showPageSizeSelector: true,
showRowCount: true,
showPagination: true,
@@ -56,8 +74,36 @@ export function DataTableFooter<TData>({
const start = totalCount === 0 ? 0 : pageIndex * pageSize + 1;
const end = Math.min((pageIndex + 1) * pageSize, totalCount);
// Back to the first page: page 12 of 50-row pages does not exist once the
// rows per page becomes 500, and a manual-pagination consumer would happily
// request it. One state update, so consumers see a single fetch.
const handlePageSizeChange = (newPageSize: number) => {
table?.setPageSize(newPageSize);
table?.setPagination({ pageIndex: 0, pageSize: newPageSize });
};
// The presets, with the active size folded in when it was typed by hand, so
// the dropdown always contains what the input shows.
const sizeOptions = useMemo(() => {
const presets = opts.pageSizeOptions ?? [];
const all = presets.includes(pageSize) ? presets : [...presets, pageSize];
return [...all].sort((a, b) => a - b).map(String);
}, [opts.pageSizeOptions, pageSize]);
const [sizeDraft, setSizeDraft] = useState(`${pageSize}`);
// Keep the input honest when the page size changes from anywhere else.
useEffect(() => {
setSizeDraft(`${pageSize}`);
}, [pageSize]);
const commitPageSize = (raw: string) => {
const size = clampPageSize(raw);
if (size === null) {
setSizeDraft(`${pageSize}`);
return;
}
setSizeDraft(`${size}`);
if (size !== pageSize) handlePageSizeChange(size);
};
return (
@@ -69,18 +115,23 @@ export function DataTableFooter<TData>({
<label htmlFor="page-size" className="font-medium text-slate-700">
{labels.rowsPerPage}
</label>
<select
<Autocomplete
id="page-size"
value={pageSize}
onChange={(e) => handlePageSizeChange(Number(e.target.value))}
className="h-9 rounded-xl border border-slate-200 bg-white px-3 text-sm text-slate-700 outline-none transition focus:border-[#33578D]/50 focus:ring-2 focus:ring-[#33578D]/20"
>
{opts.pageSizeOptions?.map((size) => (
<option key={size} value={size}>
{size}
</option>
))}
</select>
aria-label={labels.rowsPerPage}
size="sm"
w={96}
inputMode="numeric"
value={sizeDraft}
data={sizeOptions}
comboboxProps={{ position: "top", withinPortal: true }}
onChange={setSizeDraft}
onOptionSubmit={commitPageSize}
onBlur={() => commitPageSize(sizeDraft)}
onKeyDown={(e) => {
if (e.key === "Enter") commitPageSize(sizeDraft);
if (e.key === "Escape") setSizeDraft(`${pageSize}`);
}}
/>
</>
)}
{opts.showRowCount && (