mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:41:09 +02:00
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:
parent
6aa8554e21
commit
330b9907b0
15 changed files with 346 additions and 190 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
55
apps/todo/packages/shared/src/constants/task.ts
Normal file
55
apps/todo/packages/shared/src/constants/task.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -108,6 +108,7 @@ export interface UpdateTaskInput {
|
|||
recurrenceEndDate?: string | null;
|
||||
subtasks?: Subtask[] | null;
|
||||
metadata?: TaskMetadata | null;
|
||||
labelIds?: string[];
|
||||
}
|
||||
|
||||
export interface QueryTasksInput {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue