"use client"; /* eslint-disable react-hooks/set-state-in-effect */ import { useReducer, useEffect, useCallback, useRef } from "react"; function triggerDownload(url: string) { const a = document.createElement("a"); a.href = url; a.download = ""; document.body.appendChild(a); a.click(); document.body.removeChild(a); } const triggerLabel: Record = { daily_approaching: { en: "Daily OT Approaching", color: "bg-amber-100 text-amber-700" }, daily_reached: { en: "Daily OT Reached", color: "bg-red-100 text-red-700" }, weekly_approaching: { en: "Weekly OT Approaching", color: "bg-amber-100 text-amber-700" }, weekly_reached: { en: "Weekly OT Reached", color: "bg-red-100 text-red-700" }, end_of_period_summary:{ en: "Period Summary", color: "bg-green-100 text-green-700" }, }; import { getTimeTrackingWorkers, getTimeTrackingTasks, createTimeWorker, resetTimeWorkerPin, updateTimeWorker, deleteTimeWorker, createTimeTask, updateTimeTask, deleteTimeTask, getTimeTrackingSettings, updateTimeTrackingSettings, getTimeTrackingNotificationLog, type TimeWorker, type TimeTask, type TimeTrackingSettings, type NotificationLogEntry, } from "@/actions/time-tracking"; import { AdminButton } from "@/components/admin/design-system"; // One-color outline icons const Icons = { clock: (className: string) => ( ), dollarSign: (className: string) => ( ), clipboard: (className: string) => ( ), chart: (className: string) => ( ), check: (className: string) => ( ), alert: (className: string) => ( ), }; type Tab = "dashboard" | "settings" | "export"; const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; interface TimeTrackingSettingsClientProps { brandId: string; } type State = { tab: Tab; workers: TimeWorker[]; tasks: TimeTask[]; loading: boolean; notificationLog: NotificationLogEntry[]; payPeriodStartDay: number; payPeriodLength: number; dailyOvertimeThreshold: number; weeklyOvertimeThreshold: number; overtimeNotifications: boolean; settingsSaving: boolean; settingsSaved: boolean; settingsError: string | null; notificationEmails: string[]; notificationSmsNumbers: string[]; enableDailyAlerts: boolean; enableWeeklyAlerts: boolean; newEmail: string; newSms: string; showWorkerModal: boolean; editingWorker: TimeWorker | null; workerName: string; workerRole: string; workerLang: string; workerActive: boolean; resetPinResult: string | null; workerError: string | null; submitting: boolean; showTaskModal: boolean; editingTask: TimeTask | null; taskName: string; taskNameEs: string; taskUnit: string; taskActive: boolean; taskSortOrder: number; taskError: string | null; exportStart: string; exportEnd: string; exportBrand: "all" | "tuxedo" | "ird"; includeOvertime: boolean; includeNotes: boolean; }; type Action = | { type: "SET_TAB"; tab: Tab } | { type: "SET_WORKERS"; workers: TimeWorker[] } | { type: "SET_TASKS"; tasks: TimeTask[] } | { type: "SET_LOADING"; value: boolean } | { type: "SET_NOTIFICATION_LOG"; log: NotificationLogEntry[] } | { type: "SYNC_SETTINGS"; settings: TimeTrackingSettings } | { type: "SET_PAY_PERIOD_START_DAY"; value: number } | { type: "SET_PAY_PERIOD_LENGTH"; value: number } | { type: "SET_DAILY_OT_THRESHOLD"; value: number } | { type: "SET_WEEKLY_OT_THRESHOLD"; value: number } | { type: "SET_OT_NOTIFICATIONS"; value: boolean } | { type: "SET_SETTINGS_SAVING"; value: boolean } | { type: "SET_SETTINGS_SAVED"; value: boolean } | { type: "SET_SETTINGS_ERROR"; value: string | null } | { type: "SET_NOTIFICATION_EMAILS"; value: string[] } | { type: "SET_NOTIFICATION_SMS_NUMBERS"; value: string[] } | { type: "SET_ENABLE_DAILY_ALERTS"; value: boolean } | { type: "SET_ENABLE_WEEKLY_ALERTS"; value: boolean } | { type: "SET_NEW_EMAIL"; value: string } | { type: "SET_NEW_SMS"; value: string } | { type: "SET_SHOW_WORKER_MODAL"; value: boolean } | { type: "SET_EDITING_WORKER"; value: TimeWorker | null } | { type: "SET_WORKER_NAME"; value: string } | { type: "SET_WORKER_ROLE"; value: string } | { type: "SET_WORKER_LANG"; value: string } | { type: "SET_WORKER_ACTIVE"; value: boolean } | { type: "SET_RESET_PIN_RESULT"; value: string | null } | { type: "SET_WORKER_ERROR"; value: string | null } | { type: "SET_SUBMITTING"; value: boolean } | { type: "RESET_WORKER_MODAL" } | { type: "SET_SHOW_TASK_MODAL"; value: boolean } | { type: "SET_EDITING_TASK"; value: TimeTask | null } | { type: "SET_TASK_NAME"; value: string } | { type: "SET_TASK_NAME_ES"; value: string } | { type: "SET_TASK_UNIT"; value: string } | { type: "SET_TASK_ACTIVE"; value: boolean } | { type: "SET_TASK_SORT_ORDER"; value: number } | { type: "SET_TASK_ERROR"; value: string | null } | { type: "RESET_TASK_MODAL" } | { type: "SET_EXPORT_START"; value: string } | { type: "SET_EXPORT_END"; value: string } | { type: "SET_EXPORT_BRAND"; value: "all" | "tuxedo" | "ird" } | { type: "SET_INCLUDE_OVERTIME"; value: boolean } | { type: "SET_INCLUDE_NOTES"; value: boolean }; const initialState: State = { tab: "dashboard", workers: [], tasks: [], loading: true, notificationLog: [], payPeriodStartDay: 0, payPeriodLength: 7, dailyOvertimeThreshold: 12, weeklyOvertimeThreshold: 56, overtimeNotifications: true, settingsSaving: false, settingsSaved: false, settingsError: null, notificationEmails: [], notificationSmsNumbers: [], enableDailyAlerts: true, enableWeeklyAlerts: true, newEmail: "", newSms: "", showWorkerModal: false, editingWorker: null, workerName: "", workerRole: "worker", workerLang: "en", workerActive: true, resetPinResult: null, workerError: null, submitting: false, showTaskModal: false, editingTask: null, taskName: "", taskNameEs: "", taskUnit: "hours", taskActive: true, taskSortOrder: 0, taskError: null, exportStart: "", exportEnd: "", exportBrand: "all", includeOvertime: true, includeNotes: true, }; function reducer(state: State, action: Action): State { switch (action.type) { case "SET_TAB": return { ...state, tab: action.tab }; case "SET_WORKERS": return { ...state, workers: action.workers }; case "SET_TASKS": return { ...state, tasks: action.tasks }; case "SET_LOADING": return { ...state, loading: action.value }; case "SET_NOTIFICATION_LOG": return { ...state, notificationLog: action.log }; case "SYNC_SETTINGS": return { ...state, payPeriodStartDay: action.settings.pay_period_start_day, payPeriodLength: action.settings.pay_period_length_days, dailyOvertimeThreshold: action.settings.daily_overtime_threshold, weeklyOvertimeThreshold: action.settings.weekly_overtime_threshold, overtimeNotifications: action.settings.overtime_notifications, notificationEmails: action.settings.notification_emails ?? [], notificationSmsNumbers: action.settings.notification_sms_numbers ?? [], enableDailyAlerts: action.settings.enable_daily_alerts ?? true, enableWeeklyAlerts: action.settings.enable_weekly_alerts ?? true, }; case "SET_PAY_PERIOD_START_DAY": return { ...state, payPeriodStartDay: action.value }; case "SET_PAY_PERIOD_LENGTH": return { ...state, payPeriodLength: action.value }; case "SET_DAILY_OT_THRESHOLD": return { ...state, dailyOvertimeThreshold: action.value }; case "SET_WEEKLY_OT_THRESHOLD": return { ...state, weeklyOvertimeThreshold: action.value }; case "SET_OT_NOTIFICATIONS": return { ...state, overtimeNotifications: action.value }; case "SET_SETTINGS_SAVING": return { ...state, settingsSaving: action.value }; case "SET_SETTINGS_SAVED": return { ...state, settingsSaved: action.value }; case "SET_SETTINGS_ERROR": return { ...state, settingsError: action.value }; case "SET_NOTIFICATION_EMAILS": return { ...state, notificationEmails: action.value }; case "SET_NOTIFICATION_SMS_NUMBERS": return { ...state, notificationSmsNumbers: action.value }; case "SET_ENABLE_DAILY_ALERTS": return { ...state, enableDailyAlerts: action.value }; case "SET_ENABLE_WEEKLY_ALERTS": return { ...state, enableWeeklyAlerts: action.value }; case "SET_NEW_EMAIL": return { ...state, newEmail: action.value }; case "SET_NEW_SMS": return { ...state, newSms: action.value }; case "SET_SHOW_WORKER_MODAL": return { ...state, showWorkerModal: action.value }; case "SET_EDITING_WORKER": return { ...state, editingWorker: action.value }; case "SET_WORKER_NAME": return { ...state, workerName: action.value }; case "SET_WORKER_ROLE": return { ...state, workerRole: action.value }; case "SET_WORKER_LANG": return { ...state, workerLang: action.value }; case "SET_WORKER_ACTIVE": return { ...state, workerActive: action.value }; case "SET_RESET_PIN_RESULT": return { ...state, resetPinResult: action.value }; case "SET_WORKER_ERROR": return { ...state, workerError: action.value }; case "SET_SUBMITTING": return { ...state, submitting: action.value }; case "RESET_WORKER_MODAL": return { ...state, editingWorker: null, workerName: "", workerRole: "worker", workerLang: "en", workerActive: true, workerError: null, resetPinResult: null, }; case "SET_SHOW_TASK_MODAL": return { ...state, showTaskModal: action.value }; case "SET_EDITING_TASK": return { ...state, editingTask: action.value }; case "SET_TASK_NAME": return { ...state, taskName: action.value }; case "SET_TASK_NAME_ES": return { ...state, taskNameEs: action.value }; case "SET_TASK_UNIT": return { ...state, taskUnit: action.value }; case "SET_TASK_ACTIVE": return { ...state, taskActive: action.value }; case "SET_TASK_SORT_ORDER": return { ...state, taskSortOrder: action.value }; case "SET_TASK_ERROR": return { ...state, taskError: action.value }; case "RESET_TASK_MODAL": return { ...state, editingTask: null, taskName: "", taskNameEs: "", taskUnit: "hours", taskActive: true, taskSortOrder: 0, taskError: null, }; case "SET_EXPORT_START": return { ...state, exportStart: action.value }; case "SET_EXPORT_END": return { ...state, exportEnd: action.value }; case "SET_EXPORT_BRAND": return { ...state, exportBrand: action.value }; case "SET_INCLUDE_OVERTIME": return { ...state, includeOvertime: action.value }; case "SET_INCLUDE_NOTES": return { ...state, includeNotes: action.value }; } } /* ── Subcomponents ─────────────────────────────────────────────── */ function StatRow({ workersCount, tasksCount, alertsSent, }: { workersCount: number; tasksCount: number; alertsSent: number; }) { return (

{workersCount}

Workers

{tasksCount}

Tasks

{alertsSent}

Alerts Sent

); } function ExportSection({ exportStart, exportEnd, buildExportUrl, onChangeStart, onChangeEnd, }: { exportStart: string; exportEnd: string; buildExportUrl: (type: "quickbooks" | "payroll" | "detailed" | "summary") => string; onChangeStart: (v: string) => void; onChangeEnd: (v: string) => void; }) { return (

Export Data

From: onChangeStart(e.target.value)} className="px-3 py-1.5 rounded-lg border border-[var(--admin-border)] bg-white text-[var(--admin-text-primary)] focus:outline-none focus:border-[var(--admin-accent)]" /> To: onChangeEnd(e.target.value)} className="px-3 py-1.5 rounded-lg border border-[var(--admin-border)] bg-white text-[var(--admin-text-primary)] focus:outline-none focus:border-[var(--admin-accent)]" />
); } function NotificationLogSection({ log }: { log: NotificationLogEntry[] }) { return (

Recent Alerts

{log.length === 0 ? ( ) : log.slice(0, 20).map(entry => { const meta = triggerLabel[entry.trigger_type] ?? { en: entry.trigger_type, color: "bg-stone-100 text-stone-500" }; return ( ); })}
Time Worker Alert Email SMS
No alerts sent yet
{new Date(entry.created_at).toLocaleString()} {entry.worker_name ?? "—"} {meta.en} {Icons.check("h-4 w-4")} {entry.sms_sent ? Icons.check("h-4 w-4 text-[var(--admin-accent)]") : "—"}
); } function DashboardTab({ workers, tasks, notificationLog, exportStart, exportEnd, buildExportUrl, onChangeExportStart, onChangeExportEnd, }: { workers: TimeWorker[]; tasks: TimeTask[]; notificationLog: NotificationLogEntry[]; exportStart: string; exportEnd: string; buildExportUrl: (type: "quickbooks" | "payroll" | "detailed" | "summary") => string; onChangeExportStart: (v: string) => void; onChangeExportEnd: (v: string) => void; }) { const alertsSent = notificationLog.filter(n => n.email_sent || n.sms_sent).length; return (
); } function WorkersSection({ workers, onAdd, onEdit, onResetPin, onDelete, }: { workers: TimeWorker[]; onAdd: () => void; onEdit: (w: TimeWorker) => void; onResetPin: (id: string) => void; onDelete: (id: string) => void; }) { return (

Workers & PINs

+ Add Worker
{workers.length === 0 ? ( ) : workers.map(w => ( ))}
Name Role Lang Status Last Used Actions
No workers yet
{w.name} {w.role} {w.lang} {w.active ? "Active" : "Inactive"} {w.last_used_at ? new Date(w.last_used_at).toLocaleDateString() : "—"}
onEdit(w)}>Edit onResetPin(w.id)}>Reset PIN onDelete(w.id)}>Delete
); } function TasksSection({ tasks, onAdd, onEdit, onDelete, }: { tasks: TimeTask[]; onAdd: () => void; onEdit: (t: TimeTask) => void; onDelete: (id: string) => void; }) { return (

Tasks

+ Add Task
{tasks.length === 0 ? ( ) : tasks.map(t => ( ))}
Name Name (ES) Unit Sort Status Actions
No tasks yet
{t.name} {t.name_es ?? "—"} {t.unit} {t.sort_order} {t.active ? "Active" : "Inactive"}
onEdit(t)}>Edit onDelete(t.id)}>Delete
); } function PayPeriodSection({ payPeriodStartDay, payPeriodLength, dailyOvertimeThreshold, weeklyOvertimeThreshold, onChangeStartDay, onChangeLength, onChangeDaily, onChangeWeekly, }: { payPeriodStartDay: number; payPeriodLength: number; dailyOvertimeThreshold: number; weeklyOvertimeThreshold: number; onChangeStartDay: (v: number) => void; onChangeLength: (v: number) => void; onChangeDaily: (v: number) => void; onChangeWeekly: (v: number) => void; }) { return (

Pay Period & Overtime

onChangeLength(Number(e.target.value))} className="flex-1 px-4 py-3 rounded-xl border border-[var(--admin-border)] bg-white text-[var(--admin-text-primary)] focus:outline-none focus:border-[var(--admin-accent)] transition-colors" /> days
onChangeDaily(Number(e.target.value))} className="flex-1 px-4 py-3 rounded-xl border border-[var(--admin-border)] bg-white text-[var(--admin-text-primary)] focus:outline-none focus:border-[var(--admin-accent)] transition-colors" /> hrs
onChangeWeekly(Number(e.target.value))} className="flex-1 px-4 py-3 rounded-xl border border-[var(--admin-border)] bg-white text-[var(--admin-text-primary)] focus:outline-none focus:border-[var(--admin-accent)] transition-colors" /> hrs
); } function AlertSettingsSection({ enableDailyAlerts, enableWeeklyAlerts, overtimeNotifications, onToggleDaily, onToggleWeekly, onToggleOvertime, }: { enableDailyAlerts: boolean; enableWeeklyAlerts: boolean; overtimeNotifications: boolean; onToggleDaily: () => void; onToggleWeekly: () => void; onToggleOvertime: () => void; }) { return (

Alert Settings

Daily overtime alerts

Send notification when worker hits daily threshold

Weekly overtime alerts

Send notification when worker hits weekly threshold

Show overtime warning on employee screen

Display alert when worker approaches thresholds

); } function NotificationRecipientsSection({ emails, smsNumbers, newEmail, newSms, onChangeNewEmail, onChangeNewSms, onAddEmail, onAddSms, onRemoveEmail, onRemoveSms, }: { emails: string[]; smsNumbers: string[]; newEmail: string; newSms: string; onChangeNewEmail: (v: string) => void; onChangeNewSms: (v: string) => void; onAddEmail: () => void; onAddSms: () => void; onRemoveEmail: (e: string) => void; onRemoveSms: (n: string) => void; }) { return (

Notification Recipients

onChangeNewEmail(e.target.value)} onKeyDown={e => e.key === "Enter" && (onAddEmail(), e.preventDefault())} className="flex-1 px-4 py-3 rounded-xl border border-[var(--admin-border)] bg-white text-[var(--admin-text-primary)] placeholder:text-[var(--admin-text-muted)] focus:outline-none focus:border-[var(--admin-accent)] transition-colors" placeholder="manager@farm.com" /> Add
{emails.map(e => ( {e} ))}
onChangeNewSms(e.target.value)} onKeyDown={e => e.key === "Enter" && (onAddSms(), e.preventDefault())} className="flex-1 px-4 py-3 rounded-xl border border-[var(--admin-border)] bg-white text-[var(--admin-text-primary)] placeholder:text-[var(--admin-text-muted)] focus:outline-none focus:border-[var(--admin-accent)] transition-colors" placeholder="+1234567890" /> Add
{smsNumbers.map(n => ( {n} ))}
); } function SettingsTab({ workers, tasks, payPeriodStartDay, payPeriodLength, dailyOvertimeThreshold, weeklyOvertimeThreshold, overtimeNotifications, enableDailyAlerts, enableWeeklyAlerts, notificationEmails, notificationSmsNumbers, newEmail, newSms, settingsError, settingsSaved, settingsSaving, onAddWorker, onEditWorker, onResetPin, onDeleteWorker, onAddTask, onEditTask, onDeleteTask, onChangePayPeriodStartDay, onChangePayPeriodLength, onChangeDailyOvertime, onChangeWeeklyOvertime, onToggleDaily, onToggleWeekly, onToggleOvertime, onChangeNewEmail, onChangeNewSms, onAddEmail, onAddSms, onRemoveEmail, onRemoveSms, onSave, }: { workers: TimeWorker[]; tasks: TimeTask[]; payPeriodStartDay: number; payPeriodLength: number; dailyOvertimeThreshold: number; weeklyOvertimeThreshold: number; overtimeNotifications: boolean; enableDailyAlerts: boolean; enableWeeklyAlerts: boolean; notificationEmails: string[]; notificationSmsNumbers: string[]; newEmail: string; newSms: string; settingsError: string | null; settingsSaved: boolean; settingsSaving: boolean; onAddWorker: () => void; onEditWorker: (w: TimeWorker) => void; onResetPin: (id: string) => void; onDeleteWorker: (id: string) => void; onAddTask: () => void; onEditTask: (t: TimeTask) => void; onDeleteTask: (id: string) => void; onChangePayPeriodStartDay: (v: number) => void; onChangePayPeriodLength: (v: number) => void; onChangeDailyOvertime: (v: number) => void; onChangeWeeklyOvertime: (v: number) => void; onToggleDaily: () => void; onToggleWeekly: () => void; onToggleOvertime: () => void; onChangeNewEmail: (v: string) => void; onChangeNewSms: (v: string) => void; onAddEmail: () => void; onAddSms: () => void; onRemoveEmail: (e: string) => void; onRemoveSms: (n: string) => void; onSave: () => void; }) { return (
{Icons.alert("h-5 w-5")}

Colorado Overtime Law

Colorado requires daily overtime (1.5×) after 12 hours in a workday, or weekly overtime after 40 hours in a workweek.

{settingsError && (
{settingsError}
)} {settingsSaved && (
Settings saved successfully.
)} Save Settings
); } function WorkerModal({ editingWorker, workerName, workerRole, workerLang, workerActive, resetPinResult, workerError, submitting, onClose, onChangeName, onChangeRole, onChangeLang, onToggleActive, onResetPin, onSave, }: { editingWorker: TimeWorker | null; workerName: string; workerRole: string; workerLang: string; workerActive: boolean; resetPinResult: string | null; workerError: string | null; submitting: boolean; onClose: () => void; onChangeName: (v: string) => void; onChangeRole: (v: string) => void; onChangeLang: (v: string) => void; onToggleActive: () => void; onResetPin: () => void; onSave: () => void; }) { return (

{editingWorker ? "Edit Worker" : "Add Worker"}

{workerError &&
{workerError}
} {resetPinResult && (

New PIN:

{resetPinResult}

Show this to the worker — it will not be shown again.

)}
onChangeName(e.target.value)} className="w-full px-4 py-3 rounded-xl border border-[var(--admin-border)] bg-white text-[var(--admin-text-primary)] placeholder:text-[var(--admin-text-muted)] focus:outline-none focus:border-[var(--admin-accent)] transition-colors" placeholder="Worker name" />
{editingWorker && (
Reset PIN for this worker
)}
Cancel {submitting ? "..." : editingWorker ? "Save" : "Add Worker"}
); } function TaskModal({ editingTask, taskName, taskNameEs, taskUnit, taskActive, taskSortOrder, taskError, submitting, onClose, onChangeName, onChangeNameEs, onChangeUnit, onToggleActive, onChangeSortOrder, onSave, }: { editingTask: TimeTask | null; taskName: string; taskNameEs: string; taskUnit: string; taskActive: boolean; taskSortOrder: number; taskError: string | null; submitting: boolean; onClose: () => void; onChangeName: (v: string) => void; onChangeNameEs: (v: string) => void; onChangeUnit: (v: string) => void; onToggleActive: () => void; onChangeSortOrder: (v: number) => void; onSave: () => void; }) { return (

{editingTask ? "Edit Task" : "Add Task"}

{taskError &&
{taskError}
}
onChangeName(e.target.value)} className="w-full px-4 py-3 rounded-xl border border-[var(--admin-border)] bg-white text-[var(--admin-text-primary)] placeholder:text-[var(--admin-text-muted)] focus:outline-none focus:border-[var(--admin-accent)] transition-colors" placeholder="e.g. Harvesting" />
onChangeNameEs(e.target.value)} className="w-full px-4 py-3 rounded-xl border border-[var(--admin-border)] bg-white text-[var(--admin-text-primary)] placeholder:text-[var(--admin-text-muted)] focus:outline-none focus:border-[var(--admin-accent)] transition-colors" placeholder="e.g. Cosecha" />
onChangeSortOrder(parseInt(e.target.value) || 0)} className="w-full px-4 py-3 rounded-xl border border-[var(--admin-border)] bg-white text-[var(--admin-text-primary)] focus:outline-none focus:border-[var(--admin-accent)] transition-colors" />
Cancel {submitting ? "..." : editingTask ? "Save" : "Add Task"}
); } /* ── Main ──────────────────────────────────────────────────────── */ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSettingsClientProps) { const [state, dispatch] = useReducer(reducer, initialState); const settingsRef = useRef(null); const overtimeMultiplierRef = useRef(1.5); const dailyAlertThresholdRef = useRef(80); const weeklyAlertThresholdRef = useRef(80); const sendEndOfPeriodSummaryRef = useRef(true); const brandNameRef = useRef("Farm"); // Initialize tab from URL on mount useEffect(() => { if (typeof window === "undefined") return; const t = new URLSearchParams(window.location.search).get("tab"); if (t === "settings" || t === "export") { dispatch({ type: "SET_TAB", tab: t }); } }, []); const load = useCallback(async () => { dispatch({ type: "SET_LOADING", value: true }); const [w, t, s] = await Promise.all([ getTimeTrackingWorkers(brandId), getTimeTrackingTasks(brandId, false), getTimeTrackingSettings(brandId), ]); // Sync form state from fresh settings in the same batch as the data // setStates (avoids the stale-frame window a useEffect would introduce // for the form-sync step). settingsRef.current = s; if (s) { dispatch({ type: "SYNC_SETTINGS", settings: s }); overtimeMultiplierRef.current = s.overtime_multiplier; dailyAlertThresholdRef.current = Math.round((s.daily_alert_threshold ?? 0.80) * 100); weeklyAlertThresholdRef.current = Math.round((s.weekly_alert_threshold ?? 0.80) * 100); sendEndOfPeriodSummaryRef.current = s.send_end_of_period_summary ?? true; brandNameRef.current = s.brand_name ?? "Farm"; } dispatch({ type: "SET_WORKERS", workers: w }); dispatch({ type: "SET_TASKS", tasks: t }); const log = await getTimeTrackingNotificationLog(brandId, 50); dispatch({ type: "SET_NOTIFICATION_LOG", log }); dispatch({ type: "SET_LOADING", value: false }); }, [brandId]); useEffect(() => { void load(); }, [load]); const handleSaveNotifications = async () => { dispatch({ type: "SET_SETTINGS_SAVING", value: true }); dispatch({ type: "SET_SETTINGS_ERROR", value: null }); dispatch({ type: "SET_SETTINGS_SAVED", value: false }); const result = await updateTimeTrackingSettings(brandId, { pay_period_start_day: state.payPeriodStartDay, pay_period_length_days: state.payPeriodLength, daily_overtime_threshold: state.dailyOvertimeThreshold, weekly_overtime_threshold: state.weeklyOvertimeThreshold, overtime_multiplier: overtimeMultiplierRef.current, overtime_notifications: state.overtimeNotifications, notification_emails: state.notificationEmails, notification_sms_numbers: state.notificationSmsNumbers, enable_daily_alerts: state.enableDailyAlerts, enable_weekly_alerts: state.enableWeeklyAlerts, daily_alert_threshold: dailyAlertThresholdRef.current / 100, weekly_alert_threshold: weeklyAlertThresholdRef.current / 100, send_end_of_period_summary: sendEndOfPeriodSummaryRef.current, brand_name: brandNameRef.current, }); dispatch({ type: "SET_SETTINGS_SAVING", value: false }); if (!result.success) { dispatch({ type: "SET_SETTINGS_ERROR", value: result.error ?? "Failed to save" }); return; } dispatch({ type: "SET_SETTINGS_SAVED", value: true }); setTimeout(() => dispatch({ type: "SET_SETTINGS_SAVED", value: false }), 3000); load(); }; const addEmail = () => { const email = state.newEmail.trim(); if (email && !state.notificationEmails.includes(email)) { dispatch({ type: "SET_NOTIFICATION_EMAILS", value: [...state.notificationEmails, email] }); } dispatch({ type: "SET_NEW_EMAIL", value: "" }); }; const removeEmail = (email: string) => dispatch({ type: "SET_NOTIFICATION_EMAILS", value: state.notificationEmails.filter(e => e !== email) }); const addSms = () => { const num = state.newSms.replace(/[^0-9+]/g, ""); if (num && !state.notificationSmsNumbers.includes(num)) { dispatch({ type: "SET_NOTIFICATION_SMS_NUMBERS", value: [...state.notificationSmsNumbers, num] }); } dispatch({ type: "SET_NEW_SMS", value: "" }); }; const removeSms = (num: string) => dispatch({ type: "SET_NOTIFICATION_SMS_NUMBERS", value: state.notificationSmsNumbers.filter(n => n !== num) }); function buildExportUrl(type: "quickbooks" | "payroll" | "detailed" | "summary") { const params = new URLSearchParams({ type, start: state.exportStart, end: state.exportEnd, brand: state.exportBrand, overtime: String(state.includeOvertime), notes: String(state.includeNotes), }); return `/api/time-tracking/export?${params}`; } const openAddWorker = () => { dispatch({ type: "RESET_WORKER_MODAL" }); dispatch({ type: "SET_SHOW_WORKER_MODAL", value: true }); }; const openEditWorker = (w: TimeWorker) => { dispatch({ type: "SET_EDITING_WORKER", value: w }); dispatch({ type: "SET_WORKER_NAME", value: w.name }); dispatch({ type: "SET_WORKER_ROLE", value: w.role }); dispatch({ type: "SET_WORKER_LANG", value: w.lang }); dispatch({ type: "SET_WORKER_ACTIVE", value: w.active }); dispatch({ type: "SET_WORKER_ERROR", value: null }); dispatch({ type: "SET_RESET_PIN_RESULT", value: null }); dispatch({ type: "SET_SHOW_WORKER_MODAL", value: true }); }; const handleSaveWorker = async () => { if (!state.workerName.trim()) { dispatch({ type: "SET_WORKER_ERROR", value: "Name is required" }); return; } dispatch({ type: "SET_SUBMITTING", value: true }); dispatch({ type: "SET_WORKER_ERROR", value: null }); if (state.editingWorker) { const result = await updateTimeWorker(state.editingWorker.id, state.workerName.trim(), state.workerRole, state.workerLang, state.workerActive); if (!result.success) { dispatch({ type: "SET_WORKER_ERROR", value: result.error ?? "Failed" }); dispatch({ type: "SET_SUBMITTING", value: false }); return; } } else { const result = await createTimeWorker(brandId, state.workerName.trim(), state.workerRole, state.workerLang); if (!result.success) { dispatch({ type: "SET_WORKER_ERROR", value: result.error ?? "Failed" }); dispatch({ type: "SET_SUBMITTING", value: false }); return; } } dispatch({ type: "SET_SUBMITTING", value: false }); dispatch({ type: "SET_SHOW_WORKER_MODAL", value: false }); load(); }; const handleResetPin = async (workerId: string) => { const result = await resetTimeWorkerPin(workerId); if (result.success && result.pin) dispatch({ type: "SET_RESET_PIN_RESULT", value: result.pin }); else dispatch({ type: "SET_WORKER_ERROR", value: result.error ?? "Failed to reset PIN" }); }; const handleDeleteWorker = async (workerId: string) => { if (!confirm("Delete this worker? This cannot be undone.")) return; const result = await deleteTimeWorker(workerId); if (!result.success) { dispatch({ type: "SET_WORKER_ERROR", value: result.error ?? "Failed" }); return; } load(); }; const openAddTask = () => { dispatch({ type: "RESET_TASK_MODAL" }); dispatch({ type: "SET_SHOW_TASK_MODAL", value: true }); }; const openEditTask = (t: TimeTask) => { dispatch({ type: "SET_EDITING_TASK", value: t }); dispatch({ type: "SET_TASK_NAME", value: t.name }); dispatch({ type: "SET_TASK_NAME_ES", value: t.name_es ?? "" }); dispatch({ type: "SET_TASK_UNIT", value: t.unit }); dispatch({ type: "SET_TASK_ACTIVE", value: t.active }); dispatch({ type: "SET_TASK_SORT_ORDER", value: t.sort_order }); dispatch({ type: "SET_TASK_ERROR", value: null }); dispatch({ type: "SET_SHOW_TASK_MODAL", value: true }); }; const handleSaveTask = async () => { if (!state.taskName.trim()) { dispatch({ type: "SET_TASK_ERROR", value: "Name is required" }); return; } dispatch({ type: "SET_SUBMITTING", value: true }); dispatch({ type: "SET_TASK_ERROR", value: null }); if (state.editingTask) { const result = await updateTimeTask(state.editingTask.id, state.taskName.trim(), state.taskNameEs, state.taskUnit, state.taskActive, state.taskSortOrder); if (!result.success) { dispatch({ type: "SET_TASK_ERROR", value: result.error ?? "Failed" }); dispatch({ type: "SET_SUBMITTING", value: false }); return; } } else { const result = await createTimeTask(brandId, state.taskName.trim(), state.taskNameEs || null, state.taskUnit, state.taskSortOrder); if (!result.success) { dispatch({ type: "SET_TASK_ERROR", value: result.error ?? "Failed" }); dispatch({ type: "SET_SUBMITTING", value: false }); return; } } dispatch({ type: "SET_SUBMITTING", value: false }); dispatch({ type: "SET_SHOW_TASK_MODAL", value: false }); load(); }; const handleDeleteTask = async (taskId: string) => { if (!confirm("Delete this task?")) return; const result = await deleteTimeTask(taskId); if (!result.success) { dispatch({ type: "SET_TASK_ERROR", value: result.error ?? "Failed" }); return; } load(); }; if (state.loading) return
Loading...
; return (
{state.tab === "dashboard" && ( dispatch({ type: "SET_EXPORT_START", value: v })} onChangeExportEnd={(v) => dispatch({ type: "SET_EXPORT_END", value: v })} /> )} {state.tab === "settings" && ( dispatch({ type: "SET_PAY_PERIOD_START_DAY", value: v })} onChangePayPeriodLength={(v) => dispatch({ type: "SET_PAY_PERIOD_LENGTH", value: v })} onChangeDailyOvertime={(v) => dispatch({ type: "SET_DAILY_OT_THRESHOLD", value: v })} onChangeWeeklyOvertime={(v) => dispatch({ type: "SET_WEEKLY_OT_THRESHOLD", value: v })} onToggleDaily={() => dispatch({ type: "SET_ENABLE_DAILY_ALERTS", value: !state.enableDailyAlerts })} onToggleWeekly={() => dispatch({ type: "SET_ENABLE_WEEKLY_ALERTS", value: !state.enableWeeklyAlerts })} onToggleOvertime={() => dispatch({ type: "SET_OT_NOTIFICATIONS", value: !state.overtimeNotifications })} onChangeNewEmail={(v) => dispatch({ type: "SET_NEW_EMAIL", value: v })} onChangeNewSms={(v) => dispatch({ type: "SET_NEW_SMS", value: v })} onAddEmail={addEmail} onAddSms={addSms} onRemoveEmail={removeEmail} onRemoveSms={removeSms} onSave={handleSaveNotifications} /> )} {state.showWorkerModal && ( dispatch({ type: "SET_SHOW_WORKER_MODAL", value: false })} onChangeName={(v) => dispatch({ type: "SET_WORKER_NAME", value: v })} onChangeRole={(v) => dispatch({ type: "SET_WORKER_ROLE", value: v })} onChangeLang={(v) => dispatch({ type: "SET_WORKER_LANG", value: v })} onToggleActive={() => dispatch({ type: "SET_WORKER_ACTIVE", value: !state.workerActive })} onResetPin={() => state.editingWorker && handleResetPin(state.editingWorker.id)} onSave={handleSaveWorker} /> )} {state.showTaskModal && ( dispatch({ type: "SET_SHOW_TASK_MODAL", value: false })} onChangeName={(v) => dispatch({ type: "SET_TASK_NAME", value: v })} onChangeNameEs={(v) => dispatch({ type: "SET_TASK_NAME_ES", value: v })} onChangeUnit={(v) => dispatch({ type: "SET_TASK_UNIT", value: v })} onToggleActive={() => dispatch({ type: "SET_TASK_ACTIVE", value: !state.taskActive })} onChangeSortOrder={(v) => dispatch({ type: "SET_TASK_SORT_ORDER", value: v })} onSave={handleSaveTask} /> )}
); }