refactor(todo): rename priority labels for better natural language input

- low → "Später" (was "Niedrig")
- medium → "Normal" (was "Mittel")
- high → "Wichtig" (was "Hoch")
- urgent → "Dringend" (unchanged)

CommandBar syntax now supports: !später, !normal, !wichtig, !dringend
Shortcut syntax still works: !, !!, !!!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-10 14:52:11 +01:00 committed by Wuesteon
parent 6aa8554e21
commit 330b9907b0
15 changed files with 346 additions and 190 deletions

View file

@ -4,6 +4,7 @@
import { viewStore } from '$lib/stores/view.svelte';
import { projectsStore } from '$lib/stores/projects.svelte';
import type { TaskPriority } from '@todo/shared';
import { PRIORITY_OPTIONS } from '@todo/shared';
import { format, addDays } from 'date-fns';
import { de } from 'date-fns/locale';
@ -21,14 +22,6 @@
let showPriorityPicker = $state(false);
let showProjectPicker = $state(false);
// Priority options
const priorities: { value: TaskPriority; label: string; color: string }[] = [
{ value: 'low', label: 'Niedrig', color: '#22c55e' },
{ value: 'medium', label: 'Mittel', color: '#eab308' },
{ value: 'high', label: 'Hoch', color: '#f97316' },
{ value: 'urgent', label: 'Dringend', color: '#ef4444' },
];
// Quick date options
const dateOptions = [
{ label: 'Heute', date: new Date() },
@ -38,7 +31,7 @@
];
// Derived values
let currentPriority = $derived(priorities.find((p) => p.value === selectedPriority)!);
let currentPriority = $derived(PRIORITY_OPTIONS.find((p) => p.value === selectedPriority)!);
let selectedProject = $derived(
selectedProjectId ? projectsStore.getById(selectedProjectId) : undefined
);
@ -81,11 +74,14 @@
if (viewStore.currentView !== 'project') {
selectedProjectId = undefined;
}
inputRef?.focus();
} catch (error) {
console.error('Failed to create task:', error);
} finally {
isLoading = false;
// Focus after isLoading is reset (input is no longer disabled)
requestAnimationFrame(() => {
inputRef?.focus();
});
}
}
@ -232,7 +228,7 @@
{#if showPriorityPicker}
<div class="dropdown" onclick={(e) => e.stopPropagation()} role="menu">
{#each priorities as priority}
{#each PRIORITY_OPTIONS as priority}
<button
type="button"
class="dropdown-item"

View file

@ -1,5 +1,13 @@
<script lang="ts">
import type { Task, Subtask, TaskPriority, TaskStatus, EffectiveDuration } from '@todo/shared';
import type {
Task,
Subtask,
TaskPriority,
TaskStatus,
EffectiveDuration,
UpdateTaskInput,
} from '@todo/shared';
import { STATUS_OPTIONS, RECURRENCE_OPTIONS } from '@todo/shared';
import { projectsStore } from '$lib/stores/projects.svelte';
import { format } from 'date-fns';
import SubtaskList from './SubtaskList.svelte';
@ -15,7 +23,7 @@
task: Task;
open: boolean;
onClose: () => void;
onSave: (data: Partial<Task>) => void;
onSave: (data: UpdateTaskInput) => void;
onDelete: (taskId: string) => void;
}
@ -42,24 +50,6 @@
let isLoading = $state(false);
let showDeleteConfirm = $state(false);
// Status options
const statuses: { value: TaskStatus; label: string }[] = [
{ value: 'pending', label: 'Ausstehend' },
{ value: 'in_progress', label: 'In Bearbeitung' },
{ value: 'completed', label: 'Abgeschlossen' },
{ value: 'cancelled', label: 'Abgebrochen' },
];
// Recurrence options
const recurrenceOptions = [
{ value: '', label: 'Keine Wiederholung' },
{ value: 'FREQ=DAILY', label: 'Täglich' },
{ value: 'FREQ=WEEKLY', label: 'Wöchentlich' },
{ value: 'FREQ=WEEKLY;INTERVAL=2', label: 'Alle 2 Wochen' },
{ value: 'FREQ=MONTHLY', label: 'Monatlich' },
{ value: 'FREQ=YEARLY', label: 'Jährlich' },
];
// Initialize form when task changes or modal opens
$effect(() => {
if (open && task) {
@ -103,7 +93,7 @@
isLoading = true;
try {
const data: Partial<Task> = {
const data: UpdateTaskInput = {
title: title.trim(),
description: description.trim() || null,
dueDate: dueDate ? new Date(dueDate).toISOString() : null,
@ -121,11 +111,9 @@
effectiveDuration: effectiveDuration ?? undefined,
funRating: funRating ?? undefined,
},
labelIds: selectedLabelIds,
};
// Include labelIds for the update
(data as any).labelIds = selectedLabelIds;
onSave(data);
} finally {
isLoading = false;
@ -220,7 +208,7 @@
<div class="form-section">
<label class="form-label" for="task-status">Status</label>
<select id="task-status" class="form-select" bind:value={status}>
{#each statuses as s}
{#each STATUS_OPTIONS as s}
<option value={s.value}>{s.label}</option>
{/each}
</select>
@ -258,7 +246,7 @@
<div class="form-section">
<label class="form-label" for="task-recurrence">Wiederholung</label>
<select id="task-recurrence" class="form-select" bind:value={recurrenceRule}>
{#each recurrenceOptions as option}
{#each RECURRENCE_OPTIONS as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import type { TaskPriority } from '@todo/shared';
import { PRIORITY_OPTIONS } from '@todo/shared';
interface Props {
value: TaskPriority;
@ -7,17 +8,10 @@
}
let { value, onChange }: Props = $props();
const priorities: { value: TaskPriority; label: string; color: string }[] = [
{ value: 'low', label: 'Niedrig', color: '#22c55e' },
{ value: 'medium', label: 'Mittel', color: '#eab308' },
{ value: 'high', label: 'Hoch', color: '#f97316' },
{ value: 'urgent', label: 'Dringend', color: '#ef4444' },
];
</script>
<div class="priority-buttons">
{#each priorities as p}
{#each PRIORITY_OPTIONS as p}
<button
type="button"
class="priority-btn"

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import type { KanbanColumn, Task, TaskPriority } from '@todo/shared';
import { ConfirmationModal } from '@manacore/shared-ui';
import KanbanColumnComponent from './KanbanColumn.svelte';
import AddColumnButton from './AddColumnButton.svelte';
import { kanbanStore } from '$lib/stores/kanban.svelte';
@ -24,6 +25,8 @@
// Local columns state for drag and drop
let localColumns = $state<KanbanColumn[]>([]);
let showDeleteConfirm = $state(false);
let columnToDelete = $state<string | null>(null);
// Sync with store
$effect(() => {
@ -55,10 +58,17 @@
await kanbanStore.updateColumn(columnId, data);
}
async function handleDeleteColumn(columnId: string) {
if (confirm('Spalte wirklich löschen? Alle Aufgaben werden in die erste Spalte verschoben.')) {
await kanbanStore.deleteColumn(columnId);
function handleDeleteColumn(columnId: string) {
columnToDelete = columnId;
showDeleteConfirm = true;
}
async function confirmDeleteColumn() {
if (columnToDelete) {
await kanbanStore.deleteColumn(columnToDelete);
}
showDeleteConfirm = false;
columnToDelete = null;
}
async function handleTasksReorder(columnId: string, taskIds: string[]) {
@ -167,6 +177,21 @@
{/if}
</div>
<!-- Delete column confirmation modal -->
<ConfirmationModal
visible={showDeleteConfirm}
onClose={() => {
showDeleteConfirm = false;
columnToDelete = null;
}}
onConfirm={confirmDeleteColumn}
variant="danger"
title="Spalte löschen?"
message="Alle Aufgaben dieser Spalte werden in die erste Spalte verschoben."
confirmLabel="Löschen"
cancelLabel="Abbrechen"
/>
<style>
.kanban-board {
min-height: 400px;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import type { KanbanColumn, Task } from '@todo/shared';
import type { KanbanColumn, Task, UpdateTaskInput } from '@todo/shared';
import KanbanTaskCard from './KanbanTaskCard.svelte';
import KanbanColumnHeader from './KanbanColumnHeader.svelte';
import QuickAddTaskInline from './QuickAddTaskInline.svelte';
@ -71,9 +71,9 @@
}
}
async function handleSaveTask(task: Task, data: Partial<Task>) {
async function handleSaveTask(task: Task, data: UpdateTaskInput) {
// Transform data to match updateTask API (convert null to undefined)
const updateData: Parameters<typeof tasksStore.updateTask>[1] = {};
const updateData: UpdateTaskInput = {};
if (data.title !== undefined) updateData.title = data.title;
if (data.description !== undefined) updateData.description = data.description ?? undefined;
if (data.projectId !== undefined) updateData.projectId = data.projectId;
@ -84,7 +84,7 @@
if (data.recurrenceRule !== undefined)
updateData.recurrenceRule = data.recurrenceRule ?? undefined;
if (data.metadata !== undefined) updateData.metadata = data.metadata;
if ((data as any).labelIds !== undefined) (updateData as any).labelIds = (data as any).labelIds;
if (data.labelIds !== undefined) updateData.labelIds = data.labelIds;
await tasksStore.updateTask(task.id, updateData);
}

View file

@ -36,19 +36,19 @@
},
{
value: 'high',
label: 'Hoch',
label: 'Wichtig',
color: 'text-orange-600 dark:text-orange-400',
bgColor: 'bg-orange-500',
},
{
value: 'medium',
label: 'Mittel',
label: 'Normal',
color: 'text-yellow-600 dark:text-yellow-400',
bgColor: 'bg-yellow-500',
},
{
value: 'low',
label: 'Niedrig',
label: 'Später',
color: 'text-blue-600 dark:text-blue-400',
bgColor: 'bg-blue-500',
},

View file

@ -2,6 +2,7 @@
import type { Task } from '@todo/shared';
import { format, isToday, isPast, isTomorrow } from 'date-fns';
import { de } from 'date-fns/locale';
import { ConfirmationModal } from '@manacore/shared-ui';
import TaskEditModal from '../TaskEditModal.svelte';
interface Props {
@ -15,6 +16,7 @@
// Modal state
let showModal = $state(false);
let showDeleteConfirm = $state(false);
// Inline edit state
let isEditingTitle = $state(false);
@ -129,9 +131,12 @@
function handleContextDelete() {
showContextMenu = false;
if (confirm('Aufgabe wirklich löschen?')) {
onDelete?.();
}
showDeleteConfirm = true;
}
function confirmDelete() {
showDeleteConfirm = false;
onDelete?.();
}
// Modal handlers
@ -308,6 +313,18 @@
onDelete={handleModalDelete}
/>
<!-- Delete confirmation modal -->
<ConfirmationModal
visible={showDeleteConfirm}
onClose={() => (showDeleteConfirm = false)}
onConfirm={confirmDelete}
variant="danger"
title="Aufgabe löschen?"
message="Diese Aufgabe wird unwiderruflich gelöscht."
confirmLabel="Löschen"
cancelLabel="Abbrechen"
/>
<style>
.kanban-card {
display: flex;

View file

@ -22,9 +22,9 @@
// Priority labels
const PRIORITY_LABELS: Record<TaskPriority, string> = {
low: 'Niedrig',
medium: 'Mittel',
high: 'Hoch',
low: 'Später',
medium: 'Normal',
high: 'Wichtig',
urgent: 'Dringend',
};
@ -98,49 +98,48 @@
<div class="donut-container">
<h3 class="donut-title">Prioritäten</h3>
<div class="donut-content">
<div class="donut-chart">
<svg viewBox="0 0 {SIZE} {SIZE}" class="donut-svg">
{#each arcs as arc}
<path
d={arc.path}
fill={arc.color}
class="arc-segment"
class:hovered={hoveredSegment === arc.priority}
onmouseenter={() => (hoveredSegment = arc.priority)}
onmouseleave={() => (hoveredSegment = null)}
role="graphics-symbol"
aria-label="{PRIORITY_LABELS[arc.priority]}: {arc.count}"
>
<title>{PRIORITY_LABELS[arc.priority]}: {arc.count} ({arc.percentage}%)</title>
</path>
{/each}
<!-- Center text -->
<text x={CENTER} y={CENTER - 8} class="center-count">
{total}
</text>
<text x={CENTER} y={CENTER + 12} class="center-label"> Aktiv </text>
</svg>
</div>
<!-- Legend -->
<div class="donut-legend">
{#each data as item}
<div
class="legend-item"
class:active={hoveredSegment === item.priority}
onmouseenter={() => (hoveredSegment = item.priority)}
<!-- Chart centered -->
<div class="donut-chart">
<svg viewBox="0 0 {SIZE} {SIZE}" class="donut-svg">
{#each arcs as arc}
<path
d={arc.path}
fill={arc.color}
class="arc-segment"
class:hovered={hoveredSegment === arc.priority}
onmouseenter={() => (hoveredSegment = arc.priority)}
onmouseleave={() => (hoveredSegment = null)}
role="button"
tabindex="0"
role="graphics-symbol"
aria-label="{PRIORITY_LABELS[arc.priority]}: {arc.count}"
>
<span class="legend-color" style="background-color: {item.color}"></span>
<span class="legend-label">{PRIORITY_LABELS[item.priority]}</span>
<span class="legend-count">{item.count}</span>
</div>
<title>{PRIORITY_LABELS[arc.priority]}: {arc.count} ({arc.percentage}%)</title>
</path>
{/each}
</div>
<!-- Center text -->
<text x={CENTER} y={CENTER - 8} class="center-count">
{total}
</text>
<text x={CENTER} y={CENTER + 12} class="center-label"> Aktiv </text>
</svg>
</div>
<!-- Legend as horizontal grid below -->
<div class="donut-legend">
{#each data as item}
<div
class="legend-item"
class:active={hoveredSegment === item.priority}
onmouseenter={() => (hoveredSegment = item.priority)}
onmouseleave={() => (hoveredSegment = null)}
role="button"
tabindex="0"
>
<span class="legend-color" style="background-color: {item.color}"></span>
<span class="legend-label">{PRIORITY_LABELS[item.priority]}</span>
<span class="legend-count">{item.count}</span>
</div>
{/each}
</div>
</div>
@ -166,20 +165,10 @@
margin: 0 0 1rem 0;
}
.donut-content {
display: flex;
align-items: center;
gap: 1.5rem;
}
@media (max-width: 400px) {
.donut-content {
flex-direction: column;
}
}
.donut-chart {
flex-shrink: 0;
display: flex;
justify-content: center;
margin-bottom: 1rem;
}
.donut-svg {
@ -215,11 +204,9 @@
}
.donut-legend {
display: flex;
flex-direction: column;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
flex: 1;
min-width: 0;
}
.legend-item {
@ -238,20 +225,23 @@
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 3px;
width: 10px;
height: 10px;
border-radius: 2px;
flex-shrink: 0;
}
.legend-label {
font-size: 0.875rem;
font-size: 0.75rem;
color: hsl(var(--foreground));
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.legend-count {
font-size: 0.875rem;
font-size: 0.75rem;
font-weight: 600;
color: hsl(var(--muted-foreground));
}

View file

@ -42,11 +42,12 @@ export interface ParsedTaskWithIds {
}
// Priority patterns (task-specific)
// Supports: !später, !normal, !wichtig, !dringend and shortcuts !, !!, !!!
const PRIORITY_PATTERNS: { pattern: RegExp; priority: TaskPriority }[] = [
{ pattern: /!{3,}|!dringend|!urgent/i, priority: 'urgent' },
{ pattern: /!{2}|!hoch|!high/i, priority: 'high' },
{ pattern: /!mittel|!medium/i, priority: 'medium' },
{ pattern: /!niedrig|!low/i, priority: 'low' },
{ pattern: /!{3,}|!dringend/i, priority: 'urgent' },
{ pattern: /!{2}|!wichtig/i, priority: 'high' },
{ pattern: /!normal/i, priority: 'medium' },
{ pattern: /!später|!sp[aä]ter/i, priority: 'low' },
];
/**
@ -160,9 +161,9 @@ export function formatParsedTaskPreview(parsed: ParsedTask): string {
if (parsed.priority) {
const priorityLabels: Record<TaskPriority, string> = {
low: '🟢 Niedrig',
medium: '🟡 Mittel',
high: '🟠 Hoch',
low: '🟢 Später',
medium: '🟡 Normal',
high: '🟠 Wichtig',
urgent: '🔴 Dringend',
};
parts.push(priorityLabels[parsed.priority]);

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { format, addDays, startOfDay } from 'date-fns';
import { format, addDays, subDays, startOfDay } from 'date-fns';
import { de } from 'date-fns/locale';
import { ListChecks } from '@manacore/shared-icons';
import { authStore } from '$lib/stores/auth.svelte';
@ -12,7 +12,7 @@
import CollapsibleSection from '$lib/components/CollapsibleSection.svelte';
import TaskEditModal from '$lib/components/TaskEditModal.svelte';
import { TaskListSkeleton } from '$lib/components/skeletons';
import type { Task } from '@todo/shared';
import type { Task, UpdateTaskInput } from '@todo/shared';
let isLoading = $state(true);
let editingTask = $state<Task | null>(null);
@ -39,13 +39,24 @@
let todayTasks = $derived(tasksStore.todayTasks);
let completedTasks = $derived(tasksStore.completedTasks);
// Group upcoming tasks by day
// Tomorrow's tasks
let tomorrowDate = $derived(addDays(startOfDay(new Date()), 1));
let dayAfterTomorrowDate = $derived(addDays(startOfDay(new Date()), 2));
let tomorrowTasks = $derived(
tasksStore.tasks.filter((task) => {
if (!task.dueDate || task.isCompleted) return false;
const taskDate = startOfDay(new Date(task.dueDate));
return taskDate.getTime() === tomorrowDate.getTime();
})
);
// Group upcoming tasks by day (starting from day after tomorrow)
let groupedUpcomingTasks = $derived(() => {
const groups: { date: Date; label: string; tasks: Task[] }[] = [];
const today = startOfDay(new Date());
// Start from tomorrow (day 1) through day 7
for (let i = 1; i <= 7; i++) {
// Start from day after tomorrow (day 2) through day 7
for (let i = 2; i <= 7; i++) {
const date = addDays(today, i);
const dayTasks = tasksStore.tasks.filter((task) => {
if (!task.dueDate || task.isCompleted) return false;
@ -54,13 +65,7 @@
});
if (dayTasks.length > 0) {
let label: string;
if (i === 1) {
label = 'Morgen';
} else {
label = format(date, 'EEEE, d. MMMM', { locale: de });
}
const label = format(date, 'EEEE, d. MMMM', { locale: de });
groups.push({ date, label, tasks: dayTasks });
}
}
@ -68,7 +73,7 @@
return groups;
});
// Total upcoming count
// Total upcoming count (excluding tomorrow)
let upcomingCount = $derived(
groupedUpcomingTasks().reduce((sum, group) => sum + group.tasks.length, 0)
);
@ -77,6 +82,7 @@
let allEmpty = $derived(
overdueTasks.length === 0 &&
todayTasks.length === 0 &&
tomorrowTasks.length === 0 &&
upcomingCount === 0 &&
completedTasks.length === 0
);
@ -90,7 +96,7 @@
editingTask = null;
}
async function handleSaveTask(data: Partial<Task>) {
async function handleSaveTask(data: UpdateTaskInput) {
if (!editingTask) return;
try {
@ -98,8 +104,8 @@
await tasksStore.updateTask(editingTask.id, data);
// Update labels if provided
if ('labelIds' in data) {
await tasksStore.updateLabels(editingTask.id, (data as any).labelIds);
if (data.labelIds !== undefined) {
await tasksStore.updateLabels(editingTask.id, data.labelIds);
}
closeEditModal();
@ -116,6 +122,32 @@
console.error('Failed to delete task:', error);
}
}
// Drag and drop handler - uses optimistic updates for smooth UX
function handleTaskDrop(taskId: string, targetDate: Date | 'completed' | 'overdue') {
const task = tasksStore.tasks.find((t) => t.id === taskId);
if (!task) return;
if (targetDate === 'completed') {
// Mark task as completed (optimistic)
if (!task.isCompleted) {
tasksStore.updateTaskOptimistic(taskId, { isCompleted: true });
}
} else if (targetDate === 'overdue') {
// Set to yesterday (optimistic)
const yesterday = subDays(startOfDay(new Date()), 1);
tasksStore.updateTaskOptimistic(taskId, {
dueDate: yesterday.toISOString(),
isCompleted: task.isCompleted ? false : undefined,
});
} else {
// Set to specific date (optimistic)
tasksStore.updateTaskOptimistic(taskId, {
dueDate: targetDate.toISOString(),
isCompleted: task.isCompleted ? false : undefined,
});
}
}
</script>
<svelte:head>
@ -155,7 +187,13 @@
variant="warning"
defaultOpen={true}
>
<TaskList tasks={overdueTasks} onEditTask={openEditModal} />
<TaskList
tasks={overdueTasks}
enableDragDrop
dropTargetDate="overdue"
onTaskDrop={handleTaskDrop}
onEditTask={openEditModal}
/>
</CollapsibleSection>
{/if}
@ -167,13 +205,30 @@
variant="default"
defaultOpen={true}
>
{#if todayTasks.length === 0}
<div class="text-center py-6 text-muted-foreground">
<p>Keine Aufgaben für heute</p>
</div>
{:else}
<TaskList tasks={todayTasks} onEditTask={openEditModal} />
{/if}
<TaskList
tasks={todayTasks}
enableDragDrop
dropTargetDate={startOfDay(new Date())}
onTaskDrop={handleTaskDrop}
onEditTask={openEditModal}
/>
</CollapsibleSection>
<!-- Tomorrow Section -->
<CollapsibleSection
title="Morgen"
count={tomorrowTasks.length}
icon="upcoming"
variant="default"
defaultOpen={true}
>
<TaskList
tasks={tomorrowTasks}
enableDragDrop
dropTargetDate={tomorrowDate}
onTaskDrop={handleTaskDrop}
onEditTask={openEditModal}
/>
</CollapsibleSection>
<!-- Upcoming Section -->
@ -184,39 +239,49 @@
variant="default"
defaultOpen={true}
>
{#if upcomingCount === 0}
<div class="text-center py-6 text-muted-foreground">
<p>Keine anstehenden Aufgaben</p>
</div>
{:else}
<div class="space-y-4">
{#each groupedUpcomingTasks() as group}
<div>
<h3 class="text-sm font-medium text-muted-foreground mb-2 pl-2">
{group.label} ({group.tasks.length})
</h3>
<TaskList tasks={group.tasks} onEditTask={openEditModal} />
</div>
{/each}
</div>
{/if}
<div class="space-y-4">
{#each groupedUpcomingTasks() as group}
<div>
<h3 class="text-sm font-medium text-muted-foreground mb-2 pl-2">
{group.label} ({group.tasks.length})
</h3>
<TaskList
tasks={group.tasks}
enableDragDrop
dropTargetDate={group.date}
onTaskDrop={handleTaskDrop}
onEditTask={openEditModal}
/>
</div>
{/each}
{#if upcomingCount === 0}
<!-- Empty drop zone for day after tomorrow -->
<TaskList
tasks={[]}
enableDragDrop
dropTargetDate={dayAfterTomorrowDate}
onTaskDrop={handleTaskDrop}
/>
{/if}
</div>
</CollapsibleSection>
<!-- Completed Section - collapsed by default -->
<!-- Completed Section -->
<CollapsibleSection
title="Erledigt"
count={completedTasks.length}
icon="completed"
variant="success"
defaultOpen={false}
defaultOpen={true}
>
{#if completedTasks.length === 0}
<div class="text-center py-6 text-muted-foreground">
<p>Noch keine erledigten Aufgaben</p>
</div>
{:else}
<TaskList tasks={completedTasks} showCompleted onEditTask={openEditModal} />
{/if}
<TaskList
tasks={completedTasks}
enableDragDrop
dropTargetDate="completed"
onTaskDrop={handleTaskDrop}
showCompleted
onEditTask={openEditModal}
/>
</CollapsibleSection>
</div>
{/if}

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { TagList, TagEditModal, type Tag } from '@manacore/shared-ui';
import { TagList, TagEditModal, ConfirmationModal, type Tag } from '@manacore/shared-ui';
import { MagnifyingGlass, Plus, CaretLeft } from '@manacore/shared-icons';
import { labelsStore } from '$lib/stores/labels.svelte';
import type { Label } from '@todo/shared';
@ -9,6 +9,8 @@
let searchQuery = $state('');
let showModal = $state(false);
let editingLabel = $state<Label | null>(null);
let showDeleteConfirm = $state(false);
let labelToDelete = $state<Tag | null>(null);
const filteredLabels = $derived.by(() => {
if (!searchQuery.trim()) return labelsStore.labels;
@ -67,13 +69,21 @@
}
}
async function handleDeleteFromList(tag: Tag) {
if (!confirm(`Label "${tag.name}" wirklich löschen?`)) return;
function handleDeleteFromList(tag: Tag) {
labelToDelete = tag;
showDeleteConfirm = true;
}
async function confirmDeleteLabel() {
if (!labelToDelete) return;
try {
await labelsStore.deleteLabel(tag.id);
await labelsStore.deleteLabel(labelToDelete.id);
} catch (e) {
console.error('Failed to delete label:', e);
} finally {
showDeleteConfirm = false;
labelToDelete = null;
}
}
@ -168,6 +178,21 @@
deleteConfirmMessage={`Label "${editingLabel?.name || ''}" wirklich löschen?`}
/>
<!-- Delete confirmation modal -->
<ConfirmationModal
visible={showDeleteConfirm}
onClose={() => {
showDeleteConfirm = false;
labelToDelete = null;
}}
onConfirm={confirmDeleteLabel}
variant="danger"
title="Label löschen?"
message={`Das Label "${labelToDelete?.name ?? ''}" wird unwiderruflich gelöscht.`}
confirmLabel="Löschen"
cancelLabel="Abbrechen"
/>
<style>
.page-container {
max-width: 640px;

View file

@ -6,6 +6,7 @@
import { todoSettings, type TodoView, type KanbanCardSize } from '$lib/stores/settings.svelte';
import { projectsStore } from '$lib/stores/projects.svelte';
import type { TaskPriority } from '@todo/shared';
import { PRIORITY_OPTIONS } from '@todo/shared';
import {
SettingsPage,
SettingsSection,
@ -20,13 +21,8 @@
GlobalSettingsSection,
} from '@manacore/shared-ui';
// Options for selects
const priorityOptions = [
{ value: 'low', label: 'Niedrig' },
{ value: 'medium', label: 'Mittel' },
{ value: 'high', label: 'Hoch' },
{ value: 'urgent', label: 'Dringend' },
];
// Use shared priority options (without color)
const priorityOptions = PRIORITY_OPTIONS.map((p) => ({ value: p.value, label: p.label }));
const viewOptions = [
{ value: 'inbox', label: 'Inbox' },

View file

@ -69,6 +69,9 @@ export const REMINDER_PRESETS = [
{ label: '1 week before', minutes: 10080 },
] as const;
// Re-export task-specific constants (German localized versions)
export * from './task';
// View types
export type ViewType =
| 'inbox'

View file

@ -0,0 +1,55 @@
import type { TaskPriority, TaskStatus } from '../types/task';
export interface PriorityOption {
value: TaskPriority;
label: string;
color: string;
}
export interface StatusOption {
value: TaskStatus;
label: string;
}
export interface RecurrenceOption {
value: string;
label: string;
}
export const PRIORITY_OPTIONS: PriorityOption[] = [
{ value: 'low', label: 'Später', color: '#22c55e' },
{ value: 'medium', label: 'Normal', color: '#eab308' },
{ value: 'high', label: 'Wichtig', color: '#f97316' },
{ value: 'urgent', label: 'Dringend', color: '#ef4444' },
];
export const STATUS_OPTIONS: StatusOption[] = [
{ value: 'pending', label: 'Offen' },
{ value: 'in_progress', label: 'In Arbeit' },
{ value: 'completed', label: 'Erledigt' },
{ value: 'cancelled', label: 'Abgebrochen' },
];
export const RECURRENCE_OPTIONS: RecurrenceOption[] = [
{ value: '', label: 'Keine Wiederholung' },
{ value: 'FREQ=DAILY', label: 'Täglich' },
{ value: 'FREQ=WEEKLY', label: 'Wöchentlich' },
{ value: 'FREQ=WEEKLY;INTERVAL=2', label: 'Alle 2 Wochen' },
{ value: 'FREQ=MONTHLY', label: 'Monatlich' },
{ value: 'FREQ=YEARLY', label: 'Jährlich' },
];
// Fibonacci sequence for story points
export const STORYPOINT_OPTIONS = [1, 2, 3, 5, 8, 13, 21] as const;
// Helper to get priority label
export function getPriorityLabel(priority: TaskPriority): string {
const option = PRIORITY_OPTIONS.find((p) => p.value === priority);
return option?.label ?? priority;
}
// Helper to get status label
export function getStatusLabel(status: TaskStatus): string {
const option = STATUS_OPTIONS.find((s) => s.value === status);
return option?.label ?? status;
}

View file

@ -108,6 +108,7 @@ export interface UpdateTaskInput {
recurrenceEndDate?: string | null;
subtasks?: Subtask[] | null;
metadata?: TaskMetadata | null;
labelIds?: string[];
}
export interface QueryTasksInput {