feat: implement wagon transfer management modals and page

- Add TransferFulfillModal for fulfilling wagon transfer requests.
- Create TransferRequestFormModal for filing new wagon transfer requests.
- Introduce TransferCloseShortModal for closing requests that cannot be fully fulfilled.
- Develop WagonTransfersPage to manage and display wagon transfer requests.
- Implement utility functions for handling wagon transfer request data and UI components.
- Enhance UI with Mantine components for better user experience.
This commit is contained in:
Marshal
2026-07-26 15:11:50 +00:00
parent 9a1c8e5603
commit 9b13fa2ac6
40 changed files with 2584 additions and 809 deletions

View File

@@ -110,10 +110,93 @@ Cypress.Commands.add("mantineSelect", (label: string | RegExp, option: string |
.then((id) => {
cy.get(`[id="${id}"]`).click({ force: true });
});
// :visible — closed dropdowns can linger in the DOM, and two selects on one
// page may list the same option text (e.g. the intercity wizard's origin +
// destination both list every Ethiopian yard).
cy.get('[role="option"]:visible').contains(option).click();
// Scope to the OPEN listbox: closed dropdowns linger in the DOM, and two
// selects on one page may list the same option text (e.g. the intercity
// wizard's origin + destination both list every Ethiopian yard).
//
// The scope has to be the listbox rather than the options themselves. A long
// list scrolls inside a max-height dropdown, and Cypress counts the clipped
// rows as not-visible — matching on `[role="option"]:visible` silently drops
// whatever sits past the fold (this hid the alphabetically-last trains).
// force: the click still has to land on a row that needs scrolling to.
cy.get('[role="listbox"]:visible')
.last()
.contains('[role="option"]', option)
.click({ force: true });
});
/**
* Fill the accept-contract modal's validity window. The modal used to offer a
* dropdown of configured durations that defaulted to the first option; it now
* takes explicit Start/End dates and pre-fills neither, so "Accept & start
* approval" stays disabled until both are set.
*
* Mantine's DateInput parses typed text with its valueFormat, which defaults to
* "MMMM D, YYYY" — the same shape en-US toLocaleDateString produces.
*/
Cypress.Commands.add("acceptValidityWindow", (days = 365) => {
const start = new Date();
const end = new Date(start.getTime() + days * 24 * 60 * 60 * 1000);
const asInput = (d: Date) =>
d.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
for (const [label, value] of [
["Start date", start],
["End date", end],
] as const) {
cy.contains("label", label)
.invoke("attr", "for")
.then((id) => {
cy.get(`[id="${id}"]`)
.clear({ force: true })
.type(asInput(value), { force: true })
// DateInput commits on blur; it also closes the calendar popover,
// which would otherwise sit over the submit button.
.blur();
});
}
});
/**
* Attach a company stamp in the open sign-contract modal. The stamp became a
* REQUIRED field on signing — "Continue to verification" stays disabled without
* one — and StampUpload only checks the MIME type and size before reading the
* file as a data URL, so the smallest valid PNG is enough. The input is
* `hidden` (a dropzone drives it), hence force.
*/
const STAMP_PNG =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
Cypress.Commands.add("uploadCompanyStamp", () => {
cy.get('.mantine-Modal-content input[type="file"]')
.first()
.selectFile(
{
contents: Cypress.Buffer.from(STAMP_PNG, "base64"),
fileName: "stamp.png",
mimeType: "image/png",
},
{ force: true },
);
});
/**
* Fill the per-booking "Cargo description" on the new-shipment form. It is
* REQUIRED for container shipments (it moved from the contract to the booking),
* and the form validates through react-hook-form's handleSubmit — so leaving it
* blank aborts silently: no price modal, no request, no error toast.
* No-op for bulk shipments, which have no such field.
*/
Cypress.Commands.add("fillCargoDescription", (text = "Electronics") => {
cy.get("body").then(($b) => {
const field = $b.find('[placeholder^="e.g. Electronics"]');
if (!field.length) return;
cy.wrap(field.first()).clear({ force: true }).type(text, { force: true });
});
});
/** Type a 6-digit code into a Mantine PinInput. */
@@ -168,6 +251,12 @@ declare global {
getOtp(target: string): Chainable<string>;
/** Open a Mantine Select by label, pick an option. */
mantineSelect(label: string | RegExp, option: string | RegExp): Chainable<void>;
/** Fill the accept-contract modal's Start/End validity dates. */
acceptValidityWindow(days?: number): Chainable<void>;
/** Attach the required company stamp in the sign-contract modal. */
uploadCompanyStamp(): Chainable<void>;
/** Fill the required per-booking cargo description (container only). */
fillCargoDescription(text?: string): Chainable<void>;
/** Fill a Mantine PinInput with a code. */
typeOtp(code: string): Chainable<void>;
/** Scribble on the signature-pad canvas inside the open modal. */