mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 09:13:11 +00:00
Merge branch 'dev' into fix/ab/sh-368-completion-uploads
This commit is contained in:
commit
f5da80cd6e
7 changed files with 96 additions and 32 deletions
|
|
@ -4,6 +4,7 @@ import { useWorkOrdersDayPagination } from "@/app/(protected)/workorders/_hooks/
|
|||
import { currentWeekMonday } from "@/domain/work-orders/utils/list-week-utils";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
|
||||
import { todayIso } from "@/domain/work-orders/utils/wizard-date-utils";
|
||||
import { pageSelectedDayGroups } from "@/domain/work-orders/utils/day-pagination";
|
||||
import {
|
||||
buildSelectedDayGroup,
|
||||
|
|
@ -70,9 +71,7 @@ export function useWorkOrdersTableDayView({
|
|||
});
|
||||
const isEmptyWeek = !advActive && scheduled.length === 0 && !searchActive;
|
||||
const todayKey =
|
||||
filters.weekMonday === currentWeekMonday()
|
||||
? deriveDayFields(new Date().toISOString().slice(0, 10)).dayGroup
|
||||
: null;
|
||||
filters.weekMonday === currentWeekMonday() ? deriveDayFields(todayIso()).dayGroup : null;
|
||||
|
||||
return {
|
||||
scheduled,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { ADVANCED_FILTER_PREDICATES } from "@/domain/work-orders/utils/advanced-
|
|||
import { isCarryOverDisplayRow } from "@/domain/work-orders/utils/carry-over-display";
|
||||
import { isDateInWeek } from "@/domain/work-orders/utils/table-day-utils";
|
||||
import { weekRange } from "@/domain/work-orders/utils/list-week-utils";
|
||||
import { todayIso } from "@/domain/work-orders/utils/wizard-date-utils";
|
||||
|
||||
const TERMINAL_STATUSES = new Set(["Completed", "Canceled", "Done", "Cancelled"]);
|
||||
|
||||
|
|
@ -17,8 +18,10 @@ export function computeIsPastDue(row: WorkOrderTableRow): boolean {
|
|||
if (!row.scheduledOn || TERMINAL_STATUSES.has(row.status)) {
|
||||
return false;
|
||||
}
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
return row.scheduledOn.slice(0, 10) < today;
|
||||
// scheduledOn is a calendar date, so compare it to the user's local calendar
|
||||
// day. A UTC "today" rolls over hours early west of UTC and flags work
|
||||
// scheduled for today as Past Due every evening.
|
||||
return row.scheduledOn.slice(0, 10) < todayIso();
|
||||
}
|
||||
|
||||
export function enrichTableRow(row: WorkOrderTableRow): WorkOrderTableRow {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import PmSchedulesListPage from "@/app/(protected)/pmschedules/index";
|
||||
// Static import: a dynamic import inside the test transforms the edit page's
|
||||
// module graph against the 5s test budget, which times out under load.
|
||||
import EditPage from "@/app/(protected)/pmschedules/edit/[id]";
|
||||
import { renderWithProviders } from "@/test/test-utils";
|
||||
|
||||
const usePmSchedulesList = vi.fn();
|
||||
|
|
@ -75,9 +78,7 @@ describe("PmSchedulesListPage actions", () => {
|
|||
});
|
||||
|
||||
describe("PmScheduleEditUnavailablePage", () => {
|
||||
it("shows unavailable message instead of edit form when canEdit is false", async () => {
|
||||
const EditPage = (await import("@/app/(protected)/pmschedules/edit/[id]")).default;
|
||||
|
||||
it("shows unavailable message instead of edit form when canEdit is false", () => {
|
||||
renderWithProviders(<EditPage />, {
|
||||
route: "/pmschedules/edit/1",
|
||||
routerProps: { initialEntries: ["/pmschedules/edit/1"] },
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|||
import userEvent from "@testing-library/user-event";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { VendorCreateModal } from "@/app/(protected)/vendors/_components/vendor-create-modal";
|
||||
import { VendorRosterFormFields } from "@/app/(protected)/vendors/_components/vendor-roster-form-fields";
|
||||
import {
|
||||
|
|
@ -49,6 +49,10 @@ function renderCreateModal() {
|
|||
});
|
||||
}
|
||||
|
||||
// delay: null skips user-event's per-keystroke setTimeout yields, which only
|
||||
// add real-timer latency here.
|
||||
let user = userEvent.setup({ delay: null });
|
||||
|
||||
async function fillCompanyNameAndSubmit(name = "Gateway Plumbing") {
|
||||
fireEvent.change(screen.getByRole("combobox", { name: "Company (required)" }), {
|
||||
target: { value: name },
|
||||
|
|
@ -57,7 +61,7 @@ async function fillCompanyNameAndSubmit(name = "Gateway Plumbing") {
|
|||
// phone-or-email rule, so the address is filled to keep the form's only
|
||||
// outstanding problem the one under test.
|
||||
fillRequiredAddress();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Add Vendor" }));
|
||||
await user.click(screen.getByRole("button", { name: "Add Vendor" }));
|
||||
}
|
||||
|
||||
function fillRequiredAddress({
|
||||
|
|
@ -74,7 +78,15 @@ function fillRequiredAddress({
|
|||
pickVendorState(state);
|
||||
}
|
||||
|
||||
describe("VendorCreateModal validation", () => {
|
||||
// Each test mounts the full MUI dialog (Autocompletes, 50-option State list)
|
||||
// and costs ~1s of CPU when idle; the first also pays JIT warm-up. Under
|
||||
// parallel agent load the 5s default is exceeded by CPU starvation alone,
|
||||
// matching the 10s budgets already used by vendors-list.test.tsx.
|
||||
describe("VendorCreateModal validation", { timeout: 15_000 }, () => {
|
||||
beforeEach(() => {
|
||||
user = userEvent.setup({ delay: null });
|
||||
});
|
||||
|
||||
it("explains the company phone-or-email requirement after submission", async () => {
|
||||
renderCreateModal();
|
||||
await fillCompanyNameAndSubmit();
|
||||
|
|
@ -138,8 +150,8 @@ describe("VendorCreateModal validation", () => {
|
|||
const phone = screen.getByRole("textbox", { name: "Company Phone (optional)" });
|
||||
const email = screen.getByRole("textbox", { name: "Email (optional)" });
|
||||
|
||||
await userEvent.type(phone, "314");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Add Vendor" }));
|
||||
await user.type(phone, "314");
|
||||
await user.click(screen.getByRole("button", { name: "Add Vendor" }));
|
||||
|
||||
expect(await screen.findByText("Enter a 10-digit phone number")).toBeInTheDocument();
|
||||
expect(phone).toHaveAttribute("aria-invalid", "true");
|
||||
|
|
@ -153,8 +165,8 @@ describe("VendorCreateModal validation", () => {
|
|||
const phone = screen.getByRole("textbox", { name: "Company Phone (optional)" });
|
||||
const email = screen.getByRole("textbox", { name: "Email (optional)" });
|
||||
|
||||
await userEvent.type(email, "not-an-email");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Add Vendor" }));
|
||||
await user.type(email, "not-an-email");
|
||||
await user.click(screen.getByRole("button", { name: "Add Vendor" }));
|
||||
|
||||
expect(await screen.findByText("Invalid email")).toBeInTheDocument();
|
||||
expect(email).toHaveAttribute("aria-invalid", "true");
|
||||
|
|
@ -169,7 +181,7 @@ describe("VendorCreateModal validation", () => {
|
|||
|
||||
expect(await screen.findByText(VENDOR_COMPANY_CONTACT_REQUIRED_MESSAGE)).toBeInTheDocument();
|
||||
|
||||
await userEvent.type(phone, "3145550100");
|
||||
await user.type(phone, "3145550100");
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText(VENDOR_COMPANY_CONTACT_REQUIRED_MESSAGE)).not.toBeInTheDocument(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -68,11 +68,19 @@ function rosterWith(technicians: Array<Record<string, unknown>>) {
|
|||
};
|
||||
}
|
||||
|
||||
// delay: null skips user-event's per-keystroke setTimeout yields, which only
|
||||
// add real-timer latency here.
|
||||
let user = userEvent.setup({ delay: null });
|
||||
|
||||
beforeEach(() => {
|
||||
saveMutate.mockReset();
|
||||
user = userEvent.setup({ delay: null });
|
||||
});
|
||||
|
||||
describe("VendorDetailDrawer selected-technician display", () => {
|
||||
// The edit-mode drawer is a heavy MUI render and the first test pays JIT
|
||||
// warm-up; under parallel agent load the 5s default is exceeded by CPU
|
||||
// starvation alone, matching the 10s budgets in vendors-list.test.tsx.
|
||||
describe("VendorDetailDrawer selected-technician display", { timeout: 15_000 }, () => {
|
||||
it.each([
|
||||
{ removedPosition: 1, remainingId: 2, remainingName: "Beth" },
|
||||
{ removedPosition: 2, remainingId: 1, remainingName: "Adam" },
|
||||
|
|
@ -107,10 +115,10 @@ describe("VendorDetailDrawer selected-technician display", () => {
|
|||
{ route: "/vendors", withAuth: false },
|
||||
);
|
||||
|
||||
await userEvent.click(
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: `Remove technician ${removedPosition}` }),
|
||||
);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
||||
await user.click(screen.getByRole("button", { name: "Save changes" }));
|
||||
|
||||
await waitFor(() => expect(saveMutate).toHaveBeenCalledTimes(1));
|
||||
expect(saveMutate).toHaveBeenCalledWith(
|
||||
|
|
@ -161,12 +169,12 @@ describe("VendorDetailDrawer selected-technician display", () => {
|
|||
{ route: "/vendors", withAuth: false },
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Remove technician 2" }));
|
||||
await user.click(screen.getByRole("button", { name: "Remove technician 2" }));
|
||||
|
||||
const save = screen.getByRole("button", { name: "Save changes" });
|
||||
expect(save).toBeEnabled();
|
||||
|
||||
await userEvent.click(save);
|
||||
await user.click(save);
|
||||
|
||||
expect(
|
||||
await screen.findByText("Provide a company phone or email (at least one required)"),
|
||||
|
|
@ -444,10 +452,10 @@ describe("VendorDetailDrawer deactivation prompt", () => {
|
|||
);
|
||||
|
||||
const toggle = screen.getByRole("switch", { name: "Active status" });
|
||||
await userEvent.click(toggle);
|
||||
await user.click(toggle);
|
||||
expect(toggle).toBeChecked();
|
||||
|
||||
await userEvent.click(toggle);
|
||||
await user.click(toggle);
|
||||
|
||||
expect(toggle).not.toBeChecked();
|
||||
expect(onRequestDeactivation).not.toHaveBeenCalled();
|
||||
|
|
@ -472,7 +480,7 @@ describe("VendorDetailDrawer deactivation prompt", () => {
|
|||
{ route: "/vendors", withAuth: false },
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole("switch", { name: "Active status" }));
|
||||
await user.click(screen.getByRole("switch", { name: "Active status" }));
|
||||
|
||||
expect(onRequestDeactivation).toHaveBeenCalledWith(vendor);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { WorkOrderBoardSearchQueryParams } from "@/domain/work-orders/types/work-order-board";
|
||||
import { EMPTY_ADVANCED_FILTERS } from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||
|
|
@ -120,6 +120,12 @@ function pagedResult(rows: WorkOrderTableRow[], page: number, pageSize: number)
|
|||
};
|
||||
}
|
||||
|
||||
// Role and text queries over the whole board scan every row cell; scoping them
|
||||
// to the pagination footer keeps each waitFor poll cheap on a loaded machine.
|
||||
function pagination() {
|
||||
return within(screen.getByTestId("wo-day-pagination"));
|
||||
}
|
||||
|
||||
function pagesAfter(callCount: number): number[] {
|
||||
return searchBoard.mock.calls.slice(callCount).map((call) => {
|
||||
const params = call[0] as WorkOrderBoardSearchQueryParams;
|
||||
|
|
@ -213,14 +219,14 @@ describe("advanced search page sync after mutation", () => {
|
|||
);
|
||||
});
|
||||
expect(screen.getByText("WO-0")).toBeInTheDocument();
|
||||
expect(screen.getByText("Page 1 of 2")).toBeInTheDocument();
|
||||
expect(pagination().getByText("Page 1 of 2")).toBeInTheDocument();
|
||||
expect(screen.queryByText("WO-25")).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Next page" }));
|
||||
fireEvent.click(pagination().getByRole("button", { name: "Next page" }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("harness-state")).toHaveTextContent("page=1");
|
||||
expect(screen.getByText("WO-25")).toBeInTheDocument();
|
||||
expect(screen.getByText("Page 2 of 2")).toBeInTheDocument();
|
||||
expect(pagination().getByText("Page 2 of 2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const callsBeforeMutation = searchBoard.mock.calls.length;
|
||||
|
|
@ -236,12 +242,12 @@ describe("advanced search page sync after mutation", () => {
|
|||
expect(screen.getByTestId("harness-state")).toHaveTextContent(
|
||||
"adv=true rows=20 total=20 page=0",
|
||||
);
|
||||
expect(screen.getByText("Page 1 of 1")).toBeInTheDocument();
|
||||
expect(pagination().getByText("Page 1 of 1")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("WO-0")).toBeInTheDocument();
|
||||
expect(screen.queryByText("WO-25")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("RESULTS · 20 items")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Previous page" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "Next page" })).toBeDisabled();
|
||||
expect(pagination().getByRole("button", { name: "Previous page" })).toBeDisabled();
|
||||
expect(pagination().getByRole("button", { name: "Next page" })).toBeDisabled();
|
||||
}, 15000);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { EMPTY_UPLIFT_SUMMARY } from "@/domain/work-orders/types/work-order-uplift";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import { EMPTY_ADVANCED_FILTERS } from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||
|
|
@ -526,6 +526,41 @@ describe("computeIsPastDue", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("computeIsPastDue across the local/UTC day boundary", () => {
|
||||
beforeEach(() => {
|
||||
// UTC-3 with no DST: local evening is already the next day in UTC.
|
||||
vi.stubEnv("TZ", "America/Sao_Paulo");
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("does not flag today's work as Past Due at 23:30 local (02:30 UTC next day)", () => {
|
||||
vi.setSystemTime(new Date("2026-09-17T02:30:00.000Z"));
|
||||
|
||||
expect(computeIsPastDue(makeRow({ scheduledOn: "2026-09-16", status: "Scheduled" }))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(computeIsPastDue(makeRow({ scheduledOn: "2026-09-15", status: "Scheduled" }))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("flags yesterday's work as Past Due at 00:30 local", () => {
|
||||
vi.setSystemTime(new Date("2026-09-17T03:30:00.000Z"));
|
||||
|
||||
expect(computeIsPastDue(makeRow({ scheduledOn: "2026-09-16", status: "Scheduled" }))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(computeIsPastDue(makeRow({ scheduledOn: "2026-09-17", status: "Scheduled" }))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("enrichTableRow", () => {
|
||||
it("recomputes isPastDue from scheduledOn and ignores sticky API true", () => {
|
||||
const enriched = enrichTableRow(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue