Files
L-Ami-Fiduciaire/app/Http/Controllers/DashboardController.php
Saad Ibn-Ezzoubayr 3baf456640 feat: implement Story 2.3 — Worker-Scoped Dashboard
Scope stat cards and urgent declarations table to the authenticated
worker's own assignments. Add empty state when no declarations are
assigned, hide the "Assigné à" column for worker role, and expose
isWorker flag through DashboardController and dashboard types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 17:31:23 +01:00

259 lines
9.9 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Enums\DeclarationStatus;
use App\Enums\WorkspaceUserRole;
use App\Models\Declaration;
use App\Models\Workspace;
use App\Models\WorkspaceUser;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Inertia\Inertia;
use Inertia\Response;
class DashboardController extends Controller
{
/**
* Display the command center dashboard with KPI cards and urgent declarations.
*/
public function __invoke(Request $request): Response
{
$user = $request->user();
$workspaceId = $request->session()->get('current_workspace_id');
$workspace = $workspaceId ? Workspace::query()->find($workspaceId) : null;
if (! $workspace || ! $user) {
return Inertia::render('Dashboard', [
'stats' => null,
'statCards' => [],
'declarations' => [],
'alerts' => [],
'workspaceName' => null,
'roleLabel' => null,
'isWorker' => false,
'declarationsUrl' => null,
'clientsUrl' => null,
'viewAllAlertsUrl' => null,
]);
}
/** @var WorkspaceUser|null $workspaceUser */
$workspaceUser = $workspace->users()
->where('users.id', $user->id)
->first()
?->pivot;
if (! $workspaceUser) {
abort(404);
}
$cacheKey = "dashboard:{$workspace->id}:{$user->id}";
$dashboardData = Cache::remember($cacheKey, 300, function () use ($workspace, $user, $workspaceUser) {
$baseQuery = fn () => $workspace->declarations()
->active()
->whereNotIn('status', [DeclarationStatus::Termine, DeclarationStatus::MiseEnDemeure, DeclarationStatus::Ferme])
->forUser($user, $workspaceUser);
$overdue = $baseQuery()
->where('due_date', '<', now()->startOfDay())
->count();
$dueThisWeek = $baseQuery()
->whereBetween('due_date', [now()->startOfDay(), now()->addDays(7)->endOfDay()])
->count();
$enAttenteClient = $baseQuery()
->where('status', DeclarationStatus::EnAttenteClient)
->count();
$enCours = $baseQuery()
->where('status', DeclarationStatus::EnCours)
->count();
$alerts = $this->buildAlerts($baseQuery);
return [
'overdue' => $overdue,
'dueThisWeek' => $dueThisWeek,
'enAttenteClient' => $enAttenteClient,
'enCours' => $enCours,
'alerts' => $alerts,
];
});
$urgentDeclarations = $workspace->declarations()
->active()
->whereNotIn('status', [DeclarationStatus::Termine, DeclarationStatus::MiseEnDemeure, DeclarationStatus::Ferme])
->forUser($user, $workspaceUser)
->with('client:id,company_name', 'assignee:id,name')
->orderByRaw('CASE WHEN due_date IS NULL THEN 1 ELSE 0 END, due_date ASC')
->limit(15)
->get()
->map(fn (Declaration $d) => [
'id' => $d->id,
'title' => $d->title,
'type' => $d->type->value,
'typeLabel' => $this->typeLabels()[$d->type->value] ?? $d->type->value,
'clientName' => $d->client?->company_name ?? 'Client supprimé',
'assigneeName' => $d->assignee?->name,
'status' => $d->status->value,
'statusLabel' => DeclarationStatus::labels()[$d->status->value] ?? $d->status->value,
'dueDate' => $d->due_date?->format('Y-m-d'),
'showUrl' => route('declarations.show', $d),
])
->all();
$roleLabel = $this->roleLabels()[$workspaceUser->role->value] ?? $workspaceUser->role->value;
$isWorker = $workspaceUser->role->is(WorkspaceUserRole::Worker);
$assigneeParam = $isWorker ? ['assignee' => $user->id] : [];
$statCards = [
[
'label' => 'En retard',
'count' => $dashboardData['overdue'],
'status' => 'danger',
'href' => route('declarations.index', array_merge(['overdue' => 1], $assigneeParam)),
],
[
'label' => 'Cette semaine',
'count' => $dashboardData['dueThisWeek'],
'status' => 'warning',
'href' => route('declarations.index', array_merge(['due_this_week' => 1], $assigneeParam)),
],
[
'label' => 'En attente client',
'count' => $dashboardData['enAttenteClient'],
'status' => 'info',
'href' => route('declarations.index', array_merge(['status' => DeclarationStatus::EnAttenteClient], $assigneeParam)),
],
[
'label' => 'En cours',
'count' => $dashboardData['enCours'],
'status' => 'success',
'href' => route('declarations.index', array_merge(['status' => DeclarationStatus::EnCours], $assigneeParam)),
],
];
return Inertia::render('Dashboard', [
'stats' => $dashboardData,
'statCards' => $statCards,
'declarations' => $urgentDeclarations,
'alerts' => $dashboardData['alerts'],
'workspaceName' => $workspace->name,
'roleLabel' => $roleLabel,
'isWorker' => $isWorker,
'declarationsUrl' => route('declarations.index'),
'clientsUrl' => route('clients.index'),
'viewAllAlertsUrl' => route('declarations.index', ['filter' => 'alerts']),
]);
}
/**
* Build priority alerts from declarations, sorted by severity and urgency.
*
* @param \Closure $baseQuery A closure that returns a fresh query builder with workspace/user scoping applied
* @return array<int, array<string, mixed>>
*/
private function buildAlerts(\Closure $baseQuery): array
{
$typeLabels = $this->typeLabels();
$today = now()->startOfDay();
// Critical: overdue declarations (past deadline), excluding en_attente_client (handled by info)
$critical = $baseQuery()
->whereNotNull('due_date')
->where('due_date', '<', $today)
->where('status', '!=', DeclarationStatus::EnAttenteClient)
->with('client:id,company_name')
->orderBy('due_date', 'asc')
->limit(20)
->get()
->map(fn (Declaration $d) => [
'id' => $d->id,
'severity' => 'critical',
'clientName' => $d->client?->company_name ?? 'Client supprimé',
'declarationType' => $d->type->value,
'typeLabel' => $typeLabels[$d->type->value] ?? $d->type->value,
'daysValue' => (int) abs($today->diffInDays($d->due_date)),
'daysLabel' => abs($today->diffInDays($d->due_date)) <= 1 ? 'jour en retard' : 'jours en retard',
'showUrl' => route('declarations.show', $d),
]);
// Warning: approaching deadlines (due within 3 days)
$warning = $baseQuery()
->whereNotNull('due_date')
->whereBetween('due_date', [$today, now()->addDays(3)->endOfDay()])
->with('client:id,company_name')
->orderBy('due_date', 'asc')
->limit(20)
->get()
->map(fn (Declaration $d) => [
'id' => $d->id,
'severity' => 'warning',
'clientName' => $d->client?->company_name ?? 'Client supprimé',
'declarationType' => $d->type->value,
'typeLabel' => $typeLabels[$d->type->value] ?? $d->type->value,
'daysValue' => (int) $today->diffInDays($d->due_date),
'daysLabel' => $today->diffInDays($d->due_date) <= 1 ? 'jour restant' : 'jours restants',
'showUrl' => route('declarations.show', $d),
]);
// Info: waiting on client for >3 days
$info = $baseQuery()
->where('status', DeclarationStatus::EnAttenteClient)
->where('updated_at', '<', now()->subDays(3))
->with('client:id,company_name')
->orderBy('updated_at', 'asc')
->limit(20)
->get()
->map(fn (Declaration $d) => [
'id' => $d->id,
'severity' => 'info',
'clientName' => $d->client?->company_name ?? 'Client supprimé',
'declarationType' => $d->type->value,
'typeLabel' => $typeLabels[$d->type->value] ?? $d->type->value,
'daysValue' => (int) abs($today->diffInDays($d->updated_at)),
'daysLabel' => abs($today->diffInDays($d->updated_at)) <= 1 ? 'jour en attente' : 'jours en attente',
'showUrl' => route('declarations.show', $d),
]);
return $critical->concat($warning)->concat($info)->take(20)->values()->toArray();
}
/**
* Get declaration type labels.
*
* @return array<string, string>
*/
protected function typeLabels(): array
{
return [
'vat' => 'TVA',
'vat_monthly' => 'TVA mensuelle',
'vat_quarterly' => 'TVA trimestrielle',
'corporate_tax' => 'IS',
'income_tax' => 'IR',
'cnss' => 'CNSS',
'annual_balance' => 'Bilan',
'other' => 'Autre',
];
}
/**
* Get workspace user role labels.
*
* @return array<string, string>
*/
protected function roleLabels(): array
{
return [
WorkspaceUserRole::Owner => 'Propriétaire',
WorkspaceUserRole::Manager => 'Manager',
WorkspaceUserRole::Worker => 'Collaborateur',
];
}
}