Skip to content

feat: plant overview dashboard sections, review fixes and work-order permissions - #381

Merged
jakub-przepiora merged 64 commits into
developfrom
cez/dashboard-integration
Oct 25, 2026
Merged

jakub-przepiora merged 64 commits into
developfrom
cez/dashboard-integration

Conversation

@jakub-przepiora

Copy link
Copy Markdown
Contributor

What

Completes the Plant overview admin dashboard from the design mockup, on top of the foundation (#379) and the production targets (#380) already in develop. Admin → Dashboard now has every section of the mockup, fed by live data:

  • Line activity · this shift: one bar per line, Running / Idle / Blocked / Down up to Now, with uptime.
  • OEE · this shift: plant OEE since shift start, A/P/Q bars and per-line OEE against each line's target. The target is the line's Planning efficiency, or 85 % when that is not set.
  • Downtime: shift total by reason with the affected lines and the share of planned time. Unclassified stops link to the Shift Monitor.
  • Production · planned vs actual and Output per hour: the plan comes from the Schedule, actual from a new output ledger.
  • KPI tiles: orders not scheduled (the planner's own backlog rule), lines producing, scrap rate and first-pass yield, each against its target.
  • Needs action: down and blocked lines, low stock, failed inbound inspections, quarantined lots and overdue corrective actions. Each row has a button that leads only to existing pages or actions.
  • Line cards (order progress, ETA or the reason the line stopped) and a summary strip.
  • Review fixes to feat: add plant overview header and live line states to the dashboard #379:
    • "2 h 59 min left" rounding;
    • a line whose collector went silent shows No data instead of its last state;
    • several open stops per station are handled;
    • the header shows the default shift window when no shifts are set up.
  • Review fixes to feat: add production targets and first-pass yield #380: the OEE target no longer counts finished or cancelled orders, FPY is computed with a SQL aggregate, and the targets card loads lazily.

Two fixes outside the dashboard, found while analysing it:

  • Overdue alerts use the plant's midnight. The badge and alerts used the UTC date.
  • Work-order create/edit enforce WorkOrderPolicy. Having the Orders tab was enough before. The pages now return 403 without the permission and the buttons are hidden.
    • This also restores the operator sidebar for granted admin tabs, which a redesign had dropped.
    • It also fixes saving Settings → Tab Access.

Why

The admin dashboard had no view of the shift in progress. The mockup asked for one screen showing what each line is doing, how far production is from plan, and what needs attention.

How it was built

Each section was built as its own task, and the tasks were then integrated here:

  • One line-state mapping. The machine state is mapped to a line state in one place (PlantOverviewService::machineCandidate()), shared by the current state, the timeline, the cards and Needs action.
  • Lines are resolved once per refresh and shared by the line states, KPI tiles and Needs action. A line can no longer read differently in two sections.
  • No import cycle between sections. OverviewCard has its own module. Sections took it from OverviewGrid, which imports every section.
  • One line-state vocabulary. All sections use lineStates.js for labels and colours.
  • No new dependencies. Charts are hand-drawn SVG.
  • Untouched: GET /api/v1/admin/dashboard (used by the mobile app), the shared synced collections, OeeCalculationService and its cache key, OeeGauge, dashboard_widgets and the WidgetRegistry contract.
  • Existing widgets stay under Last 30 days.

Please review

  • Schema change: new table work_order_output_entries, written by WorkOrderOutputObserver on every change to produced_qty. It is the source of output per hour. All produced_qty writes in app/ go through the model, so none bypass it. Hourly figures start from the upgrade.
  • Line state rule: a line is Down while any of its stations is down (fault, maintenance, unplanned stop). Workstations have no "critical" flag, so every station counts. On lines with parallel stations this can overstate Down.
  • Left out on purpose: a change to realtimeCollection.js that re-read every live collection after any form submit. It is app-wide and unrelated to this work, so it is a separate follow-up.

How it was verified

  • php artisan test: 3715 passed (1 skipped, 1 risky, as on develop).
  • Frontend: npx vitest run, 251 passed.
  • vite build passes, pint --test is clean on the changed files, and en.json / pl.json have the same key set.
  • The dashboard was checked by hand on a local instance with all three example companies (machine shop, print shop, bakery) plus the Shift Monitor demo seeder:
    • every section renders and there are no console errors;
    • no horizontal scroll at 1440, 1024 and 390 px, in light and dark;
    • section figures were cross-checked against direct database queries: downtime minutes, work orders in progress, output in the shift, and the line-state reasons.

Known follow-ups (not in this PR)

  • System reset fails without ADMIN_* env (web-installer and manual installs). With the Docker compose defaults it recreates the admin with a known password.
  • The timeline's hour labels are crowded on phones.
  • Page-level performance measurement and admin-guide docs for the dashboard.

🤖 Generated with Claude Code

jakub-przepiora and others added 30 commits October 24, 2026 20:55
The sidebar alert badge, its Latest alerts panel, the Alerts page and the
work orders ?overdue=1 filter took "today" from toISOString(), the UTC
date. Outside UTC that moved the overdue boundary hours away from the
plant's midnight and made the live badge disagree with the server's
nav.alertCount (AlertController::totalCount uses plant-zone today()).

- lib/i18n: todayInTimezone() and timezone() getter
- lib/alerts: one shared overdue/count rule mirroring totalCount
- lib/usePlantToday: re-checks the date every minute so the badge flips
  at the plant's midnight without a data change
- tests/fixtures/alert-count-cases.json feeds both the vitest and the
  Laravel parity tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…y targets card

- ProductionTargets::plannedMinutesByLine() only weighs scheduled operations
  of active orders, so finishing, cancelling or rejecting a planned order no
  longer keeps its line's weight in the plant OEE target.
- FirstPassYieldService finds each batch's first check with SQL aggregates
  (MIN(checked_at) per batch, lowest id on ties) instead of loading every
  check; results are unchanged.
- Settings → System sends the Production targets prop only for the
  Production tab (Inertia::optional otherwise); the card fetches it with a
  partial reload when the tab opens and shows "Loading…" meanwhile.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The admin work-order create/store and edit/update routes were gated only
by the Orders tab, and the supervisor edit/update only by the role, so
WorkOrderPolicy::create/update were never consulted there.

- StoreWorkOrderRequest / both UpdateWorkOrderRequests authorize via the
  policy, so the 403 lands before validation (no order_no probing)
- admin create/edit and supervisor edit call $this->authorize()
- share auth.user.can.{createWorkOrders,editWorkOrders} with Inertia and
  hide New Work Order (lists, planner, weekly schedule) and Edit without them
- list pages no longer send create-form options to users who can't create

Seeder unchanged: Admin and Supervisor keep create/edit; an Operator given
tab:orders loses create/edit on /admin/work-orders (deliberate).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…silent collectors

- Header time left: "2 h 59 min left" under three hours, whole hours rounded
  up above (was truncated, so 179 min read "2 h left").
- Every ingested machine signal bumps the open workstation_states row's
  updated_at (throttled to once a minute, query-builder update, so no
  broadcast). A line whose machine-reported states have not been refreshed
  for more than 10 minutes now reads `unknown` ("No data") instead of its last
  state; manual states and person-reported facts still apply.
- Open stops are read once and passed to ShiftMonitorService::currentStates(),
  which now keeps every open stop per station: since = the oldest, reason = the
  oldest given cause, needs_reason while any is a placeholder.
- MAINTENANCE and WAITING set needs_reason like FAULT/STOPPED; AUTO-* reason
  names are never shown as a cause.
- New Lines section (state pill, since, reason) with a legend including
  "No data"; shared line-state vocabulary in lineStates.js.
- useOverviewRefresh tells its own reload apart from user visits by the visit
  params and counts overlapping visits, matching its comment.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
#	backend/lang/en.json
#	backend/lang/pl.json
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The operator layout restyle dropped the sidebar that rendered
auth.user.accessibleTabLinks, so an Operator granted a tab in Settings →
Access (e.g. Orders) had no way to click through to it. Bring it back
with the current design tokens: "Lines" first, then each granted tab.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Overdue orders panel now states the cut-off it uses ("Due before
24/10/2026 (Europe/Warsaw)"), so the plant-timezone day boundary is
visible rather than implied. The Latest alerts panel no longer shows a
bare "No open issues." under a non-zero count: it says the alerts are
overdue or blocked work orders.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
First plant-overview section, "Line activity · this shift": one bar per
active line across the current plant shift (Running / Idle / Blocked /
Down up to a Now marker), hover tooltips with times, duration and stop
reason, per-line uptime, and the line name linking to its Shift Overview
for users with the shift_monitor tab.

PlantOverviewService::lineTimeline() aggregates per minute: every signal
on the line casts a candidate and the worst wins (down > blocked >
running > idle), reusing the line-state mapping now shared with
lineStates(). Station slices come from the new
ShiftMonitorService::stateTimelines() (no counter feed); lines without a
collector fall back to batches, in-progress orders and manual stops.
Fixed query count regardless of line count.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`distinct` on the nested `grants.*.*` wildcard compares values across
roles, so the form - which posts every role's grants, and Admin and
Supervisor both hold both inspection grants on a fresh install - always
failed validation, and the page showed no error. Drop the rule (the
controller already de-duplicates) and render validation errors above Save.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The hatched background ran under the label, so "No data" was hard to read on
white. The pill now has a plain card background with a dashed edge, and the
hatch moves to a small swatch in front of the label.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With no open issues the Latest alerts panel now reads "No open issues.
The alerts are work orders: 1 overdue." instead of pointing at a list
that isn't there. Clicking the Alerts nav item closes the hover panel and
keeps it closed until the pointer leaves, so it no longer covers the
Alerts page it just opened.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Plant OEE since shift start with the gap to target and the biggest loss,
availability / performance / quality bars with a target tick, and every
active line's OEE sorted best first, coloured against the line's own
target (planning efficiency, default 85%). An (i) tooltip explains the
formula and where the target comes from.

Data is the shift monitor's live machine data, not oee_records:
ShiftMonitorService::oeeInputs() exposes the raw per-station sums and
PlantOverviewService::shiftOee() combines them — availability weighted by
opportunity minutes, performance by expected pieces (capped per station),
quality by pieces made — so a single-station line matches its Plant Board
tile exactly. Query count is fixed regardless of line and station count.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A collection snapshot sends work_orders.due_date as the raw column (plant
wall time, "2026-06-15 00:00:00"), but a live delta sends the model's
datetime cast serialised as UTC ISO ("2026-06-14T22:00:00.000000Z" for
the same value in Warsaw). Slicing the first 10 characters of the delta
form gave the previous day, so an order due today counted as overdue as
soon as it was updated.

- lib/i18n: plantDate() — zone-less values kept as-is, zoned ones
  converted into the plant timezone
- lib/alerts: isOverdue/countAlerts read due dates through plantDate
- fixture gains broadcast_due_date; the server test asserts it equals
  attributesToArray(), the front counts both forms
- work-orders/Index imports TERMINAL_WO_STATUSES instead of a copy

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- work-order lists tell a user without `create work orders` why there is
  no New Work Order button
- the admin/supervisor detail pages hide Edit without `edit work orders`;
  the admin show page no longer serves the edit-form options to them
- Settings → Tab Access lists, read-only, which roles hold `create work
  orders` / `edit work orders`
- synced lists re-read their snapshot after any completed Inertia write,
  so a just-created order or account shows even when the WebSocket is down

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Current-shift downtime grouped by reason, next to OEE: total, lines hit,
loss as a share of planned time so far, reason rows with line codes and
kind-coloured bars, unclassified stops linking to Shift Monitor, and a
"Downtime report" link to Plant Board. Data comes from the new
PlantOverviewService::downtime(), delivered as overview.downtime so the
60 s partial reload refreshes it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…om collectors

- A stop a person logged or classified on a station whose collector went
  silent now still makes the line Down with its reason, instead of being
  hidden behind "No data". Only the machine's own untouched AUTO-* stop is
  dropped with the stale state.
- WorkstationStateMachine::transition() no longer marks a signal, so an
  operator re-setting the same state cannot make a silent line look live;
  the ingestor's call is the only one.
- markSignal() remembers per instance when it last ran per station, so a
  long-running poller issues the UPDATE once a minute, not once per pulse.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Four "Shift at a glance" tiles next to the production chart row:
- Orders not scheduled: the planner backlog, through a new shared
  WorkOrder::plannerBacklog() scope that SchedulePlannerService::board()
  now uses too, with due-this-week / overdue in the plant timezone
- Lines producing: running / active lines from the header's line states
- Scrap rate: ScrapReportService maths over the current ShiftWindow,
  limited to the tenant's active lines, against the D2 target
- First-pass yield: FirstPassYieldService for the shift, "—" when the
  quality module is off or nothing was checked

Tone and links are resolved server-side (ShiftKpiService); a tile links
only to a tab the user can open.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two plant-overview sections fed by a new `production` key in the
dashboard's `overview` prop (ShiftPlanService::forShift):

- plan: scheduled orders' planned_qty spread linearly over their
  planned_start_at/planned_end_at window, clipped to the shift hours —
  the same window the Schedule board draws (an applied simulation writes
  its envelope back onto it; draft simulations are never read);
- actual: a new append-only ledger, work_order_output_entries, written
  by WorkOrderOutputObserver on every produced_qty change, whichever
  path made it;
- running totals, pace now, plan pcs/h and a factual summary (below/
  above plan since, overlapping unplanned downtime). No plan is null.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
jakub-przepiora and others added 26 commits October 24, 2026 22:42
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…erview

- The OPC UA gateway posts readings only on change, so a press stuck in
  FAULT went quiet while its gateway kept heart-beating, and the line turned
  to "No data" after 10 minutes. The heartbeat endpoint now marks every
  station its connection's active tags read as heard from (one throttled
  query-builder update, no model events).
- An operator's note on an unchanged state no longer bumps updated_at, so it
  cannot make a silent machine look live.
- A stop logged by hand on a fresh station whose state carries no stops
  (RUNNING, IDLE, SETUP, CLEANING) now reaches the line state, e.g. a quality
  hold on a running machine.
- ShiftMonitorDemoSeeder documents that its lines read "No data" ten
  minutes after seeding unless a collector reports on them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The "% of planned time" summed every station's stop against one line's
time, so four stations faulting together read 400 %. Overlapping stops
are now merged per line and planned stops taken out of both loss and
base, so it stays within 100 %. The total and reason rows still list
every stop, matching Plant Board.

Plant Board's downtime total is cached per tenant for a minute, the
poll lag for line-level stops is documented, and durationLabel moves to
lib/i18n so production pages don't import a dashboard component.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Plant overview gains a "Lines" section — one card per active line with its
state pill (coloured edge for blocked/down), the current order's progress
(produced / planned · product code, +N more), and either the ETA from the
last hour's output (falling back to planned_end_at, "Late" past it) or the
state reason with its duration. Cards open the line's Shift Overview.

A summary strip shows work orders today (plant day), materials below
minimum, open non-conformances (new Issue::nonConformance scope, not every
open issue) and the next order; fields of disabled modules are dropped and
links are gated by tab access.

Data comes from two new services (LineProgressService, PlantSummaryService)
added to PlantOverviewService::overview() as `line_orders` and `summary`,
so the existing 60 s partial reload refreshes them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s from

- ShiftMonitorDemoSeeder mirrors each line's output (its last station's
  batches) into work_order_output_entries in quarter-hour steps, so the
  example company's dashboard shows actual output instead of 0; re-runs
  replace their own rows.
- The planned-vs-actual card says how many scheduled orders make up the
  shift plan, with an "Open schedule" link for users with the schedule tab.
- The cumulative chart is shorter on phones so the card stack stays compact.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The card listed every classified reason first and Unclassified last, so
a 0 m changeover sat above a 3 h unclassified block and the list read as
unsorted. Unclassified now takes its place by time (after equal ones).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The plant-overview page brings its own padding (32/40 px as in the
  mockup, tighter on phones); AppLayout's <main> has none, so the heading,
  button and cards touched the window edges.
- Output per hour fits a phone: narrower columns, "5.3k" value labels and
  hour-only time labels when the columns are too narrow for "5,318" and
  "22:00".
- Shared useWidth hook (callback ref) for both charts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The example company's output ledger was a copy of the shift-monitor
batches, which run at nameplate rate on whatever orders a line has: next
to the shift plan the dashboard read 150-200 % of plan within an hour.
ShiftMonitorDemoSeeder now records, for every order scheduled in each
line's live shift, 72-98 % of its planned pieces per hour in quarter-hour
steps up to now; lines with nothing scheduled get nothing. App logic is
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
#	backend/lang/en.json
#	backend/lang/pl.json
# Conflicts:
#	CHANGELOG.md
#	backend/app/Services/Dashboard/PlantOverviewService.php
#	backend/lang/en.json
#	backend/lang/pl.json
#	backend/resources/js/components/dashboard/overview/sections.js
# Conflicts:
#	backend/app/Services/Dashboard/PlantOverviewService.php
#	backend/lang/en.json
#	backend/lang/pl.json
#	backend/resources/js/components/dashboard/overview/sections.js
# Conflicts:
#	backend/app/Services/Dashboard/PlantOverviewService.php
#	backend/lang/en.json
#	backend/lang/pl.json
#	backend/resources/js/components/dashboard/overview/sections.js
# Conflicts:
#	CHANGELOG.md
#	backend/app/Services/Dashboard/PlantOverviewService.php
#	backend/lang/en.json
#	backend/lang/pl.json
#	backend/resources/js/components/dashboard/overview/sections.js
# Conflicts:
#	CHANGELOG.md
#	backend/app/Http/Controllers/Web/Admin/DashboardController.php
#	backend/app/Services/Dashboard/PlantOverviewService.php
#	backend/lang/en.json
#	backend/lang/pl.json
#	backend/resources/js/components/dashboard/overview/sections.js
# Conflicts:
#	CHANGELOG.md
#	backend/app/Http/Controllers/Web/Admin/DashboardController.php
#	backend/app/Services/Dashboard/PlantOverviewService.php
#	backend/app/Services/Production/ShiftMonitorService.php
#	backend/lang/en.json
#	backend/lang/pl.json
#	backend/resources/js/components/dashboard/overview/sections.js
# Conflicts:
#	CHANGELOG.md
#	backend/app/Services/Dashboard/PlantOverviewService.php
#	backend/lang/en.json
#	backend/lang/pl.json
#	backend/resources/js/components/dashboard/overview/sections.js
# Conflicts:
#	CHANGELOG.md
#	backend/lang/en.json
#	backend/lang/pl.json
Work-order create/edit permission on web forms (task G2), with its
operator-sidebar regression fix and the Settings → Access quality-grant fix.

Left out on purpose: the realtimeCollection.js change that re-reads every
live collection after any non-GET Inertia visit. It is an app-wide
behaviour change unrelated to permissions; tracked as a separate follow-up.
The temporary Lines section from the review fixes was replaced by the line
cards, so the spec looks for data-testid="line-card" instead of
"overview-line".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…the create permission

With no shifts set up, the overview sections report on the fallback 06–18 /
18–06 window while the header said "No shift running". The header now
shows that window, labelled as the default, so the two agree.

The "New work order" button now also requires the create work orders
permission (auth.user.can.createWorkOrders), matching the form, which
answers 403 without it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…gration

# Conflicts:
#	CHANGELOG.md
#	backend/lang/en.json
#	backend/lang/pl.json
Drops a fragment the merge left attached to the line-cards entry, updates
the header entry for the line cards and the default shift window, and
removes the entry for the live-sync re-read that was left out of this
branch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one): 1
  • cla-signed

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

en-GB formats 5318 as "5.3K" or "5.3k" depending on the CLDR data the
Node runtime ships (CI runs Node 20, which gives "5.3k"). The test now
checks the shortening, not the letter case.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jakub-przepiora
jakub-przepiora marked this pull request as ready for review October 25, 2026 14:20
@jakub-przepiora
jakub-przepiora merged commit 5924337 into develop Oct 25, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant