128 lines
4.8 KiB
TypeScript
128 lines
4.8 KiB
TypeScript
import { error, fail, redirect } from "@sveltejs/kit";
|
|
import type { Actions, PageServerLoad } from "./$types";
|
|
import type { StackqReceipt } from "$lib/types/receipts";
|
|
import type { ReceiptDataShape } from "$lib/types/receipts";
|
|
import { convertHeicFileToJpegIfNeeded } from "$lib/convert-heic-server";
|
|
import { parseNumWithDefault } from "$lib/parse-form";
|
|
import { validateImageUpload } from "$lib/validate-image-upload";
|
|
|
|
const COLLECTION = "Stackq_Receipts";
|
|
|
|
const STATUS_OPTIONS = ["New", "Data Entered", "Reviewed", "Consolidated"] as const;
|
|
const TYPE_OPTIONS = ["Purchase", "Gas"] as const;
|
|
const MAX_LINE_ITEMS = 500;
|
|
|
|
export const load: PageServerLoad = async ({ params, locals }) => {
|
|
const { id } = params;
|
|
try {
|
|
const record = await locals.pb
|
|
.collection(COLLECTION)
|
|
.getOne<StackqReceipt>(id, { expand: "User" });
|
|
return { receipt: record };
|
|
} catch (err: unknown) {
|
|
const is404 =
|
|
err &&
|
|
typeof err === "object" &&
|
|
"status" in err &&
|
|
(err as { status: number }).status === 404;
|
|
if (is404) throw error(404, "Receipt not found");
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
export const actions: Actions = {
|
|
update: async ({ request, locals, params }) => {
|
|
const id = params.id;
|
|
if (!id) return fail(400, { error: "Missing id", form: "update" });
|
|
|
|
const form = await request.formData();
|
|
const Status = (form.get("Status") as string)?.trim() || "New";
|
|
const Type = (form.get("Type") as string)?.trim() || "Purchase";
|
|
const dataJson = (form.get("dataJson") as string)?.trim() ?? "{}";
|
|
const imageFile = form.get("Image") as File | null;
|
|
const hasImage = imageFile && imageFile.size > 0;
|
|
if (hasImage) {
|
|
const imgError = validateImageUpload(imageFile);
|
|
if (imgError) return fail(400, { error: imgError, form: "update" });
|
|
}
|
|
|
|
let imageToSave: File | null = hasImage ? imageFile : null;
|
|
if (imageToSave) {
|
|
try {
|
|
imageToSave = await convertHeicFileToJpegIfNeeded(imageToSave);
|
|
} catch {
|
|
return fail(400, {
|
|
error: "Could not convert HEIC image. Try saving as JPEG (iPhone: Settings → Camera → Formats) or use a different photo.",
|
|
form: "update",
|
|
});
|
|
}
|
|
}
|
|
|
|
let Data: ReceiptDataShape = {};
|
|
try {
|
|
const parsed = JSON.parse(dataJson) as unknown;
|
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
const o = parsed as Record<string, unknown>;
|
|
if (Array.isArray(o.lineItems)) {
|
|
const filtered = o.lineItems.filter(
|
|
(row: unknown): row is { item: string; description: string; quantity: number; price: number } =>
|
|
row != null &&
|
|
typeof row === "object" &&
|
|
"item" in row &&
|
|
"quantity" in row &&
|
|
"price" in row
|
|
);
|
|
Data.lineItems = filtered.slice(0, MAX_LINE_ITEMS).map((row) => ({
|
|
item: String(row.item ?? ""),
|
|
description: String(row.description ?? ""),
|
|
quantity: parseNumWithDefault(row.quantity, 0),
|
|
price: parseNumWithDefault(row.price, 0),
|
|
}));
|
|
}
|
|
if (typeof o.tax === "number" && Number.isFinite(o.tax)) Data.tax = o.tax;
|
|
else if (typeof o.tax === "string") Data.tax = parseNumWithDefault(o.tax, 0);
|
|
if (typeof o.receiptTotal === "number" && Number.isFinite(o.receiptTotal)) {
|
|
Data.receiptTotal = o.receiptTotal;
|
|
} else if (typeof o.receiptTotal === "string") {
|
|
Data.receiptTotal = parseNumWithDefault(o.receiptTotal, 0);
|
|
}
|
|
}
|
|
} catch {
|
|
return fail(400, { error: "Invalid receipt data JSON", form: "update" });
|
|
}
|
|
|
|
const body: Record<string, unknown> = {
|
|
Status: STATUS_OPTIONS.includes(Status as (typeof STATUS_OPTIONS)[number]) ? Status : "New",
|
|
Type: TYPE_OPTIONS.includes(Type as (typeof TYPE_OPTIONS)[number]) ? Type : "Purchase",
|
|
Data: Object.keys(Data).length ? Data : undefined,
|
|
};
|
|
if (imageToSave) body.Image = imageToSave;
|
|
|
|
try {
|
|
await locals.pb.collection(COLLECTION).update(id, body);
|
|
} catch (e: unknown) {
|
|
const message =
|
|
e && typeof e === "object" && "message" in e
|
|
? String((e as { message: string }).message)
|
|
: "Update failed";
|
|
return fail(500, { error: message, form: "update" });
|
|
}
|
|
return { success: true };
|
|
},
|
|
|
|
delete: async ({ locals, params }) => {
|
|
const id = params.id;
|
|
if (!id) return fail(400, { error: "Missing id", form: "delete" });
|
|
try {
|
|
await locals.pb.collection(COLLECTION).delete(id);
|
|
} catch (e: unknown) {
|
|
const message =
|
|
e && typeof e === "object" && "message" in e
|
|
? String((e as { message: string }).message)
|
|
: "Delete failed";
|
|
return fail(500, { error: message, form: "delete" });
|
|
}
|
|
throw redirect(303, "/receipts");
|
|
},
|
|
};
|