feat: campo "hasta" opcional en Tareas recurrentes
Vacio = se repite sin fin (comportamiento previo). Al completar un ciclo cuyo proximo vencimiento supera "hasta", la tarea se desactiva sola. Probado en dev con curl (3 ciclos seguidos, se desactiva justo al superar la fecha) y validacion de que "hasta" no puede ser anterior al proximo vencimiento. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "project-linkedcorp-frontend",
|
||||
"private": true,
|
||||
"version": "1.12.0",
|
||||
"version": "1.12.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -153,6 +153,9 @@ export const messages = {
|
||||
recurring_task_reminder_days: 'Avisar con (días de anticipación)',
|
||||
recurring_task_reminder_hour: 'Hora del recordatorio',
|
||||
recurring_task_active: 'Activa',
|
||||
recurring_task_end_date: 'Hasta',
|
||||
recurring_task_unlimited: 'Indefinida',
|
||||
recurring_task_unlimited_hint: 'Vacío = se repite sin fin. Al pasar esta fecha, la tarea se desactiva sola.',
|
||||
recurring_task_complete_cycle: 'Completar ciclo',
|
||||
recurring_task_complete_confirm: '¿Confirmas que ya completaste este ciclo? Se calcula el próximo vencimiento sumando la periodicidad configurada.',
|
||||
recurring_task_promote: 'Convertir en recurrente',
|
||||
@@ -383,6 +386,9 @@ export const messages = {
|
||||
recurring_task_reminder_days: 'Remind (days ahead)',
|
||||
recurring_task_reminder_hour: 'Reminder hour',
|
||||
recurring_task_active: 'Active',
|
||||
recurring_task_end_date: 'Until',
|
||||
recurring_task_unlimited: 'Unlimited',
|
||||
recurring_task_unlimited_hint: 'Empty = repeats forever. Once past this date, the task deactivates itself.',
|
||||
recurring_task_complete_cycle: 'Complete cycle',
|
||||
recurring_task_complete_confirm: 'Confirm you already completed this cycle? The next due date is calculated by adding the configured recurrence.',
|
||||
recurring_task_promote: 'Turn into recurring',
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi
|
||||
setPromoting(task);
|
||||
setPromoteForm({
|
||||
name: task.name, responsibleUserId: task.responsible_user_id || '',
|
||||
intervalUnit: 'months', intervalValue: 1, nextDueDate: task.due_date || '',
|
||||
intervalUnit: 'months', intervalValue: 1, nextDueDate: task.due_date || '', endDate: '',
|
||||
reminderDaysBefore: 7, reminderHour: 9,
|
||||
});
|
||||
}
|
||||
@@ -93,7 +93,7 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi
|
||||
if (!promoteForm.nextDueDate) return;
|
||||
await runAction(async () => {
|
||||
await promoteTaskToRecurring(promoting.id, {
|
||||
...promoteForm, responsibleUserId: promoteForm.responsibleUserId || null,
|
||||
...promoteForm, responsibleUserId: promoteForm.responsibleUserId || null, endDate: promoteForm.endDate || null,
|
||||
});
|
||||
setPromoting(null);
|
||||
pushToast({ tone: 'success', message: t('common_saved') });
|
||||
@@ -253,7 +253,7 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi
|
||||
title={t('recurring_task_promote_title')}
|
||||
onClose={() => setPromoting(null)}
|
||||
width={560}
|
||||
height={520}
|
||||
height={600}
|
||||
>
|
||||
{promoteForm && (
|
||||
<>
|
||||
@@ -269,6 +269,11 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi
|
||||
<input className="input" type="date" value={promoteForm.nextDueDate} onChange={(e) => setPromoteForm((f) => ({ ...f, nextDueDate: e.target.value }))} />
|
||||
</Field>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Field label={t('recurring_task_end_date')} hint={t('recurring_task_unlimited_hint')}>
|
||||
<input className="input" type="date" value={promoteForm.endDate} onChange={(e) => setPromoteForm((f) => ({ ...f, endDate: e.target.value }))} />
|
||||
</Field>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Field label={t('recurring_task_every')}>
|
||||
<input className="input" type="number" min={1} value={promoteForm.intervalValue} onChange={(e) => setPromoteForm((f) => ({ ...f, intervalValue: Number(e.target.value) }))} />
|
||||
|
||||
@@ -19,7 +19,7 @@ const HOUR_OPTIONS = Array.from({ length: 24 }, (_, h) => ({ value: h, label: `$
|
||||
|
||||
const EMPTY_FORM = {
|
||||
name: '', description: '', projectId: '', responsibleUserId: '',
|
||||
intervalUnit: 'months', intervalValue: 1, nextDueDate: '',
|
||||
intervalUnit: 'months', intervalValue: 1, nextDueDate: '', endDate: '',
|
||||
reminderDaysBefore: 7, reminderHour: 9,
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ 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,
|
||||
intervalValue: r.interval_value, nextDueDate: r.next_due_date, endDate: r.end_date || '',
|
||||
reminderDaysBefore: r.reminder_days_before, reminderHour: r.reminder_hour,
|
||||
};
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export default function RecurringTasksPage() {
|
||||
async function save() {
|
||||
if (!form.name.trim() || !form.nextDueDate) return;
|
||||
await runAction(async () => {
|
||||
const payload = { ...form, projectId: form.projectId || null, responsibleUserId: form.responsibleUserId || null };
|
||||
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);
|
||||
setEditing(false);
|
||||
@@ -118,6 +118,7 @@ export default function RecurringTasksPage() {
|
||||
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`,
|
||||
@@ -155,7 +156,7 @@ export default function RecurringTasksPage() {
|
||||
title={selected ? t('common_edit') : t('recurring_task_new')}
|
||||
onClose={() => setEditing(false)}
|
||||
width={640}
|
||||
height={640}
|
||||
height={720}
|
||||
>
|
||||
<Field label={t('common_name')}>
|
||||
<input className="input" value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} autoFocus />
|
||||
@@ -182,6 +183,11 @@ export default function RecurringTasksPage() {
|
||||
<input className="input" type="date" value={form.nextDueDate} onChange={(e) => setForm((f) => ({ ...f, nextDueDate: e.target.value }))} />
|
||||
</Field>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Field label={t('recurring_task_end_date')} hint={t('recurring_task_unlimited_hint')}>
|
||||
<input className="input" type="date" value={form.endDate} onChange={(e) => setForm((f) => ({ ...f, endDate: e.target.value }))} />
|
||||
</Field>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Field label={t('recurring_task_reminder_days')}>
|
||||
<input className="input" type="number" min={0} value={form.reminderDaysBefore} onChange={(e) => setForm((f) => ({ ...f, reminderDaysBefore: Number(e.target.value) }))} />
|
||||
|
||||
Reference in New Issue
Block a user