DataAccessLog records bookings list/view/export and run-sheet views (actor, action, target, time); pruned after 12 months by the worker; migration 0003. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
54 lines
2.3 KiB
TypeScript
54 lines
2.3 KiB
TypeScript
import type { LoaderFunctionArgs } from "react-router";
|
|
import prisma from "../db.server";
|
|
import { toCsv } from "../engine/runsheet";
|
|
import { localParts } from "../engine/time";
|
|
import { adminContext } from "../lib/admin.server";
|
|
import { logAccess } from "../lib/audit.server";
|
|
import { bookingWhere } from "../lib/bookings-query.server";
|
|
|
|
/** Excel-friendly CSV (UTF-8 BOM) of the bookings matching the list filters. */
|
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
const { cfg, actor } = await adminContext(request);
|
|
const sp = new URL(request.url).searchParams;
|
|
logAccess(cfg.shop.id, actor, "bookings.export", sp.toString() || null);
|
|
const today = localParts(Date.now(), cfg.shop.timezone).date;
|
|
const rows = await prisma.booking.findMany({
|
|
where: bookingWhere(cfg.shop.id, sp, today),
|
|
orderBy: [{ date: "asc" }, { slotStart: "asc" }],
|
|
take: 20000,
|
|
});
|
|
const loc = Object.fromEntries(cfg.locations.map((l) => [l.id, l.name]));
|
|
const csv = toCsv(
|
|
rows.map((b) => {
|
|
const a = (b.address ?? {}) as { line1?: string; city?: string; postal?: string };
|
|
return {
|
|
order: b.orderName ?? "",
|
|
status: b.status,
|
|
review_note: b.reviewReason ?? "",
|
|
method: b.method,
|
|
location: b.locationId ? loc[b.locationId] ?? b.locationId : "",
|
|
date: b.date ?? "",
|
|
slot_start: b.slotStart ?? "",
|
|
slot_end: b.slotEnd ?? "",
|
|
timezone: b.timezone,
|
|
start_utc: b.startUtc?.toISOString() ?? "",
|
|
end_utc: b.endUtc?.toISOString() ?? "",
|
|
customer: b.customerName ?? "",
|
|
email: b.customerEmail ?? "",
|
|
phone: b.customerPhone ?? "",
|
|
address: [a.line1, a.city, a.postal].filter(Boolean).join(", "),
|
|
items: ((b.items ?? []) as { title: string; quantity: number }[]).map((i) => `${i.quantity}x ${i.title}`).join("; "),
|
|
fee: (b.fee / 100).toFixed(2),
|
|
rescheduled: b.rescheduleCount,
|
|
};
|
|
}),
|
|
["order", "status", "review_note", "method", "location", "date", "slot_start", "slot_end", "timezone", "start_utc", "end_utc", "customer", "email", "phone", "address", "items", "fee", "rescheduled"],
|
|
);
|
|
return new Response(csv, {
|
|
headers: {
|
|
"Content-Type": "text/csv; charset=utf-8",
|
|
"Content-Disposition": `attachment; filename="datewise-bookings-${today}.csv"`,
|
|
},
|
|
});
|
|
};
|