Skip to content

Commit 0f051a9

Browse files
committed
feat(inventory): Phase 64 — Quality Control with checklists and inspections
https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent fc7e10f commit 0f051a9

22 files changed

Lines changed: 1388 additions & 0 deletions
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\Inventory\Models\Product;
7+
use App\Modules\Inventory\Models\QcChecklist;
8+
use Illuminate\Http\RedirectResponse;
9+
use Illuminate\Http\Request;
10+
use Illuminate\Validation\Rule;
11+
use Inertia\Inertia;
12+
use Inertia\Response;
13+
14+
class QcChecklistController extends Controller
15+
{
16+
public function index(): Response
17+
{
18+
$this->authorize('viewAny', QcChecklist::class);
19+
20+
$checklists = QcChecklist::with('product')
21+
->withCount('items')
22+
->orderByDesc('created_at')
23+
->paginate(15);
24+
25+
return Inertia::render('Inventory/QcChecklists/Index', compact('checklists'));
26+
}
27+
28+
public function create(): Response
29+
{
30+
$this->authorize('create', QcChecklist::class);
31+
32+
$products = Product::orderBy('name')->get(['id', 'name', 'sku']);
33+
34+
return Inertia::render('Inventory/QcChecklists/Create', compact('products'));
35+
}
36+
37+
public function store(Request $request): RedirectResponse
38+
{
39+
$this->authorize('create', QcChecklist::class);
40+
41+
$validated = $request->validate([
42+
'name' => ['required', 'string', 'max:255'],
43+
'product_id' => ['nullable', Rule::exists('products', 'id')],
44+
'description' => ['nullable', 'string'],
45+
'items' => ['required', 'array', 'min:1'],
46+
'items.*.name' => ['required', 'string', 'max:255'],
47+
'items.*.is_required' => ['boolean'],
48+
'items.*.sort_order' => ['nullable', 'integer', 'min:0'],
49+
]);
50+
51+
$tenantId = auth()->user()->tenant_id;
52+
53+
$checklist = QcChecklist::create([
54+
'tenant_id' => $tenantId,
55+
'name' => $validated['name'],
56+
'product_id' => $validated['product_id'] ?? null,
57+
'description' => $validated['description'] ?? null,
58+
'is_active' => true,
59+
]);
60+
61+
foreach ($validated['items'] as $item) {
62+
$checklist->items()->create([
63+
'tenant_id' => $tenantId,
64+
'name' => $item['name'],
65+
'is_required' => $item['is_required'] ?? true,
66+
'sort_order' => $item['sort_order'] ?? 0,
67+
]);
68+
}
69+
70+
return redirect()->route('inventory.qc-checklists.show', $checklist)
71+
->with('success', 'Checklist created successfully.');
72+
}
73+
74+
public function show(QcChecklist $qcChecklist): Response
75+
{
76+
$this->authorize('view', $qcChecklist);
77+
78+
$qcChecklist->load(['product', 'items']);
79+
80+
return Inertia::render('Inventory/QcChecklists/Show', [
81+
'checklist' => $qcChecklist,
82+
]);
83+
}
84+
85+
public function destroy(QcChecklist $qcChecklist): RedirectResponse
86+
{
87+
$this->authorize('delete', $qcChecklist);
88+
89+
$qcChecklist->delete();
90+
91+
return redirect()->route('inventory.qc-checklists.index')
92+
->with('success', 'Checklist deleted.');
93+
}
94+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\Inventory\Models\Product;
7+
use App\Modules\Inventory\Models\QcChecklist;
8+
use App\Modules\Inventory\Models\QcInspection;
9+
use App\Modules\Inventory\Models\QcInspectionResult;
10+
use Illuminate\Http\RedirectResponse;
11+
use Illuminate\Http\Request;
12+
use Illuminate\Validation\Rule;
13+
use Inertia\Inertia;
14+
use Inertia\Response;
15+
16+
class QcInspectionController extends Controller
17+
{
18+
public function index(Request $request): Response
19+
{
20+
$this->authorize('viewAny', QcInspection::class);
21+
22+
$inspections = QcInspection::with(['checklist', 'product', 'inspector'])
23+
->when($request->status, fn ($q) => $q->where('status', $request->status))
24+
->orderByDesc('created_at')
25+
->paginate(15)
26+
->withQueryString();
27+
28+
return Inertia::render('Inventory/QcInspections/Index', [
29+
'inspections' => $inspections,
30+
'filters' => $request->only(['status']),
31+
]);
32+
}
33+
34+
public function create(): Response
35+
{
36+
$this->authorize('create', QcInspection::class);
37+
38+
$checklists = QcChecklist::where('is_active', true)->orderBy('name')->get(['id', 'name']);
39+
$products = Product::orderBy('name')->get(['id', 'name', 'sku']);
40+
41+
return Inertia::render('Inventory/QcInspections/Create', compact('checklists', 'products'));
42+
}
43+
44+
public function store(Request $request): RedirectResponse
45+
{
46+
$this->authorize('create', QcInspection::class);
47+
48+
$validated = $request->validate([
49+
'qc_checklist_id' => ['required', Rule::exists('qc_checklists', 'id')],
50+
'product_id' => ['nullable', Rule::exists('products', 'id')],
51+
'batch_reference' => ['nullable', 'string', 'max:100'],
52+
'notes' => ['nullable', 'string'],
53+
]);
54+
55+
$tenantId = auth()->user()->tenant_id;
56+
57+
$inspection = QcInspection::create([
58+
'tenant_id' => $tenantId,
59+
'qc_checklist_id' => $validated['qc_checklist_id'],
60+
'product_id' => $validated['product_id'] ?? null,
61+
'batch_reference' => $validated['batch_reference'] ?? null,
62+
'notes' => $validated['notes'] ?? null,
63+
'status' => 'pending',
64+
]);
65+
66+
$checklist = QcChecklist::with('items')->find($validated['qc_checklist_id']);
67+
foreach ($checklist->items as $item) {
68+
QcInspectionResult::create([
69+
'tenant_id' => $tenantId,
70+
'qc_inspection_id' => $inspection->id,
71+
'qc_checklist_item_id' => $item->id,
72+
'result' => 'na',
73+
]);
74+
}
75+
76+
return redirect()->route('inventory.qc-inspections.show', $inspection)
77+
->with('success', 'Inspection created successfully.');
78+
}
79+
80+
public function show(QcInspection $qcInspection): Response
81+
{
82+
$this->authorize('view', $qcInspection);
83+
84+
$qcInspection->load(['checklist.items', 'results.checklistItem', 'product', 'inspector']);
85+
86+
return Inertia::render('Inventory/QcInspections/Show', [
87+
'inspection' => $qcInspection->append('pass_rate'),
88+
]);
89+
}
90+
91+
public function destroy(QcInspection $qcInspection): RedirectResponse
92+
{
93+
$this->authorize('delete', $qcInspection);
94+
95+
$qcInspection->delete();
96+
97+
return redirect()->route('inventory.qc-inspections.index')
98+
->with('success', 'Inspection deleted.');
99+
}
100+
101+
public function updateResult(Request $request, QcInspection $qcInspection, QcInspectionResult $result): RedirectResponse
102+
{
103+
$this->authorize('create', $qcInspection);
104+
105+
$validated = $request->validate([
106+
'result' => ['required', Rule::in(['pass', 'fail', 'na'])],
107+
'notes' => ['nullable', 'string'],
108+
]);
109+
110+
$result->update($validated);
111+
112+
return redirect()->back()->with('success', 'Result updated.');
113+
}
114+
115+
public function complete(Request $request, QcInspection $qcInspection): RedirectResponse
116+
{
117+
$this->authorize('create', $qcInspection);
118+
119+
$validated = $request->validate([
120+
'overall_result' => ['required', Rule::in(['pass', 'fail', 'conditional'])],
121+
]);
122+
123+
$qcInspection->complete($validated['overall_result']);
124+
125+
return redirect()->back()->with('success', 'Inspection completed.');
126+
}
127+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Models;
4+
5+
use App\Modules\Core\Traits\BelongsToTenant;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
8+
use Illuminate\Database\Eloquent\Relations\HasMany;
9+
use Illuminate\Database\Eloquent\SoftDeletes;
10+
11+
class QcChecklist extends Model
12+
{
13+
use BelongsToTenant;
14+
use SoftDeletes;
15+
16+
protected $fillable = [
17+
'tenant_id',
18+
'name',
19+
'product_id',
20+
'description',
21+
'is_active',
22+
];
23+
24+
protected $casts = [
25+
'is_active' => 'boolean',
26+
];
27+
28+
public function product(): BelongsTo
29+
{
30+
return $this->belongsTo(Product::class);
31+
}
32+
33+
public function items(): HasMany
34+
{
35+
return $this->hasMany(QcChecklistItem::class)->orderBy('sort_order');
36+
}
37+
38+
public function inspections(): HasMany
39+
{
40+
return $this->hasMany(QcInspection::class);
41+
}
42+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Models;
4+
5+
use App\Modules\Core\Traits\BelongsToTenant;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
8+
9+
class QcChecklistItem extends Model
10+
{
11+
use BelongsToTenant;
12+
13+
protected $fillable = [
14+
'tenant_id',
15+
'qc_checklist_id',
16+
'name',
17+
'description',
18+
'is_required',
19+
'sort_order',
20+
];
21+
22+
protected $casts = [
23+
'is_required' => 'boolean',
24+
];
25+
26+
public function checklist(): BelongsTo
27+
{
28+
return $this->belongsTo(QcChecklist::class);
29+
}
30+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Models;
4+
5+
use App\Models\User;
6+
use App\Modules\Core\Traits\BelongsToTenant;
7+
use Illuminate\Database\Eloquent\Model;
8+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
9+
use Illuminate\Database\Eloquent\Relations\HasMany;
10+
use Illuminate\Database\Eloquent\SoftDeletes;
11+
12+
class QcInspection extends Model
13+
{
14+
use BelongsToTenant;
15+
use SoftDeletes;
16+
17+
protected $fillable = [
18+
'tenant_id',
19+
'qc_checklist_id',
20+
'product_id',
21+
'inspector_id',
22+
'batch_reference',
23+
'status',
24+
'overall_result',
25+
'notes',
26+
'inspected_at',
27+
];
28+
29+
protected $casts = [
30+
'inspected_at' => 'datetime',
31+
];
32+
33+
public function checklist(): BelongsTo
34+
{
35+
return $this->belongsTo(QcChecklist::class, 'qc_checklist_id');
36+
}
37+
38+
public function product(): BelongsTo
39+
{
40+
return $this->belongsTo(Product::class);
41+
}
42+
43+
public function inspector(): BelongsTo
44+
{
45+
return $this->belongsTo(User::class, 'inspector_id');
46+
}
47+
48+
public function results(): HasMany
49+
{
50+
return $this->hasMany(QcInspectionResult::class);
51+
}
52+
53+
public function getPassRateAttribute(): ?float
54+
{
55+
$results = $this->results;
56+
if ($results->isEmpty()) {
57+
return null;
58+
}
59+
60+
return round($results->where('result', 'pass')->count() / $results->count() * 100, 1);
61+
}
62+
63+
public function complete(string $overallResult): void
64+
{
65+
$this->status = $overallResult === 'pass' ? 'passed' : 'failed';
66+
$this->overall_result = $overallResult;
67+
$this->inspected_at = now();
68+
$this->inspector_id = auth()->id();
69+
$this->save();
70+
}
71+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Models;
4+
5+
use App\Modules\Core\Traits\BelongsToTenant;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
8+
9+
class QcInspectionResult extends Model
10+
{
11+
use BelongsToTenant;
12+
13+
protected $fillable = [
14+
'tenant_id',
15+
'qc_inspection_id',
16+
'qc_checklist_item_id',
17+
'result',
18+
'notes',
19+
];
20+
21+
public function inspection(): BelongsTo
22+
{
23+
return $this->belongsTo(QcInspection::class, 'qc_inspection_id');
24+
}
25+
26+
public function checklistItem(): BelongsTo
27+
{
28+
return $this->belongsTo(QcChecklistItem::class, 'qc_checklist_item_id');
29+
}
30+
}

0 commit comments

Comments
 (0)