Merge branch 'dev' into feat/ab/sh-235-severity-badge

This commit is contained in:
Alexandre Brandizzi 2026-09-16 18:38:26 -03:00 • committed by GitHub
commit d3b74afe5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 87 additions and 15 deletions

View file

@ -9,10 +9,10 @@ import {
tableRowToClosabilityInput,
} from "@/domain/work-orders/utils/wo-closability";
import {
BOARD_INLINE_STATUSES,
PAST_DUE_BADGE_TOOLTIP,
PAST_DUE_POPOVER_BODY,
PAST_DUE_RESCHEDULE_CTA,
getInlineStatusTransitions,
getStatusOptionState,
} from "@/domain/work-orders/utils/wo-lifecycle-transitions";
import { HoverChevron } from "./hover-chevron";
@ -27,6 +27,7 @@ type StatusCellProps = {
onPastDueAttempt?: () => void;
onRequestComplete?: () => void;
readOnly?: boolean;
/** Explicit list for inline create; existing WOs derive valid transitions from `wo.status`. */
statusOptions?: readonly WizardWOStatus[];
};
@ -128,10 +129,11 @@ export function StatusCell({
onPastDueAttempt,
onRequestComplete,
readOnly = false,
statusOptions = BOARD_INLINE_STATUSES,
statusOptions,
}: StatusCellProps) {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const closabilityGaps = rowClosabilityGaps(wo);
const options = statusOptions ?? getInlineStatusTransitions(wo.status);
if (readOnly) {
return <ReadOnlyStatusLabel status={wo.status} />;
@ -169,7 +171,7 @@ export function StatusCell({
onClick={(e) => e.stopPropagation()}
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);
return (

View file

@ -11,8 +11,21 @@ import {
UPLIFT_PENDING_COMPLETED_TITLE,
} 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. */
export const CREATION_INLINE_STATUSES: readonly WizardWOStatus[] = [

View file

@ -1,6 +1,7 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
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";
const incompleteRow = {
@ -43,21 +44,62 @@ describe("StatusCell closability gate", () => {
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();
render(
<StatusCell
wo={{ ...incompleteRow, status: "Pending", scheduledOn: "" }}
wo={{ ...incompleteRow, status: current, scheduledOn: "2026-06-01" }}
onChangeStatus={onChangeStatus}
onOpenSlideOver={vi.fn()}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /pending/i }));
const scheduled = screen.getByRole("button", { name: /^scheduled$/i });
expect(scheduled).toBeDisabled();
fireEvent.click(scheduled);
expect(onChangeStatus).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole("button", { name: new RegExp(current, "i") }));
const offered = Array.from(document.querySelectorAll(".MuiPopover-paper button")).map(
(button) => button.textContent?.trim(),
);
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", () => {

View file

@ -1,12 +1,14 @@
import { describe, expect, it } from "vitest";
import { PENDING_UPLIFT_GAP } from "@/domain/work-orders/utils/wo-closability";
import {
AUTOMATIC_LIFECYCLE_STATUSES,
BOARD_INLINE_STATUSES,
COMPLETED_DOC_BLOCK_MSG,
COMPLETED_MEDIA_BLOCK_MSG,
CREATION_INLINE_STATUSES,
SCHEDULED_NEEDS_DATE_MSG,
formatAapCompletedBlockTitle,
getInlineStatusTransitions,
getStatusOptionState,
hasConcreteScheduleDate,
maybeAutoSchedulePatch,
@ -17,11 +19,9 @@ import {
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
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([
"Incomplete",
"Pending",
"Scheduled",
"En Route",
"On Site",
"In Progress",
@ -30,6 +30,21 @@ describe("wo-lifecycle-transitions", () => {
"Pending Quote",
]);
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", () => {