startOfDay(); $yesterday = now()->subDay()->startOfDay(); $staleThreshold = now()->subDays(7); // assumption: stale if no activity in last 7 days $clientsTotal = Client::count(); $clientsNew7d = Client::where('created_at', '>=', now()->subDays(7))->count(); // FieldJob table does not have a scheduled_at column (schema shows: assigned_at, completed_at, cancelled_at) // Temporary logic: if scheduled_at ever added we'll use it; otherwise fall back to assigned_at then created_at. if (Schema::hasColumn('field_jobs', 'scheduled_at')) { $fieldJobsToday = FieldJob::whereDate('scheduled_at', $today)->count(); } else { // Prefer assigned_at when present, otherwise created_at $fieldJobsToday = FieldJob::whereDate(DB::raw('COALESCE(assigned_at, created_at)'), $today)->count(); } $documentsToday = Document::whereDate('created_at', $today)->count(); $activeImports = Import::whereIn('status', ['queued', 'processing'])->count(); $activeContracts = Contract::where('active', 1)->count(); // Basic activities deferred list (limit 10) $activities = Activity::query() ->with(['clientCase:id,uuid']) ->latest() ->limit(10) ->get(['id', 'note', 'created_at', 'client_case_id', 'contract_id', 'action_id', 'decision_id']) ->map(fn ($a) => [ 'id' => $a->id, 'note' => $a->note, 'created_at' => $a->created_at, 'client_case_id' => $a->client_case_id, 'client_case_uuid' => $a->clientCase?->uuid, 'contract_id' => $a->contract_id, 'action_id' => $a->action_id, 'decision_id' => $a->decision_id, ]); // 7-day trends (including today) $start = now()->subDays(6)->startOfDay(); $end = now()->endOfDay(); $dateKeys = collect(range(0, 6)) ->map(fn ($i) => now()->subDays(6 - $i)->format('Y-m-d')); $clientTrendRaw = Client::whereBetween('created_at', [$start, $end]) ->selectRaw('DATE(created_at) as d, COUNT(*) as c') ->groupBy('d') ->pluck('c', 'd'); $documentTrendRaw = Document::whereBetween('created_at', [$start, $end]) ->selectRaw('DATE(created_at) as d, COUNT(*) as c') ->groupBy('d') ->pluck('c', 'd'); $fieldJobTrendRaw = FieldJob::whereBetween(DB::raw('COALESCE(assigned_at, created_at)'), [$start, $end]) ->selectRaw('DATE(COALESCE(assigned_at, created_at)) as d, COUNT(*) as c') ->groupBy('d') ->pluck('c', 'd'); $importTrendRaw = Import::whereBetween('created_at', [$start, $end]) ->selectRaw('DATE(created_at) as d, COUNT(*) as c') ->groupBy('d') ->pluck('c', 'd'); // Completed field jobs last 7 days $fieldJobCompletedRaw = FieldJob::whereNotNull('completed_at') ->whereBetween('completed_at', [$start, $end]) ->selectRaw('DATE(completed_at) as d, COUNT(*) as c') ->groupBy('d') ->pluck('c', 'd'); $trends = [ 'clients_new' => $dateKeys->map(fn ($d) => (int) ($clientTrendRaw[$d] ?? 0))->values(), 'documents_new' => $dateKeys->map(fn ($d) => (int) ($documentTrendRaw[$d] ?? 0))->values(), 'field_jobs' => $dateKeys->map(fn ($d) => (int) ($fieldJobTrendRaw[$d] ?? 0))->values(), 'imports_new' => $dateKeys->map(fn ($d) => (int) ($importTrendRaw[$d] ?? 0))->values(), 'field_jobs_completed' => $dateKeys->map(fn ($d) => (int) ($fieldJobCompletedRaw[$d] ?? 0))->values(), 'labels' => $dateKeys, ]; // Stale client cases (no activity in last 7 days) $staleCases = \App\Models\ClientCase::query() ->leftJoin('activities', function ($join) { $join->on('activities.client_case_id', '=', 'client_cases.id') ->whereNull('activities.deleted_at'); }) ->selectRaw('client_cases.id, client_cases.uuid, client_cases.client_ref, MAX(activities.created_at) as last_activity_at, client_cases.created_at') ->groupBy('client_cases.id', 'client_cases.uuid', 'client_cases.client_ref', 'client_cases.created_at') ->havingRaw('(MAX(activities.created_at) IS NULL OR MAX(activities.created_at) < ?) AND client_cases.created_at < ?', [$staleThreshold, $staleThreshold]) ->orderByRaw('last_activity_at NULLS FIRST, client_cases.created_at ASC') ->limit(10) ->get() ->map(function ($c) { // Reference point: last activity if exists, else creation. $reference = $c->last_activity_at ? \Illuminate\Support\Carbon::parse($c->last_activity_at) : $c->created_at; // Use minute precision to avoid jumping to 1 too early (e.g. created just before midnight). $minutes = $reference ? max(0, $reference->diffInMinutes(now())) : 0; $daysFraction = $minutes / 1440; // 60 * 24 // Provide both fractional and integer versions (integer preserved for backwards compatibility if needed) $daysInteger = (int) floor($daysFraction); return [ 'id' => $c->id, 'uuid' => $c->uuid, 'client_ref' => $c->client_ref, 'last_activity_at' => $c->last_activity_at, 'created_at' => $c->created_at, 'days_without_activity' => round($daysFraction, 4), // fractional for finer UI decision (<1 day) 'days_stale' => $daysInteger, // legacy key (integer) 'has_activity' => (bool) $c->last_activity_at, ]; }); // Field jobs assigned today $fieldJobsAssignedToday = FieldJob::query() ->whereDate(DB::raw('COALESCE(assigned_at, created_at)'), $today) ->select(['id', 'assigned_user_id', 'priority', 'assigned_at', 'created_at', 'contract_id']) ->with(['contract' => function ($q) { $q->select('id', 'uuid', 'reference', 'client_case_id') ->with(['clientCase:id,uuid,person_id', 'clientCase.person:id,full_name', 'segments:id,name']); }]) ->latest(DB::raw('COALESCE(assigned_at, created_at)')) ->limit(15) ->get() ->map(function ($fj) { $contract = $fj->contract; $segmentId = null; if ($contract && method_exists($contract, 'segments')) { // Determine active segment via pivot active flag if present $activeSeg = $contract->segments->first(); if ($activeSeg && isset($activeSeg->pivot) && ($activeSeg->pivot->active ?? true)) { $segmentId = $activeSeg->id; } } return [ 'id' => $fj->id, 'priority' => $fj->priority, // Normalize to ISO8601 strings so FE retains timezone & time component 'assigned_at' => $fj->assigned_at?->toIso8601String(), 'created_at' => $fj->created_at?->toIso8601String(), 'contract' => $contract ? [ 'uuid' => $contract->uuid, 'reference' => $contract->reference, 'client_case_uuid' => optional($contract->clientCase)->uuid, 'person_full_name' => optional(optional($contract->clientCase)->person)->full_name, 'segment_id' => $segmentId, ] : null, ]; }); // Imports in progress (queued / processing) $importsInProgress = Import::query() ->whereIn('status', ['queued', 'processing']) ->latest('created_at') ->limit(10) ->get(['id', 'uuid', 'file_name', 'status', 'total_rows', 'imported_rows', 'valid_rows', 'invalid_rows', 'started_at']) ->map(fn ($i) => [ 'id' => $i->id, 'uuid' => $i->uuid, 'file_name' => $i->file_name, 'status' => $i->status, 'total_rows' => $i->total_rows, 'imported_rows' => $i->imported_rows, 'valid_rows' => $i->valid_rows, 'invalid_rows' => $i->invalid_rows, 'progress_pct' => $i->total_rows ? round(($i->imported_rows / max(1, $i->total_rows)) * 100, 1) : null, 'started_at' => $i->started_at, ]); // Active document templates summary (active versions) $activeTemplates = \App\Models\DocumentTemplate::query() ->where('active', true) ->latest('updated_at') ->limit(10) ->get(['id', 'name', 'slug', 'version', 'updated_at']); // System health (deferred) $queueBacklog = Schema::hasTable('jobs') ? DB::table('jobs')->count() : null; $failedJobs = Schema::hasTable('failed_jobs') ? DB::table('failed_jobs')->count() : null; $recentActivity = Activity::query()->latest('created_at')->value('created_at'); $lastActivityMinutes = null; if ($recentActivity) { // diffInMinutes is absolute (non-negative) but guard anyway & cast to int $lastActivityMinutes = (int) max(0, now()->diffInMinutes($recentActivity)); } $systemHealth = [ 'queue_backlog' => $queueBacklog, 'failed_jobs' => $failedJobs, 'last_activity_minutes' => $lastActivityMinutes, 'last_activity_iso' => $recentActivity?->toIso8601String(), 'generated_at' => now()->toIso8601String(), ]; return Inertia::render('Dashboard', [ 'kpis' => [ 'clients_total' => $clientsTotal, 'clients_new_7d' => $clientsNew7d, 'field_jobs_today' => $fieldJobsToday, 'documents_today' => $documentsToday, 'active_imports' => $activeImports, 'active_contracts' => $activeContracts, ], 'trends' => $trends, ])->with([ // deferred props (Inertia v2 style) 'activities' => fn () => $activities, 'systemHealth' => fn () => $systemHealth, 'staleCases' => fn () => $staleCases, 'fieldJobsAssignedToday' => fn () => $fieldJobsAssignedToday, 'importsInProgress' => fn () => $importsInProgress, 'activeTemplates' => fn () => $activeTemplates, ]); } }