## cleanup-debug-logging - Create environment-aware logger utility (logger.ts) - Replace 60+ console.log/error statements across 28 files - Production: only warn/error logs visible - Development: all log levels with prefixes Updated files: - Contexts: NotificationContext, ProjectSyncContext, AuthContext - Components: GanttChart, CalendarView, ErrorBoundary, and 11 others - Pages: Tasks, Projects, Dashboard, and 7 others - Services: api.ts ## complete-i18n-coverage - WeeklyReportPreview: all strings translated, dynamic locale - ReportHistory: all strings translated, dynamic locale - AuditPage: detail modal and verification modal translated - WorkloadPage: error message translated Locale files updated: - en/common.json, zh-TW/common.json: reports section - en/audit.json, zh-TW/audit.json: modal sections - en/workload.json, zh-TW/workload.json: errors section Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
166 lines
4.1 KiB
TypeScript
166 lines
4.1 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react'
|
|
import { auditService, AuditLog } from '../services/audit'
|
|
import { logger } from '../utils/logger'
|
|
|
|
interface ResourceHistoryProps {
|
|
resourceType: string
|
|
resourceId: string
|
|
title?: string
|
|
}
|
|
|
|
export function ResourceHistory({ resourceType, resourceId, title = 'Change History' }: ResourceHistoryProps) {
|
|
const [logs, setLogs] = useState<AuditLog[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [expanded, setExpanded] = useState(false)
|
|
|
|
const loadHistory = useCallback(async () => {
|
|
setLoading(true)
|
|
try {
|
|
const response = await auditService.getResourceHistory(resourceType, resourceId, 10)
|
|
setLogs(response.logs)
|
|
} catch (error) {
|
|
logger.error('Failed to load resource history:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [resourceType, resourceId])
|
|
|
|
useEffect(() => {
|
|
loadHistory()
|
|
}, [loadHistory])
|
|
|
|
const formatChanges = (changes: AuditLog['changes']): string => {
|
|
if (!changes || changes.length === 0) return ''
|
|
return changes.map(c => `${c.field}: ${c.old_value ?? 'null'} → ${c.new_value ?? 'null'}`).join(', ')
|
|
}
|
|
|
|
if (loading) {
|
|
return <div style={styles.loading}>Loading history...</div>
|
|
}
|
|
|
|
if (logs.length === 0) {
|
|
return <div style={styles.empty}>No change history available</div>
|
|
}
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault()
|
|
setExpanded(!expanded)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div style={styles.container}>
|
|
<div
|
|
style={styles.header}
|
|
onClick={() => setExpanded(!expanded)}
|
|
role="button"
|
|
tabIndex={0}
|
|
onKeyDown={handleKeyDown}
|
|
aria-expanded={expanded}
|
|
aria-label={`${title}, ${logs.length} items`}
|
|
>
|
|
<span style={styles.title}>{title}</span>
|
|
<span style={styles.toggleIcon} aria-hidden="true">{expanded ? '▼' : '▶'}</span>
|
|
</div>
|
|
{expanded && (
|
|
<div style={styles.content}>
|
|
{logs.map((log) => (
|
|
<div key={log.id} style={styles.logItem}>
|
|
<div style={styles.logHeader}>
|
|
<span style={styles.eventType}>{log.event_type}</span>
|
|
<span style={styles.time}>
|
|
{new Date(log.created_at).toLocaleString()}
|
|
</span>
|
|
</div>
|
|
<div style={styles.logBody}>
|
|
<span style={styles.userName}>{log.user_name || 'System'}</span>
|
|
{log.changes && log.changes.length > 0 && (
|
|
<span style={styles.changes}>{formatChanges(log.changes)}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const styles: Record<string, React.CSSProperties> = {
|
|
container: {
|
|
backgroundColor: '#f8f9fa',
|
|
borderRadius: '8px',
|
|
border: '1px solid #e9ecef',
|
|
marginTop: '16px',
|
|
},
|
|
header: {
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
padding: '12px 16px',
|
|
cursor: 'pointer',
|
|
userSelect: 'none',
|
|
},
|
|
title: {
|
|
fontWeight: 600,
|
|
fontSize: '14px',
|
|
color: '#495057',
|
|
},
|
|
toggleIcon: {
|
|
fontSize: '12px',
|
|
color: '#6c757d',
|
|
},
|
|
content: {
|
|
borderTop: '1px solid #e9ecef',
|
|
padding: '8px',
|
|
maxHeight: '300px',
|
|
overflowY: 'auto',
|
|
},
|
|
logItem: {
|
|
padding: '8px 12px',
|
|
borderBottom: '1px solid #e9ecef',
|
|
},
|
|
logHeader: {
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
marginBottom: '4px',
|
|
},
|
|
eventType: {
|
|
fontSize: '12px',
|
|
fontWeight: 600,
|
|
color: '#007bff',
|
|
},
|
|
time: {
|
|
fontSize: '11px',
|
|
color: '#6c757d',
|
|
},
|
|
logBody: {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '2px',
|
|
},
|
|
userName: {
|
|
fontSize: '12px',
|
|
color: '#495057',
|
|
},
|
|
changes: {
|
|
fontSize: '11px',
|
|
color: '#6c757d',
|
|
fontStyle: 'italic',
|
|
},
|
|
loading: {
|
|
padding: '16px',
|
|
textAlign: 'center',
|
|
color: '#6c757d',
|
|
},
|
|
empty: {
|
|
padding: '16px',
|
|
textAlign: 'center',
|
|
color: '#6c757d',
|
|
fontSize: '14px',
|
|
},
|
|
}
|
|
|
|
export default ResourceHistory
|