import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { Plus, Save, Trash2, RotateCcw, Pencil } from 'lucide-react'; import { useUi } from '../../context/ui-context.jsx'; import { useAsync, useAction } from '../../hooks/useAsync.js'; import { listRecurringTasks, createRecurringTask, updateRecurringTask, deleteRecurringTask, completeRecurringTaskCycle, } from '../../services/recurringTasks.js'; import { listUsers } from '../../services/users.js'; import { listProjectOptions } from '../../services/projects.js'; import { ListView } from '../../components/ListView.jsx'; import { Button } from '../../components/Button.jsx'; import { Field, FieldRow } from '../../components/Field.jsx'; import { ComboBox } from '../../components/ComboBox.jsx'; import { Badge } from '../../components/Badge.jsx'; import { DetailPopup } from '../../components/DetailPopup.jsx'; import { DraftNotice } from '../../components/DraftNotice.jsx'; import { DraftMark, useGuardedClose } from '../../components/NavigationGuard.jsx'; import { useDraftForm, useUserDrafts, hasDraftFor, useReopenDraft } from '../../hooks/useDraftForm.js'; const HOUR_OPTIONS = Array.from({ length: 24 }, (_, h) => ({ value: h, label: `${String(h).padStart(2, '0')}:00` })); const EMPTY_FORM = { name: '', description: '', projectId: '', responsibleUserId: '', intervalUnit: 'months', intervalValue: 1, nextDueDate: '', endDate: '', reminderDaysBefore: 7, reminderHour: 9, }; function toForm(r) { return { name: r.name, description: r.description || '', projectId: r.project_id || '', responsibleUserId: r.responsible_user_id || '', intervalUnit: r.interval_unit, intervalValue: r.interval_value, nextDueDate: r.next_due_date, endDate: r.end_date || '', reminderDaysBefore: r.reminder_days_before, reminderHour: r.reminder_hour, editVersion: r.edit_version, }; } export default function RecurringTasksPage() { const { t, pushToast, openDialog } = useUi(); const runAction = useAction(); const { data: items, reload } = useAsync(listRecurringTasks, []); const { data: users } = useAsync(listUsers, []); const { data: projects } = useAsync(listProjectOptions, []); const [editing, setEditing] = useState(false); const [selected, setSelected] = useState(null); const [searchParams, setSearchParams] = useSearchParams(); // Borrador del popup (nueva o edicion de una existente). const draft = useDraftForm({ formId: selected ? 'recurring-edit' : 'recurring-new', entityId: selected?.id ?? null, initial: selected ? toForm(selected) : EMPTY_FORM, label: selected ? `${t('common_edit')}: ${selected.name}` : t('recurring_task_new'), enabled: editing, }); const form = draft.values; const setForm = draft.setValues; const guardedClose = useGuardedClose(); const pendingNew = hasDraftFor(useUserDrafts(), 'recurring-new', null); useReopenDraft({ newFormId: 'recurring-new', editFormId: 'recurring-edit', items, openNew: () => startNew(), openEdit: (r) => startEdit(r), }); const userOptions = (users || []).map((u) => ({ value: u.id, label: u.name })); const projectOptions = (projects || []).map((p) => ({ value: p.id, label: `${p.code} — ${p.name}` })); const unitOptions = [ { value: 'days', label: t('recurring_task_unit_days') }, { value: 'months', label: t('recurring_task_unit_months') }, { value: 'years', label: t('recurring_task_unit_years') }, ]; const unitLabel = { days: t('recurring_task_unit_days'), months: t('recurring_task_unit_months'), years: t('recurring_task_unit_years') }; // Llegada desde el Calendario (?task=id): abre esa tarea como un clic en su fila. useEffect(() => { const id = Number(searchParams.get('task')); if (!id || !items) return; const found = items.find((r) => r.id === id); if (found) startEdit(found); setSearchParams((prev) => { const next = new URLSearchParams(prev); next.delete('task'); return next; }, { replace: true }); }, [searchParams, items]); // eslint-disable-line react-hooks/exhaustive-deps function startNew() { setSelected(null); setEditing(true); } function startEdit(r) { setSelected(r); setEditing(true); } async function save() { if (!form.name.trim() || !form.nextDueDate) return; const run = () => runAction(async () => { const payload = { ...form, projectId: form.projectId || null, responsibleUserId: form.responsibleUserId || null, endDate: form.endDate || null }; if (selected) await updateRecurringTask(selected.id, payload); else await createRecurringTask(payload); draft.clear(); setEditing(false); pushToast({ tone: 'success', message: t('common_saved') }); await reload(); }); // Edicion: si el registro cambio en el servidor desde que se empezo a editar, se confirma. if (selected) await draft.guardSave(run); else await run(); } function confirmDelete(r) { openDialog({ tone: 'warning', title: t('common_confirm_delete_title'), message: t('common_confirm_delete_msg'), actions: [ { label: t('common_cancel'), variant: 'standard' }, { label: t('common_delete'), variant: 'danger', onClick: () => runAction(async () => { await deleteRecurringTask(r.id); await reload(); }), }, ], }); } function confirmComplete(r) { openDialog({ tone: 'info', title: t('recurring_task_complete_cycle'), message: t('recurring_task_complete_confirm'), actions: [ { label: t('common_cancel'), variant: 'standard' }, { label: t('recurring_task_complete_cycle'), variant: 'accent', onClick: () => runAction(async () => { await completeRecurringTaskCycle(r.id); await reload(); pushToast({ tone: 'success', message: t('common_saved') }); }), }, ], }); } const columns = [ { key: 'name', label: t('common_name') }, { key: 'project_name', label: t('recurring_task_project'), render: (r) => r.project_name || '—' }, { key: 'interval_value', label: t('recurring_task_interval'), render: (r) => `${t('recurring_task_every')} ${r.interval_value} ${unitLabel[r.interval_unit]}`, }, { key: 'next_due_date', label: t('recurring_task_next_due'), render: (r) => String(r.next_due_date).slice(0, 10) }, { key: 'end_date', label: t('recurring_task_end_date'), render: (r) => r.end_date ? String(r.end_date).slice(0, 10) : t('recurring_task_unlimited') }, { key: 'reminder_hour', label: t('recurring_task_reminder'), render: (r) => `${r.reminder_days_before}d / ${String(r.reminder_hour).padStart(2, '0')}:00`, }, { key: 'responsible_name', label: t('common_responsible'), render: (r) => r.responsible_name || '—' }, { key: 'is_active', label: t('recurring_task_active'), render: (r) => {r.is_active ? t('common_yes') : t('common_no')} }, ]; if (!items) return
{t('common_loading')}
; return (

{t('nav_recurring_tasks')}

r.id} onRowClick={startEdit} initialSort={{ key: 'next_due_date', dir: 1 }} rowActions={(r) => [ { label: t('common_edit'), icon: Pencil, onClick: () => startEdit(r) }, { label: t('recurring_task_complete_cycle'), icon: RotateCcw, onClick: () => confirmComplete(r) }, { label: t('common_delete'), icon: Trash2, danger: true, onClick: () => confirmDelete(r) }, ]} /> guardedClose(draft, () => setEditing(false))} width={640} height={760} > setForm((f) => ({ ...f, name: e.target.value }))} autoFocus />