Remove repeated Plant, Financial and Workforce shortcut cards. Add site-scoped Plant and Financial sidebar attention status with automatic refresh. Include accumulated delivery processing, purchase order recovery, supplier reconciliation, plant data and user management changes and audit records.
677 lines
22 KiB
TypeScript
677 lines
22 KiB
TypeScript
/**
|
|
* Typed shapes matching the MEGA INFRASTRUCTURE collections on
|
|
* `https://pb.mega.infra.apphub.co.za`. Keep in sync with the live
|
|
* schema — re-run `scripts/pb-bootstrap-schema.py` after changes.
|
|
*
|
|
* `fuel_prices` is additive and lives in its own script
|
|
* (`scripts/pb-add-fuel-prices.py`); the app degrades to the
|
|
* RATE_PER_LITRE fallback when it is absent.
|
|
*/
|
|
import type { RecordModel } from "./pb";
|
|
import type { BatchStage, CandidateStage } from "./dn-pipeline/stages";
|
|
|
|
export interface SiteRecord extends RecordModel {
|
|
collectionName: "sites";
|
|
project_code: string;
|
|
name: string;
|
|
duration: string;
|
|
contract_value: number;
|
|
work_hours: number;
|
|
hours_per_week: number;
|
|
wages_hours_per_day: number;
|
|
/** ISO date (YYYY-MM-DD) the project started/starts. Optional. */
|
|
job_start_date?: string;
|
|
/**
|
|
* Decimal degrees. Drives the rain outlook on the Overview page and
|
|
* nothing else — a site without them is listed as "no location set"
|
|
* rather than being hidden. Added by
|
|
* `scripts/pb-add-site-coordinates.py`.
|
|
*/
|
|
latitude?: number | null;
|
|
longitude?: number | null;
|
|
expand?: Record<string, never>;
|
|
}
|
|
|
|
export interface SupplierReconciliationRule {
|
|
rule_key: string;
|
|
version: number;
|
|
status: "draft" | "active" | "retired";
|
|
financially_active: boolean;
|
|
title: string;
|
|
supplier_ids: string[];
|
|
conditions: Record<string, unknown>;
|
|
instructions: string[];
|
|
evidence: Array<Record<string, unknown>>;
|
|
examples: Array<Record<string, unknown>>;
|
|
approved_by: string | null;
|
|
approved_at: string | null;
|
|
}
|
|
|
|
export interface SupplierReconciliationKnowledge {
|
|
schema_version: 1;
|
|
prepared_on: string;
|
|
status: "draft" | "active" | "retired";
|
|
financially_active: boolean;
|
|
rules: SupplierReconciliationRule[];
|
|
}
|
|
|
|
export interface SupplierRecord extends RecordModel {
|
|
collectionName: "suppliers";
|
|
/** Stored pre-normalized (trim, collapse whitespace, uppercase) — see
|
|
* normalizeSupplierName in purchase-order-schema.ts. */
|
|
name: string;
|
|
/** Optional identifiers printed on supplier documents. */
|
|
registration_number?: string;
|
|
vat_number?: string;
|
|
/** Versioned supplier guidance; drafts are not executable posting policy. */
|
|
reconciliation_rules?: SupplierReconciliationKnowledge | null;
|
|
}
|
|
|
|
/**
|
|
* One diesel price change. `effective_from` is inclusive — a fuel log is
|
|
* priced at the latest row on or before its own date. Created by
|
|
* `scripts/pb-add-fuel-prices.py`; read via `loadFuelPrices()`.
|
|
*/
|
|
export interface FuelPriceRecord extends RecordModel {
|
|
collectionName: "fuel_prices";
|
|
/** ISO date (YYYY-MM-DD) the price took effect. */
|
|
effective_from: string;
|
|
price_per_litre: number;
|
|
notes?: string;
|
|
expand?: Record<string, never>;
|
|
}
|
|
|
|
export type WorkerType = "permanent" | "casual";
|
|
|
|
export interface EmployeeRecord extends RecordModel {
|
|
collectionName: "employees";
|
|
site: string; // relation id
|
|
name: string;
|
|
employee_no: string;
|
|
position: string;
|
|
rate_per_hour: number;
|
|
hours_per_day_standard: number;
|
|
worker_type: WorkerType;
|
|
expand?: { site?: SiteRecord };
|
|
}
|
|
|
|
export type UnitType = "fuel" | "plant";
|
|
|
|
export interface VehicleRecord extends RecordModel {
|
|
collectionName: "vehicles";
|
|
site: string;
|
|
description: string;
|
|
standard_work_hours: string | number;
|
|
fuel_limits: string;
|
|
qr_code_id: string;
|
|
cost_per_hour: number;
|
|
unit_type: UnitType;
|
|
/**
|
|
* ISO timestamp of the user's most recent acknowledgement of this
|
|
* vehicle's "Attention required" status. null/absent when never
|
|
* dismissed. Written by PATCH /api/vehicles/[id]/attention-dismiss.
|
|
*/
|
|
attention_dismissed_at?: string | null;
|
|
/**
|
|
* Latest GPS fix from a MEGA Clock Fleet scan. Denormalised off
|
|
* `fuel_logs` so the map page can render every vehicle in a single
|
|
* query, without an N+1 walk back through the scan history. Stays
|
|
* null for vehicles that have never been scanned from a kiosk phone
|
|
* (e.g. ones only ever imported from a pump sheet). Updated by
|
|
* /api/clock/vehicle-scan after every successful fuel_logs write;
|
|
* if the PATCH fails the next scan catches up, so a missed write
|
|
* is self-healing rather than a hard inconsistency.
|
|
*
|
|
* `last_scanned_at` is the `date_logged` of the most recent scan
|
|
* (UTC midnight anchor, same convention as every other date in the
|
|
* schema), not the wall-clock time the scan happened.
|
|
*/
|
|
last_latitude?: number | null;
|
|
last_longitude?: number | null;
|
|
last_scanned_at?: string | null;
|
|
last_location_accuracy_m?: number | null;
|
|
expand?: { site?: SiteRecord };
|
|
}
|
|
|
|
export interface PlantRecord extends VehicleRecord {
|
|
// Deprecated alias — Plant has been merged into Vehicles.
|
|
}
|
|
|
|
export interface FixedCostRecord extends RecordModel {
|
|
collectionName: "fixed_costs";
|
|
site: string;
|
|
date_logged: string;
|
|
description: string;
|
|
cost: number;
|
|
notes: string;
|
|
is_recurring?: boolean;
|
|
expand?: { site?: SiteRecord };
|
|
}
|
|
|
|
export type IncomeClaimStatus = "draft" | "certified" | "paid";
|
|
|
|
export interface IncomeClaimRecord extends RecordModel {
|
|
collectionName: "income_claims";
|
|
claim_number: string;
|
|
site: string;
|
|
period_start: string;
|
|
period_end: string;
|
|
gross_amount: number;
|
|
certified_by: string;
|
|
/** ISO date the claim was actually paid. Empty until payment. */
|
|
paid_on: string;
|
|
status: IncomeClaimStatus;
|
|
is_verified: boolean;
|
|
notes: string;
|
|
expand?: { site?: SiteRecord };
|
|
}
|
|
|
|
/**
|
|
* Shared by every document/queue collection's `processing_status`.
|
|
* `data_import_batches` (the collection this state machine originally
|
|
* lived on) was dropped once purchase_orders, delivery_notes, timesheets,
|
|
* fuel_sheets and plant_hours_sheets each grew their own copy of it.
|
|
*/
|
|
export type BatchStatus =
|
|
| "pending"
|
|
| "processing"
|
|
| "processed"
|
|
| "failed"
|
|
| "dead";
|
|
|
|
export interface PurchaseOrderRecord extends RecordModel {
|
|
collectionName: "purchase_orders";
|
|
/**
|
|
* Unique (idx_po_number). Born as a `PENDING-...` placeholder at
|
|
* upload time (src/lib/po-placeholder.ts) — a row cannot hold "" here
|
|
* because the index is UNIQUE, and two unprocessed uploads would
|
|
* collide. Overwritten with the real number once n8n's extraction
|
|
* completes.
|
|
*/
|
|
purchase_order_number: string;
|
|
site: string;
|
|
supplier: string;
|
|
/**
|
|
* The authoritative value. `supplier` above is kept as raw OCR
|
|
* provenance — this is what everything else reads. Optional: not
|
|
* every PO has a match yet.
|
|
*/
|
|
supplier_ref?: string;
|
|
required_by_date: string;
|
|
ordered_by: string;
|
|
items_count: number;
|
|
total_value: number;
|
|
purchase_order_file: string;
|
|
raw_ocr_data: unknown;
|
|
is_verified: boolean;
|
|
/**
|
|
* SHA-256 of purchase_order_file, stamped at upload. Powers dedup
|
|
* against re-uploads of the same bytes, windowed by date_uploaded.
|
|
*/
|
|
file_hash?: string;
|
|
/** Stamped at upload — this is the row's own audit trail, not a copy. */
|
|
date_uploaded?: string;
|
|
/**
|
|
* The row IS the queue item now (see docs/n8n/purchase-order-ingest-implementation.md):
|
|
* n8n claims pending rows, extracts via AI, and PATCHes the result in.
|
|
* Same state machine data_import_batches used to carry.
|
|
*/
|
|
processing_status?: BatchStatus;
|
|
attempts?: number;
|
|
last_error?: string;
|
|
next_retry_at?: string;
|
|
last_attempt_at?: string;
|
|
expand?: { site?: SiteRecord; supplier_ref?: SupplierRecord };
|
|
}
|
|
|
|
export interface PurchaseOrderItemRecord extends RecordModel {
|
|
collectionName: "purchase_order_items";
|
|
purchase_order: string;
|
|
material_code: string;
|
|
cost_code: string;
|
|
description: string;
|
|
qty: number;
|
|
unit: string;
|
|
rate: number;
|
|
disc_percent: number;
|
|
vat_percent: number;
|
|
amount: number;
|
|
expand?: { purchase_order?: PurchaseOrderRecord };
|
|
}
|
|
|
|
/**
|
|
* One logged shift: this employee worked these hours on this date. The
|
|
* collection was called `timesheets` until the 2026-09-12 rename; that
|
|
* name now belongs to the uploaded timesheet document.
|
|
*/
|
|
export interface TimesheetEntryRecord extends RecordModel {
|
|
collectionName: "timesheet_entries";
|
|
employee: string;
|
|
/** Stamped from the employee's site at import time. Never joined at read time. */
|
|
site: string;
|
|
date_logged: string;
|
|
hours_worked: number;
|
|
/** Optional. Empty when captured directly (QR scan) rather than from a sheet. */
|
|
timesheet?: string;
|
|
expand?: {
|
|
employee?: EmployeeRecord;
|
|
site?: SiteRecord;
|
|
timesheet?: TimesheetRecord;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* One scan event — the raw audit log behind the MEGA Clock PWA.
|
|
* Independent of the derived daily totals in `timesheet_entries`: every
|
|
* "in" and every "out" gets its own row here, in scan order.
|
|
*/
|
|
export interface ClockEventRecord extends RecordModel {
|
|
collectionName: "clock_events";
|
|
employee: string;
|
|
site: string;
|
|
direction: "in" | "out";
|
|
scanned_at: string;
|
|
/** Set only on "out" events — the timesheet_entries row this scan created/updated. */
|
|
timesheet_entry?: string;
|
|
expand?: {
|
|
employee?: EmployeeRecord;
|
|
site?: SiteRecord;
|
|
timesheet_entry?: TimesheetEntryRecord;
|
|
};
|
|
}
|
|
|
|
/** Manual resolution of an unmatched QR IN. The original event is retained. */
|
|
export interface ClockMissedOutCorrectionRecord extends RecordModel {
|
|
collectionName: "clock_missed_out_corrections";
|
|
clock_in_event: string;
|
|
employee: string;
|
|
site: string;
|
|
actual_out_at: string;
|
|
reason: string;
|
|
recorded_at: string;
|
|
timesheet_entry: string;
|
|
}
|
|
|
|
export interface FuelLogRecord extends RecordModel {
|
|
collectionName: "fuel_logs";
|
|
site: string;
|
|
vehicle: string;
|
|
date_logged: string;
|
|
litres: number;
|
|
odo_reading: number;
|
|
/**
|
|
* "Latest Consumption" value as reported by the pump sheet (typically
|
|
* litres/100km or litres/hour). Persisted verbatim from the source
|
|
* Excel/CSV rather than recomputed, so the audit trail reflects what
|
|
* the fuel team actually filed. Optional — the Aug 2026 sample has
|
|
* ~9% empty cells, which we preserve as `undefined` rather than
|
|
* fabricating a value. Added in the 2026-08-24 migration.
|
|
*/
|
|
latest_consumption?: number;
|
|
/**
|
|
* GPS fix captured at scan time on the kiosk phone (MEGA Clock Fleet
|
|
* tab). The spreadsheet import path doesn't have a GPS context, so
|
|
* desktop-imported rows will have all three fields null — exactly
|
|
* the same posture as `latest_consumption`. Decimal degrees; range
|
|
* validation is the route's responsibility, not PB's. Added in the
|
|
* 2026-09-22 migration.
|
|
*/
|
|
latitude?: number | null;
|
|
longitude?: number | null;
|
|
/** Reported `Position.coords.accuracy` in metres at scan time. */
|
|
location_accuracy_m?: number | null;
|
|
/** The uploaded pump sheet this row came from. Optional: pre-migration rows have none. */
|
|
fuel_sheet?: string;
|
|
expand?: {
|
|
site?: SiteRecord;
|
|
vehicle?: VehicleRecord;
|
|
fuel_sheet?: FuelSheetRecord;
|
|
};
|
|
}
|
|
|
|
export interface PlantHoursLogRecord extends RecordModel {
|
|
collectionName: "plant_hours_log";
|
|
date_logged: string;
|
|
vehicle_qr: string;
|
|
vehicle: string;
|
|
site: string;
|
|
start_date: string;
|
|
end_date: string;
|
|
start_time?: string;
|
|
end_time?: string;
|
|
start_hours: number;
|
|
end_hours: number;
|
|
hours: number;
|
|
notes?: string;
|
|
hour_meter_budget: number;
|
|
cost_per_hour: number;
|
|
value: number;
|
|
over_budget: boolean;
|
|
hours_mismatch: boolean;
|
|
vehicle_unit_type_at_import: UnitType;
|
|
/** The uploaded plant hours sheet this row came from. Optional: pre-migration rows have none. */
|
|
plant_hours_sheet?: string;
|
|
expand?: {
|
|
vehicle?: VehicleRecord;
|
|
site?: SiteRecord;
|
|
plant_hours_sheet?: PlantHoursSheetRecord;
|
|
};
|
|
}
|
|
|
|
export interface DeliveryNoteRecord extends RecordModel {
|
|
collectionName: "delivery_notes";
|
|
delivery_note_number: string;
|
|
site: string;
|
|
purchase_order: string;
|
|
date_received: string;
|
|
received_by: string;
|
|
slip_image: string;
|
|
raw_ocr_data: unknown;
|
|
is_verified: boolean;
|
|
processing_status?: DeliveryNoteProcessingStatus;
|
|
ocr_confidence?: number;
|
|
ocr_completed_at?: string;
|
|
received_by_name?: string;
|
|
document_type?: "delivery_note" | "invoice" | "collection_note" | "unknown";
|
|
batch_note?: string;
|
|
expand?: {
|
|
site?: SiteRecord;
|
|
purchase_order?: PurchaseOrderRecord;
|
|
batch_note?: DnBatchNoteRecord;
|
|
};
|
|
}
|
|
|
|
export type DeliveryNoteProcessingStatus =
|
|
| "pending"
|
|
| "ocr-complete"
|
|
| "auto-verified"
|
|
| "needs-review"
|
|
| "manual-verified"
|
|
| "failed";
|
|
/**
|
|
* One line on a delivery note. The collection was called
|
|
* `received_items` until the 2026-09-12 rename — the old name described
|
|
* the receiving event rather than the thing received.
|
|
*/
|
|
export interface DeliveryNoteItemRecord extends RecordModel {
|
|
collectionName: "delivery_note_items";
|
|
allocation_key?: string;
|
|
source_line_keys?: string[];
|
|
booking?: import("./dn-pipeline/line-booking").LineBooking;
|
|
delivery_note: string;
|
|
/** Optional — empty when OCR could not match the line to a PO item. */
|
|
purchase_order_item: string;
|
|
/** In the matched PO line's unit. Zero when the line is unmatched or unconvertible. */
|
|
qty_received: number;
|
|
/** Verbatim quantity from the slip, before any conversion. */
|
|
qty_raw: number;
|
|
/** Verbatim unit from the slip, e.g. "kg". Empty when the slip showed none. */
|
|
unit_raw: string;
|
|
/** True when qty_received differs from qty_raw because a conversion was applied. */
|
|
unit_converted: boolean;
|
|
condition_notes: string;
|
|
expand?: {
|
|
delivery_note?: DeliveryNoteRecord;
|
|
purchase_order_item?: PurchaseOrderItemRecord;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The uploaded delivery-note document. One row per file (PDF or
|
|
* image) before any page-level work has happened — the inbox item, not
|
|
* the ledger row. A `DN_BATCH_NOTES` child row exists for every
|
|
* candidate note the splitter found inside it; only after those pass
|
|
* every check does a `DELIVERY_NOTES` row get promoted.
|
|
*
|
|
* Added by `scripts/pb-add-dn-batches.py`.
|
|
*/
|
|
export type DnBatchStatus =
|
|
| "inbox"
|
|
| "analysing"
|
|
| "needs_attention"
|
|
| "processed";
|
|
|
|
export interface DnBatchRecord extends RecordModel {
|
|
collectionName: "dn_batches";
|
|
/** The uploaded PDF/image. Empty when only metadata was recorded. */
|
|
file: string;
|
|
/** SHA-256 of `file`. Powers dedup against re-uploads of the same bytes. */
|
|
file_hash: string;
|
|
/** Site the delivery belongs to. Required so n8n never has to guess. */
|
|
site: string;
|
|
/** Upload channel. Empty when the script that wrote the row didn't set it. */
|
|
source: "web" | "mobile" | "email" | "";
|
|
/** Free-form operator id. Empty when uploaded by an unauthenticated flow. */
|
|
uploaded_by: string;
|
|
/** Pipeline position. "inbox" until n8n claims it; "processed" once every child note is promoted or blocked. */
|
|
status: DnBatchStatus;
|
|
/** Total pages in the source file. Set during ingestion; unchanged after. */
|
|
page_count: number;
|
|
/** How many candidate notes the page splitter pulled out. */
|
|
notes_detected: number;
|
|
/** Of those, how many became real DELIVERY_NOTES rows. */
|
|
notes_promoted: number;
|
|
/** Raw segmentation output (per-page notes, bounding boxes, OCR text). Free-form. */
|
|
segmentation: unknown;
|
|
/** Aggregated check results across the whole batch. Free-form. */
|
|
checks: unknown;
|
|
/** Separate, revisioned AI proposal; never approved ledger or extraction data. */
|
|
reconciliation_preview?: import("./dn-pipeline/ai-preview").ProcessingPreview | null;
|
|
/** n8n retry counter. */
|
|
attempts: number;
|
|
/** ISO timestamp of the most recent n8n claim. Empty until first claim. */
|
|
claimed_at: string;
|
|
/** Last error message from n8n. Empty when the batch is healthy. */
|
|
last_error: string;
|
|
/** Set explicitly at upload — PocketBase's `created` is unusable here. */
|
|
uploaded_at: string;
|
|
open_pages?: unknown;
|
|
stage?: BatchStage;
|
|
source_count?: number;
|
|
processing_completed_at?: string;
|
|
capture_session_id?: string;
|
|
/** Batch-wide assembly revision; increments for every manual reassembly. */
|
|
assembly_revision?: number;
|
|
expand?: { site?: SiteRecord };
|
|
}
|
|
|
|
/**
|
|
* One candidate note found inside a `DN_BATCHES` row. Stays a candidate
|
|
* (status="candidate") until every check passes — only then is a real
|
|
* DELIVERY_NOTES row created and `delivery_note` is set. "blocked" notes
|
|
* are the ones that failed a check; "promoted" is the terminal state.
|
|
*
|
|
* Added by `scripts/pb-add-dn-batches.py`.
|
|
*/
|
|
export type DnBatchNoteStatus = "candidate" | "blocked" | "promoted";
|
|
|
|
export interface DnBatchNoteRecord extends RecordModel {
|
|
collectionName: "dn_batch_notes";
|
|
/** Owning DN_BATCHES row. Required — cascade-deletes with the batch. */
|
|
batch: string;
|
|
/** Position of this note within the batch (0-based). Stable across retries. */
|
|
note_index: number;
|
|
/** First page of the source PDF/image this note came from. */
|
|
page_from: number;
|
|
/** Last page of the source PDF/image this note came from. Inclusive. */
|
|
page_to: number;
|
|
/** Cropped page image (PDF single-page or image) for re-OCR or operator review. */
|
|
page_file: string;
|
|
/** As printed on the slip. Empty when OCR could not read it. */
|
|
delivery_note_number: string;
|
|
/** PO number as OCR saw it (any case, any spacing). */
|
|
po_number_raw: string;
|
|
/** PO number after normalisation (trim, collapse, uppercase). Used for the match lookup. */
|
|
po_number_normalised: string;
|
|
/** Resolved purchase_orders row. Empty until the normalised PO matches a real PO. */
|
|
purchase_order: string;
|
|
/** Gross mass (kg) from the slip, when present. */
|
|
gross_mass: number;
|
|
/** Tare mass (kg) from the slip, when present. */
|
|
tare_mass: number;
|
|
/** Nett mass (kg) from the slip, when present. */
|
|
nett_mass: number;
|
|
/** Quantity from the slip in `unit`. Zero when the slip showed none. */
|
|
quantity: number;
|
|
/** Verbatim unit from the slip, e.g. "kg" or "m³". Empty when absent. */
|
|
unit: string;
|
|
/** Parsed line items for this note. Free-form until the items extractor is finalised. */
|
|
items: unknown;
|
|
/** Per-check results (PO match, totals, duplicates, …). Free-form. */
|
|
checks: unknown;
|
|
/** Promotion state. See DnBatchNoteStatus. */
|
|
status: DnBatchNoteStatus;
|
|
/** Back-reference to the DELIVERY_NOTES row this note was promoted into. Empty while a candidate or blocked. */
|
|
delivery_note: string;
|
|
stage?: CandidateStage;
|
|
/** Stable staged identity derived from first persisted page plus document identity. */
|
|
candidate_key?: string;
|
|
assembly_revision?: number;
|
|
pages?: unknown;
|
|
supplier?: string;
|
|
supplier_name_raw?: string;
|
|
match_method?: string;
|
|
match_evidence?: unknown;
|
|
posted_at?: string;
|
|
expand?: { batch?: DnBatchRecord; supplier?: SupplierRecord; delivery_note?: DeliveryNoteRecord };
|
|
}
|
|
|
|
export interface DnBatchSourceRecord extends RecordModel {
|
|
collectionName: "dn_batch_sources";
|
|
batch: string;
|
|
file: string;
|
|
file_hash: string;
|
|
ordinal: number;
|
|
mime: string;
|
|
original_name: string;
|
|
capture_session_id?: string;
|
|
delivery_session_id?: string;
|
|
captured_at?: string;
|
|
latitude?: number;
|
|
longitude?: number;
|
|
location_accuracy_m?: number;
|
|
expand?: { batch?: DnBatchRecord };
|
|
}
|
|
|
|
export interface DnBatchPageRecord extends RecordModel {
|
|
collectionName: "dn_batch_pages";
|
|
batch: string;
|
|
source: string;
|
|
source_page: number;
|
|
/** Stable batch-wide page number; gaps are intentional when a page fails. */
|
|
global_page?: number;
|
|
region: number;
|
|
image: string;
|
|
image_hash: string;
|
|
mime: string;
|
|
width?: number;
|
|
height?: number;
|
|
rotation?: number;
|
|
read_status?: string;
|
|
extraction?: unknown;
|
|
schema_version?: string;
|
|
model?: string;
|
|
prompt_version?: string;
|
|
read_error?: string;
|
|
manual_group?: string;
|
|
expand?: { batch?: DnBatchRecord; source?: DnBatchSourceRecord };
|
|
}
|
|
|
|
export interface SupplierDocumentAliasRecord extends RecordModel {
|
|
collectionName: "supplier_document_aliases";
|
|
supplier: string;
|
|
raw_value: string;
|
|
normalised_key: string;
|
|
source?: string;
|
|
approved_by?: string;
|
|
expand?: { supplier?: SupplierRecord };
|
|
}
|
|
|
|
export interface DnReviewEventRecord extends RecordModel {
|
|
collectionName: "dn_review_events";
|
|
batch: string;
|
|
note?: string;
|
|
event_key?: string;
|
|
action?: string;
|
|
before?: unknown;
|
|
after?: unknown;
|
|
reason?: string;
|
|
actor?: string;
|
|
expand?: { batch?: DnBatchRecord; note?: DnBatchNoteRecord };
|
|
}
|
|
|
|
export interface DnPipelineLockRecord extends RecordModel {
|
|
collectionName: "dn_pipeline_locks";
|
|
batch: string;
|
|
expires_at: string;
|
|
worker: string;
|
|
expand?: { batch?: DnBatchRecord };
|
|
}
|
|
|
|
export type DocumentStatus =
|
|
| "pending"
|
|
| "processing"
|
|
| "processed"
|
|
| "failed"
|
|
| "dead";
|
|
|
|
/** One row per failed input row, persisted so the operator can see why. */
|
|
export interface RowError {
|
|
row: number;
|
|
reason: string;
|
|
}
|
|
|
|
/**
|
|
* The shared contract every uploaded document carries (spec section 4.1).
|
|
* One list component and one status helper serve all of them.
|
|
*/
|
|
export interface UploadedDocument extends RecordModel {
|
|
site: string;
|
|
source_file: string;
|
|
content_type?: string;
|
|
file_hash?: string;
|
|
date_uploaded: string;
|
|
uploaded_by: string;
|
|
processing_status: DocumentStatus;
|
|
processed_at?: string;
|
|
attempts?: number;
|
|
last_error?: string;
|
|
rows_total?: number;
|
|
rows_created?: number;
|
|
rows_failed?: number;
|
|
row_errors?: RowError[];
|
|
expand?: { site?: SiteRecord };
|
|
}
|
|
|
|
/** The uploaded wage/hours spreadsheet. Its rows are TIMESHEET_ENTRIES. */
|
|
export interface TimesheetRecord extends UploadedDocument {
|
|
collectionName: "timesheets";
|
|
}
|
|
|
|
/**
|
|
* The uploaded pump sheet. Its rows are FUEL_LOGS.
|
|
*
|
|
* `site` is optional on this collection in PocketBase and may come back
|
|
* as "" — a pump sheet legitimately spans sites and the row-level site
|
|
* stays authoritative. Same for PlantHoursSheetRecord.
|
|
*/
|
|
export interface FuelSheetRecord extends UploadedDocument {
|
|
collectionName: "fuel_sheets";
|
|
}
|
|
|
|
/** The uploaded plant hours sheet. Its rows are PLANT_HOURS_LOG. */
|
|
export interface PlantHoursSheetRecord extends UploadedDocument {
|
|
collectionName: "plant_hours_sheets";
|
|
}
|
|
|
|
export interface DnItemAliasRecord extends RecordModel {
|
|
collectionName: "dn_item_aliases";
|
|
supplier_key: string;
|
|
slip_description_raw: string;
|
|
slip_description_key: string;
|
|
material_code: string;
|
|
po_description: string;
|
|
created_by: string;
|
|
times_used: number;
|
|
last_used: string;
|
|
}
|