WFDASH-WEBAPP/scripts/migrations/2026-09-28-create-rep-trip-legs.ts
Dave 7555196166 feat(audit): implement audit trail for delivery notes and orders
- Added an audit trail feature that logs every create, update, and delete action on delivery notes and orders, capturing detailed changes per field.
- Introduced the `AuditTimeline` component to display the history of changes for delivery notes and orders, enhancing traceability.
- Updated the `page.tsx` files for delivery notes and orders to fetch and display audit logs, ensuring users can view the history of modifications.
- Enhanced the `package.json` with a new migration script for setting up the audit log functionality.

This feature improves accountability and transparency in the management of delivery notes and orders.
2026-09-30 01:48:44 +02:00

130 lines
4.5 KiB
TypeScript

/**
* One-shot migration: create the `rep_trip_legs` collection.
*
* Run with:
*
* npx tsx scripts/migrations/2026-09-28-create-rep-trip-legs.ts
*
* One row per contiguous interval from a vehicle-tracker's "Travel History"
* CSV export — exactly what the file gives us, un-aggregated. Nothing is
* interpreted at write time: the work-hours clip, the proximity clustering
* and the company matching all happen at read time
* (`src/lib/server/rep-time-log-service.ts`) so that backfilling company
* coordinates later automatically improves historical reports without a
* re-import.
*
* Writes are superuser-only (null) — rows arrive via
* `POST /api/v1/reports/rep-performance/[repId]/time-log/import`, which
* authenticates as an admin and uses the admin client.
*
* Idempotent — safe to re-run; exits early if the collection exists.
*
* Rollback: delete the `rep_trip_legs` collection in the PB admin UI.
* Nothing outside the time-log feature references it.
*/
import "./_env-loader";
import PocketBase from "pocketbase";
const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090";
const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL;
const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD;
// Explicit `max` on every text field: PB treats `max: 0` as a 5000-char
// cap, not "unlimited", and fails the insert with a generic 400.
const text = (name: string, max: number, required = false) => ({
name,
type: "text",
required,
max,
});
async function main() {
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
throw new Error(
"Set PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD env vars (e.g. sourced from .env.local).",
);
}
const pb = new PocketBase(PB_URL);
await pb.collection("_superusers").authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
console.log(`[rep-trip-legs] Authenticated against ${PB_URL}`);
const existing = await pb.collections
.getFullList()
.then((all) => all.find((c) => c.name === "rep_trip_legs"));
if (existing) {
console.log("[rep-trip-legs] rep_trip_legs already exists — skipping.");
return;
}
await pb.collections.create({
name: "rep_trip_legs",
type: "base",
listRule: "@request.auth.id != ''",
viewRule: "@request.auth.id != ''",
createRule: null,
updateRule: null,
deleteRule: null,
fields: [
// Whose vehicle this was. Resolved from the CSV's Object column
// (e.g. "MW13TCGP-MW13TCGP - TIAAN") or chosen by the operator.
{
name: "rep",
type: "relation",
required: true,
collectionId: "_pb_users_auth_",
cascadeDelete: false,
minSelect: 1,
maxSelect: 1,
},
// Tracker unit id, e.g. "MW13TCGP". Kept so a vehicle can be mapped to
// a rep later (users.tracker_vehicle_id) without re-importing.
text("vehicle", 64),
{
name: "start_time",
type: "date",
required: true,
},
{
name: "end_time",
type: "date",
required: true,
},
// Interval length in seconds, straight from the file's Duration column.
// Stored so the timeline survives a change to the work-hours window.
{ name: "duration_seconds", type: "number", required: true },
{
name: "status",
type: "select",
required: true,
maxSelect: 1,
values: ["Idle", "Running", "Stopped"],
},
{ name: "lat", type: "number", required: true },
{ name: "lon", type: "number", required: true },
// The tracker's own reverse-geocoded address, used as the label when no
// company with coordinates is close enough.
text("address", 300),
// Per-row distance in km. NOTE: the file's "Distance" column is a
// running odometer, NOT per-row — see the parser for details.
{ name: "distance_km", type: "number" },
// Import batch id so a bad import can be traced and rolled back.
text("import_batch", 64),
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
],
indexes: [
// The hot path: one rep, one date range, ordered by time.
"CREATE INDEX idx_rep_trip_legs_rep_start ON rep_trip_legs (rep, start_time)",
// Lets an import find (and replace) the rows it is about to overwrite.
"CREATE INDEX idx_rep_trip_legs_batch ON rep_trip_legs (import_batch)",
],
});
console.log("[rep-trip-legs] Created rep_trip_legs collection. Migration complete.");
}
main().catch((err) => {
console.error("[rep-trip-legs] Migration failed:", err);
process.exit(1);
});