app->singleton(AlertService::class); $this->app->singleton(DashboardWidgetRegistry::class); $this->app->singleton(NavigationRegistry::class); $this->app->singleton(ModuleManager::class, fn ($app) => new ModuleManager($app)); $this->app->singleton(WeatherProviderRegistry::class, function ($app) { $registry = new WeatherProviderRegistry($app); $registry->register('openmeteo_free', OpenMeteoFreeProvider::class); $registry->register('openmeteo_api', OpenMeteoApiProvider::class); $registry->register('brightsky', BrightSkyProvider::class); $registry->register('met_norway', MetNorwayProvider::class); return $registry; }); if (empty(config('app.key'))) { config(['app.key' => 'base64:'.base64_encode(random_bytes(32))]); } } /** * Bootstrap any application services. */ public function boot(): void { TrustProxies::at('*'); TrustProxies::withHeaders( Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST ); if (str_starts_with(config('app.url'), 'https://')) { URL::forceScheme('https'); } $locale = Setting::get('default_locale'); if ($locale && in_array($locale, ['de', 'en'], true)) { App::setLocale($locale); } Gate::policy(Job::class, JobPolicy::class); Route::bind('driver', fn (string $value) => User::drivers()->findOrFail($value)); Route::model('serviceJob', Job::class); Route::model('object', CustomerObject::class); View::composer('layouts.admin', function ($view) { try { $view->with('alertCount', app(AlertService::class)->openCount()); } catch (\Exception) { $view->with('alertCount', 0); } }); $this->registerCoreNavigation(); $this->registerCoreDashboardWidgets(); $this->app->booted(function () { app(ModuleManager::class)->boot(); }); } private function registerCoreNavigation(): void { $nav = app(NavigationRegistry::class); $nav->addGroup('top', '', 0); $nav->addGroup('stammdaten', __('admin.nav_group_master_data'), 10); $nav->addGroup('einsaetze', __('admin.nav_group_operations'), 20); $nav->addGroup('auswertungen', __('admin.nav_group_reports'), 30); $nav->addGroup('system', __('admin.nav_group_system'), 40); // Top-level $nav->addItem( group: 'top', slug: 'dashboard', label: __('admin.nav_dashboard'), route: 'admin.dashboard', icon: 'M2.25 12l8.954-8.955a1.126 1.126 0 011.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25', order: 10, activePattern: 'admin.dashboard', ); // Stammdaten $nav->addItem( group: 'stammdaten', slug: 'customers', label: __('admin.nav_customers'), route: 'admin.customers.index', icon: 'M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z', order: 10, activePattern: 'admin.customers.*', ); $nav->addItem( group: 'stammdaten', slug: 'drivers', label: __('admin.nav_drivers'), route: 'admin.drivers.index', icon: 'M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z', order: 20, activePattern: 'admin.drivers.*&!admin.drivers.archived', ); $nav->addItem( group: 'stammdaten', slug: 'archived-drivers', label: __('admin.nav_archived_drivers'), route: 'admin.drivers.archived', icon: 'M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z', order: 30, activePattern: 'admin.drivers.archived', ); $nav->addItem( group: 'stammdaten', slug: 'vehicles', label: __('admin.nav_vehicles'), route: 'admin.vehicles.index', icon: 'M8.25 18.75a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 01-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0H6.375c-.621 0-1.125-.504-1.125-1.125V14.25m17.25 0V6.375c0-.621-.504-1.125-1.125-1.125H4.125c-.621 0-1.125.504-1.125 1.125v7.875m18 0h-1.5m-17.25 0h1.5', order: 40, activePattern: 'admin.vehicles.*', ); // Einsätze $nav->addItem( group: 'einsaetze', slug: 'jobs', label: __('admin.nav_jobs'), route: 'admin.jobs.index', icon: 'M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z', order: 10, routeCheck: 'admin.jobs.index', activePattern: 'admin.jobs.*', ); $nav->addItem( group: 'einsaetze', slug: 'workshifts', label: __('admin.nav_workshifts'), route: 'admin.workshifts.index', icon: 'M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z', order: 20, routeCheck: 'admin.workshifts.index', activePattern: 'admin.workshifts.*', ); // Auswertungen $nav->addItem( group: 'auswertungen', slug: 'overview-daily', label: __('admin.nav_overview_daily'), route: 'admin.overview.daily', icon: 'M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5m-9-6h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z', order: 10, routeCheck: 'admin.overview.daily', activePattern: 'admin.overview.daily', ); $nav->addItem( group: 'auswertungen', slug: 'overview-monthly', label: __('admin.nav_overview_monthly'), route: 'admin.overview.monthly', icon: 'M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5||M3 11.25h18', order: 20, routeCheck: 'admin.overview.monthly', activePattern: 'admin.overview.monthly', ); $nav->addItem( group: 'auswertungen', slug: 'overview-driver-report', label: __('admin.nav_overview_driver_report'), route: 'admin.overview.driver-report', icon: 'M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z', order: 30, routeCheck: 'admin.overview.driver-report', activePattern: 'admin.overview.driver-report', ); $nav->addItem( group: 'auswertungen', slug: 'overview-customer-report', label: __('admin.nav_overview_customer_report'), route: 'admin.overview.customer-report', icon: 'M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z', order: 40, routeCheck: 'admin.overview.customer-report', activePattern: 'admin.overview.customer-report', ); $nav->addItem( group: 'auswertungen', slug: 'exports-csv', label: __('admin.nav_exports'), route: 'admin.exports.csv', icon: 'M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m.75 12l3 3m0 0l3-3m-3 3v-6m-1.5-9H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z', order: 50, routeCheck: 'admin.exports.csv', activePattern: 'admin.exports.*', ); // System $nav->addItem( group: 'system', slug: 'alerts', label: __('admin.nav_alerts'), route: 'admin.alerts.index', icon: 'M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z', order: 10, routeCheck: 'admin.alerts.index', activePattern: 'admin.alerts.*', badge: 'alertCount', ); $nav->addItem( group: 'system', slug: 'gps-status', label: __('admin.nav_gps_status'), route: 'admin.owntracks.overview', icon: 'M15 10.5a3 3 0 11-6 0 3 3 0 016 0z||M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z', order: 20, routeCheck: 'admin.owntracks.overview', activePattern: 'admin.owntracks.*', ); $nav->addItem( group: 'system', slug: 'dsgvo', label: __('admin.nav_dsgvo'), route: 'admin.dsgvo.index', icon: 'M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z', order: 30, activePattern: 'admin.dsgvo.*', ); $nav->addItem( group: 'system', slug: 'help', label: __('admin.nav_help'), route: 'admin.help.index', icon: 'M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M12 18.75h.007v.008H12v-.008z||M21 12a9 9 0 11-18 0 9 9 0 0118 0z', order: 40, routeCheck: 'admin.help.index', activePattern: 'admin.help.*', ); $nav->addItem( group: 'system', slug: 'settings', label: __('admin.nav_settings'), route: 'admin.settings.index', icon: 'M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z||M15 12a3 3 0 11-6 0 3 3 0 016 0z', order: 50, activePattern: 'admin.settings.*', ); $nav->addItem( group: 'system', slug: 'modules', label: __('admin.nav_modules'), route: 'admin.settings.modules.index', icon: 'M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9', order: 51, activePattern: 'admin.settings.modules.*', ); } private function registerCoreDashboardWidgets(): void { $registry = app(DashboardWidgetRegistry::class); $registry->registerWidget('onboarding-checklist', [ 'label' => 'Onboarding Checklist', 'view' => 'admin.dashboard.widgets.onboarding-checklist', 'order' => 10, 'size' => 'full', 'condition' => function () { if (Setting::get('onboarding_dismissed') === '1') { return false; } $customerCount = Customer::count(); $driverCount = User::drivers()->count(); $vehicleCount = Vehicle::count(); $cronLastRun = cache()->get('cron.last_run'); $cronOk = $cronLastRun && $cronLastRun->diffInMinutes(now()) < 5; $mailConfigured = ! empty(config('mail.mailers.smtp.host')); $onboarding = [ 'company' => ! empty(Setting::get('company_name')), 'driver' => $driverCount > 0, 'customer' => $customerCount > 0, 'vehicle' => $vehicleCount > 0, 'email' => $mailConfigured, 'cron' => $cronOk, ]; $onboardingComplete = ! in_array(false, $onboarding, true); if ($onboardingComplete) { Setting::set('onboarding_dismissed', '1'); return false; } return true; }, 'dataCallback' => function () { $customerCount = Customer::count(); $driverCount = User::drivers()->count(); $vehicleCount = Vehicle::count(); $cronLastRun = cache()->get('cron.last_run'); $cronOk = $cronLastRun && $cronLastRun->diffInMinutes(now()) < 5; $mailConfigured = ! empty(config('mail.mailers.smtp.host')); return [ 'onboarding' => [ 'company' => ! empty(Setting::get('company_name')), 'driver' => $driverCount > 0, 'customer' => $customerCount > 0, 'vehicle' => $vehicleCount > 0, 'email' => $mailConfigured, 'cron' => $cronOk, ], ]; }, ]); $registry->registerWidget('cron-warning', [ 'label' => 'Cron Warning', 'view' => 'admin.dashboard.widgets.cron-warning', 'order' => 20, 'size' => 'full', 'condition' => function () { $cronLastRun = cache()->get('cron.last_run'); return ! ($cronLastRun && $cronLastRun->diffInMinutes(now()) < 5); }, 'dataCallback' => null, ]); $registry->registerWidget('stat-cards', [ 'label' => 'Statistics', 'view' => 'admin.dashboard.widgets.stat-cards', 'order' => 30, 'size' => 'full', 'dataCallback' => function () { return [ 'customerCount' => Customer::count(), 'driverCount' => User::drivers()->count(), 'vehicleCount' => Vehicle::count(), ]; }, ]); $registry->registerWidget('retention-hint', [ 'label' => 'Retention Hint', 'view' => 'admin.dashboard.widgets.retention-hint', 'order' => 40, 'size' => 'full', 'condition' => function () { return app(RetentionService::class)->getRetentionStats() !== null; }, 'dataCallback' => function () { return [ 'retentionStats' => app(RetentionService::class)->getRetentionStats(), ]; }, ]); $registry->registerWidget('weather-forecast', [ 'label' => 'Weather', 'view' => 'admin.dashboard.widgets.weather-forecast', 'order' => 50, 'size' => 'full', 'dataCallback' => function () { $companyLat = Setting::get('company_lat'); $companyLon = Setting::get('company_lon'); $weather = null; $weatherMissing = false; if ($companyLat && $companyLon) { $weather = app(ForecastService::class)->current((float) $companyLat, (float) $companyLon); } else { $weatherMissing = true; } return [ 'weather' => $weather, 'weatherMissing' => $weatherMissing, ]; }, ]); $registry->registerWidget('season-statistics', [ 'label' => 'Season Statistics', 'view' => 'admin.dashboard.widgets.season-statistics', 'order' => 60, 'size' => 'full', 'dataCallback' => function () { $season = app(SeasonService::class)->currentOrLastSeason(); $isSqlite = DB::getDriverName() === 'sqlite'; $monthExpr = $isSqlite ? "CAST(strftime('%m', started_at) AS INTEGER)" : 'MONTH(started_at)'; $jobsPerMonth = Job::selectRaw("{$monthExpr} as month, COUNT(*) as job_count") ->whereBetween('started_at', [$season->start, $season->end]) ->groupByRaw($monthExpr) ->pluck('job_count', 'month'); $durationExpr = $isSqlite ? "SUM((JULIANDAY(COALESCE(ended_at, started_at)) - JULIANDAY(started_at)) * 1440)" : "SUM(TIMESTAMPDIFF(MINUTE, started_at, COALESCE(ended_at, started_at)))"; $seasonTotalMinutes = (int) Job::selectRaw("{$durationExpr} as total_minutes") ->whereBetween('started_at', [$season->start, $season->end]) ->whereNotNull('ended_at') ->value('total_minutes'); $seasonJobCount = Job::whereBetween('started_at', [$season->start, $season->end])->count(); return [ 'season' => $season, 'jobsPerMonth' => $jobsPerMonth, 'seasonTotalMinutes' => $seasonTotalMinutes, 'seasonJobCount' => $seasonJobCount, ]; }, ]); $registry->registerWidget('driver-ranking', [ 'label' => 'Driver Ranking', 'view' => 'admin.dashboard.widgets.driver-ranking', 'order' => 70, 'size' => 'full', 'dataCallback' => function () { $season = app(SeasonService::class)->currentOrLastSeason(); $isSqlite = DB::getDriverName() === 'sqlite'; $durationExpr = $isSqlite ? "SUM((JULIANDAY(COALESCE(ended_at, started_at)) - JULIANDAY(started_at)) * 1440)" : "SUM(TIMESTAMPDIFF(MINUTE, started_at, COALESCE(ended_at, started_at)))"; $drivers = User::withAnonymized() ->drivers() ->withCount(['serviceJobs as season_jobs' => fn ($q) => $q->whereBetween('started_at', [$season->start, $season->end])]) ->get(); $driverMinutes = Job::selectRaw("user_id, {$durationExpr} as total_minutes") ->whereBetween('started_at', [$season->start, $season->end]) ->whereNotNull('ended_at') ->groupBy('user_id') ->pluck('total_minutes', 'user_id'); $driverRanking = $drivers->map(function ($driver) use ($driverMinutes) { return (object) [ 'driver' => $driver, 'season_jobs' => $driver->season_jobs, 'total_minutes' => (int) ($driverMinutes[$driver->id] ?? 0), ]; })->sortByDesc('season_jobs')->values(); return ['driverRanking' => $driverRanking]; }, ]); $registry->registerWidget('recent-jobs', [ 'label' => 'Recent Jobs', 'view' => 'admin.dashboard.widgets.recent-jobs', 'order' => 80, 'size' => 'full', 'dataCallback' => function () { return [ 'recentJobs' => Job::with(['customer', 'customerObject.customer', 'user' => fn ($q) => $q->withAnonymized()]) ->whereNotNull('ended_at') ->latest('started_at') ->take(10) ->get(), ]; }, ]); $registry->registerWidget('system-status', [ 'label' => 'System Status', 'view' => 'admin.dashboard.widgets.system-status', 'order' => 90, 'size' => 'full', 'dataCallback' => function () { $pendingQueueJobs = 0; $failedQueueJobs = 0; try { if (Schema::hasTable('jobs')) { $pendingQueueJobs = DB::table('jobs')->count(); } if (Schema::hasTable('failed_jobs')) { $failedQueueJobs = DB::table('failed_jobs')->count(); } } catch (\Throwable) { } return [ 'pendingQueueJobs' => $pendingQueueJobs, 'failedQueueJobs' => $failedQueueJobs, 'photoCount' => JobPhoto::count(), ]; }, ]); $registry->registerWidget('update-check', [ 'label' => 'Update Check', 'view' => 'admin.dashboard.widgets.update-check', 'order' => 100, 'size' => 'full', 'condition' => function () { if (! function_exists('sodium_crypto_sign_verify_detached')) { return false; } try { return (new SchneespurUpdater)->getState() !== null; } catch (\Throwable) { return false; } }, 'dataCallback' => function () { return [ 'updateState' => (new SchneespurUpdater)->getState(), ]; }, ]); $registry->registerWidget('alert-cards', [ 'label' => 'Alerts', 'view' => 'admin.dashboard.widgets.alert-cards', 'order' => 110, 'size' => 'full', 'condition' => function () { return Route::has('admin.alerts.index'); }, 'dataCallback' => function () { $alertService = app(AlertService::class); $alertCounts = $alertService->counts(); $alertTypes = [ 'missing_gps' => [ 'border' => 'border-red-500', 'bg' => 'bg-red-50', 'text' => 'text-red-700', 'count_text' => 'text-red-600', 'recentJobs' => $alertCounts['missing_gps'] > 0 ? $alertService->missingGpsQuery()->with('customer')->latest('started_at')->take(3)->get() : collect(), ], 'missing_weather' => [ 'border' => 'border-orange-500', 'bg' => 'bg-orange-50', 'text' => 'text-orange-700', 'count_text' => 'text-orange-600', 'recentJobs' => $alertCounts['missing_weather'] > 0 ? $alertService->missingWeatherQuery()->with('customer')->latest('started_at')->take(3)->get() : collect(), ], 'overdue' => [ 'border' => 'border-yellow-500', 'bg' => 'bg-yellow-50', 'text' => 'text-yellow-700', 'count_text' => 'text-yellow-600', 'recentJobs' => $alertCounts['overdue'] > 0 ? $alertService->overdueQuery()->with('customer')->latest('started_at')->take(3)->get() : collect(), ], ]; return [ 'alertCounts' => $alertCounts, 'alertTypes' => $alertTypes, ]; }, ]); } }