Skip to content

Commit dbb4faa

Browse files
committed
feat(phase-6): add Repairs, Live Chat, Social Marketing, and Frontdesk modules
Completes Phase 6 with 4 new ERP modules matching Odoo 19 feature parity: - Repairs: repair orders + lines, lifecycle (draft→confirmed→in_progress→done/cancelled), parts/labor tracking, 12 tests - Live Chat: channels, visitor sessions, messages, public widget API, agent assignment, 10 tests - Social Marketing: social accounts (multi-platform), post scheduling, metrics tracking, 10 tests - Frontdesk: stations, visitor check-in/check-out, pre-registration, badge generation, 10 tests All 2237 tests pass (6803 assertions). Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8b1196d commit dbb4faa

51 files changed

Lines changed: 5185 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
use App\Modules\Planning\Providers\PlanningServiceProvider;
3232
use App\Modules\Sign\Providers\SignServiceProvider;
3333
use App\Modules\Maintenance\Providers\MaintenanceServiceProvider;
34+
use App\Modules\LiveChat\Providers\LiveChatServiceProvider;
35+
use App\Modules\Repairs\Providers\RepairsServiceProvider;
36+
use App\Modules\SocialMarketing\Providers\SocialMarketingServiceProvider;
37+
use App\Modules\Frontdesk\Providers\FrontdeskServiceProvider;
3438
use Illuminate\Support\Facades\Gate;
3539
use Illuminate\Support\ServiceProvider;
3640

@@ -63,6 +67,10 @@ public function register(): void
6367
$this->app->register(PlanningServiceProvider::class);
6468
$this->app->register(SignServiceProvider::class);
6569
$this->app->register(MaintenanceServiceProvider::class);
70+
$this->app->register(LiveChatServiceProvider::class);
71+
$this->app->register(RepairsServiceProvider::class);
72+
$this->app->register(SocialMarketingServiceProvider::class);
73+
$this->app->register(FrontdeskServiceProvider::class);
6674
}
6775

6876
public function boot(): void
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<?php
2+
3+
namespace App\Modules\Frontdesk\Http\Controllers;
4+
5+
use App\Models\User;
6+
use App\Modules\Frontdesk\Models\FrontdeskStation;
7+
use App\Modules\Frontdesk\Models\VisitorLog;
8+
use Illuminate\Http\RedirectResponse;
9+
use Illuminate\Http\Request;
10+
use Illuminate\Routing\Controller;
11+
use Inertia\Inertia;
12+
use Inertia\Response;
13+
14+
class FrontdeskController extends Controller
15+
{
16+
public function dashboard(): Response
17+
{
18+
$today = now()->startOfDay();
19+
20+
$stats = [
21+
'currently_in' => VisitorLog::where('status', 'checked_in')->count(),
22+
'expected_today' => VisitorLog::where('status', 'expected')
23+
->whereDate('expected_at', today())
24+
->count(),
25+
'checked_in_today' => VisitorLog::where('status', 'checked_in')
26+
->whereDate('check_in_at', today())
27+
->count(),
28+
'checked_out_today' => VisitorLog::where('status', 'checked_out')
29+
->whereDate('check_out_at', today())
30+
->count(),
31+
'no_shows_today' => VisitorLog::where('status', 'no_show')
32+
->whereDate('updated_at', today())
33+
->count(),
34+
'stations_count' => FrontdeskStation::where('is_active', true)->count(),
35+
];
36+
37+
$recentVisitors = VisitorLog::with(['station', 'host'])
38+
->orderByDesc('check_in_at')
39+
->limit(10)
40+
->get();
41+
42+
return Inertia::render('Frontdesk/Dashboard', [
43+
'stats' => $stats,
44+
'recentVisitors' => $recentVisitors,
45+
]);
46+
}
47+
48+
public function stations(): Response
49+
{
50+
$stations = FrontdeskStation::with('responsible')->get();
51+
52+
return Inertia::render('Frontdesk/Stations/Index', [
53+
'stations' => $stations,
54+
]);
55+
}
56+
57+
public function storeStation(Request $request): RedirectResponse
58+
{
59+
$data = $request->validate([
60+
'name' => 'required|string|max:255',
61+
'location' => 'nullable|string|max:255',
62+
'responsible_id' => 'nullable|exists:users,id',
63+
]);
64+
65+
FrontdeskStation::create($data);
66+
67+
return redirect()->back()->with('success', 'Station created successfully.');
68+
}
69+
70+
public function visitors(Request $request): Response
71+
{
72+
$date = $request->input('date', today()->toDateString());
73+
$status = $request->input('status');
74+
75+
$query = VisitorLog::with(['station', 'host'])
76+
->whereDate('created_at', $date);
77+
78+
if ($status) {
79+
$query->where('status', $status);
80+
}
81+
82+
$visitors = $query->orderByDesc('created_at')->paginate(20);
83+
84+
return Inertia::render('Frontdesk/Visitors/Index', [
85+
'visitors' => $visitors,
86+
'filters' => ['date' => $date, 'status' => $status],
87+
]);
88+
}
89+
90+
public function checkIn(Request $request): Response|RedirectResponse
91+
{
92+
if ($request->isMethod('POST')) {
93+
$data = $request->validate([
94+
'visitor_name' => 'required|string|max:255',
95+
'visitor_email' => 'nullable|email|max:255',
96+
'visitor_phone' => 'nullable|string|max:50',
97+
'visitor_company' => 'nullable|string|max:255',
98+
'visit_purpose' => 'nullable|string|max:255',
99+
'host_employee_id' => 'nullable|exists:users,id',
100+
'station_id' => 'nullable|exists:frontdesk_stations,id',
101+
'expected_at' => 'nullable|date',
102+
'badge_number' => 'nullable|string|max:50',
103+
]);
104+
105+
$visitor = VisitorLog::create(array_merge($data, [
106+
'status' => 'checked_in',
107+
'check_in_at' => now(),
108+
]));
109+
110+
return redirect()->route('frontdesk.visitors')->with('success', 'Visitor checked in successfully.');
111+
}
112+
113+
$stations = FrontdeskStation::where('is_active', true)->get();
114+
$expectedToday = VisitorLog::where('status', 'expected')
115+
->whereDate('expected_at', today())
116+
->with(['host', 'station'])
117+
->get();
118+
119+
return Inertia::render('Frontdesk/CheckIn', [
120+
'stations' => $stations,
121+
'expectedToday' => $expectedToday,
122+
'users' => User::orderBy('name')->get(['id', 'name']),
123+
]);
124+
}
125+
126+
public function doCheckOut(VisitorLog $visitor): RedirectResponse
127+
{
128+
$visitor->checkOut();
129+
130+
return redirect()->back()->with('success', 'Visitor checked out successfully.');
131+
}
132+
133+
public function markNoShow(VisitorLog $visitor): RedirectResponse
134+
{
135+
$visitor->markNoShow();
136+
137+
return redirect()->back();
138+
}
139+
140+
public function preRegister(Request $request): RedirectResponse
141+
{
142+
$data = $request->validate([
143+
'visitor_name' => 'required|string|max:255',
144+
'visitor_email' => 'nullable|email|max:255',
145+
'visitor_phone' => 'nullable|string|max:50',
146+
'visitor_company' => 'nullable|string|max:255',
147+
'visit_purpose' => 'nullable|string|max:255',
148+
'host_employee_id' => 'nullable|exists:users,id',
149+
'station_id' => 'nullable|exists:frontdesk_stations,id',
150+
'expected_at' => 'nullable|date',
151+
'badge_number' => 'nullable|string|max:50',
152+
]);
153+
154+
VisitorLog::create(array_merge($data, [
155+
'status' => 'expected',
156+
]));
157+
158+
return redirect()->route('frontdesk.visitors')->with('success', 'Visitor pre-registered successfully.');
159+
}
160+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace App\Modules\Frontdesk\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+
11+
class FrontdeskStation extends Model
12+
{
13+
use BelongsToTenant;
14+
15+
protected $table = 'frontdesk_stations';
16+
17+
protected $fillable = [
18+
'tenant_id',
19+
'name',
20+
'location',
21+
'is_active',
22+
'responsible_id',
23+
];
24+
25+
protected $casts = [
26+
'is_active' => 'boolean',
27+
];
28+
29+
public function visitors(): HasMany
30+
{
31+
return $this->hasMany(VisitorLog::class, 'station_id');
32+
}
33+
34+
public function responsible(): BelongsTo
35+
{
36+
return $this->belongsTo(User::class, 'responsible_id');
37+
}
38+
39+
public function checkedInCount(): int
40+
{
41+
return $this->visitors()->where('status', 'checked_in')->count();
42+
}
43+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace App\Modules\Frontdesk\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+
10+
class VisitorLog extends Model
11+
{
12+
use BelongsToTenant;
13+
14+
protected $table = 'visitor_logs';
15+
16+
protected $fillable = [
17+
'tenant_id',
18+
'station_id',
19+
'visitor_name',
20+
'visitor_email',
21+
'visitor_phone',
22+
'visitor_company',
23+
'visit_purpose',
24+
'host_employee_id',
25+
'badge_number',
26+
'status',
27+
'expected_at',
28+
'check_in_at',
29+
'check_out_at',
30+
'visitor_photo_path',
31+
'notes',
32+
];
33+
34+
protected $casts = [
35+
'expected_at' => 'datetime',
36+
'check_in_at' => 'datetime',
37+
'check_out_at' => 'datetime',
38+
];
39+
40+
public function station(): BelongsTo
41+
{
42+
return $this->belongsTo(FrontdeskStation::class, 'station_id');
43+
}
44+
45+
public function host(): BelongsTo
46+
{
47+
return $this->belongsTo(User::class, 'host_employee_id');
48+
}
49+
50+
public function checkIn(): void
51+
{
52+
$this->update([
53+
'status' => 'checked_in',
54+
'check_in_at' => now(),
55+
]);
56+
}
57+
58+
public function checkOut(): void
59+
{
60+
$this->update([
61+
'status' => 'checked_out',
62+
'check_out_at' => now(),
63+
]);
64+
}
65+
66+
public function markNoShow(): void
67+
{
68+
$this->update(['status' => 'no_show']);
69+
}
70+
71+
public function durationMinutes(): ?int
72+
{
73+
if ($this->check_in_at && $this->check_out_at) {
74+
return (int) $this->check_in_at->diffInMinutes($this->check_out_at);
75+
}
76+
77+
return null;
78+
}
79+
80+
public function generateBadge(): string
81+
{
82+
return 'VIS-' . strtoupper(substr($this->visitor_name, 0, 3)) . '-' . str_pad($this->id, 4, '0', STR_PAD_LEFT);
83+
}
84+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace App\Modules\Frontdesk\Providers;
4+
5+
use Illuminate\Support\ServiceProvider;
6+
7+
class FrontdeskServiceProvider extends ServiceProvider
8+
{
9+
public function register(): void {}
10+
11+
public function boot(): void
12+
{
13+
$this->loadRoutesFrom(__DIR__ . '/../routes/frontdesk.php');
14+
}
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
use App\Modules\Frontdesk\Http\Controllers\FrontdeskController;
4+
use Illuminate\Support\Facades\Route;
5+
6+
Route::middleware(['web', 'auth', 'verified'])->prefix('frontdesk')->name('frontdesk.')->group(function () {
7+
Route::get('dashboard', [FrontdeskController::class, 'dashboard'])->name('dashboard');
8+
Route::get('stations', [FrontdeskController::class, 'stations'])->name('stations');
9+
Route::post('stations', [FrontdeskController::class, 'storeStation'])->name('stations.store');
10+
Route::get('visitors', [FrontdeskController::class, 'visitors'])->name('visitors');
11+
Route::get('check-in', [FrontdeskController::class, 'checkIn'])->name('check-in');
12+
Route::post('check-in', [FrontdeskController::class, 'checkIn'])->name('check-in.store');
13+
Route::post('visitors/{visitor}/check-out', [FrontdeskController::class, 'doCheckOut'])->name('visitors.check-out');
14+
Route::post('visitors/{visitor}/no-show', [FrontdeskController::class, 'markNoShow'])->name('visitors.no-show');
15+
Route::post('pre-register', [FrontdeskController::class, 'preRegister'])->name('pre-register');
16+
});

0 commit comments

Comments
 (0)