mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 06:53:12 +00:00
Compare commits
2 commits
f2ece0f248
...
e8c3a8ef4f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8c3a8ef4f | ||
|
|
64686a59eb |
13 changed files with 232 additions and 39 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 156 KiB |
|
|
@ -6,8 +6,8 @@ import { Text } from "@/components/ui/text";
|
||||||
const ACCEPTED_TYPES = new Set(["application/pdf", "image/jpeg", "image/jpg", "image/png"]);
|
const ACCEPTED_TYPES = new Set(["application/pdf", "image/jpeg", "image/jpg", "image/png"]);
|
||||||
|
|
||||||
type UpliftAttachmentPickerProps = {
|
type UpliftAttachmentPickerProps = {
|
||||||
file: File | null;
|
files: File[];
|
||||||
onChange: (file: File | null) => void;
|
onChange: (files: File[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function isAllowedUpliftEvidenceFile(file: File): boolean {
|
function isAllowedUpliftEvidenceFile(file: File): boolean {
|
||||||
|
|
@ -15,9 +15,9 @@ function isAllowedUpliftEvidenceFile(file: File): boolean {
|
||||||
return /\.(pdf|jpe?g|png)$/i.test(file.name);
|
return /\.(pdf|jpe?g|png)$/i.test(file.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function firstAllowedFile(incoming: FileList | null): File | null {
|
function allowedFiles(incoming: FileList | null): File[] {
|
||||||
if (!incoming || incoming.length === 0) return null;
|
if (!incoming || incoming.length === 0) return [];
|
||||||
return Array.from(incoming).find(isAllowedUpliftEvidenceFile) ?? null;
|
return Array.from(incoming).filter(isAllowedUpliftEvidenceFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UpliftStagedFile({ file, onRemove }: { file: File; onRemove: () => void }) {
|
function UpliftStagedFile({ file, onRemove }: { file: File; onRemove: () => void }) {
|
||||||
|
|
@ -46,19 +46,20 @@ function UpliftStagedFile({ file, onRemove }: { file: File; onRemove: () => void
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UpliftAttachmentPicker({ file, onChange }: UpliftAttachmentPickerProps) {
|
export function UpliftAttachmentPicker({ files, onChange }: UpliftAttachmentPickerProps) {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const stageFile = (incoming: FileList | null) => {
|
const stageFiles = (incoming: FileList | null) => {
|
||||||
const next = firstAllowedFile(incoming);
|
const next = allowedFiles(incoming);
|
||||||
if (next) onChange(next);
|
if (next.length === 0) return;
|
||||||
|
onChange([...files, ...next]);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<FieldLabel>Attachment</FieldLabel>
|
<FieldLabel>Attachment</FieldLabel>
|
||||||
<Text as="p" variant="caption" tone="muted" className="mb-2">
|
<Text as="p" variant="caption" tone="muted" className="mb-2">
|
||||||
Optional — PDF, JPG, or PNG.
|
Optional — one or more PDF, JPG, or PNG files.
|
||||||
</Text>
|
</Text>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -66,7 +67,7 @@ export function UpliftAttachmentPicker({ file, onChange }: UpliftAttachmentPicke
|
||||||
onDragOver={(event) => event.preventDefault()}
|
onDragOver={(event) => event.preventDefault()}
|
||||||
onDrop={(event) => {
|
onDrop={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
stageFile(event.dataTransfer.files);
|
stageFiles(event.dataTransfer.files);
|
||||||
}}
|
}}
|
||||||
className="flex w-full items-center justify-center gap-2 transition-colors hover:bg-(--color-bg-muted)"
|
className="flex w-full items-center justify-center gap-2 transition-colors hover:bg-(--color-bg-muted)"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -77,22 +78,29 @@ export function UpliftAttachmentPicker({ file, onChange }: UpliftAttachmentPicke
|
||||||
>
|
>
|
||||||
<Upload size={14} aria-hidden style={{ color: "var(--color-text-muted)" }} />
|
<Upload size={14} aria-hidden style={{ color: "var(--color-text-muted)" }} />
|
||||||
<Text as="span" variant="caption" tone="muted">
|
<Text as="span" variant="caption" tone="muted">
|
||||||
Drag a file here or click to browse
|
Drag files here or click to browse
|
||||||
</Text>
|
</Text>
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="file"
|
type="file"
|
||||||
|
multiple
|
||||||
accept="application/pdf,image/jpeg,image/png,.pdf,.jpg,.jpeg,.png"
|
accept="application/pdf,image/jpeg,image/png,.pdf,.jpg,.jpeg,.png"
|
||||||
hidden
|
hidden
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
stageFile(event.target.files);
|
stageFiles(event.target.files);
|
||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{file != null && (
|
{files.length > 0 && (
|
||||||
<div className="mt-2">
|
<div className="mt-2 flex flex-col gap-1">
|
||||||
<UpliftStagedFile file={file} onRemove={() => onChange(null)} />
|
{files.map((file, index) => (
|
||||||
|
<UpliftStagedFile
|
||||||
|
key={`${file.name}-${file.lastModified}-${index}`}
|
||||||
|
file={file}
|
||||||
|
onRemove={() => onChange(files.filter((_, itemIndex) => itemIndex !== index))}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,7 @@ import {
|
||||||
type WorkOrderUpliftCreateFormProps = {
|
type WorkOrderUpliftCreateFormProps = {
|
||||||
pending?: boolean;
|
pending?: boolean;
|
||||||
remainingAllowance: number;
|
remainingAllowance: number;
|
||||||
onSubmit: (input: {
|
onSubmit: (input: { amount: number; notes: string; files: File[] }) => void | Promise<unknown>;
|
||||||
amount: number;
|
|
||||||
notes: string;
|
|
||||||
file: File | null;
|
|
||||||
}) => void | Promise<unknown>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function canSubmitUplift(amount: number, notes: string): boolean {
|
function canSubmitUplift(amount: number, notes: string): boolean {
|
||||||
|
|
@ -64,17 +60,17 @@ export function WorkOrderUpliftCreateForm({
|
||||||
}: WorkOrderUpliftCreateFormProps) {
|
}: WorkOrderUpliftCreateFormProps) {
|
||||||
const [amount, setAmount] = useState("");
|
const [amount, setAmount] = useState("");
|
||||||
const [notes, setNotes] = useState("");
|
const [notes, setNotes] = useState("");
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
const parsedAmount = Number(amount);
|
const parsedAmount = Number(amount);
|
||||||
const helper = upliftAllowanceHelper(parsedAmount, remainingAllowance);
|
const helper = upliftAllowanceHelper(parsedAmount, remainingAllowance);
|
||||||
const canSubmit = canSubmitUplift(parsedAmount, notes);
|
const canSubmit = canSubmitUplift(parsedAmount, notes);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
await onSubmit({ amount: parsedAmount, notes: notes.trim(), file });
|
await onSubmit({ amount: parsedAmount, notes: notes.trim(), files });
|
||||||
setAmount("");
|
setAmount("");
|
||||||
setNotes("");
|
setNotes("");
|
||||||
setFile(null);
|
setFiles([]);
|
||||||
} catch {
|
} catch {
|
||||||
// why: the mutation toasts onError, so a failed submit keeps the typed values.
|
// why: the mutation toasts onError, so a failed submit keeps the typed values.
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +109,7 @@ export function WorkOrderUpliftCreateForm({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UpliftAttachmentPicker file={file} onChange={setFile} />
|
<UpliftAttachmentPicker files={files} onChange={setFiles} />
|
||||||
|
|
||||||
<div className="mt-3 flex justify-end">
|
<div className="mt-3 flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,8 @@ function UpliftFiledAttachment({
|
||||||
}) {
|
}) {
|
||||||
const hasUrl = file.url !== "";
|
const hasUrl = file.url !== "";
|
||||||
const downloadEvidence = () => {
|
const downloadEvidence = () => {
|
||||||
void upliftsApi.downloadEvidence(upliftId, file.name).catch((error: unknown) => {
|
const documentId = /^\d+$/.test(file.id) ? file.id : undefined;
|
||||||
|
void upliftsApi.downloadEvidence(upliftId, file.name, documentId).catch((error: unknown) => {
|
||||||
toast.error(error instanceof Error ? error.message : "Unable to download evidence.");
|
toast.error(error instanceof Error ? error.message : "Unable to download evidence.");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -105,8 +105,9 @@ export const upliftsApi = {
|
||||||
downloadEvidence: async (
|
downloadEvidence: async (
|
||||||
id: string | number,
|
id: string | number,
|
||||||
fallbackFileName = "uplift-evidence",
|
fallbackFileName = "uplift-evidence",
|
||||||
|
documentId?: string | number,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const response = await fetchEvidence(id, "upliftsApi.downloadEvidence");
|
const response = await fetchEvidence(id, "upliftsApi.downloadEvidence", documentId);
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
saveBlob(blob, readContentDispositionFilename(response, fallbackFileName));
|
saveBlob(blob, readContentDispositionFilename(response, fallbackFileName));
|
||||||
},
|
},
|
||||||
|
|
@ -120,8 +121,9 @@ export const upliftsApi = {
|
||||||
id: string | number,
|
id: string | number,
|
||||||
tab: Window | null,
|
tab: Window | null,
|
||||||
fallbackFileName = "uplift-evidence",
|
fallbackFileName = "uplift-evidence",
|
||||||
|
documentId?: string | number,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const response = await fetchEvidence(id, "upliftsApi.openEvidence");
|
const response = await fetchEvidence(id, "upliftsApi.openEvidence", documentId);
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
const type = blob.type.split(";")[0].trim().toLowerCase();
|
const type = blob.type.split(";")[0].trim().toLowerCase();
|
||||||
if (tab == null || !INLINE_EVIDENCE_TYPES.has(type)) {
|
if (tab == null || !INLINE_EVIDENCE_TYPES.has(type)) {
|
||||||
|
|
@ -145,9 +147,14 @@ const INLINE_EVIDENCE_TYPES = new Set([
|
||||||
|
|
||||||
const EVIDENCE_URL_TTL_MS = 60_000;
|
const EVIDENCE_URL_TTL_MS = 60_000;
|
||||||
|
|
||||||
async function fetchEvidence(id: string | number, operation: string): Promise<Response> {
|
async function fetchEvidence(
|
||||||
|
id: string | number,
|
||||||
|
operation: string,
|
||||||
|
documentId?: string | number,
|
||||||
|
): Promise<Response> {
|
||||||
|
const options = documentId == null ? undefined : { searchParams: { documentId } };
|
||||||
try {
|
try {
|
||||||
return await apiRequestRaw("get", evidenceUrl(id), undefined, operation);
|
return await apiRequestRaw("get", evidenceUrl(id), options, operation);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HTTPError) {
|
if (error instanceof HTTPError) {
|
||||||
throw evidenceHttpError(error.response.status);
|
throw evidenceHttpError(error.response.status);
|
||||||
|
|
|
||||||
|
|
@ -46,13 +46,21 @@ export const workOrderUpliftsApi = {
|
||||||
workOrderId: string | number,
|
workOrderId: string | number,
|
||||||
input: CreateWorkOrderUpliftInput,
|
input: CreateWorkOrderUpliftInput,
|
||||||
): Promise<WorkOrderUplift> => {
|
): Promise<WorkOrderUplift> => {
|
||||||
const body: { amount: number; notes: string; evidenceDocumentId?: number } = {
|
const body: {
|
||||||
|
amount: number;
|
||||||
|
notes: string;
|
||||||
|
evidenceDocumentId?: number;
|
||||||
|
evidenceDocumentIds?: number[];
|
||||||
|
} = {
|
||||||
amount: input.amount,
|
amount: input.amount,
|
||||||
notes: input.notes,
|
notes: input.notes,
|
||||||
};
|
};
|
||||||
if (input.evidenceDocumentId != null) {
|
if (input.evidenceDocumentId != null) {
|
||||||
body.evidenceDocumentId = input.evidenceDocumentId;
|
body.evidenceDocumentId = input.evidenceDocumentId;
|
||||||
}
|
}
|
||||||
|
if (input.evidenceDocumentIds != null && input.evidenceDocumentIds.length > 0) {
|
||||||
|
body.evidenceDocumentIds = input.evidenceDocumentIds;
|
||||||
|
}
|
||||||
const data = await apiPost<unknown>(API_PATHS.workOrder.uplifts(workOrderId), body);
|
const data = await apiPost<unknown>(API_PATHS.workOrder.uplifts(workOrderId), body);
|
||||||
return mapWorkOrderUplift(handleApiResponse(data));
|
return mapWorkOrderUplift(handleApiResponse(data));
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,9 @@ export interface WorkOrderUplift {
|
||||||
export interface CreateWorkOrderUpliftInput {
|
export interface CreateWorkOrderUpliftInput {
|
||||||
amount: number;
|
amount: number;
|
||||||
notes: string;
|
notes: string;
|
||||||
file?: File | null;
|
files?: File[];
|
||||||
evidenceDocumentId?: number;
|
evidenceDocumentId?: number;
|
||||||
|
evidenceDocumentIds?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EMPTY_UPLIFT_SUMMARY: UpliftSummary = {
|
export const EMPTY_UPLIFT_SUMMARY: UpliftSummary = {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ type CreateUpliftBody = {
|
||||||
amount: number;
|
amount: number;
|
||||||
notes: string;
|
notes: string;
|
||||||
evidenceDocumentId?: number;
|
evidenceDocumentId?: number;
|
||||||
|
evidenceDocumentIds?: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SubmitWorkOrderUpliftDeps = {
|
export type SubmitWorkOrderUpliftDeps = {
|
||||||
|
|
@ -62,12 +63,15 @@ export async function submitWorkOrderUplift(
|
||||||
input: CreateWorkOrderUpliftInput,
|
input: CreateWorkOrderUpliftInput,
|
||||||
deps: SubmitWorkOrderUpliftDeps,
|
deps: SubmitWorkOrderUpliftDeps,
|
||||||
): Promise<WorkOrderUplift> {
|
): Promise<WorkOrderUplift> {
|
||||||
const evidenceDocumentId = input.file
|
const evidenceDocumentIds: number[] = [];
|
||||||
? await waitForPassedScan(workOrderId, input.file, deps)
|
for (const file of input.files ?? []) {
|
||||||
: undefined;
|
evidenceDocumentIds.push(await waitForPassedScan(workOrderId, file, deps));
|
||||||
|
}
|
||||||
|
const [evidenceDocumentId] = evidenceDocumentIds;
|
||||||
return deps.create(workOrderId, {
|
return deps.create(workOrderId, {
|
||||||
amount: input.amount,
|
amount: input.amount,
|
||||||
notes: input.notes,
|
notes: input.notes,
|
||||||
evidenceDocumentId,
|
evidenceDocumentId,
|
||||||
|
evidenceDocumentIds: evidenceDocumentIds.length > 0 ? evidenceDocumentIds : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { fireEvent, screen } from "@testing-library/react";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { UpliftAttachmentPicker } from "@/app/(protected)/workorders/_components/uplifts/uplift-attachment-picker";
|
||||||
|
import { renderWithProviders } from "@/test/test-utils";
|
||||||
|
|
||||||
|
function PickerHarness({ onChange }: { onChange: (files: File[]) => void }) {
|
||||||
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
return (
|
||||||
|
<UpliftAttachmentPicker
|
||||||
|
files={files}
|
||||||
|
onChange={(next) => {
|
||||||
|
setFiles(next);
|
||||||
|
onChange(next);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectFiles(files: File[]) {
|
||||||
|
const input = document.querySelector('input[type="file"]');
|
||||||
|
fireEvent.change(input as HTMLInputElement, { target: { files } });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("UpliftAttachmentPicker", () => {
|
||||||
|
it("keeps every allowed file from one multi-select", () => {
|
||||||
|
const onChange = vi.fn();
|
||||||
|
renderWithProviders(<PickerHarness onChange={onChange} />);
|
||||||
|
const quote = new File(["%PDF"], "quote.pdf", { type: "application/pdf" });
|
||||||
|
const notes = new File(["text"], "notes.txt", { type: "text/plain" });
|
||||||
|
const photo = new File(["jpeg"], "photo.jpg", { type: "image/jpeg" });
|
||||||
|
|
||||||
|
selectFiles([quote, notes, photo]);
|
||||||
|
|
||||||
|
expect(screen.getByText("quote.pdf")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("photo.jpg")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("notes.txt")).not.toBeInTheDocument();
|
||||||
|
expect(onChange).toHaveBeenCalledWith([quote, photo]);
|
||||||
|
expect(document.querySelector('input[type="file"]')?.hasAttribute("multiple")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("appends a later selection instead of replacing the staged files", () => {
|
||||||
|
const onChange = vi.fn();
|
||||||
|
renderWithProviders(<PickerHarness onChange={onChange} />);
|
||||||
|
const quote = new File(["%PDF"], "quote.pdf", { type: "application/pdf" });
|
||||||
|
const photo = new File(["jpeg"], "photo.jpg", { type: "image/jpeg" });
|
||||||
|
|
||||||
|
selectFiles([quote]);
|
||||||
|
selectFiles([photo]);
|
||||||
|
|
||||||
|
expect(screen.getByText("quote.pdf")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("photo.jpg")).toBeInTheDocument();
|
||||||
|
expect(onChange).toHaveBeenLastCalledWith([quote, photo]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { fireEvent, screen } from "@testing-library/react";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { WorkOrderUpliftListItem } from "@/app/(protected)/workorders/_components/uplifts/work-order-uplift-list-item";
|
||||||
|
import type { WorkOrderUplift } from "@/domain/work-orders/types/work-order-uplift";
|
||||||
|
import { renderWithProviders } from "@/test/test-utils";
|
||||||
|
|
||||||
|
const downloadEvidence = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
||||||
|
|
||||||
|
vi.mock("@/domain/uplifts/api/uplifts-api", () => ({
|
||||||
|
upliftsApi: { downloadEvidence },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const uplift: WorkOrderUplift = {
|
||||||
|
id: 9,
|
||||||
|
status: "auto_approved",
|
||||||
|
amount: 25,
|
||||||
|
notes: "Leak",
|
||||||
|
requestedAt: "2026-07-20T10:00:00Z",
|
||||||
|
requestedById: "dispatcher-1",
|
||||||
|
requestedByName: "Dispatcher",
|
||||||
|
decidedAt: "",
|
||||||
|
decidedByName: "",
|
||||||
|
decisionNote: "",
|
||||||
|
attachments: [
|
||||||
|
{ id: "22", name: "quote.pdf", url: "" },
|
||||||
|
{ id: "23", name: "photo.jpg", url: "" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("WorkOrderUpliftListItem evidence download", () => {
|
||||||
|
it("downloads each attachment by its document id", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<WorkOrderUpliftListItem
|
||||||
|
uplift={uplift}
|
||||||
|
readOnly={false}
|
||||||
|
currentUserId="dispatcher-1"
|
||||||
|
currentUserIsAdmin={false}
|
||||||
|
/>,
|
||||||
|
{ withAuth: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "photo.jpg" }));
|
||||||
|
|
||||||
|
expect(downloadEvidence).toHaveBeenCalledWith(9, "photo.jpg", "23");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -43,6 +43,27 @@ describe("upliftsApi", () => {
|
||||||
await expect(upliftsApi.canApprove(2)).resolves.toBe(false);
|
await expect(upliftsApi.canApprove(2)).resolves.toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("requests one linked document when a document id is provided", async () => {
|
||||||
|
apiRequestRaw.mockResolvedValueOnce(
|
||||||
|
new Response("%PDF", {
|
||||||
|
headers: { "Content-Disposition": 'attachment; filename="photo.jpg"' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
URL.createObjectURL = vi.fn(() => "blob:evidence");
|
||||||
|
URL.revokeObjectURL = vi.fn();
|
||||||
|
const click = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {});
|
||||||
|
|
||||||
|
await upliftsApi.downloadEvidence(7, "photo.jpg", 23);
|
||||||
|
|
||||||
|
expect(apiRequestRaw).toHaveBeenCalledWith(
|
||||||
|
"get",
|
||||||
|
"uplifts/7/evidence",
|
||||||
|
{ searchParams: { documentId: 23 } },
|
||||||
|
"upliftsApi.downloadEvidence",
|
||||||
|
);
|
||||||
|
click.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
describe("openEvidence", () => {
|
describe("openEvidence", () => {
|
||||||
function evidenceResponse(body: string, contentType: string): Response {
|
function evidenceResponse(body: string, contentType: string): Response {
|
||||||
return new Response(body, {
|
return new Response(body, {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,24 @@ describe("workOrderUpliftsApi", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes every scanned document id when several files passed", async () => {
|
||||||
|
apiPost.mockResolvedValueOnce({ id: 9, status: "pending", amount: 25, notes: "Leak" });
|
||||||
|
|
||||||
|
await workOrderUpliftsApi.create(7, {
|
||||||
|
amount: 25,
|
||||||
|
notes: "Leak",
|
||||||
|
evidenceDocumentId: 22,
|
||||||
|
evidenceDocumentIds: [22, 23],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(apiPost).toHaveBeenCalledWith("workorders/7/uplifts", {
|
||||||
|
amount: 25,
|
||||||
|
notes: "Leak",
|
||||||
|
evidenceDocumentId: 22,
|
||||||
|
evidenceDocumentIds: [22, 23],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("uploads one evidence file and reads the pending document id", async () => {
|
it("uploads one evidence file and reads the pending document id", async () => {
|
||||||
apiPostForm.mockResolvedValueOnce({
|
apiPostForm.mockResolvedValueOnce({
|
||||||
data: { id: 22, scanStatus: "Pending", originalFileName: "quote.pdf" },
|
data: { id: 22, scanStatus: "Pending", originalFileName: "quote.pdf" },
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,14 @@ describe("submitWorkOrderUplift", () => {
|
||||||
it("creates without an evidence id when no file is attached", async () => {
|
it("creates without an evidence id when no file is attached", async () => {
|
||||||
const collaborators = deps();
|
const collaborators = deps();
|
||||||
|
|
||||||
await submitWorkOrderUplift(7, { amount: 25, notes: "Leak", file: null }, collaborators);
|
await submitWorkOrderUplift(7, { amount: 25, notes: "Leak", files: [] }, collaborators);
|
||||||
|
|
||||||
expect(collaborators.uploadEvidence).not.toHaveBeenCalled();
|
expect(collaborators.uploadEvidence).not.toHaveBeenCalled();
|
||||||
expect(collaborators.create).toHaveBeenCalledWith(7, {
|
expect(collaborators.create).toHaveBeenCalledWith(7, {
|
||||||
amount: 25,
|
amount: 25,
|
||||||
notes: "Leak",
|
notes: "Leak",
|
||||||
evidenceDocumentId: undefined,
|
evidenceDocumentId: undefined,
|
||||||
|
evidenceDocumentIds: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -35,13 +36,14 @@ describe("submitWorkOrderUplift", () => {
|
||||||
getEvidenceStatus: vi.fn().mockResolvedValue({ id: 22, scanStatus: "Passed" }),
|
getEvidenceStatus: vi.fn().mockResolvedValue({ id: 22, scanStatus: "Passed" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
await submitWorkOrderUplift(7, { amount: 25, notes: "Leak", file: quote }, collaborators);
|
await submitWorkOrderUplift(7, { amount: 25, notes: "Leak", files: [quote] }, collaborators);
|
||||||
|
|
||||||
expect(collaborators.uploadEvidence).toHaveBeenCalledWith(7, quote);
|
expect(collaborators.uploadEvidence).toHaveBeenCalledWith(7, quote);
|
||||||
expect(collaborators.create).toHaveBeenCalledWith(7, {
|
expect(collaborators.create).toHaveBeenCalledWith(7, {
|
||||||
amount: 25,
|
amount: 25,
|
||||||
notes: "Leak",
|
notes: "Leak",
|
||||||
evidenceDocumentId: 22,
|
evidenceDocumentId: 22,
|
||||||
|
evidenceDocumentIds: [22],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -52,7 +54,7 @@ describe("submitWorkOrderUplift", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
submitWorkOrderUplift(7, { amount: 25, notes: "Leak", file: quote }, collaborators),
|
submitWorkOrderUplift(7, { amount: 25, notes: "Leak", files: [quote] }, collaborators),
|
||||||
).rejects.toThrow(/rejected/i);
|
).rejects.toThrow(/rejected/i);
|
||||||
expect(collaborators.create).not.toHaveBeenCalled();
|
expect(collaborators.create).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
@ -65,9 +67,35 @@ describe("submitWorkOrderUplift", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
submitWorkOrderUplift(7, { amount: 25, notes: "Leak", file: quote }, collaborators),
|
submitWorkOrderUplift(7, { amount: 25, notes: "Leak", files: [quote] }, collaborators),
|
||||||
).rejects.toThrow(/still being scanned/i);
|
).rejects.toThrow(/still being scanned/i);
|
||||||
expect(collaborators.create).not.toHaveBeenCalled();
|
expect(collaborators.create).not.toHaveBeenCalled();
|
||||||
expect(collaborators.sleep).toHaveBeenCalledTimes(30);
|
expect(collaborators.sleep).toHaveBeenCalledTimes(30);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uploads every file and sends each passed document id", async () => {
|
||||||
|
const quote = new File(["%PDF"], "quote.pdf", { type: "application/pdf" });
|
||||||
|
const photo = new File(["jpeg"], "photo.jpg", { type: "image/jpeg" });
|
||||||
|
const collaborators = deps({
|
||||||
|
uploadEvidence: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({ id: 22, scanStatus: "Passed" })
|
||||||
|
.mockResolvedValueOnce({ id: 23, scanStatus: "Passed" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await submitWorkOrderUplift(
|
||||||
|
7,
|
||||||
|
{ amount: 25, notes: "Leak", files: [quote, photo] },
|
||||||
|
collaborators,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(collaborators.uploadEvidence).toHaveBeenNthCalledWith(1, 7, quote);
|
||||||
|
expect(collaborators.uploadEvidence).toHaveBeenNthCalledWith(2, 7, photo);
|
||||||
|
expect(collaborators.create).toHaveBeenCalledWith(7, {
|
||||||
|
amount: 25,
|
||||||
|
notes: "Leak",
|
||||||
|
evidenceDocumentId: 22,
|
||||||
|
evidenceDocumentIds: [22, 23],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue