import React, { useState } from 'react'; import { FaHistory, FaSync, FaChevronDown, FaChevronUp, FaUser, FaClock, FaCheckCircle, FaSpinner, } from 'react-icons/fa'; import { useAuditLogs } from '../hooks/useAuditLogs'; import type { AuditLogEntry, VersionChange } from '../hooks/useAuditLogs'; // ============== PROPS ============== interface ActivityLogProps { /** Frappe DocType name (e.g. 'Asset', 'Inspection', 'Work_Order') */ doctype: string; /** Document name / ID */ docname: string | null; /** Document creation date (for "Created" entry at bottom) */ creationDate?: string; /** Document owner/creator email */ createdBy?: string; /** Title shown in header */ title?: string; /** Max entries to fetch */ limit?: number; /** Number of entries visible before "Show All" */ initialVisible?: number; /** Allow collapse/expand */ collapsible?: boolean; /** Start collapsed */ startCollapsed?: boolean; /** Compact mode for sidebar placement */ compact?: boolean; /** Additional CSS class */ className?: string; /** Callback after refresh */ onRefresh?: () => void; } // ============== HELPER FUNCTIONS ============== const formatFieldName = (fieldName: string): string => { if (!fieldName) return ''; return fieldName .replace(/^custom_/, '') .replace(/_/g, ' ') .replace(/\b\w/g, (char) => char.toUpperCase()); }; const formatValue = (value: any): string => { if (value === null || value === undefined) return '(empty)'; if (value === '') return '(empty)'; if (value === 0) return '0'; if (value === 1) return '1'; if (typeof value === 'boolean') return value ? 'Yes' : 'No'; if (typeof value === 'object') return JSON.stringify(value); return String(value); }; const formatAuditDate = (dateStr: string): string => { if (!dateStr) return ''; const date = new Date(dateStr); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return 'Just now'; if (diffMins < 60) return `${diffMins} min${diffMins > 1 ? 's' : ''} ago`; if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined, hour: '2-digit', minute: '2-digit', }); }; const formatUsername = (email: string): string => { if (!email) return 'Unknown'; const atIndex = email.indexOf('@'); if (atIndex === -1) return email; return email.substring(0, atIndex); }; const getChangeColor = (fieldName: string): string => { const lower = fieldName.toLowerCase(); if (lower.includes('status') || lower.includes('state') || lower.includes('workflow')) { return 'text-purple-600 dark:text-purple-400'; } if (lower.includes('date')) { return 'text-blue-600 dark:text-blue-400'; } if ( lower.includes('technician') || lower.includes('supervisor') || lower.includes('assigned') || lower.includes('location') || lower.includes('department') || lower.includes('building') || lower.includes('room') ) { return 'text-green-600 dark:text-green-400'; } return 'text-gray-600 dark:text-gray-400'; }; // ============== SUB-COMPONENTS ============== /** Single timeline entry */ const TimelineEntry: React.FC<{ log: AuditLogEntry; isLatest: boolean; compact: boolean; }> = ({ log, isLatest, compact }) => { const dotSize = compact ? 'w-2.5 h-2.5' : 'w-3 h-3'; const avatarSize = compact ? 'w-5 h-5' : 'w-6 h-6'; const iconSize = compact ? 8 : 10; const textSize = compact ? 'text-[10px]' : 'text-xs'; const valueSize = compact ? 'text-[9px]' : 'text-[10px]'; return (
{/* Timeline dot */}
{/* Entry content */}
{/* Header */}
{formatUsername(log.owner)}
{formatAuditDate(log.creation)}
{/* Changes */}
{log.changes.length > 0 ? ( log.changes.map((change, i) => (
{formatFieldName(change.field)} changed from {formatValue(change.oldValue)} {formatValue(change.newValue)}
)) ) : (

Document updated

)} {log.added && log.added.length > 0 && (
Added: {log.added.length} item(s)
)} {log.removed && log.removed.length > 0 && (
Removed: {log.removed.length} item(s)
)} {log.rowChanged && log.rowChanged.length > 0 && (
Modified: {log.rowChanged.length} row(s)
)}
); }; /** "Created this document" entry */ const CreatedEntry: React.FC<{ creationDate: string; createdBy: string; doctype: string; compact: boolean; }> = ({ creationDate, createdBy, doctype, compact }) => { const dotSize = compact ? 'w-2.5 h-2.5' : 'w-3 h-3'; const avatarSize = compact ? 'w-5 h-5' : 'w-6 h-6'; const iconSize = compact ? 8 : 10; const textSize = compact ? 'text-[10px]' : 'text-xs'; // Clean doctype for display (e.g. "Work_Order" → "Work Order") const displayDoctype = doctype.replace(/_/g, ' '); return (
{formatUsername(createdBy)}
{formatAuditDate(creationDate)}
Created this {displayDoctype}
); }; // ============== MAIN COMPONENT ============== const ActivityLog: React.FC = ({ doctype, docname, creationDate, createdBy, title = 'Activity Log', limit = 50, initialVisible = 5, collapsible = true, startCollapsed = false, compact = false, className = '', onRefresh, }) => { const [isExpanded, setIsExpanded] = useState(!startCollapsed); const [showAll, setShowAll] = useState(false); const { auditLogs, loading, refetch } = useAuditLogs({ doctype, docname, limit, enabled: !!docname, }); const handleRefresh = () => { refetch(); onRefresh?.(); }; if (!docname) return null; const headerIconSize = compact ? 14 : 16; const headerTextClass = compact ? 'text-sm' : 'text-base'; const timelineLineLeft = compact ? 'left-2' : 'left-3'; const showMoreTextSize = compact ? 'text-[10px]' : 'text-xs'; const showMoreIconSize = compact ? 8 : 10; const visibleLogs = showAll ? auditLogs : auditLogs.slice(0, initialVisible); return (
{/* Header */}
collapsible && setIsExpanded(!isExpanded)} >

{title}

{auditLogs.length > 0 && ( {auditLogs.length} )}
{collapsible && ( )}
{/* Content */} {isExpanded && (
{/* Loading */} {loading && (
Loading...
)} {/* Empty State */} {!loading && auditLogs.length === 0 && (

No changes recorded yet

{creationDate && createdBy && ( )}
)} {/* Timeline */} {!loading && auditLogs.length > 0 && (
{visibleLogs.map((log, index) => ( ))}
{/* Show More/Less */} {auditLogs.length > initialVisible && (
)} {/* Created entry at bottom */} {creationDate && createdBy && (
)}
)}
)}
); }; export default ActivityLog;