findBySimilar( $errorData['error_type'], $errorData['error_file'], $errorData['error_line'] ); if ($existing) { // Update existing error $this->incrementOccurrence($existing['id']); return $existing['id']; } // Create new error log return $this->create($errorData); } /** * Find similar error (same type, file, line) */ private function findBySimilar(string $type, string $file, int $line): ?array { $sql = "SELECT * FROM error_logs WHERE error_type = ? AND error_file = ? AND error_line = ? AND is_resolved = FALSE LIMIT 1"; $stmt = $this->db->prepare($sql); $stmt->execute([$type, $file, $line]); $result = $stmt->fetch(); return $result ?: null; } /** * Increment occurrence counter */ private function incrementOccurrence(int $id): void { $sql = "UPDATE error_logs SET occurrences = occurrences + 1, last_occurred_at = NOW() WHERE id = ?"; $stmt = $this->db->prepare($sql); $stmt->execute([$id]); } /** * Find error by error_id (unique reference) */ public function findByErrorId(string $errorId): ?array { $sql = "SELECT el.*, u.username, u.full_name, r.username as resolved_by_name FROM error_logs el LEFT JOIN users u ON el.user_id = u.id LEFT JOIN users r ON el.resolved_by = r.id WHERE el.error_id = ?"; $stmt = $this->db->prepare($sql); $stmt->execute([$errorId]); $result = $stmt->fetch(); return $result ?: null; } /** * Get recent errors with pagination */ public function getRecent(int $limit = 50, int $offset = 0, array $filters = []): array { $where = ['1=1']; $params = []; // Filter by resolution status if (isset($filters['resolved'])) { $where[] = 'el.is_resolved = ?'; $params[] = $filters['resolved'] ? 1 : 0; } // Filter by error type if (!empty($filters['type'])) { $where[] = 'el.error_type LIKE ?'; $params[] = '%' . $filters['type'] . '%'; } // Filter by user if (!empty($filters['user_id'])) { $where[] = 'el.user_id = ?'; $params[] = $filters['user_id']; } $whereClause = implode(' AND ', $where); $sql = "SELECT el.*, u.username, u.full_name FROM error_logs el LEFT JOIN users u ON el.user_id = u.id WHERE {$whereClause} ORDER BY el.last_occurred_at DESC LIMIT ? OFFSET ?"; $params[] = $limit; $params[] = $offset; $stmt = $this->db->prepare($sql); $stmt->execute($params); return $stmt->fetchAll(); } /** * Count total errors */ public function count(array $filters = []): int { $where = ['1=1']; $params = []; if (isset($filters['resolved'])) { $where[] = 'is_resolved = ?'; $params[] = $filters['resolved'] ? 1 : 0; } if (!empty($filters['type'])) { $where[] = 'error_type LIKE ?'; $params[] = '%' . $filters['type'] . '%'; } if (!empty($filters['user_id'])) { $where[] = 'user_id = ?'; $params[] = $filters['user_id']; } $whereClause = implode(' AND ', $where); $sql = "SELECT COUNT(*) as count FROM error_logs WHERE {$whereClause}"; $stmt = $this->db->prepare($sql); $stmt->execute($params); return (int)$stmt->fetch()['count']; } /** * Get error statistics */ public function getStats(): array { $sql = "SELECT COUNT(*) as total_errors, SUM(occurrences) as total_occurrences, COUNT(CASE WHEN is_resolved = FALSE THEN 1 END) as unresolved, COUNT(CASE WHEN is_resolved = TRUE THEN 1 END) as resolved, COUNT(CASE WHEN occurred_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) THEN 1 END) as last_24h, COUNT(CASE WHEN occurred_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN 1 END) as last_7d FROM error_logs"; $stmt = $this->db->query($sql); return $stmt->fetch(); } /** * Mark error as resolved */ public function resolve(int $id, int $resolvedBy, ?string $notes = null): bool { return $this->update($id, [ 'is_resolved' => true, 'resolved_at' => date('Y-m-d H:i:s'), 'resolved_by' => $resolvedBy, 'notes' => $notes ]); } /** * Delete old resolved errors */ public function deleteOldResolved(int $daysOld = 30): int { $sql = "DELETE FROM error_logs WHERE is_resolved = TRUE AND resolved_at < DATE_SUB(NOW(), INTERVAL ? DAY)"; $stmt = $this->db->prepare($sql); $stmt->execute([$daysOld]); return $stmt->rowCount(); } /** * Get most frequent errors */ public function getMostFrequent(int $limit = 10): array { $sql = "SELECT el.*, u.username, u.full_name FROM error_logs el LEFT JOIN users u ON el.user_id = u.id WHERE el.is_resolved = FALSE ORDER BY el.occurrences DESC, el.last_occurred_at DESC LIMIT ?"; $stmt = $this->db->prepare($sql); $stmt->execute([$limit]); return $stmt->fetchAll(); } /** * Get paginated errors with filters for admin panel */ public function getPaginatedErrors(array $filters, int $perPage, int $offset): array { $where = []; $params = []; if ($filters['resolved'] !== '') { $where[] = 'is_resolved = ?'; $params[] = (int)$filters['resolved']; } if (!empty($filters['type'])) { $where[] = 'error_type LIKE ?'; $params[] = '%' . $filters['type'] . '%'; } $whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; $sortColumn = $filters['sort']; $sortOrder = strtoupper($filters['order']) === 'DESC' ? 'DESC' : 'ASC'; $query = " SELECT error_id, error_type, error_message, error_file, error_line, is_resolved, MIN(occurred_at) as occurred_at, MAX(occurred_at) as last_occurred_at, COUNT(*) as occurrences FROM error_logs $whereClause GROUP BY error_id ORDER BY $sortColumn $sortOrder LIMIT ? OFFSET ? "; $stmt = $this->db->prepare($query); $stmt->execute([...$params, $perPage, $offset]); return $stmt->fetchAll(); } /** * Count total unique errors with filters */ public function countUniqueErrors(array $filters): int { $where = []; $params = []; if ($filters['resolved'] !== '') { $where[] = 'is_resolved = ?'; $params[] = (int)$filters['resolved']; } if (!empty($filters['type'])) { $where[] = 'error_type LIKE ?'; $params[] = '%' . $filters['type'] . '%'; } $whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; $query = "SELECT COUNT(DISTINCT error_id) as total FROM error_logs $whereClause"; $stmt = $this->db->prepare($query); $stmt->execute($params); return (int)$stmt->fetch()['total']; } /** * Get all occurrences of a specific error */ public function getOccurrencesByErrorId(string $errorId): array { $stmt = $this->db->prepare(" SELECT * FROM error_logs WHERE error_id = ? ORDER BY occurred_at DESC "); $stmt->execute([$errorId]); return $stmt->fetchAll(); } /** * Get admin statistics */ public function getAdminStats(): array { // Total unique errors $stmt = $this->db->query("SELECT COUNT(DISTINCT error_id) as total FROM error_logs"); $totalErrors = $stmt->fetch()['total']; // Unresolved errors $stmt = $this->db->query("SELECT COUNT(DISTINCT error_id) as total FROM error_logs WHERE is_resolved = 0"); $unresolved = $stmt->fetch()['total']; // Errors in last 24h $stmt = $this->db->query("SELECT COUNT(DISTINCT error_id) as total FROM error_logs WHERE occurred_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)"); $last24h = $stmt->fetch()['total']; // Total occurrences $stmt = $this->db->query("SELECT COUNT(*) as total FROM error_logs"); $totalOccurrences = $stmt->fetch()['total']; return [ 'total_errors' => $totalErrors, 'unresolved' => $unresolved, 'last_24h' => $last24h, 'total_occurrences' => $totalOccurrences ]; } /** * Mark all occurrences of an error as resolved */ public function markErrorResolved(string $errorId, int $userId, ?string $notes): bool { $stmt = $this->db->prepare(" UPDATE error_logs SET is_resolved = 1, resolved_at = NOW(), resolved_by = ?, notes = ? WHERE error_id = ? "); return $stmt->execute([$userId, $notes, $errorId]); } /** * Mark all occurrences of an error as unresolved */ public function markErrorUnresolved(string $errorId): bool { $stmt = $this->db->prepare(" UPDATE error_logs SET is_resolved = 0, resolved_at = NULL, resolved_by = NULL, notes = NULL WHERE error_id = ? "); return $stmt->execute([$errorId]); } /** * Delete all occurrences of an error */ public function deleteByErrorId(string $errorId): bool { $stmt = $this->db->prepare("DELETE FROM error_logs WHERE error_id = ?"); return $stmt->execute([$errorId]); } /** * Clear old resolved errors */ public function clearOldResolved(int $daysOld): int { $stmt = $this->db->prepare(" DELETE FROM error_logs WHERE is_resolved = 1 AND resolved_at < DATE_SUB(NOW(), INTERVAL ? DAY) "); $stmt->execute([$daysOld]); return $stmt->rowCount(); } }