mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
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:
@@ -95,7 +95,8 @@ describe("contract lifecycle: creation to finalization", { retries: 0 }, () => {
|
||||
withContract((c) => cy.visit(`/dashboard/contract-requests/${c.id}`));
|
||||
|
||||
cy.contains("button", "Accept for approval", { timeout: 20000 }).click();
|
||||
// Validity defaults to the first configured option in the accept modal.
|
||||
// The modal takes an explicit validity window and pre-fills neither date.
|
||||
cy.acceptValidityWindow();
|
||||
cy.contains("button", "Accept & start approval", { timeout: 20000 })
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
@@ -146,7 +147,11 @@ describe("contract lifecycle: creation to finalization", { retries: 0 }, () => {
|
||||
);
|
||||
cy.wait(500).then(() => {
|
||||
cy.get("body").then(($b) => {
|
||||
if ($b.text().includes("I have read the entire contract")) return;
|
||||
// Probe the description that only renders once hasScrolledToBottom
|
||||
// flips — the consent LABEL is always in the DOM (just its checkbox
|
||||
// is disabled), so matching on it raced ahead of the scroll handler
|
||||
// and left the Sign button disabled.
|
||||
if ($b.text().includes("You may now sign the contract")) return;
|
||||
expect(attempt, "consent bar unlocked").to.be.lessThan(20);
|
||||
unlockConsent(attempt + 1);
|
||||
});
|
||||
@@ -154,7 +159,12 @@ describe("contract lifecycle: creation to finalization", { retries: 0 }, () => {
|
||||
};
|
||||
unlockConsent(0);
|
||||
|
||||
cy.contains("I have read the entire contract", { timeout: 15000 }).click();
|
||||
// Check the input itself — clicking the label text lands on the Checkbox's
|
||||
// label span, which does not toggle it, leaving Sign disabled.
|
||||
cy.contains("I have read the entire contract", { timeout: 15000 })
|
||||
.closest(".mantine-Checkbox-root")
|
||||
.find('input[type="checkbox"]')
|
||||
.check({ force: true });
|
||||
cy.contains("button", /^Sign contract$|^Approve & sign$/).click();
|
||||
|
||||
// Signature modal: name + drawn signature.
|
||||
@@ -164,6 +174,7 @@ describe("contract lifecycle: creation to finalization", { retries: 0 }, () => {
|
||||
cy.get(`[id="${id}"]`).clear().type("Demo User");
|
||||
});
|
||||
cy.drawSignature();
|
||||
cy.uploadCompanyStamp();
|
||||
cy.contains("button", "Continue to verification").click();
|
||||
|
||||
// OTP modal — code goes to the signer's registered contacts (email only
|
||||
@@ -191,6 +202,8 @@ describe("contract lifecycle: creation to finalization", { retries: 0 }, () => {
|
||||
cy.get(`[id="${id}"]`).clear().type("EDR Marketer");
|
||||
});
|
||||
cy.drawSignature();
|
||||
// Staff counter-sign gates on a stamp too, same as the customer's modal.
|
||||
cy.uploadCompanyStamp();
|
||||
// Scoped to the modal — the toolbar behind it has its own "Approve & sign".
|
||||
cy.get(".mantine-Modal-content")
|
||||
.contains("button", /^Confirm signature$|^Approve & sign$/)
|
||||
|
||||
@@ -166,6 +166,8 @@ function approveChain(freight: "CONTAINER" | "BULK") {
|
||||
cy.loginBackoffice("marketer@edr.local");
|
||||
withContract(freight, (c) => cy.visit(`/dashboard/contract-requests/${c.id}`));
|
||||
cy.contains("button", "Accept for approval", { timeout: 20000 }).click();
|
||||
// The modal takes an explicit validity window and pre-fills neither date.
|
||||
cy.acceptValidityWindow();
|
||||
cy.contains("button", "Accept & start approval", { timeout: 20000 })
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
@@ -198,7 +200,9 @@ function customerSigns(freight: "CONTAINER" | "BULK") {
|
||||
});
|
||||
cy.wait(500).then(() => {
|
||||
cy.get("body").then(($b) => {
|
||||
if ($b.text().includes("I have read the entire contract")) return;
|
||||
// Only rendered once hasScrolledToBottom flips; the consent label is
|
||||
// always present, so matching it raced ahead of the scroll handler.
|
||||
if ($b.text().includes("You may now sign the contract")) return;
|
||||
expect(attempt, "consent bar unlocked").to.be.lessThan(20);
|
||||
unlockConsent(attempt + 1);
|
||||
});
|
||||
@@ -206,7 +210,11 @@ function customerSigns(freight: "CONTAINER" | "BULK") {
|
||||
};
|
||||
unlockConsent(0);
|
||||
|
||||
cy.contains("I have read the entire contract", { timeout: 15000 }).click();
|
||||
// Check the input itself — clicking the label text does not toggle it.
|
||||
cy.contains("I have read the entire contract", { timeout: 15000 })
|
||||
.closest(".mantine-Checkbox-root")
|
||||
.find('input[type="checkbox"]')
|
||||
.check({ force: true });
|
||||
cy.contains("button", /^Sign contract$|^Approve & sign$/).click();
|
||||
cy.contains("label", "Full name")
|
||||
.invoke("attr", "for")
|
||||
@@ -214,6 +222,7 @@ function customerSigns(freight: "CONTAINER" | "BULK") {
|
||||
cy.get(`[id="${id}"]`).clear().type("Demo User");
|
||||
});
|
||||
cy.drawSignature();
|
||||
cy.uploadCompanyStamp();
|
||||
cy.contains("button", "Continue to verification").click();
|
||||
cy.contains("Verify it's you", { timeout: 20000 }).should("be.visible");
|
||||
cy.getOtp(customer).then((otp) => cy.typeOtp(otp));
|
||||
@@ -233,6 +242,8 @@ function counterSignAndFinalize(freight: "CONTAINER" | "BULK") {
|
||||
cy.get(`[id="${id}"]`).clear().type("EDR Marketer");
|
||||
});
|
||||
cy.drawSignature();
|
||||
// Staff counter-sign gates on a stamp too, same as the customer's modal.
|
||||
cy.uploadCompanyStamp();
|
||||
cy.get(".mantine-Modal-content")
|
||||
.contains("button", /^Confirm signature$|^Approve & sign$/)
|
||||
.click();
|
||||
@@ -240,8 +251,11 @@ function counterSignAndFinalize(freight: "CONTAINER" | "BULK") {
|
||||
expectContractStatus(freight, "AWAITING_CLEARANCE_DOCUMENTS");
|
||||
|
||||
cy.loginBackoffice(opsStaff);
|
||||
withContract(freight, (c) => cy.visit(`/dashboard/contract-requests/${c.id}`));
|
||||
cy.contains('[role="tab"]', "Clearance Review", { timeout: 30000 }).click();
|
||||
// Clearance review + finalize live solely on the Operations "Clearance
|
||||
// Documents" hub — the contract detail page no longer embeds a tab for it.
|
||||
withContract(freight, (c) =>
|
||||
cy.visit(`/dashboard/contracts/clearance-documents/${c.id}`),
|
||||
);
|
||||
cy.contains("button", "Finalize document approval", { timeout: 30000 })
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
@@ -342,7 +356,14 @@ describe("export one-time journeys: container + bulk on one train", { retries: 0
|
||||
cy.visit("/dashboard/operations/train-scheduling-v2");
|
||||
cy.contains("button", "New schedule", { timeout: 20000 }).click();
|
||||
cy.contains("Create train schedule", { timeout: 15000 }).should("be.visible");
|
||||
cy.mantineSelect(/^Route$/, new RegExp(ORIGIN_YARD));
|
||||
// Anchor on the FIRST yard in the label. The corridor also has the
|
||||
// reverse (import) route, whose label ends at this same yard — an
|
||||
// unanchored match picks it, and the API then rejects the departure
|
||||
// against the 3-day import lead instead of the 24h export one.
|
||||
cy.mantineSelect(
|
||||
/^Route$/,
|
||||
new RegExp(`^\\s*${ORIGIN_YARD}.*${PORT_YARD}\\s*$`),
|
||||
);
|
||||
|
||||
// Just past the 24h scheduling lead: creatable now, and the export
|
||||
// window (opens departure − lead) flips OPEN a couple of minutes later.
|
||||
@@ -362,7 +383,24 @@ describe("export one-time journeys: container + bulk on one train", { retries: 0
|
||||
);
|
||||
});
|
||||
|
||||
// The 10s window tick flips PRE_WINDOW → OPEN once the lead moment passes.
|
||||
// The export window opens at departure − 24h CLAMPED into the booking desk
|
||||
// hours (8–17 EAT), so a run outside those hours would sit at PRE_WINDOW
|
||||
// until the clamp time no matter how long it waited. Force it OPEN — this
|
||||
// spec exercises the FCFS reservation flow, not the window engine
|
||||
// (segment_weight arranges its windows the same way).
|
||||
dbUpcomingSchedule().then(({ rows }) => {
|
||||
expect(rows, "upcoming export schedule").to.have.length(1);
|
||||
cy.task("db:query", {
|
||||
sql: `UPDATE freight.train_schedules
|
||||
SET window_opens_at = LEAST(window_opens_at, now()),
|
||||
window_phase = 'OPEN',
|
||||
booking_window_status = 'OPEN'
|
||||
WHERE id = $1 AND booking_window_status <> 'FULL'`,
|
||||
params: [rows[0].id],
|
||||
});
|
||||
});
|
||||
|
||||
// Belt-and-braces: confirm the engine keeps it OPEN.
|
||||
const waitForOpenWindow = (attempt: number) => {
|
||||
dbUpcomingSchedule().then(({ rows }) => {
|
||||
expect(rows, "upcoming export schedule").to.have.length(1);
|
||||
@@ -434,6 +472,7 @@ describe("export one-time journeys: container + bulk on one train", { retries: 0
|
||||
|
||||
pickShipmentDay();
|
||||
|
||||
cy.fillCargoDescription();
|
||||
cy.contains("button", "Review price & book").should("not.be.disabled").click();
|
||||
cy.contains("Confirm shipment price", { timeout: 30000 }).should("be.visible");
|
||||
cy.contains("button", "Confirm & book").click();
|
||||
@@ -501,6 +540,7 @@ describe("export one-time journeys: container + bulk on one train", { retries: 0
|
||||
fill(/^Quantity \(tons\)/, "60");
|
||||
pickShipmentDay();
|
||||
|
||||
cy.fillCargoDescription();
|
||||
cy.contains("button", "Review price & book").should("not.be.disabled").click();
|
||||
cy.contains("Confirm shipment price", { timeout: 30000 }).should("be.visible");
|
||||
cy.contains("button", "Confirm & book").click();
|
||||
|
||||
@@ -113,24 +113,36 @@ function withBooking(
|
||||
});
|
||||
}
|
||||
|
||||
/** Latest export schedule created by this journey. */
|
||||
/**
|
||||
* The export schedule this journey rides — scoped to a MOJO-origin EXPORT run,
|
||||
* the same shape the create-guard below counts. Taking the newest row in the
|
||||
* whole table instead picked up whatever schedule another spec happened to
|
||||
* create last (an IMPORT one, in a full-suite run).
|
||||
*/
|
||||
function dbSchedule() {
|
||||
return cy.task<{
|
||||
rows: Array<{ id: string; status: string; direction: string; window_closes_at: string }>;
|
||||
}>("db:query", {
|
||||
sql: `SELECT ts.id, ts.status, ts.direction, ts.window_closes_at
|
||||
FROM freight.train_schedules ts
|
||||
JOIN freight.routes r ON r.id = ts.route_id
|
||||
JOIN freight.yards o ON o.id = r.origin_yard_id AND o.code = 'MOJO'
|
||||
WHERE ts.direction = 'EXPORT' AND ts.deleted_at IS NULL
|
||||
ORDER BY ts.created_at DESC LIMIT 1`,
|
||||
});
|
||||
}
|
||||
|
||||
/** Fill a labelled Mantine input (label[for] → input id). */
|
||||
function fill(label: string | RegExp, value: string) {
|
||||
cy.contains("label", label)
|
||||
.invoke("attr", "for")
|
||||
.then((id) => {
|
||||
cy.get(`[id="${id}"]`).clear({ force: true }).type(value, { force: true });
|
||||
});
|
||||
/**
|
||||
* Fill the N-th labelled Mantine input (label[for] → input id). Indexed
|
||||
* because the booking form renders one "Quantity *" per container size.
|
||||
*/
|
||||
function fillNth(label: RegExp, index: number, value: string) {
|
||||
cy.get("label").then(($labels) => {
|
||||
const matches = $labels.filter((_, el) => label.test(el.textContent ?? ""));
|
||||
expect(matches.length, `labels matching ${label}`).to.be.greaterThan(index);
|
||||
const id = matches.eq(index).attr("for");
|
||||
cy.get(`[id="${id}"]`).clear({ force: true }).type(value, { force: true });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,6 +240,8 @@ describe("intercity one-time journey: contract → booking → export train", {
|
||||
withContract((c) => cy.visit(`/dashboard/contract-requests/${c.id}`));
|
||||
|
||||
cy.contains("button", "Accept for approval", { timeout: 20000 }).click();
|
||||
// The modal takes an explicit validity window and pre-fills neither date.
|
||||
cy.acceptValidityWindow();
|
||||
cy.contains("button", "Accept & start approval", { timeout: 20000 })
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
@@ -267,7 +281,9 @@ describe("intercity one-time journey: contract → booking → export train", {
|
||||
});
|
||||
cy.wait(500).then(() => {
|
||||
cy.get("body").then(($b) => {
|
||||
if ($b.text().includes("I have read the entire contract")) return;
|
||||
// Only rendered once hasScrolledToBottom flips; the consent label is
|
||||
// always present, so matching it raced ahead of the scroll handler.
|
||||
if ($b.text().includes("You may now sign the contract")) return;
|
||||
expect(attempt, "consent bar unlocked").to.be.lessThan(20);
|
||||
unlockConsent(attempt + 1);
|
||||
});
|
||||
@@ -275,7 +291,11 @@ describe("intercity one-time journey: contract → booking → export train", {
|
||||
};
|
||||
unlockConsent(0);
|
||||
|
||||
cy.contains("I have read the entire contract", { timeout: 15000 }).click();
|
||||
// Check the input itself — clicking the label text does not toggle it.
|
||||
cy.contains("I have read the entire contract", { timeout: 15000 })
|
||||
.closest(".mantine-Checkbox-root")
|
||||
.find('input[type="checkbox"]')
|
||||
.check({ force: true });
|
||||
cy.contains("button", /^Sign contract$|^Approve & sign$/).click();
|
||||
|
||||
cy.contains("label", "Full name")
|
||||
@@ -284,6 +304,7 @@ describe("intercity one-time journey: contract → booking → export train", {
|
||||
cy.get(`[id="${id}"]`).clear().type("Demo User");
|
||||
});
|
||||
cy.drawSignature();
|
||||
cy.uploadCompanyStamp();
|
||||
cy.contains("button", "Continue to verification").click();
|
||||
|
||||
cy.contains("Verify it's you", { timeout: 20000 }).should("be.visible");
|
||||
@@ -305,6 +326,8 @@ describe("intercity one-time journey: contract → booking → export train", {
|
||||
cy.get(`[id="${id}"]`).clear().type("EDR Marketer");
|
||||
});
|
||||
cy.drawSignature();
|
||||
// Staff counter-sign gates on a stamp too, same as the customer's modal.
|
||||
cy.uploadCompanyStamp();
|
||||
cy.get(".mantine-Modal-content")
|
||||
.contains("button", /^Confirm signature$|^Approve & sign$/)
|
||||
.click();
|
||||
@@ -337,10 +360,11 @@ describe("intercity one-time journey: contract → booking → export train", {
|
||||
|
||||
it("operations approves the document and finalizes — contract fully executed", () => {
|
||||
cy.loginBackoffice(opsStaff);
|
||||
withContract((c) => cy.visit(`/dashboard/contract-requests/${c.id}`));
|
||||
|
||||
// The clearance review section lives behind its own tab on the detail page.
|
||||
cy.contains('[role="tab"]', "Clearance Review", { timeout: 30000 }).click();
|
||||
// Clearance review + finalize live solely on the Operations "Clearance
|
||||
// Documents" hub — the contract detail page no longer embeds a tab for it.
|
||||
withContract((c) =>
|
||||
cy.visit(`/dashboard/contracts/clearance-documents/${c.id}`),
|
||||
);
|
||||
|
||||
// Approve the uploaded Cargo Manifest, then finalize.
|
||||
cy.contains("button", /Approve all/, { timeout: 30000 }).click();
|
||||
@@ -362,8 +386,12 @@ describe("intercity one-time journey: contract → booking → export train", {
|
||||
|
||||
cy.contains("New Shipment Booking", { timeout: 20000 }).should("be.visible");
|
||||
|
||||
// 20ft quantities must be even (pairs share a wagon).
|
||||
fill(/^Quantity/, "2");
|
||||
// Container contracts cover BOTH sizes, so the form renders a 20ft and a
|
||||
// 40ft line, each seeded quantity 1. Book 2 × 20ft (pairs share a wagon,
|
||||
// so the count must be even) and zero the 40ft line — otherwise its
|
||||
// default unit adds a third container-number input.
|
||||
fillNth(/^Quantity/, 0, "2");
|
||||
fillNth(/^Quantity/, 1, "0");
|
||||
|
||||
// One ISO container number per unit.
|
||||
cy.get('input[placeholder*="MSCU"]', { timeout: 15000 }).should("have.length", 2);
|
||||
@@ -378,6 +406,7 @@ describe("intercity one-time journey: contract → booking → export train", {
|
||||
// Intercity: no "Shipment day" picker — the ride-along note renders instead.
|
||||
cy.contains("Shipment day").should("not.exist");
|
||||
|
||||
cy.fillCargoDescription();
|
||||
cy.contains("button", "Review price & book").should("not.be.disabled").click();
|
||||
cy.contains("Confirm shipment price", { timeout: 30000 }).should("be.visible");
|
||||
cy.contains("button", "Confirm & book").click();
|
||||
@@ -471,7 +500,12 @@ describe("intercity one-time journey: contract → booking → export train", {
|
||||
cy.contains("button", "New schedule", { timeout: 20000 }).click();
|
||||
cy.contains("Create train schedule", { timeout: 15000 }).should("be.visible");
|
||||
|
||||
cy.mantineSelect(/^Route$/, new RegExp(ORIGIN_YARD));
|
||||
// Anchored: the reverse (import) route ends at this same yard, and
|
||||
// picking it would schedule against the import lead rule.
|
||||
cy.mantineSelect(
|
||||
/^Route$/,
|
||||
new RegExp(`^\\s*${ORIGIN_YARD}.*${PORT_YARD}\\s*$`),
|
||||
);
|
||||
|
||||
// Two days out, local datetime-local format.
|
||||
const departure = new Date(Date.now() + 2 * 86400000);
|
||||
|
||||
@@ -235,7 +235,14 @@ function createSchedule(trainCode: string, departure: Date) {
|
||||
cy.visit("/dashboard/operations/train-scheduling-v2");
|
||||
cy.contains("button", "New schedule", { timeout: 20000 }).click();
|
||||
cy.contains("Create train schedule", { timeout: 15000 }).should("be.visible");
|
||||
cy.mantineSelect(/^Route$/, /Nagad/);
|
||||
// Anchored at BOTH ends. /Nagad/ alone also matches the reverse (import)
|
||||
// route, which carries a different scheduling lead; anchoring only the
|
||||
// origin still collides with the other Mojo-origin corridor that runs on
|
||||
// through to Djibouti Port, whose schedules this spec's lookups ignore.
|
||||
cy.mantineSelect(
|
||||
/^Route$/,
|
||||
new RegExp(`^\\s*${ORIGIN_YARD}.*${PORT_YARD}\\s*$`),
|
||||
);
|
||||
const local = new Date(departure.getTime() - departure.getTimezoneOffset() * 60000)
|
||||
.toISOString()
|
||||
.slice(0, 16);
|
||||
@@ -275,6 +282,7 @@ function bookIntercityPair(contractRef: string, isoOffset: number) {
|
||||
});
|
||||
cy.contains("Shipment day").should("not.exist");
|
||||
|
||||
cy.fillCargoDescription();
|
||||
cy.contains("button", "Review price & book").should("not.be.disabled").click();
|
||||
cy.contains("Confirm shipment price", { timeout: 30000 }).should("be.visible");
|
||||
cy.contains("button", "Confirm & book").click();
|
||||
@@ -451,6 +459,7 @@ describe(
|
||||
cy.contains("New Shipment Booking", { timeout: 20000 }).should("be.visible");
|
||||
fill(/^Quantity \(tons\)/, "130");
|
||||
pickShipmentDay(DEPART_W);
|
||||
cy.fillCargoDescription();
|
||||
cy.contains("button", "Review price & book").should("not.be.disabled").click();
|
||||
cy.contains("Confirm shipment price", { timeout: 30000 }).should("be.visible");
|
||||
cy.contains("button", "Confirm & book").click();
|
||||
@@ -541,6 +550,7 @@ describe(
|
||||
cy.wrap($input).clear({ force: true }).type("10", { force: true });
|
||||
});
|
||||
pickShipmentDay(DEPART_F);
|
||||
cy.fillCargoDescription();
|
||||
cy.contains("button", "Review price & book").should("not.be.disabled").click();
|
||||
cy.contains("Confirm shipment price", { timeout: 30000 }).should("be.visible");
|
||||
cy.contains("button", "Confirm & book").click();
|
||||
|
||||
@@ -28,7 +28,11 @@ from (values
|
||||
('edr_gl_ethiopia', 'edr_gl_ethiopia'),
|
||||
('edr_gl_djibouti', 'edr_gl_djibouti'),
|
||||
('demo_user1', 'Demo User1'),
|
||||
('demo_user2', 'Demo User2')
|
||||
('demo_user2', 'Demo User2'),
|
||||
-- `super_admin` is the key isSuperAdmin() looks for. It is what lets this
|
||||
-- account bypass separation-of-duties rules (e.g. approving a rate it
|
||||
-- proposed), which several specs depend on.
|
||||
('super_admin', 'Super Admin')
|
||||
) v(key, name)
|
||||
where not exists (select 1 from iam.roles r where r.key = v.key);
|
||||
|
||||
@@ -64,20 +68,28 @@ from (values
|
||||
('gl-et@edr.local', 'gl_et', 'gl_et'),
|
||||
('gl-dj@edr.local', 'gl_dj', 'gl_dj'),
|
||||
('user@gmail.com', 'user', 'Demo User 1'),
|
||||
('user2@gmail.com', 'user2', 'Demo User 2')
|
||||
('user2@gmail.com', 'user2', 'Demo User 2'),
|
||||
-- Full-authority operator the corridor specs drive the customs/GL steps with
|
||||
-- (t1-close, risk, gate pass, second duty, import release). Nothing in the
|
||||
-- repo seeded it before — the suite silently relied on a hand-made row that
|
||||
-- only existed in a long-lived dev database, so a fresh stack failed 7 specs.
|
||||
('superadmin@tria.com', 'superadmin', 'Super Admin')
|
||||
) v(email, username, display)
|
||||
where not exists (select 1 from iam.users u where u.email = v.email);
|
||||
|
||||
-- ── Credentials ──────────────────────────────────────────────────────────────
|
||||
insert into iam.user_credentials (id, user_id, password, is_active)
|
||||
select gen_random_uuid(), u.id,
|
||||
case when u.email like '%@edr.local'
|
||||
case when u.email like '%@edr.local' or u.email = 'superadmin@tria.com'
|
||||
then '$argon2id$v=19$m=65536,t=3,p=4$JFEcHu4Kp55fsrVDPbHDPg$0NfnGzaE39T/qdmzte73oCkohC0Ri+f8DcrvAF4kyH4' -- password@tria
|
||||
else '$argon2id$v=19$m=65536,t=3,p=4$aBwVFf7I74pqSJqe9cBoig$gCIKa+6dCAb2X86G+0IjgPtil127cx6A6mwhvLj00Bw' -- 12345678
|
||||
end,
|
||||
true
|
||||
from iam.users u
|
||||
where (u.email like '%@edr.local' or u.email in ('user@gmail.com', 'user2@gmail.com'))
|
||||
where (
|
||||
u.email like '%@edr.local'
|
||||
or u.email in ('user@gmail.com', 'user2@gmail.com', 'superadmin@tria.com')
|
||||
)
|
||||
and not exists (select 1 from iam.user_credentials c where c.user_id = u.id);
|
||||
|
||||
-- ── User → role (staff under edr_freight, demo under demo_iam) ──────────────
|
||||
@@ -92,6 +104,7 @@ from (values
|
||||
('operation@edr.local', 'edr_operations_officer', 'edr_freight'),
|
||||
('gl-et@edr.local', 'edr_gl_ethiopia', 'edr_freight'),
|
||||
('gl-dj@edr.local', 'edr_gl_djibouti', 'edr_freight'),
|
||||
('superadmin@tria.com', 'super_admin', 'edr_freight'),
|
||||
('user@gmail.com', 'demo_user1', 'demo_iam'),
|
||||
('user2@gmail.com', 'demo_user2', 'demo_iam')
|
||||
) v(email, role_key, org_key)
|
||||
@@ -106,7 +119,7 @@ select gen_random_uuid(), true, 'pending', u.name, o.id, un.id, u.id
|
||||
from iam.users u
|
||||
join iam.organizations o on o.key = 'edr_freight'
|
||||
join iam.units un on un.key = 'edr_freight_app' and un.organization_id = o.id
|
||||
where u.email like '%@edr.local'
|
||||
where (u.email like '%@edr.local' or u.email = 'superadmin@tria.com')
|
||||
and not exists (select 1 from iam.employees e where e.user_id = u.id);
|
||||
|
||||
-- start_date must be set: the login query filters positions on
|
||||
@@ -122,7 +135,10 @@ from (values
|
||||
('marketer@edr.local', 'marketer'),
|
||||
('operation@edr.local', 'operation'),
|
||||
('gl-et@edr.local', 'ethiopian_gl'),
|
||||
('gl-dj@edr.local', 'djibouti_gl')
|
||||
('gl-dj@edr.local', 'djibouti_gl'),
|
||||
-- operations_chief carries the whole freight permission catalog, which is
|
||||
-- what the corridor specs need from this account.
|
||||
('superadmin@tria.com', 'operations_chief')
|
||||
) v(email, position_key)
|
||||
join iam.users u on u.email = v.email
|
||||
join iam.employees e on e.user_id = u.id
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user