2026-03-11 23:33:10 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
|
|
feat: add notification center with bell dropdown, full page, and workspace scoping (Story 3.3)
Enhance NotificationDropdown with type-specific icons, French description builder,
click-to-navigate with mark-as-read, and "Voir toutes les notifications" link.
Add full notifications page at /notifications with pagination (25/page), individual
mark-as-read, and empty state. Includes code review fixes: workspace-scoped unread
count and dropdown items, race condition fix (mark-as-read before navigate),
efficient markAllAsRead via direct update, deleted declaration URL handling,
and per-workspace cache keys. 7 new feature tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:26:40 +01:00
|
|
|
use App\Models\Declaration;
|
|
|
|
|
use App\Models\User;
|
2026-03-18 00:12:50 +00:00
|
|
|
use App\Models\WorkspaceUser;
|
2026-03-11 23:33:10 +00:00
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
|
use Inertia\Inertia;
|
|
|
|
|
use Inertia\Middleware;
|
|
|
|
|
|
|
|
|
|
class HandleInertiaRequests extends Middleware
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* The root template that's loaded on the first page visit.
|
|
|
|
|
*
|
|
|
|
|
* @see https://inertiajs.com/server-side-setup#root-template
|
|
|
|
|
*
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
protected $rootView = 'app';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determines the current asset version.
|
|
|
|
|
*
|
|
|
|
|
* @see https://inertiajs.com/asset-versioning
|
|
|
|
|
*/
|
|
|
|
|
public function version(Request $request): ?string
|
|
|
|
|
{
|
|
|
|
|
return parent::version($request);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Define the props that are shared by default.
|
|
|
|
|
*
|
|
|
|
|
* @see https://inertiajs.com/shared-data
|
|
|
|
|
*
|
|
|
|
|
* @return array<string, mixed>
|
|
|
|
|
*/
|
|
|
|
|
public function share(Request $request): array
|
|
|
|
|
{
|
|
|
|
|
$user = $request->user();
|
|
|
|
|
|
|
|
|
|
$workspaces = $user
|
|
|
|
|
? $user->workspaces()
|
|
|
|
|
->orderBy('name')
|
|
|
|
|
->get(['workspaces.id', 'workspaces.name', 'workspaces.slug'])
|
|
|
|
|
->map(fn ($w) => [
|
|
|
|
|
'id' => $w->id,
|
|
|
|
|
'name' => $w->name,
|
|
|
|
|
'slug' => $w->slug,
|
|
|
|
|
])
|
|
|
|
|
->values()
|
|
|
|
|
->all()
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
$currentWorkspaceId = $request->session()->get('current_workspace_id');
|
|
|
|
|
$currentWorkspace = collect($workspaces)->firstWhere('id', $currentWorkspaceId)
|
|
|
|
|
?? ($workspaces[0] ?? null);
|
|
|
|
|
|
|
|
|
|
if (! $currentWorkspaceId && count($workspaces) > 0) {
|
|
|
|
|
$request->session()->put('current_workspace_id', $currentWorkspace['id']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
...parent::share($request),
|
2026-03-18 00:12:50 +00:00
|
|
|
'flash' => [
|
|
|
|
|
'success' => $request->session()->get('success'),
|
|
|
|
|
'error' => $request->session()->get('error'),
|
|
|
|
|
],
|
2026-03-11 23:33:10 +00:00
|
|
|
'name' => config('app.name'),
|
|
|
|
|
'auth' => [
|
|
|
|
|
'user' => $user,
|
|
|
|
|
'workspaces' => $workspaces,
|
|
|
|
|
'currentWorkspace' => $currentWorkspace,
|
2026-03-18 00:12:50 +00:00
|
|
|
'workspaceRole' => $user && $currentWorkspace
|
|
|
|
|
? WorkspaceUser::where('user_id', $user->id)
|
|
|
|
|
->where('workspace_id', $currentWorkspace['id'])
|
|
|
|
|
->first()?->role?->value
|
|
|
|
|
: null,
|
|
|
|
|
'workspaceSwitchUrl' => $user ? route('workspace.switch') : null,
|
2026-03-11 23:33:10 +00:00
|
|
|
],
|
|
|
|
|
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
|
|
|
|
'userNotifications' => [
|
feat: add notification center with bell dropdown, full page, and workspace scoping (Story 3.3)
Enhance NotificationDropdown with type-specific icons, French description builder,
click-to-navigate with mark-as-read, and "Voir toutes les notifications" link.
Add full notifications page at /notifications with pagination (25/page), individual
mark-as-read, and empty state. Includes code review fixes: workspace-scoped unread
count and dropdown items, race condition fix (mark-as-read before navigate),
efficient markAllAsRead via direct update, deleted declaration URL handling,
and per-workspace cache keys. 7 new feature tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:26:40 +01:00
|
|
|
'unread_count' => $user && $currentWorkspace ? Cache::remember(
|
|
|
|
|
"user:{$user->id}:workspace:{$currentWorkspace['id']}:unread_notifications",
|
2026-03-11 23:33:10 +00:00
|
|
|
60,
|
feat: add notification center with bell dropdown, full page, and workspace scoping (Story 3.3)
Enhance NotificationDropdown with type-specific icons, French description builder,
click-to-navigate with mark-as-read, and "Voir toutes les notifications" link.
Add full notifications page at /notifications with pagination (25/page), individual
mark-as-read, and empty state. Includes code review fixes: workspace-scoped unread
count and dropdown items, race condition fix (mark-as-read before navigate),
efficient markAllAsRead via direct update, deleted declaration URL handling,
and per-workspace cache keys. 7 new feature tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:26:40 +01:00
|
|
|
fn () => $user->unreadNotifications()
|
|
|
|
|
->whereJsonContains('data->workspace_id', $currentWorkspace['id'])
|
|
|
|
|
->count()
|
2026-03-11 23:33:10 +00:00
|
|
|
) : 0,
|
|
|
|
|
'readUrl' => fn () => $user ? route('notifications.read', ['id' => '__ID__']) : null,
|
|
|
|
|
'readAllUrl' => fn () => $user ? route('notifications.readAll') : null,
|
feat: add notification center with bell dropdown, full page, and workspace scoping (Story 3.3)
Enhance NotificationDropdown with type-specific icons, French description builder,
click-to-navigate with mark-as-read, and "Voir toutes les notifications" link.
Add full notifications page at /notifications with pagination (25/page), individual
mark-as-read, and empty state. Includes code review fixes: workspace-scoped unread
count and dropdown items, race condition fix (mark-as-read before navigate),
efficient markAllAsRead via direct update, deleted declaration URL handling,
and per-workspace cache keys. 7 new feature tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:26:40 +01:00
|
|
|
'notificationsUrl' => fn () => $user ? route('notifications.index') : null,
|
|
|
|
|
'items' => Inertia::defer(function () use ($user, $currentWorkspace) {
|
|
|
|
|
if (! $user || ! $currentWorkspace) {
|
2026-03-11 23:33:10 +00:00
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
feat: add notification center with bell dropdown, full page, and workspace scoping (Story 3.3)
Enhance NotificationDropdown with type-specific icons, French description builder,
click-to-navigate with mark-as-read, and "Voir toutes les notifications" link.
Add full notifications page at /notifications with pagination (25/page), individual
mark-as-read, and empty state. Includes code review fixes: workspace-scoped unread
count and dropdown items, race condition fix (mark-as-read before navigate),
efficient markAllAsRead via direct update, deleted declaration URL handling,
and per-workspace cache keys. 7 new feature tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:26:40 +01:00
|
|
|
$notifications = $user->notifications()
|
|
|
|
|
->whereJsonContains('data->workspace_id', $currentWorkspace['id'])
|
|
|
|
|
->latest()
|
|
|
|
|
->take(10)
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
$declarationIds = $notifications->pluck('data.declaration_id')->filter()->unique()->values();
|
|
|
|
|
$senderIds = $notifications->pluck('data.sender_id')->filter()->unique()->values();
|
|
|
|
|
|
|
|
|
|
$declarations = Declaration::whereIn('id', $declarationIds)->pluck('title', 'id');
|
|
|
|
|
$senders = User::whereIn('id', $senderIds)->pluck('name', 'id');
|
|
|
|
|
|
|
|
|
|
return $notifications->map(function ($n) use ($declarations, $senders) {
|
|
|
|
|
$data = $n->data;
|
|
|
|
|
$declarationId = $data['declaration_id'] ?? null;
|
|
|
|
|
$senderId = $data['sender_id'] ?? null;
|
|
|
|
|
|
|
|
|
|
$declarationExists = $declarationId && isset($declarations[$declarationId]);
|
|
|
|
|
$data['declaration_title'] = $declarationExists ? $declarations[$declarationId] : null;
|
|
|
|
|
$data['sender_name'] = $senderId ? ($senders[$senderId] ?? null) : null;
|
|
|
|
|
$data['url'] = $declarationExists ? route('declarations.show', $declarationId) : null;
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'id' => $n->id,
|
|
|
|
|
'type' => class_basename($n->type),
|
|
|
|
|
'data' => $data,
|
|
|
|
|
'read_at' => $n->read_at?->toISOString(),
|
|
|
|
|
'created_at' => $n->created_at->diffForHumans(),
|
|
|
|
|
];
|
|
|
|
|
})->all();
|
2026-03-11 23:33:10 +00:00
|
|
|
} catch (\Throwable) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|