Skip to content

Commit 2d3f857

Browse files
committed
feat(inventory): Phase 136 — Inventory Quality Alerts
Adds full CRUD + lifecycle management for quality alerts in the Inventory module. Includes migration, model with accessors/methods, policy, controller, routes (investigate/resolve/close custom actions), ServiceProvider registration, React stubs, and Pest feature tests. https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent 939cc29 commit 2d3f857

11 files changed

Lines changed: 436 additions & 0 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\Inventory\Models\QualityAlert;
7+
use Illuminate\Http\RedirectResponse;
8+
use Illuminate\Http\Request;
9+
use Inertia\Inertia;
10+
use Inertia\Response;
11+
12+
class QualityAlertController extends Controller
13+
{
14+
public function index(Request $request): Response
15+
{
16+
$this->authorize('viewAny', QualityAlert::class);
17+
18+
$alerts = QualityAlert::with(['product'])
19+
->latest()
20+
->paginate(20)
21+
->withQueryString();
22+
23+
return Inertia::render('Inventory/QualityAlerts/Index', [
24+
'alerts' => $alerts,
25+
]);
26+
}
27+
28+
public function create(): Response
29+
{
30+
$this->authorize('create', QualityAlert::class);
31+
32+
return Inertia::render('Inventory/QualityAlerts/Create');
33+
}
34+
35+
public function store(Request $request): RedirectResponse
36+
{
37+
$this->authorize('create', QualityAlert::class);
38+
39+
$validated = $request->validate([
40+
'title' => 'required|string',
41+
'alert_type' => 'nullable|in:defect,contamination,non-conformance,recall,expiry',
42+
'severity' => 'nullable|in:low,medium,high,critical',
43+
'product_id' => 'nullable|exists:products,id',
44+
]);
45+
46+
QualityAlert::create([
47+
'tenant_id' => app('tenant')->id,
48+
'created_by' => auth()->id(),
49+
'reported_by' => auth()->id(),
50+
...$validated,
51+
]);
52+
53+
return redirect()->route('inventory.quality-alerts.index')
54+
->with('success', 'Quality alert created.');
55+
}
56+
57+
public function show(QualityAlert $qualityAlert): Response
58+
{
59+
$this->authorize('view', $qualityAlert);
60+
61+
$qualityAlert->load(['product']);
62+
63+
return Inertia::render('Inventory/QualityAlerts/Show', [
64+
'alert' => $qualityAlert,
65+
]);
66+
}
67+
68+
public function edit(QualityAlert $qualityAlert): Response
69+
{
70+
$this->authorize('update', $qualityAlert);
71+
72+
$qualityAlert->load(['product']);
73+
74+
return Inertia::render('Inventory/QualityAlerts/Edit', [
75+
'alert' => $qualityAlert,
76+
]);
77+
}
78+
79+
public function update(Request $request, QualityAlert $qualityAlert): RedirectResponse
80+
{
81+
$this->authorize('update', $qualityAlert);
82+
83+
$validated = $request->validate([
84+
'title' => 'required|string',
85+
'alert_type' => 'nullable|in:defect,contamination,non-conformance,recall,expiry',
86+
'severity' => 'nullable|in:low,medium,high,critical',
87+
'product_id' => 'nullable|exists:products,id',
88+
]);
89+
90+
$qualityAlert->update($validated);
91+
92+
return redirect()->route('inventory.quality-alerts.index')
93+
->with('success', 'Quality alert updated.');
94+
}
95+
96+
public function destroy(QualityAlert $qualityAlert): RedirectResponse
97+
{
98+
$this->authorize('delete', $qualityAlert);
99+
100+
$qualityAlert->delete();
101+
102+
return redirect()->route('inventory.quality-alerts.index')
103+
->with('success', 'Quality alert deleted.');
104+
}
105+
106+
public function investigate(QualityAlert $qualityAlert): RedirectResponse
107+
{
108+
$this->authorize('investigate', $qualityAlert);
109+
110+
$qualityAlert->investigate();
111+
112+
return redirect()->route('inventory.quality-alerts.index')
113+
->with('success', 'Quality alert is now under investigation.');
114+
}
115+
116+
public function resolve(Request $request, QualityAlert $qualityAlert): RedirectResponse
117+
{
118+
$this->authorize('resolve', $qualityAlert);
119+
120+
$data = $request->validate([
121+
'root_cause' => 'required|string',
122+
'corrective_action' => 'required|string',
123+
]);
124+
125+
$qualityAlert->resolve($data['root_cause'], $data['corrective_action']);
126+
127+
return redirect()->route('inventory.quality-alerts.index')
128+
->with('success', 'Quality alert resolved.');
129+
}
130+
131+
public function close(QualityAlert $qualityAlert): RedirectResponse
132+
{
133+
$this->authorize('close', $qualityAlert);
134+
135+
$qualityAlert->close();
136+
137+
return redirect()->route('inventory.quality-alerts.index')
138+
->with('success', 'Quality alert closed.');
139+
}
140+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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\SoftDeletes;
9+
10+
class QualityAlert extends Model
11+
{
12+
use BelongsToTenant;
13+
use SoftDeletes;
14+
15+
protected $fillable = [
16+
'tenant_id',
17+
'product_id',
18+
'alert_number',
19+
'title',
20+
'description',
21+
'alert_type',
22+
'severity',
23+
'status',
24+
'affected_quantity',
25+
'affected_batch',
26+
'root_cause',
27+
'corrective_action',
28+
'resolved_at',
29+
'reported_by',
30+
'assigned_to',
31+
'created_by',
32+
];
33+
34+
protected $attributes = [
35+
'status' => 'open',
36+
'severity' => 'medium',
37+
'alert_type' => 'defect',
38+
'affected_quantity' => 0,
39+
];
40+
41+
protected $casts = [
42+
'affected_quantity' => 'integer',
43+
'resolved_at' => 'datetime',
44+
];
45+
46+
public function product(): BelongsTo
47+
{
48+
return $this->belongsTo(Product::class);
49+
}
50+
51+
public function investigate(): void
52+
{
53+
$this->status = 'investigating';
54+
$this->save();
55+
}
56+
57+
public function resolve(string $rootCause, string $correctiveAction): void
58+
{
59+
$this->status = 'resolved';
60+
$this->root_cause = $rootCause;
61+
$this->corrective_action = $correctiveAction;
62+
$this->resolved_at = now();
63+
$this->save();
64+
}
65+
66+
public function close(): void
67+
{
68+
$this->status = 'closed';
69+
$this->save();
70+
}
71+
72+
public function generateAlertNumber(): string
73+
{
74+
return 'QA-' . date('Y') . '-' . str_pad((string) $this->id, 5, '0', STR_PAD_LEFT);
75+
}
76+
77+
public function getIsOpenAttribute(): bool
78+
{
79+
return $this->status === 'open';
80+
}
81+
82+
public function getIsCriticalAttribute(): bool
83+
{
84+
return $this->severity === 'critical';
85+
}
86+
87+
public function getIsResolvedAttribute(): bool
88+
{
89+
return $this->status === 'resolved' || $this->status === 'closed';
90+
}
91+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Policies;
4+
5+
use App\Models\User;
6+
7+
class QualityAlertPolicy
8+
{
9+
public function viewAny(User $user): bool
10+
{
11+
return $user->can('inventory.view');
12+
}
13+
14+
public function view(User $user): bool
15+
{
16+
return $user->can('inventory.view');
17+
}
18+
19+
public function create(User $user): bool
20+
{
21+
return $user->can('inventory.create');
22+
}
23+
24+
public function update(User $user): bool
25+
{
26+
return $user->can('inventory.create');
27+
}
28+
29+
public function investigate(User $user): bool
30+
{
31+
return $user->can('inventory.create');
32+
}
33+
34+
public function resolve(User $user): bool
35+
{
36+
return $user->can('inventory.create');
37+
}
38+
39+
public function close(User $user): bool
40+
{
41+
return $user->can('inventory.create');
42+
}
43+
44+
public function delete(User $user): bool
45+
{
46+
return $user->can('inventory.delete');
47+
}
48+
}

erp/app/Modules/Inventory/Providers/InventoryServiceProvider.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@
7474
use App\Modules\Inventory\Policies\ReorderRulePolicy;
7575
use App\Modules\Inventory\Models\SupplierScorecard;
7676
use App\Modules\Inventory\Policies\SupplierScorecardPolicy;
77+
use App\Modules\Inventory\Models\QualityAlert;
78+
use App\Modules\Inventory\Policies\QualityAlertPolicy;
7779
use Illuminate\Support\Facades\Gate;
7880
use Illuminate\Support\ServiceProvider;
7981

@@ -131,5 +133,6 @@ public function boot(): void
131133
Gate::policy(ProductBundleItem::class, ProductBundlePolicy::class);
132134
Gate::policy(ReorderRule::class, ReorderRulePolicy::class);
133135
Gate::policy(SupplierScorecard::class, SupplierScorecardPolicy::class);
136+
Gate::policy(QualityAlert::class, QualityAlertPolicy::class);
134137
}
135138
}

erp/app/Modules/Inventory/routes/inventory.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,3 +271,12 @@
271271
Route::post('supplier-scorecards/{supplier_scorecard}/publish', [SupplierScorecardController::class, 'publish'])->name('supplier-scorecards.publish');
272272
Route::resource('supplier-scorecards', SupplierScorecardController::class);
273273
});
274+
275+
// Quality Alerts
276+
use App\Modules\Inventory\Http\Controllers\QualityAlertController;
277+
Route::middleware(['web', 'auth', 'verified'])->prefix('inventory')->name('inventory.')->group(function () {
278+
Route::post('quality-alerts/{quality_alert}/investigate', [QualityAlertController::class, 'investigate'])->name('quality-alerts.investigate');
279+
Route::post('quality-alerts/{quality_alert}/resolve', [QualityAlertController::class, 'resolve'])->name('quality-alerts.resolve');
280+
Route::post('quality-alerts/{quality_alert}/close', [QualityAlertController::class, 'close'])->name('quality-alerts.close');
281+
Route::resource('quality-alerts', QualityAlertController::class);
282+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up(): void
10+
{
11+
Schema::dropIfExists('quality_alerts');
12+
Schema::create('quality_alerts', function (Blueprint $table) {
13+
$table->id();
14+
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
15+
$table->foreignId('product_id')->nullable()->constrained()->nullOnDelete();
16+
$table->string('alert_number')->nullable();
17+
$table->string('title');
18+
$table->text('description')->nullable();
19+
$table->string('alert_type')->default('defect'); // defect/contamination/non-conformance/recall/expiry
20+
$table->string('severity')->default('medium'); // low/medium/high/critical
21+
$table->string('status')->default('open'); // open/investigating/resolved/closed
22+
$table->integer('affected_quantity')->default(0);
23+
$table->string('affected_batch')->nullable();
24+
$table->text('root_cause')->nullable();
25+
$table->text('corrective_action')->nullable();
26+
$table->timestamp('resolved_at')->nullable();
27+
$table->foreignId('reported_by')->nullable()->constrained('users')->nullOnDelete();
28+
$table->foreignId('assigned_to')->nullable()->constrained('users')->nullOnDelete();
29+
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
30+
$table->timestamps();
31+
$table->softDeletes();
32+
});
33+
}
34+
35+
public function down(): void
36+
{
37+
Schema::dropIfExists('quality_alerts');
38+
}
39+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default function Create() { return <div>Create</div>; }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default function Edit() { return <div>Edit</div>; }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default function Index() { return <div>Quality Alerts</div>; }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default function Show() { return <div>Show</div>; }

0 commit comments

Comments
 (0)