requestKpiCache = []; $kpis = $this->cachedKpis($businessId, $startDate, $endDate); $alerts = $this->alerts($businessId); $targets = $this->targets($businessId); $moduleCockpit = $this->moduleCockpit($businessId); $systemHealth = $this->systemHealth($alerts); [$prevStartDate, $prevEndDate] = $this->previousPeriodRange($startDate, $endDate); $prevKpis = $this->cachedKpis($businessId, $prevStartDate, $prevEndDate); $drilldown = $this->drilldown($businessId, $startDate, $endDate, $kpis, $prevKpis, $prevStartDate, $prevEndDate, $onlyDrillTab); $payload = [ 'range' => ['start' => $startDate->copy(), 'end' => $endDate->copy()], 'previous_range' => ['start' => $prevStartDate->copy(), 'end' => $prevEndDate->copy()], 'kpis' => $kpis, 'alerts' => $alerts, 'focus_items' => $this->focusItems($businessId), 'trends' => $this->trends($businessId, $startDate, $endDate), 'risk_score' => $this->riskScore($kpis, $alerts), 'action_center' => $this->actionCenter($businessId, $kpis, $alerts), 'targets' => $targets, 'variance' => $this->variance($kpis, $targets), 'notes' => $this->latestNotes($businessId), 'module_cockpit' => $moduleCockpit, 'system_health' => $systemHealth, 'summary' => [ 'total_modules' => count($moduleCockpit), 'enabled_modules' => count(array_filter($moduleCockpit, fn ($m) => $m['enabled'])), 'critical_alerts' => count($alerts), 'system_score' => $systemHealth['score'], ], 'drilldown' => $this->filterDrilldownByEnabledModules($drilldown), 'drilldown_labels' => $this->drilldownTabLabels(), 'trend_month_labels' => $this->monthlyTrendLabels($endDate), 'kpi_comparisons' => $this->kpiPeriodComparisons($kpis, $prevKpis), 'holding_comparison' => $this->holdingEntityComparison($businessId, $startDate, $endDate), ]; $payload['executive_summary'] = $this->executiveSummary($payload); return $payload; } protected function executiveSummary(array $payload): array { $comparisons = $payload['kpi_comparisons'] ?? []; $alerts = $payload['alerts'] ?? []; $variance = $payload['variance'] ?? []; $riskScore = (int) ($payload['risk_score'] ?? 0); $systemHealth = $payload['system_health'] ?? []; $range = $payload['range'] ?? []; $prevRange = $payload['previous_range'] ?? []; $highlights = []; $concerns = []; $watchItems = []; foreach (['sales_this_month', 'gross_margin_estimate', 'workforce_productivity'] as $metric) { if (! isset($comparisons[$metric])) { continue; } $comparison = $comparisons[$metric]; if (($comparison['direction'] ?? 'flat') === 'flat') { continue; } $label = __('managementtools::lang.'.$metric); $pct = abs((float) ($comparison['delta_pct'] ?? 0)); if (! empty($comparison['favorable'])) { $highlights[] = __('managementtools::lang.summary_metric_improved', ['metric' => $label, 'pct' => $pct]); } else { $concerns[] = __('managementtools::lang.summary_metric_declined', ['metric' => $label, 'pct' => $pct]); } } if ($riskScore >= 75) { $highlights[] = __('managementtools::lang.summary_risk_stable', ['score' => $riskScore]); } elseif ($riskScore < 55) { $concerns[] = __('managementtools::lang.summary_risk_critical', ['score' => $riskScore]); } else { $watchItems[] = __('managementtools::lang.summary_risk_watch', ['score' => $riskScore]); } if (count($alerts) > 0) { $concerns[] = __('managementtools::lang.summary_alerts_count', ['count' => count($alerts)]); } else { $highlights[] = __('managementtools::lang.summary_no_critical_alerts'); } foreach (['sales_this_month', 'gross_margin_estimate', 'workforce_productivity'] as $metric) { if (! isset($variance[$metric]) || ($variance[$metric]['delta'] ?? 0) >= 0) { continue; } $watchItems[] = __('managementtools::lang.summary_below_target', [ 'metric' => __('managementtools::lang.'.$metric), 'pct' => abs((float) ($variance[$metric]['delta_pct'] ?? 0)), ]); } $periodStart = $range['start'] ?? Carbon::today(); $periodEnd = $range['end'] ?? Carbon::today(); $prevStart = $prevRange['start'] ?? null; $prevEnd = $prevRange['end'] ?? null; return [ 'headline' => $this->executiveHeadline($highlights, $concerns, $riskScore), 'opening' => __('managementtools::lang.summary_opening', [ 'start' => $periodStart->format('Y-m-d'), 'end' => $periodEnd->format('Y-m-d'), 'health' => $systemHealth['score'] ?? 0, ]), 'highlights' => $highlights, 'concerns' => $concerns, 'watch_items' => $watchItems, 'period_label' => $periodStart->format('Y-m-d').' — '.$periodEnd->format('Y-m-d'), 'previous_period_label' => ($prevStart && $prevEnd) ? $prevStart->format('Y-m-d').' — '.$prevEnd->format('Y-m-d') : '', ]; } protected function executiveHeadline(array $highlights, array $concerns, int $riskScore): string { if (count($concerns) >= 2 || $riskScore < 55) { return __('managementtools::lang.summary_headline_action_required'); } if (count($highlights) >= 2 && count($concerns) === 0) { return __('managementtools::lang.summary_headline_strong_performance'); } return __('managementtools::lang.summary_headline_mixed'); } protected function previousPeriodRange(Carbon $startDate, Carbon $endDate): array { $days = $startDate->diffInDays($endDate) + 1; $prevEnd = $startDate->copy()->subDay()->endOfDay(); $prevStart = $prevEnd->copy()->subDays($days - 1)->startOfDay(); return [$prevStart, $prevEnd]; } protected function periodComparison(float $current, float $previous, bool $lowerIsBetter = false): array { $delta = $current - $previous; $deltaPct = $previous != 0 ? round(($delta / abs($previous)) * 100, 1) : ($current != 0 ? 100.0 : 0.0); $direction = abs($delta) < 0.0001 ? 'flat' : ($delta > 0 ? 'up' : 'down'); return [ 'previous' => $previous, 'delta' => $delta, 'delta_pct' => $deltaPct, 'direction' => $direction, 'favorable' => $lowerIsBetter ? ($delta <= 0) : ($delta >= 0), ]; } protected function enrichKpiWithComparison(array $kpi, ?array $comparison): array { if ($comparison !== null) { $kpi['comparison'] = $comparison; if (! empty($kpi['currency'])) { $kpi['comparison']['currency'] = true; } } return $kpi; } protected function kpiPeriodComparisons(array $kpis, array $prevKpis): array { $map = [ 'sales_this_month' => false, 'gross_margin_estimate' => false, 'purchase_this_month' => true, 'expense_this_month' => true, 'workforce_productivity' => false, 'mt_reports_submitted' => false, 'open_work_orders' => true, 'open_tickets' => true, 'active_projects' => true, ]; $comparisons = []; foreach ($map as $metric => $lowerIsBetter) { $current = (float) ($kpis[$metric] ?? 0); $previous = (float) ($prevKpis[$metric] ?? 0); $comparisons[$metric] = $this->periodComparison($current, $previous, $lowerIsBetter); } return $comparisons; } protected function drilldownPeriodMetrics(int $businessId, Carbon $startDate, Carbon $endDate): array { $metrics = [ 'sales_count' => 0, 'average_sale' => 0, 'projects_completed_in_period' => 0, 'crm_new_leads' => 0, 'production_count' => 0, 'production_value' => 0, 'journal_entry_count' => 0, 'ie_mrp_runs' => 0, ]; if (Schema::hasTable('transactions')) { $salesCount = (int) DB::table('transactions') ->where('business_id', $businessId) ->where('type', 'sell') ->where('status', 'final') ->whereBetween(DB::raw('date(transaction_date)'), [$startDate->toDateString(), $endDate->toDateString()]) ->count(); $salesTotal = (float) DB::table('transactions') ->where('business_id', $businessId) ->where('type', 'sell') ->where('status', 'final') ->whereBetween(DB::raw('date(transaction_date)'), [$startDate->toDateString(), $endDate->toDateString()]) ->sum('final_total'); $metrics['sales_count'] = $salesCount; $metrics['average_sale'] = $salesCount > 0 ? round($salesTotal / $salesCount, 2) : 0; } if (Schema::hasTable('pp_projects')) { $metrics['projects_completed_in_period'] = (int) DB::table('pp_projects') ->where('business_id', $businessId) ->where('status', 'completed') ->whereBetween(DB::raw('date(updated_at)'), [$startDate->toDateString(), $endDate->toDateString()]) ->count(); } if (Schema::hasTable('contacts')) { $metrics['crm_new_leads'] = (int) DB::table('contacts') ->where('business_id', $businessId) ->where('type', 'lead') ->whereBetween(DB::raw('date(created_at)'), [$startDate->toDateString(), $endDate->toDateString()]) ->count(); } if (Schema::hasTable('transactions')) { $metrics['production_count'] = (int) DB::table('transactions') ->where('business_id', $businessId) ->where('type', 'production_purchase') ->whereBetween(DB::raw('date(transaction_date)'), [$startDate->toDateString(), $endDate->toDateString()]) ->count(); $metrics['production_value'] = (float) DB::table('transactions') ->where('business_id', $businessId) ->where('type', 'production_purchase') ->whereBetween(DB::raw('date(transaction_date)'), [$startDate->toDateString(), $endDate->toDateString()]) ->sum('final_total'); } if (Schema::hasTable('accounting_accounts_transactions') && Schema::hasTable('accounting_accounts')) { $dateColumn = Schema::hasColumn('accounting_accounts_transactions', 'operation_date') ? 'operation_date' : 'created_at'; $metrics['journal_entry_count'] = (int) DB::table('accounting_accounts_transactions as aat') ->join('accounting_accounts as aa', 'aa.id', '=', 'aat.accounting_account_id') ->where('aa.business_id', $businessId) ->whereBetween(DB::raw('date(aat.'.$dateColumn.')'), [$startDate->toDateString(), $endDate->toDateString()]) ->count(); } if (Schema::hasTable('ie_mrp_runs')) { $metrics['ie_mrp_runs'] = (int) DB::table('ie_mrp_runs') ->where('business_id', $businessId) ->whereBetween(DB::raw('date(created_at)'), [$startDate->toDateString(), $endDate->toDateString()]) ->count(); } return $metrics; } public function buildForViewer(int $businessId, Carbon $startDate, Carbon $endDate, $user = null, bool $lazyDrillTabs = false, ?string $activeTab = null): array { $onlyTab = ($lazyDrillTabs && $activeTab) ? $activeTab : null; $data = $this->build($businessId, $startDate, $endDate, $onlyTab); if ($user) { $data['drilldown'] = $this->filterDrilldownForUser($data['drilldown'], $user); $data['focus_items'] = $this->filterFocusItemsForUser($data['focus_items'], $user); } return $data; } public function buildDrilldownTabForViewer(int $businessId, Carbon $startDate, Carbon $endDate, string $tabKey, $user = null): ?array { $labels = $this->drilldownTabLabels(); if (! array_key_exists($tabKey, $labels)) { return null; } [$prevStartDate, $prevEndDate] = $this->previousPeriodRange($startDate, $endDate); $kpis = $this->cachedKpis($businessId, $startDate, $endDate); $prevKpis = $this->cachedKpis($businessId, $prevStartDate, $prevEndDate); $sections = $this->filterDrilldownByEnabledModules( $this->drilldown($businessId, $startDate, $endDate, $kpis, $prevKpis, $prevStartDate, $prevEndDate, $tabKey) ); if (! isset($sections[$tabKey])) { return null; } $section = $sections[$tabKey]; if ($user) { $section['top_items'] = array_values(array_filter(array_map(function ($item) use ($user) { return $this->filterPermissionedItem($item, $user); }, $section['top_items'] ?? []))); } return [ 'tab_key' => $tabKey, 'section' => $section, 'trend_month_labels' => $this->monthlyTrendLabels($endDate), 'drilldown_labels' => $labels, ]; } protected function drilldownTabLabels(): array { $labels = [ 'finance' => __('managementtools::lang.drilldown_finance'), 'sales' => __('managementtools::lang.drilldown_sales'), 'maintenance' => __('managementtools::lang.drilldown_maintenance'), 'projects' => __('managementtools::lang.drilldown_projects'), 'workforce' => __('managementtools::lang.drilldown_workforce'), ]; if ($this->isModuleEnabled('Crm')) { $labels['crm'] = __('managementtools::lang.drilldown_crm'); } if ($this->isModuleEnabled('Accounting')) { $labels['accounting'] = __('managementtools::lang.drilldown_accounting'); } if ($this->isModuleEnabled('Manufacturing')) { $labels['manufacturing'] = __('managementtools::lang.drilldown_manufacturing'); } if ($this->isModuleEnabled('IndustrialEngineering')) { $labels['ie'] = __('managementtools::lang.drilldown_ie'); } if ($this->isModuleEnabled('QualityManagement')) { $labels['quality'] = __('managementtools::lang.drilldown_quality'); } if ($this->isModuleEnabled('ShopFloor')) { $labels['shopfloor'] = __('managementtools::lang.drilldown_shopfloor'); } if ($this->isModuleEnabled('SupplyChain')) { $labels['supplychain'] = __('managementtools::lang.drilldown_supplychain'); } if ($this->isModuleEnabled('AdvancedPlanning')) { $labels['planning'] = __('managementtools::lang.drilldown_planning'); } return $labels; } protected function moduleStatuses(): array { return $this->moduleStatusResolver->all(); } protected function isModuleEnabled(string $moduleKey): bool { return $this->moduleStatusResolver->isEnabled($moduleKey); } protected function cachedKpis(int $businessId, Carbon $startDate, Carbon $endDate): array { $key = $startDate->toDateString().':'.$endDate->toDateString(); if (is_array($this->requestKpiCache) && array_key_exists($key, $this->requestKpiCache)) { return $this->requestKpiCache[$key]; } $kpis = $this->metricsCache->rememberKpis( $businessId, $startDate, $endDate, fn () => $this->kpis($businessId, $startDate, $endDate) ); if (is_array($this->requestKpiCache)) { $this->requestKpiCache[$key] = $kpis; } return $kpis; } protected function filterDrilldownByEnabledModules(array $drilldown): array { $moduleMap = [ 'crm' => 'Crm', 'accounting' => 'Accounting', 'manufacturing' => 'Manufacturing', 'ie' => 'IndustrialEngineering', 'quality' => 'QualityManagement', 'shopfloor' => 'ShopFloor', 'supplychain' => 'SupplyChain', 'planning' => 'AdvancedPlanning', ]; foreach ($moduleMap as $tab => $moduleKey) { if (! $this->isModuleEnabled($moduleKey)) { unset($drilldown[$tab]); } } return $drilldown; } protected function monthlyTrendLabels(Carbon $endDate): array { $labels = []; for ($i = 5; $i >= 0; $i--) { $labels[] = $endDate->copy()->startOfMonth()->subMonths($i)->format('Y/m'); } return $labels; } public function filterDrilldownForUser(array $drilldown, $user): array { foreach ($drilldown as $key => $section) { $drilldown[$key]['top_items'] = array_values(array_filter(array_map(function ($item) use ($user) { return $this->filterPermissionedItem($item, $user); }, $section['top_items'] ?? []))); } return $drilldown; } public function filterFocusItemsForUser(array $items, $user): array { return array_values(array_filter(array_map(function ($item) use ($user) { return $this->filterPermissionedItem($item, $user); }, $items))); } protected function filterPermissionedItem(array $item, $user): ?array { if (! $this->userCanAny($user, $item['permissions'] ?? [])) { return null; } $item['actions'] = array_values(array_filter($item['actions'] ?? [], function ($action) use ($user) { return $this->userCanAny($user, $action['permissions'] ?? []); })); return $item; } protected function userCanAny($user, array $permissions): bool { if (empty($permissions)) { return true; } if (! $user) { return false; } foreach ($permissions as $permission) { if ($user->can($permission)) { return true; } } return false; } protected function contactViewPermissions(): array { return ['customer.view', 'customer.view_own', 'supplier.view', 'supplier.view_own']; } protected function contactActions(int $contactId): array { $contactUrl = $contactId > 0 ? url('/contacts/'.$contactId) : url('/contacts/customers'); $paymentsUrl = $contactId > 0 ? url('/contacts/payments/'.$contactId) : url('/contacts/customers'); $permissions = $this->contactViewPermissions(); return [ 'url' => $contactUrl, 'permissions' => $permissions, 'actions' => [ [ 'label' => __('managementtools::lang.view_details'), 'url' => $contactUrl, 'permissions' => $permissions, ], [ 'label' => __('managementtools::lang.open_payments'), 'url' => $paymentsUrl, 'permissions' => $permissions, ], ], ]; } protected function kpis(int $businessId, Carbon $startDate, Carbon $endDate): array { $sales = 0; $purchase = 0; $expense = 0; $customerDue = 0; $supplierDue = 0; if (Schema::hasTable('transactions')) { $sales = $this->transactionMetrics->sumFinalSells($businessId, $startDate, $endDate); $purchase = (float) DB::table('transactions') ->where('business_id', $businessId) ->where('type', 'purchase') ->where('transaction_date', '>=', $startDate->copy()->startOfDay()) ->where('transaction_date', '<=', $endDate->copy()->endOfDay()) ->sum('final_total'); $expense = (float) DB::table('transactions') ->where('business_id', $businessId) ->where('type', 'expense') ->where('transaction_date', '>=', $startDate->copy()->startOfDay()) ->where('transaction_date', '<=', $endDate->copy()->endOfDay()) ->sum('final_total'); } if (Schema::hasTable('contacts')) { $customerDue = (float) DB::table('contacts') ->where('business_id', $businessId) ->whereIn('type', ['customer', 'both']) ->where('balance', '>', 0) ->sum('balance'); $supplierDue = (float) DB::table('contacts') ->where('business_id', $businessId) ->whereIn('type', ['supplier', 'both']) ->where('balance', '>', 0) ->sum('balance'); } $openTickets = 0; if (Schema::hasTable('mt_user_tickets')) { $openTickets = (int) DB::table('mt_user_tickets') ->where('business_id', $businessId) ->whereIn('status', [0, 1]) ->count(); } $openWorkOrders = 0; if (Schema::hasTable('maintenance_work_orders')) { $openWorkOrders = (int) DB::table('maintenance_work_orders') ->where('business_id', $businessId) ->whereIn('status', ['draft', 'open', 'in_progress', 'on_hold']) ->count(); } $activeProjects = 0; if (Schema::hasTable('pp_projects')) { $activeProjects = (int) DB::table('pp_projects') ->where('business_id', $businessId) ->whereIn('status', ['not_started', 'in_progress', 'on_hold']) ->where('is_archived', false) ->count(); } $overdueInvoices = 0; if (Schema::hasTable('transactions') && Schema::hasColumn('transactions', 'due_date')) { $overdueInvoices = (int) DB::table('transactions') ->where('business_id', $businessId) ->where('type', 'sell') ->where('status', 'final') ->where('payment_status', '!=', 'paid') ->whereNotNull('due_date') ->whereDate('due_date', '<', Carbon::today()->toDateString()) ->count(); } return [ 'sales_this_month' => $sales, 'purchase_this_month' => $purchase, 'expense_this_month' => $expense, 'gross_margin_estimate' => $sales - $purchase - $expense, 'customer_due' => $customerDue, 'supplier_due' => $supplierDue, 'open_tickets' => $openTickets, 'open_work_orders' => $openWorkOrders, 'active_projects' => $activeProjects, 'overdue_invoices' => $overdueInvoices, 'mt_reports_submitted' => $this->submittedReportsCount($businessId, $startDate, $endDate), 'workforce_productivity' => $this->workforceProductivity($businessId, $startDate, $endDate), 'open_ncrs' => $this->openNcrCount($businessId), 'avg_oee_30d' => $this->avgOeeLast30Days($businessId), 'open_iot_alerts' => $this->openIotAlertCount($businessId), ]; } protected function openNcrCount(int $businessId): int { if (! Schema::hasTable('qms_ncr_reports')) { return 0; } return (int) DB::table('qms_ncr_reports') ->where('business_id', $businessId) ->whereIn('status', ['open', 'investigating', 'containment']) ->count(); } protected function avgOeeLast30Days(int $businessId): float { if (! Schema::hasTable('sf_oee_snapshots')) { return 0; } return round((float) DB::table('sf_oee_snapshots') ->where('business_id', $businessId) ->where('snapshot_date', '>=', now()->subDays(30)->toDateString()) ->avg('oee_pct'), 2); } protected function openIotAlertCount(int $businessId): int { if (! Schema::hasTable('mt_iot_alerts')) { return 0; } return (int) DB::table('mt_iot_alerts') ->where('business_id', $businessId) ->where('status', 'open') ->count(); } protected function submittedReportsCount(int $businessId, Carbon $startDate, Carbon $endDate): int { if (! Schema::hasTable('mt_daily_reports')) { return 0; } return (int) DB::table('mt_daily_reports') ->where('business_id', $businessId) ->whereBetween(DB::raw('date(report_date)'), [$startDate->toDateString(), $endDate->toDateString()]) ->count(); } protected function workforceProductivity(int $businessId, Carbon $startDate, Carbon $endDate): float { if (! Schema::hasTable('mt_user_tasks')) { return 0; } $total = (int) DB::table('mt_user_tasks') ->where('business_id', $businessId) ->whereBetween(DB::raw('date(created_at)'), [$startDate->toDateString(), $endDate->toDateString()]) ->count(); if ($total === 0) { return 0; } $completed = (int) DB::table('mt_user_tasks') ->where('business_id', $businessId) ->whereBetween(DB::raw('date(created_at)'), [$startDate->toDateString(), $endDate->toDateString()]) ->where('task_status', 1) ->count(); return round(($completed / $total) * 100, 2); } protected function alerts(int $businessId): array { $items = []; if (Schema::hasTable('transactions') && Schema::hasColumn('transactions', 'due_date')) { $overdueInvoices = (int) DB::table('transactions') ->where('business_id', $businessId) ->where('type', 'sell') ->where('status', 'final') ->where('payment_status', '!=', 'paid') ->whereNotNull('due_date') ->whereDate('due_date', '<', Carbon::today()->toDateString()) ->count(); if ($overdueInvoices > 0) { $items[] = [ 'level' => 'danger', 'title' => __('managementtools::lang.alert_overdue_invoices'), 'value' => $overdueInvoices, 'url' => url('/reports/sell-payment-report'), ]; } } if (Schema::hasTable('maintenance_preventive_schedules')) { $upcomingPm = (int) DB::table('maintenance_preventive_schedules') ->where('business_id', $businessId) ->where('is_active', true) ->whereNotNull('next_due_at') ->whereDate('next_due_at', '<=', Carbon::today()->addDays(7)->toDateString()) ->count(); if ($upcomingPm > 0) { $items[] = [ 'level' => 'warning', 'title' => __('managementtools::lang.alert_pm_due'), 'value' => $upcomingPm, 'url' => url('/maintenance/dashboard'), ]; } } if (Schema::hasTable('mt_user_tickets')) { $urgentTickets = (int) DB::table('mt_user_tickets') ->where('business_id', $businessId) ->where('priority', 'urgent') ->whereIn('status', [0, 1]) ->count(); if ($urgentTickets > 0) { $items[] = [ 'level' => 'danger', 'title' => __('managementtools::lang.alert_urgent_tickets'), 'value' => $urgentTickets, 'url' => url('/management-tools/tickets'), ]; } } if (Schema::hasTable('accounting_installment_schedule') && Schema::hasTable('accounting_installment_plans')) { $overdueInstallments = (int) DB::table('accounting_installment_schedule as s') ->join('accounting_installment_plans as p', 'p.id', '=', 's.installment_plan_id') ->where('p.business_id', $businessId) ->where('s.status', 'overdue') ->count(); if ($overdueInstallments > 0) { $overdueAmount = (float) DB::table('accounting_installment_schedule as s') ->join('accounting_installment_plans as p', 'p.id', '=', 's.installment_plan_id') ->where('p.business_id', $businessId) ->where('s.status', 'overdue') ->selectRaw('SUM(s.amount - COALESCE(s.paid_amount, 0)) as remaining') ->value('remaining'); $items[] = [ 'level' => 'danger', 'title' => __('managementtools::lang.alert_overdue_installments'), 'value' => $overdueInstallments, 'subtitle' => __('managementtools::lang.alert_overdue_installments_amount', [ 'amount' => number_format($overdueAmount, 2), ]), 'url' => $this->routeIfExists('accounting.installments'), ]; } } if (Schema::hasTable('accounting_cheques')) { $pendingCheques = (int) DB::table('accounting_cheques') ->where('business_id', $businessId) ->whereIn('status', ['pending', 'deposited']) ->count(); if ($pendingCheques > 0) { $items[] = [ 'level' => 'warning', 'title' => __('managementtools::lang.alert_pending_cheques'), 'value' => $pendingCheques, 'url' => $this->routeIfExists('accounting.cheques'), ]; } } if (class_exists(\Modules\Accounting\Services\DeferredDocumentsService::class)) { $deferredSummary = app(\Modules\Accounting\Services\DeferredDocumentsService::class)->summary($businessId); if ($deferredSummary['total'] > 0) { $items[] = [ 'level' => 'danger', 'title' => __('managementtools::lang.alert_deferred_documents'), 'value' => $deferredSummary['total'], 'subtitle' => __('managementtools::lang.alert_deferred_documents_breakdown', [ 'cheques' => $deferredSummary['cheques'], 'installments' => $deferredSummary['installments'], 'invoices' => $deferredSummary['invoices'], 'journals' => $deferredSummary['journals'], ]), 'url' => $this->routeIfExists('accounting.deferredDocuments'), ]; } } if (class_exists(\Modules\Accounting\Services\ExchangeRateService::class) && Schema::hasTable('accounting_exchange_rates')) { $fxSummary = app(\Modules\Accounting\Services\ExchangeRateService::class)->staleRateSummary($businessId, 2); if (($fxSummary['total_issues'] ?? (($fxSummary['stale_count'] ?? 0) + ($fxSummary['missing_count'] ?? 0))) > 0) { $items[] = [ 'level' => 'warning', 'title' => __('managementtools::lang.alert_stale_exchange_rates'), 'value' => $fxSummary['total_issues'] ?? (($fxSummary['stale_count'] ?? 0) + ($fxSummary['missing_count'] ?? 0)), 'subtitle' => __('managementtools::lang.alert_stale_exchange_rates_hint', [ 'stale' => $fxSummary['stale_count'] ?? 0, 'missing' => $fxSummary['missing_count'] ?? 0, 'pairs' => implode(', ', array_slice($fxSummary['pairs'] ?? [], 0, 4)), ]), 'url' => $this->routeIfExists('accounting.exchangeRates'), ]; } } if (class_exists(\Modules\Accounting\Services\PaymentAccountGlLinkService::class) && Schema::hasColumn('accounts', 'accounting_account_id')) { $paymentGl = app(\Modules\Accounting\Services\PaymentAccountGlLinkService::class)->summary($businessId); if (($paymentGl['unlinked'] ?? 0) > 0) { $items[] = [ 'level' => 'warning', 'title' => __('managementtools::lang.alert_unlinked_payment_accounts'), 'value' => $paymentGl['unlinked'], 'subtitle' => __('managementtools::lang.alert_unlinked_payment_accounts_hint', [ 'linked' => $paymentGl['linked'], 'total' => $paymentGl['total'], ]), 'url' => url('/accounting/settings').'#payment_gl_tab', ]; } } if (Schema::hasTable('qms_ncr_reports')) { $openNcrs = (int) DB::table('qms_ncr_reports') ->where('business_id', $businessId) ->whereIn('status', ['open', 'investigating', 'containment']) ->count(); if ($openNcrs > 0) { $items[] = [ 'level' => 'warning', 'title' => __('managementtools::lang.alert_open_ncrs'), 'value' => $openNcrs, 'url' => url('/qualitymanagement/ncr'), ]; } } if (Schema::hasTable('mt_iot_alerts')) { $iotAlerts = (int) DB::table('mt_iot_alerts') ->where('business_id', $businessId) ->where('status', 'open') ->count(); if ($iotAlerts > 0) { $items[] = [ 'level' => 'danger', 'title' => __('managementtools::lang.alert_iot_predictive'), 'value' => $iotAlerts, 'url' => url('/maintenance/iot'), ]; } } return $items; } protected function focusItems(int $businessId): array { $items = []; if (Schema::hasTable('contacts')) { $topDebtors = DB::table('contacts') ->where('business_id', $businessId) ->whereIn('type', ['customer', 'both']) ->where('balance', '>', 0) ->orderByDesc('balance') ->limit(5) ->get(['id', 'name', 'balance']); foreach ($topDebtors as $debtor) { $contactMeta = $this->contactActions((int) $debtor->id); $items[] = array_merge([ 'type' => 'customer_due', 'label' => (string) ($debtor->name ?: '-'), 'amount' => (float) $debtor->balance, 'currency' => true, ], $contactMeta); } } return $items; } protected function trends(int $businessId, Carbon $startDate, Carbon $endDate): array { $labels = []; $salesSeries = []; $purchaseSeries = []; $salesByDate = []; $purchaseByDate = []; if (Schema::hasTable('transactions')) { $salesRows = DB::table('transactions') ->selectRaw('DATE(transaction_date) as d, SUM(final_total) as total') ->where('business_id', $businessId) ->where('type', 'sell') ->where('status', 'final') ->whereBetween(DB::raw('date(transaction_date)'), [$startDate->toDateString(), $endDate->toDateString()]) ->groupBy('d') ->pluck('total', 'd') ->toArray(); $purchaseRows = DB::table('transactions') ->selectRaw('DATE(transaction_date) as d, SUM(final_total) as total') ->where('business_id', $businessId) ->where('type', 'purchase') ->whereBetween(DB::raw('date(transaction_date)'), [$startDate->toDateString(), $endDate->toDateString()]) ->groupBy('d') ->pluck('total', 'd') ->toArray(); $salesByDate = array_map('floatval', $salesRows); $purchaseByDate = array_map('floatval', $purchaseRows); } $cursor = $startDate->copy()->startOfDay(); while ($cursor->lte($endDate)) { $key = $cursor->toDateString(); $labels[] = $cursor->format('m/d'); $salesSeries[] = $salesByDate[$key] ?? 0; $purchaseSeries[] = $purchaseByDate[$key] ?? 0; $cursor->addDay(); } return [ 'labels' => $labels, 'sales' => $salesSeries, 'purchase' => $purchaseSeries, ]; } protected function riskScore(array $kpis, array $alerts): int { $score = 100; $score -= min(40, count($alerts) * 12); $score -= min(20, (int) (($kpis['overdue_invoices'] ?? 0) * 1.5)); $score -= min(20, max(0, 70 - (int) ($kpis['workforce_productivity'] ?? 0))); $score -= min(20, ((int) ($kpis['open_tickets'] ?? 0) > 15) ? 20 : (int) ($kpis['open_tickets'] ?? 0)); return max(0, $score); } protected function actionCenter(int $businessId, array $kpis, array $alerts): array { $actions = []; if (($kpis['overdue_invoices'] ?? 0) > 0) { $actions[] = [ 'priority' => 'high', 'title' => __('managementtools::lang.action_collect_overdue'), 'description' => __('managementtools::lang.action_collect_overdue_hint'), 'url' => url('/reports/sell-payment-report'), 'drill_tab' => 'finance', ]; } if (($kpis['open_work_orders'] ?? 0) > 10) { $actions[] = [ 'priority' => 'medium', 'title' => __('managementtools::lang.action_stabilize_maintenance'), 'description' => __('managementtools::lang.action_stabilize_maintenance_hint'), 'url' => url('/maintenance/work-orders'), 'drill_tab' => 'maintenance', ]; } if (($kpis['workforce_productivity'] ?? 0) < 65) { $actions[] = [ 'priority' => 'medium', 'title' => __('managementtools::lang.action_productivity_review'), 'description' => __('managementtools::lang.action_productivity_review_hint'), 'url' => url('/management-tools/daily-reports'), 'drill_tab' => 'workforce', ]; } if (class_exists(\Modules\Accounting\Services\ExchangeRateService::class) && Schema::hasTable('accounting_exchange_rates')) { $fxSummary = app(\Modules\Accounting\Services\ExchangeRateService::class)->staleRateSummary($businessId, 2); if (($fxSummary['total_issues'] ?? 0) > 0) { $actions[] = [ 'priority' => 'medium', 'title' => __('managementtools::lang.action_refresh_exchange_rates'), 'description' => __('managementtools::lang.action_refresh_exchange_rates_hint'), 'url' => $this->routeIfExists('accounting.exchangeRates') ?: url('/accounting/exchange-rates'), 'drill_tab' => 'finance', ]; } } if (empty($actions) && empty($alerts)) { $actions[] = [ 'priority' => 'low', 'title' => __('managementtools::lang.action_keep_monitoring'), 'description' => __('managementtools::lang.action_keep_monitoring_hint'), 'url' => url('/management-tools/executive-cockpit'), 'drill_tab' => 'finance', ]; } return $actions; } protected function targets(int $businessId): array { if (! Schema::hasTable('mt_executive_targets')) { return []; } $today = Carbon::today()->toDateString(); $rows = MtExecutiveTarget::query() ->where('business_id', $businessId) ->where(function ($q) use ($today) { $q->whereNull('effective_from')->orWhere('effective_from', '<=', $today); }) ->where(function ($q) use ($today) { $q->whereNull('effective_to')->orWhere('effective_to', '>=', $today); }) ->orderByDesc('effective_from') ->get(); $targets = []; foreach ($rows as $row) { if (! array_key_exists($row->metric_key, $targets)) { $targets[$row->metric_key] = (float) $row->target_value; } } return $targets; } protected function variance(array $kpis, array $targets): array { $metrics = ['sales_this_month', 'gross_margin_estimate', 'workforce_productivity']; $result = []; foreach ($metrics as $metric) { if (! isset($targets[$metric])) { continue; } $actual = (float) ($kpis[$metric] ?? 0); $target = (float) $targets[$metric]; $delta = $actual - $target; $pct = $target != 0 ? round(($delta / $target) * 100, 2) : 0; $result[$metric] = [ 'actual' => $actual, 'target' => $target, 'delta' => $delta, 'delta_pct' => $pct, ]; } return $result; } protected function latestNotes(int $businessId): array { if (! Schema::hasTable('mt_executive_notes')) { return []; } $query = MtExecutiveNote::query() ->where('mt_executive_notes.business_id', $businessId) ->orderByDesc('mt_executive_notes.note_date') ->orderByDesc('mt_executive_notes.id') ->limit(8); if (Schema::hasTable('users')) { $query->leftJoin('users', 'users.id', '=', 'mt_executive_notes.user_id') ->select([ 'mt_executive_notes.title', 'mt_executive_notes.note', 'mt_executive_notes.note_date', 'mt_executive_notes.visibility', 'users.first_name', 'users.last_name', 'users.surname', ]); } else { $query->select(['title', 'note', 'note_date', 'visibility']); } return $query->get()->map(function ($n) { $author = trim(implode(' ', array_filter([ $n->first_name ?? '', $n->last_name ?? '', $n->surname ?? '', ]))); return [ 'title' => $n->title, 'note' => $n->note, 'note_date' => $n->note_date, 'author' => $author !== '' ? $author : null, 'visibility' => $n->visibility ?? 'leadership', ]; })->toArray(); } protected function moduleCockpit(int $businessId): array { $statuses = $this->moduleStatuses(); $modules = [ 'Crm' => ['label' => 'CRM', 'table' => 'contacts', 'url' => url('/crm/dashboard'), 'query' => fn () => DB::table('contacts')->where('business_id', $businessId)->count()], 'Maintenance' => ['label' => 'Maintenance', 'table' => 'maintenance_work_orders', 'url' => url('/maintenance/dashboard'), 'query' => fn () => DB::table('maintenance_work_orders')->where('business_id', $businessId)->count()], 'ProfessionalProject' => ['label' => 'Project', 'table' => 'pp_projects', 'url' => url('/management-tools/project-categories'), 'query' => fn () => DB::table('pp_projects')->where('business_id', $businessId)->count()], 'Manufacturing' => ['label' => 'Manufacturing', 'table' => 'mfg_recipes', 'url' => url('/manufacturing/settings'), 'query' => fn () => $this->countManufacturingRecipes($businessId)], 'Accounting' => [ 'label' => 'Accounting', 'table' => 'accounting_accounts_transactions', 'url' => url('/accounting/dashboard'), 'query' => function () use ($businessId) { return DB::table('accounting_accounts_transactions as aat') ->join('accounting_accounts as aa', 'aa.id', '=', 'aat.accounting_account_id') ->where('aa.business_id', $businessId) ->count(); }, ], 'InventoryManagement' => [ 'label' => 'Inventory', 'table' => 'variation_location_details', 'url' => url('/reports/stock-report'), 'query' => function () use ($businessId) { return DB::table('variation_location_details as vld') ->join('variations as v', 'v.id', '=', 'vld.variation_id') ->join('products as p', 'p.id', '=', 'v.product_id') ->where('p.business_id', $businessId) ->count(); }, ], 'ManagementTools' => ['label' => 'Management Tools', 'table' => 'mt_daily_reports', 'url' => url('/management-tools/executive-cockpit'), 'query' => fn () => DB::table('mt_daily_reports')->where('business_id', $businessId)->count()], 'IndustrialEngineering' => ['label' => 'Industrial Engineering', 'table' => 'ie_item_masters', 'url' => url('/industrial-engineering/dashboard'), 'query' => fn () => DB::table('ie_item_masters')->where('business_id', $businessId)->count()], 'QualityManagement' => ['label' => 'Quality (QMS)', 'table' => 'qms_ncr_reports', 'url' => url('/qualitymanagement'), 'query' => fn () => DB::table('qms_ncr_reports')->where('business_id', $businessId)->count()], 'ShopFloor' => ['label' => 'Shop Floor / MES', 'table' => 'sf_work_centers', 'url' => url('/shopfloor'), 'query' => fn () => DB::table('sf_work_centers')->where('business_id', $businessId)->count()], 'SupplyChain' => ['label' => 'Supply Chain', 'table' => 'sc_supplier_scorecards', 'url' => url('/supplychain/dashboard'), 'query' => fn () => DB::table('sc_supplier_scorecards')->where('business_id', $businessId)->count()], 'AdvancedPlanning' => ['label' => __('advancedplanning::lang.module_name'), 'table' => 'ap_capacity_resources', 'url' => url('/advanced-planning/dashboard'), 'query' => fn () => DB::table('ap_capacity_resources')->where('business_id', $businessId)->count()], 'IntegrationHub' => ['label' => __('integrationhub::lang.module_name'), 'table' => 'ih_webhook_endpoints', 'url' => url('/integrationhub/dashboard'), 'query' => fn () => DB::table('ih_webhook_endpoints')->where('business_id', $businessId)->count()], ]; $result = []; foreach ($modules as $key => $meta) { $enabled = (bool) ($statuses[$key] ?? false); $tableExists = Schema::hasTable($meta['table']); $count = 0; if ($enabled && $tableExists) { try { $count = (int) $meta['query'](); } catch (\Throwable $e) { report($e); $count = 0; } } $score = 0; if (! $enabled) { $score = 100; } else { $score += 50; if ($tableExists) { $score += 25; } if ($count > 0) { $score += 25; } } $result[] = [ 'key' => $key, 'label' => $meta['label'], 'enabled' => $enabled, 'table_exists' => $tableExists, 'records' => $count, 'score' => $score, 'url' => $meta['url'], ]; } return $result; } protected function systemHealth(array $alerts): array { $severity = count($alerts) === 0 ? 'good' : (count($alerts) <= 2 ? 'watch' : 'critical'); return [ 'status' => $severity, 'score' => max(0, 100 - (count($alerts) * 18)), ]; } protected function drilldown(int $businessId, Carbon $startDate, Carbon $endDate, array $kpis, array $prevKpis, Carbon $prevStartDate, Carbon $prevEndDate, ?string $onlyTab = null): array { $tabsNeedingPeriodMetrics = ['sales', 'projects', 'crm', 'accounting', 'manufacturing', 'ie']; $needsPeriodMetrics = $onlyTab === null || in_array($onlyTab, $tabsNeedingPeriodMetrics, true); $ctx = [ 'businessId' => $businessId, 'startDate' => $startDate, 'endDate' => $endDate, 'prevStartDate' => $prevStartDate, 'prevEndDate' => $prevEndDate, 'kpis' => $kpis, 'prevKpis' => $prevKpis, 'periodMetrics' => $needsPeriodMetrics ? $this->drilldownPeriodMetrics($businessId, $startDate, $endDate) : [], 'prevPeriodMetrics' => $needsPeriodMetrics ? $this->drilldownPeriodMetrics($businessId, $prevStartDate, $prevEndDate) : [], ]; $tabs = $onlyTab ? [$onlyTab] : $this->coreDrilldownTabKeys(); $sections = []; foreach ($tabs as $tabKey) { $section = $this->buildDrilldownTabSection($ctx, $tabKey); if ($section !== null) { $sections[$tabKey] = $section; } } return $sections; } protected function coreDrilldownTabKeys(): array { return ['finance', 'sales', 'maintenance', 'projects', 'workforce', 'crm', 'accounting', 'manufacturing', 'ie', 'quality', 'shopfloor', 'supplychain', 'planning']; } protected function buildDrilldownTabSection(array $ctx, string $tabKey): ?array { $businessId = $ctx['businessId']; $startDate = $ctx['startDate']; $endDate = $ctx['endDate']; $kpis = $ctx['kpis']; $prevKpis = $ctx['prevKpis']; $periodMetrics = $ctx['periodMetrics']; $prevPeriodMetrics = $ctx['prevPeriodMetrics']; return match ($tabKey) { 'finance' => [ 'kpis' => [ $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.customer_due'), 'value' => $kpis['customer_due'] ?? 0, 'currency' => true], null ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.supplier_due'), 'value' => $kpis['supplier_due'] ?? 0, 'currency' => true], null ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.gross_margin_estimate'), 'value' => $kpis['gross_margin_estimate'] ?? 0, 'currency' => true], $this->periodComparison( (float) ($kpis['gross_margin_estimate'] ?? 0), (float) ($prevKpis['gross_margin_estimate'] ?? 0) ) ), ], 'trend' => $this->monthlySalesSeries($businessId, $endDate), 'top_items' => $this->topDebtors($businessId), ], 'sales' => [ 'kpis' => [ $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.sales_this_month'), 'value' => $kpis['sales_this_month'] ?? 0, 'currency' => true], $this->periodComparison( (float) ($kpis['sales_this_month'] ?? 0), (float) ($prevKpis['sales_this_month'] ?? 0) ) ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.sales_count'), 'value' => $periodMetrics['sales_count']], $this->periodComparison( (float) $periodMetrics['sales_count'], (float) ($prevPeriodMetrics['sales_count'] ?? 0) ) ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.average_sale'), 'value' => $periodMetrics['average_sale'], 'currency' => true], $this->periodComparison( (float) $periodMetrics['average_sale'], (float) ($prevPeriodMetrics['average_sale'] ?? 0) ) ), ], 'trend' => $this->monthlySalesSeries($businessId, $endDate), 'top_items' => $this->topCustomersBySales($businessId, $startDate, $endDate), ], 'maintenance' => $this->buildMaintenanceDrilldownSection($ctx), 'projects' => $this->buildProjectsDrilldownSection($ctx), 'workforce' => $this->buildWorkforceDrilldownSection($ctx), 'crm' => $this->buildCrmDrilldownSection($ctx), 'accounting' => $this->buildAccountingDrilldownSection($ctx), 'manufacturing' => $this->buildManufacturingDrilldownSection($ctx), 'ie' => $this->buildIeDrilldownSection($ctx), 'quality' => $this->buildQualityDrilldownSection($ctx), 'shopfloor' => $this->buildShopFloorDrilldownSection($ctx), 'supplychain' => $this->buildSupplyChainDrilldownSection($ctx), 'planning' => $this->buildPlanningDrilldownSection($ctx), default => null, }; } protected function buildMaintenanceDrilldownSection(array $ctx): array { $businessId = $ctx['businessId']; $endDate = $ctx['endDate']; $kpis = $ctx['kpis']; $prevKpis = $ctx['prevKpis']; $draftWorkOrders = 0; $inProgressWorkOrders = 0; if (Schema::hasTable('maintenance_work_orders')) { $draftWorkOrders = (int) DB::table('maintenance_work_orders') ->where('business_id', $businessId) ->where('status', 'draft') ->count(); $inProgressWorkOrders = (int) DB::table('maintenance_work_orders') ->where('business_id', $businessId) ->where('status', 'in_progress') ->count(); } return [ 'kpis' => [ $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.open_work_orders'), 'value' => $kpis['open_work_orders'] ?? 0], $this->periodComparison( (float) ($kpis['open_work_orders'] ?? 0), (float) ($prevKpis['open_work_orders'] ?? 0), true ) ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.work_orders_draft'), 'value' => $draftWorkOrders], null ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.work_orders_in_progress'), 'value' => $inProgressWorkOrders], null ), ], 'trend' => $this->monthlyCountSeries($businessId, 'maintenance_work_orders', 'created_at', $endDate), 'top_items' => $this->topMaintenanceBacklog($businessId), ]; } protected function buildProjectsDrilldownSection(array $ctx): array { $businessId = $ctx['businessId']; $endDate = $ctx['endDate']; $kpis = $ctx['kpis']; $prevKpis = $ctx['prevKpis']; $periodMetrics = $ctx['periodMetrics']; $prevPeriodMetrics = $ctx['prevPeriodMetrics']; $inProgressProjects = 0; if (Schema::hasTable('pp_projects')) { $inProgressProjects = (int) DB::table('pp_projects') ->where('business_id', $businessId) ->where('status', 'in_progress') ->count(); } return [ 'kpis' => [ $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.active_projects'), 'value' => $kpis['active_projects'] ?? 0], $this->periodComparison( (float) ($kpis['active_projects'] ?? 0), (float) ($prevKpis['active_projects'] ?? 0), true ) ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.projects_in_progress'), 'value' => $inProgressProjects], null ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.projects_completed_in_period'), 'value' => $periodMetrics['projects_completed_in_period']], $this->periodComparison( (float) $periodMetrics['projects_completed_in_period'], (float) ($prevPeriodMetrics['projects_completed_in_period'] ?? 0) ) ), ], 'trend' => $this->monthlyCountSeries($businessId, 'pp_projects', 'created_at', $endDate), 'top_items' => $this->topProjectRisks($businessId), ]; } protected function buildWorkforceDrilldownSection(array $ctx): array { $businessId = $ctx['businessId']; $startDate = $ctx['startDate']; $endDate = $ctx['endDate']; $kpis = $ctx['kpis']; $prevKpis = $ctx['prevKpis']; $activeEmployees = 0; $submittedReports = 0; if (Schema::hasTable('users')) { $activeEmployees = (int) DB::table('users') ->where('business_id', $businessId) ->where('allow_login', 1) ->where('user_type', 'user') ->count(); } if (Schema::hasTable('mt_daily_reports')) { $submittedReports = (int) DB::table('mt_daily_reports') ->where('business_id', $businessId) ->whereBetween(DB::raw('date(report_date)'), [$startDate->toDateString(), $endDate->toDateString()]) ->count(); } return [ 'kpis' => [ $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.active_employees'), 'value' => $activeEmployees], null ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.mt_reports_submitted'), 'value' => $submittedReports], $this->periodComparison( (float) $submittedReports, (float) ($prevKpis['mt_reports_submitted'] ?? 0) ) ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.workforce_productivity'), 'value' => ($kpis['workforce_productivity'] ?? 0).'%'], $this->periodComparison( (float) ($kpis['workforce_productivity'] ?? 0), (float) ($prevKpis['workforce_productivity'] ?? 0) ) ), ], 'trend' => $this->monthlyCountSeries($businessId, 'mt_daily_reports', 'report_date', $endDate), 'top_items' => $this->topReporters($businessId, $startDate, $endDate), ]; } protected function buildCrmDrilldownSection(array $ctx): array { $businessId = $ctx['businessId']; $startDate = $ctx['startDate']; $endDate = $ctx['endDate']; $prevPeriodMetrics = $ctx['prevPeriodMetrics']; $crmMetrics = $this->crmDrilldownMetrics($businessId, $startDate, $endDate); return [ 'kpis' => [ $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.crm_total_leads'), 'value' => $crmMetrics['total_leads']], null ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.crm_new_leads'), 'value' => $crmMetrics['new_leads']], $this->periodComparison( (float) $crmMetrics['new_leads'], (float) ($prevPeriodMetrics['crm_new_leads'] ?? 0) ) ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.crm_pipeline_value'), 'value' => $crmMetrics['pipeline_value'], 'currency' => true], null ), ], 'trend' => $this->monthlyLeadsSeries($businessId, $endDate), 'top_items' => $this->topCrmLeads($businessId), ]; } protected function buildAccountingDrilldownSection(array $ctx): array { $businessId = $ctx['businessId']; $endDate = $ctx['endDate']; $prevPeriodMetrics = $ctx['prevPeriodMetrics']; $accountingMetrics = $this->accountingDrilldownMetrics($businessId, $ctx['startDate'], $endDate); return [ 'kpis' => [ $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.accounting_journal_entries'), 'value' => $accountingMetrics['journal_entries']], $this->periodComparison( (float) $accountingMetrics['journal_entries'], (float) ($prevPeriodMetrics['journal_entry_count'] ?? 0) ) ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.accounting_pending_cheques'), 'value' => $accountingMetrics['pending_cheques']], $this->periodComparison( (float) $accountingMetrics['pending_cheques'], (float) ($accountingMetrics['prev_pending_cheques'] ?? 0), true ) ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.accounting_overdue_installments'), 'value' => $accountingMetrics['overdue_installments']], null ), ], 'trend' => $this->monthlyAccountingJournalSeries($businessId, $endDate), 'top_items' => $this->topAccountingExposure($businessId), ]; } protected function buildManufacturingDrilldownSection(array $ctx): array { $businessId = $ctx['businessId']; $startDate = $ctx['startDate']; $endDate = $ctx['endDate']; $periodMetrics = $ctx['periodMetrics']; $prevPeriodMetrics = $ctx['prevPeriodMetrics']; $manufacturingMetrics = $this->manufacturingDrilldownMetrics($businessId, $startDate, $endDate, $periodMetrics, $prevPeriodMetrics); return [ 'kpis' => $manufacturingMetrics['kpis'], 'trend' => $this->monthlyProductionCountSeries($businessId, $endDate), 'top_items' => $this->topProductions($businessId, $startDate, $endDate), ]; } protected function buildIeDrilldownSection(array $ctx): array { $businessId = $ctx['businessId']; $startDate = $ctx['startDate']; $endDate = $ctx['endDate']; $periodMetrics = $ctx['periodMetrics']; $prevPeriodMetrics = $ctx['prevPeriodMetrics']; $ieMetrics = $this->ieDrilldownMetrics($businessId, $startDate, $endDate, $periodMetrics, $prevPeriodMetrics); return [ 'kpis' => $ieMetrics['kpis'], 'trend' => $this->monthlyMrpRunsSeries($businessId, $endDate), 'top_items' => $this->topIeExposure($businessId, $startDate, $endDate), ]; } protected function buildQualityDrilldownSection(array $ctx): array { $businessId = $ctx['businessId']; $openNcrs = Schema::hasTable('qms_ncr_reports') ? (int) DB::table('qms_ncr_reports')->where('business_id', $businessId)->whereNotIn('status', ['closed', 'cancelled'])->count() : 0; $openCapas = Schema::hasTable('qms_capa_actions') ? (int) DB::table('qms_capa_actions')->where('business_id', $businessId)->whereNotIn('status', ['closed', 'cancelled'])->count() : 0; $spcAlerts = Schema::hasTable('qms_spc_measurements') ? (int) DB::table('qms_spc_measurements')->where('business_id', $businessId)->where('in_control', false)->where('measured_at', '>=', now()->subDays(30))->count() : 0; return [ 'kpis' => [ $this->enrichKpiWithComparison(['label' => __('managementtools::lang.open_ncrs'), 'value' => $openNcrs], null), $this->enrichKpiWithComparison(['label' => __('managementtools::lang.open_capas'), 'value' => $openCapas], null), $this->enrichKpiWithComparison(['label' => __('managementtools::lang.spc_out_of_control'), 'value' => $spcAlerts], null), ], 'trend' => $this->monthlyCountSeries($businessId, 'qms_ncr_reports', 'created_at', $ctx['endDate']), 'top_items' => [], ]; } protected function buildShopFloorDrilldownSection(array $ctx): array { $businessId = $ctx['businessId']; $avgOee = Schema::hasTable('sf_oee_snapshots') ? (float) DB::table('sf_oee_snapshots')->where('business_id', $businessId)->where('snapshot_date', '>=', $ctx['startDate']->toDateString())->avg('oee_pct') : 0; $events = Schema::hasTable('sf_production_events') ? (int) DB::table('sf_production_events')->where('business_id', $businessId)->whereBetween('recorded_at', [$ctx['startDate'], $ctx['endDate']])->count() : 0; $downtime = Schema::hasTable('sf_downtime_events') ? (int) DB::table('sf_downtime_events')->where('business_id', $businessId)->whereNull('ended_at')->count() : 0; return [ 'kpis' => [ $this->enrichKpiWithComparison(['label' => __('managementtools::lang.avg_oee'), 'value' => round($avgOee, 1)], null), $this->enrichKpiWithComparison(['label' => __('managementtools::lang.production_events'), 'value' => $events], null), $this->enrichKpiWithComparison(['label' => __('managementtools::lang.active_downtime'), 'value' => $downtime], null), ], 'trend' => $this->monthlyCountSeries($businessId, 'sf_production_events', 'recorded_at', $ctx['endDate']), 'top_items' => [], ]; } protected function buildSupplyChainDrilldownSection(array $ctx): array { $businessId = $ctx['businessId']; $avgScore = Schema::hasTable('sc_supplier_scorecards') ? (float) DB::table('sc_supplier_scorecards')->where('business_id', $businessId)->avg('overall_score') : 0; $forecasts = Schema::hasTable('sc_demand_forecasts') ? (int) DB::table('sc_demand_forecasts')->where('business_id', $businessId)->count() : 0; $openRfq = Schema::hasTable('sc_rfq_requests') ? (int) DB::table('sc_rfq_requests')->where('business_id', $businessId)->whereIn('status', ['draft', 'open'])->count() : 0; return [ 'kpis' => [ $this->enrichKpiWithComparison(['label' => __('managementtools::lang.supplier_score_avg'), 'value' => round($avgScore, 1)], null), $this->enrichKpiWithComparison(['label' => __('managementtools::lang.demand_forecasts'), 'value' => $forecasts], null), $this->enrichKpiWithComparison(['label' => __('managementtools::lang.open_rfqs'), 'value' => $openRfq], null), ], 'trend' => $this->monthlyCountSeries($businessId, 'sc_rfq_requests', 'created_at', $ctx['endDate']), 'top_items' => [], ]; } protected function buildPlanningDrilldownSection(array $ctx): array { $businessId = $ctx['businessId']; $resources = Schema::hasTable('ap_capacity_resources') ? (int) DB::table('ap_capacity_resources')->where('business_id', $businessId)->count() : 0; $schedules = Schema::hasTable('ap_schedule_runs') ? (int) DB::table('ap_schedule_runs')->where('business_id', $businessId)->where('status', 'completed')->count() : 0; $sopDrafts = Schema::hasTable('ap_sop_cycles') ? (int) DB::table('ap_sop_cycles')->where('business_id', $businessId)->where('status', 'draft')->count() : 0; return [ 'kpis' => [ $this->enrichKpiWithComparison(['label' => __('managementtools::lang.capacity_resources'), 'value' => $resources], null), $this->enrichKpiWithComparison(['label' => __('managementtools::lang.completed_schedules'), 'value' => $schedules], null), $this->enrichKpiWithComparison(['label' => __('managementtools::lang.sop_draft_cycles'), 'value' => $sopDrafts], null), ], 'trend' => $this->monthlyCountSeries($businessId, 'ap_schedule_runs', 'created_at', $ctx['endDate']), 'top_items' => [], ]; } public function holdingEntityComparison(int $businessId, Carbon $startDate, Carbon $endDate): array { return $this->metricsCache->rememberHoldingComparison( $businessId, $startDate, $endDate, fn () => $this->holdingComparisonService->compare( $businessId, $startDate, $endDate, fn ($current, $previous) => $this->periodComparison($current, $previous), fn ($start, $end) => $this->previousPeriodRange($start, $end) ) ); } protected function entityPeriodSales(int $businessId, int $entityId, int $locationId, Carbon $startDate, Carbon $endDate): float { $batch = $this->transactionMetrics->entitySalesBatch( $businessId, [$entityId], [$entityId => $locationId], $startDate, $endDate ); return (float) ($batch[$entityId] ?? 0); } protected function monthlySalesSeries(int $businessId, Carbon $endDate): array { return $this->transactionMetrics->monthlySalesSeries($businessId, $endDate); } protected function monthlyCountSeries(int $businessId, string $table, string $dateColumn, Carbon $endDate): array { return $this->transactionMetrics->monthlyCountSeries($businessId, $table, $dateColumn, $endDate); } protected function topDebtors(int $businessId): array { if (! Schema::hasTable('contacts')) { return []; } return DB::table('contacts') ->where('business_id', $businessId) ->whereIn('type', ['customer', 'both']) ->where('balance', '>', 0) ->orderByDesc('balance') ->limit(5) ->get(['id', 'name', 'balance']) ->map(function ($row) { $contactMeta = $this->contactActions((int) $row->id); return array_merge([ 'label' => (string) ($row->name ?: '-'), 'value' => (float) $row->balance, 'currency' => true, ], $contactMeta); }) ->toArray(); } protected function topCustomersBySales(int $businessId, Carbon $startDate, Carbon $endDate): array { if (! Schema::hasTable('transactions') || ! Schema::hasColumn('transactions', 'contact_id')) { return []; } $query = DB::table('transactions as t') ->leftJoin('contacts as c', 'c.id', '=', 't.contact_id') ->where('t.business_id', $businessId) ->where('t.type', 'sell') ->where('t.status', 'final') ->whereBetween(DB::raw('date(t.transaction_date)'), [$startDate->toDateString(), $endDate->toDateString()]) ->groupBy('t.contact_id', 'c.name') ->selectRaw('t.contact_id, COALESCE(c.name, ?) as label, SUM(t.final_total) as total', ['-']) ->orderByDesc('total') ->limit(5) ->get(); return $query->map(function ($row) { $contactId = isset($row->contact_id) ? (int) $row->contact_id : 0; $contactMeta = $this->contactActions($contactId); return array_merge([ 'label' => (string) $row->label, 'value' => (float) $row->total, 'currency' => true, ], $contactMeta); })->toArray(); } protected function topMaintenanceBacklog(int $businessId): array { if (! Schema::hasTable('maintenance_work_orders')) { return []; } $rows = DB::table('maintenance_work_orders') ->where('business_id', $businessId) ->whereIn('status', ['draft', 'open', 'in_progress', 'on_hold']) ->orderByDesc('updated_at') ->limit(5) ->get(['id', 'status']); return $rows->map(function ($row) { return [ 'label' => '#'.$row->id.' ('.$row->status.')', 'value' => 1, 'url' => url('/maintenance/work-orders/'.$row->id), 'permissions' => ['maintenance.view', 'maintenance.access'], 'actions' => [ [ 'label' => __('managementtools::lang.view_details'), 'url' => url('/maintenance/work-orders/'.$row->id), 'permissions' => ['maintenance.view', 'maintenance.access'], ], [ 'label' => __('managementtools::lang.open_maintenance_board'), 'url' => url('/maintenance/dashboard'), 'permissions' => ['maintenance.view', 'maintenance.access'], ], ], ]; })->toArray(); } protected function topProjectRisks(int $businessId): array { if (! Schema::hasTable('pp_projects')) { return []; } $query = DB::table('pp_projects') ->where('business_id', $businessId) ->whereIn('status', ['not_started', 'in_progress', 'on_hold']) ->orderByDesc('updated_at') ->limit(5) ->get(['id', 'name', 'status']); return $query->map(function ($row) { return [ 'label' => (string) ($row->name ?: '-').' ('.$row->status.')', 'value' => 1, 'url' => url('/professional-project/projects/'.$row->id), 'permissions' => ['managementtools.board_view'], 'actions' => [ [ 'label' => __('managementtools::lang.view_details'), 'url' => url('/professional-project/projects/'.$row->id), 'permissions' => ['managementtools.board_view'], ], [ 'label' => __('managementtools::lang.open_project_center'), 'url' => url('/professional-project/projects?view=list'), 'permissions' => ['managementtools.board_view'], ], ], ]; })->toArray(); } protected function topReporters(int $businessId, Carbon $startDate, Carbon $endDate): array { if (! Schema::hasTable('mt_daily_reports') || ! Schema::hasColumn('mt_daily_reports', 'user_id')) { return []; } $query = DB::table('mt_daily_reports as r') ->leftJoin('users as u', 'u.id', '=', 'r.user_id') ->where('r.business_id', $businessId) ->whereBetween(DB::raw('date(r.report_date)'), [$startDate->toDateString(), $endDate->toDateString()]) ->groupBy('r.user_id', 'u.username') ->selectRaw('r.user_id, COALESCE(u.username, ?) as label, COUNT(*) as total', ['-']) ->orderByDesc('total') ->limit(5) ->get(); return $query->map(function ($row) { $userId = isset($row->user_id) ? (int) $row->user_id : 0; return [ 'label' => (string) $row->label, 'value' => (int) $row->total, 'url' => $userId > 0 ? url('/users/'.$userId.'/edit') : url('/users'), 'permissions' => ['user.view', 'user.create', 'user.update'], 'actions' => [ [ 'label' => __('managementtools::lang.view_details'), 'url' => $userId > 0 ? url('/users/'.$userId.'/edit') : url('/users'), 'permissions' => ['user.view', 'user.create', 'user.update'], ], [ 'label' => __('managementtools::lang.open_daily_reports'), 'url' => url('/management-tools/daily-reports'), 'permissions' => ['managementtools.board_view'], ], ], ]; })->toArray(); } protected function crmViewPermissions(): array { return ['crm.access_all_leads', 'crm.access_own_leads', 'managementtools.board_view']; } protected function accountingViewPermissions(): array { return ['accounting.access_accounting_module', 'accounting.view_reports', 'managementtools.board_view']; } protected function manufacturingViewPermissions(): array { return ['manufacturing.access_production', 'manufacturing.access_recipe', 'managementtools.board_view']; } protected function crmDrilldownMetrics(int $businessId, Carbon $startDate, Carbon $endDate): array { $metrics = [ 'total_leads' => 0, 'new_leads' => 0, 'pipeline_value' => 0.0, ]; if (! Schema::hasTable('contacts')) { return $metrics; } $leadQuery = DB::table('contacts') ->where('business_id', $businessId) ->where('type', 'lead'); if (Schema::hasColumn('contacts', 'contact_status')) { $leadQuery->where('contact_status', 'active'); } $metrics['total_leads'] = (int) (clone $leadQuery)->count(); $metrics['new_leads'] = (int) (clone $leadQuery) ->whereBetween(DB::raw('date(created_at)'), [$startDate->toDateString(), $endDate->toDateString()]) ->count(); if (Schema::hasColumn('contacts', 'crm_lead_value')) { $metrics['pipeline_value'] = (float) (clone $leadQuery)->sum('crm_lead_value'); } return $metrics; } protected function accountingDrilldownMetrics(int $businessId, Carbon $startDate, Carbon $endDate): array { $metrics = [ 'journal_entries' => 0, 'pending_cheques' => 0, 'prev_pending_cheques' => 0, 'overdue_installments' => 0, ]; if (Schema::hasTable('accounting_accounts_transactions') && Schema::hasTable('accounting_accounts')) { $dateColumn = Schema::hasColumn('accounting_accounts_transactions', 'operation_date') ? 'operation_date' : 'created_at'; $metrics['journal_entries'] = (int) DB::table('accounting_accounts_transactions as aat') ->join('accounting_accounts as aa', 'aa.id', '=', 'aat.accounting_account_id') ->where('aa.business_id', $businessId) ->whereBetween(DB::raw('date(aat.'.$dateColumn.')'), [$startDate->toDateString(), $endDate->toDateString()]) ->count(); } if (Schema::hasTable('accounting_cheques')) { $metrics['pending_cheques'] = (int) DB::table('accounting_cheques') ->where('business_id', $businessId) ->whereIn('status', ['pending', 'deposited']) ->count(); } if (Schema::hasTable('accounting_installment_schedule') && Schema::hasTable('accounting_installment_plans')) { $metrics['overdue_installments'] = (int) DB::table('accounting_installment_schedule as s') ->join('accounting_installment_plans as p', 'p.id', '=', 's.installment_plan_id') ->where('p.business_id', $businessId) ->where('s.status', 'overdue') ->count(); } return $metrics; } protected function manufacturingDrilldownMetrics(int $businessId, Carbon $startDate, Carbon $endDate, array $periodMetrics, array $prevPeriodMetrics): array { $productionCount = (int) ($periodMetrics['production_count'] ?? 0); $productionValue = (float) ($periodMetrics['production_value'] ?? 0); $activeRecipes = 0; if (Schema::hasTable('mfg_recipes')) { $activeRecipes = $this->countManufacturingRecipes($businessId); } return [ 'kpis' => [ $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.manufacturing_production_count'), 'value' => $productionCount], $this->periodComparison( (float) $productionCount, (float) ($prevPeriodMetrics['production_count'] ?? 0) ) ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.manufacturing_production_value'), 'value' => $productionValue, 'currency' => true], $this->periodComparison( $productionValue, (float) ($prevPeriodMetrics['production_value'] ?? 0) ) ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.manufacturing_active_recipes'), 'value' => $activeRecipes], null ), ], ]; } protected function monthlyLeadsSeries(int $businessId, Carbon $endDate): array { if (! Schema::hasTable('contacts')) { return [0, 0, 0, 0, 0, 0]; } $series = []; for ($i = 5; $i >= 0; $i--) { $monthStart = $endDate->copy()->startOfMonth()->subMonths($i); $monthEnd = $monthStart->copy()->endOfMonth(); $query = DB::table('contacts') ->where('business_id', $businessId) ->where('type', 'lead') ->whereBetween(DB::raw('date(created_at)'), [$monthStart->toDateString(), $monthEnd->toDateString()]); if (Schema::hasColumn('contacts', 'contact_status')) { $query->where('contact_status', 'active'); } $series[] = (int) $query->count(); } return $series; } protected function monthlyAccountingJournalSeries(int $businessId, Carbon $endDate): array { if (! Schema::hasTable('accounting_accounts_transactions') || ! Schema::hasTable('accounting_accounts')) { return [0, 0, 0, 0, 0, 0]; } $dateColumn = Schema::hasColumn('accounting_accounts_transactions', 'operation_date') ? 'operation_date' : 'created_at'; $series = []; for ($i = 5; $i >= 0; $i--) { $monthStart = $endDate->copy()->startOfMonth()->subMonths($i); $monthEnd = $monthStart->copy()->endOfMonth(); $series[] = (int) DB::table('accounting_accounts_transactions as aat') ->join('accounting_accounts as aa', 'aa.id', '=', 'aat.accounting_account_id') ->where('aa.business_id', $businessId) ->whereBetween(DB::raw('date(aat.'.$dateColumn.')'), [$monthStart->toDateString(), $monthEnd->toDateString()]) ->count(); } return $series; } protected function monthlyProductionCountSeries(int $businessId, Carbon $endDate): array { if (! Schema::hasTable('transactions')) { return [0, 0, 0, 0, 0, 0]; } $series = []; for ($i = 5; $i >= 0; $i--) { $monthStart = $endDate->copy()->startOfMonth()->subMonths($i); $monthEnd = $monthStart->copy()->endOfMonth(); $series[] = (int) DB::table('transactions') ->where('business_id', $businessId) ->where('type', 'production_purchase') ->whereBetween(DB::raw('date(transaction_date)'), [$monthStart->toDateString(), $monthEnd->toDateString()]) ->count(); } return $series; } protected function topCrmLeads(int $businessId): array { if (! Schema::hasTable('contacts')) { return []; } $query = DB::table('contacts') ->where('business_id', $businessId) ->where('type', 'lead'); if (Schema::hasColumn('contacts', 'contact_status')) { $query->where('contact_status', 'active'); } if (Schema::hasColumn('contacts', 'crm_lead_value')) { $query->orderByDesc('crm_lead_value'); } else { $query->orderByDesc('created_at'); } $permissions = $this->crmViewPermissions(); $hasLeadValue = Schema::hasColumn('contacts', 'crm_lead_value'); $columns = ['id', 'name']; if (Schema::hasColumn('contacts', 'crm_lead_title')) { $columns[] = 'crm_lead_title'; } if ($hasLeadValue) { $columns[] = 'crm_lead_value'; } return $query->limit(5)->get($columns) ->map(function ($row) use ($permissions, $hasLeadValue) { $label = (string) (($row->crm_lead_title ?? null) ?: $row->name ?: '-'); $value = $hasLeadValue ? (float) ($row->crm_lead_value ?? 0) : 0; return [ 'label' => $label, 'value' => $value, 'currency' => $hasLeadValue, 'url' => url('/contacts/'.$row->id), 'permissions' => $permissions, 'actions' => [ [ 'label' => __('managementtools::lang.view_details'), 'url' => url('/contacts/'.$row->id), 'permissions' => $permissions, ], [ 'label' => __('managementtools::lang.open_crm_board'), 'url' => url('/crm/dashboard'), 'permissions' => $permissions, ], ], ]; })->toArray(); } protected function topAccountingExposure(int $businessId): array { $items = []; $permissions = $this->accountingViewPermissions(); if (Schema::hasTable('accounting_cheques')) { $cheques = DB::table('accounting_cheques') ->where('business_id', $businessId) ->whereIn('status', ['pending', 'deposited']) ->orderByDesc('amount') ->limit(5) ->get(['id', 'cheque_number', 'amount', 'status']); foreach ($cheques as $cheque) { $items[] = [ 'label' => __('managementtools::lang.accounting_cheque_item', [ 'number' => $cheque->cheque_number, 'status' => $cheque->status, ]), 'value' => (float) $cheque->amount, 'currency' => true, 'url' => $this->routeIfExists('accounting.cheques') ?: url('/accounting/cheques'), 'permissions' => $permissions, 'actions' => [ [ 'label' => __('managementtools::lang.view_details'), 'url' => $this->routeIfExists('accounting.cheques') ?: url('/accounting/cheques'), 'permissions' => $permissions, ], [ 'label' => __('managementtools::lang.open_accounting_dashboard'), 'url' => url('/accounting/dashboard'), 'permissions' => $permissions, ], ], ]; } } if (empty($items) && Schema::hasTable('accounting_accounts') && Schema::hasTable('accounting_accounts_transactions')) { $accounts = DB::table('accounting_accounts as aa') ->leftJoin('accounting_accounts_transactions as aat', 'aat.accounting_account_id', '=', 'aa.id') ->where('aa.business_id', $businessId) ->groupBy('aa.id', 'aa.name') ->selectRaw('aa.id, aa.name as label, COUNT(aat.id) as total') ->orderByDesc('total') ->limit(5) ->get(); foreach ($accounts as $account) { $items[] = [ 'label' => (string) $account->label, 'value' => (int) $account->total, 'url' => $this->routeIfExists('accounting.ledger') ? route('accounting.ledger', ['id' => $account->id]) : url('/accounting/chart-of-accounts'), 'permissions' => $permissions, 'actions' => [ [ 'label' => __('managementtools::lang.view_details'), 'url' => $this->routeIfExists('accounting.ledger') ? route('accounting.ledger', ['id' => $account->id]) : url('/accounting/chart-of-accounts'), 'permissions' => $permissions, ], ], ]; } } return $items; } protected function topProductions(int $businessId, Carbon $startDate, Carbon $endDate): array { if (! Schema::hasTable('transactions')) { return []; } $permissions = $this->manufacturingViewPermissions(); return DB::table('transactions') ->where('business_id', $businessId) ->where('type', 'production_purchase') ->whereBetween(DB::raw('date(transaction_date)'), [$startDate->toDateString(), $endDate->toDateString()]) ->orderByDesc('final_total') ->limit(5) ->get(['id', 'ref_no', 'final_total', 'transaction_date']) ->map(function ($row) use ($permissions) { $label = (string) ($row->ref_no ?: '#'.$row->id); return [ 'label' => $label, 'value' => (float) $row->final_total, 'currency' => true, 'url' => url('/manufacturing/production/'.$row->id), 'permissions' => $permissions, 'actions' => [ [ 'label' => __('managementtools::lang.view_details'), 'url' => url('/manufacturing/production/'.$row->id), 'permissions' => $permissions, ], [ 'label' => __('managementtools::lang.open_manufacturing_board'), 'url' => url('/manufacturing/production'), 'permissions' => $permissions, ], ], ]; })->toArray(); } protected function ieViewPermissions(): array { return ['ie.view', 'ie.bom.view', 'ie.costing.view', 'managementtools.board_view']; } protected function ieDrilldownMetrics(int $businessId, Carbon $startDate, Carbon $endDate, array $periodMetrics, array $prevPeriodMetrics): array { $mrpRuns = (int) ($periodMetrics['ie_mrp_runs'] ?? 0); $activeBoms = 0; $productionLines = 0; $releasedRevisions = 0; $shortageRuns = 0; if (Schema::hasTable('ie_boms')) { $activeBoms = (int) DB::table('ie_boms') ->where('business_id', $businessId) ->whereNull('deleted_at') ->count(); } if (Schema::hasTable('ie_line_templates')) { $lineQuery = DB::table('ie_line_templates') ->where('business_id', $businessId) ->whereNull('deleted_at'); if (Schema::hasColumn('ie_line_templates', 'parent_template_id')) { $lineQuery->whereNull('parent_template_id'); } $productionLines = (int) $lineQuery->count(); } if (Schema::hasTable('ie_line_revisions')) { $releasedRevisions = (int) DB::table('ie_line_revisions') ->where('business_id', $businessId) ->where('status', 'released') ->whereNull('deleted_at') ->count(); } if (Schema::hasTable('ie_mrp_runs')) { $runs = DB::table('ie_mrp_runs') ->where('business_id', $businessId) ->whereBetween(DB::raw('date(created_at)'), [$startDate->toDateString(), $endDate->toDateString()]) ->get(['shortage_analysis']); foreach ($runs as $run) { $analysis = json_decode((string) ($run->shortage_analysis ?? ''), true); if (! empty($analysis['shortages'])) { $shortageRuns++; } } } return [ 'kpis' => [ $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.ie_mrp_runs'), 'value' => $mrpRuns], $this->periodComparison( (float) $mrpRuns, (float) ($prevPeriodMetrics['ie_mrp_runs'] ?? 0) ) ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.ie_active_boms'), 'value' => $activeBoms], null ), $this->enrichKpiWithComparison( ['label' => __('managementtools::lang.ie_production_lines'), 'value' => $productionLines], null ), ], 'released_revisions' => $releasedRevisions, 'shortage_runs' => $shortageRuns, ]; } protected function monthlyMrpRunsSeries(int $businessId, Carbon $endDate): array { if (! Schema::hasTable('ie_mrp_runs')) { return [0, 0, 0, 0, 0, 0]; } $series = []; for ($i = 5; $i >= 0; $i--) { $monthStart = $endDate->copy()->startOfMonth()->subMonths($i); $monthEnd = $monthStart->copy()->endOfMonth(); $series[] = (int) DB::table('ie_mrp_runs') ->where('business_id', $businessId) ->whereBetween(DB::raw('date(created_at)'), [$monthStart->toDateString(), $monthEnd->toDateString()]) ->count(); } return $series; } protected function topIeExposure(int $businessId, Carbon $startDate, Carbon $endDate): array { $items = []; $permissions = $this->ieViewPermissions(); if (Schema::hasTable('ie_mrp_runs')) { $runs = DB::table('ie_mrp_runs as r') ->leftJoin('ie_boms as b', 'b.id', '=', 'r.bom_id') ->where('r.business_id', $businessId) ->whereBetween(DB::raw('date(r.created_at)'), [$startDate->toDateString(), $endDate->toDateString()]) ->orderByDesc('r.created_at') ->limit(5) ->get(['r.id', 'r.run_code', 'r.planned_quantity', 'r.shortage_analysis', 'b.name as bom_name']); foreach ($runs as $run) { $analysis = json_decode((string) ($run->shortage_analysis ?? ''), true); $shortageCount = is_array($analysis['shortages'] ?? null) ? count($analysis['shortages']) : 0; $shortageValue = (float) ($analysis['totals']['shortage_value'] ?? 0); $label = (string) ($run->run_code ?: 'MRP #'.$run->id); if (! empty($run->bom_name)) { $label .= ' — '.$run->bom_name; } $items[] = [ 'label' => $label, 'value' => $shortageValue > 0 ? $shortageValue : $shortageCount, 'currency' => $shortageValue > 0, 'url' => url('/industrial-engineering/mrp'), 'permissions' => $permissions, 'actions' => [ [ 'label' => __('managementtools::lang.view_details'), 'url' => url('/industrial-engineering/mrp'), 'permissions' => $permissions, ], [ 'label' => __('managementtools::lang.open_ie_dashboard'), 'url' => url('/industrial-engineering/dashboard'), 'permissions' => $permissions, ], ], ]; } } if (empty($items) && Schema::hasTable('ie_cost_scenarios')) { $scenarios = DB::table('ie_cost_scenarios') ->where('business_id', $businessId) ->whereNull('deleted_at') ->orderByDesc('total_cost') ->limit(5) ->get(['id', 'scenario_code', 'name', 'total_cost']); foreach ($scenarios as $scenario) { $items[] = [ 'label' => (string) (($scenario->name ?: $scenario->scenario_code) ?: '-'), 'value' => (float) $scenario->total_cost, 'currency' => true, 'url' => url('/industrial-engineering/dashboard'), 'permissions' => $permissions, 'actions' => [ [ 'label' => __('managementtools::lang.view_details'), 'url' => url('/industrial-engineering/dashboard'), 'permissions' => $permissions, ], ], ]; } } return $items; } protected function countManufacturingRecipes(int $businessId): int { if (! Schema::hasTable('mfg_recipes')) { return 0; } $query = DB::table('mfg_recipes'); if (Schema::hasColumn('mfg_recipes', 'business_id')) { return (int) $query->where('business_id', $businessId)->count(); } if (! Schema::hasTable('products') || ! Schema::hasColumn('products', 'business_id')) { return (int) $query->count(); } return (int) $query ->join('products as p', 'p.id', '=', 'mfg_recipes.product_id') ->where('p.business_id', $businessId) ->count(); } protected function routeIfExists(string $name): ?string { return Route::has($name) ? route($name) : null; } }