db = Database::getInstance(); } // ==================== MÉTHODES PUBLIQUES ==================== /** * Récupère le résumé des stats pour une date donnée * * @param int|null $entityId ID entité (null = toutes entités pour super-admin) * @param string|null $date Date (YYYY-MM-DD), défaut = aujourd'hui * @return array Stats résumées par catégorie */ public function getSummary(?int $entityId, ?string $date = null): array { $date = $date ?? date('Y-m-d'); $sql = " SELECT event_type, count, sum_amount, unique_users, metadata FROM event_stats_daily WHERE stat_date = :date "; $params = ['date' => $date]; if ($entityId !== null) { $sql .= " AND entity_id = :entity_id"; $params['entity_id'] = $entityId; } else { $sql .= " AND entity_id IS NULL"; } $stmt = $this->db->prepare($sql); $stmt->execute($params); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); return $this->formatSummary($date, $rows); } /** * Récupère les stats journalières sur une plage de dates * * @param int|null $entityId ID entité (null = toutes entités) * @param string $from Date début (YYYY-MM-DD) * @param string $to Date fin (YYYY-MM-DD) * @param array $eventTypes Filtrer par types d'événements (optionnel) * @return array Stats par jour */ public function getDaily(?int $entityId, string $from, string $to, array $eventTypes = []): array { $sql = " SELECT stat_date, event_type, count, sum_amount, unique_users FROM event_stats_daily WHERE stat_date BETWEEN :from AND :to "; $params = ['from' => $from, 'to' => $to]; if ($entityId !== null) { $sql .= " AND entity_id = :entity_id"; $params['entity_id'] = $entityId; } else { $sql .= " AND entity_id IS NULL"; } if (!empty($eventTypes)) { $placeholders = []; foreach ($eventTypes as $i => $type) { $key = "event_type_{$i}"; $placeholders[] = ":{$key}"; $params[$key] = $type; } $sql .= " AND event_type IN (" . implode(', ', $placeholders) . ")"; } $sql .= " ORDER BY stat_date ASC, event_type ASC"; $stmt = $this->db->prepare($sql); $stmt->execute($params); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); return $this->formatDaily($rows); } /** * Récupère les stats hebdomadaires (calculées depuis daily) * * @param int|null $entityId ID entité * @param string $from Date début * @param string $to Date fin * @param array $eventTypes Filtrer par types d'événements * @return array Stats par semaine */ public function getWeekly(?int $entityId, string $from, string $to, array $eventTypes = []): array { $daily = $this->getDaily($entityId, $from, $to, $eventTypes); $weekly = []; foreach ($daily as $day) { $date = new \DateTime($day['date']); $weekStart = clone $date; $weekStart->modify('monday this week'); $weekKey = $weekStart->format('Y-m-d'); if (!isset($weekly[$weekKey])) { $weekly[$weekKey] = [ 'week_start' => $weekKey, 'week_number' => (int) $date->format('W'), 'year' => (int) $date->format('Y'), 'events' => [], 'totals' => [ 'count' => 0, 'sum_amount' => 0.0, ], ]; } // Agréger les événements foreach ($day['events'] as $eventType => $stats) { if (!isset($weekly[$weekKey]['events'][$eventType])) { $weekly[$weekKey]['events'][$eventType] = [ 'count' => 0, 'sum_amount' => 0.0, 'unique_users' => 0, ]; } $weekly[$weekKey]['events'][$eventType]['count'] += $stats['count']; $weekly[$weekKey]['events'][$eventType]['sum_amount'] += $stats['sum_amount']; $weekly[$weekKey]['events'][$eventType]['unique_users'] += $stats['unique_users']; } $weekly[$weekKey]['totals']['count'] += $day['totals']['count']; $weekly[$weekKey]['totals']['sum_amount'] += $day['totals']['sum_amount']; } return array_values($weekly); } /** * Récupère les stats mensuelles (calculées depuis daily) * * @param int|null $entityId ID entité * @param int $year Année * @param array $eventTypes Filtrer par types d'événements * @return array Stats par mois */ public function getMonthly(?int $entityId, int $year, array $eventTypes = []): array { $from = "{$year}-01-01"; $to = "{$year}-12-31"; $daily = $this->getDaily($entityId, $from, $to, $eventTypes); $monthly = []; foreach ($daily as $day) { $monthKey = substr($day['date'], 0, 7); // YYYY-MM if (!isset($monthly[$monthKey])) { $monthly[$monthKey] = [ 'month' => $monthKey, 'year' => $year, 'month_number' => (int) substr($monthKey, 5, 2), 'events' => [], 'totals' => [ 'count' => 0, 'sum_amount' => 0.0, ], ]; } // Agréger les événements foreach ($day['events'] as $eventType => $stats) { if (!isset($monthly[$monthKey]['events'][$eventType])) { $monthly[$monthKey]['events'][$eventType] = [ 'count' => 0, 'sum_amount' => 0.0, 'unique_users' => 0, ]; } $monthly[$monthKey]['events'][$eventType]['count'] += $stats['count']; $monthly[$monthKey]['events'][$eventType]['sum_amount'] += $stats['sum_amount']; $monthly[$monthKey]['events'][$eventType]['unique_users'] += $stats['unique_users']; } $monthly[$monthKey]['totals']['count'] += $day['totals']['count']; $monthly[$monthKey]['totals']['sum_amount'] += $day['totals']['sum_amount']; } return array_values($monthly); } /** * Récupère le détail des événements depuis le fichier JSONL * * @param int|null $entityId ID entité (null = tous) * @param string $date Date (YYYY-MM-DD) * @param string|null $eventType Filtrer par type d'événement * @param int $limit Nombre max de résultats * @param int $offset Décalage pour pagination * @return array Événements détaillés avec pagination */ public function getDetails( ?int $entityId, string $date, ?string $eventType = null, int $limit = 50, int $offset = 0 ): array { $limit = min($limit, self::MAX_DETAILS_LIMIT); $filePath = self::EVENT_LOG_DIR . '/' . $date . '.jsonl'; if (!file_exists($filePath)) { return [ 'date' => $date, 'events' => [], 'pagination' => [ 'total' => 0, 'limit' => $limit, 'offset' => $offset, 'has_more' => false, ], ]; } $events = []; $total = 0; $currentIndex = 0; $handle = fopen($filePath, 'r'); if (!$handle) { return [ 'date' => $date, 'events' => [], 'pagination' => [ 'total' => 0, 'limit' => $limit, 'offset' => $offset, 'has_more' => false, ], 'error' => 'Impossible de lire le fichier', ]; } while (($line = fgets($handle)) !== false) { $line = trim($line); if (empty($line)) { continue; } $event = json_decode($line, true); if (!$event || !isset($event['event'])) { continue; } // Filtrer par entity_id if ($entityId !== null) { $eventEntityId = $event['entity_id'] ?? null; if ($eventEntityId !== $entityId) { continue; } } // Filtrer par event_type if ($eventType !== null && ($event['event'] ?? '') !== $eventType) { continue; } $total++; // Pagination if ($currentIndex >= $offset && count($events) < $limit) { $events[] = $this->sanitizeEventForOutput($event); } $currentIndex++; } fclose($handle); return [ 'date' => $date, 'events' => $events, 'pagination' => [ 'total' => $total, 'limit' => $limit, 'offset' => $offset, 'has_more' => ($offset + $limit) < $total, ], ]; } /** * Récupère les types d'événements disponibles * * @return array Liste des types d'événements */ public function getEventTypes(): array { return [ 'auth' => ['login_success', 'login_failed', 'logout'], 'passages' => ['passage_created', 'passage_updated', 'passage_deleted'], 'sectors' => ['sector_created', 'sector_updated', 'sector_deleted'], 'users' => ['user_created', 'user_updated', 'user_deleted'], 'entities' => ['entity_created', 'entity_updated', 'entity_deleted'], 'operations' => ['operation_created', 'operation_updated', 'operation_deleted'], 'stripe' => [ 'stripe_payment_created', 'stripe_payment_success', 'stripe_payment_failed', 'stripe_payment_cancelled', 'stripe_terminal_error', ], ]; } /** * Vérifie si des stats existent pour une date * * @param string $date Date à vérifier * @return bool */ public function hasStatsForDate(string $date): bool { $stmt = $this->db->prepare(" SELECT COUNT(*) FROM event_stats_daily WHERE stat_date = :date "); $stmt->execute(['date' => $date]); return (int) $stmt->fetchColumn() > 0; } // ==================== MÉTHODES PRIVÉES ==================== /** * Formate le résumé des stats par catégorie */ private function formatSummary(string $date, array $rows): array { $summary = [ 'date' => $date, 'stats' => [ 'auth' => ['success' => 0, 'failed' => 0, 'logout' => 0], 'passages' => ['created' => 0, 'updated' => 0, 'deleted' => 0, 'amount' => 0.0], 'users' => ['created' => 0, 'updated' => 0, 'deleted' => 0], 'sectors' => ['created' => 0, 'updated' => 0, 'deleted' => 0], 'entities' => ['created' => 0, 'updated' => 0, 'deleted' => 0], 'operations' => ['created' => 0, 'updated' => 0, 'deleted' => 0], 'stripe' => ['created' => 0, 'success' => 0, 'failed' => 0, 'cancelled' => 0, 'amount' => 0.0], ], 'totals' => [ 'events' => 0, 'unique_users' => 0, ], ]; $uniqueUsersSet = []; foreach ($rows as $row) { $eventType = $row['event_type']; $count = (int) $row['count']; $amount = (float) $row['sum_amount']; $uniqueUsers = (int) $row['unique_users']; $summary['totals']['events'] += $count; $uniqueUsersSet[$eventType] = $uniqueUsers; // Mapper vers les catégories switch ($eventType) { case 'login_success': $summary['stats']['auth']['success'] = $count; break; case 'login_failed': $summary['stats']['auth']['failed'] = $count; break; case 'logout': $summary['stats']['auth']['logout'] = $count; break; case 'passage_created': $summary['stats']['passages']['created'] = $count; $summary['stats']['passages']['amount'] += $amount; break; case 'passage_updated': $summary['stats']['passages']['updated'] = $count; break; case 'passage_deleted': $summary['stats']['passages']['deleted'] = $count; break; case 'user_created': $summary['stats']['users']['created'] = $count; break; case 'user_updated': $summary['stats']['users']['updated'] = $count; break; case 'user_deleted': $summary['stats']['users']['deleted'] = $count; break; case 'sector_created': $summary['stats']['sectors']['created'] = $count; break; case 'sector_updated': $summary['stats']['sectors']['updated'] = $count; break; case 'sector_deleted': $summary['stats']['sectors']['deleted'] = $count; break; case 'entity_created': $summary['stats']['entities']['created'] = $count; break; case 'entity_updated': $summary['stats']['entities']['updated'] = $count; break; case 'entity_deleted': $summary['stats']['entities']['deleted'] = $count; break; case 'operation_created': $summary['stats']['operations']['created'] = $count; break; case 'operation_updated': $summary['stats']['operations']['updated'] = $count; break; case 'operation_deleted': $summary['stats']['operations']['deleted'] = $count; break; case 'stripe_payment_created': $summary['stats']['stripe']['created'] = $count; break; case 'stripe_payment_success': $summary['stats']['stripe']['success'] = $count; $summary['stats']['stripe']['amount'] += $amount; break; case 'stripe_payment_failed': $summary['stats']['stripe']['failed'] = $count; break; case 'stripe_payment_cancelled': $summary['stats']['stripe']['cancelled'] = $count; break; } } // Estimation des utilisateurs uniques (max des catégories car overlap possible) $summary['totals']['unique_users'] = !empty($uniqueUsersSet) ? max($uniqueUsersSet) : 0; return $summary; } /** * Formate les stats journalières */ private function formatDaily(array $rows): array { $daily = []; foreach ($rows as $row) { $date = $row['stat_date']; if (!isset($daily[$date])) { $daily[$date] = [ 'date' => $date, 'events' => [], 'totals' => [ 'count' => 0, 'sum_amount' => 0.0, ], ]; } $eventType = $row['event_type']; $count = (int) $row['count']; $amount = (float) $row['sum_amount']; $daily[$date]['events'][$eventType] = [ 'count' => $count, 'sum_amount' => $amount, 'unique_users' => (int) $row['unique_users'], ]; $daily[$date]['totals']['count'] += $count; $daily[$date]['totals']['sum_amount'] += $amount; } return array_values($daily); } /** * Nettoie un événement pour l'affichage (supprime données sensibles) */ private function sanitizeEventForOutput(array $event): array { // Supprimer l'IP complète, garder seulement les 2 premiers octets if (isset($event['ip'])) { $parts = explode('.', $event['ip']); if (count($parts) === 4) { $event['ip'] = $parts[0] . '.' . $parts[1] . '.x.x'; } } // Supprimer le user_agent complet unset($event['user_agent']); // Supprimer les données chiffrées si présentes unset($event['encrypted_name']); unset($event['encrypted_email']); return $event; } }