import { useState, useEffect, useRef, useCallback } from 'react' import { UserWorkloadDetail, LoadLevel, workloadApi } from '../services/workload' import { SkeletonList } from './Skeleton' interface WorkloadUserDetailProps { userId: string userName: string weekStart: string isOpen: boolean onClose: () => void } // Color mapping for load levels const loadLevelColors: Record = { normal: '#4caf50', warning: '#ff9800', overloaded: '#f44336', unavailable: '#9e9e9e', } const loadLevelLabels: Record = { normal: 'Normal', warning: 'Warning', overloaded: 'Overloaded', unavailable: 'Unavailable', } export function WorkloadUserDetail({ userId, userName, weekStart, isOpen, onClose, }: WorkloadUserDetailProps) { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [detail, setDetail] = useState(null) const modalOverlayRef = useRef(null) // A11Y: Handle Escape key to close modal useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && isOpen) { onClose() } } if (isOpen) { document.addEventListener('keydown', handleKeyDown) modalOverlayRef.current?.focus() } return () => { document.removeEventListener('keydown', handleKeyDown) } }, [isOpen, onClose]) const loadUserDetail = useCallback(async () => { setLoading(true) setError(null) try { const data = await workloadApi.getUserWorkload(userId, weekStart) setDetail(data) } catch (err) { console.error('Failed to load user workload:', err) setError('Failed to load workload details') } finally { setLoading(false) } }, [userId, weekStart]) useEffect(() => { if (isOpen && userId) { loadUserDetail() } }, [isOpen, userId, weekStart, loadUserDetail]) const formatDate = (dateStr: string | null) => { if (!dateStr) return '-' const date = new Date(dateStr) return date.toLocaleDateString('zh-TW', { month: 'short', day: 'numeric' }) } if (!isOpen) return null return (
e.stopPropagation()}>

{userName}

Workload Details
{loading ? (
) : error ? (
{error}
) : detail ? ( <> {/* Summary Section */}
Allocated Hours {detail.summary.allocated_hours}h
Capacity {detail.summary.capacity_hours}h
Load {detail.summary.load_percentage}%
Status {loadLevelLabels[detail.summary.load_level]}
{/* Tasks Section */}

Tasks This Week

{detail.tasks.length === 0 ? (
No tasks assigned for this week.
) : (
{detail.tasks.map((task) => (
{task.task_title} {task.project_name}
{task.time_estimate}h {task.due_date && ( Due: {formatDate(task.due_date)} )} {task.status_name && ( {task.status_name} )}
))}
)}
{/* Total hours breakdown */}
Total Estimated Hours: {detail.tasks.reduce((sum, task) => sum + task.time_estimate, 0)}h
) : null}
) } const styles: { [key: string]: React.CSSProperties } = { overlay: { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0, 0, 0, 0.5)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 1000, }, modal: { backgroundColor: 'white', borderRadius: '8px', width: '600px', maxWidth: '90%', maxHeight: '80vh', overflow: 'hidden', display: 'flex', flexDirection: 'column', }, header: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', padding: '20px 24px', borderBottom: '1px solid #eee', }, title: { fontSize: '20px', fontWeight: 600, margin: 0, color: '#333', }, subtitle: { fontSize: '14px', color: '#666', }, closeButton: { background: 'none', border: 'none', fontSize: '28px', cursor: 'pointer', color: '#767676', // WCAG AA compliant padding: '0', lineHeight: 1, }, loading: { padding: '48px', textAlign: 'center', color: '#666', }, error: { padding: '48px', textAlign: 'center', color: '#f44336', }, summarySection: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '12px', padding: '20px 24px', backgroundColor: '#f9f9f9', }, summaryCard: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px', }, summaryLabel: { fontSize: '12px', color: '#666', textTransform: 'uppercase', letterSpacing: '0.5px', }, summaryValue: { fontSize: '20px', fontWeight: 600, color: '#333', }, statusBadge: { display: 'inline-block', padding: '4px 10px', borderRadius: '4px', fontSize: '12px', fontWeight: 500, color: 'white', }, tasksSection: { flex: 1, overflowY: 'auto', padding: '20px 24px', }, sectionTitle: { fontSize: '14px', fontWeight: 600, color: '#666', margin: '0 0 12px 0', textTransform: 'uppercase', letterSpacing: '0.5px', }, emptyTasks: { textAlign: 'center', padding: '24px', color: '#767676', // WCAG AA compliant fontSize: '14px', }, taskList: { display: 'flex', flexDirection: 'column', gap: '8px', }, taskItem: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', backgroundColor: '#f9f9f9', borderRadius: '6px', borderLeft: '3px solid #0066cc', }, taskMain: { display: 'flex', flexDirection: 'column', gap: '2px', }, taskTitle: { fontSize: '14px', fontWeight: 500, color: '#333', }, projectName: { fontSize: '12px', color: '#666', }, taskMeta: { display: 'flex', alignItems: 'center', gap: '12px', }, timeEstimate: { fontSize: '14px', fontWeight: 600, color: '#0066cc', }, dueDate: { fontSize: '12px', color: '#666', }, status: { fontSize: '11px', padding: '2px 8px', backgroundColor: '#e0e0e0', borderRadius: '4px', color: '#666', }, totalSection: { display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: '8px', padding: '16px 24px', borderTop: '1px solid #eee', backgroundColor: '#f9f9f9', }, totalLabel: { fontSize: '14px', color: '#666', }, totalValue: { fontSize: '18px', fontWeight: 600, color: '#333', }, } export default WorkloadUserDetail