Intégration fonctionnalites V1
This commit is contained in:
@@ -19,6 +19,8 @@ class BookingPageController extends Controller
|
||||
->with(['user:id,first_name,last_name,email', 'machine:id,uuid,name,establishment_id', 'machine.establishment:id,name']);
|
||||
|
||||
$this->scopeMachineRelationQuery($query);
|
||||
$this->applyEstablishmentFilter($request, $query, 'machine');
|
||||
$this->applyUserSearchFilter($request, $query);
|
||||
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->string('status'));
|
||||
@@ -29,8 +31,14 @@ class BookingPageController extends Controller
|
||||
}
|
||||
|
||||
$bookings = $query
|
||||
->orderByDesc('slot_start')
|
||||
->paginate(20)
|
||||
->tap(fn ($builder) => $this->applySort($request, $builder, [
|
||||
'slot_start' => 'bookings.slot_start',
|
||||
'user' => fn ($builder, string $direction) => $this->sortByRelatedUser($builder, 'bookings', 'user_id', $direction),
|
||||
'machine' => fn ($builder, string $direction) => $this->sortByRelatedMachine($builder, 'bookings', $direction),
|
||||
'status' => 'bookings.status',
|
||||
'booking_fee' => 'bookings.booking_fee',
|
||||
], 'slot_start', 'desc'))
|
||||
->paginate($this->supervisorListPerPage($request))
|
||||
->withQueryString()
|
||||
->through(fn (Booking $booking) => [
|
||||
'uuid' => $booking->uuid,
|
||||
@@ -54,9 +62,15 @@ class BookingPageController extends Controller
|
||||
|
||||
return Inertia::render('Supervisor/Bookings/Index', [
|
||||
'bookings' => $bookings,
|
||||
'establishments' => $this->establishmentOptions(),
|
||||
'filters' => [
|
||||
'establishment_id' => $request->filled('establishment_id') ? (int) $request->input('establishment_id') : null,
|
||||
'user' => $request->string('user')->toString() ?: null,
|
||||
'status' => $request->string('status')->toString() ?: null,
|
||||
'date' => $request->string('date')->toString() ?: null,
|
||||
'sort' => $request->string('sort')->toString() ?: 'slot_start',
|
||||
'direction' => $request->string('direction')->toString() === 'asc' ? 'asc' : 'desc',
|
||||
'per_page' => $this->supervisorListPerPage($request),
|
||||
],
|
||||
'statusOptions' => $this->statusOptions(),
|
||||
]);
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Http\Controllers\Supervisor\Concerns;
|
||||
|
||||
use App\Models\Establishment;
|
||||
use App\Models\Supervisor;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
trait ScopesSupervisorResources
|
||||
@@ -16,6 +18,21 @@ trait ScopesSupervisorResources
|
||||
return $supervisor;
|
||||
}
|
||||
|
||||
protected function supervisorListPerPage(Request $request): int
|
||||
{
|
||||
$perPage = $request->integer('per_page', 10);
|
||||
|
||||
return in_array($perPage, $this->supervisorPerPageOptions(), true) ? $perPage : 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
protected function supervisorPerPageOptions(): array
|
||||
{
|
||||
return [10, 25, 50];
|
||||
}
|
||||
|
||||
protected function scopeEstablishmentQuery(Builder $query, string $establishmentColumn = 'establishment_id'): Builder
|
||||
{
|
||||
$supervisor = $this->supervisor();
|
||||
@@ -47,4 +64,124 @@ trait ScopesSupervisorResources
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function applyEstablishmentFilter(Request $request, Builder $query, string $mode = 'direct'): void
|
||||
{
|
||||
if (! $request->filled('establishment_id')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$establishmentId = (int) $request->input('establishment_id');
|
||||
$this->assertEstablishmentAccessible($establishmentId);
|
||||
|
||||
if ($mode === 'machine') {
|
||||
$query->whereHas('machine', fn (Builder $query) => $query->where('establishment_id', $establishmentId));
|
||||
} else {
|
||||
$query->where('establishment_id', $establishmentId);
|
||||
}
|
||||
}
|
||||
|
||||
protected function applyUserSearchFilter(Request $request, Builder $query): void
|
||||
{
|
||||
if (! $request->filled('user')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$search = $request->string('user');
|
||||
|
||||
$query->whereHas('user', function (Builder $query) use ($search) {
|
||||
$query->where(function (Builder $query) use ($search) {
|
||||
$query->where('first_name', 'like', "%{$search}%")
|
||||
->orWhere('last_name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected function assertEstablishmentAccessible(int $establishmentId): void
|
||||
{
|
||||
$supervisor = $this->supervisor();
|
||||
|
||||
$exists = Establishment::query()
|
||||
->where('id', $establishmentId)
|
||||
->where('organization_id', $supervisor->organization_id)
|
||||
->when($supervisor->establishment_id, fn ($query) => $query->where('id', $supervisor->establishment_id))
|
||||
->exists();
|
||||
|
||||
abort_unless($exists, 403);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{id: int, name: string}>
|
||||
*/
|
||||
protected function establishmentOptions(): array
|
||||
{
|
||||
$supervisor = $this->supervisor();
|
||||
|
||||
return Establishment::query()
|
||||
->where('organization_id', $supervisor->organization_id)
|
||||
->when($supervisor->establishment_id, fn ($query) => $query->where('id', $supervisor->establishment_id))
|
||||
->orderBy('name')
|
||||
->get(['id', 'name'])
|
||||
->map(fn (Establishment $establishment) => [
|
||||
'id' => $establishment->id,
|
||||
'name' => $establishment->name,
|
||||
])
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|callable(Builder, string): void> $sortableColumns
|
||||
*/
|
||||
protected function applySort(
|
||||
Request $request,
|
||||
Builder $query,
|
||||
array $sortableColumns,
|
||||
string $defaultColumn,
|
||||
string $defaultDirection = 'asc',
|
||||
): void {
|
||||
$sort = $request->string('sort')->toString();
|
||||
$direction = strtolower($request->string('direction')->toString()) === 'desc' ? 'desc' : 'asc';
|
||||
|
||||
if ($sort === '' || ! array_key_exists($sort, $sortableColumns)) {
|
||||
$sort = $defaultColumn;
|
||||
$direction = $defaultDirection;
|
||||
}
|
||||
|
||||
$handler = $sortableColumns[$sort];
|
||||
|
||||
if (is_callable($handler)) {
|
||||
$handler($query, $direction);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query->orderBy($handler, $direction);
|
||||
}
|
||||
|
||||
protected function sortByRelatedUser(Builder $query, string $parentTable, string $foreignKey, string $direction): void
|
||||
{
|
||||
$query->leftJoin('users', "{$parentTable}.{$foreignKey}", '=', 'users.id')
|
||||
->orderBy('users.last_name', $direction)
|
||||
->orderBy('users.first_name', $direction)
|
||||
->select("{$parentTable}.*");
|
||||
}
|
||||
|
||||
protected function sortByRelatedMachine(Builder $query, string $parentTable, string $direction): void
|
||||
{
|
||||
$query->leftJoin('machines', "{$parentTable}.machine_id", '=', 'machines.id')
|
||||
->orderBy('machines.name', $direction)
|
||||
->select("{$parentTable}.*");
|
||||
}
|
||||
|
||||
protected function sortByRelatedEstablishment(
|
||||
Builder $query,
|
||||
string $parentTable,
|
||||
string $foreignKeyColumn,
|
||||
string $direction,
|
||||
): void {
|
||||
$query->leftJoin('establishments', "{$parentTable}.{$foreignKeyColumn}", '=', 'establishments.id')
|
||||
->orderBy('establishments.name', $direction)
|
||||
->select("{$parentTable}.*");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ namespace App\Http\Controllers\Supervisor;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Supervisor\Concerns\ScopesSupervisorResources;
|
||||
use App\Models\Supervisor;
|
||||
use App\Services\DashboardService;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
@@ -12,14 +14,40 @@ class DashboardController extends Controller
|
||||
{
|
||||
use ScopesSupervisorResources;
|
||||
|
||||
public function index(DashboardService $dashboardService): Response
|
||||
public function index(Request $request, DashboardService $dashboardService): Response
|
||||
{
|
||||
$supervisor = $this->supervisor();
|
||||
$establishmentFilter = $this->resolveDashboardEstablishmentFilter($request, $supervisor);
|
||||
$establishments = $this->establishmentOptions();
|
||||
|
||||
return Inertia::render('Supervisor/Dashboard', [
|
||||
'kpis' => $dashboardService->getKpis($supervisor),
|
||||
'alerts' => $dashboardService->getAlerts($supervisor),
|
||||
'machinesOverview' => $dashboardService->getMachinesOverview($supervisor),
|
||||
'kpis' => $dashboardService->getKpis($supervisor, $establishmentFilter),
|
||||
'alerts' => $dashboardService->getAlerts($supervisor, $establishmentFilter),
|
||||
'machinesOverview' => $dashboardService->getMachinesOverview($supervisor, $establishmentFilter),
|
||||
'machineStatusBreakdown' => $dashboardService->getMachineStatusBreakdown($supervisor, $establishmentFilter),
|
||||
'recentWashes' => $dashboardService->getRecentWashes($supervisor, $establishmentFilter),
|
||||
'context' => $dashboardService->getContext($supervisor, $establishmentFilter),
|
||||
'establishments' => $establishments,
|
||||
'showEstablishmentFilter' => $supervisor->establishment_id === null && count($establishments) > 1,
|
||||
'filters' => [
|
||||
'establishment_id' => $establishmentFilter,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveDashboardEstablishmentFilter(Request $request, Supervisor $supervisor): ?int
|
||||
{
|
||||
if ($supervisor->establishment_id !== null) {
|
||||
return $supervisor->establishment_id;
|
||||
}
|
||||
|
||||
if (! $request->filled('establishment_id')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$establishmentId = (int) $request->input('establishment_id');
|
||||
$this->assertEstablishmentAccessible($establishmentId);
|
||||
|
||||
return $establishmentId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,17 @@ namespace App\Http\Controllers\Supervisor;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Supervisor\Concerns\ScopesSupervisorResources;
|
||||
use App\Models\Establishment;
|
||||
use App\Models\Machine;
|
||||
use App\Models\MachineCommand;
|
||||
use App\Models\MachineEvent;
|
||||
use App\Models\MachineIntegration;
|
||||
use App\Models\MachineStatusHistory;
|
||||
use App\Models\PricingRule;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
@@ -20,6 +28,7 @@ class MachinePageController extends Controller
|
||||
->with('establishment:id,name,uuid');
|
||||
|
||||
$this->scopeEstablishmentQuery($query);
|
||||
$this->applyEstablishmentFilter($request, $query);
|
||||
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->string('status'));
|
||||
@@ -38,23 +47,100 @@ class MachinePageController extends Controller
|
||||
}
|
||||
|
||||
$machines = $query
|
||||
->orderBy('name')
|
||||
->paginate(20)
|
||||
->tap(fn ($builder) => $this->applySort($request, $builder, [
|
||||
'name' => 'machines.name',
|
||||
'establishment' => fn ($builder, string $direction) => $builder
|
||||
->leftJoin('establishments', 'machines.establishment_id', '=', 'establishments.id')
|
||||
->orderBy('establishments.name', $direction)
|
||||
->select('machines.*'),
|
||||
'type' => 'machines.type',
|
||||
'status' => 'machines.status',
|
||||
'last_heartbeat_at' => 'machines.last_heartbeat_at',
|
||||
], 'name', 'asc'))
|
||||
->paginate($this->supervisorListPerPage($request))
|
||||
->withQueryString()
|
||||
->through(fn (Machine $machine) => $this->formatMachineListItem($machine));
|
||||
|
||||
return Inertia::render('Supervisor/Machines/Index', [
|
||||
'machines' => $machines,
|
||||
'establishments' => $this->establishmentOptions(),
|
||||
'canManageMachines' => $this->canManageMachines(),
|
||||
'filters' => [
|
||||
'establishment_id' => $request->filled('establishment_id') ? (int) $request->input('establishment_id') : null,
|
||||
'status' => $request->string('status')->toString() ?: null,
|
||||
'type' => $request->string('type')->toString() ?: null,
|
||||
'search' => $request->string('search')->toString() ?: null,
|
||||
'sort' => $request->string('sort')->toString() ?: 'name',
|
||||
'direction' => $request->string('direction')->toString() === 'desc' ? 'desc' : 'asc',
|
||||
'per_page' => $this->supervisorListPerPage($request),
|
||||
],
|
||||
'statusOptions' => $this->statusOptions(),
|
||||
'typeOptions' => $this->typeOptions(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
abort_unless($this->canManageMachines(), 403);
|
||||
|
||||
$validated = $this->validateMachine($request);
|
||||
$this->assertEstablishmentAccessible((int) $validated['establishment_id']);
|
||||
|
||||
$validated['qr_code'] = $this->resolveQrCode($validated['qr_code'] ?? null);
|
||||
$validated['status'] = $validated['status'] ?? 'available';
|
||||
|
||||
$machine = Machine::query()->create($validated);
|
||||
$this->createSimulatedIntegration($machine);
|
||||
|
||||
return redirect()->route('supervisor.machines.index')
|
||||
->with('success', 'Machine créée.');
|
||||
}
|
||||
|
||||
public function update(Request $request, string $uuid): RedirectResponse
|
||||
{
|
||||
abort_unless($this->canManageMachines(), 403);
|
||||
|
||||
$machine = $this->findAccessibleMachine($uuid);
|
||||
$validated = $this->validateMachine($request, $machine);
|
||||
$this->assertEstablishmentAccessible((int) $validated['establishment_id']);
|
||||
|
||||
if (array_key_exists('qr_code', $validated) && blank($validated['qr_code'])) {
|
||||
unset($validated['qr_code']);
|
||||
}
|
||||
|
||||
$machine->update($validated);
|
||||
|
||||
return redirect()->route('supervisor.machines.index')
|
||||
->with('success', 'Machine mise à jour.');
|
||||
}
|
||||
|
||||
public function destroy(string $uuid): RedirectResponse
|
||||
{
|
||||
abort_unless($this->canManageMachines(), 403);
|
||||
|
||||
$machine = $this->findAccessibleMachine($uuid);
|
||||
|
||||
if (in_array($machine->status, ['running', 'reserved'], true)) {
|
||||
return redirect()->route('supervisor.machines.index')
|
||||
->withErrors(['delete' => 'Impossible de supprimer une machine en cours d\'utilisation.']);
|
||||
}
|
||||
|
||||
if ($machine->bookings()->exists() || $machine->washes()->exists()) {
|
||||
return redirect()->route('supervisor.machines.index')
|
||||
->withErrors(['delete' => 'Impossible de supprimer une machine avec un historique de réservations ou de lavages.']);
|
||||
}
|
||||
|
||||
PricingRule::query()->where('machine_id', $machine->id)->update(['machine_id' => null]);
|
||||
MachineCommand::query()->where('machine_id', $machine->id)->delete();
|
||||
MachineEvent::query()->where('machine_id', $machine->id)->delete();
|
||||
MachineStatusHistory::query()->where('machine_id', $machine->id)->delete();
|
||||
$machine->machineIntegration()?->delete();
|
||||
$machine->delete();
|
||||
|
||||
return redirect()->route('supervisor.machines.index')
|
||||
->with('success', 'Machine supprimée.');
|
||||
}
|
||||
|
||||
public function show(string $uuid): Response
|
||||
{
|
||||
$query = Machine::query()->where('uuid', $uuid);
|
||||
@@ -115,9 +201,11 @@ class MachinePageController extends Controller
|
||||
{
|
||||
return [
|
||||
'uuid' => $machine->uuid,
|
||||
'establishment_id' => $machine->establishment_id,
|
||||
'name' => $machine->name,
|
||||
'type' => $machine->type,
|
||||
'type_label' => $this->typeLabel($machine->type),
|
||||
'qr_code' => $machine->qr_code,
|
||||
'status' => $machine->status,
|
||||
'status_label' => $this->statusLabel($machine->status),
|
||||
'establishment_name' => $machine->establishment?->name,
|
||||
@@ -125,6 +213,68 @@ class MachinePageController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function validateMachine(Request $request, ?Machine $machine = null): array
|
||||
{
|
||||
return $request->validate([
|
||||
'establishment_id' => ['required', 'integer', 'exists:establishments,id'],
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'type' => ['required', 'in:washer_small,washer_large,dryer_small,dryer_large'],
|
||||
'qr_code' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('machines', 'qr_code')->ignore($machine?->id),
|
||||
],
|
||||
'status' => ['required', 'in:available,reserved,running,maintenance,offline,error'],
|
||||
]);
|
||||
}
|
||||
|
||||
private function findAccessibleMachine(string $uuid): Machine
|
||||
{
|
||||
$query = Machine::query()->where('uuid', $uuid);
|
||||
$this->scopeEstablishmentQuery($query);
|
||||
|
||||
return $query->firstOrFail();
|
||||
}
|
||||
|
||||
private function resolveQrCode(?string $qrCode): string
|
||||
{
|
||||
if (filled($qrCode)) {
|
||||
return $qrCode;
|
||||
}
|
||||
|
||||
do {
|
||||
$candidate = 'LAVERIE-'.strtoupper(Str::random(8));
|
||||
} while (Machine::query()->where('qr_code', $candidate)->exists());
|
||||
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
private function createSimulatedIntegration(Machine $machine): void
|
||||
{
|
||||
$machine->load('establishment');
|
||||
|
||||
MachineIntegration::query()->create([
|
||||
'machine_id' => $machine->id,
|
||||
'provider' => config('laverie.simulation.provider_name', 'simulated'),
|
||||
'external_machine_id' => 'SIM-'.$machine->uuid,
|
||||
'external_site_id' => 'SITE-'.$machine->establishment?->uuid,
|
||||
'mode' => 'simulated',
|
||||
'config' => [
|
||||
'cycle_duration_seconds' => config('laverie.simulation.cycle_duration_seconds', 30),
|
||||
],
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
private function canManageMachines(): bool
|
||||
{
|
||||
return $this->supervisor()->role !== 'viewer';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Supervisor;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Supervisor\Concerns\ScopesSupervisorResources;
|
||||
use App\Models\Establishment;
|
||||
use App\Services\GeocodingService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class OrganizationPageController extends Controller
|
||||
{
|
||||
use ScopesSupervisorResources;
|
||||
|
||||
public function __construct(private GeocodingService $geocoding) {}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$supervisor = $this->supervisor();
|
||||
|
||||
$baseQuery = Establishment::query()
|
||||
->where('organization_id', $supervisor->organization_id);
|
||||
|
||||
if ($supervisor->establishment_id !== null) {
|
||||
$baseQuery->where('id', $supervisor->establishment_id);
|
||||
}
|
||||
|
||||
$totalCount = (clone $baseQuery)->count();
|
||||
$viewMode = $totalCount === 1 ? 'single' : 'list';
|
||||
|
||||
$canManageEstablishments = $this->canManageEstablishments();
|
||||
$canCreateEstablishments = $this->canCreateEstablishments();
|
||||
|
||||
if ($viewMode === 'single') {
|
||||
$establishment = (clone $baseQuery)->withCount('machines')->first();
|
||||
|
||||
return Inertia::render('Supervisor/Organizations/Index', [
|
||||
'establishments' => [$this->formatEstablishment($establishment)],
|
||||
'viewMode' => 'single',
|
||||
'canManageEstablishments' => $canManageEstablishments,
|
||||
'canCreateEstablishments' => $canCreateEstablishments,
|
||||
'filters' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
$query = (clone $baseQuery)->withCount('machines');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->string('search');
|
||||
$query->where(function ($builder) use ($search) {
|
||||
$builder->where('name', 'like', "%{$search}%")
|
||||
->orWhere('address', 'like', "%{$search}%")
|
||||
->orWhere('city', 'like', "%{$search}%")
|
||||
->orWhere('zip_code', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->has('is_active') && $request->string('is_active')->toString() !== '') {
|
||||
$query->where('is_active', $request->boolean('is_active'));
|
||||
}
|
||||
|
||||
$establishments = $query
|
||||
->tap(fn ($builder) => $this->applySort($request, $builder, [
|
||||
'name' => 'name',
|
||||
'city' => 'city',
|
||||
'machines_count' => 'machines_count',
|
||||
'is_active' => 'is_active',
|
||||
], 'name', 'asc'))
|
||||
->paginate($this->supervisorListPerPage($request))
|
||||
->withQueryString()
|
||||
->through(fn (Establishment $establishment) => $this->formatEstablishment($establishment));
|
||||
|
||||
return Inertia::render('Supervisor/Organizations/Index', [
|
||||
'establishments' => $establishments,
|
||||
'viewMode' => 'list',
|
||||
'canManageEstablishments' => $canManageEstablishments,
|
||||
'canCreateEstablishments' => $canCreateEstablishments,
|
||||
'filters' => [
|
||||
'search' => $request->string('search')->toString() ?: null,
|
||||
'is_active' => $request->has('is_active') && $request->string('is_active')->toString() !== ''
|
||||
? ($request->boolean('is_active') ? '1' : '0')
|
||||
: null,
|
||||
'sort' => $request->string('sort')->toString() ?: 'name',
|
||||
'direction' => $request->string('direction')->toString() === 'desc' ? 'desc' : 'asc',
|
||||
'per_page' => $this->supervisorListPerPage($request),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function searchAddresses(Request $request): JsonResponse
|
||||
{
|
||||
abort_unless($this->canManageEstablishments(), 403);
|
||||
|
||||
$validated = $request->validate([
|
||||
'q' => ['required', 'string', 'min:3', 'max:200'],
|
||||
]);
|
||||
|
||||
return response()->json(
|
||||
$this->geocoding->search($validated['q']),
|
||||
);
|
||||
}
|
||||
|
||||
public function storeEstablishment(Request $request): RedirectResponse
|
||||
{
|
||||
abort_unless($this->canManageEstablishments(), 403);
|
||||
abort_if($this->supervisor()->establishment_id !== null, 403);
|
||||
|
||||
$validated = $this->prepareEstablishmentData(
|
||||
$request->validate($this->establishmentRules()),
|
||||
);
|
||||
$validated['organization_id'] = $this->supervisor()->organization_id;
|
||||
|
||||
Establishment::query()->create($validated);
|
||||
|
||||
return redirect()->route('supervisor.organizations.index')
|
||||
->with('success', 'Enseigne créée.');
|
||||
}
|
||||
|
||||
public function updateEstablishment(Request $request, int $id): RedirectResponse
|
||||
{
|
||||
abort_unless($this->canManageEstablishments(), 403);
|
||||
|
||||
$establishment = $this->findAccessibleEstablishment($id);
|
||||
$validated = $this->prepareEstablishmentData(
|
||||
$request->validate($this->establishmentRules()),
|
||||
);
|
||||
|
||||
if (! empty($validated['is_active']) && ! $establishment->is_active) {
|
||||
$this->validateEstablishmentReadyForActivation($validated, $establishment);
|
||||
}
|
||||
|
||||
$establishment->update($validated);
|
||||
|
||||
return redirect()->route('supervisor.organizations.index')
|
||||
->with('success', 'Enseigne mise à jour.');
|
||||
}
|
||||
|
||||
public function toggleEstablishmentActive(int $id): RedirectResponse
|
||||
{
|
||||
abort_unless($this->canManageEstablishments(), 403);
|
||||
|
||||
$establishment = $this->findAccessibleEstablishment($id);
|
||||
$isActive = ! $establishment->is_active;
|
||||
|
||||
if ($isActive) {
|
||||
$this->validateEstablishmentReadyForActivation([
|
||||
'name' => $establishment->name,
|
||||
'address' => $establishment->address,
|
||||
'timezone' => $establishment->timezone,
|
||||
], $establishment);
|
||||
}
|
||||
|
||||
$establishment->update(['is_active' => $isActive]);
|
||||
|
||||
return redirect()->route('supervisor.organizations.index')
|
||||
->with('success', $isActive ? 'Enseigne réactivée.' : 'Enseigne désactivée.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function formatEstablishment(Establishment $establishment): array
|
||||
{
|
||||
return [
|
||||
'id' => $establishment->id,
|
||||
'uuid' => $establishment->uuid,
|
||||
'name' => $establishment->name,
|
||||
'address' => $establishment->address,
|
||||
'city' => $establishment->city,
|
||||
'zip_code' => $establishment->zip_code,
|
||||
'latitude' => $establishment->latitude !== null ? (float) $establishment->latitude : null,
|
||||
'longitude' => $establishment->longitude !== null ? (float) $establishment->longitude : null,
|
||||
'timezone' => $establishment->timezone,
|
||||
'is_active' => $establishment->is_active,
|
||||
'machines_count' => $establishment->machines_count,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function establishmentRules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:200'],
|
||||
'address' => ['required', 'string'],
|
||||
'city' => ['nullable', 'string', 'max:100'],
|
||||
'zip_code' => ['nullable', 'string', 'max:10'],
|
||||
'timezone' => ['required', 'string', 'max:50'],
|
||||
'is_active' => ['boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function prepareEstablishmentData(array $validated): array
|
||||
{
|
||||
$coordinates = $this->geocoding->geocode(
|
||||
$validated['address'],
|
||||
$validated['city'] ?? null,
|
||||
$validated['zip_code'] ?? null,
|
||||
);
|
||||
|
||||
$validated['latitude'] = $coordinates['latitude'] ?? null;
|
||||
$validated['longitude'] = $coordinates['longitude'] ?? null;
|
||||
|
||||
return $validated;
|
||||
}
|
||||
|
||||
private function findAccessibleEstablishment(int $id): Establishment
|
||||
{
|
||||
$supervisor = $this->supervisor();
|
||||
|
||||
return Establishment::query()
|
||||
->where('id', $id)
|
||||
->where('organization_id', $supervisor->organization_id)
|
||||
->when($supervisor->establishment_id, fn ($query) => $query->where('id', $supervisor->establishment_id))
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
private function canCreateEstablishments(): bool
|
||||
{
|
||||
$supervisor = $this->supervisor();
|
||||
|
||||
if ($supervisor->role === 'viewer') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($supervisor->role, ['owner', 'platform_admin'], true)
|
||||
&& $supervisor->establishment_id === null;
|
||||
}
|
||||
|
||||
private function canManageEstablishments(): bool
|
||||
{
|
||||
return $this->supervisor()->role !== 'viewer';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
private function validateEstablishmentReadyForActivation(array $data, Establishment $establishment): void
|
||||
{
|
||||
$validator = validator(
|
||||
$data,
|
||||
[
|
||||
'name' => ['required', 'string', 'max:200'],
|
||||
'address' => ['required', 'string'],
|
||||
'timezone' => ['required', 'string', 'max:50'],
|
||||
],
|
||||
[
|
||||
'name.required' => 'Le nom est requis pour réactiver l\'enseigne.',
|
||||
'address.required' => 'L\'adresse est requise pour réactiver l\'enseigne.',
|
||||
'timezone.required' => 'Le fuseau horaire est requis pour réactiver l\'enseigne.',
|
||||
],
|
||||
);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw ValidationException::withMessages($validator->errors()->toArray());
|
||||
}
|
||||
|
||||
$establishment->loadMissing('organization');
|
||||
|
||||
if (! $establishment->organization?->is_active) {
|
||||
throw ValidationException::withMessages([
|
||||
'is_active' => 'L\'organisation doit être active pour réactiver cette enseigne.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,18 +21,60 @@ class PricingPageController extends Controller
|
||||
->with(['establishment:id,name', 'machine:id,uuid,name']);
|
||||
|
||||
$this->scopeEstablishmentQuery($query);
|
||||
$this->applyEstablishmentFilter($request, $query);
|
||||
|
||||
if ($request->filled('machine_type')) {
|
||||
$query->where('machine_type', $request->string('machine_type'));
|
||||
}
|
||||
|
||||
if ($request->filled('day_type')) {
|
||||
$query->where('day_type', $request->string('day_type'));
|
||||
}
|
||||
|
||||
if ($request->has('is_active') && $request->string('is_active')->toString() !== '') {
|
||||
$query->where('is_active', $request->boolean('is_active'));
|
||||
}
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->string('search');
|
||||
$query->where('label', 'like', "%{$search}%");
|
||||
}
|
||||
|
||||
$rules = $query
|
||||
->orderBy('priority')
|
||||
->orderBy('slot_start')
|
||||
->get()
|
||||
->map(fn (PricingRule $rule) => $this->formatRule($rule));
|
||||
->tap(fn ($builder) => $this->applySort($request, $builder, [
|
||||
'establishment' => fn ($builder, string $direction) => $this->sortByRelatedEstablishment(
|
||||
$builder,
|
||||
'pricing_rules',
|
||||
'establishment_id',
|
||||
$direction,
|
||||
),
|
||||
'slot_start' => 'slot_start',
|
||||
'machine_type' => 'machine_type',
|
||||
'price' => 'price',
|
||||
'priority' => 'priority',
|
||||
'is_active' => 'is_active',
|
||||
], 'priority', 'asc'))
|
||||
->paginate($this->supervisorListPerPage($request))
|
||||
->withQueryString()
|
||||
->through(fn (PricingRule $rule) => $this->formatRule($rule));
|
||||
|
||||
return Inertia::render('Supervisor/Pricing/Index', [
|
||||
'rules' => $rules,
|
||||
'establishments' => $this->establishmentOptions(),
|
||||
'machineTypes' => $this->machineTypeOptions(),
|
||||
'dayTypes' => $this->dayTypeOptions(),
|
||||
'filters' => [
|
||||
'establishment_id' => $request->filled('establishment_id') ? (int) $request->input('establishment_id') : null,
|
||||
'machine_type' => $request->string('machine_type')->toString() ?: null,
|
||||
'day_type' => $request->string('day_type')->toString() ?: null,
|
||||
'is_active' => $request->has('is_active') && $request->string('is_active')->toString() !== ''
|
||||
? ($request->boolean('is_active') ? '1' : '0')
|
||||
: null,
|
||||
'search' => $request->string('search')->toString() ?: null,
|
||||
'sort' => $request->string('sort')->toString() ?: 'priority',
|
||||
'direction' => $request->string('direction')->toString() === 'desc' ? 'desc' : 'asc',
|
||||
'per_page' => $this->supervisorListPerPage($request),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -134,22 +176,6 @@ class PricingPageController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{id: int, name: string}>
|
||||
*/
|
||||
private function establishmentOptions(): array
|
||||
{
|
||||
$supervisor = $this->supervisor();
|
||||
|
||||
return Establishment::query()
|
||||
->where('organization_id', $supervisor->organization_id)
|
||||
->when($supervisor->establishment_id, fn ($q) => $q->where('id', $supervisor->establishment_id))
|
||||
->orderBy('name')
|
||||
->get(['id', 'name'])
|
||||
->map(fn (Establishment $e) => ['id' => $e->id, 'name' => $e->name])
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
|
||||
@@ -15,21 +15,59 @@ class PromotionPageController extends Controller
|
||||
{
|
||||
use ScopesSupervisorResources;
|
||||
|
||||
public function index(): Response
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$query = Promotion::query()->with('establishment:id,name');
|
||||
$this->scopeEstablishmentQuery($query);
|
||||
$this->applyEstablishmentFilter($request, $query);
|
||||
|
||||
if ($request->filled('machine_type')) {
|
||||
$query->where('machine_type', $request->string('machine_type'));
|
||||
}
|
||||
|
||||
if ($request->has('is_active') && $request->string('is_active')->toString() !== '') {
|
||||
$query->where('is_active', $request->boolean('is_active'));
|
||||
}
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->string('search');
|
||||
$query->where('description', 'like', "%{$search}%");
|
||||
}
|
||||
|
||||
$promotions = $query
|
||||
->orderByDesc('starts_at')
|
||||
->get()
|
||||
->map(fn (Promotion $promotion) => $this->formatPromotion($promotion));
|
||||
->tap(fn ($builder) => $this->applySort($request, $builder, [
|
||||
'establishment' => fn ($builder, string $direction) => $this->sortByRelatedEstablishment(
|
||||
$builder,
|
||||
'promotions',
|
||||
'establishment_id',
|
||||
$direction,
|
||||
),
|
||||
'discount_value' => 'discount_value',
|
||||
'starts_at' => 'starts_at',
|
||||
'ends_at' => 'ends_at',
|
||||
'machine_type' => 'machine_type',
|
||||
'is_active' => 'is_active',
|
||||
], 'starts_at', 'desc'))
|
||||
->paginate($this->supervisorListPerPage($request))
|
||||
->withQueryString()
|
||||
->through(fn (Promotion $promotion) => $this->formatPromotion($promotion));
|
||||
|
||||
return Inertia::render('Supervisor/Promotions/Index', [
|
||||
'promotions' => $promotions,
|
||||
'establishments' => $this->establishmentOptions(),
|
||||
'machineTypes' => $this->machineTypeOptions(),
|
||||
'discountTypes' => $this->discountTypeOptions(),
|
||||
'filters' => [
|
||||
'establishment_id' => $request->filled('establishment_id') ? (int) $request->input('establishment_id') : null,
|
||||
'machine_type' => $request->string('machine_type')->toString() ?: null,
|
||||
'is_active' => $request->has('is_active') && $request->string('is_active')->toString() !== ''
|
||||
? ($request->boolean('is_active') ? '1' : '0')
|
||||
: null,
|
||||
'search' => $request->string('search')->toString() ?: null,
|
||||
'sort' => $request->string('sort')->toString() ?: 'starts_at',
|
||||
'direction' => $request->string('direction')->toString() === 'desc' ? 'desc' : 'asc',
|
||||
'per_page' => $this->supervisorListPerPage($request),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -124,22 +162,6 @@ class PromotionPageController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{id: int, name: string}>
|
||||
*/
|
||||
private function establishmentOptions(): array
|
||||
{
|
||||
$supervisor = $this->supervisor();
|
||||
|
||||
return Establishment::query()
|
||||
->where('organization_id', $supervisor->organization_id)
|
||||
->when($supervisor->establishment_id, fn ($q) => $q->where('id', $supervisor->establishment_id))
|
||||
->orderBy('name')
|
||||
->get(['id', 'name'])
|
||||
->map(fn (Establishment $e) => ['id' => $e->id, 'name' => $e->name])
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,8 @@ class WashPageController extends Controller
|
||||
->with(['user:id,first_name,last_name,email', 'machine:id,uuid,name,establishment_id', 'machine.establishment:id,name']);
|
||||
|
||||
$this->scopeMachineRelationQuery($query);
|
||||
$this->applyEstablishmentFilter($request, $query, 'machine');
|
||||
$this->applyUserSearchFilter($request, $query);
|
||||
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->string('status'));
|
||||
@@ -29,8 +31,15 @@ class WashPageController extends Controller
|
||||
}
|
||||
|
||||
$washes = $query
|
||||
->orderByDesc('started_at')
|
||||
->paginate(20)
|
||||
->tap(fn ($builder) => $this->applySort($request, $builder, [
|
||||
'started_at' => 'washes.started_at',
|
||||
'user' => fn ($builder, string $direction) => $this->sortByRelatedUser($builder, 'washes', 'user_id', $direction),
|
||||
'machine' => fn ($builder, string $direction) => $this->sortByRelatedMachine($builder, 'washes', $direction),
|
||||
'status' => 'washes.status',
|
||||
'duration_minutes' => 'washes.duration_minutes',
|
||||
'cost' => 'washes.cost',
|
||||
], 'started_at', 'desc'))
|
||||
->paginate($this->supervisorListPerPage($request))
|
||||
->withQueryString()
|
||||
->through(fn (Wash $wash) => [
|
||||
'uuid' => $wash->uuid,
|
||||
@@ -56,9 +65,15 @@ class WashPageController extends Controller
|
||||
|
||||
return Inertia::render('Supervisor/Washes/Index', [
|
||||
'washes' => $washes,
|
||||
'establishments' => $this->establishmentOptions(),
|
||||
'filters' => [
|
||||
'establishment_id' => $request->filled('establishment_id') ? (int) $request->input('establishment_id') : null,
|
||||
'user' => $request->string('user')->toString() ?: null,
|
||||
'status' => $request->string('status')->toString() ?: null,
|
||||
'date' => $request->string('date')->toString() ?: null,
|
||||
'sort' => $request->string('sort')->toString() ?: 'started_at',
|
||||
'direction' => $request->string('direction')->toString() === 'asc' ? 'asc' : 'desc',
|
||||
'per_page' => $this->supervisorListPerPage($request),
|
||||
],
|
||||
'statusOptions' => $this->statusOptions(),
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user