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>
259 lines
9.9 KiB
PHP
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',
|
|
];
|
|
}
|
|
}
|