import { useState } from 'react'; import { Plus, Edit2, Trash2, Building2, Target, ChevronDown, ChevronUp, X } from 'lucide-react'; import { useClients } from '@/hooks/useClients'; import { useClientTargets } from '@/hooks/useClientTargets'; import { Modal } from '@/components/Modal'; import { ConfirmModal } from '@/components/ConfirmModal'; import { Spinner } from '@/components/Spinner'; import { formatDurationHoursMinutes } from '@/utils/dateUtils'; import type { Client, CreateClientInput, UpdateClientInput, ClientTargetWithBalance, CreateCorrectionInput, } from '@/types'; const ALL_DAYS = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] as const; const DAY_LABELS: Record = { MON: 'Mon', TUE: 'Tue', WED: 'Wed', THU: 'Thu', FRI: 'Fri', SAT: 'Sat', SUN: 'Sun', }; function balanceLabel(seconds: number): { text: string; color: string } { if (seconds === 0) return { text: '±0', color: 'text-gray-500' }; const abs = Math.abs(seconds); const text = (seconds > 0 ? '+' : '−') + formatDurationHoursMinutes(abs); const color = seconds > 0 ? 'text-green-600' : 'text-red-600'; return { text, color }; } // Inline target panel shown inside each client card function ClientTargetPanel({ target, onCreated, onDeleted, }: { client: Client; target: ClientTargetWithBalance | undefined; onCreated: (input: { targetHours: number; periodType: 'weekly' | 'monthly'; workingDays: string[]; startDate: string; }) => Promise; onDeleted: () => Promise; }) { const { addCorrection, deleteCorrection, updateTarget } = useClientTargets(); const [expanded, setExpanded] = useState(false); const [showForm, setShowForm] = useState(false); const [editing, setEditing] = useState(false); // Create/edit form state const [formHours, setFormHours] = useState(''); const [formPeriodType, setFormPeriodType] = useState<'weekly' | 'monthly'>('weekly'); const [formWorkingDays, setFormWorkingDays] = useState(['MON', 'TUE', 'WED', 'THU', 'FRI']); const [formStartDate, setFormStartDate] = useState(''); const [formError, setFormError] = useState(null); const [formSaving, setFormSaving] = useState(false); // Correction form state const [showCorrectionForm, setShowCorrectionForm] = useState(false); const [corrDate, setCorrDate] = useState(''); const [corrHours, setCorrHours] = useState(''); const [corrDesc, setCorrDesc] = useState(''); const [corrError, setCorrError] = useState(null); const [corrSaving, setCorrSaving] = useState(false); const todayIso = new Date().toISOString().split('T')[0]; const openCreate = () => { setFormHours(''); setFormPeriodType('weekly'); setFormWorkingDays(['MON', 'TUE', 'WED', 'THU', 'FRI']); setFormStartDate(todayIso); setFormError(null); setEditing(false); setShowForm(true); }; const openEdit = () => { if (!target) return; setFormHours(String(target.targetHours)); setFormPeriodType(target.periodType); setFormWorkingDays([...target.workingDays]); setFormStartDate(target.startDate); setFormError(null); setEditing(true); setShowForm(true); }; const toggleDay = (day: string) => { setFormWorkingDays(prev => prev.includes(day) ? prev.filter(d => d !== day) : [...prev, day], ); }; const handleFormSubmit = async (e: React.FormEvent) => { e.preventDefault(); setFormError(null); const hours = parseFloat(formHours); if (isNaN(hours) || hours <= 0 || hours > 168) { setFormError(`${formPeriodType === 'weekly' ? 'Weekly' : 'Monthly'} hours must be between 0 and 168`); return; } if (formWorkingDays.length === 0) { setFormError('Select at least one working day'); return; } if (!formStartDate) { setFormError('Please select a start date'); return; } setFormSaving(true); try { if (editing && target) { await updateTarget.mutateAsync({ id: target.id, input: { targetHours: hours, periodType: formPeriodType, workingDays: formWorkingDays, startDate: formStartDate, }, }); } else { await onCreated({ targetHours: hours, periodType: formPeriodType, workingDays: formWorkingDays, startDate: formStartDate, }); } setShowForm(false); } catch (err) { setFormError(err instanceof Error ? err.message : 'Failed to save'); } finally { setFormSaving(false); } }; const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const handleDelete = async () => { try { await onDeleted(); } catch (err) { alert(err instanceof Error ? err.message : 'Failed to delete'); } }; const handleAddCorrection = async (e: React.FormEvent) => { e.preventDefault(); setCorrError(null); if (!target) return; const hours = parseFloat(corrHours); if (isNaN(hours) || hours < -1000 || hours > 1000) { setCorrError('Hours must be between -1000 and 1000'); return; } if (!corrDate) { setCorrError('Please select a date'); return; } setCorrSaving(true); try { const input: CreateCorrectionInput = { date: corrDate, hours, description: corrDesc || undefined }; await addCorrection.mutateAsync({ targetId: target.id, input }); setCorrDate(''); setCorrHours(''); setCorrDesc(''); setShowCorrectionForm(false); } catch (err) { setCorrError(err instanceof Error ? err.message : 'Failed to add correction'); } finally { setCorrSaving(false); } }; const handleDeleteCorrection = async (correctionId: string) => { if (!target) return; try { await deleteCorrection.mutateAsync({ targetId: target.id, correctionId }); } catch (err) { alert(err instanceof Error ? err.message : 'Failed to delete correction'); } }; if (!target && !showForm) { return (
); } if (showForm) { const hoursLabel = formPeriodType === 'weekly' ? 'Hours/week' : 'Hours/month'; return (

{editing ? 'Edit target' : 'Set target'}

{formError &&

{formError}

} {/* Period type */}
{(['weekly', 'monthly'] as const).map(pt => ( ))}
{/* Hours + Start Date */}
setFormHours(e.target.value)} className="input text-sm py-1" placeholder="e.g. 40" min="0.5" max="168" step="0.5" required />
setFormStartDate(e.target.value)} className="input text-sm py-1" required />
{/* Working days */}
{ALL_DAYS.map(day => { const active = formWorkingDays.includes(day); return ( ); })}
); } // Target exists — show summary + expandable details const balance = balanceLabel(target!.totalBalanceSeconds); const periodLabel = target!.periodType === 'weekly' ? 'week' : 'month'; return (
{/* Target summary row */}
{target!.targetHours}h/{periodLabel} {balance.text} {target!.hasOngoingTimer && ( )}
{/* Expanded: corrections list + add form */} {expanded && (

Corrections

{target!.corrections.length === 0 && !showCorrectionForm && (

No corrections yet

)} {target!.corrections.map(c => (
{c.date} {c.description && — {c.description}}
= 0 ? 'text-green-600' : 'text-red-600'}`}> {c.hours >= 0 ? '+' : ''}{c.hours}h
))} {showCorrectionForm ? (
{corrError &&

{corrError}

}
setCorrDate(e.target.value)} className="input text-xs py-1" required />
setCorrHours(e.target.value)} className="input text-xs py-1" placeholder="+8 / -4" step="0.5" required />
setCorrDesc(e.target.value)} className="input text-xs py-1" placeholder="e.g. Public holiday" maxLength={255} />
) : ( )}
)} {showDeleteConfirm && ( setShowDeleteConfirm(false)} /> )}
); } export function ClientsPage() { const { clients, isLoading, createClient, updateClient, deleteClient } = useClients(); const { targets, createTarget, deleteTarget } = useClientTargets(); const [isModalOpen, setIsModalOpen] = useState(false); const [editingClient, setEditingClient] = useState(null); const [formData, setFormData] = useState({ name: '', description: '' }); const [error, setError] = useState(null); const handleOpenModal = (client?: Client) => { if (client) { setEditingClient(client); setFormData({ name: client.name, description: client.description || '' }); } else { setEditingClient(null); setFormData({ name: '', description: '' }); } setError(null); setIsModalOpen(true); }; const handleCloseModal = () => { setIsModalOpen(false); setEditingClient(null); setFormData({ name: '', description: '' }); setError(null); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); if (!formData.name.trim()) { setError('Client name is required'); return; } try { if (editingClient) { await updateClient.mutateAsync({ id: editingClient.id, input: formData as UpdateClientInput, }); } else { await createClient.mutateAsync(formData); } handleCloseModal(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save client'); } }; const [confirmClient, setConfirmClient] = useState(null); const handleDeleteConfirmed = async () => { if (!confirmClient) return; try { await deleteClient.mutateAsync(confirmClient.id); } catch (err) { alert(err instanceof Error ? err.message : 'Failed to delete client'); } }; if (isLoading) { return ; } return (
{/* Page Header */}

Clients

Manage your clients and customers

{/* Clients List */} {clients?.length === 0 ? (

No clients yet

Get started by adding your first client

) : (
{clients?.map((client) => { const target = targets?.find(t => t.clientId === client.id); return (

{client.name}

{client.description && (

{client.description}

)}
{ await createTarget.mutateAsync({ clientId: client.id, targetHours, periodType, workingDays, startDate, }); }} onDeleted={async () => { if (target) await deleteTarget.mutateAsync(target.id); }} />
); })}
)} {/* Modal */} {isModalOpen && (
{error && (
{error}
)}
setFormData({ ...formData, name: e.target.value })} className="input" placeholder="Enter client name" autoFocus />