import React, { useEffect, useMemo, useState } from 'react'; import axios from 'axios'; import { AlertTriangle, ArrowRight, History, Loader2, ShieldCheck, Sparkles, User } from 'lucide-react'; import { supabase } from '../../lib/supabaseClient'; import { API_CONFIG } from '../../config'; import { formatFullTimestamp } from '../../utils/dateUtils'; const ACTION_META = { TICKET_CREATED: { label: 'Ticket Created', tone: 'emerald', icon: Sparkles, description: 'The ticket entered the secure audit trail.' }, STATUS_CHANGED: { label: 'Status Changed', tone: 'blue', icon: ArrowRight, description: 'The ticket status was updated.' }, STATUS_ESCALATED: { label: 'Status Escalated', tone: 'orange', icon: AlertTriangle, description: 'The ticket moved into an escalation state.' }, PRIORITY_CHANGED: { label: 'Priority Changed', tone: 'amber', icon: ArrowRight, description: 'The ticket priority was revised.' }, PRIORITY_ESCALATED: { label: 'Priority Escalated', tone: 'red', icon: AlertTriangle, description: 'The ticket priority increased.' }, TICKET_ASSIGNED: { label: 'Ticket Assigned', tone: 'emerald', icon: User, description: 'Ownership was reassigned.' }, TEAM_ROUTED: { label: 'Team Routed', tone: 'emerald', icon: ShieldCheck, description: 'The ticket was routed to a new support team.' }, METADATA_UPDATED: { label: 'Metadata Updated', tone: 'slate', icon: History, description: 'Supporting details changed on the ticket.' }, AUTO_ESCALATED: { label: 'Auto Escalated', tone: 'red', icon: AlertTriangle, description: 'A scheduled escalation rule fired automatically.' } }; const TONE_CLASSES = { emerald: 'border-emerald-200 bg-emerald-50/80 text-emerald-700', blue: 'border-blue-200 bg-blue-50/80 text-blue-700', amber: 'border-amber-200 bg-amber-50/80 text-amber-700', orange: 'border-orange-200 bg-orange-50/80 text-orange-700', red: 'border-red-200 bg-red-50/80 text-red-700', slate: 'border-slate-200 bg-slate-50/80 text-slate-700' }; const formatFieldValue = (value) => { if (value === null || value === undefined || value === '') return 'None'; if (typeof value === 'object') { if ('value' in value) return formatFieldValue(value.value); if ('reason' in value) return String(value.reason); return JSON.stringify(value); } return String(value); }; const extractValue = (value) => { if (value && typeof value === 'object' && 'value' in value) return value.value; return value; }; const getActorLabel = (record) => { const profile = record.performed_by_profile; if (profile?.full_name) return profile.full_name; if (profile?.email) return profile.email; if (record.performed_by) return 'System / API'; return 'System'; }; const buildChangeDescription = (record) => { const action = record.action || 'TICKET_UPDATED'; if (action === 'TICKET_CREATED') return 'Ticket was created and entered the audit ledger.'; if (action === 'AUTO_ESCALATED') { const reason = record.new_value?.reason || record.old_value?.reason || 'stale state'; return `Automated escalation logged because the ticket remained ${reason}.`; } const field = record.old_value?.field || record.new_value?.field || 'value'; const previous = formatFieldValue(extractValue(record.old_value)); const next = formatFieldValue(extractValue(record.new_value)); if (action === 'STATUS_CHANGED' || action === 'STATUS_ESCALATED') { return `Status changed from ${previous} to ${next}.`; } if (action === 'PRIORITY_CHANGED' || action === 'PRIORITY_ESCALATED') { return `Priority moved from ${previous} to ${next}.`; } if (action === 'TICKET_ASSIGNED') { return `Assignment updated from ${previous} to ${next}.`; } if (action === 'TEAM_ROUTED') { return `Support routing changed from ${previous} to ${next}.`; } if (action === 'METADATA_UPDATED') { return `Context metadata was updated for ${field}.`; } return `Audited field ${field} changed from ${previous} to ${next}.`; }; const buildBadgeTitle = (record) => { const action = record.action || 'TICKET_UPDATED'; const meta = ACTION_META[action]; return meta?.description || 'Tracked ticket mutation'; }; const mergeLogs = (existing, incoming) => { const map = new Map(); [...existing, ...incoming].forEach((item) => { if (item?.id) map.set(item.id, item); }); return Array.from(map.values()).sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); }; const TicketAuditTimeline = ({ ticketId, companyId }) => { const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { if (!ticketId || !companyId) { setLogs([]); setLoading(false); return undefined; } let cancelled = false; const loadLogs = async () => { setLoading(true); setError(null); try { const { data } = await axios.get(`${API_CONFIG.BACKEND_URL}/tickets/${ticketId}/audit_logs`, { params: { company_id: companyId } }); if (!cancelled) { setLogs(Array.isArray(data) ? data : []); } } catch (err) { if (!cancelled) { setError(err?.response?.data?.detail || err.message || 'Failed to load audit history.'); } } finally { if (!cancelled) setLoading(false); } }; loadLogs(); const channel = supabase .channel(`ticket_audit_${ticketId}`) .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'audit_logs', filter: `ticket_id=eq.${ticketId}` }, (payload) => { setLogs((current) => mergeLogs(current, [payload.new])); } ) .subscribe(); return () => { cancelled = true; supabase.removeChannel(channel); }; }, [ticketId, companyId]); const hasLogs = useMemo(() => logs.length > 0, [logs.length]); return (

Audit Log Timeline

Secure change history

Chronological
{loading ? (
Loading secure audit history...
) : error ? (
{error}
) : !hasLogs ? (
No audit events have been recorded yet.
) : (
{logs.map((record) => { const meta = ACTION_META[record.action] || ACTION_META.METADATA_UPDATED; const Icon = meta.icon; const toneClass = TONE_CLASSES[meta.tone] || TONE_CLASSES.slate; const actorLabel = getActorLabel(record); const description = buildChangeDescription(record); const fieldName = record.old_value?.field || record.new_value?.field || record.action || 'event'; const previousValue = formatFieldValue(extractValue(record.old_value)); const nextValue = formatFieldValue(extractValue(record.new_value)); return (
{meta.label} {fieldName}

{description}

Performed By

{actorLabel}

{formatFullTimestamp(record.created_at)}

Previous Value

{previousValue}

Current Value

{nextValue}

); })}
)}
); }; export default TicketAuditTimeline;