Skip to content

Commit 59f2b67

Browse files
committed
Phases 201-205: Field Service Module — 18 tests passing
5 migrations (service_orders, service_order_items, service_checklists, service_checklist_items, service_order_checklist_results), 5 models (ServiceOrder with FS-YYYY-NNNNN numbering + start/complete/cancel workflow, ServiceOrderItem, ServiceChecklist/Item, ChecklistResult), FieldServicePolicy, 3 controllers (Dashboard/Order/Checklist), 6 React pages (Dashboard, Orders CRUD+Show with checklist, Checklists), Sidebar Field Service section. 18/18 feature tests passing. https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent 892d1b5 commit 59f2b67

26 files changed

Lines changed: 2386 additions & 0 deletions

erp/app/Models/User.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Illuminate\Contracts\Auth\MustVerifyEmail;
99
use Illuminate\Database\Eloquent\Factories\HasFactory;
1010
use Illuminate\Database\Eloquent\Relations\BelongsTo;
11+
use Illuminate\Database\Eloquent\Relations\HasMany;
1112
use Illuminate\Foundation\Auth\User as Authenticatable;
1213
use Illuminate\Notifications\Notifiable;
1314
use Spatie\Permission\Traits\HasRoles;
@@ -50,6 +51,11 @@ public function tenant(): BelongsTo
5051
return $this->belongsTo(Tenant::class);
5152
}
5253

54+
public function assignedServiceOrders(): HasMany
55+
{
56+
return $this->hasMany(\App\Modules\FieldService\Models\ServiceOrder::class, 'assigned_to');
57+
}
58+
5359
public function getInitialsAttribute(): string
5460
{
5561
return collect(explode(' ', $this->name))

erp/app/Modules/Core/Providers/CoreServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use App\Modules\Accounting\Providers\AccountingServiceProvider;
1818
use App\Modules\Fleet\Providers\FleetServiceProvider;
1919
use App\Modules\Marketing\Providers\MarketingServiceProvider;
20+
use App\Modules\FieldService\Providers\FieldServiceProvider;
2021
use Illuminate\Support\Facades\Gate;
2122
use Illuminate\Support\ServiceProvider;
2223

@@ -35,6 +36,7 @@ public function register(): void
3536
$this->app->register(AccountingServiceProvider::class);
3637
$this->app->register(FleetServiceProvider::class);
3738
$this->app->register(MarketingServiceProvider::class);
39+
$this->app->register(FieldServiceProvider::class);
3840
}
3941

4042
public function boot(): void
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace App\Modules\FieldService\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Models\User;
7+
use App\Modules\FieldService\Models\ServiceOrder;
8+
use Inertia\Inertia;
9+
use Inertia\Response;
10+
11+
class FieldServiceDashboardController extends Controller
12+
{
13+
public function index(): Response
14+
{
15+
$pendingCount = ServiceOrder::where('status', 'pending')->count();
16+
17+
$inProgressCount = ServiceOrder::where('status', 'in_progress')->count();
18+
19+
$completedTodayCount = ServiceOrder::where('status', 'completed')
20+
->whereDate('completed_at', today())
21+
->count();
22+
23+
$overdueCount = ServiceOrder::where('scheduled_at', '<', now())
24+
->whereNotIn('status', ['completed', 'cancelled'])
25+
->count();
26+
27+
// Orders by technician breakdown
28+
$technicianBreakdown = User::whereHas('assignedServiceOrders', function ($q) {
29+
$q->whereNotIn('status', ['completed', 'cancelled']);
30+
})
31+
->withCount(['assignedServiceOrders as active_orders_count' => function ($q) {
32+
$q->whereNotIn('status', ['completed', 'cancelled']);
33+
}])
34+
->orderByDesc('active_orders_count')
35+
->get(['id', 'name']);
36+
37+
$recentOrders = ServiceOrder::with('technician')
38+
->orderByDesc('created_at')
39+
->limit(10)
40+
->get();
41+
42+
return Inertia::render('FieldService/Dashboard', [
43+
'stats' => [
44+
'pending' => $pendingCount,
45+
'inProgress' => $inProgressCount,
46+
'completedToday' => $completedTodayCount,
47+
'overdue' => $overdueCount,
48+
],
49+
'technicianBreakdown' => $technicianBreakdown,
50+
'recentOrders' => $recentOrders,
51+
]);
52+
}
53+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
namespace App\Modules\FieldService\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\FieldService\Models\ServiceChecklist;
7+
use Illuminate\Http\RedirectResponse;
8+
use Illuminate\Http\Request;
9+
use Inertia\Inertia;
10+
use Inertia\Response;
11+
12+
class ServiceChecklistController extends Controller
13+
{
14+
public function index(): Response
15+
{
16+
$checklists = ServiceChecklist::withCount('items')
17+
->orderBy('name')
18+
->get();
19+
20+
return Inertia::render('FieldService/Checklists/Index', [
21+
'checklists' => $checklists,
22+
]);
23+
}
24+
25+
public function store(Request $request): RedirectResponse
26+
{
27+
$validated = $request->validate([
28+
'name' => 'required|string|max:255',
29+
'description' => 'nullable|string',
30+
'is_active' => 'boolean',
31+
'items' => 'nullable|array',
32+
'items.*.label' => 'required_with:items|string|max:255',
33+
'items.*.sequence' => 'nullable|integer|min:0',
34+
]);
35+
36+
$checklist = ServiceChecklist::create([
37+
'tenant_id' => auth()->user()->tenant_id,
38+
'name' => $validated['name'],
39+
'description' => $validated['description'] ?? null,
40+
'is_active' => $validated['is_active'] ?? true,
41+
]);
42+
43+
if (!empty($validated['items'])) {
44+
foreach ($validated['items'] as $index => $item) {
45+
$checklist->items()->create([
46+
'label' => $item['label'],
47+
'sequence' => $item['sequence'] ?? $index,
48+
]);
49+
}
50+
}
51+
52+
return redirect()->back()->with('success', 'Checklist created.');
53+
}
54+
55+
public function update(Request $request, ServiceChecklist $checklist): RedirectResponse
56+
{
57+
$validated = $request->validate([
58+
'name' => 'required|string|max:255',
59+
'description' => 'nullable|string',
60+
'is_active' => 'boolean',
61+
'items' => 'nullable|array',
62+
'items.*.label' => 'required_with:items|string|max:255',
63+
'items.*.sequence' => 'nullable|integer|min:0',
64+
]);
65+
66+
$checklist->update([
67+
'name' => $validated['name'],
68+
'description' => $validated['description'] ?? null,
69+
'is_active' => $validated['is_active'] ?? $checklist->is_active,
70+
]);
71+
72+
// Sync items
73+
$checklist->items()->delete();
74+
if (!empty($validated['items'])) {
75+
foreach ($validated['items'] as $index => $item) {
76+
$checklist->items()->create([
77+
'label' => $item['label'],
78+
'sequence' => $item['sequence'] ?? $index,
79+
]);
80+
}
81+
}
82+
83+
return redirect()->back()->with('success', 'Checklist updated.');
84+
}
85+
86+
public function destroy(ServiceChecklist $checklist): RedirectResponse
87+
{
88+
$checklist->delete();
89+
90+
return redirect()->back()->with('success', 'Checklist deleted.');
91+
}
92+
}

0 commit comments

Comments
 (0)