- 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.
130 lines
4.5 KiB
TypeScript
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);
|
|
});
|