Files
L-Ami-Fiduciaire/app/Http/Controllers/DashboardController.php
Saad Ibn-Ezzoubayr a2ab6f365d feat: implement Story 2.1 — Owner/Manager Command Center Dashboard
- Rewrite DashboardController with cached role-scoped KPI aggregation
  (Cache::remember, 5-min TTL, Declaration::forUser scope)
- Create StatCard.vue component with CVA status variants and a11y
- Rewrite Dashboard.vue with 4-column KPI grid + urgent declarations table
- Add mise_en_demeure status to DeclarationStatus enum with transitions
- Exclude termine, mise_en_demeure, ferme from dashboard queries
- Set deadline proximity red threshold to ≤5 days
- Add abort(404) for non-member workspace access per architecture
- Fix null-safe client access for soft-deleted clients
- Fix hardcoded routes with Wayfinder type-safe imports
- Fix DashboardProps.stats type to allow null
- Add aria-pressed to StatCard for accessibility
- Install shadcn-vue table component (11 files)
- Add 11 Pest feature tests + 3 mise_en_demeure transition tests
- Fix DeclarationFactory eager workspace creation causing slug collisions
- 196 tests pass, 836 assertions, zero regressions
2026-03-20 12:00:24 +00:00

175 lines
5.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' => [],
'workspaceName' => null,
'roleLabel' => null,
'declarationsUrl' => null,
'clientsUrl' => 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();
return [
'overdue' => $overdue,
'dueThisWeek' => $dueThisWeek,
'enAttenteClient' => $enAttenteClient,
'enCours' => $enCours,
];
});
$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;
$statCards = [
[
'label' => 'En retard',
'count' => $dashboardData['overdue'],
'status' => 'danger',
'href' => route('declarations.index', ['overdue' => 1]),
],
[
'label' => 'Cette semaine',
'count' => $dashboardData['dueThisWeek'],
'status' => 'warning',
'href' => route('declarations.index', ['due_this_week' => 1]),
],
[
'label' => 'En attente client',
'count' => $dashboardData['enAttenteClient'],
'status' => 'info',
'href' => route('declarations.index', ['status' => DeclarationStatus::EnAttenteClient]),
],
[
'label' => 'En cours',
'count' => $dashboardData['enCours'],
'status' => 'success',
'href' => route('declarations.index', ['status' => DeclarationStatus::EnCours]),
],
];
return Inertia::render('Dashboard', [
'stats' => $dashboardData,
'statCards' => $statCards,
'declarations' => $urgentDeclarations,
'workspaceName' => $workspace->name,
'roleLabel' => $roleLabel,
'declarationsUrl' => route('declarations.index'),
'clientsUrl' => route('clients.index'),
]);
}
/**
* 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',
];
}
}