Commit graph

28 commits

Author SHA1 Message Date
DEls
f72397259c feat(auth): add Office access and restrict Clock users 2026-10-07 14:22:57 +02:00
DEls
6824d2c62e Add Workforce attention alerts and 24-hour correction times 2026-10-04 23:38:43 +02:00
DEls
ed69b517e8 Integrate agent work and add dashboard attention indicators
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.
2026-10-04 21:53:32 +02:00
DEls
4ad262b508 Let site managers create clock users for assigned sites 2026-09-30 00:11:24 +02:00
DEls
f5259d852f feat(auth): add PocketBase logins and site access controls 2026-09-28 23:51:47 +02:00
DEls
1899ed8e2c fix(dn): unstick long OCR runs, preserve OCR data through manual saves
Tick path
- Bump STALE_CLAIM_MS from 10 to 30 minutes. A 29-source OCR batch
  hit ~29 min and was being reclaimed mid-run by the 10-minute cap,
  leaving the batch in inbox with last_error='reclaimed after a
  stalled run' even though nothing was stalled. MAX_ATTEMPTS still
  bounds a truly stuck batch.
- After each dn_batch_pages create, the staged runner bumps
  dn_batches.page_count in the same lease-fenced batch. Final
  assembly write at run-staged.ts:223 still sets the authoritative
  value, so any drift from a reclaim-then-restart cycle is corrected.
  Inbox tiles now show live progress instead of 0 pages for the
  entire reading window.

PocketBase batch transport
- sendRecordBatch chunks across pb.createBatch() calls at the
  server 50-request cap. A 71-candidate assembly sent as one
  request was returning Invalid batch request data. and 500ing —
  the assembly route caught it and surfaced as the generic Grouping
  could not be saved. 73 mutations now split into 50 + 23 and land.

Assembly save UX
- The dismiss dialog now renders directly under the open-pages alert
  instead of at the bottom of the section (which was hidden behind 71
  document tiles on big batches). Clicking Dismiss page opens the
  reason input where the user is looking.
- saveGrouping surfaces the server-side error code
  (stale_revision / posted_candidate / dismissed_candidate /
  assembly_in_progress / invalid_page_coverage) instead of a generic
  Try again. On success, the page now redirects to /inbox so the
  documents land in their columns.
- The Technical details disclosure is gone from the grouping screen.
  Open pages already surface their reason in the alert above; the
  duplicate disclosure just competed with the decision UI.

Assembly route correctness
- After a successful manual save the route now writes
  stage=complete, status=needs_attention, notes_detected,
  processing_completed_at and clears claimed_at/last_error. The
  runner advances stage on its own runs but a manual save was
  never followed by a runner pass — without this the batch stayed
  in Check page grouping forever even though assembly was done.
- For existing candidates the route only updates grouping metadata
  (pages, page_from, page_to, assembly_revision, status, stage,
  checks). OCR-derived fields (doc_number, supplier_name_raw,
  items, raw_text) are preserved on the row — the runner only
  re-fills those for batches in status=inbox, and post-save the
  batch moves to needs_attention and never reaches the runner
  again. New candidates still get a createdCandidateFields row
  with the OCR fields blank, ready for the runner or operator
  entry.
- A Re-run batch button was added to the document detail header
  so the operator can refill the wiped OCR data (or re-resolve
  checks after a manual override) without first navigating to the
  batch detail page.

Customer identity check
- A Skip this check toggle on the document detail page records
  manual_overrides.skip_customer_identity. run-staged reads the
  override and turns the customer_identity check into passed=null
  (skip) instead of passed=false (fail), so OCR false-positives
  on the customer VAT no longer block posting while still
  requiring an explicit operator decision.
- The PATCH route accepts skip_customer_identity as an editable
  field so the toggle saves through the standard note route.

Tests
- 180 files / 1767 tests pass; typecheck clean.
2026-09-27 13:40:27 +02:00
DEls
0f022d5ffa fix(ask): keep the Asking-as bar pinned under the navbar
The Ask page sized itself as 100svh-4rem, but the Topbar is 89px on
desktop and 167px on phones, so the page overflowed and the bar scrolled
under the header. The Topbar now publishes its live height as
--topbar-h and the Ask page fills exactly the space below it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:56:40 +02:00
DEls
3feddebcaa fix(mobile): audit and fix layout overflow across dashboard pages at 375/768px
Audited /dashboard, /dashboard/sites (+ detail), /dashboard/workforce/workers,
/dashboard/workforce/on-site, /dashboard/plant/vehicles (+ detail),
/dashboard/plant/equipment (+ detail), /dashboard/plant/fuel-logs,
/dashboard/financial/purchase-orders (+ detail),
/dashboard/financial/delivery-notes/inbox, /dashboard/financial/suppliers,
and /dashboard/chat at 375x812 and 768x1024 using a scroll-right test to
catch real horizontal page overflow (as opposed to a table's own,
intentional internal scroll).

Two repeating bug patterns accounted for every overflow found:

1. A page-specific `right={<div className="flex items-center gap-2">...}`
   passed to Topbar can carry two or three labelled buttons (edit/import/
   merge/back etc.). Without `flex-wrap` they don't fit a 375px screen in
   one row and push the whole page sideways. Fixed on the vehicles,
   equipment, and site detail pages, plus their list-page counterparts
   (vehicles fuel-view, equipment plant-view) — all five instances of
   this exact pattern. Same fix applied to a delivery-note card's button
   cluster on the PO detail page, and to Topbar's own right-side
   container as a general backstop.

2. `Intl.NumberFormat("en-ZA", ...)` joins currency/number groups with
   non-breaking spaces, so a wide value like "R 24 988 663" can't wrap.
   In a 3-up grid (site cards' Contract/Spent/Remaining) that overflowed
   the column; fixed by stacking those to 1-up below `sm`. In a 2-up grid
   (PO detail KPI tiles) added `min-w-0` so the tile clips instead of
   growing the grid track past the viewport.

Also: the shared Dialog now caps at `max-h-[90dvh]` with internal
scroll (a dialog taller than the phone viewport had no way to reach
its close button or footer before), and the sidebar hamburger button
was overlapping the Topbar's title on every page below `lg` — reserved
space for it instead.

Tables already scroll horizontally inside their own card via the
shared `<Table>` component or an `overflow-auto` wrapper, and KPI
grids already collapse responsively — both were fine everywhere checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:44:19 +02:00
DEls
efc1ed61d0 fix(layout): keep the Topbar pinned with iOS safe-area padding
The Topbar was already `sticky top-0`, which tracks the whole-page
scroll fine on both phones and desktop since neither the dashboard
layout nor any page wraps it in a clipping scroll container. What was
missing was the iOS PWA notch: add `paddingTop: env(safe-area-inset-top)`
so the bar's own background paints under the status bar/notch in
home-screen standalone mode instead of leaving a transparent gap.

z-30 stays below the z-40/z-50 used by drawers, dropdowns and dialogs,
and below the Leaflet map page's isolated stacking context (1760f77),
so the Topbar's dropdowns still float above the map and dialogs still
float above the Topbar.

Also fixes a real overlap bug surfaced while auditing at 375px: the
sidebar's fixed hamburger button sits at the same top-left corner as
the Topbar's title below `lg`, and nothing reserved room for it, so
the page title rendered partially underneath the button. Reserve that
space with padding instead, and truncate the title/subtitle so long
site names don't overflow the header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:00:27 +02:00
DEls
bdb2b2361b feat(ask): log app change requests for the developer
file_change_request (tools-actions.ts) logs an ask_log row
(type "request", status "open") for a change to the app itself — a new
page, column, button or report layout — distinct from propose_changes,
which changes data; the prompt now routes between the two.
list_change_requests (tools.ts, read-only) lists them, which the
existing MCP server picks up automatically since it iterates the whole
tools array. /dashboard/change-requests lists and re-statuses them
(open/planned/done/declined) via new GET/PATCH routes under
/api/ask/change-requests, linked from the sidebar under Ask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:07:27 +02:00
DEIs
87f75dd98f feat(plant): fleet map page with GPS-denormalised vehicles, clustering, and tile switcher
Adds /dashboard/plant/map: a Leaflet view of every vehicle scanned from
the MEGA Clock kiosk, plotted at its last known position.

Schema
- vehicles.last_latitude / last_longitude / last_scanned_at /
  last_location_accuracy_m denormalised off fuel_logs so the map page
  renders every vehicle in a single query. Same best-effort pattern as
  attention_dismissed_at; updated by /api/clock/vehicle-scan after every
  successful scan. PB init defaults new number cols to 0 (not null) —
  filter uses 'last_latitude > 0 || last_latitude < 0' to mean 'real fix'.
- Indexed via idx_vehicles_last_scanned.

Map UI
- Custom SVG markers per unit_type: car silhouette for fuel, tractor
  silhouette for plant, 24x24, drop-shadowed.
- Clustering via react-leaflet-cluster over Leaflet.markercluster.
  Cluster icons colour-ramp with size (green->sky->amber->rose) and
  scale 32->54px.
- Three-button base-layer switcher: Street (OSM), Dark (ESRI Dark
  Gray Canvas — CartoDB watermarks tiles without a key), Satellite
  (ESRI World Imagery). Last choice persists in localStorage.
- Site pins: small purple map-marker divIcon, click flies the map to
  zoom level 15.
- Empty-state is a floating chip on the map rather than a full-page
  card so the map is always visible.

APIs
- /api/clock/vehicle-scan: stores the GPS fix on the fuel_logs row
  AND denormalises onto the vehicle when present; missing-coord
  scan leaves last_* untouched.

Sidebar: new Map entry under Plant group.
2026-09-22 21:39:25 +02:00
DEls
978ddf03cc feat(workforce): add "On site now" page derived from MEGA Clock scans
A supervisor-facing view of who's currently clocked in, per site — a
live muster list built entirely from the existing clock_events data
(no schema change). Lives in the dashboard next to Workers/Timesheets,
not on the kiosk phone, since it's a manager tool rather than
something a worker scanning their own badge needs to see.

The per-employee "latest event" reduction is shared via
src/lib/clock-events.ts so the on-site page and the Workforce
landing page's new "On site now" KPI tile stay in sync by
construction rather than by copy-pasted logic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 00:59:43 +02:00
DEls
30a45f759d Merge branch 'feat/site-scope' into main
Global site scope: one switcher in the topbar, carried in the URL as
?site=, resolved server-side via the getSiteScope() seam, and applied as
a PocketBase site= filter across every dashboard module. Replaces the
four independent per-table project-code dropdowns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

# Conflicts:
#	docs/superpowers/plans/2026-09-11-site-scope.md
2026-09-11 23:57:51 +02:00
DEls
0c29049324 fix(site-scope): close two latent leaks and the stale scope pill
Three review findings, all of which break the moment `allowed` stops
being every site — i.e. as soon as PocketBase users land.

siteIdFilter(): the Overview spelled its `sites` filter out as
`mode === "all" ? "" : id="..."`, which drops the `unrestricted` check
that makes "all" safe and would load every site for a manager allowed
only a subset. Both field variants now go through one shared function so
the check cannot be forgotten again. Covered by six new tests.

Site pickers read scope.allowed: upload and create dialogs took their
options from unfiltered `sites` reads, so a restricted user could
attribute a PO, delivery note, timesheet or plant item to a site they
cannot even open — a write path, not just a display leak. View scope
must not narrow a picker, but permission scope must. Also drops one
redundant sites query on the equipment page.

useScopeFallback(): the pill fell back to a value the layout computes
once, and layouts do not re-render on navigation — so after any link
that doesn't carry ?site= forward (24 of them), the pill read
"All sites" over a single site's figures. It now reads the live cookie,
which the switcher keeps current, fixing every link rather than the four
scopeAwareHref already patched.

Verified against the running app: scope survives a client-side
navigation through a bare link, and the Overview reads 1/"The Hills
ext 12" scoped and 2/"Across every site" unscoped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 23:56:28 +02:00
DEls
4ac5f3a220 fix(site-scope): preserve ?site= across sidebar navigation
Bug: picking a site in the topbar pill updated both URL and cookie,
but the sidebar nav Links were plain paths ('/dashboard/sites',
'/dashboard/financial/purchase-orders', ...). Clicking any of them
stripped the '?site=' query string. The page's getSiteScope() then
fell back to the still-set cookie and filtered the data correctly,
but the pill's useSearchParams() and the layout's frozen fallbackValue
both produced mode='all' — so the topbar pill showed 'All sites · N'
while every table was filtered to the picked site. The visual pill
and the data disagreed on every screen the user clicked.

Fix: extract scopeAwareHref() and route every sidebar Link through
it. The helper carries the current '?site=' (and nothing else) into
the destination URL, so the URL stays the single source of truth
for both the page filter and the topbar pill across navigation.

Layouts do not re-render on navigation, so preserving the URL is
the only viable fix — making the layout re-run getSiteScope() on
every navigation is what the plan explicitly avoided by design.

Tests: 5 new unit tests for scopeAwareHref covering the bare-path,
scope-only, drop-other-params, url-encoding, and 'all' sentinel
cases. Total: 651 tests passing, typecheck clean.
2026-09-11 23:41:11 +02:00
DEls
729118d5dd feat(site-scope): exempt the Sites list, add a per-row scope action 2026-09-11 23:20:05 +02:00
DEls
6609ce8e50 feat(site-scope): add the topbar scope switcher 2026-09-11 23:03:10 +02:00
DEls
63eb8ed931 feat(site-scope): load the allowed site list in the dashboard layout 2026-09-11 23:02:30 +02:00
DEls
f80c30d369 feat(financial): wire up per-collection upload status pages
Adds the "Upload status" buttons on Purchase Orders and Delivery Notes,
the shared badge/KPI/filter UI they use, and removes the now-dead
Uploads sidebar entry and Financial landing card.
2026-09-11 21:44:12 +02:00
DEls
b454621ecf feat(suppliers): add Suppliers to the Financial sidebar group 2026-09-11 18:20:34 +02:00
DEls
1d706e1985 feat(dashboard): grouped sidebar navigation with landing pages
Implements docs/superpowers/specs/2026-09-11-sidebar-restructure-design.md.
Reorganizes the flat 7-item sidebar into Workforce/Plant/Financial groups,
each with a landing dashboard and sub-pages split out of the former
combined Employees and Purchasing pages. Old routes (/dashboard/employees,
/fuel, /fuel/[vehicleId], /plant/[vehicleId], /purchasing,
/purchasing/[poId]) now redirect to their new locations so existing
bookmarks and links keep working.

New pages: Workforce, Plant, Financial landing dashboards; Timesheets
(entries table + upload, surfacing document status that was previously
invisible); Fuel Logs (flat table across both vehicle types); Delivery
Notes (the page docs/superpowers/specs/2026-09-11-ingestion-restructure-design.md
section 8 deliberately deferred); Uploads (combined PO/DN ingestion status).

Moved-unchanged pages (Workers, Vehicles, Equipment) verified
byte-identical to their prior content via direct diff against main --
relocated only, nothing altered.

Reviewed the initial implementation and found two issues, both fixed
here before commit:
  - The delivery-note review queue (NeedsReviewDns) was duplicated onto
    the Purchase Orders page instead of moving to the new Delivery Notes
    page as specified -- same workflow rendered in two places from two
    independent queries. Removed from Purchase Orders; it now lives
    exclusively on Delivery Notes.
  - The new Uploads page's timestamp column fell back to a `created`
    field that doesn't exist on this PocketBase instance, so it could
    never help. All 24 real purchase_orders predate date_uploaded being
    backfilled and would have shown a blank column forever. Dropped the
    dead fallback; the view already degrades to an em-dash for missing
    timestamps.

Typecheck and full suite verified clean before and after both fixes
(583/583).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 17:47:27 +02:00
DEls
395c0ff114 feat(chat): sidebar link, editable-while-waiting, sticky footer, limit warning
Three concerns from testing:

1. Chat typing UX: the textarea was disabled while a request was in
   flight, so the user couldn't draft the next question. Removed the
   disabled attribute; Enter is just a no-op while busy. Send button
   still gates actual submission.

2. Sticky footer: switched the chat wrapper to 100svh (mobile-keyboard
   aware) and made the composer sticky bottom-0 with a solid background
   so it no longer fades into the scroll area.

3. Limit-warning: the SSE stream now reads the 'done' event and surfaces
   a banner when the server reports the agent loop hit its iteration
   cap, so a quiet stream end isn't mistaken for a complete answer.

Sidebar: added 'Ask' nav item pointing at /dashboard/chat with the
Sparkles icon so the chat is reachable from the dashboard even if the
standalone-PWA install path keeps misbehaving.
2026-08-30 23:51:45 +02:00
DEls
20a16a27f8 Enhance Dashboard and Sidebar Navigation
- Updated the Dashboard page to improve card styling with a warm-amber glow strip and adjusted icon presentation for better visual appeal.
- Expanded the Sidebar navigation by adding new entries for "Sites," "Workforce," "Plant & Equipment," "Fleet & Fuel," and "Purchasing," each with corresponding icons and descriptions to enhance user navigation and access to key features.
2026-08-19 01:54:16 +02:00
DEls
c5b1acfc7f Update package dependencies and remove unused API routes
- Added `pocketbase` as a dependency in `package.json` and `package-lock.json`.
- Updated the `dev` script in `package.json` to run on port 6004 instead of 3000.
- Removed unused API routes: `src/app/api/health/route.ts`, `src/app/api/mcp/route.ts`, `src/app/api/upload/route.ts`, and their associated logic to streamline the codebase.
- Cleaned up `package-lock.json` by removing unnecessary peer dependencies and ensuring consistency in the dependency tree.
2026-08-19 00:14:47 +02:00
DEls
97cb5a48b5 Sidebar: switch breakpoint from xl (1280px) to lg (1024px)
The sidebar was hidden until 1280px wide, which meant laptops and
small desktops between 1024-1280px (incl. iPads in landscape, many
Windows laptops, Chromebooks) saw the hamburger instead of the
persistent sidebar. That felt wrong on what was clearly a desktop.

Dropping to 'lg' (1024px):
- Phone portrait (< 768 px)  → hamburger ✅
- Tablet portrait (768-1024 px) → hamburger ✅
- iPad landscape / laptop (1024 px+) → sidebar ✅
- 932 px iPhone landscape cap is below 1024 px, so the hamburger
  still kicks in for all landscape phones.

Changed 4 occurrences: 3x 'xl:hidden' → 'lg:hidden', 1x
'hidden xl:block' → 'hidden lg:block' on the <aside> element.
2026-08-18 13:31:31 +02:00
DEls
3506640e4a Add Uploads page with 4 colored drop zones
New /dashboard/uploads page with a 2x2 grid of drop zones, one per
ingest source:

  - Orders (orange) — CSV / XLS / XLSX, max 10 MB
  - Delivery Notes (teal) — PDF / images, max 20 MB
  - Fuel Logs (yellow-green) — CSV / XLS / XLSX, max 5 MB
  - Time Sheets (green) — CSV / XLS / XLSX, max 5 MB

Each block has:
  - A coloured left stripe + icon disc in the accent colour
  - Title, format-count badge, and a long description of what to upload
  - File-format chips and a max-size hint
  - A drag-and-drop drop zone that also accepts files via click
  - A primary 'Upload X' button in the accent colour
  - A 'Recent' list showing the last 4 uploads with status badges

When files are dropped or picked, the client POSTs to
/api/upload with a 'type' field. The endpoint validates MIME and size
against the kind-specific rules in src/lib/uploads.ts and returns a
JSON descriptor with accepted/rejected entries. New entries are
prepended to the page's recent-uploads list in real time, and a
bottom-right toast shows accepted/rejected counts.

The sidebar gets a new 'Uploads' nav item with the upload icon.

Files:
  - src/lib/types.ts                    type definitions
  - src/lib/uploads.ts                   acceptance rules + mock history
  - src/app/api/upload/route.ts          type-aware upload handler
  - src/app/dashboard/uploads/page.tsx   the new page
  - src/components/layout/sidebar.tsx    Uploads nav entry
2026-08-09 19:30:00 +02:00
SJCoetz
df459de0f5 Remove Active Sites card from sidebar
The card showed hardcoded '5 projects · 3 flagged · 2 on schedule'
stats that were never updated from the mock data and duplicated
information in the Sites module. Removing it gives the sidebar a
cleaner Logo → Nav → Lock structure and means the Lock screen button
is visible without scrolling on standard heights.

Also align the mobile drawer's close-button breakpoint from lg to xl
to match the rest of the sidebar's breakpoint logic.
2026-08-09 03:02:59 +02:00
SJCoetz
eeea5b7592 Initial commit: MEGA INFRASTRUCTURE dashboard + Coolify Dockerfile 2026-08-09 01:37:40 +02:00