diff --git a/apps/todo/apps/web/src/lib/components/SubtaskList.svelte b/apps/todo/apps/web/src/lib/components/SubtaskList.svelte index 5df00fbb6..ec790be1b 100644 --- a/apps/todo/apps/web/src/lib/components/SubtaskList.svelte +++ b/apps/todo/apps/web/src/lib/components/SubtaskList.svelte @@ -4,6 +4,8 @@ import { flip } from 'svelte/animate'; import { untrack } from 'svelte'; import { Check, Plus, X, DotsSixVertical } from '@manacore/shared-icons'; + import { TodoEvents } from '@manacore/shared-utils/analytics'; + import { t } from 'svelte-i18n'; interface Props { subtasks: Subtask[]; @@ -20,7 +22,9 @@ const current = subtasks; untrack(() => { const currentIds = new Set(current.map((s) => s.id)); - const itemIds = new Set(items.filter((i) => i.id !== SHADOW_PLACEHOLDER_ITEM_ID).map((i) => i.id)); + const itemIds = new Set( + items.filter((i) => i.id !== SHADOW_PLACEHOLDER_ITEM_ID).map((i) => i.id) + ); const idsChanged = currentIds.size !== itemIds.size || current.some((s) => !itemIds.has(s.id)); @@ -42,10 +46,13 @@ items = e.detail.items.filter((item) => item.id !== SHADOW_PLACEHOLDER_ITEM_ID); onChange(items.map((item, index) => ({ ...item, order: index }))); dropInProgress = true; - setTimeout(() => { dropInProgress = false; }, 500); + setTimeout(() => { + dropInProgress = false; + }, 500); } function toggleComplete(id: string) { + const target = subtasks.find((s) => s.id === id); const updated = subtasks.map((s) => s.id === id ? { @@ -55,6 +62,7 @@ } : s ); + if (target && !target.isCompleted) TodoEvents.subtaskCompleted(); onChange(updated); } @@ -122,7 +130,7 @@ }} > -
+
@@ -147,15 +155,15 @@ spellcheck="false" onclick={(e) => e.stopPropagation()} onkeydown={(e) => handleTitleKeydown(e, subtask)} - onblur={(e) => handleTitleBlur(e, subtask)} - >{subtask.title} + onblur={(e) => handleTitleBlur(e, subtask)}>{subtask.title} @@ -172,12 +180,12 @@ {#if newSubtaskTitle.trim()} - + {/if}
diff --git a/apps/todo/apps/web/src/lib/components/TagStrip.svelte b/apps/todo/apps/web/src/lib/components/TagStrip.svelte index 9d241bec5..7485a02e2 100644 --- a/apps/todo/apps/web/src/lib/components/TagStrip.svelte +++ b/apps/todo/apps/web/src/lib/components/TagStrip.svelte @@ -5,6 +5,7 @@ import { goto } from '$app/navigation'; import { DotsThree, Plus, X } from '@manacore/shared-icons'; import TagStripModal from './TagStripModal.svelte'; + import { t } from 'svelte-i18n'; const tagsCtx: { readonly value: Tag[] } = getContext('tags'); @@ -54,7 +55,7 @@ class="clear-filter-pill glass-tag" class:hidden={!hasSelectedTags} onclick={() => viewStore.setFilterLabelIds([])} - title="Filter löschen" + title={$t('filters.clearFilter')} disabled={!hasSelectedTags} > @@ -62,15 +63,15 @@ - {#if !hasTags} {:else} {#each sortedTags as tag (tag.id)} @@ -90,10 +91,10 @@ {/if} diff --git a/apps/todo/apps/web/src/lib/components/TagStripModal.svelte b/apps/todo/apps/web/src/lib/components/TagStripModal.svelte index e7f8d3bf4..ec4dbb131 100644 --- a/apps/todo/apps/web/src/lib/components/TagStripModal.svelte +++ b/apps/todo/apps/web/src/lib/components/TagStripModal.svelte @@ -4,6 +4,7 @@ import { tagMutations } from '@manacore/shared-stores'; import { Plus, X, Check, Pencil, Trash, MagnifyingGlass } from '@manacore/shared-icons'; import { TagColorPicker, focusTrap } from '@manacore/shared-ui'; + import { t } from 'svelte-i18n'; const tagsCtx: { readonly value: Tag[] } = getContext('tags'); @@ -156,10 +157,10 @@ @@ -276,7 +281,7 @@ @@ -286,7 +291,7 @@ {#if searchQuery && sortedTags.length === 0}
-

Keine Tags gefunden für "{searchQuery}"

+

{$t('tags.noTagsFound', { values: { query: searchQuery } })}

{/if} {/if} @@ -298,12 +303,16 @@ {#if searchQuery} - {/if} diff --git a/apps/todo/apps/web/src/lib/components/TaskEditModal.svelte b/apps/todo/apps/web/src/lib/components/TaskEditModal.svelte index 0ab8a062a..3c2c40e65 100644 --- a/apps/todo/apps/web/src/lib/components/TaskEditModal.svelte +++ b/apps/todo/apps/web/src/lib/components/TaskEditModal.svelte @@ -10,11 +10,13 @@ DurationPicker, FunRatingPicker, TagSelector, + ReminderSelector, } from './form'; import { ContactSelector, focusTrap } from '@manacore/shared-ui'; import { ManaLinkList, ManaLinkPicker } from '@manacore/shared-links/ui'; import { searchCrossApp } from '$lib/data/cross-app-search'; import { X, Trash } from '@manacore/shared-icons'; + import { t } from 'svelte-i18n'; interface Props { task: Task; @@ -50,6 +52,7 @@ form.isLoading = true; try { onSave(form.buildUpdateInput(task)); + await form.persistReminder(task.id); } finally { form.isLoading = false; } @@ -84,13 +87,15 @@
{#if form.showDeleteConfirm} - Wirklich löschen? - + {$t('taskForm.confirmDelete')} + {$t('common.cancel')} {:else} - {/if} @@ -101,9 +106,11 @@ onclick={handleSave} disabled={form.isLoading || !form.title.trim()} > - {#if form.isLoading}{:else}Speichern{/if} + {#if form.isLoading}{:else}{$t('common.save')}{/if} - +
@@ -112,7 +119,7 @@ @@ -121,23 +128,25 @@
- Beschreibung + {$t('taskForm.description')}
- Subtasks + {$t('taskForm.subtasks')} @@ -148,7 +157,7 @@
- Status + {$t('taskForm.status')}
- Uhrzeit + {$t('taskForm.time')}
- Startdatum + {$t('taskForm.startDate')}
- Wiederholung + {$t('taskForm.recurrence')}
+ +
+ {$t('reminders.label')} + (form.reminderMinutes = v)} + disabled={!form.dueDate} + /> +
+
- Tags + {$t('taskForm.tags')} (form.selectedLabelIds = ids)} @@ -209,31 +228,31 @@
- Zuständig + {$t('taskForm.assignee')} (form.assignee = c)} onSearch={(q) => contactsStore.searchContacts(q)} singleSelect={true} allowManualEntry={false} - placeholder="Zuweisen..." - addLabel="Zuweisen" - searchPlaceholder="Name oder E-Mail..." + placeholder={$t('taskForm.assignPlaceholder')} + addLabel={$t('taskForm.assignLabel')} + searchPlaceholder={$t('taskForm.nameOrEmail')} isAvailable={form.contactsAvailable ?? false} />
- Beteiligte + {$t('taskForm.involved')} (form.involvedContacts = c)} onSearch={(q) => contactsStore.searchContacts(q)} allowManualEntry={false} - placeholder="Hinzufügen..." - addLabel="Hinzufügen" - searchPlaceholder="Name oder E-Mail..." + placeholder={$t('taskForm.addPlaceholder')} + addLabel={$t('taskForm.addLabel')} + searchPlaceholder={$t('taskForm.nameOrEmail')} isAvailable={form.contactsAvailable ?? false} />
@@ -242,13 +261,13 @@
- Storypoints + {$t('taskForm.storypoints')} (form.storyPoints = v)} />
- Dauer + {$t('taskForm.duration')} (form.effectiveDuration = v)} @@ -257,7 +276,8 @@
- Spaß{form.funRating !== null ? ` (${form.funRating})` : ''}{$t('taskForm.fun')}{form.funRating !== null ? ` (${form.funRating})` : ''} (form.funRating = v)} />
diff --git a/apps/todo/apps/web/src/lib/components/TaskFilters.svelte b/apps/todo/apps/web/src/lib/components/TaskFilters.svelte index 78e7b0326..2ef8ba549 100644 --- a/apps/todo/apps/web/src/lib/components/TaskFilters.svelte +++ b/apps/todo/apps/web/src/lib/components/TaskFilters.svelte @@ -7,6 +7,7 @@ const tagsCtx: { readonly value: Tag[] } = getContext('tags'); import type { SortBy, SortOrder } from '$lib/stores/view.svelte'; import { CaretDown, Check, CheckCircle, MagnifyingGlass, X } from '@manacore/shared-icons'; + import { t } from 'svelte-i18n'; interface Props { // Layout @@ -61,18 +62,38 @@ onToggleCompleted, }: Props = $props(); - const priorities: { value: TaskPriority; label: string; color: string; bgColor: string }[] = [ - { value: 'urgent', label: 'Dringend', color: '#ef4444', bgColor: 'bg-red-500' }, - { value: 'high', label: 'Hoch', color: '#f97316', bgColor: 'bg-orange-500' }, - { value: 'medium', label: 'Normal', color: '#eab308', bgColor: 'bg-yellow-500' }, - { value: 'low', label: 'Niedrig', color: '#3b82f6', bgColor: 'bg-blue-500' }, - ]; + let priorities = $derived([ + { + value: 'urgent' as TaskPriority, + label: $t('priority.urgent'), + color: '#ef4444', + bgColor: 'bg-red-500', + }, + { + value: 'high' as TaskPriority, + label: $t('priority.high'), + color: '#f97316', + bgColor: 'bg-orange-500', + }, + { + value: 'medium' as TaskPriority, + label: $t('priority.medium'), + color: '#eab308', + bgColor: 'bg-yellow-500', + }, + { + value: 'low' as TaskPriority, + label: $t('priority.low'), + color: '#3b82f6', + bgColor: 'bg-blue-500', + }, + ]); - const sortOptions: { id: SortBy; label: string }[] = [ - { id: 'dueDate', label: 'Datum' }, - { id: 'priority', label: 'Priorit.' }, - { id: 'title', label: 'Name' }, - ]; + let sortOptions = $derived([ + { id: 'dueDate' as SortBy, label: $t('filters.date') }, + { id: 'priority' as SortBy, label: $t('filters.priorityShort') }, + { id: 'title' as SortBy, label: $t('filters.name') }, + ]); // Dropdown states let showLabelsDropdown = $state(false); @@ -107,7 +128,7 @@ class="clear-filter-pill glass-pill" class:hidden={!hasActiveFilters} onclick={onClearFilters} - title="Filter löschen" + title={$t('filters.clearFilter')} disabled={!hasActiveFilters} > @@ -116,7 +137,11 @@ {#if showTags} - {#if tagsCtx.value.length > 0} @@ -163,7 +188,7 @@ class="sort-pill glass-pill" class:active={sortBy === option.id} onclick={() => onSortChange(option.id)} - title="Nach {option.label} sortieren" + title={$t('tags.sortBy', { values: { field: option.label } })} > {option.label} @@ -176,10 +201,10 @@ class="glass-pill" class:active={isCompletedVisible} onclick={onToggleCompleted} - title={isCompletedVisible ? 'Erledigte ausblenden' : 'Erledigte anzeigen'} + title={isCompletedVisible ? $t('filters.hideCompleted') : $t('filters.showCompleted')} > - Erledigt + {$t('nav.completed')} {/if}
@@ -200,7 +225,7 @@ type="text" value={searchQuery} oninput={(e) => onSearchChange(e.currentTarget.value)} - placeholder="Aufgaben suchen..." + placeholder={$t('filters.searchTasks')} class="w-full pl-10 pr-8 py-2 text-sm bg-background border border-border rounded-lg outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary placeholder:text-muted-foreground transition-all" /> {#if searchQuery} @@ -219,7 +244,7 @@ onclick={onClearFilters} > - Zurücksetzen + {$t('filters.resetFilters')} {/if}
@@ -230,7 +255,7 @@
Priorität{$t('task.priority')}
{#each priorities as priority} @@ -277,7 +302,7 @@ {/if}
{:else} - Auswählen + {$t('filters.select')} {/if} {#if tagsCtx.value.length === 0} -

Keine Tags vorhanden

+

+ {$t('filters.noTagsAvailable')} +

{:else}
{#each tagsCtx.value as label} diff --git a/apps/todo/apps/web/src/lib/components/TaskItem.svelte b/apps/todo/apps/web/src/lib/components/TaskItem.svelte index 095f1513c..18a2c0611 100644 --- a/apps/todo/apps/web/src/lib/components/TaskItem.svelte +++ b/apps/todo/apps/web/src/lib/components/TaskItem.svelte @@ -17,8 +17,11 @@ DurationPicker, FunRatingPicker, TagSelector, + ReminderSelector, } from './form'; import { PRIORITY_COLORS } from '$lib/constants/priority'; + import { TodoEvents } from '@manacore/shared-utils/analytics'; + import { t } from 'svelte-i18n'; interface Props { task: Task; @@ -124,6 +127,7 @@ form.funRating, form.assignee, form.involvedContacts, + form.reminderMinutes, ]; scheduleAutoSave(); }); @@ -208,6 +212,7 @@ try { const data = form.buildUpdateInput(task); onSave(data); + await form.persistReminder(task.id); } finally { form.isLoading = false; } @@ -228,6 +233,7 @@ function toggleSubtask(subtaskId: string) { if (!onSave) return; const subtasks = $state.snapshot(task.subtasks) ?? []; + const target = subtasks.find((s) => s.id === subtaskId); const updated = subtasks.map((s) => s.id === subtaskId ? { @@ -237,6 +243,7 @@ } : s ); + if (target && !target.isCompleted) TodoEvents.subtaskCompleted(); onSave({ subtasks: updated }); } @@ -343,11 +350,7 @@ /> {:else} - + {task.title} {/if} @@ -371,7 +374,12 @@ {#if task.metadata?.assignee || (task.metadata?.involvedContacts && task.metadata.involvedContacts.length > 0)}
{#if task.metadata?.assignee} -
+
0}
{#each task.metadata.involvedContacts.slice(0, 2) as contact} -
+
{/each} @@ -403,15 +414,15 @@ e.stopPropagation(); showCreatedDate = !showCreatedDate; }} - title="Klicken für Erstellungsdatum" + title={$t('taskForm.clickForCreatedDate')} > {#if showCreatedDate} - Erstellt + {$t('taskForm.created')} {format(new Date(task.createdAt), 'd. MMM yyyy', { locale: de })} {/if} - Erledigt + {$t('taskForm.completed')} {format(new Date(task.completedAt), 'd. MMM yyyy', { locale: de })} @@ -431,7 +442,7 @@ type="button" class="detail-btn" onclick={handleOpenModal} - title="Details öffnen" + title={$t('taskForm.openDetails')} tabindex="-1" > @@ -470,35 +481,37 @@
- +
- +
- +
- +
- +
- +
- + (form.priority = p)} />
- + {#each RECURRENCE_OPTIONS as option} @@ -568,33 +582,43 @@
+ +
+ + (form.reminderMinutes = v)} + disabled={!form.dueDate} + /> +
+
- + (form.assignee = contacts)} onSearch={(q) => contactsStore.searchContacts(q)} singleSelect={true} allowManualEntry={false} - placeholder="Person zuweisen..." - addLabel="Zuweisen" - searchPlaceholder="Name oder E-Mail..." + placeholder={$t('taskForm.assignPerson')} + addLabel={$t('taskForm.assignLabel')} + searchPlaceholder={$t('taskForm.nameOrEmail')} isAvailable={form.contactsAvailable ?? false} />
- + (form.involvedContacts = contacts)} onSearch={(q) => contactsStore.searchContacts(q)} allowManualEntry={false} - placeholder="Personen hinzufügen..." - addLabel="Person hinzufügen" - searchPlaceholder="Name oder E-Mail..." + placeholder={$t('taskForm.addPeoplePlaceholder')} + addLabel={$t('taskForm.addPersonLabel')} + searchPlaceholder={$t('taskForm.nameOrEmail')} isAvailable={form.contactsAvailable ?? false} />
@@ -602,18 +626,18 @@
- + (form.storyPoints = v)} />
- + (form.effectiveDuration = v)} />
- + (form.funRating = v)} />
@@ -626,7 +650,7 @@ onclick={handleDeleteClick} disabled={form.isLoading} > - {form.showDeleteConfirm ? 'Wirklich löschen?' : 'Löschen'} + {form.showDeleteConfirm ? $t('taskForm.confirmDelete') : $t('common.delete')}
diff --git a/apps/todo/apps/web/src/lib/components/TodoToolbar.svelte b/apps/todo/apps/web/src/lib/components/TodoToolbar.svelte index c84846e90..00770b36b 100644 --- a/apps/todo/apps/web/src/lib/components/TodoToolbar.svelte +++ b/apps/todo/apps/web/src/lib/components/TodoToolbar.svelte @@ -1,6 +1,7 @@