Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
- **Count production from MQTT machines onto a line/step** *(admin / connectivity)* — a machine/MQTT device can be **assigned to a production line**, and a new **"Count at Station / Step"** topic-mapping action turns each sensor pulse (e.g. a break-beam sensor: one unit leaving a station) into `+1` on the addressed step of the line's **currently running** work order — no per-order configuration. The per-step throughput is tracked in a new `passed_qty` counter; a mapping flagged as the finished-goods counting point also feeds the work order's `produced_qty` (through the shared machine-count path, so `counting_source` and auto start/complete are honoured — no double counting). `update_work_order_qty` can now also target a line directly (the running order) instead of a fixed order number. The device form gains an **Assigned line** picker and the topic-mapping editor a guided **Line + Station/Step** form for the count action (no more hand-written JSON).
- **Traceability when editing an in-use process template** *(admin)* — editing a template's steps (add / rename / delete / reorder) still mutates the current template in place, and running work orders correctly keep their frozen snapshot — but that used to happen silently. Now the template page shows a **warning banner** when the template backs active (non-finished) work orders, destructive step edits ask for confirmation while it's in use, and **every step change is written to the immutable audit log** (before/after shape) so the previous version is never lost. No change to how orders resolve their steps.

### Added
- **Add maintenance to the planner** *(admin)* — a new **+ Maintenance** button on the schedule planner opens a modal to place a **defined maintenance** (a maintenance schedule, which pre-fills its title / type / line) or an ad-hoc one onto a line at a chosen date, time and duration. It lands as a **distinct yellow tile** in the line's maintenance strip (maintenance tiles are now yellow instead of purple, so they stand out from work orders). Backed by `POST /admin/schedule/maintenance`.

### Added
- **Plant timezone is changeable after installation** *(admin)* — Settings → System → General now carries a timezone picker (region + zone), writing the same `system_settings` row the installer's step does; the wizard already promised this was possible. The chosen zone is re-applied per request and before each queued job, so on Octane a change reaches every worker immediately instead of waiting for a container restart. Saving reloads the page so every displayed time switches over at once.
- **Product types as Bill-of-Materials components** *(admin)* — a BOM line can now be a manufactured **product type** (a sub-assembly), not only a material. In the BOM editor a Material / Product type switch picks the component kind; product-type lines carry the same quantity-per-unit, step, scrap %, consumption timing and notes as materials. A product type can't be a component of itself, and each appears once per template. Lines are captured in the work-order snapshot as sub-assembly references; they're a simple component reference (they don't explode into their own BOM) and are skipped by the material stock/consumption engine. Additive — existing material BOMs are unaffected.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Http\Controllers\Web\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\Web\Admin\StoreMaintenanceEventRequest;
use App\Models\ScheduleChangeLog;
use App\Models\WorkOrder;
use App\Services\Schedule\SchedulePlannerService;
Expand Down Expand Up @@ -158,6 +159,17 @@ public function resizeOrder(Request $request, WorkOrder $workOrder)
]);
}

/**
* Place a maintenance event on the planner (the "Add maintenance" modal). A
* defined maintenance schedule can pre-fill it, or an ad-hoc title/type.
*/
public function storeMaintenance(StoreMaintenanceEventRequest $request)
{
$this->planner->createMaintenanceEvent($request->validated());

return back()->with('success', __('Maintenance added to the planner.'));
}

public function checkUpdates(Request $request)
{
$lastUpdated = WorkOrder::max('updated_at');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace App\Http\Requests\Web\Admin;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

/**
* Validate an "Add maintenance" placement on the planner. A defined maintenance
* schedule can pre-fill it (must be active — the planner only offers active
* schedules, so a direct POST cannot smuggle in an inactive one), or an ad-hoc
* title/type is supplied instead. Admin access is gated by the route middleware.
*/
class StoreMaintenanceEventRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}

public function rules(): array
{
return [
'schedule_id' => [
'nullable', 'integer',
Rule::exists('maintenance_schedules', 'id')->where('is_active', true),
],
'title' => ['required_without:schedule_id', 'nullable', 'string', 'max:255'],
'event_type' => ['nullable', 'in:planned,corrective,inspection'],
'line_id' => ['required', 'integer', 'exists:lines,id'],
'workstation_id' => ['nullable', 'integer', 'exists:workstations,id'],
'scheduled_at' => ['required', 'date'],
'duration_minutes' => ['nullable', 'integer', 'min:1', 'max:10080'],
'description' => ['nullable', 'string'],
];
}
}
42 changes: 42 additions & 0 deletions backend/app/Services/Schedule/SchedulePlannerService.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,19 @@ public function board(array $params = []): array
: 60,
'description' => $m->description,
])->values()->all(),
// Defined maintenance (schedules) offered in the planner's "Add
// maintenance" modal — drop one onto a line/day as a yellow tile.
'maintenanceSchedules' => MaintenanceSchedule::where('is_active', true)
->orderBy('name')
->get(['id', 'name', 'event_type', 'line_id', 'workstation_id', 'description'])
->map(fn ($s) => [
'id' => $s->id,
'name' => $s->name,
'event_type' => $s->event_type,
'line_id' => $s->line_id,
'workstation_id' => $s->workstation_id,
'description' => $s->description,
])->values()->all(),
'realtimeMode' => $realtimeMode,
'overdueImportant' => [
'count' => $importantOverdueCount,
Expand All @@ -251,6 +264,35 @@ public function board(array $params = []): array
*
* @return array{conflict:bool, message?:string, warnings?:array<string>}
*/
/**
* Place a maintenance event on the planner (the "Add maintenance" modal). A
* defined schedule (schedule_id) pre-fills the title / type / line; a bare
* title + event_type works too. Lands as a pending tile at the chosen slot.
*
* @param array<string, mixed> $input
*/
public function createMaintenanceEvent(array $input): MaintenanceEvent
{
$schedule = ! empty($input['schedule_id'])
? MaintenanceSchedule::find($input['schedule_id'])
: null;

$scheduledAt = Carbon::parse($input['scheduled_at']);
$duration = (int) ($input['duration_minutes'] ?? 60);

return MaintenanceEvent::create([
'title' => $input['title'] ?? $schedule?->name ?? 'Maintenance',
'event_type' => $input['event_type'] ?? $schedule?->event_type ?? MaintenanceEvent::TYPE_PLANNED,
'status' => MaintenanceEvent::STATUS_PENDING,
'line_id' => $input['line_id'] ?? $schedule?->line_id,
'workstation_id' => $input['workstation_id'] ?? $schedule?->workstation_id,
'schedule_id' => $schedule?->id,
'scheduled_at' => $scheduledAt,
'scheduled_end_at' => $scheduledAt->copy()->addMinutes(max(1, $duration)),
'description' => $input['description'] ?? $schedule?->description,
]);
}

public function updateOrder(WorkOrder $workOrder, array $input, bool $force = false): array
{
$data = [];
Expand Down
15 changes: 14 additions & 1 deletion backend/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5670,6 +5670,15 @@
"Select timezone": "Select timezone",
"Plant timezone. Every timestamp, report boundary and shift edge in the app is expressed in it.": "Plant timezone. Every timestamp, report boundary and shift edge in the app is expressed in it.",
"Changing the timezone reloads the page so every displayed time switches over at once.": "Changing the timezone reloads the page so every displayed time switches over at once.",
"Add maintenance": "Add maintenance",
"Defined maintenance": "Defined maintenance",
"— None (custom) —": "— None (custom) —",
"e.g. Lubrication": "e.g. Lubrication",
"Minutes": "Minutes",
"Add to planner": "Add to planner",
"Pick a line, a date and a maintenance (defined or a title).": "Pick a line, a date and a maintenance (defined or a title).",
"Maintenance added to the planner.": "Maintenance added to the planner.",
"Could not add maintenance.": "Could not add maintenance.",
"Choose your setup": "Choose your setup",
"Before the setup wizard": "Before the setup wizard",
"First pick the feature set you want. Core areas (Dashboard, Orders, Production, Admin) are always on. You can change this anytime in Settings → System → Modules. Next, the setup wizard will walk you through your first line, product, process template and work order.": "First pick the feature set you want. Core areas (Dashboard, Orders, Production, Admin) are always on. You can change this anytime in Settings → System → Modules. Next, the setup wizard will walk you through your first line, product, process template and work order.",
Expand All @@ -5682,5 +5691,9 @@
"Editing steps here does not affect those running orders — they keep the frozen snapshot taken when they were created. Changes are not versioned; the previous shape is kept only in the audit log. For a controlled change to a specific order, use its change request instead.": "Editing steps here does not affect those running orders — they keep the frozen snapshot taken when they were created. Changes are not versioned; the previous shape is kept only in the audit log. For a controlled change to a specific order, use its change request instead.",
"Edit a template that is in use?": "Edit a template that is in use?",
"This template backs :count active work order(s). They keep their frozen snapshot and are unaffected, but this change is not versioned — only the audit log keeps the previous shape.": "This template backs :count active work order(s). They keep their frozen snapshot and are unaffected, but this change is not versioned — only the audit log keeps the previous shape.",
"Action parameters must be a JSON object.": "Action parameters must be a JSON object."
"Action parameters must be a JSON object.": "Action parameters must be a JSON object.",
"Could not add the maintenance.": "Could not add the maintenance.",
"Pick a maintenance for this slot": "Pick a maintenance for this slot",
"Search maintenance": "Search maintenance",
"No maintenance schedules.": "No maintenance schedules."
}
15 changes: 14 additions & 1 deletion backend/lang/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -5670,6 +5670,15 @@
"Select timezone": "Wybierz strefę czasową",
"Plant timezone. Every timestamp, report boundary and shift edge in the app is expressed in it.": "Strefa czasowa zakładu. Wszystkie znaczniki czasu, granice raportów i zmian w aplikacji są w niej wyrażone.",
"Changing the timezone reloads the page so every displayed time switches over at once.": "Zmiana strefy czasowej przeładowuje stronę, aby wszystkie wyświetlane godziny zmieniły się naraz.",
"Add maintenance": "Dodaj utrzymanie",
"Defined maintenance": "Zdefiniowane utrzymanie",
"— None (custom) —": "— Brak (własne) —",
"e.g. Lubrication": "np. Smarowanie",
"Minutes": "Minuty",
"Add to planner": "Dodaj do planera",
"Pick a line, a date and a maintenance (defined or a title).": "Wybierz linię, datę i utrzymanie (zdefiniowane lub tytuł).",
"Maintenance added to the planner.": "Utrzymanie dodane do planera.",
"Could not add maintenance.": "Nie udało się dodać utrzymania.",
"Choose your setup": "Wybierz konfigurację",
"Before the setup wizard": "Przed kreatorem konfiguracji",
"First pick the feature set you want. Core areas (Dashboard, Orders, Production, Admin) are always on. You can change this anytime in Settings → System → Modules. Next, the setup wizard will walk you through your first line, product, process template and work order.": "Najpierw wybierz zestaw funkcji. Obszary podstawowe (Pulpit, Zlecenia, Produkcja, Admin) są zawsze włączone. Możesz to zmienić w każdej chwili w Ustawienia → System → Moduły. Następnie kreator konfiguracji przeprowadzi Cię przez pierwszą linię, produkt, szablon procesu i zlecenie.",
Expand All @@ -5682,5 +5691,9 @@
"Editing steps here does not affect those running orders — they keep the frozen snapshot taken when they were created. Changes are not versioned; the previous shape is kept only in the audit log. For a controlled change to a specific order, use its change request instead.": "Edycja kroków tutaj nie wpływa na trwające zlecenia — zachowują one zamrożony snapshot z chwili utworzenia. Zmiany nie są wersjonowane; poprzedni kształt pozostaje wyłącznie w dzienniku audytu. Dla kontrolowanej zmiany konkretnego zlecenia użyj jego wniosku o zmianę.",
"Edit a template that is in use?": "Edytować szablon będący w użyciu?",
"This template backs :count active work order(s). They keep their frozen snapshot and are unaffected, but this change is not versioned — only the audit log keeps the previous shape.": "Ten szablon obsługuje :count aktywnych zleceń. Zachowują one zamrożony snapshot i pozostają nienaruszone, ale ta zmiana nie jest wersjonowana — poprzedni kształt zachowuje tylko dziennik audytu.",
"Action parameters must be a JSON object.": "Parametry akcji muszą być obiektem JSON."
"Action parameters must be a JSON object.": "Parametry akcji muszą być obiektem JSON.",
"Could not add the maintenance.": "Nie udało się dodać przeglądu.",
"Pick a maintenance for this slot": "Wybierz przegląd dla tego slotu",
"Search maintenance": "Szukaj przeglądu",
"No maintenance schedules.": "Brak harmonogramów przeglądów."
}
46 changes: 44 additions & 2 deletions backend/resources/js/Pages/admin/schedule/Planner.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { HourlyView, MonthlyView } from './planner/views2';
import { Toolbar, BacklogRail } from './planner/panels';
import {
OrderEditSheet, AssignPopup, ConflictDialog, LiveTrackingBar, Toasts, SavingOverlay,
AddMaintenanceModal,
} from './planner/modals';

// Hover affordances for the design's work-order blocks (brightness + reveal ✕).
Expand Down Expand Up @@ -40,7 +41,7 @@ export default function Planner() {
workOrders = [], lines = [], allLines = [], shifts = [],
viewMode = 'weekly', shiftsPerDay = 1, slotMinutes = 15, showWeekends = true,
startDate, rangeStart, rangeEnd, navPrev, navNext,
backlogOrders = [], maintenanceEvents = [], realtimeMode = 'polling',
backlogOrders = [], maintenanceEvents = [], maintenanceSchedules = [], realtimeMode = 'polling',
overdueImportant = { count: 0, orders: [] },
} = usePage().props;

Expand All @@ -65,6 +66,7 @@ export default function Planner() {
const [conflict, setConflict] = useState(null); // { apply }
const [confirmBox, setConfirmBox] = useState(null); // { title, body, confirmLabel, apply }
const [trackingData, setTrackingData] = useState(null);
const [maintOpen, setMaintOpen] = useState(false);
const draggingRef = useRef(false);

const toast = useCallback((msg, kind = 'success') => {
Expand Down Expand Up @@ -362,6 +364,13 @@ export default function Planner() {
<Toolbar ctx={ctx} view={viewMode} setView={setView} lineFilter={lineId} setLineFilter={setLineFilter}
live={live} onPrev={() => goTo(navPrev)} onNext={() => goTo(navNext)} onToday={() => nav({ view_mode: viewMode, line_id: lineId })} rangeLabel={rangeLabel} />

<div className="flex justify-end mb-2">
<button type="button" onClick={() => setMaintOpen(true)}
style={{ fontSize: 12.5, fontWeight: 600, color: '#78350f', background: '#fde68a', border: '1px solid #d97706', borderRadius: 8, padding: '7px 12px' }}>
+ {__('Maintenance')}
</button>
</div>

<div className="flex items-start" style={{ border: '1px solid var(--om-line)', borderRadius: 12, overflow: 'hidden', background: 'var(--om-bg)' }}>
<div className="om-main flex-1 min-w-0" style={{ padding: '18px 20px', overflow: 'auto' }}>
{viewMode === 'weekly' && <WeeklyView ctx={ctx} />}
Expand All @@ -374,7 +383,30 @@ export default function Planner() {
</DndProvider>

{selected && <OrderEditSheet wo={selected} ctx={ctx} onClose={() => setSelected(null)} onSave={saveEdit} onUnassign={unassign} />}
{assignTarget && <AssignPopup target={assignTarget} ctx={ctx} onClose={() => setAssignTarget(null)} onPick={(wo, target) => { setAssignTarget(null); dropToCell(wo, target); }} />}
{assignTarget && (
<AssignPopup
target={assignTarget}
ctx={ctx}
schedules={maintenanceSchedules}
onClose={() => setAssignTarget(null)}
onPick={(wo, target) => { setAssignTarget(null); dropToCell(wo, target); }}
onPickMaintenance={(s, target) => {
setAssignTarget(null);
router.post('/admin/schedule/maintenance', {
schedule_id: s.id,
title: s.name || null,
event_type: s.event_type || 'planned',
line_id: target.lineId,
scheduled_at: `${target.date} 08:00`,
duration_minutes: s.duration_minutes || 60,
}, {
preserveScroll: true,
onSuccess: () => toast(__('Maintenance added to the planner.')),
onError: (errors) => toast(Object.values(errors || {})[0] || __('Could not add maintenance.'), 'error'),
});
}}
/>
)}
{conflict && <ConflictDialog onCancel={() => setConflict(null)} onConfirm={() => { conflict.apply(); setConflict(null); }} />}
{confirmBox && (
<ConfirmDialog open onClose={() => setConfirmBox(null)}
Expand All @@ -383,6 +415,16 @@ export default function Planner() {
{confirmBox.body}
</ConfirmDialog>
)}
{maintOpen && (
<AddMaintenanceModal
lines={allLines}
schedules={maintenanceSchedules}
startDate={startDate}
onClose={() => setMaintOpen(false)}
onCreated={() => { setMaintOpen(false); toast(__('Maintenance added to the planner.')); }}
onError={(msg) => toast(msg || __('Could not add maintenance.'), 'error')}
/>
)}
{saving && <SavingOverlay />}
<Toasts toasts={toasts} />
</>
Expand Down
Loading
Loading