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}
+
+
+
Nächstes Fälligkeitsdatum berechnen ab
+
+ refreshMode = 'completion'}
+ >
+ Erledigung
+
+ refreshMode = 'planned'}
+ >
+ Geplantes Datum
+
+
+
+ {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;