2026-03-11 23:33:10 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
|
|
2026-03-12 18:25:32 +00:00
|
|
|
use App\Enums\DeclarationStatus;
|
2026-03-20 12:00:24 +00:00
|
|
|
use App\Enums\WorkspaceUserRole;
|
2026-03-12 18:25:32 +00:00
|
|
|
use App\Models\Declaration;
|
2026-03-11 23:33:10 +00:00
|
|
|
use App\Models\Workspace;
|
2026-03-20 12:00:24 +00:00
|
|
|
use App\Models\WorkspaceUser;
|
2026-03-11 23:33:10 +00:00
|
|
|
use Illuminate\Http\Request;
|
2026-03-20 12:00:24 +00:00
|
|
|
use Illuminate\Support\Facades\Cache;
|
2026-03-11 23:33:10 +00:00
|
|
|
use Inertia\Inertia;
|
|
|
|
|
use Inertia\Response;
|
feat: implement Story 2.4 — Dashboard Activity Feed with review fixes
Add role-scoped activity feed to the dashboard showing the 20 most recent
workspace events. Owners/Managers see all activity (declarations, clients,
team changes); Workers see only their assigned declarations. Includes
French descriptions, relative timestamps, responsive layout (desktop
sidebar, tablet inline, mobile collapsible), and 7 passing Pest tests.
Review fixes applied: batch-load declarations/clients/users to eliminate
N+1 queries, consistent soft-delete handling in URL resolution, French
grammar singular/plural fix, missing icon map entry, and corrected tablet
breakpoint per spec.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 21:21:07 +01:00
|
|
|
use Spatie\Activitylog\Models\Activity;
|
2026-03-11 23:33:10 +00:00
|
|
|
|
|
|
|
|
class DashboardController extends Controller
|
|
|
|
|
{
|
|
|
|
|
/**
|
2026-03-20 12:00:24 +00:00
|
|
|
* Display the command center dashboard with KPI cards and urgent declarations.
|
2026-03-11 23:33:10 +00:00
|
|
|
*/
|
|
|
|
|
public function __invoke(Request $request): Response
|
|
|
|
|
{
|
|
|
|
|
$user = $request->user();
|
|
|
|
|
$workspaceId = $request->session()->get('current_workspace_id');
|
|
|
|
|
$workspace = $workspaceId ? Workspace::query()->find($workspaceId) : null;
|
|
|
|
|
|
2026-03-20 12:00:24 +00:00
|
|
|
if (! $workspace || ! $user) {
|
|
|
|
|
return Inertia::render('Dashboard', [
|
|
|
|
|
'stats' => null,
|
|
|
|
|
'statCards' => [],
|
|
|
|
|
'declarations' => [],
|
2026-03-20 12:33:27 +00:00
|
|
|
'alerts' => [],
|
feat: implement Story 2.4 — Dashboard Activity Feed with review fixes
Add role-scoped activity feed to the dashboard showing the 20 most recent
workspace events. Owners/Managers see all activity (declarations, clients,
team changes); Workers see only their assigned declarations. Includes
French descriptions, relative timestamps, responsive layout (desktop
sidebar, tablet inline, mobile collapsible), and 7 passing Pest tests.
Review fixes applied: batch-load declarations/clients/users to eliminate
N+1 queries, consistent soft-delete handling in URL resolution, French
grammar singular/plural fix, missing icon map entry, and corrected tablet
breakpoint per spec.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 21:21:07 +01:00
|
|
|
'activities' => [],
|
2026-03-20 12:00:24 +00:00
|
|
|
'workspaceName' => null,
|
|
|
|
|
'roleLabel' => null,
|
2026-03-22 17:31:23 +01:00
|
|
|
'isWorker' => false,
|
2026-03-20 12:00:24 +00:00
|
|
|
'declarationsUrl' => null,
|
|
|
|
|
'clientsUrl' => null,
|
2026-03-20 12:33:27 +00:00
|
|
|
'viewAllAlertsUrl' => null,
|
2026-03-20 12:00:24 +00:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @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()
|
2026-03-11 23:33:10 +00:00
|
|
|
->where('due_date', '<', now()->startOfDay())
|
2026-03-20 12:00:24 +00:00
|
|
|
->count();
|
|
|
|
|
|
|
|
|
|
$dueThisWeek = $baseQuery()
|
2026-03-11 23:33:10 +00:00
|
|
|
->whereBetween('due_date', [now()->startOfDay(), now()->addDays(7)->endOfDay()])
|
2026-03-20 12:00:24 +00:00
|
|
|
->count();
|
|
|
|
|
|
|
|
|
|
$enAttenteClient = $baseQuery()
|
2026-03-12 18:25:32 +00:00
|
|
|
->where('status', DeclarationStatus::EnAttenteClient)
|
2026-03-20 12:00:24 +00:00
|
|
|
->count();
|
|
|
|
|
|
|
|
|
|
$enCours = $baseQuery()
|
|
|
|
|
->where('status', DeclarationStatus::EnCours)
|
|
|
|
|
->count();
|
|
|
|
|
|
2026-03-20 12:33:27 +00:00
|
|
|
$alerts = $this->buildAlerts($baseQuery);
|
feat: implement Story 2.4 — Dashboard Activity Feed with review fixes
Add role-scoped activity feed to the dashboard showing the 20 most recent
workspace events. Owners/Managers see all activity (declarations, clients,
team changes); Workers see only their assigned declarations. Includes
French descriptions, relative timestamps, responsive layout (desktop
sidebar, tablet inline, mobile collapsible), and 7 passing Pest tests.
Review fixes applied: batch-load declarations/clients/users to eliminate
N+1 queries, consistent soft-delete handling in URL resolution, French
grammar singular/plural fix, missing icon map entry, and corrected tablet
breakpoint per spec.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 21:21:07 +01:00
|
|
|
$activities = $this->buildActivityFeed($workspace, $user, $workspaceUser);
|
2026-03-20 12:33:27 +00:00
|
|
|
|
2026-03-20 12:00:24 +00:00
|
|
|
return [
|
2026-03-11 23:33:10 +00:00
|
|
|
'overdue' => $overdue,
|
2026-03-20 12:00:24 +00:00
|
|
|
'dueThisWeek' => $dueThisWeek,
|
|
|
|
|
'enAttenteClient' => $enAttenteClient,
|
|
|
|
|
'enCours' => $enCours,
|
2026-03-20 12:33:27 +00:00
|
|
|
'alerts' => $alerts,
|
feat: implement Story 2.4 — Dashboard Activity Feed with review fixes
Add role-scoped activity feed to the dashboard showing the 20 most recent
workspace events. Owners/Managers see all activity (declarations, clients,
team changes); Workers see only their assigned declarations. Includes
French descriptions, relative timestamps, responsive layout (desktop
sidebar, tablet inline, mobile collapsible), and 7 passing Pest tests.
Review fixes applied: batch-load declarations/clients/users to eliminate
N+1 queries, consistent soft-delete handling in URL resolution, French
grammar singular/plural fix, missing icon map entry, and corrected tablet
breakpoint per spec.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 21:21:07 +01:00
|
|
|
'activities' => $activities,
|
2026-03-11 23:33:10 +00:00
|
|
|
];
|
2026-03-20 12:00:24 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$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;
|
2026-03-22 17:31:23 +01:00
|
|
|
$isWorker = $workspaceUser->role->is(WorkspaceUserRole::Worker);
|
|
|
|
|
|
|
|
|
|
$assigneeParam = $isWorker ? ['assignee' => $user->id] : [];
|
2026-03-20 12:00:24 +00:00
|
|
|
|
|
|
|
|
$statCards = [
|
|
|
|
|
[
|
|
|
|
|
'label' => 'En retard',
|
|
|
|
|
'count' => $dashboardData['overdue'],
|
|
|
|
|
'status' => 'danger',
|
2026-03-22 17:31:23 +01:00
|
|
|
'href' => route('declarations.index', array_merge(['overdue' => 1], $assigneeParam)),
|
2026-03-20 12:00:24 +00:00
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'label' => 'Cette semaine',
|
|
|
|
|
'count' => $dashboardData['dueThisWeek'],
|
|
|
|
|
'status' => 'warning',
|
2026-03-22 17:31:23 +01:00
|
|
|
'href' => route('declarations.index', array_merge(['due_this_week' => 1], $assigneeParam)),
|
2026-03-20 12:00:24 +00:00
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'label' => 'En attente client',
|
|
|
|
|
'count' => $dashboardData['enAttenteClient'],
|
|
|
|
|
'status' => 'info',
|
2026-03-22 17:31:23 +01:00
|
|
|
'href' => route('declarations.index', array_merge(['status' => DeclarationStatus::EnAttenteClient], $assigneeParam)),
|
2026-03-20 12:00:24 +00:00
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'label' => 'En cours',
|
|
|
|
|
'count' => $dashboardData['enCours'],
|
|
|
|
|
'status' => 'success',
|
2026-03-22 17:31:23 +01:00
|
|
|
'href' => route('declarations.index', array_merge(['status' => DeclarationStatus::EnCours], $assigneeParam)),
|
2026-03-20 12:00:24 +00:00
|
|
|
],
|
|
|
|
|
];
|
2026-03-11 23:33:10 +00:00
|
|
|
|
|
|
|
|
return Inertia::render('Dashboard', [
|
2026-03-20 12:00:24 +00:00
|
|
|
'stats' => $dashboardData,
|
|
|
|
|
'statCards' => $statCards,
|
|
|
|
|
'declarations' => $urgentDeclarations,
|
2026-03-20 12:33:27 +00:00
|
|
|
'alerts' => $dashboardData['alerts'],
|
feat: implement Story 2.4 — Dashboard Activity Feed with review fixes
Add role-scoped activity feed to the dashboard showing the 20 most recent
workspace events. Owners/Managers see all activity (declarations, clients,
team changes); Workers see only their assigned declarations. Includes
French descriptions, relative timestamps, responsive layout (desktop
sidebar, tablet inline, mobile collapsible), and 7 passing Pest tests.
Review fixes applied: batch-load declarations/clients/users to eliminate
N+1 queries, consistent soft-delete handling in URL resolution, French
grammar singular/plural fix, missing icon map entry, and corrected tablet
breakpoint per spec.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 21:21:07 +01:00
|
|
|
'activities' => $dashboardData['activities'],
|
2026-03-20 12:00:24 +00:00
|
|
|
'workspaceName' => $workspace->name,
|
|
|
|
|
'roleLabel' => $roleLabel,
|
2026-03-22 17:31:23 +01:00
|
|
|
'isWorker' => $isWorker,
|
2026-03-20 12:00:24 +00:00
|
|
|
'declarationsUrl' => route('declarations.index'),
|
|
|
|
|
'clientsUrl' => route('clients.index'),
|
2026-03-20 12:33:27 +00:00
|
|
|
'viewAllAlertsUrl' => route('declarations.index', ['filter' => 'alerts']),
|
2026-03-11 23:33:10 +00:00
|
|
|
]);
|
|
|
|
|
}
|
2026-03-20 12:00:24 +00:00
|
|
|
|
2026-03-20 12:33:27 +00:00
|
|
|
/**
|
|
|
|
|
* 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();
|
|
|
|
|
}
|
|
|
|
|
|
feat: implement Story 2.4 — Dashboard Activity Feed with review fixes
Add role-scoped activity feed to the dashboard showing the 20 most recent
workspace events. Owners/Managers see all activity (declarations, clients,
team changes); Workers see only their assigned declarations. Includes
French descriptions, relative timestamps, responsive layout (desktop
sidebar, tablet inline, mobile collapsible), and 7 passing Pest tests.
Review fixes applied: batch-load declarations/clients/users to eliminate
N+1 queries, consistent soft-delete handling in URL resolution, French
grammar singular/plural fix, missing icon map entry, and corrected tablet
breakpoint per spec.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 21:21:07 +01:00
|
|
|
/**
|
|
|
|
|
* Build the activity feed for the dashboard.
|
|
|
|
|
*
|
|
|
|
|
* @return array<int, array<string, mixed>>
|
|
|
|
|
*/
|
|
|
|
|
private function buildActivityFeed(Workspace $workspace, \App\Models\User $user, WorkspaceUser $workspaceUser): array
|
|
|
|
|
{
|
|
|
|
|
$isWorker = $workspaceUser->role->is(WorkspaceUserRole::Worker);
|
|
|
|
|
|
|
|
|
|
if ($isWorker) {
|
|
|
|
|
$activities = Activity::query()
|
|
|
|
|
->where('subject_type', 'App\\Models\\Declaration')
|
|
|
|
|
->whereIn('subject_id',
|
|
|
|
|
$workspace->declarations()
|
|
|
|
|
->where('assigned_to', $user->id)
|
|
|
|
|
->select('id')
|
|
|
|
|
)
|
|
|
|
|
->with('causer:id,name')
|
|
|
|
|
->latest()
|
|
|
|
|
->limit(20)
|
|
|
|
|
->get();
|
|
|
|
|
} else {
|
|
|
|
|
$activities = Activity::query()
|
|
|
|
|
->where(function ($query) use ($workspace) {
|
|
|
|
|
$query->where(function ($q) use ($workspace) {
|
|
|
|
|
$q->where('subject_type', 'App\\Models\\Declaration')
|
|
|
|
|
->whereIn('subject_id', $workspace->declarations()->select('id'));
|
|
|
|
|
})
|
|
|
|
|
->orWhere(function ($q) use ($workspace) {
|
|
|
|
|
$q->where('subject_type', 'App\\Models\\Client')
|
|
|
|
|
->whereIn('subject_id', $workspace->clients()->select('id'));
|
|
|
|
|
})
|
|
|
|
|
->orWhere(function ($q) use ($workspace) {
|
|
|
|
|
$q->whereIn('subject_type', ['App\\Models\\WorkspaceUser', 'App\\Models\\TeamInvitation'])
|
|
|
|
|
->whereIn('causer_id', $workspace->users()->select('users.id'));
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
->with('causer:id,name')
|
|
|
|
|
->latest()
|
|
|
|
|
->limit(20)
|
|
|
|
|
->get();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$typeLabels = $this->typeLabels();
|
|
|
|
|
|
|
|
|
|
// Batch-load declarations (with trashed) and their clients to avoid N+1
|
|
|
|
|
$declarationSubjectIds = $activities
|
|
|
|
|
->where('subject_type', 'App\\Models\\Declaration')
|
|
|
|
|
->pluck('subject_id')
|
|
|
|
|
->unique()
|
|
|
|
|
->filter()
|
|
|
|
|
->values();
|
|
|
|
|
|
|
|
|
|
$declarationsMap = $declarationSubjectIds->isNotEmpty()
|
|
|
|
|
? Declaration::query()
|
|
|
|
|
->withTrashed()
|
|
|
|
|
->with('client:id,company_name')
|
|
|
|
|
->whereIn('id', $declarationSubjectIds)
|
|
|
|
|
->get()
|
|
|
|
|
->keyBy('id')
|
|
|
|
|
: collect();
|
|
|
|
|
|
|
|
|
|
// Batch-load clients referenced as subjects
|
|
|
|
|
$clientSubjectIds = $activities
|
|
|
|
|
->where('subject_type', 'App\\Models\\Client')
|
|
|
|
|
->pluck('subject_id')
|
|
|
|
|
->unique()
|
|
|
|
|
->filter()
|
|
|
|
|
->values();
|
|
|
|
|
|
|
|
|
|
$existingClientIds = $clientSubjectIds->isNotEmpty()
|
|
|
|
|
? \App\Models\Client::query()
|
|
|
|
|
->whereIn('id', $clientSubjectIds)
|
|
|
|
|
->pluck('id')
|
|
|
|
|
->flip()
|
|
|
|
|
: collect();
|
|
|
|
|
|
|
|
|
|
// Batch-load reassigned user names
|
|
|
|
|
$reassignedUserIds = $activities
|
|
|
|
|
->filter(fn (Activity $a) => $a->subject_type === 'App\\Models\\Declaration' && $a->event === 'updated')
|
|
|
|
|
->map(fn (Activity $a) => $a->properties->get('attributes', [])['assigned_to'] ?? null)
|
|
|
|
|
->filter()
|
|
|
|
|
->unique()
|
|
|
|
|
->values();
|
|
|
|
|
|
|
|
|
|
$reassignedUsersMap = $reassignedUserIds->isNotEmpty()
|
|
|
|
|
? \App\Models\User::query()
|
|
|
|
|
->whereIn('id', $reassignedUserIds)
|
|
|
|
|
->pluck('name', 'id')
|
|
|
|
|
: collect();
|
|
|
|
|
|
|
|
|
|
return $activities->map(function (Activity $activity) use ($typeLabels, $declarationsMap, $existingClientIds, $reassignedUsersMap) {
|
|
|
|
|
$actorName = $activity->causer?->name ?? 'Système';
|
|
|
|
|
$names = explode(' ', trim($actorName));
|
|
|
|
|
$actorInitials = count($names) >= 2
|
|
|
|
|
? strtoupper(mb_substr($names[0], 0, 1).mb_substr(end($names), 0, 1))
|
|
|
|
|
: strtoupper(mb_substr($actorName, 0, 1));
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'id' => $activity->id,
|
|
|
|
|
'actorName' => $actorName,
|
|
|
|
|
'actorInitials' => $actorInitials,
|
|
|
|
|
'description' => $this->formatActivityDescription($activity, $typeLabels, $declarationsMap, $reassignedUsersMap),
|
|
|
|
|
'targetUrl' => $this->resolveActivityTargetUrl($activity, $declarationsMap, $existingClientIds),
|
|
|
|
|
'targetLabel' => $activity->subject_type
|
|
|
|
|
? class_basename($activity->subject_type)
|
|
|
|
|
: null,
|
|
|
|
|
'timestamp' => $activity->created_at->toISOString(),
|
|
|
|
|
'eventType' => $this->resolveEventType($activity),
|
|
|
|
|
];
|
|
|
|
|
})->values()->toArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Format a Spatie activity record into a human-readable French description.
|
|
|
|
|
*
|
|
|
|
|
* @param array<string, string> $typeLabels
|
|
|
|
|
* @param \Illuminate\Support\Collection $declarationsMap Pre-loaded declarations keyed by ID
|
|
|
|
|
* @param \Illuminate\Support\Collection $reassignedUsersMap Pre-loaded user names keyed by ID
|
|
|
|
|
*/
|
|
|
|
|
private function formatActivityDescription(Activity $activity, array $typeLabels, $declarationsMap, $reassignedUsersMap): string
|
|
|
|
|
{
|
|
|
|
|
$actorName = $activity->causer?->name ?? 'Système';
|
|
|
|
|
$subjectType = $activity->subject_type ? class_basename($activity->subject_type) : null;
|
|
|
|
|
$event = $activity->event;
|
|
|
|
|
$properties = $activity->properties;
|
|
|
|
|
$old = $properties->get('old', []);
|
|
|
|
|
$attributes = $properties->get('attributes', []);
|
|
|
|
|
|
|
|
|
|
if ($subjectType === 'Declaration') {
|
|
|
|
|
$typeValue = $attributes['type'] ?? $old['type'] ?? null;
|
|
|
|
|
$typeLabel = $typeValue ? ($typeLabels[$typeValue] ?? $typeValue) : 'déclaration';
|
|
|
|
|
|
|
|
|
|
$declaration = $declarationsMap->get($activity->subject_id);
|
|
|
|
|
$clientName = $declaration?->client?->company_name ?? 'client supprimé';
|
|
|
|
|
|
|
|
|
|
if ($event === 'created') {
|
|
|
|
|
return "{$actorName} a créé la déclaration {$typeLabel} pour {$clientName}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($event === 'updated') {
|
|
|
|
|
if (isset($attributes['status']) && isset($old['status'])) {
|
|
|
|
|
$statusLabels = DeclarationStatus::labels();
|
|
|
|
|
$newStatus = $statusLabels[$attributes['status']] ?? $attributes['status'];
|
|
|
|
|
|
|
|
|
|
return "{$actorName} a changé le statut de {$typeLabel} ({$clientName}) en {$newStatus}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isset($attributes['assigned_to']) && isset($old['assigned_to'])) {
|
|
|
|
|
$newAssignee = $reassignedUsersMap->get($attributes['assigned_to'], 'inconnu');
|
|
|
|
|
|
|
|
|
|
return "{$actorName} a réassigné {$typeLabel} ({$clientName}) à {$newAssignee}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "{$actorName} a modifié la déclaration {$typeLabel} ({$clientName})";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($event === 'deleted') {
|
|
|
|
|
return "{$actorName} a supprimé la déclaration {$typeLabel} ({$clientName})";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($subjectType === 'Client') {
|
|
|
|
|
$clientName = $attributes['company_name'] ?? $old['company_name'] ?? 'client';
|
|
|
|
|
if ($event === 'created') {
|
|
|
|
|
return "{$actorName} a créé le client {$clientName}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "{$actorName} a modifié le client {$clientName}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($subjectType === 'WorkspaceUser' || $subjectType === 'TeamInvitation') {
|
|
|
|
|
if ($event === 'updated' && isset($attributes['role'])) {
|
|
|
|
|
return "{$actorName} a changé le rôle d'un membre";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "{$actorName} a modifié l'équipe";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "{$actorName} a modifié {$subjectType}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Resolve the target URL for an activity entry.
|
|
|
|
|
*
|
|
|
|
|
* @param \Illuminate\Support\Collection $declarationsMap Pre-loaded declarations (with trashed) keyed by ID
|
|
|
|
|
* @param \Illuminate\Support\Collection $existingClientIds Pre-loaded existing (non-deleted) client IDs
|
|
|
|
|
*/
|
|
|
|
|
private function resolveActivityTargetUrl(Activity $activity, $declarationsMap, $existingClientIds): ?string
|
|
|
|
|
{
|
|
|
|
|
$subjectType = $activity->subject_type ? class_basename($activity->subject_type) : null;
|
|
|
|
|
$subjectId = $activity->subject_id;
|
|
|
|
|
|
|
|
|
|
if (! $subjectType || ! $subjectId) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($subjectType === 'Declaration') {
|
|
|
|
|
$declaration = $declarationsMap->get($subjectId);
|
|
|
|
|
|
|
|
|
|
if ($declaration && ! $declaration->trashed()) {
|
|
|
|
|
return route('declarations.show', $subjectId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($subjectType === 'Client') {
|
|
|
|
|
if ($existingClientIds->has($subjectId)) {
|
|
|
|
|
return route('clients.show', $subjectId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($subjectType === 'WorkspaceUser' || $subjectType === 'TeamInvitation') {
|
|
|
|
|
return route('team.index');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Resolve the event type hint for frontend icon selection.
|
|
|
|
|
*/
|
|
|
|
|
private function resolveEventType(Activity $activity): string
|
|
|
|
|
{
|
|
|
|
|
$subjectType = $activity->subject_type ? class_basename($activity->subject_type) : null;
|
|
|
|
|
$event = $activity->event;
|
|
|
|
|
$attributes = $activity->properties->get('attributes', []);
|
|
|
|
|
|
|
|
|
|
if ($subjectType === 'Declaration') {
|
|
|
|
|
if ($event === 'created') {
|
|
|
|
|
return 'declaration_created';
|
|
|
|
|
}
|
|
|
|
|
if (isset($attributes['status'])) {
|
|
|
|
|
return 'status_change';
|
|
|
|
|
}
|
|
|
|
|
if (isset($attributes['assigned_to'])) {
|
|
|
|
|
return 'reassignment';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'declaration_updated';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($subjectType === 'Client') {
|
|
|
|
|
return 'client_updated';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($subjectType === 'WorkspaceUser' || $subjectType === 'TeamInvitation') {
|
|
|
|
|
return 'role_change';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'default';
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 12:00:24 +00:00
|
|
|
/**
|
|
|
|
|
* 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',
|
|
|
|
|
];
|
|
|
|
|
}
|
2026-03-11 23:33:10 +00:00
|
|
|
}
|