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 @@ -7,6 +7,9 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).

## [Unreleased]

### Changed
- **Sample data now fills the production planner** *(demo)*: the "Load sample data" seeder used to scatter ~46 work orders one-every-two-days across a 3-month horizon, so any single week of the planner looked nearly empty (~9 orders spread over 5 lines × 7 days × 3 shifts). It now **densely packs the current week** — an order in most (line, day, shift) slots, anchored to `now()->startOfWeek()` so the visible week is always the busy one — with a lighter taper over the neighbouring weeks and last week's work marked DONE. The current week stays **active end-to-end** (in-progress / accepted / pending) so every day renders blocks, including days already behind "today". A freshly seeded demo now opens on a realistic, almost-full weekly board instead of a sparse one. `PrintShopDemoSeeder` only; no schema or app-logic change.

## [0.17.1] - 2026-07-23

### Fixed
Expand Down
112 changes: 76 additions & 36 deletions backend/database/seeders/PrintShopDemoSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,11 @@ private function seedWorkOrders(array $pt, array $lines): void
);
}

// Generate additional orders spread over 3 months
// Fill the planner densely for the CURRENT week — a busy shop should read
// as an almost-full weekly board — then taper off over the neighbouring
// weeks so paging forward/back still shows work (with a short DONE tail
// behind "today"). Everything is anchored to now()->startOfWeek(), so the
// visible week is always the packed one whenever the sample data loads.
$allLines = array_values($lines);
$allPt = array_values($pt);
$descriptions = [
Expand All @@ -467,41 +471,77 @@ private function seedWorkOrders(array $pt, array $lines): void
'Prototype — client approval pending',
];

$n = 8; // start after WO-2026-007
for ($day = -7; $day <= 83; $day += 2) {
$n++;
$orderNo = sprintf('WO-2026-%04d', $n);
$line = $allLines[array_rand($allLines)];
$product = $allPt[array_rand($allPt)];
$qty = rand(10, 200);
$priority = rand(1, 5);
$startDate = now()->addDays($day)->setTime(rand(6, 14), 0);
$durationHours = rand(4, 48);
$endDate = $startDate->copy()->addHours($durationHours);

$status = match (true) {
$day < -3 => WorkOrder::STATUS_DONE,
$day < 0 => WorkOrder::STATUS_IN_PROGRESS,
$day < 3 => WorkOrder::STATUS_ACCEPTED,
default => WorkOrder::STATUS_PENDING,
};

WorkOrder::updateOrCreate(
['order_no' => $orderNo],
[
'line_id' => $line->id,
'product_type_id' => $product->id,
'planned_qty' => $qty,
'produced_qty' => $status === WorkOrder::STATUS_DONE ? $qty : 0,
'status' => $status,
'priority' => $priority,
'due_date' => $endDate->copy()->addDays(rand(0, 3)),
'planned_start_at' => $startDate,
'planned_end_at' => $endDate,
'description' => $descriptions[array_rand($descriptions)],
'completed_at' => $status === WorkOrder::STATUS_DONE ? $endDate : null,
]
);
// Representative start hour per shift column (Morning 06–14, Afternoon
// 14–22, Night 22–06). The planner derives a block's shift column from
// planned_start_at's time, so a start inside a shift lands the block in
// that column. Night runs on DTG/SITO, Mon–Thu only (see seedShifts).
$shifts = [['h' => 8, 'night' => false], ['h' => 16, 'night' => false], ['h' => 23, 'night' => true]];
$nightLines = ['DTG', 'SITO'];

// week offset => fill probability. The current week (0) is packed; the
// neighbours taper so the board stays believable as you page around.
$weekFill = [-1 => 0.35, 0 => 0.85, 1 => 0.40, 2 => 0.25];

$weekStart = now()->startOfWeek();
$n = 100; // WO-2026-0100+, clear of the hand-authored WO-2026-001..010

foreach ($weekFill as $weekOffset => $fill) {
foreach ($allLines as $line) {
for ($d = 0; $d < 7; $d++) {
foreach ($shifts as $shift) {
// Night is DTG/SITO, Mon–Thu only — keep the board honest.
if ($shift['night'] && (! in_array($line->code, $nightLines, true) || $d > 3)) {
continue;
}
if (mt_rand(1, 100) > (int) round($fill * 100)) {
continue;
}

$n++;
$start = $weekStart->copy()->addWeeks($weekOffset)->addDays($d)->setTime($shift['h'], 0);
$end = $start->copy()->addHours(mt_rand(2, 7));
$qty = mt_rand(10, 200);

// Last week's work is DONE (drops off the active board);
// the current week stays ACTIVE end-to-end so every day
// renders blocks (a packed board), even the days already
// behind "today" (long / carried-over jobs); future weeks
// are still-to-start. DONE orders aren't drawn as blocks,
// so keeping the visible week active is what fills the grid.
$status = match (true) {
$weekOffset < 0 => WorkOrder::STATUS_DONE,
$weekOffset > 0 => mt_rand(0, 1) ? WorkOrder::STATUS_PENDING : WorkOrder::STATUS_ACCEPTED,
default => [
WorkOrder::STATUS_IN_PROGRESS,
WorkOrder::STATUS_IN_PROGRESS,
WorkOrder::STATUS_ACCEPTED,
WorkOrder::STATUS_PENDING,
][mt_rand(0, 3)],
};

WorkOrder::updateOrCreate(
['order_no' => sprintf('WO-2026-%04d', $n)],
[
'line_id' => $line->id,
'product_type_id' => $allPt[array_rand($allPt)]->id,
'planned_qty' => $qty,
'produced_qty' => match ($status) {
WorkOrder::STATUS_DONE => $qty,
WorkOrder::STATUS_IN_PROGRESS => intdiv($qty, 2),
default => 0,
},
'status' => $status,
'priority' => mt_rand(1, 5),
'due_date' => $end->copy()->addDays(mt_rand(0, 2)),
'planned_start_at' => $start,
'planned_end_at' => $end,
'description' => $descriptions[array_rand($descriptions)],
'completed_at' => $status === WorkOrder::STATUS_DONE ? $end : null,
]
);
Comment on lines +485 to +541

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Order numbering isn't idempotent across re-seeds — random skip decisions shift which slot each $n maps to.

$n only increments when the mt_rand(1,100) <= fill*100 gate passes (Line 496-498), and that gate is unseeded, so on a second run of this seeder the same $n/order_no can end up bound to a completely different (weekOffset, line, day, shift) slot than it was on the previous run. updateOrCreate will then overwrite that pre-existing order's line/product/quantities/status with data for an unrelated slot, and any slot count difference between runs leaves stale orders (higher $n) untouched from the prior run. Every other seeder method in this file keys updateOrCreate by a stable natural key (code, name, etc.) specifically to make re-seeding safe — this block breaks that invariant.

Derive the order_no key deterministically from the slot coordinates instead of a counter gated by randomness, so re-running the seeder is a true no-op/refresh for unchanged slots:

🛠️ Suggested deterministic keying
-        $weekStart = now()->startOfWeek();
-        $n = 100; // WO-2026-0100+, clear of the hand-authored WO-2026-001..010
+        $weekStart = now()->startOfWeek();
+        $weekOffsets = array_keys($weekFill);
+        $shiftCount = count($shifts);
+        $lineCount = count($allLines);
 
-        foreach ($weekFill as $weekOffset => $fill) {
-            foreach ($allLines as $line) {
-                for ($d = 0; $d < 7; $d++) {
-                    foreach ($shifts as $shift) {
+        foreach ($weekFill as $weekOffset => $fill) {
+            $weekIdx = array_search($weekOffset, $weekOffsets, true);
+            foreach ($allLines as $lineIdx => $line) {
+                for ($d = 0; $d < 7; $d++) {
+                    foreach ($shifts as $shiftIdx => $shift) {
                         if ($shift['night'] && (! in_array($line->code, $nightLines, true) || $d > 3)) {
                             continue;
                         }
                         if (mt_rand(1, 100) > (int) round($fill * 100)) {
                             continue;
                         }
 
-                        $n++;
+                        // Deterministic slot index — same (week, line, day, shift) always
+                        // maps to the same order_no, so re-seeding updates in place instead
+                        // of colliding with an unrelated slot's counter value.
+                        $n = 100 + ((($weekIdx * $lineCount + $lineIdx) * 7 + $d) * $shiftCount + $shiftIdx);
                         $start = $weekStart->copy()->addWeeks($weekOffset)->addDays($d)->setTime($shift['h'], 0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$weekStart = now()->startOfWeek();
$n = 100; // WO-2026-0100+, clear of the hand-authored WO-2026-001..010
foreach ($weekFill as $weekOffset => $fill) {
foreach ($allLines as $line) {
for ($d = 0; $d < 7; $d++) {
foreach ($shifts as $shift) {
// Night is DTG/SITO, Mon–Thu only — keep the board honest.
if ($shift['night'] && (! in_array($line->code, $nightLines, true) || $d > 3)) {
continue;
}
if (mt_rand(1, 100) > (int) round($fill * 100)) {
continue;
}
$n++;
$start = $weekStart->copy()->addWeeks($weekOffset)->addDays($d)->setTime($shift['h'], 0);
$end = $start->copy()->addHours(mt_rand(2, 7));
$qty = mt_rand(10, 200);
// Last week's work is DONE (drops off the active board);
// the current week stays ACTIVE end-to-end so every day
// renders blocks (a packed board), even the days already
// behind "today" (long / carried-over jobs); future weeks
// are still-to-start. DONE orders aren't drawn as blocks,
// so keeping the visible week active is what fills the grid.
$status = match (true) {
$weekOffset < 0 => WorkOrder::STATUS_DONE,
$weekOffset > 0 => mt_rand(0, 1) ? WorkOrder::STATUS_PENDING : WorkOrder::STATUS_ACCEPTED,
default => [
WorkOrder::STATUS_IN_PROGRESS,
WorkOrder::STATUS_IN_PROGRESS,
WorkOrder::STATUS_ACCEPTED,
WorkOrder::STATUS_PENDING,
][mt_rand(0, 3)],
};
WorkOrder::updateOrCreate(
['order_no' => sprintf('WO-2026-%04d', $n)],
[
'line_id' => $line->id,
'product_type_id' => $allPt[array_rand($allPt)]->id,
'planned_qty' => $qty,
'produced_qty' => match ($status) {
WorkOrder::STATUS_DONE => $qty,
WorkOrder::STATUS_IN_PROGRESS => intdiv($qty, 2),
default => 0,
},
'status' => $status,
'priority' => mt_rand(1, 5),
'due_date' => $end->copy()->addDays(mt_rand(0, 2)),
'planned_start_at' => $start,
'planned_end_at' => $end,
'description' => $descriptions[array_rand($descriptions)],
'completed_at' => $status === WorkOrder::STATUS_DONE ? $end : null,
]
);
$weekStart = now()->startOfWeek();
$weekOffsets = array_keys($weekFill);
$shiftCount = count($shifts);
$lineCount = count($allLines);
foreach ($weekFill as $weekOffset => $fill) {
$weekIdx = array_search($weekOffset, $weekOffsets, true);
foreach ($allLines as $lineIdx => $line) {
for ($d = 0; $d < 7; $d++) {
foreach ($shifts as $shiftIdx => $shift) {
// Night is DTG/SITO, Mon–Thu only — keep the board honest.
if ($shift['night'] && (! in_array($line->code, $nightLines, true) || $d > 3)) {
continue;
}
if (mt_rand(1, 100) > (int) round($fill * 100)) {
continue;
}
// Deterministic slot index — same (week, line, day, shift) always
// maps to the same order_no, so re-seeding updates in place instead
// of colliding with an unrelated slot's counter value.
$n = 100 + ((($weekIdx * $lineCount + $lineIdx) * 7 + $d) * $shiftCount + $shiftIdx);
$start = $weekStart->copy()->addWeeks($weekOffset)->addDays($d)->setTime($shift['h'], 0);
$end = $start->copy()->addHours(mt_rand(2, 7));
$qty = mt_rand(10, 200);
// Last week's work is DONE (drops off the active board);
// the current week stays ACTIVE end-to-end so every day
// renders blocks (a packed board), even the days already
// behind "today" (long / carried-over jobs); future weeks
// are still-to-start. DONE orders aren't drawn as blocks,
// so keeping the visible week active is what fills the grid.
$status = match (true) {
$weekOffset < 0 => WorkOrder::STATUS_DONE,
$weekOffset > 0 => mt_rand(0, 1) ? WorkOrder::STATUS_PENDING : WorkOrder::STATUS_ACCEPTED,
default => [
WorkOrder::STATUS_IN_PROGRESS,
WorkOrder::STATUS_IN_PROGRESS,
WorkOrder::STATUS_ACCEPTED,
WorkOrder::STATUS_PENDING,
][mt_rand(0, 3)],
};
WorkOrder::updateOrCreate(
['order_no' => sprintf('WO-2026-%04d', $n)],
[
'line_id' => $line->id,
'product_type_id' => $allPt[array_rand($allPt)]->id,
'planned_qty' => $qty,
'produced_qty' => match ($status) {
WorkOrder::STATUS_DONE => $qty,
WorkOrder::STATUS_IN_PROGRESS => intdiv($qty, 2),
default => 0,
},
'status' => $status,
'priority' => mt_rand(1, 5),
'due_date' => $end->copy()->addDays(mt_rand(0, 2)),
'planned_start_at' => $start,
'planned_end_at' => $end,
'description' => $descriptions[array_rand($descriptions)],
'completed_at' => $status === WorkOrder::STATUS_DONE ? $end : null,
]
);
🧰 Tools
🪛 ast-grep (0.44.1)

[error] 495-495: Avoid pseudo-random numbers
Context: mt_rand(1, 100)
Note: [CWE-338] Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG).

(no-pseudo-random-php)


[error] 501-501: Avoid pseudo-random numbers
Context: mt_rand(2, 7)
Note: [CWE-338] Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG).

(no-pseudo-random-php)


[error] 502-502: Avoid pseudo-random numbers
Context: mt_rand(10, 200)
Note: [CWE-338] Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG).

(no-pseudo-random-php)


[error] 512-512: Avoid pseudo-random numbers
Context: mt_rand(0, 1)
Note: [CWE-338] Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG).

(no-pseudo-random-php)


[error] 518-518: Avoid pseudo-random numbers
Context: mt_rand(0, 3)
Note: [CWE-338] Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG).

(no-pseudo-random-php)


[error] 533-533: Avoid pseudo-random numbers
Context: mt_rand(1, 5)
Note: [CWE-338] Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG).

(no-pseudo-random-php)


[error] 534-534: Avoid pseudo-random numbers
Context: mt_rand(0, 2)
Note: [CWE-338] Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG).

(no-pseudo-random-php)

🪛 PHPStan (2.2.5)

[error] 522-522: Call to an undefined static method App\Models\WorkOrder::updateOrCreate().

(staticMethod.notFound)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/database/seeders/PrintShopDemoSeeder.php` around lines 485 - 541,
Make the generated work-order key in the seeding loop deterministic from each
slot’s coordinates (weekOffset, line, day, and shift) instead of incrementing $n
only after the random fill gate. Update the updateOrCreate order_no construction
accordingly, preserving the existing fill randomness and status/data generation
so unchanged slots refresh the same records and skipped slots do not leave
counter-based stale orders.

}
}
}
}
}

Expand Down
Loading