aexUtil->getAexSettings($businessId); return [ 'inquiry_sla_hours' => max(1, (int) ($settings['inquiry_sla_hours'] ?? 48)), 'inquiry_sla_warning_percent' => min(99, max(1, (int) ($settings['inquiry_sla_warning_percent'] ?? 75))), 'research_task_sla_days' => max(1, (int) ($settings['research_task_sla_days'] ?? 7)), ]; } /** * @return 'ok'|'warning'|'overdue'|'breached'|'na' */ public function inquirySlaStatus(PriceInquiry $inquiry, ?array $sla = null): string { if (! in_array($inquiry->status, ['sent', 'partial'], true)) { return 'na'; } $sla = $sla ?? $this->slaSettings((int) $inquiry->business_id); $now = now(); if ($inquiry->due_date && $inquiry->due_date->lt($now->copy()->startOfDay())) { return 'overdue'; } $sentAt = $this->resolveSentAt($inquiry); if (! $sentAt) { return 'na'; } $deadline = $sentAt->copy()->addHours($sla['inquiry_sla_hours']); $warningAt = $sentAt->copy()->addHours( (int) floor($sla['inquiry_sla_hours'] * ($sla['inquiry_sla_warning_percent'] / 100)) ); $pendingSuppliers = $inquiry->recipients ? $inquiry->recipients->where('recipient_type', 'supplier')->where('status', 'pending')->count() : 0; if ($pendingSuppliers === 0) { return 'ok'; } if ($now->gte($deadline)) { return 'breached'; } if ($now->gte($warningAt)) { return 'warning'; } return 'ok'; } public function snapshot(Business $business): array { if (! Schema::hasTable('aex_price_inquiries')) { return [ 'inquiry_breached' => 0, 'inquiry_warning' => 0, 'inquiry_overdue_due' => 0, 'research_overdue' => 0, 'urgent_items' => [], ]; } $sla = $this->slaSettings($business->id); $inquiries = PriceInquiry::where('business_id', $business->id) ->whereIn('status', ['sent', 'partial']) ->with(['listing', 'recipients']) ->orderByDesc('updated_at') ->limit(200) ->get(); $breached = 0; $warning = 0; $overdueDue = 0; $urgent = collect(); foreach ($inquiries as $inquiry) { $status = $this->inquirySlaStatus($inquiry, $sla); if ($status === 'breached') { $breached++; } elseif ($status === 'warning') { $warning++; } elseif ($status === 'overdue') { $overdueDue++; } if (in_array($status, ['breached', 'overdue', 'warning'], true)) { $urgent->push([ 'type' => 'inquiry', 'id' => $inquiry->id, 'code' => $inquiry->inquiry_code, 'title' => $inquiry->title, 'listing' => $inquiry->listing?->title, 'sla_status' => $status, 'url' => action([\Modules\AssetExchange\Http\Controllers\PriceInquiryController::class, 'show'], [$inquiry->id]), ]); } } $researchOverdue = 0; if (Schema::hasTable('aex_research_tasks')) { $researchOverdue = ResearchTask::where('business_id', $business->id) ->whereNotIn('status', ['completed', 'cancelled']) ->whereDate('due_date', '<', now()->toDateString()) ->count(); } return [ 'inquiry_breached' => $breached, 'inquiry_warning' => $warning, 'inquiry_overdue_due' => $overdueDue, 'research_overdue' => $researchOverdue, 'urgent_items' => $urgent->take(15)->values()->all(), 'sla' => $sla, ]; } public function applyUrgentFilter($query, string $filter, int $businessId) { if ($filter !== 'urgent') { return $query; } $sla = $this->slaSettings($businessId); $ids = PriceInquiry::where('business_id', $businessId) ->whereIn('status', ['sent', 'partial']) ->with('recipients') ->get() ->filter(fn ($inq) => in_array($this->inquirySlaStatus($inq, $sla), ['breached', 'overdue', 'warning'], true)) ->pluck('id') ->all(); return $query->whereIn('id', $ids ?: [0]); } protected function resolveSentAt(PriceInquiry $inquiry): ?Carbon { if ($inquiry->relationLoaded('recipients')) { $sent = $inquiry->recipients->whereNotNull('sent_at')->min('sent_at'); if ($sent) { return Carbon::parse($sent); } } return $inquiry->updated_at ? Carbon::parse($inquiry->updated_at) : null; } }