"use client"; /* eslint-disable react-hooks/set-state-in-effect */ import { useState, useEffect, useCallback } from "react"; 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 { AdminToggle } from "./design-system/AdminToggle"; import { AdminInput, AdminTextInput, AdminSelect, AdminButton } from "./design-system"; const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; type AccordionProps = { title: string; id?: string; description?: string; defaultOpen?: boolean; accentColor?: "emerald" | "violet" | "amber" | "stone"; children: React.ReactNode; }; function Accordion({ title, id, description, defaultOpen = false, accentColor = "stone", children }: AccordionProps) { const [open, setOpen] = useState(defaultOpen); const accent = { emerald: "bg-emerald-50 text-emerald-700 border-emerald-200", violet: "bg-violet-50 text-violet-700 border-violet-200", amber: "bg-amber-50 text-amber-700 border-amber-200", stone: "bg-stone-50 text-stone-700 border-stone-200", }[accentColor]; const borderColor = open ? "border-stone-300" : "border-stone-200"; return (
{children}
); } type Props = { brandId: string; workersOnly?: boolean; tasksOnly?: boolean; }; export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Props) { const [workers, setWorkers] = useState([]); const [tasks, setTasks] = useState([]); const [settings, setSettings] = useState(null); const [notificationLog, setNotificationLog] = useState([]); const [loading, setLoading] = useState(true); // Settings form state const [payPeriodStartDay, setPayPeriodStartDay] = useState(0); const [payPeriodLength, setPayPeriodLength] = useState(7); const [dailyOvertimeThreshold, setDailyOvertimeThreshold] = useState(12); const [weeklyOvertimeThreshold, setWeeklyOvertimeThreshold] = useState(56); const [overtimeMultiplier, setOvertimeMultiplier] = useState(1.5); const [overtimeNotifications, setOvertimeNotifications] = useState(true); const [settingsSaving, setSettingsSaving] = useState(false); const [settingsSaved, setSettingsSaved] = useState(false); const [settingsError, setSettingsError] = useState(null); // Notification form state const [notificationEmails, setNotificationEmails] = useState([]); const [notificationSmsNumbers, setNotificationSmsNumbers] = useState([]); const [enableDailyAlerts, setEnableDailyAlerts] = useState(true); const [enableWeeklyAlerts, setEnableWeeklyAlerts] = useState(true); const [dailyAlertThreshold, setDailyAlertThreshold] = useState(80); const [weeklyAlertThreshold, setWeeklyAlertThreshold] = useState(80); const [sendEndOfPeriodSummary, setSendEndOfPeriodSummary] = useState(true); const [brandName, setBrandName] = useState("Farm"); const [newEmail, setNewEmail] = useState(""); const [newSms, setNewSms] = useState(""); // Worker modal state const [showWorkerModal, setShowWorkerModal] = useState(false); const [editingWorker, setEditingWorker] = useState(null); const [workerName, setWorkerName] = useState(""); const [workerRole, setWorkerRole] = useState("worker"); const [workerLang, setWorkerLang] = useState("en"); const [workerActive, setWorkerActive] = useState(true); const [resetPinResult, setResetPinResult] = useState(null); const [workerError, setWorkerError] = useState(null); const [submitting, setSubmitting] = useState(false); // Task modal state const [showTaskModal, setShowTaskModal] = useState(false); const [editingTask, setEditingTask] = useState(null); const [taskName, setTaskName] = useState(""); const [taskNameEs, setTaskNameEs] = useState(""); const [taskUnit, setTaskUnit] = useState("hours"); const [taskActive, setTaskActive] = useState(true); const [taskSortOrder, setTaskSortOrder] = useState(0); const [taskError, setTaskError] = useState(null); const load = useCallback(async () => { setLoading(true); const [w, t, s] = await Promise.all([ getTimeTrackingWorkers(brandId), getTimeTrackingTasks(brandId, false), getTimeTrackingSettings(brandId), ]); setWorkers(w); setTasks(t); if (s) { setSettings(s); setPayPeriodStartDay(s.pay_period_start_day); setPayPeriodLength(s.pay_period_length_days); setDailyOvertimeThreshold(s.daily_overtime_threshold); setWeeklyOvertimeThreshold(s.weekly_overtime_threshold); setOvertimeMultiplier(s.overtime_multiplier); setOvertimeNotifications(s.overtime_notifications); setNotificationEmails(s.notification_emails ?? []); setNotificationSmsNumbers(s.notification_sms_numbers ?? []); setEnableDailyAlerts(s.enable_daily_alerts ?? true); setEnableWeeklyAlerts(s.enable_weekly_alerts ?? true); setDailyAlertThreshold(Math.round((s.daily_alert_threshold ?? 0.80) * 100)); setWeeklyAlertThreshold(Math.round((s.weekly_alert_threshold ?? 0.80) * 100)); setSendEndOfPeriodSummary(s.send_end_of_period_summary ?? true); setBrandName(s.brand_name ?? "Farm"); } const log = await getTimeTrackingNotificationLog(brandId, 50); setNotificationLog(log); setLoading(false); }, [brandId]); useEffect(() => { load(); }, [load]); const handleSaveNotifications = async () => { setSettingsSaving(true); setSettingsError(null); setSettingsSaved(false); const result = await updateTimeTrackingSettings(brandId, { pay_period_start_day: payPeriodStartDay, pay_period_length_days: payPeriodLength, daily_overtime_threshold: dailyOvertimeThreshold, weekly_overtime_threshold: weeklyOvertimeThreshold, overtime_multiplier: overtimeMultiplier, overtime_notifications: overtimeNotifications, notification_emails: notificationEmails, notification_sms_numbers: notificationSmsNumbers, enable_daily_alerts: enableDailyAlerts, enable_weekly_alerts: enableWeeklyAlerts, daily_alert_threshold: dailyAlertThreshold / 100, weekly_alert_threshold: weeklyAlertThreshold / 100, send_end_of_period_summary: sendEndOfPeriodSummary, brand_name: brandName, }); setSettingsSaving(false); if (!result.success) { setSettingsError(result.error ?? "Failed to save"); return; } setSettingsSaved(true); setTimeout(() => setSettingsSaved(false), 3000); load(); }; const addEmail = () => { const email = newEmail.trim(); if (email && !notificationEmails.includes(email)) { setNotificationEmails([...notificationEmails, email]); } setNewEmail(""); }; const removeEmail = (email: string) => setNotificationEmails(notificationEmails.filter(e => e !== email)); const addSms = () => { const num = newSms.replace(/[^0-9+]/g, ""); if (num && !notificationSmsNumbers.includes(num)) { setNotificationSmsNumbers([...notificationSmsNumbers, num]); } setNewSms(""); }; const removeSms = (num: string) => setNotificationSmsNumbers(notificationSmsNumbers.filter(n => n !== num)); const openAddWorker = () => { setEditingWorker(null); setWorkerName(""); setWorkerRole("worker"); setWorkerLang("en"); setWorkerActive(true); setWorkerError(null); setResetPinResult(null); setShowWorkerModal(true); }; const openEditWorker = (w: TimeWorker) => { setEditingWorker(w); setWorkerName(w.name); setWorkerRole(w.role); setWorkerLang(w.lang); setWorkerActive(w.active); setWorkerError(null); setResetPinResult(null); setShowWorkerModal(true); }; const handleSaveWorker = async () => { if (!workerName.trim()) { setWorkerError("Name is required"); return; } setSubmitting(true); setWorkerError(null); if (editingWorker) { const result = await updateTimeWorker(editingWorker.id, workerName.trim(), workerRole, workerLang, workerActive); if (!result.success) { setWorkerError(result.error ?? "Failed"); setSubmitting(false); return; } } else { const result = await createTimeWorker(brandId, workerName.trim(), workerRole, workerLang); if (!result.success) { setWorkerError(result.error ?? "Failed"); setSubmitting(false); return; } } setSubmitting(false); setShowWorkerModal(false); load(); }; const handleResetPin = async (workerId: string) => { const result = await resetTimeWorkerPin(workerId); if (result.success && result.pin) setResetPinResult(result.pin); else setWorkerError(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) { setWorkerError(result.error ?? "Failed"); return; } load(); }; const openAddTask = () => { setEditingTask(null); setTaskName(""); setTaskNameEs(""); setTaskUnit("hours"); setTaskActive(true); setTaskSortOrder(0); setTaskError(null); setShowTaskModal(true); }; const openEditTask = (t: TimeTask) => { setEditingTask(t); setTaskName(t.name); setTaskNameEs(t.name_es ?? ""); setTaskUnit(t.unit); setTaskActive(t.active); setTaskSortOrder(t.sort_order); setTaskError(null); setShowTaskModal(true); }; const handleSaveTask = async () => { if (!taskName.trim()) { setTaskError("Name is required"); return; } setSubmitting(true); setTaskError(null); if (editingTask) { const result = await updateTimeTask(editingTask.id, taskName.trim(), taskNameEs, taskUnit, taskActive, taskSortOrder); if (!result.success) { setTaskError(result.error ?? "Failed"); setSubmitting(false); return; } } else { const result = await createTimeTask(brandId, taskName.trim(), taskNameEs || null, taskUnit, taskSortOrder); if (!result.success) { setTaskError(result.error ?? "Failed"); setSubmitting(false); return; } } setSubmitting(false); setShowTaskModal(false); load(); }; const handleDeleteTask = async (taskId: string) => { if (!confirm("Delete this task?")) return; const result = await deleteTimeTask(taskId); if (!result.success) { setTaskError(result.error ?? "Failed"); return; } load(); }; if (loading) return (
Loading settings...
); return (
{(!workersOnly && !tasksOnly) && ( <> {/* ACCORDION 1: General Settings */}
{/* Pay Period & Overtime */}

Pay Period & Overtime

setPayPeriodLength(Number(e.target.value))} className="flex-1 px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 focus:outline-none focus:border-emerald-500 transition-colors" /> days
setDailyOvertimeThreshold(Number(e.target.value))} className="flex-1 px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 focus:outline-none focus:border-emerald-500 transition-colors" /> hrs
setWeeklyOvertimeThreshold(Number(e.target.value))} className="flex-1 px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 focus:outline-none focus:border-emerald-500 transition-colors" /> hrs
{/* Colorado notice */}

Colorado Overtime Law

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

{/* Alert Settings */}

Alert Settings

Daily overtime alerts

Notify when worker hits daily limit

Weekly overtime alerts

Notify when worker hits weekly limit

{/* Notification Recipients */}

Notification Recipients

setNewEmail(e.target.value)} onKeyDown={e => e.key === "Enter" && (addEmail(), e.preventDefault())} className="flex-1 px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 placeholder:text-stone-400 focus:outline-none focus:border-emerald-500" placeholder="manager@farm.com" />
{notificationEmails.map(e => ( {e} ))}
setNewSms(e.target.value)} onKeyDown={e => e.key === "Enter" && (addSms(), e.preventDefault())} className="flex-1 px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 placeholder:text-stone-400 focus:outline-none focus:border-emerald-500" placeholder="+1234567890" />
{notificationSmsNumbers.map(n => ( {n} ))}
{settingsError && (
{settingsError}
)} {settingsSaved && (
Settings saved successfully!
)}
{settingsSaving ? ( Saving... ) : ( <> Save Settings )}
)} {/* ACCORDION 2: Workers & PINs */} {(workersOnly || (!workersOnly && !tasksOnly)) && (

Manage time tracking workers and PIN codes.

{workers.length === 0 ? ( ) : workers.map(w => ( ))}
Name Role Lang Status Last Used Actions
No workers yet — add one to get started.
{w.name} {w.role} {w.lang} {w.active ? "Active" : "Inactive"} {w.last_used_at ? new Date(w.last_used_at).toLocaleDateString() : "—"}
)} {/* ACCORDION 3: Tasks */} {(tasksOnly || (!workersOnly && !tasksOnly)) && (

Define tasks workers can clock into.

{tasks.length === 0 ? ( ) : tasks.map(t => ( ))}
Name (EN) Name (ES) Unit Sort Status Actions
No tasks yet — add one to get started.
{t.name} {t.name_es ?? "—"} {t.unit} {t.sort_order} {t.active ? "Active" : "Inactive"}
)} {/* Worker Modal */} {showWorkerModal && (

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

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

New PIN:

{resetPinResult}

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

)}
setWorkerName(e.target.value)} className="w-full px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 placeholder:text-stone-400 focus:outline-none focus:border-emerald-500 transition-colors" placeholder="Worker name" />
setWorkerActive(e.target.checked)} className="w-4 h-4 rounded border-stone-300 bg-white text-emerald-600" />
{editingWorker && (
)}
)} {/* Task Modal */} {showTaskModal && (

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

{taskError &&
{taskError}
}
setTaskName(e.target.value)} className="w-full px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 placeholder:text-stone-400 focus:outline-none focus:border-emerald-500 transition-colors" placeholder="e.g. Harvesting" />
setTaskNameEs(e.target.value)} className="w-full px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 placeholder:text-stone-400 focus:outline-none focus:border-emerald-500 transition-colors" placeholder="e.g. Cosecha" />
setTaskSortOrder(parseInt(e.target.value) || 0)} className="w-full px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 focus:outline-none focus:border-emerald-500 transition-colors" />
setTaskActive(e.target.checked)} className="w-4 h-4 rounded border-stone-300 bg-white text-emerald-600" />
)}
); }