feat(demo): pack the production planner with sample data#206
Conversation
📝 WalkthroughWalkthroughThe demo seeder now densely populates the current planner week with shift-based work orders, tapers generation in neighboring weeks, restricts night shifts by line and weekday, and derives timestamps and completion data from each order’s scheduled week. ChangesPlanner seeding
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with 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.
Inline comments:
In `@backend/database/seeders/PrintShopDemoSeeder.php`:
- Around line 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.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e07b5edd-70e4-4c07-bae1-cd336d851f89
📒 Files selected for processing (2)
CHANGELOG.mdbackend/database/seeders/PrintShopDemoSeeder.php
| $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, | ||
| ] | ||
| ); |
There was a problem hiding this comment.
🗄️ 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.
| $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.
What
The "Load sample data" demo previously left the production planner nearly empty on any given week: the seeder scattered ~46 work orders one-every-two-days across a 3-month horizon, so a single week had ~9 orders spread over 5 lines × 7 days × 3 shifts.
Change
PrintShopDemoSeedernow densely fills the current week (an order in most line × day × shift slots, anchored tonow()->startOfWeek()so the visible week is always the busy one), with a lighter taper over neighbouring weeks and last week marked DONE. The current week stays active end-to-end (in-progress / accepted / pending) so every day renders blocks — including days already behind "today" (DONE orders aren't drawn as planner blocks).Result (verified on a fresh local install)
Seeder-only; no schema or app-logic change. Full suite green (1922).
🤖 Generated with claude-flow
Summary by CodeRabbit