The project code was locked once set, on the belief that changing it
would break joins. Nothing joins on it: every record links to the site by
id, and the PO email intake reads codes live. A mistyped code (Nellmapius
was 001 instead of C26/27) could only be fixed in the database.
In Edit site an admin double-clicks the code to unlock it. PATCH accepts
project_code from admins only, trims it, requires 1-64 characters, answers
409 when another site already uses it, and writes nothing when unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PO's site was whatever the uploader or email picked, and process-batch
never re-checked it. Every MEGA PO line starts its cost code with the
project code ("C26/09, MC-SEW" is The Hills), which agreed with the filed
site on every live PO whose code is a site in the app.
After OCR, process-batch now reads those codes:
- all lines name the filed site -> confirmed
- all lines name one other site, nothing booked yet -> PO moves there and
waiting deliveries are queued at the new site
- mixed projects, an unknown code, or receipts already booked -> flagged
in raw_ocr_data.site_check, filed site kept
The check is never fatal: if the site list can't be read the PO is still
filed on the uploaded site.
Nellmapius Ext 25's code was corrected from 001 to C26/27 in the data;
the intake runbook is updated to match.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
users.last_active_at (migration applied) is stamped at sign-in and by the
proxy in the background at most every 15 minutes per person; a failed
stamp never blocks a request. The Users list shows "Today" / "5 days ago" /
"3 months ago" / "Never", amber after a quiet week and red after an idle
month, with a count of active accounts idle 30+ days.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Site managers get an Edit action on the clock users they can see; the
dialog shows only Name and Email. PATCH /api/admin/users/[id] is now
reachable by managers (proxy-path), and the route limits them to name
and email on clock users wholly within their sites (others read as 404).
Delete, access, mail and card actions stay admin-only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Email field in Edit user was locked and PATCH ignored it, so a typo
(e.g. "megainfra.coza") could only be fixed by deleting the account.
Admins can now edit it; the route trims, lower-cases and validates it,
only writes it when it changed, and answers 409 when another account
already uses the address.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PASSWORD_MIN_LENGTH (session-policy.ts) now drives every check: the
change-password and set-password screens and routes, admin add/edit,
and the welcome email copy. Previously 10 for self-chosen and 8 for
admin-set. PocketBase's users password field min is already 6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Admins can email a new user a welcome with a one-time link to choose
their password, and send a reset link from the users list. The sign-in
page gets "Forgot password?". Mail goes out through Resend; the emails
show the person's sign-in address.
- password_links collection (migration applied): only the SHA-256 of each
token is stored; invites last 7 days, resets 1 hour, one use each
- /set-password and /forgot-password pages and their /api/auth routes
- forgot-password never reveals whether an account exists, throttles
repeat mails, and builds links only from APP_URL
- env: RESEND_API_KEY, EMAIL_FROM, EMAIL_REPLY_TO, APP_URL (docs/setup/email.md)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the last-slip field with cumulative spending since the latest top-up, falling back to the opening when no top-up exists. Update the management table to display the new amount and cover the calculation with server and UI tests.
Backdating before the latest assignment and future dates were refused, so a
wrong start date could never be corrected. Corrections still append a revision.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Plant overview: monthly Actual vs Bill hours and Total vs Bill value across
all equipment, using the same rules as the equipment detail tiles
(billed-day logic moved to lib/metrics/plant-billing).
- Attention Needed moves under the KPI cards and now holds the missing hire
rate, readings-to-review and fuel price notices.
- Sidebar Plant mark also counts equipment with no hire rate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Clock slip form: no cost code (a slip is booked to its site). Each photo
is previewed, with "Add another page" for the invoice behind a bank slip,
up to 5 pages in order; extra pages can be removed.
- Slip API takes 1-5 photos; the photo route serves ?page=N and is now
holder-or-admin only (the shared-login rule was left over).
- Movements sheet shows every page; CSV drops the cost code column.
- Cash Floats table: a card under its limit gets a red row, and the card
number opens its movements.
- Sidebar: the low-card mark shows on Cash Floats only, not on Management.
- Migration: slip_photo holds up to 5 files (applied to production
2026-10-28).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inflows (claims) now shows only Certified invoice (VAT incl) and Retention
(ex VAT), fed by an Import certificate dialog that reads the PDF (INVOICE
TOTAL, this month's Less RETENTION, valuation number, date) for review
before saving. Adds a monthly + cumulative chart and a certificates table
linking each stored PDF.
- income_claims gains retention_amount and certificate (migration applied)
- pdftotext with an unpdf fallback for machines without poppler
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a /dashboard/plant/fuel-prices page to view, add, edit and delete
dated diesel prices, plus a monthly check that prices each fuel log at
the rate in force on its own date.
- admin-only POST/PATCH/DELETE /api/fuel-prices with zod validation
- monthly cost helper flags months that fall back to the default rate
- plant landing and fuel logs pages link to the new screen and show the
current rate per litre
- /api/fuel-prices added to the plant module path access list
Clock scanner: double-tap the previous reading to remove the latest
scanner-entered reading. Fuel Logs and vehicle pages: double-click a
meter-only row to delete it. Reason, actor and values are logged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Each cash float card lives on its holder's user record (card number, last
4-6 digits of the physical card, low limit, assigned date). Every movement
goes in one cash_float_log collection: opening, top-up, slip, handover and
correction rows with signed amounts. Balances are always summed from the log.
- Management -> Cash Floats: cards table (lowest balance first), add, edit,
top up, hand over, correct balance, retire, movements sheet with slip
photos and CSV export. Admin-only.
- Settings -> Users: card section in the edit form and the balance per holder.
- Clock app: Slips tab, shown only to a user who holds a card. Photo, site,
supplier and amount; the slip comes off that user's own card.
- Sidebar needs-attention mark when a card is under its limit.
- Slips count as site spend ("Cash float slips" on the site page).
- Migration scripts/migrations/2026-10-24-cash-floats.py (already applied to
production on 2026-10-24).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds an all-sites payroll (Excel/PDF/CSV, one row per worker, per-site split and
site wage cost), a period-selectable labour cost by site table on the Workforce
dashboard, and each worker's all-sites total plus site wage cost in the
timesheet matrix and its exports.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A destination scan now ends the worker's previous site session at that moment
(transfer OUT, hours booked at the origin) and opens the new one, so nothing is
left for the office to review. Vehicle scans take a scanning site, move the unit
there and log the reading against it. Adds a script to close already-flagged moves.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Awarded (not started) and non-calculated projects now appear on the
Retention table, labelled, blank until values are entered. Entered values
count towards totals and the company balance, and the retention save
endpoint accepts any project in the plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add planned vs actual turnover dashboards, monthly actual costs, editable retention forecasts and actuals, starting retention, payouts and running balances. Preserve manual values across workbook imports and include retention backfill tooling.
Add Cedar Gate model extraction and reports, measured service routes and ground surfaces, plus site-map section totals and quantity refinements.
Move the Management planning snapshots into PocketBase collections
(plans, projects, per-year project plans, retention, edit log), loaded
with a bundled-JSON fallback. Admins can double-click an income cell to
enter forecast and actual, edit awarded jobs' planned amounts, change
status and costs, and add jobs, with revision checks and an audit trail.
Income chart now shows forecast and invoiced bars with not-yet-started
money stacked in red; Income is in the sidebar. Adds schema and import
scripts, and jsdom/Testing Library dev dependencies for UI tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The employer and broker API routes were never named in the path
classifier, so they fell through to its admin-only default and the
proxy rejected a site manager with a bare {"error":"Not allowed"}
before the route handler ran. The handler's own site scoping was
always correct — it just never got the chance to apply.
- classify /api/employers as dashboard and map it to the workforce
module, mirroring /api/employees
- let the site dropdown be chosen when adding an employer; only a
rename freezes the site, which preserves assignment history
- cover the rename path, which had no route test, and the proxy
gate for the site-manager role
- Tick runs new scans before batches requeued for PO rechecks; old
uploaded_at dates kept rechecks ahead of today's scans for hours.
- Supplier records with the same normalised name and no conflicting
registration/VAT count as one company, so PO9480 (held by the second
Drift Laezonia record) no longer fails as a supplier conflict.
- Inbox shows a queued scan as waiting rather than being read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Improve workforce scanning, site transfers and timesheet exports.
Audit plant imports, duplicate identities, rates and dashboard totals.
Repair delivery PO selection, periodic rechecks and receiving guards.
Align financial ledgers, supplier periods and mobile layouts.
Add the site map preview, shared theme controls and audit evidence.
Verify 3,455 application tests, six fuel export tests, TypeScript and
an optimized production build. Clean up a leaked test timer and keep
local worktrees and generated reports out of ESLint traversal.
Changed-file lint passes; repository-wide lint retains 33 existing
errors in unchanged files.
- write PocketBase batches in chunks of at most 50 (production limit)
- survey import: refuse files with no rows, write peg events before the import record
- read en-ZA Excel CSVs (decimal comma), quoted fields, CR line endings; no crash without pegs
- save queue: reset on model reload, drop 400-refused items instead of blocking,
keep queued values on screen when a server state arrives, flush on leaving the page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text PDFs already get it from pdftotext; photos and scans have no text,
so 4 of 5 undated POs were photos that do print a PO Creation Date.
- n8n PO reader (stored patch): asked for order_date = 'PO Creation
Date' (day/month/year), never Required By or Printed; the callback
passes it on.
- process-batch accepts extracted.order_date (YYYY-MM-DD); the PDF text
wins when both exist.
- create_purchase_order / update_purchase_order / move_to_purchase_orders
take order_date, so agents record it too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reader always read the slip date (and receiver, site name, customer
VAT, supplier registration), but dn_batch_notes never had those fields,
so they were dropped on save and every receipt was dated the moment it
was posted. The Financial dashboard dated orders by upload date, so 24
of 41 POs (R7.08M) sat in no month and deliveries piled up in the month
they were booked: 'R1.1M ordered, R2.4M delivered'.
- scripts/pb-add-dn-dates.py: adds the five header fields,
delivery_notes.posted_at and purchase_orders.order_date (applied).
- scripts/dn-backfill-dates.py: filled 270 documents from the stored
readings, moved 192 receipts to their slip date (booking time kept in
posted_at) and read 36 PO order dates from the PDFs (applied; replaced
values in docs/audits/2026-10-11-dates-backfill-backup.json).
- Posting refuses a slip without a delivery date instead of using today;
receipts record posted_at. The review page shows the date and locks
Post with a plain message when it is missing.
- PO intake reads 'PO Creation Date' from the PDF text (pdftotext).
- Dashboard tile and monthly chart date orders by order_date only; an
order without one is undated, never put on its upload month.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Set-aside delivery documents were kept but shown nowhere, so they looked
lost (e.g. The Waste Group slip T025511, set aside 'Waiting for
invoice'). The inbox now has a Set aside button: every set-aside
document with why (in plain words) and when, a link to its page, and a
Restore button that puts it back in the inbox through the same restore
the agents use, recorded with who and why.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scan in up to 07:10 pays from 07:00; a scan out between 16:20 and
16:40 pays to 16:30; a session over five hours loses a 30-minute lunch.
A normal day is therefore exactly 9 hours (green on the calendar and
matrix); late arrivals, early exits and overtime use the real times.
Applied on clock-out, in missed-scan corrections and their preview.
The calendar keeps the raw scan times and labels hours "Paid hours".
Includes a dry-run-first script to recalculate existing QR entries; it
only rewrites rows still equal to their raw scan total.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agents can now save a whole receiving plan (lines by number, kits, NIL
auto-close) and leave the slip in Ready to post; the site manager checks
the totals and presses Confirm delivery, which posts exactly what was
saved. Quantities over the PO need acceptExcess (site-approved) and a
slip with an open site question is refused.
Confirm delivery on the web now refreshes a slip whose match went stale
because another slip on the same PO was booked, re-applying the saved
decisions instead of sending the user back to an AI re-match.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When nobody scanned a worker in, the evening scan was stored as the IN
and the correction dialog only allowed a leaving time after it. The
dialog now shows an editable arrival and leaving time, with a one-click
"this scan was actually the clock-out" swap. Late scans (16:00+) get a
hint on the card. A corrected arrival is stored as the optional
actual_in_at on the correction row (migration script included, not
applied); the original QR scan is untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both are page reads; two in one tick can pass the 300s ceiling and get a
batch reclaimed and read twice. The job waits for the next tick instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>