import { ref } from 'vue' import type { AuditLogEntityTypes, AuditLogEntry, AuditLogFilters } from '~/shared/types' import type { HydraCollection } from '~/shared/utils/api' import { onAuthSessionCleared } from '~/shared/stores/auth' /** * Cache module-level : evite un double-fetch si la page et le composant * Timeline demandent la meme page simultanement. Volontairement minimaliste : * on ne cache que le dernier resultat, pas un LRU par filtre — un CRM interne * n'en a pas besoin et le cache complexe complique le reset. * * Un logout / 401 doit purger ce cache : on s'enregistre au callback * `onAuthSessionCleared` expose par auth.ts. */ const lastCollection = ref | null>(null) function resetAuditLog(): void { lastCollection.value = null } // Auto-enregistrement singleton : si la session est invalidee (401, // logout) le cache est purge automatiquement, evitant qu'un autre user // connecte ensuite ne voit des donnees residuelles. onAuthSessionCleared(resetAuditLog) /** * Traduit le modele front (camelCase) en query params API Platform * (snake_case, avec la syntaxe performed_at[after] / [before]). * * @returns objet plat directement consommable par `useApi().get(url, query)`. */ function buildQuery(filters: AuditLogFilters | undefined): Record { const query: Record = {} if (!filters) return query // `entity_type` : chaine simple ou liste pour un filtre multi-selection. // Cote PHP, la syntaxe `entity_type[]=X&entity_type[]=Y` est requise pour // que $_GET['entity_type'] soit un tableau (sinon "last wins"). if (Array.isArray(filters.entityType)) { if (filters.entityType.length > 0) query['entity_type[]'] = filters.entityType } else if (filters.entityType) { query.entity_type = filters.entityType } if (filters.entityId) query.entity_id = filters.entityId if (filters.action) query.action = filters.action if (filters.performedBy) query.performed_by = filters.performedBy if (filters.performedAtAfter) query['performed_at[after]'] = filters.performedAtAfter if (filters.performedAtBefore) query['performed_at[before]'] = filters.performedAtBefore if (filters.page) query.page = filters.page if (filters.itemsPerPage) query.itemsPerPage = filters.itemsPerPage return query } /** * Composable partage entre la page globale d'audit (admin) et le composant * Timeline. Expose des methodes de lecture + une fonction `resetAuditLog()` * pour purger le cache (conforme a la regle CLAUDE.md sur les composables * singletons, cf. `useSidebar.resetSidebar`). */ // Accept explicitement JSON-LD : API Platform 4 retourne un tableau PLAT (liste // d'items, sans envelope de pagination) sous `application/json`, et un objet // Hydra complet avec `member`, `totalItems` et `view` (first/last/next/previous) // sous `application/ld+json`. Pour obtenir `view` cote front — indispensable // a la pagination prev/next — on force donc ld+json. const JSONLD_HEADERS = { Accept: 'application/ld+json' } as const export function useAuditLog() { const api = useApi() async function fetchLogs(filters?: AuditLogFilters): Promise> { return api.get>( '/audit-logs', buildQuery(filters), { toast: false, headers: JSONLD_HEADERS }, ) } /** * Variante de `fetchLogs` qui met a jour le cache `lastCollection`. * N'est utilisee que par la page admin — le composant Timeline appelle * `fetchEntityLogs` qui bypass le cache pour ne pas polluer la reference * page-level quand plusieurs timelines sont ouvertes. */ async function fetchLogsCached(filters?: AuditLogFilters): Promise> { const data = await fetchLogs(filters) lastCollection.value = data return data } async function fetchLogById(id: string): Promise { return api.get(`/audit-logs/${id}`, {}, { toast: false, headers: JSONLD_HEADERS }) } /** * Liste des valeurs distinctes de `entity_type` pour alimenter le filtre * multi-selection. Alimente par un endpoint DBAL, aucune cache cote front * (la liste peut evoluer a chaque nouvelle ecriture d'audit). */ async function fetchEntityTypes(): Promise { const data = await api.get( '/audit-log-entity-types', {}, { toast: false, headers: JSONLD_HEADERS }, ) return data.entityTypes ?? [] } async function fetchEntityLogs( entityType: string, entityId: string | number, page: number = 1, itemsPerPage: number = 10, ): Promise> { // Volontairement via `fetchLogs` (sans cache) pour ne pas ecraser // `lastCollection` — la timeline peut etre rendue simultanement a // la page globale et doit rester independante. // // Le backend pagine a 30 par defaut (paginationItemsPerPage) ; on // passe explicitement itemsPerPage ici pour que la taille de page // soit alignee avec l'UX timeline (10 items + bouton "Voir plus"). // Sans ce param, le client slice a 10 et rate 20 entrees par page. return fetchLogs({ entityType, entityId: String(entityId), page, itemsPerPage, }) } // API publique : on expose volontairement deux noms distincts pour les // deux contrats (cache/no-cache). Aliaser `fetchLogs` vers la version // cachee trompait les appelants : un consommateur qui destructurait // `{ fetchLogs }` en pensant faire un appel neutre polluait en realite // `lastCollection`, effet indetectable sans lire l'impl. return { lastCollection, fetchLogsCached, fetchLogById, fetchEntityLogs, fetchEntityTypes, resetAuditLog, } }