$this->revenueStats(), 'quote_conversion' => $this->quoteConversionRate(), 'avg_amount_per_case' => $this->avgAmountPerCase(), 'avg_payment_delay' => $this->avgPaymentDelay(), 'avoirs' => $this->avoirsStats(), 'receivables' => $this->receivablesStats(), ]; } // ─── 1. Chiffre d'affaires mensuel & annuel ─────────────────────────────── private function revenueStats(): array { $currentYear = now()->year; // Monthly CA for current year $monthly = DB::table('invoices') ->selectRaw('MONTH(invoice_date) as month, SUM(total_ttc) as total_ttc, SUM(total_ht) as total_ht, COUNT(*) as count') ->whereIn('status', self::REVENUE_STATUSES) ->whereYear('invoice_date', $currentYear) ->groupByRaw('MONTH(invoice_date)') ->orderByRaw('MONTH(invoice_date)') ->get() ->keyBy('month'); // Build a full 12-month array (0 for missing months) $monthlyFull = []; for ($m = 1; $m <= 12; $m++) { $row = $monthly->get($m); $monthlyFull[] = [ 'month' => $m, 'total_ttc' => $row ? (float) $row->total_ttc : 0.0, 'total_ht' => $row ? (float) $row->total_ht : 0.0, 'count' => $row ? (int) $row->count : 0, ]; } // Annual CA — current and previous year for comparison $annualRows = DB::table('invoices') ->selectRaw('YEAR(invoice_date) as year, SUM(total_ttc) as total_ttc, SUM(total_ht) as total_ht, COUNT(*) as count') ->whereIn('status', self::REVENUE_STATUSES) ->whereYear('invoice_date', '>=', $currentYear - 1) ->groupByRaw('YEAR(invoice_date)') ->orderByRaw('YEAR(invoice_date)') ->get() ->keyBy('year'); $yearCurrent = $annualRows->get($currentYear); $yearPrevious = $annualRows->get($currentYear - 1); $annualCurrentTtc = $yearCurrent ? (float) $yearCurrent->total_ttc : 0.0; $annualPreviousTtc = $yearPrevious ? (float) $yearPrevious->total_ttc : 0.0; $annualGrowthPct = $annualPreviousTtc > 0 ? round((($annualCurrentTtc - $annualPreviousTtc) / $annualPreviousTtc) * 100, 2) : null; return [ 'current_year' => $currentYear, 'annual_current' => [ 'total_ttc' => $annualCurrentTtc, 'total_ht' => $yearCurrent ? (float) $yearCurrent->total_ht : 0.0, 'count' => $yearCurrent ? (int) $yearCurrent->count : 0, ], 'annual_previous' => [ 'total_ttc' => $annualPreviousTtc, 'total_ht' => $yearPrevious ? (float) $yearPrevious->total_ht : 0.0, 'count' => $yearPrevious ? (int) $yearPrevious->count : 0, ], 'annual_growth_pct' => $annualGrowthPct, 'monthly' => $monthlyFull, ]; } // ─── 2. Taux de conversion devis → facture ──────────────────────────────── private function quoteConversionRate(): array { $totalQuotes = (int) DB::table('quotes') ->whereNotIn('status', ['annule']) ->count(); // A quote is "converted" when at least one invoice references it $convertedQuotes = (int) DB::table('quotes') ->join('invoices', 'invoices.source_quote_id', '=', 'quotes.id') ->whereNotIn('quotes.status', ['annule']) ->distinct('quotes.id') ->count('quotes.id'); $rate = $totalQuotes > 0 ? round(($convertedQuotes / $totalQuotes) * 100, 2) : 0.0; // Breakdown by quote status $byStatus = DB::table('quotes') ->selectRaw('status, COUNT(*) as total') ->groupBy('status') ->get(); return [ 'total_quotes' => $totalQuotes, 'converted_quotes' => $convertedQuotes, 'conversion_rate' => $rate, 'by_status' => $byStatus, ]; } // ─── 3. Montant moyen par dossier (panier moyen) ───────────────────────── private function avgAmountPerCase(): array { $row = DB::table('invoices') ->selectRaw('AVG(total_ttc) as avg_ttc, AVG(total_ht) as avg_ht, COUNT(*) as total_count, SUM(total_ttc) as sum_ttc') ->whereIn('status', self::REVENUE_STATUSES) ->first(); return [ 'avg_ttc' => $row && $row->avg_ttc !== null ? round((float) $row->avg_ttc, 2) : null, 'avg_ht' => $row && $row->avg_ht !== null ? round((float) $row->avg_ht, 2) : null, 'total_count' => $row ? (int) $row->total_count : 0, 'total_ttc' => $row ? (float) $row->sum_ttc : 0.0, ]; } // ─── 4. Délai moyen de paiement ─────────────────────────────────────────── private function avgPaymentDelay(): array { // Use document_status_history to find the exact moment status became 'payee' $avgFromHistory = DB::table('invoices') ->join('document_status_history as dsh', function ($join) { $join->on('dsh.document_id', '=', 'invoices.id') ->where('dsh.document_type', '=', 'invoice') ->where('dsh.new_status', '=', 'payee'); }) ->selectRaw('AVG(DATEDIFF(dsh.changed_at, invoices.invoice_date)) as avg_days') ->where('invoices.status', 'payee') ->value('avg_days'); // Fallback: avg delay between invoice_date and due_date for paid invoices $avgFromDueDate = DB::table('invoices') ->selectRaw('AVG(DATEDIFF(due_date, invoice_date)) as avg_days') ->where('status', 'payee') ->whereNotNull('due_date') ->value('avg_days'); // Count overdue invoices (echue = past due_date, still unpaid) $overdueCount = (int) DB::table('invoices') ->where('status', 'echue') ->count(); $overdueTotal = (float) DB::table('invoices') ->where('status', 'echue') ->sum('total_ttc'); return [ 'avg_days_to_payment' => $avgFromHistory !== null ? round((float) $avgFromHistory, 1) : ($avgFromDueDate !== null ? round((float) $avgFromDueDate, 1) : null), 'avg_days_invoice_to_due' => $avgFromDueDate !== null ? round((float) $avgFromDueDate, 1) : null, 'overdue_invoices_count' => $overdueCount, 'overdue_invoices_total_ttc' => $overdueTotal, ]; } // ─── 5. Volume d'avoirs émis ────────────────────────────────────────────── private function avoirsStats(): array { $totals = DB::table('avoirs') ->selectRaw('COUNT(*) as count, SUM(total_ttc) as total_ttc, SUM(total_ht) as total_ht') ->whereIn('status', self::AVOIR_EMITTED_STATUSES) ->first(); // Breakdown by reason type $byReason = DB::table('avoirs') ->selectRaw('reason_type, COUNT(*) as count, SUM(total_ttc) as total_ttc') ->whereIn('status', self::AVOIR_EMITTED_STATUSES) ->groupBy('reason_type') ->orderByDesc('count') ->get(); // Monthly trend (current year) $monthlyTrend = DB::table('avoirs') ->selectRaw('MONTH(avoir_date) as month, COUNT(*) as count, SUM(total_ttc) as total_ttc') ->whereIn('status', self::AVOIR_EMITTED_STATUSES) ->whereYear('avoir_date', now()->year) ->groupByRaw('MONTH(avoir_date)') ->orderByRaw('MONTH(avoir_date)') ->get(); return [ 'total_count' => $totals ? (int) $totals->count : 0, 'total_ttc' => $totals ? (float) $totals->total_ttc : 0.0, 'total_ht' => $totals ? (float) $totals->total_ht : 0.0, 'by_reason' => $byReason, 'monthly_trend' => $monthlyTrend, ]; } // ─── 6. Créances en cours (relances prioritaires) ───────────────────────── private function receivablesStats(): array { // Global totals $totals = DB::table('invoices') ->selectRaw('COUNT(*) as count, SUM(total_ttc) as total_ttc') ->whereIn('status', self::RECEIVABLE_STATUSES) ->first(); // Breakdown by status $byStatus = DB::table('invoices') ->selectRaw('status, COUNT(*) as count, SUM(total_ttc) as total_ttc') ->whereIn('status', self::RECEIVABLE_STATUSES) ->groupBy('status') ->get(); // Top 10 clients with highest outstanding balance $topDebtors = DB::table('invoices') ->join('clients', 'invoices.client_id', '=', 'clients.id') ->select( 'clients.id', 'clients.name', DB::raw('COUNT(invoices.id) as invoice_count'), DB::raw('SUM(invoices.total_ttc) as total_outstanding') ) ->whereIn('invoices.status', self::RECEIVABLE_STATUSES) ->groupBy('clients.id', 'clients.name') ->orderByDesc('total_outstanding') ->limit(10) ->get(); // Overdue (echue) + near-due (due within 7 days) detail $criticalInvoices = DB::table('invoices') ->join('clients', 'invoices.client_id', '=', 'clients.id') ->select( 'invoices.id', 'invoices.invoice_number', 'invoices.status', 'invoices.due_date', 'invoices.total_ttc', 'clients.name as client_name', 'clients.id as client_id', DB::raw('DATEDIFF(NOW(), invoices.due_date) as days_overdue') ) ->where(function ($q) { $q->where('invoices.status', 'echue') ->orWhere(function ($q2) { $q2->whereIn('invoices.status', ['emise', 'envoyee', 'partiellement_payee']) ->whereRaw('invoices.due_date <= DATE_ADD(NOW(), INTERVAL 7 DAY)') ->whereNotNull('invoices.due_date'); }); }) ->orderByRaw('invoices.due_date ASC') ->limit(20) ->get(); return [ 'total_count' => $totals ? (int) $totals->count : 0, 'total_outstanding' => $totals ? (float) $totals->total_ttc : 0.0, 'by_status' => $byStatus, 'top_debtors' => $topDebtors, 'critical_invoices' => $criticalInvoices, ]; } }