Skip to content

Commit 22e6598

Browse files
committed
feat(inventory): Phase 50 — Asset Management
https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent 4d9bec1 commit 22e6598

18 files changed

Lines changed: 1423 additions & 0 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\HR\Models\Employee;
7+
use App\Modules\Inventory\Models\Asset;
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 AssetController extends Controller
15+
{
16+
public function index(Request $request): Response
17+
{
18+
$this->authorize('viewAny', Asset::class);
19+
20+
$assets = Asset::withCount('maintenances')
21+
->with('assignedEmployee')
22+
->when($request->status, fn ($q) => $q->where('status', $request->status))
23+
->orderBy('name')
24+
->paginate(15)
25+
->withQueryString();
26+
27+
return Inertia::render('Inventory/Assets/Index', [
28+
'assets' => $assets,
29+
'filters' => $request->only(['status']),
30+
]);
31+
}
32+
33+
public function create(): Response
34+
{
35+
$this->authorize('create', Asset::class);
36+
37+
$tenantId = auth()->user()->tenant_id;
38+
39+
return Inertia::render('Inventory/Assets/Create', [
40+
'employees' => Employee::where('tenant_id', $tenantId)
41+
->where('status', 'active')
42+
->orderBy('first_name')
43+
->get(['id', 'first_name', 'last_name']),
44+
]);
45+
}
46+
47+
public function store(Request $request): RedirectResponse
48+
{
49+
$this->authorize('create', Asset::class);
50+
51+
$validated = $request->validate([
52+
'name' => ['required', 'string', 'max:255'],
53+
'asset_code' => ['nullable', 'string', 'max:100'],
54+
'category' => ['nullable', 'string', 'max:100'],
55+
'location' => ['nullable', 'string', 'max:255'],
56+
'purchase_date' => ['nullable', 'date'],
57+
'purchase_cost' => ['nullable', 'numeric', 'min:0'],
58+
'current_value' => ['nullable', 'numeric', 'min:0'],
59+
'serial_number' => ['nullable', 'string', 'max:100'],
60+
'status' => ['required', Rule::in(['active', 'inactive', 'disposed', 'under_maintenance'])],
61+
'notes' => ['nullable', 'string'],
62+
]);
63+
64+
$asset = Asset::create([...$validated, 'tenant_id' => auth()->user()->tenant_id]);
65+
66+
return redirect()->route('inventory.assets.show', $asset)
67+
->with('success', 'Asset created successfully.');
68+
}
69+
70+
public function show(Asset $asset): Response
71+
{
72+
$this->authorize('view', $asset);
73+
74+
$asset->load('assignedEmployee');
75+
$asset->setRelation('maintenances', $asset->maintenances()->latest('scheduled_date')->get());
76+
77+
return Inertia::render('Inventory/Assets/Show', [
78+
'asset' => $asset->append('depreciation'),
79+
'employees' => Employee::where('tenant_id', auth()->user()->tenant_id)
80+
->where('status', 'active')
81+
->orderBy('first_name')
82+
->get(['id', 'first_name', 'last_name']),
83+
]);
84+
}
85+
86+
public function destroy(Asset $asset): RedirectResponse
87+
{
88+
$this->authorize('delete', $asset);
89+
90+
$asset->delete();
91+
92+
return redirect()->route('inventory.assets.index')
93+
->with('success', 'Asset deleted.');
94+
}
95+
96+
public function dispose(Asset $asset): RedirectResponse
97+
{
98+
$this->authorize('delete', $asset);
99+
100+
$asset->dispose();
101+
102+
return redirect()->back()
103+
->with('success', 'Asset disposed.');
104+
}
105+
106+
public function assign(Request $request, Asset $asset): RedirectResponse
107+
{
108+
$this->authorize('update', $asset);
109+
110+
$validated = $request->validate([
111+
'employee_id' => ['required', 'integer', Rule::exists('employees', 'id')],
112+
]);
113+
114+
$asset->assignTo($validated['employee_id']);
115+
116+
return redirect()->back()
117+
->with('success', 'Asset assigned.');
118+
}
119+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\Inventory\Models\Asset;
7+
use App\Modules\Inventory\Models\AssetMaintenance;
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 AssetMaintenanceController extends Controller
15+
{
16+
public function index(Request $request): Response
17+
{
18+
$this->authorize('viewAny', AssetMaintenance::class);
19+
20+
$maintenances = AssetMaintenance::with('asset')
21+
->when($request->asset_id, fn ($q) => $q->where('asset_id', $request->asset_id))
22+
->orderBy('scheduled_date', 'desc')
23+
->paginate(15)
24+
->withQueryString();
25+
26+
return Inertia::render('Inventory/AssetMaintenances/Index', [
27+
'maintenances' => $maintenances,
28+
'filters' => $request->only(['asset_id']),
29+
]);
30+
}
31+
32+
public function create(): Response
33+
{
34+
$this->authorize('create', AssetMaintenance::class);
35+
36+
return Inertia::render('Inventory/AssetMaintenances/Create', [
37+
'assets' => Asset::orderBy('name')->get(['id', 'name']),
38+
]);
39+
}
40+
41+
public function store(Request $request): RedirectResponse
42+
{
43+
$this->authorize('create', AssetMaintenance::class);
44+
45+
$validated = $request->validate([
46+
'asset_id' => ['required', Rule::exists('assets', 'id')],
47+
'scheduled_date' => ['required', 'date'],
48+
'type' => ['required', Rule::in(['routine', 'repair', 'inspection', 'calibration'])],
49+
'description' => ['nullable', 'string'],
50+
'cost' => ['nullable', 'numeric', 'min:0'],
51+
'performed_by' => ['nullable', 'string', 'max:255'],
52+
]);
53+
54+
$maintenance = AssetMaintenance::create([...$validated, 'tenant_id' => auth()->user()->tenant_id]);
55+
56+
return redirect()->route('inventory.asset-maintenances.show', $maintenance)
57+
->with('success', 'Maintenance scheduled successfully.');
58+
}
59+
60+
public function show(AssetMaintenance $assetMaintenance): Response
61+
{
62+
$this->authorize('view', $assetMaintenance);
63+
64+
$assetMaintenance->load('asset');
65+
66+
return Inertia::render('Inventory/AssetMaintenances/Show', [
67+
'maintenance' => $assetMaintenance,
68+
]);
69+
}
70+
71+
public function destroy(AssetMaintenance $assetMaintenance): RedirectResponse
72+
{
73+
$this->authorize('delete', $assetMaintenance);
74+
75+
$assetMaintenance->delete();
76+
77+
return redirect()->route('inventory.asset-maintenances.index')
78+
->with('success', 'Maintenance record deleted.');
79+
}
80+
81+
public function complete(Request $request, AssetMaintenance $assetMaintenance): RedirectResponse
82+
{
83+
$this->authorize('update', $assetMaintenance);
84+
85+
$validated = $request->validate([
86+
'completed_date' => ['required', 'date'],
87+
'cost' => ['nullable', 'numeric', 'min:0'],
88+
]);
89+
90+
$assetMaintenance->complete(
91+
$validated['completed_date'],
92+
isset($validated['cost']) ? (float) $validated['cost'] : null
93+
);
94+
95+
return redirect()->back()
96+
->with('success', 'Maintenance completed.');
97+
}
98+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Models;
4+
5+
use App\Modules\Core\Traits\BelongsToTenant;
6+
use App\Modules\HR\Models\Employee;
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 Asset extends Model
13+
{
14+
use BelongsToTenant;
15+
use SoftDeletes;
16+
17+
protected $fillable = [
18+
'tenant_id', 'name', 'asset_code', 'category', 'location',
19+
'assigned_to_employee_id', 'purchase_date', 'purchase_cost',
20+
'current_value', 'status', 'serial_number', 'notes', 'disposed_at',
21+
];
22+
23+
protected $casts = [
24+
'purchase_date' => 'date',
25+
'disposed_at' => 'datetime',
26+
'purchase_cost' => 'decimal:2',
27+
'current_value' => 'decimal:2',
28+
];
29+
30+
public function assignedEmployee(): BelongsTo
31+
{
32+
return $this->belongsTo(Employee::class, 'assigned_to_employee_id');
33+
}
34+
35+
public function maintenances(): HasMany
36+
{
37+
return $this->hasMany(AssetMaintenance::class);
38+
}
39+
40+
public function getDepreciationAttribute(): ?float
41+
{
42+
if ($this->purchase_cost !== null && $this->current_value !== null) {
43+
return round((float) $this->purchase_cost - (float) $this->current_value, 2);
44+
}
45+
46+
return null;
47+
}
48+
49+
public function dispose(): void
50+
{
51+
$this->status = 'disposed';
52+
$this->disposed_at = now();
53+
$this->save();
54+
}
55+
56+
public function assignTo(int $employeeId): void
57+
{
58+
$this->assigned_to_employee_id = $employeeId;
59+
$this->status = 'active';
60+
$this->save();
61+
}
62+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 AssetMaintenance extends Model
11+
{
12+
use BelongsToTenant;
13+
use SoftDeletes;
14+
15+
protected $fillable = [
16+
'tenant_id', 'asset_id', 'scheduled_date', 'completed_date',
17+
'type', 'description', 'cost', 'performed_by', 'status',
18+
];
19+
20+
protected $casts = [
21+
'scheduled_date' => 'date',
22+
'completed_date' => 'date',
23+
'cost' => 'decimal:2',
24+
];
25+
26+
public function asset(): BelongsTo
27+
{
28+
return $this->belongsTo(Asset::class);
29+
}
30+
31+
public function complete(string $completedDate, ?float $cost = null): void
32+
{
33+
$this->status = 'completed';
34+
$this->completed_date = $completedDate;
35+
if ($cost !== null) {
36+
$this->cost = $cost;
37+
}
38+
$this->save();
39+
}
40+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Policies;
4+
5+
use App\Models\User;
6+
7+
class AssetPolicy
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 delete(User $user): bool
30+
{
31+
return $user->can('inventory.delete');
32+
}
33+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
namespace App\Modules\Inventory\Providers;
44

5+
use App\Modules\Inventory\Models\Asset;
6+
use App\Modules\Inventory\Models\AssetMaintenance;
57
use App\Modules\Inventory\Models\Product;
68
use App\Modules\Inventory\Models\ProductCategory;
79
use App\Modules\Inventory\Models\PurchaseRequisition;
810
use App\Modules\Inventory\Models\StockAdjustment;
911
use App\Modules\Inventory\Models\WarehouseTransfer;
12+
use App\Modules\Inventory\Policies\AssetPolicy;
1013
use App\Modules\Inventory\Policies\ProductCategoryPolicy;
1114
use App\Modules\Inventory\Policies\ProductPolicy;
1215
use App\Modules\Inventory\Policies\PurchaseRequisitionPolicy;
@@ -28,5 +31,7 @@ public function boot(): void
2831
Gate::policy(WarehouseTransfer::class, WarehouseTransferPolicy::class);
2932
Gate::policy(StockAdjustment::class, StockAdjustmentPolicy::class);
3033
Gate::policy(PurchaseRequisition::class, PurchaseRequisitionPolicy::class);
34+
Gate::policy(Asset::class, AssetPolicy::class);
35+
Gate::policy(AssetMaintenance::class, AssetPolicy::class);
3136
}
3237
}

0 commit comments

Comments
 (0)