mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 08:03:13 +00:00
fix(work-orders): persist Extra Docs Aveta category round-trip
Keep Extra uploads uncategorized until the dispatcher picks Aveta. Drive the shield from hasAvetaDocument after reopen.
This commit is contained in:
parent
838d79c2dd
commit
0a2544fae4
22 changed files with 234 additions and 23 deletions
|
|
@ -37,7 +37,9 @@ export function SlideOverHeader({
|
|||
#{row.woNumber || "—"}
|
||||
</Text>
|
||||
<RescheduleCounterPill count={row.rescheduleCount} originalDate={row.originalDate} />
|
||||
{Boolean(row.avetaRequired) && <AvetaBadge size={13} />}
|
||||
{Boolean(row.avetaRequired) && (
|
||||
<AvetaBadge size={13} attached={Boolean(row.hasAvetaDocument)} />
|
||||
)}
|
||||
|
||||
{isCancelled && (
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -11,8 +11,12 @@ import {
|
|||
} from "lucide-react";
|
||||
import { toast } from "react-toastify";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import type { UploadableMediaCategory } from "@/domain/work-orders/mappers/work-order-board-detail-mapper";
|
||||
import type { FileCategory, MediaFile } from "@/domain/work-orders/types/work-order-media";
|
||||
import { resolveExtraDocCategory } from "@/domain/work-orders/utils/extra-docs-aveta";
|
||||
import {
|
||||
hasExplicitAvetaFile,
|
||||
resolveExtraDocCategory,
|
||||
} from "@/domain/work-orders/utils/extra-docs-aveta";
|
||||
import {
|
||||
canDeleteWorkOrderMedia,
|
||||
formatMediaSizeLimit,
|
||||
|
|
@ -29,6 +33,11 @@ const FILETYPE_ICON: Record<FileCategory, typeof ImageIcon> = {
|
|||
other: Paperclip,
|
||||
};
|
||||
|
||||
const EXTRA_DOC_PURPOSES: Array<Extract<UploadableMediaCategory, "Extra" | "Aveta">> = [
|
||||
"Extra",
|
||||
"Aveta",
|
||||
];
|
||||
|
||||
type SlideOverExtrasTabProps = {
|
||||
uploads: MediaFile[];
|
||||
isReadOnly: boolean;
|
||||
|
|
@ -36,6 +45,7 @@ type SlideOverExtrasTabProps = {
|
|||
avetaRequired?: boolean;
|
||||
onUpload: (files: File[]) => void;
|
||||
onRemove: (id: string) => void;
|
||||
onCategorize?: (id: string, category: UploadableMediaCategory) => void;
|
||||
};
|
||||
|
||||
function ExtraDocRow({
|
||||
|
|
@ -43,16 +53,20 @@ function ExtraDocRow({
|
|||
isReadOnly,
|
||||
pending,
|
||||
onRemove,
|
||||
onCategorize,
|
||||
}: {
|
||||
file: MediaFile;
|
||||
isReadOnly: boolean;
|
||||
pending: boolean;
|
||||
onRemove: (id: string) => void;
|
||||
onCategorize?: (id: string, category: UploadableMediaCategory) => void;
|
||||
}) {
|
||||
const category = resolveExtraDocCategory(file);
|
||||
const isAveta = category === "Aveta";
|
||||
const ItemIcon = isAveta ? ShieldCheck : FILETYPE_ICON[file.fileType];
|
||||
const showDelete = !isReadOnly && canDeleteWorkOrderMedia(file);
|
||||
const showCategory = !isReadOnly && Boolean(onCategorize) && !file.isLegacy;
|
||||
const purpose = file.purpose === "Aveta" || isAveta ? "Aveta" : "Extra";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-1">
|
||||
|
|
@ -70,8 +84,34 @@ function ExtraDocRow({
|
|||
>
|
||||
{file.name}
|
||||
</a>
|
||||
{isAveta && (
|
||||
<span style={{ fontSize: 10, fontWeight: 600, color: "var(--warning)" }}>Aveta</span>
|
||||
{showCategory ? (
|
||||
<select
|
||||
aria-label={`Category for ${file.name}`}
|
||||
value={purpose}
|
||||
disabled={pending}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value;
|
||||
if (value === "Extra" || value === "Aveta") {
|
||||
onCategorize?.(file.id, value);
|
||||
}
|
||||
}}
|
||||
className="rounded border px-1 py-0.5 text-[10px]"
|
||||
style={{
|
||||
borderColor: "var(--color-border)",
|
||||
color: "var(--color-text-primary)",
|
||||
background: "var(--color-bg-surface)",
|
||||
}}
|
||||
>
|
||||
{EXTRA_DOC_PURPOSES.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
isAveta && (
|
||||
<span style={{ fontSize: 10, fontWeight: 600, color: "var(--warning)" }}>Aveta</span>
|
||||
)
|
||||
)}
|
||||
{showDelete && (
|
||||
<button
|
||||
|
|
@ -96,8 +136,10 @@ export function SlideOverExtrasTab({
|
|||
avetaRequired = false,
|
||||
onUpload,
|
||||
onRemove,
|
||||
onCategorize,
|
||||
}: SlideOverExtrasTabProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const avetaAttached = hasExplicitAvetaFile(uploads);
|
||||
|
||||
const addFiles = (files: FileList | null) => {
|
||||
if (isReadOnly || pending || !files || files.length === 0) return;
|
||||
|
|
@ -128,7 +170,11 @@ export function SlideOverExtrasTab({
|
|||
{Boolean(avetaRequired) && (
|
||||
<span
|
||||
className="inline-flex items-center gap-1"
|
||||
style={{ fontSize: 11, fontWeight: 500, color: "var(--warning)" }}
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
color: avetaAttached ? "var(--success)" : "var(--warning)",
|
||||
}}
|
||||
>
|
||||
<ShieldCheck size={12} />
|
||||
Aveta
|
||||
|
|
@ -193,6 +239,7 @@ export function SlideOverExtrasTab({
|
|||
isReadOnly={isReadOnly}
|
||||
pending={pending}
|
||||
onRemove={onRemove}
|
||||
onCategorize={onCategorize}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export function SlideOverInfoTabDisplayView({
|
|||
serviceNotes={source.serviceNotes}
|
||||
extraServices={source.extraServices}
|
||||
avetaRequired={source.avetaRequired}
|
||||
avetaAttached={source.hasAvetaDocument}
|
||||
woNumber={source.woNumber}
|
||||
viewOnly
|
||||
onSave={() => undefined}
|
||||
|
|
@ -110,7 +111,9 @@ export function SlideOverInfoTabDisplayView({
|
|||
<span className="inline-flex items-center gap-1.5 text-[12.5px]">
|
||||
<PmTypeIcon pmType={source.pm} size={14} />
|
||||
{source.pm}
|
||||
{Boolean(source.avetaRequired) && <AvetaBadge size={11} />}
|
||||
{Boolean(source.avetaRequired) && (
|
||||
<AvetaBadge size={11} attached={Boolean(source.hasAvetaDocument)} />
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ color: "var(--color-text-muted)" }}>—</span>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,9 @@ export function useSlideOverMediaState({
|
|||
const extrasUploads = useMemo(
|
||||
() =>
|
||||
boardMediaItemsToMediaFiles(
|
||||
(detailForRow?.media ?? []).filter((item) => item.category === "Extra"),
|
||||
(detailForRow?.media ?? []).filter(
|
||||
(item) => item.category === "Extra" || item.category === "Aveta",
|
||||
),
|
||||
),
|
||||
[detailForRow?.media],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ export function WorkOrderSlideOverTabs({
|
|||
void state.uploadExtraFiles(files);
|
||||
}}
|
||||
onRemove={state.removeMedia}
|
||||
onCategorize={state.categorizeMedia}
|
||||
/>
|
||||
</WorkOrderSlideOverTabPanel>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,21 @@ import { Tooltip } from "@mui/material";
|
|||
|
||||
type AvetaBadgeProps = {
|
||||
size?: number;
|
||||
attached?: boolean;
|
||||
};
|
||||
|
||||
/** Per-WO Aveta shield — never a site-level flag. Color is paired with the icon. */
|
||||
export function AvetaBadge({ size = 11 }: AvetaBadgeProps) {
|
||||
export function AvetaBadge({ size = 11, attached = false }: AvetaBadgeProps) {
|
||||
const color = attached ? "var(--success)" : "var(--warning)";
|
||||
const title = attached
|
||||
? "Aveta documentation is attached"
|
||||
: "This work order requires Aveta compliance documentation";
|
||||
const label = attached ? "Aveta attached" : "Aveta required";
|
||||
|
||||
return (
|
||||
<Tooltip title="This work order requires Aveta compliance documentation" arrow placement="top">
|
||||
<span className="inline-flex shrink-0" aria-label="Aveta required">
|
||||
<ShieldCheck size={size} style={{ color: "var(--warning)" }} />
|
||||
<Tooltip title={title} arrow placement="top">
|
||||
<span className="inline-flex shrink-0" aria-label={label}>
|
||||
<ShieldCheck size={size} style={{ color }} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ type PMTypeCellProps = {
|
|||
serviceNotes?: string;
|
||||
extraServices?: string[];
|
||||
avetaRequired?: boolean;
|
||||
avetaAttached?: boolean;
|
||||
woNumber?: string;
|
||||
onSaveDetails?: (details: ServiceNotesDetails) => void;
|
||||
viewOnly?: boolean;
|
||||
|
|
@ -35,6 +36,7 @@ export function PMTypeCell({
|
|||
serviceNotes = "",
|
||||
extraServices = [],
|
||||
avetaRequired = false,
|
||||
avetaAttached = false,
|
||||
woNumber,
|
||||
onSaveDetails,
|
||||
viewOnly = false,
|
||||
|
|
@ -99,7 +101,7 @@ export function PMTypeCell({
|
|||
) : (
|
||||
<span style={{ fontSize: 13, color: "var(--color-text-muted)" }}>—</span>
|
||||
)}
|
||||
{Boolean(avetaRequired) && <AvetaBadge size={11} />}
|
||||
{Boolean(avetaRequired) && <AvetaBadge size={11} attached={avetaAttached} />}
|
||||
</button>
|
||||
{modal}
|
||||
</>
|
||||
|
|
@ -115,6 +117,7 @@ export function PMTypeCell({
|
|||
notePreview={notePreview}
|
||||
extrasCount={extras.length}
|
||||
avetaRequired={avetaRequired}
|
||||
avetaAttached={avetaAttached}
|
||||
onOpenDetails={() => setModalOpen(true)}
|
||||
/>
|
||||
{modal}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ type PmTypeDetailsTriggerProps = {
|
|||
notePreview: string;
|
||||
extrasCount: number;
|
||||
avetaRequired?: boolean;
|
||||
avetaAttached?: boolean;
|
||||
onOpenDetails: () => void;
|
||||
};
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ export function PmTypeDetailsTrigger({
|
|||
notePreview,
|
||||
extrasCount,
|
||||
avetaRequired = false,
|
||||
avetaAttached = false,
|
||||
onOpenDetails,
|
||||
}: PmTypeDetailsTriggerProps) {
|
||||
return (
|
||||
|
|
@ -44,7 +46,7 @@ export function PmTypeDetailsTrigger({
|
|||
>
|
||||
{value ? <HighlightText text={value} q={q} /> : "Select …"}
|
||||
</span>
|
||||
{Boolean(avetaRequired) && <AvetaBadge size={11} />}
|
||||
{Boolean(avetaRequired) && <AvetaBadge size={11} attached={avetaAttached} />}
|
||||
</span>
|
||||
{notePreview ? (
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export function WoTableRowServiceCells({
|
|||
serviceNotes={row.serviceNotes}
|
||||
extraServices={row.extraServices}
|
||||
avetaRequired={row.avetaRequired}
|
||||
avetaAttached={row.hasAvetaDocument}
|
||||
woNumber={row.woNumber}
|
||||
viewOnly={viewOnly}
|
||||
onSave={(pm) => onPatchRow({ pm })}
|
||||
|
|
|
|||
|
|
@ -99,11 +99,13 @@ function mapDocStatus(raw: unknown): CompletionDocStatus {
|
|||
}
|
||||
|
||||
function mapMediaCategory(raw: unknown): BoardMediaCategory {
|
||||
if (raw === "Aveta" || raw === "aveta") return "Aveta";
|
||||
const numeric = typeof raw === "number" ? raw : Number(raw);
|
||||
if (numeric === BOARD_MEDIA_CATEGORY_API.Before) return "Before";
|
||||
if (numeric === BOARD_MEDIA_CATEGORY_API.After) return "After";
|
||||
if (numeric === BOARD_MEDIA_CATEGORY_API.Extra) return "Extra";
|
||||
if (numeric === BOARD_MEDIA_CATEGORY_API.Completion) return "Completion";
|
||||
if (numeric === BOARD_MEDIA_CATEGORY_API.Aveta) return "Aveta";
|
||||
return "Extra";
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +147,7 @@ export function mapBoardDetailAuditEntry(raw: unknown, index: number): WorkOrder
|
|||
};
|
||||
}
|
||||
|
||||
export type UploadableMediaCategory = "Before" | "After" | "Extra";
|
||||
export type UploadableMediaCategory = "Before" | "After" | "Extra" | "Aveta";
|
||||
|
||||
export function buildMediaFormData(file: File, category?: UploadableMediaCategory): FormData {
|
||||
const form = new FormData();
|
||||
|
|
@ -246,6 +248,7 @@ function classifyFileName(fileName: string, url: string): FileCategory {
|
|||
}
|
||||
|
||||
function boardCategoryToMediaCategory(category: BoardMediaCategory): MediaCategory {
|
||||
if (category === "Aveta") return "Aveta";
|
||||
if (category === "Extra") return "Other";
|
||||
if (category === "Completion") return "Report";
|
||||
return "Photo";
|
||||
|
|
|
|||
|
|
@ -369,6 +369,7 @@ export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow {
|
|||
carriedOver: readNumber(item, "carriedOver", "CarriedOver") ?? 0,
|
||||
isAddOn: readIsAddOn(item),
|
||||
avetaRequired: readBool(item, "avetaRequired", "AvetaRequired"),
|
||||
hasAvetaDocument: readBool(item, "hasAvetaDocument", "HasAvetaDocument"),
|
||||
originalWeek: readString(item, "originalWeek", "OriginalWeek"),
|
||||
originalDate: toDateInputValue(readString(item, "originalDate", "OriginalDate")),
|
||||
isPastDue: readBool(item, "isPastDue", "IsPastDue"),
|
||||
|
|
|
|||
|
|
@ -2,18 +2,19 @@ import type { WorkOrderAuditEntry, WorkOrderComment } from "./work-order-detail"
|
|||
import type { WorkOrderAdditionalContact } from "./work-order-additional-contact";
|
||||
import type { CompletionDocStatus, WorkOrderTableRow } from "./work-order-table-row";
|
||||
|
||||
/** API media.category: 1=Before, 2=After, 3=Extra, 4=Completion */
|
||||
/** API media.category: 1=Before, 2=After, 3=Extra, 4=Completion, 5=Aveta */
|
||||
export const BOARD_MEDIA_CATEGORY_API = {
|
||||
Before: 1,
|
||||
After: 2,
|
||||
Extra: 3,
|
||||
Completion: 4,
|
||||
Aveta: 5,
|
||||
} as const;
|
||||
|
||||
export type BoardMediaCategoryApi =
|
||||
(typeof BOARD_MEDIA_CATEGORY_API)[keyof typeof BOARD_MEDIA_CATEGORY_API];
|
||||
|
||||
export type BoardMediaCategory = "Before" | "After" | "Extra" | "Completion";
|
||||
export type BoardMediaCategory = "Before" | "After" | "Extra" | "Completion" | "Aveta";
|
||||
|
||||
export interface WorkOrderBoardDetailInfoApi {
|
||||
id: number;
|
||||
|
|
@ -52,6 +53,8 @@ export interface WorkOrderBoardDetailInfoApi {
|
|||
extraServices?: string[] | null;
|
||||
/** Per-WO Aveta compliance flag (Service & Notes). */
|
||||
avetaRequired?: boolean | null;
|
||||
/** True when an Extra Docs file is categorized Aveta. */
|
||||
hasAvetaDocument?: boolean | null;
|
||||
docStatus?: number | null;
|
||||
flagColor?: string | null;
|
||||
primaryDispatchId?: number | null;
|
||||
|
|
|
|||
|
|
@ -160,6 +160,8 @@ export interface WorkOrderBoardRowApi {
|
|||
isAddOn?: boolean | null;
|
||||
/** Per-WO Aveta compliance flag (Service & Notes). */
|
||||
avetaRequired?: boolean | null;
|
||||
/** True when an Extra Docs file is categorized Aveta. */
|
||||
hasAvetaDocument?: boolean | null;
|
||||
/** Original schedule snapshot for Reschedule hover history. */
|
||||
originalDate?: string | null;
|
||||
originalWeek?: string | null;
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ export interface MediaFile {
|
|||
fileType: FileCategory;
|
||||
url: string;
|
||||
category: MediaCategory;
|
||||
/** Board purpose label (Before/After/Extra/Completion). Null/undefined = needs categorization. */
|
||||
purpose?: "Before" | "After" | "Extra" | "Completion" | null;
|
||||
/** Board purpose label (Before/After/Extra/Completion/Aveta). Null/undefined = needs categorization. */
|
||||
purpose?: "Before" | "After" | "Extra" | "Completion" | "Aveta" | null;
|
||||
/** True when media is a legacy Before/After/Completion column (not deletable). */
|
||||
isLegacy?: boolean;
|
||||
/** 0–100 while uploading; undefined when idle/complete. */
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ export interface WorkOrderTableRow {
|
|||
isAddOn: boolean;
|
||||
/** Per-WO Aveta compliance flag from Service & Notes (not a site attribute). */
|
||||
avetaRequired?: boolean;
|
||||
/** True when an Extra Docs file is categorized Aveta. */
|
||||
hasAvetaDocument?: boolean;
|
||||
originalWeek: string;
|
||||
originalDate: string;
|
||||
isPastDue: boolean;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,23 @@
|
|||
import type { MediaCategory, MediaFile } from "@/domain/work-orders/types/work-order-media";
|
||||
|
||||
/**
|
||||
* Extra Docs overlay category. Board purpose stays Extra
|
||||
* (API enum Before/After/Extra/Completion). Aveta is never inferred from
|
||||
* upload order — only an explicit Aveta category counts.
|
||||
* Extra Docs Aveta is an explicit API media category (value 5).
|
||||
* Upload stays Extra; only an explicit Aveta category counts as attached.
|
||||
*/
|
||||
export function resolveExtraDocCategory(file: MediaFile): MediaCategory {
|
||||
if (file.purpose === "Aveta" || file.category === "Aveta") {
|
||||
return "Aveta";
|
||||
}
|
||||
return file.category;
|
||||
}
|
||||
|
||||
export function hasExplicitAvetaFile(files: MediaFile[]): boolean {
|
||||
return files.some((item) => item.category === "Aveta");
|
||||
return files.some((item) => resolveExtraDocCategory(item) === "Aveta");
|
||||
}
|
||||
|
||||
export function isAvetaDocumentAttached(
|
||||
hasAvetaDocument: boolean | undefined,
|
||||
files: MediaFile[] = [],
|
||||
): boolean {
|
||||
return Boolean(hasAvetaDocument) || hasExplicitAvetaFile(files);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,23 @@ describe("Aveta Service & Notes", () => {
|
|||
expect(screen.getByLabelText("Aveta required")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the green Aveta shield when the document is attached", () => {
|
||||
renderWithProviders(
|
||||
<PMTypeCell
|
||||
value="HVAC"
|
||||
pmTypes={["HVAC"]}
|
||||
serviceNotes=""
|
||||
extraServices={[]}
|
||||
avetaRequired
|
||||
avetaAttached
|
||||
onSave={vi.fn()}
|
||||
onSaveDetails={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("Aveta attached")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("persists avetaRequired from Service & Notes", () => {
|
||||
const onSave = vi.fn();
|
||||
renderWithProviders(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { screen } from "@testing-library/react";
|
||||
import { fireEvent, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { SlideOverExtrasTab } from "@/app/(protected)/workorders/_components/detail/tabs/slide-over-extras-tab";
|
||||
import type { MediaFile } from "@/domain/work-orders/types/work-order-media";
|
||||
|
|
@ -51,4 +51,24 @@ describe("SlideOverExtrasTab Aveta", () => {
|
|||
expect(screen.getByText("aveta-clearance.pdf")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("Aveta").length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("categorizes an Extra file as Aveta without auto-labeling the first upload", () => {
|
||||
const onCategorize = vi.fn();
|
||||
renderWithProviders(
|
||||
<SlideOverExtrasTab
|
||||
uploads={[photo]}
|
||||
avetaRequired
|
||||
isReadOnly={false}
|
||||
onUpload={vi.fn()}
|
||||
onRemove={vi.fn()}
|
||||
onCategorize={onCategorize}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Category for safety-form.pdf"), {
|
||||
target: { value: "Aveta" },
|
||||
});
|
||||
expect(onCategorize).toHaveBeenCalledWith("m1", "Aveta");
|
||||
expect(screen.queryByText("safety-form.pdf")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -143,4 +143,38 @@ describe("useSlideOverMediaState completion filter", () => {
|
|||
expect(result.current.extrasUploads).toHaveLength(1);
|
||||
expect(result.current.extrasUploads[0]?.purpose).toBe("Extra");
|
||||
});
|
||||
|
||||
it("includes Aveta-categorized extra documents in extrasUploads", () => {
|
||||
const detail = detailWithMedia([
|
||||
{
|
||||
id: "4",
|
||||
category: "Extra",
|
||||
url: "https://cdn.example.com/extra.pdf",
|
||||
fileName: "extra.pdf",
|
||||
uploadedAt: "2026-07-15T18:00:00.0000000Z",
|
||||
isLegacy: false,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
category: "Aveta",
|
||||
url: "https://cdn.example.com/aveta.pdf",
|
||||
fileName: "aveta.pdf",
|
||||
uploadedAt: "2026-07-15T18:00:00.0000000Z",
|
||||
isLegacy: false,
|
||||
},
|
||||
]);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSlideOverMediaState({
|
||||
detailForRow: detail,
|
||||
uploadMediaMutation: idleMutation(),
|
||||
deleteMediaMutation: idleMutation(),
|
||||
categorizeMediaMutation: idleMutation(),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.extrasUploads).toHaveLength(2);
|
||||
expect(result.current.extrasUploads.map((item) => item.purpose)).toEqual(["Extra", "Aveta"]);
|
||||
expect(result.current.extrasUploads[1]?.category).toBe("Aveta");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -189,6 +189,28 @@ describe("mapWorkOrderBoardDetail", () => {
|
|||
expect(files[1]?.fileType).toBe("pdf");
|
||||
});
|
||||
|
||||
it("maps Aveta board category to Extra Docs Aveta", () => {
|
||||
const aveta = mapBoardDetailMediaItem({
|
||||
id: 22,
|
||||
category: 5,
|
||||
url: "https://cdn.example.com/aveta.pdf",
|
||||
fileName: "aveta.pdf",
|
||||
});
|
||||
const fromName = mapBoardDetailMediaItem({
|
||||
id: 23,
|
||||
category: "Aveta",
|
||||
url: "https://cdn.example.com/aveta.pdf",
|
||||
fileName: "aveta.pdf",
|
||||
});
|
||||
const files = boardMediaItemsToMediaFiles([aveta, fromName]);
|
||||
expect(files[0]).toMatchObject({
|
||||
category: "Aveta",
|
||||
purpose: "Aveta",
|
||||
fileType: "pdf",
|
||||
});
|
||||
expect(files[1]?.purpose).toBe("Aveta");
|
||||
});
|
||||
|
||||
it("maps After and Completion board categories to purpose for completion UI", () => {
|
||||
const after = mapBoardDetailMediaItem({
|
||||
id: 20,
|
||||
|
|
|
|||
|
|
@ -70,6 +70,23 @@ describe("mapWorkOrderTableRow avetaRequired", () => {
|
|||
});
|
||||
expect(row.avetaRequired).toBe(false);
|
||||
});
|
||||
|
||||
it("maps hasAvetaDocument from the board list row", () => {
|
||||
const attached = mapWorkOrderTableRow({
|
||||
id: 9,
|
||||
workOrderType: WORK_ORDER_TYPE_API.PM,
|
||||
avetaRequired: true,
|
||||
hasAvetaDocument: true,
|
||||
});
|
||||
expect(attached.hasAvetaDocument).toBe(true);
|
||||
|
||||
const pending = mapWorkOrderTableRow({
|
||||
id: 10,
|
||||
workOrderType: WORK_ORDER_TYPE_API.PM,
|
||||
avetaRequired: true,
|
||||
});
|
||||
expect(pending.hasAvetaDocument).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapWorkOrderTableRow primaryDispatchStatus", () => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||
import type { MediaFile } from "@/domain/work-orders/types/work-order-media";
|
||||
import {
|
||||
hasExplicitAvetaFile,
|
||||
isAvetaDocumentAttached,
|
||||
resolveExtraDocCategory,
|
||||
} from "@/domain/work-orders/utils/extra-docs-aveta";
|
||||
|
||||
|
|
@ -35,4 +36,16 @@ describe("extra-docs-aveta", () => {
|
|||
expect(resolveExtraDocCategory(files[0])).toBe("Photo");
|
||||
expect(hasExplicitAvetaFile(files)).toBe(false);
|
||||
});
|
||||
|
||||
it("treats purpose Aveta as an explicit Aveta file", () => {
|
||||
const aveta: MediaFile = { ...file("Other"), purpose: "Aveta" };
|
||||
expect(resolveExtraDocCategory(aveta)).toBe("Aveta");
|
||||
expect(hasExplicitAvetaFile([aveta])).toBe(true);
|
||||
expect(isAvetaDocumentAttached(false, [aveta])).toBe(true);
|
||||
});
|
||||
|
||||
it("uses hasAvetaDocument from the board row after reopen", () => {
|
||||
expect(isAvetaDocumentAttached(true, [])).toBe(true);
|
||||
expect(isAvetaDocumentAttached(false, [file("Other")])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue