mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 15:52:04 +00:00
fix(work-orders): harden CompDoc generate seed persist and blob revoke
This commit is contained in:
parent
7ea0d2c464
commit
bfc230d421
6 changed files with 255 additions and 118 deletions
|
|
@ -0,0 +1,170 @@
|
||||||
|
import type { QueryClient } from "@tanstack/react-query";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { showCompDocDraftSavedToast } from "@/app/(protected)/workorders/_components/comp-doc/comp-doc-draft-toast";
|
||||||
|
import { locationsApi } from "@/domain/locations/api/locations-api";
|
||||||
|
import type { Location } from "@/domain/locations/types/location";
|
||||||
|
import type {
|
||||||
|
CompDocData,
|
||||||
|
CompDocSaveStatus,
|
||||||
|
DocFields,
|
||||||
|
DocState,
|
||||||
|
} from "@/domain/work-orders/types/completion-doc";
|
||||||
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
import { buildDocHtml, printDoc } from "@/domain/work-orders/utils/completion-doc-html";
|
||||||
|
import { fmtDocNow } from "@/domain/work-orders/utils/completion-doc-state";
|
||||||
|
import { queryKeys } from "@/infra/query-key/query-key";
|
||||||
|
|
||||||
|
type CompDocSite = { street: string; fullAddress: string };
|
||||||
|
|
||||||
|
export function buildCompDocAddressLine(site: CompDocSite): string {
|
||||||
|
if (site.street.trim()) return site.street.trim();
|
||||||
|
return site.fullAddress !== "—" ? site.fullAddress : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function locationUpdatePayload(location: Location, address: string): Location {
|
||||||
|
return {
|
||||||
|
...location,
|
||||||
|
address,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateCompDocDialogActionsParams = {
|
||||||
|
row: WorkOrderTableRow;
|
||||||
|
notes: string;
|
||||||
|
generatedAt: string;
|
||||||
|
editDateCompleted: string;
|
||||||
|
editAddress: string;
|
||||||
|
docState: DocState;
|
||||||
|
docFields: DocFields;
|
||||||
|
site: CompDocSite;
|
||||||
|
locId: string;
|
||||||
|
isMockLocation: boolean;
|
||||||
|
locationDetail: Location | undefined;
|
||||||
|
queryClient: QueryClient;
|
||||||
|
onSave: (data: CompDocData, status: CompDocSaveStatus) => void | Promise<void>;
|
||||||
|
onDateCompletedChange?: (date: string) => void | Promise<void>;
|
||||||
|
setDocState: (state: DocState) => void;
|
||||||
|
setGeneratedAt: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createCompDocDialogActions({
|
||||||
|
row,
|
||||||
|
notes,
|
||||||
|
generatedAt,
|
||||||
|
editDateCompleted,
|
||||||
|
editAddress,
|
||||||
|
docState,
|
||||||
|
docFields,
|
||||||
|
site,
|
||||||
|
locId,
|
||||||
|
isMockLocation,
|
||||||
|
locationDetail,
|
||||||
|
queryClient,
|
||||||
|
onSave,
|
||||||
|
onDateCompletedChange,
|
||||||
|
setDocState,
|
||||||
|
setGeneratedAt,
|
||||||
|
}: CreateCompDocDialogActionsParams) {
|
||||||
|
async function persist(extra: Partial<CompDocData>, status: CompDocSaveStatus): Promise<void> {
|
||||||
|
await onSave({ notes, generatedAt, ...extra }, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncCompletedDateIfChanged(): Promise<void> {
|
||||||
|
if (editDateCompleted !== (row.completedDate || "")) {
|
||||||
|
await onDateCompletedChange?.(editDateCompleted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncAddressIfChanged(): Promise<void> {
|
||||||
|
const next = editAddress.trim();
|
||||||
|
const current = buildCompDocAddressLine(site);
|
||||||
|
if (next === current) return;
|
||||||
|
if (isMockLocation || !locId) return;
|
||||||
|
if (!locationDetail) {
|
||||||
|
throw new Error("Location details are still loading. Try saving again.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await locationsApi.update(locId, locationUpdatePayload(locationDetail, next));
|
||||||
|
void queryClient.invalidateQueries({ queryKey: queryKeys.locations.detail(locId) });
|
||||||
|
void queryClient.invalidateQueries({ queryKey: queryKeys.locations.all });
|
||||||
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.all });
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAuthoritativeState() {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.all });
|
||||||
|
if (locId && !isMockLocation) {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: queryKeys.locations.detail(locId) });
|
||||||
|
void queryClient.invalidateQueries({ queryKey: queryKeys.locations.all });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMarkNotRequired() {
|
||||||
|
const previous = docState;
|
||||||
|
setDocState("not-required");
|
||||||
|
try {
|
||||||
|
await persist({}, "NN");
|
||||||
|
} catch (error) {
|
||||||
|
setDocState(previous);
|
||||||
|
refreshAuthoritativeState();
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Failed to mark document as not required",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUndo() {
|
||||||
|
const previous = docState;
|
||||||
|
setDocState("draft");
|
||||||
|
try {
|
||||||
|
await persist({}, "No");
|
||||||
|
} catch (error) {
|
||||||
|
setDocState(previous);
|
||||||
|
refreshAuthoritativeState();
|
||||||
|
toast.error(error instanceof Error ? error.message : "Failed to undo not-required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveDraft() {
|
||||||
|
try {
|
||||||
|
await syncCompletedDateIfChanged();
|
||||||
|
await syncAddressIfChanged();
|
||||||
|
await persist({}, "No");
|
||||||
|
showCompDocDraftSavedToast();
|
||||||
|
} catch (error) {
|
||||||
|
refreshAuthoritativeState();
|
||||||
|
toast.error(error instanceof Error ? error.message : "Failed to save draft");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGenerate() {
|
||||||
|
if (!editDateCompleted.trim()) {
|
||||||
|
toast.error("Date Work Completed is required to generate the document.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ts = fmtDocNow();
|
||||||
|
try {
|
||||||
|
// Prerequisites first; only mark Generated after completedDate/address persist.
|
||||||
|
await syncCompletedDateIfChanged();
|
||||||
|
await syncAddressIfChanged();
|
||||||
|
await persist({ generatedAt: ts }, "Yes");
|
||||||
|
setGeneratedAt(ts);
|
||||||
|
setDocState("generated");
|
||||||
|
} catch (error) {
|
||||||
|
// Keep Draft UI; refresh so any partially persisted date/address is authoritative.
|
||||||
|
refreshAuthoritativeState();
|
||||||
|
toast.error(error instanceof Error ? error.message : "Failed to generate document");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDownloadPdf() {
|
||||||
|
printDoc(buildDocHtml(row, { notes, generatedAt }, docFields));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleMarkNotRequired,
|
||||||
|
handleUndo,
|
||||||
|
handleSaveDraft,
|
||||||
|
handleGenerate,
|
||||||
|
handleDownloadPdf,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "react-toastify";
|
import {
|
||||||
import { showCompDocDraftSavedToast } from "@/app/(protected)/workorders/_components/comp-doc/comp-doc-draft-toast";
|
buildCompDocAddressLine,
|
||||||
import { locationsApi } from "@/domain/locations/api/locations-api";
|
createCompDocDialogActions,
|
||||||
import type { Location } from "@/domain/locations/types/location";
|
} from "@/app/(protected)/workorders/_hooks/create-comp-doc-dialog-actions";
|
||||||
import { useLocationDetail } from "@/domain/locations/use-cases/use-location-detail";
|
import { useLocationDetail } from "@/domain/locations/use-cases/use-location-detail";
|
||||||
import type {
|
import type {
|
||||||
CompDocData,
|
CompDocData,
|
||||||
|
|
@ -13,14 +13,8 @@ import type {
|
||||||
} from "@/domain/work-orders/types/completion-doc";
|
} from "@/domain/work-orders/types/completion-doc";
|
||||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
import { getMockWoWizardLocationDetail } from "@/domain/work-orders/mocks/wo-wizard-mock-data";
|
import { getMockWoWizardLocationDetail } from "@/domain/work-orders/mocks/wo-wizard-mock-data";
|
||||||
import { buildDocHtml, printDoc } from "@/domain/work-orders/utils/completion-doc-html";
|
|
||||||
import { resolveCompDocSite } from "@/domain/work-orders/utils/completion-doc-site";
|
import { resolveCompDocSite } from "@/domain/work-orders/utils/completion-doc-site";
|
||||||
import {
|
import { DOC_STATE_CFG, docStateOf } from "@/domain/work-orders/utils/completion-doc-state";
|
||||||
DOC_STATE_CFG,
|
|
||||||
docStateOf,
|
|
||||||
fmtDocNow,
|
|
||||||
} from "@/domain/work-orders/utils/completion-doc-state";
|
|
||||||
import { queryKeys } from "@/infra/query-key/query-key";
|
|
||||||
|
|
||||||
type UseCompDocDialogParams = {
|
type UseCompDocDialogParams = {
|
||||||
row: WorkOrderTableRow;
|
row: WorkOrderTableRow;
|
||||||
|
|
@ -30,18 +24,6 @@ type UseCompDocDialogParams = {
|
||||||
onDateCompletedChange?: (date: string) => void | Promise<void>;
|
onDateCompletedChange?: (date: string) => void | Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildAddressLine(site: { street: string; fullAddress: string }): string {
|
|
||||||
if (site.street.trim()) return site.street.trim();
|
|
||||||
return site.fullAddress !== "—" ? site.fullAddress : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function locationUpdatePayload(location: Location, address: string): Location {
|
|
||||||
return {
|
|
||||||
...location,
|
|
||||||
address,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCompDocDialog({
|
export function useCompDocDialog({
|
||||||
row,
|
row,
|
||||||
open,
|
open,
|
||||||
|
|
@ -63,14 +45,22 @@ export function useCompDocDialog({
|
||||||
const [generatedAt, setGeneratedAt] = useState("");
|
const [generatedAt, setGeneratedAt] = useState("");
|
||||||
const [editDateCompleted, setEditDateCompleted] = useState("");
|
const [editDateCompleted, setEditDateCompleted] = useState("");
|
||||||
const [editAddress, setEditAddress] = useState("");
|
const [editAddress, setEditAddress] = useState("");
|
||||||
|
const seedKeyRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
// Seed once per open/WO — row/detail refetches must not wipe in-progress edits.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) {
|
||||||
|
seedKeyRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const key = String(row.id);
|
||||||
|
if (seedKeyRef.current === key) return;
|
||||||
|
seedKeyRef.current = key;
|
||||||
setNotes(data?.notes ?? "");
|
setNotes(data?.notes ?? "");
|
||||||
setDocState(docStateOf(row.docStatus));
|
setDocState(docStateOf(row.docStatus));
|
||||||
setGeneratedAt(data?.generatedAt ?? "");
|
setGeneratedAt(data?.generatedAt ?? "");
|
||||||
setEditDateCompleted(row.completedDate || "");
|
setEditDateCompleted(row.completedDate || "");
|
||||||
setEditAddress(buildAddressLine(resolveCompDocSite(row, locationDetail)));
|
setEditAddress(buildCompDocAddressLine(resolveCompDocSite(row, locationDetail)));
|
||||||
}, [open, row, data?.notes, data?.generatedAt, locationDetail]);
|
}, [open, row, data?.notes, data?.generatedAt, locationDetail]);
|
||||||
|
|
||||||
const amazonRep = row.pocName || "";
|
const amazonRep = row.pocName || "";
|
||||||
|
|
@ -102,80 +92,24 @@ export function useCompDocDialog({
|
||||||
[site, effectiveStreet],
|
[site, effectiveStreet],
|
||||||
);
|
);
|
||||||
|
|
||||||
async function persist(extra: Partial<CompDocData>, status: CompDocSaveStatus): Promise<void> {
|
const actions = createCompDocDialogActions({
|
||||||
await onSave({ notes, generatedAt, ...extra }, status);
|
row,
|
||||||
}
|
notes,
|
||||||
|
generatedAt,
|
||||||
async function syncCompletedDateIfChanged(): Promise<void> {
|
editDateCompleted,
|
||||||
if (editDateCompleted !== (row.completedDate || "")) {
|
editAddress,
|
||||||
await onDateCompletedChange?.(editDateCompleted);
|
docState,
|
||||||
}
|
docFields,
|
||||||
}
|
site,
|
||||||
|
locId,
|
||||||
async function syncAddressIfChanged(): Promise<void> {
|
isMockLocation,
|
||||||
const next = editAddress.trim();
|
locationDetail,
|
||||||
const current = buildAddressLine(site);
|
queryClient,
|
||||||
if (next === current) return;
|
onSave,
|
||||||
if (isMockLocation || !locId) return;
|
onDateCompletedChange,
|
||||||
if (!locationDetail) {
|
setDocState,
|
||||||
throw new Error("Location details are still loading. Try saving again.");
|
setGeneratedAt,
|
||||||
}
|
});
|
||||||
|
|
||||||
await locationsApi.update(locId, locationUpdatePayload(locationDetail, next));
|
|
||||||
void queryClient.invalidateQueries({ queryKey: queryKeys.locations.detail(locId) });
|
|
||||||
void queryClient.invalidateQueries({ queryKey: queryKeys.locations.all });
|
|
||||||
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.all });
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMarkNotRequired() {
|
|
||||||
setDocState("not-required");
|
|
||||||
void persist({}, "NN");
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUndo() {
|
|
||||||
setDocState("draft");
|
|
||||||
void persist({}, "No");
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshAuthoritativeState() {
|
|
||||||
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.all });
|
|
||||||
if (locId && !isMockLocation) {
|
|
||||||
void queryClient.invalidateQueries({ queryKey: queryKeys.locations.detail(locId) });
|
|
||||||
void queryClient.invalidateQueries({ queryKey: queryKeys.locations.all });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSaveDraft() {
|
|
||||||
try {
|
|
||||||
await syncCompletedDateIfChanged();
|
|
||||||
await syncAddressIfChanged();
|
|
||||||
await persist({}, "No");
|
|
||||||
showCompDocDraftSavedToast();
|
|
||||||
} catch (error) {
|
|
||||||
refreshAuthoritativeState();
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to save draft");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleGenerate() {
|
|
||||||
const ts = fmtDocNow();
|
|
||||||
try {
|
|
||||||
// Prerequisites first; only mark Generated after completedDate/address persist.
|
|
||||||
await syncCompletedDateIfChanged();
|
|
||||||
await syncAddressIfChanged();
|
|
||||||
await persist({ generatedAt: ts }, "Yes");
|
|
||||||
setGeneratedAt(ts);
|
|
||||||
setDocState("generated");
|
|
||||||
} catch (error) {
|
|
||||||
// Keep Draft UI; refresh so any partially persisted date/address is authoritative.
|
|
||||||
refreshAuthoritativeState();
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to generate document");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDownloadPdf() {
|
|
||||||
printDoc(buildDocHtml(row, { notes, generatedAt }, docFields));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
site: previewSite,
|
site: previewSite,
|
||||||
|
|
@ -192,10 +126,6 @@ export function useCompDocDialog({
|
||||||
cfg,
|
cfg,
|
||||||
isNotRequired,
|
isNotRequired,
|
||||||
isGenerated,
|
isGenerated,
|
||||||
handleMarkNotRequired,
|
...actions,
|
||||||
handleUndo,
|
|
||||||
handleSaveDraft,
|
|
||||||
handleGenerate,
|
|
||||||
handleDownloadPdf,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,12 @@ type UploadMutate = (input: {
|
||||||
onProgress?: (percent: number) => void;
|
onProgress?: (percent: number) => void;
|
||||||
}) => Promise<unknown>;
|
}) => Promise<unknown>;
|
||||||
|
|
||||||
|
function revokeBlobUrl(url: string | undefined) {
|
||||||
|
if (url?.startsWith("blob:")) {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toLocalUpload(request: MediaUploadRequest): LocalUpload {
|
function toLocalUpload(request: MediaUploadRequest): LocalUpload {
|
||||||
const classified = classifyUpload(request.file);
|
const classified = classifyUpload(request.file);
|
||||||
return {
|
return {
|
||||||
|
|
@ -60,9 +66,7 @@ export function useMediaUploadSession(
|
||||||
const index = next.findIndex((item) => item.id === request.localId);
|
const index = next.findIndex((item) => item.id === request.localId);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
const previous = next[index];
|
const previous = next[index];
|
||||||
if (previous?.url.startsWith("blob:")) {
|
revokeBlobUrl(previous?.url);
|
||||||
URL.revokeObjectURL(previous.url);
|
|
||||||
}
|
|
||||||
next[index] = entry;
|
next[index] = entry;
|
||||||
} else {
|
} else {
|
||||||
next.push(entry);
|
next.push(entry);
|
||||||
|
|
@ -84,11 +88,23 @@ export function useMediaUploadSession(
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setLocalUploads((prev) => prev.filter((item) => !isSameAttempt(item, request)));
|
setLocalUploads((prev) => {
|
||||||
|
const dropped = prev.filter((item) => isSameAttempt(item, request));
|
||||||
|
for (const item of dropped) {
|
||||||
|
revokeBlobUrl(item.url);
|
||||||
|
}
|
||||||
|
return prev.filter((item) => !isSameAttempt(item, request));
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
if (error.name === "AbortError") {
|
if (error.name === "AbortError") {
|
||||||
setLocalUploads((prev) => prev.filter((item) => !isSameAttempt(item, request)));
|
setLocalUploads((prev) => {
|
||||||
|
const dropped = prev.filter((item) => isSameAttempt(item, request));
|
||||||
|
for (const item of dropped) {
|
||||||
|
revokeBlobUrl(item.url);
|
||||||
|
}
|
||||||
|
return prev.filter((item) => !isSameAttempt(item, request));
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLocalUploads((prev) =>
|
setLocalUploads((prev) =>
|
||||||
|
|
@ -121,7 +137,13 @@ export function useMediaUploadSession(
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeLocal(id: string) {
|
function removeLocal(id: string) {
|
||||||
setLocalUploads((prev) => prev.filter((item) => item.id !== id));
|
setLocalUploads((prev) => {
|
||||||
|
const dropped = prev.filter((item) => item.id === id);
|
||||||
|
for (const item of dropped) {
|
||||||
|
revokeBlobUrl(item.url);
|
||||||
|
}
|
||||||
|
return prev.filter((item) => item.id !== id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLocal(id: string) {
|
function isLocal(id: string) {
|
||||||
|
|
|
||||||
|
|
@ -161,8 +161,9 @@ export default function WorkOrdersListPage() {
|
||||||
completionDocs={completionDocs}
|
completionDocs={completionDocs}
|
||||||
onSaveCompDoc={async (data, status) => {
|
onSaveCompDoc={async (data, status) => {
|
||||||
if (!activeDocRow) return;
|
if (!activeDocRow) return;
|
||||||
setCompletionDocs((prev) => ({ ...prev, [String(activeDocRow.id)]: data }));
|
// Persist board docStatus first so local generatedAt/notes never outrun the server.
|
||||||
await patchFieldAsync(activeDocRow.id, { docStatus: status });
|
await patchFieldAsync(activeDocRow.id, { docStatus: status });
|
||||||
|
setCompletionDocs((prev) => ({ ...prev, [String(activeDocRow.id)]: data }));
|
||||||
}}
|
}}
|
||||||
onDateCompletedChange={async (date) => {
|
onDateCompletedChange={async (date) => {
|
||||||
if (!activeDocRow) return;
|
if (!activeDocRow) return;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,13 @@
|
||||||
import type { WorkOrderBoardEditableField } from "@/domain/work-orders/types/work-order-board";
|
import type { WorkOrderBoardEditableField } from "@/domain/work-orders/types/work-order-board";
|
||||||
import type { WorkOrderTablePatch } from "@/domain/work-orders/types/work-order-table-row";
|
import type { WorkOrderTablePatch } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
import { mapDocStatusToApi } from "@/domain/work-orders/mappers/doc-status-mapper";
|
||||||
|
|
||||||
export type BoardFieldOp = {
|
export type BoardFieldOp = {
|
||||||
field: WorkOrderBoardEditableField;
|
field: WorkOrderBoardEditableField;
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function mapDocStatusToApi(value: string): string {
|
export { mapDocStatusToApi };
|
||||||
if (value === "NN") {
|
|
||||||
return "NotNeeded";
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ScalarBoardOpRule = {
|
type ScalarBoardOpRule = {
|
||||||
key: keyof WorkOrderTablePatch;
|
key: keyof WorkOrderTablePatch;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
import { CompDocDialog } from "@/app/(protected)/workorders/_components/comp-doc/comp-doc-dialog";
|
import { CompDocDialog } from "@/app/(protected)/workorders/_components/comp-doc/comp-doc-dialog";
|
||||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
|
||||||
|
|
@ -73,6 +74,7 @@ describe("CompDocDialog Save Draft / Generate completion contract", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
onSave.mockReset();
|
onSave.mockReset();
|
||||||
onDateCompletedChange.mockReset();
|
onDateCompletedChange.mockReset();
|
||||||
|
vi.mocked(toast.error).mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderDialog(row: WorkOrderTableRow) {
|
function renderDialog(row: WorkOrderTableRow) {
|
||||||
|
|
@ -148,6 +150,22 @@ describe("CompDocDialog Save Draft / Generate completion contract", () => {
|
||||||
expect(row.status).toBe("Scheduled");
|
expect(row.status).toBe("Scheduled");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Generate keeps Draft and skips docStatus Yes when Date Work Completed is blank", async () => {
|
||||||
|
const row = baseRow({ completedDate: "" });
|
||||||
|
renderDialog(row);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /^generate$/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(toast.error).toHaveBeenCalledWith(
|
||||||
|
"Date Work Completed is required to generate the document.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(onDateCompletedChange).not.toHaveBeenCalled();
|
||||||
|
expect(onSave).not.toHaveBeenCalled();
|
||||||
|
expect(screen.getByRole("button", { name: /^generate$/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("Generate keeps Draft and skips docStatus Yes when completedDate patch fails", async () => {
|
it("Generate keeps Draft and skips docStatus Yes when completedDate patch fails", async () => {
|
||||||
const row = baseRow({ completedDate: "2026-07-01" });
|
const row = baseRow({ completedDate: "2026-07-01" });
|
||||||
const client = new QueryClient({
|
const client = new QueryClient({
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue