diff --git a/src/lib/components/tasks/TaskForm.svelte b/src/lib/components/tasks/TaskForm.svelte index 87d2a77..1f78325 100644 --- a/src/lib/components/tasks/TaskForm.svelte +++ b/src/lib/components/tasks/TaskForm.svelte @@ -37,6 +37,7 @@ /** @type {string[]} */ let selectedTags = $state(task?.tags ? [...task.tags] : []); let difficulty = $state(task?.difficulty || ''); + let refreshMode = $state(task?.refreshMode || 'completion'); let isRecurring = $state(task?.isRecurring || false); let frequencyType = $state(task?.frequency?.type || 'weekly'); let customDays = $state(task?.frequency?.customDays || 7); @@ -121,6 +122,7 @@ tags: selectedTags, difficulty: difficulty || undefined, isRecurring, + refreshMode: isRecurring ? refreshMode : undefined, frequency: isRecurring ? { type: frequencyType, customDays: frequencyType === 'custom' ? customDays : undefined @@ -321,6 +323,33 @@ {/if} + +
+ +
+ + +
+ + {refreshMode === 'completion' + ? 'Intervall startet ab dem Zeitpunkt der Erledigung' + : 'Intervall startet ab dem geplanten Fälligkeitsdatum (holt auf bei Verspätung)'} + +
{/if}
@@ -608,6 +637,36 @@ color: var(--nord12); } + /* ── Refresh mode buttons ── */ + .refresh-mode-buttons { + display: flex; + gap: 0.4rem; + margin-bottom: 0.3rem; + } + .refresh-btn { + all: unset; + flex: 1; + text-align: center; + padding: 0.4rem 0.5rem; + font-size: 0.8rem; + font-weight: 500; + border-radius: 8px; + cursor: pointer; + border: 1.5px solid var(--color-border, #ddd); + transition: all 120ms; + color: var(--color-text-secondary, #777); + } + .refresh-btn:hover { + border-color: var(--nord10); + background: rgba(94, 129, 172, 0.06); + } + .refresh-btn.selected { + border-color: var(--nord10); + background: rgba(94, 129, 172, 0.1); + color: var(--nord10); + font-weight: 600; + } + .field-row { margin-bottom: 0.75rem; } diff --git a/src/models/Task.ts b/src/models/Task.ts index beef688..0063116 100644 --- a/src/models/Task.ts +++ b/src/models/Task.ts @@ -7,6 +7,7 @@ export interface ITask { assignees: string[]; tags: string[]; difficulty?: 'low' | 'medium' | 'high'; + refreshMode?: 'completion' | 'planned'; isRecurring: boolean; frequency?: { type: 'daily' | 'weekly' | 'biweekly' | 'monthly' | 'custom'; @@ -45,6 +46,11 @@ const TaskSchema = new mongoose.Schema( type: String, enum: ['low', 'medium', 'high'] }, + refreshMode: { + type: String, + enum: ['completion', 'planned'], + default: 'completion' + }, isRecurring: { type: Boolean, required: true, diff --git a/src/routes/api/tasks/+server.ts b/src/routes/api/tasks/+server.ts index dad35ba..a7d5f9c 100644 --- a/src/routes/api/tasks/+server.ts +++ b/src/routes/api/tasks/+server.ts @@ -21,7 +21,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { if (!auth?.user?.nickname) throw error(401, 'Not logged in'); const data = await request.json(); - const { title, description, assignees, tags, difficulty, isRecurring, frequency, nextDueDate } = data; + const { title, description, assignees, tags, difficulty, refreshMode, isRecurring, frequency, nextDueDate } = data; if (!title?.trim()) throw error(400, 'Title is required'); if (!nextDueDate) throw error(400, 'Due date is required'); @@ -35,6 +35,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { assignees: assignees || [], tags: tags || [], difficulty: difficulty || undefined, + refreshMode: isRecurring ? (refreshMode || 'completion') : undefined, isRecurring: !!isRecurring, frequency: isRecurring ? frequency : undefined, nextDueDate: new Date(nextDueDate), diff --git a/src/routes/api/tasks/[id]/+server.ts b/src/routes/api/tasks/[id]/+server.ts index 4a45382..bd29b03 100644 --- a/src/routes/api/tasks/[id]/+server.ts +++ b/src/routes/api/tasks/[id]/+server.ts @@ -8,7 +8,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => { if (!auth?.user?.nickname) throw error(401, 'Not logged in'); const data = await request.json(); - const { title, description, assignees, tags, difficulty, isRecurring, frequency, nextDueDate, active } = data; + const { title, description, assignees, tags, difficulty, refreshMode, isRecurring, frequency, nextDueDate, active } = data; await dbConnect(); @@ -20,6 +20,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => { if (assignees !== undefined) task.assignees = assignees; if (tags !== undefined) task.tags = tags; if (difficulty !== undefined) task.difficulty = difficulty || undefined; + if (refreshMode !== undefined) task.refreshMode = refreshMode || 'completion'; if (isRecurring !== undefined) { task.isRecurring = isRecurring; task.frequency = isRecurring ? frequency : undefined; diff --git a/src/routes/api/tasks/[id]/complete/+server.ts b/src/routes/api/tasks/[id]/complete/+server.ts index 9d90967..18b70a8 100644 --- a/src/routes/api/tasks/[id]/complete/+server.ts +++ b/src/routes/api/tasks/[id]/complete/+server.ts @@ -55,8 +55,15 @@ export const POST: RequestHandler = async ({ params, request, locals }) => { task.lastCompletedBy = completedFor; if (task.isRecurring && task.frequency) { - // Reset from NOW (completion time), not from the original due date - task.nextDueDate = getNextDueDate(now, task.frequency.type, task.frequency.customDays); + // 'planned': calculate from the original due date (catches up if overdue) + // 'completion' (default): calculate from now + const baseDate = task.refreshMode === 'planned' ? task.nextDueDate : now; + let next = getNextDueDate(baseDate, task.frequency.type, task.frequency.customDays); + // If planned mode produced a date in the past, keep advancing until it's in the future + while (task.refreshMode === 'planned' && next <= now) { + next = getNextDueDate(next, task.frequency.type, task.frequency.customDays); + } + task.nextDueDate = next; } else { // One-off task: deactivate task.active = false;