Compare commits

2 Commits

Author SHA1 Message Date
866d2e9fff tasks: use Toggle component for recurring task switch
All checks were successful
CI / update (push) Successful in 5m12s
2026-04-02 17:29:35 +02:00
381012db98 tasks: add refresh mode toggle (completion date vs planned date)
Recurring tasks can now calculate next due date from either the
completion time (default) or the planned due date, catching up
if overdue.
2026-04-02 17:29:08 +02:00
5 changed files with 80 additions and 15 deletions

View File

@@ -2,6 +2,7 @@
import { X, Sparkles, Wind, Bath, UtensilsCrossed, CookingPot, WashingMachine, import { X, Sparkles, Wind, Bath, UtensilsCrossed, CookingPot, WashingMachine,
Flower2, Droplets, Leaf, ShoppingCart, Trash2, Shirt, Brush } from 'lucide-svelte'; Flower2, Droplets, Leaf, ShoppingCart, Trash2, Shirt, Brush } from 'lucide-svelte';
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte'; import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
import Toggle from '$lib/components/Toggle.svelte';
const USERS = ['anna', 'alexander']; const USERS = ['anna', 'alexander'];
@@ -37,6 +38,7 @@
/** @type {string[]} */ /** @type {string[]} */
let selectedTags = $state(task?.tags ? [...task.tags] : []); let selectedTags = $state(task?.tags ? [...task.tags] : []);
let difficulty = $state(task?.difficulty || ''); let difficulty = $state(task?.difficulty || '');
let refreshMode = $state(task?.refreshMode || 'completion');
let isRecurring = $state(task?.isRecurring || false); let isRecurring = $state(task?.isRecurring || false);
let frequencyType = $state(task?.frequency?.type || 'weekly'); let frequencyType = $state(task?.frequency?.type || 'weekly');
let customDays = $state(task?.frequency?.customDays || 7); let customDays = $state(task?.frequency?.customDays || 7);
@@ -121,6 +123,7 @@
tags: selectedTags, tags: selectedTags,
difficulty: difficulty || undefined, difficulty: difficulty || undefined,
isRecurring, isRecurring,
refreshMode: isRecurring ? refreshMode : undefined,
frequency: isRecurring ? { frequency: isRecurring ? {
type: frequencyType, type: frequencyType,
customDays: frequencyType === 'custom' ? customDays : undefined customDays: frequencyType === 'custom' ? customDays : undefined
@@ -297,10 +300,7 @@
</div> </div>
<div class="field-row"> <div class="field-row">
<label class="checkbox-label"> <Toggle bind:checked={isRecurring} label="Wiederkehrend" accentColor="var(--nord10)" />
<input type="checkbox" bind:checked={isRecurring} />
Wiederkehrend
</label>
</div> </div>
{#if isRecurring} {#if isRecurring}
@@ -321,6 +321,33 @@
<input id="customDays" type="number" bind:value={customDays} min="1" max="365" /> <input id="customDays" type="number" bind:value={customDays} min="1" max="365" />
</div> </div>
{/if} {/if}
<div class="field">
<label>Nächstes Fälligkeitsdatum berechnen ab</label>
<div class="refresh-mode-buttons">
<button
type="button"
class="refresh-btn"
class:selected={refreshMode === 'completion'}
onclick={() => refreshMode = 'completion'}
>
Erledigung
</button>
<button
type="button"
class="refresh-btn"
class:selected={refreshMode === 'planned'}
onclick={() => refreshMode = 'planned'}
>
Geplantes Datum
</button>
</div>
<span class="hint">
{refreshMode === 'completion'
? 'Intervall startet ab dem Zeitpunkt der Erledigung'
: 'Intervall startet ab dem geplanten Fälligkeitsdatum (holt auf bei Verspätung)'}
</span>
</div>
{/if} {/if}
<div class="form-actions"> <div class="form-actions">
@@ -608,16 +635,39 @@
color: var(--nord12); 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 { .field-row {
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
cursor: pointer;
}
.form-actions { .form-actions {
display: flex; display: flex;

View File

@@ -7,6 +7,7 @@ export interface ITask {
assignees: string[]; assignees: string[];
tags: string[]; tags: string[];
difficulty?: 'low' | 'medium' | 'high'; difficulty?: 'low' | 'medium' | 'high';
refreshMode?: 'completion' | 'planned';
isRecurring: boolean; isRecurring: boolean;
frequency?: { frequency?: {
type: 'daily' | 'weekly' | 'biweekly' | 'monthly' | 'custom'; type: 'daily' | 'weekly' | 'biweekly' | 'monthly' | 'custom';
@@ -45,6 +46,11 @@ const TaskSchema = new mongoose.Schema(
type: String, type: String,
enum: ['low', 'medium', 'high'] enum: ['low', 'medium', 'high']
}, },
refreshMode: {
type: String,
enum: ['completion', 'planned'],
default: 'completion'
},
isRecurring: { isRecurring: {
type: Boolean, type: Boolean,
required: true, required: true,

View File

@@ -21,7 +21,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
if (!auth?.user?.nickname) throw error(401, 'Not logged in'); if (!auth?.user?.nickname) throw error(401, 'Not logged in');
const data = await request.json(); 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 (!title?.trim()) throw error(400, 'Title is required');
if (!nextDueDate) throw error(400, 'Due date is required'); if (!nextDueDate) throw error(400, 'Due date is required');
@@ -35,6 +35,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
assignees: assignees || [], assignees: assignees || [],
tags: tags || [], tags: tags || [],
difficulty: difficulty || undefined, difficulty: difficulty || undefined,
refreshMode: isRecurring ? (refreshMode || 'completion') : undefined,
isRecurring: !!isRecurring, isRecurring: !!isRecurring,
frequency: isRecurring ? frequency : undefined, frequency: isRecurring ? frequency : undefined,
nextDueDate: new Date(nextDueDate), nextDueDate: new Date(nextDueDate),

View File

@@ -8,7 +8,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
if (!auth?.user?.nickname) throw error(401, 'Not logged in'); if (!auth?.user?.nickname) throw error(401, 'Not logged in');
const data = await request.json(); 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(); await dbConnect();
@@ -20,6 +20,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
if (assignees !== undefined) task.assignees = assignees; if (assignees !== undefined) task.assignees = assignees;
if (tags !== undefined) task.tags = tags; if (tags !== undefined) task.tags = tags;
if (difficulty !== undefined) task.difficulty = difficulty || undefined; if (difficulty !== undefined) task.difficulty = difficulty || undefined;
if (refreshMode !== undefined) task.refreshMode = refreshMode || 'completion';
if (isRecurring !== undefined) { if (isRecurring !== undefined) {
task.isRecurring = isRecurring; task.isRecurring = isRecurring;
task.frequency = isRecurring ? frequency : undefined; task.frequency = isRecurring ? frequency : undefined;

View File

@@ -55,8 +55,15 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
task.lastCompletedBy = completedFor; task.lastCompletedBy = completedFor;
if (task.isRecurring && task.frequency) { if (task.isRecurring && task.frequency) {
// Reset from NOW (completion time), not from the original due date // 'planned': calculate from the original due date (catches up if overdue)
task.nextDueDate = getNextDueDate(now, task.frequency.type, task.frequency.customDays); // '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 { } else {
// One-off task: deactivate // One-off task: deactivate
task.active = false; task.active = false;