mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-04 16:02:08 +00:00
Merge branch 'dev' into feat/ab/sh-197-inline-create-fields
This commit is contained in:
commit
8b5b5903fa
4 changed files with 87 additions and 15 deletions
|
|
@ -9,10 +9,10 @@ import {
|
||||||
tableRowToClosabilityInput,
|
tableRowToClosabilityInput,
|
||||||
} from "@/domain/work-orders/utils/wo-closability";
|
} from "@/domain/work-orders/utils/wo-closability";
|
||||||
import {
|
import {
|
||||||
BOARD_INLINE_STATUSES,
|
|
||||||
PAST_DUE_BADGE_TOOLTIP,
|
PAST_DUE_BADGE_TOOLTIP,
|
||||||
PAST_DUE_POPOVER_BODY,
|
PAST_DUE_POPOVER_BODY,
|
||||||
PAST_DUE_RESCHEDULE_CTA,
|
PAST_DUE_RESCHEDULE_CTA,
|
||||||
|
getInlineStatusTransitions,
|
||||||
getStatusOptionState,
|
getStatusOptionState,
|
||||||
} from "@/domain/work-orders/utils/wo-lifecycle-transitions";
|
} from "@/domain/work-orders/utils/wo-lifecycle-transitions";
|
||||||
import { HoverChevron } from "./hover-chevron";
|
import { HoverChevron } from "./hover-chevron";
|
||||||
|
|
@ -27,6 +27,7 @@ type StatusCellProps = {
|
||||||
onPastDueAttempt?: () => void;
|
onPastDueAttempt?: () => void;
|
||||||
onRequestComplete?: () => void;
|
onRequestComplete?: () => void;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
/** Explicit list for inline create; existing WOs derive valid transitions from `wo.status`. */
|
||||||
statusOptions?: readonly WizardWOStatus[];
|
statusOptions?: readonly WizardWOStatus[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -128,10 +129,11 @@ export function StatusCell({
|
||||||
onPastDueAttempt,
|
onPastDueAttempt,
|
||||||
onRequestComplete,
|
onRequestComplete,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
statusOptions = BOARD_INLINE_STATUSES,
|
statusOptions,
|
||||||
}: StatusCellProps) {
|
}: StatusCellProps) {
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||||
const closabilityGaps = rowClosabilityGaps(wo);
|
const closabilityGaps = rowClosabilityGaps(wo);
|
||||||
|
const options = statusOptions ?? getInlineStatusTransitions(wo.status);
|
||||||
|
|
||||||
if (readOnly) {
|
if (readOnly) {
|
||||||
return <ReadOnlyStatusLabel status={wo.status} />;
|
return <ReadOnlyStatusLabel status={wo.status} />;
|
||||||
|
|
@ -169,7 +171,7 @@ export function StatusCell({
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
slotProps={{ paper: { className: "min-w-[190px] rounded-lg border border-border p-1" } }}
|
slotProps={{ paper: { className: "min-w-[190px] rounded-lg border border-border p-1" } }}
|
||||||
>
|
>
|
||||||
{statusOptions.map((status) => {
|
{options.map((status) => {
|
||||||
const option = getStatusOptionState(wo, status, closabilityGaps);
|
const option = getStatusOptionState(wo, status, closabilityGaps);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,21 @@ import {
|
||||||
UPLIFT_PENDING_COMPLETED_TITLE,
|
UPLIFT_PENDING_COMPLETED_TITLE,
|
||||||
} from "@/domain/work-orders/utils/wo-closability";
|
} from "@/domain/work-orders/utils/wo-closability";
|
||||||
|
|
||||||
/** AAP `ALL_STATUSES` — Canceled is kebab-only. */
|
/**
|
||||||
export const BOARD_INLINE_STATUSES: readonly WizardWOStatus[] = ALL_WIZARD_STATUSES;
|
* Lifecycle states the server derives (Incomplete from create, Scheduled from Schedule On).
|
||||||
|
* Past Due is an overlay, not a status. None of these may be picked manually on an existing WO.
|
||||||
|
*/
|
||||||
|
export const AUTOMATIC_LIFECYCLE_STATUSES: readonly WizardWOStatus[] = ["Incomplete", "Scheduled"];
|
||||||
|
|
||||||
|
/** Manually selectable statuses for an existing WO — Canceled is kebab-only. */
|
||||||
|
export const BOARD_INLINE_STATUSES: readonly WizardWOStatus[] = ALL_WIZARD_STATUSES.filter(
|
||||||
|
(status) => !AUTOMATIC_LIFECYCLE_STATUSES.includes(status),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Inline options for an existing WO never repeat its current status. */
|
||||||
|
export function getInlineStatusTransitions(currentStatus: string): WizardWOStatus[] {
|
||||||
|
return BOARD_INLINE_STATUSES.filter((status) => status !== currentStatus);
|
||||||
|
}
|
||||||
|
|
||||||
/** AAP `CREATION_STATUSES` — wizard Step 3 and inline create. */
|
/** AAP `CREATION_STATUSES` — wizard Step 3 and inline create. */
|
||||||
export const CREATION_INLINE_STATUSES: readonly WizardWOStatus[] = [
|
export const CREATION_INLINE_STATUSES: readonly WizardWOStatus[] = [
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { StatusCell } from "@/app/(protected)/workorders/_components/list/table/cells/status-cell";
|
import { StatusCell } from "@/app/(protected)/workorders/_components/list/table/cells/status-cell";
|
||||||
|
import type { WizardWOStatus } from "@/domain/work-orders/types/work-order-wizard";
|
||||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
|
||||||
const incompleteRow = {
|
const incompleteRow = {
|
||||||
|
|
@ -43,21 +44,62 @@ describe("StatusCell closability gate", () => {
|
||||||
expect(screen.queryByRole("button", { name: /^canceled$/i })).toBeNull();
|
expect(screen.queryByRole("button", { name: /^canceled$/i })).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables Scheduled when the row has no concrete date", () => {
|
it.each<WizardWOStatus>([
|
||||||
|
"Pending",
|
||||||
|
"En Route",
|
||||||
|
"On Site",
|
||||||
|
"In Progress",
|
||||||
|
"Rescheduled",
|
||||||
|
"Pending Quote",
|
||||||
|
])("offers only manual transitions away from %s (SH-357, SH-358)", (current) => {
|
||||||
const onChangeStatus = vi.fn();
|
const onChangeStatus = vi.fn();
|
||||||
render(
|
render(
|
||||||
<StatusCell
|
<StatusCell
|
||||||
wo={{ ...incompleteRow, status: "Pending", scheduledOn: "" }}
|
wo={{ ...incompleteRow, status: current, scheduledOn: "2026-06-01" }}
|
||||||
onChangeStatus={onChangeStatus}
|
onChangeStatus={onChangeStatus}
|
||||||
onOpenSlideOver={vi.fn()}
|
onOpenSlideOver={vi.fn()}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /pending/i }));
|
fireEvent.click(screen.getByRole("button", { name: new RegExp(current, "i") }));
|
||||||
const scheduled = screen.getByRole("button", { name: /^scheduled$/i });
|
const offered = Array.from(document.querySelectorAll(".MuiPopover-paper button")).map(
|
||||||
expect(scheduled).toBeDisabled();
|
(button) => button.textContent?.trim(),
|
||||||
fireEvent.click(scheduled);
|
);
|
||||||
expect(onChangeStatus).not.toHaveBeenCalled();
|
|
||||||
|
expect(offered).not.toContain("Incomplete");
|
||||||
|
expect(offered).not.toContain("Scheduled");
|
||||||
|
expect(offered).not.toContain("Past Due");
|
||||||
|
expect(offered).not.toContain("Canceled");
|
||||||
|
expect(offered).not.toContain(current);
|
||||||
|
expect(offered).toEqual(
|
||||||
|
[
|
||||||
|
"Pending",
|
||||||
|
"En Route",
|
||||||
|
"On Site",
|
||||||
|
"In Progress",
|
||||||
|
"Completed",
|
||||||
|
"Rescheduled",
|
||||||
|
"Pending Quote",
|
||||||
|
].filter((status) => status !== current),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("offers every manual status, and no automatic one, from Incomplete or Scheduled", () => {
|
||||||
|
for (const current of ["Incomplete", "Scheduled"] as const) {
|
||||||
|
const { unmount } = render(
|
||||||
|
<StatusCell
|
||||||
|
wo={{ ...incompleteRow, status: current }}
|
||||||
|
onChangeStatus={vi.fn()}
|
||||||
|
onOpenSlideOver={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: new RegExp(current, "i") }));
|
||||||
|
expect(screen.getByRole("button", { name: /^pending$/i })).toBeTruthy();
|
||||||
|
expect(screen.queryByRole("button", { name: /^incomplete$/i })).toBeNull();
|
||||||
|
expect(screen.queryByRole("button", { name: /^scheduled$/i })).toBeNull();
|
||||||
|
unmount();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("limits creation options to Incomplete, Pending, and Scheduled", () => {
|
it("limits creation options to Incomplete, Pending, and Scheduled", () => {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { PENDING_UPLIFT_GAP } from "@/domain/work-orders/utils/wo-closability";
|
import { PENDING_UPLIFT_GAP } from "@/domain/work-orders/utils/wo-closability";
|
||||||
import {
|
import {
|
||||||
|
AUTOMATIC_LIFECYCLE_STATUSES,
|
||||||
BOARD_INLINE_STATUSES,
|
BOARD_INLINE_STATUSES,
|
||||||
COMPLETED_DOC_BLOCK_MSG,
|
COMPLETED_DOC_BLOCK_MSG,
|
||||||
COMPLETED_MEDIA_BLOCK_MSG,
|
COMPLETED_MEDIA_BLOCK_MSG,
|
||||||
CREATION_INLINE_STATUSES,
|
CREATION_INLINE_STATUSES,
|
||||||
SCHEDULED_NEEDS_DATE_MSG,
|
SCHEDULED_NEEDS_DATE_MSG,
|
||||||
formatAapCompletedBlockTitle,
|
formatAapCompletedBlockTitle,
|
||||||
|
getInlineStatusTransitions,
|
||||||
getStatusOptionState,
|
getStatusOptionState,
|
||||||
hasConcreteScheduleDate,
|
hasConcreteScheduleDate,
|
||||||
maybeAutoSchedulePatch,
|
maybeAutoSchedulePatch,
|
||||||
|
|
@ -17,11 +19,9 @@ import {
|
||||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
|
||||||
describe("wo-lifecycle-transitions", () => {
|
describe("wo-lifecycle-transitions", () => {
|
||||||
it("lists AAP board statuses without Canceled", () => {
|
it("lists only manual board statuses: no automatic states and no Canceled (SH-169)", () => {
|
||||||
expect(BOARD_INLINE_STATUSES).toEqual([
|
expect(BOARD_INLINE_STATUSES).toEqual([
|
||||||
"Incomplete",
|
|
||||||
"Pending",
|
"Pending",
|
||||||
"Scheduled",
|
|
||||||
"En Route",
|
"En Route",
|
||||||
"On Site",
|
"On Site",
|
||||||
"In Progress",
|
"In Progress",
|
||||||
|
|
@ -30,6 +30,21 @@ describe("wo-lifecycle-transitions", () => {
|
||||||
"Pending Quote",
|
"Pending Quote",
|
||||||
]);
|
]);
|
||||||
expect(BOARD_INLINE_STATUSES).not.toContain("Canceled");
|
expect(BOARD_INLINE_STATUSES).not.toContain("Canceled");
|
||||||
|
for (const automatic of AUTOMATIC_LIFECYCLE_STATUSES) {
|
||||||
|
expect(BOARD_INLINE_STATUSES).not.toContain(automatic);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never offers the current status as a transition (SH-358)", () => {
|
||||||
|
expect(getInlineStatusTransitions("On Site")).toEqual([
|
||||||
|
"Pending",
|
||||||
|
"En Route",
|
||||||
|
"In Progress",
|
||||||
|
"Completed",
|
||||||
|
"Rescheduled",
|
||||||
|
"Pending Quote",
|
||||||
|
]);
|
||||||
|
expect(getInlineStatusTransitions("Incomplete")).toEqual(BOARD_INLINE_STATUSES);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("lists AAP creation statuses only", () => {
|
it("lists AAP creation statuses only", () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue