mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
refactor(todo): remove projects concept, unify views with Fokus/Übersicht/Matrix
- Remove LocalProject, projectId from all tasks, and project-related stores/queries
- Add 'fokus' layout mode to LocalBoardView; activeLayoutMode setting in settings store
- Build FokusLayout.svelte: scroll-snap paper sheets with cross-page DnD ('task-dnd')
- Inline column edit mode: ViewColumnHeader with color picker popup, rename, reorder, delete
- Add "Neues Board" placeholder as last column in all layout modes
- PillNav now state-based (Fokus/Übersicht/Matrix tabs) instead of route-based
- Unified filter strip: merge TagStrip + FilterStrip with "Tags:" and "Filter:" label pills
- Fix all 3 test files after project removal; 100/100 tests passing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d460c9ec07
commit
ed9672ef2b
30 changed files with 74 additions and 866 deletions
|
|
@ -1,10 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
import { viewStore } from '$lib/stores/view.svelte';
|
||||
import type { Project } from '@todo/shared';
|
||||
import { getActiveProjects, getProjectById } from '$lib/data/task-queries';
|
||||
import {
|
||||
parseMultiTaskInput,
|
||||
resolveTaskIds,
|
||||
|
|
@ -16,12 +13,11 @@
|
|||
import { labelCollection } from '$lib/data/local-store';
|
||||
import { todoSettings } from '$lib/stores/settings.svelte';
|
||||
|
||||
const projectsCtx: { readonly value: Project[] } = getContext('projects');
|
||||
import type { TaskPriority } from '@todo/shared';
|
||||
import { PRIORITY_OPTIONS } from '@todo/shared';
|
||||
import { format, addDays } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { CalendarBlank, Folder, Plus, Flag, ArrowRight } from '@manacore/shared-icons';
|
||||
import { CalendarBlank, Plus, Flag, ArrowRight } from '@manacore/shared-icons';
|
||||
|
||||
let inputValue = $state('');
|
||||
let isLoading = $state(false);
|
||||
|
|
@ -30,12 +26,10 @@
|
|||
// Task options (used as fallback when parser doesn't extract these)
|
||||
let selectedDate = $state<Date>(new Date());
|
||||
let selectedPriority = $state<TaskPriority>('medium');
|
||||
let selectedProjectId = $state<string | undefined>(undefined);
|
||||
|
||||
// Dropdown states
|
||||
let showDatePicker = $state(false);
|
||||
let showPriorityPicker = $state(false);
|
||||
let showProjectPicker = $state(false);
|
||||
|
||||
// Parser preview
|
||||
let parsePreview = $state('');
|
||||
|
|
@ -53,9 +47,6 @@
|
|||
|
||||
// Derived values
|
||||
let currentPriority = $derived(PRIORITY_OPTIONS.find((p) => p.value === selectedPriority)!);
|
||||
let selectedProject = $derived(
|
||||
selectedProjectId ? getProjectById(projectsCtx.value, selectedProjectId) : undefined
|
||||
);
|
||||
let dateLabel = $derived(() => {
|
||||
const today = new Date();
|
||||
if (selectedDate.toDateString() === today.toDateString()) return 'Heute';
|
||||
|
|
@ -106,7 +97,6 @@
|
|||
.filter((t) => t.isCompleted && t.completedAt)
|
||||
.map((t) => ({
|
||||
title: t.title,
|
||||
projectId: t.projectId,
|
||||
priority: t.priority,
|
||||
estimatedDuration: t.estimatedDuration,
|
||||
completedAt: t.completedAt,
|
||||
|
|
@ -116,7 +106,6 @@
|
|||
const estimate = estimateDuration(
|
||||
{
|
||||
title: parsed.title,
|
||||
projectId: selectedProjectId,
|
||||
priority: selectedPriority,
|
||||
},
|
||||
completed
|
||||
|
|
@ -131,11 +120,6 @@
|
|||
|
||||
onMount(() => {
|
||||
inputRef?.focus();
|
||||
|
||||
// Set project if in project view
|
||||
if (viewStore.currentView === 'project' && viewStore.currentProjectId) {
|
||||
selectedProjectId = viewStore.currentProjectId;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit(event: Event) {
|
||||
|
|
@ -147,14 +131,13 @@
|
|||
isLoading = true;
|
||||
|
||||
try {
|
||||
const projects = projectsCtx.value.map((p) => ({ id: p.id, name: p.name }));
|
||||
const allLabels = await labelCollection.getAll();
|
||||
const labels = allLabels.map((l) => ({ id: l.id, name: l.name }));
|
||||
|
||||
const parsedTasks = parseMultiTaskInput(text);
|
||||
|
||||
for (const parsed of parsedTasks) {
|
||||
const resolved = resolveTaskIds(parsed, projects, labels);
|
||||
const resolved = resolveTaskIds(parsed, labels);
|
||||
|
||||
// Duration: explicit from parser > auto-estimated > default (if enabled)
|
||||
const duration =
|
||||
|
|
@ -164,7 +147,6 @@
|
|||
|
||||
await tasksStore.createTask({
|
||||
title: resolved.title,
|
||||
projectId: resolved.projectId ?? selectedProjectId,
|
||||
dueDate: resolved.dueDate ?? selectedDate.toISOString(),
|
||||
priority: resolved.priority ?? selectedPriority,
|
||||
labelIds: resolved.labelIds.length > 0 ? resolved.labelIds : undefined,
|
||||
|
|
@ -186,9 +168,6 @@
|
|||
autoEstimatedDuration = null;
|
||||
selectedDate = new Date();
|
||||
selectedPriority = 'medium';
|
||||
if (viewStore.currentView !== 'project') {
|
||||
selectedProjectId = undefined;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create task:', error);
|
||||
} finally {
|
||||
|
|
@ -204,7 +183,6 @@
|
|||
inputValue = '';
|
||||
showDatePicker = false;
|
||||
showPriorityPicker = false;
|
||||
showProjectPicker = false;
|
||||
inputRef?.blur();
|
||||
}
|
||||
}
|
||||
|
|
@ -212,25 +190,16 @@
|
|||
function closeAllPickers() {
|
||||
showDatePicker = false;
|
||||
showPriorityPicker = false;
|
||||
showProjectPicker = false;
|
||||
}
|
||||
|
||||
function toggleDatePicker() {
|
||||
showDatePicker = !showDatePicker;
|
||||
showPriorityPicker = false;
|
||||
showProjectPicker = false;
|
||||
}
|
||||
|
||||
function togglePriorityPicker() {
|
||||
showPriorityPicker = !showPriorityPicker;
|
||||
showDatePicker = false;
|
||||
showProjectPicker = false;
|
||||
}
|
||||
|
||||
function toggleProjectPicker() {
|
||||
showProjectPicker = !showProjectPicker;
|
||||
showDatePicker = false;
|
||||
showPriorityPicker = false;
|
||||
}
|
||||
|
||||
function selectDate(date: Date) {
|
||||
|
|
@ -242,11 +211,6 @@
|
|||
selectedPriority = priority;
|
||||
showPriorityPicker = false;
|
||||
}
|
||||
|
||||
function selectProject(projectId: string | undefined) {
|
||||
selectedProjectId = projectId;
|
||||
showProjectPicker = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onclick={closeAllPickers} />
|
||||
|
|
@ -347,47 +311,6 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Project picker -->
|
||||
<div class="option-wrapper">
|
||||
<button
|
||||
type="button"
|
||||
class="option-btn"
|
||||
class:active={showProjectPicker}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleProjectPicker();
|
||||
}}
|
||||
title="Projekt"
|
||||
>
|
||||
<Folder size={20} class="option-icon" />
|
||||
</button>
|
||||
|
||||
{#if showProjectPicker}
|
||||
<div class="dropdown" onclick={(e) => e.stopPropagation()} role="menu">
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
class:selected={!selectedProjectId}
|
||||
onclick={() => selectProject(undefined)}
|
||||
>
|
||||
<span class="project-dot" style="background-color: #6b7280"></span>
|
||||
Kein Projekt
|
||||
</button>
|
||||
{#each getActiveProjects(projectsCtx.value) as project}
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
class:selected={selectedProjectId === project.id}
|
||||
onclick={() => selectProject(project.id)}
|
||||
>
|
||||
<span class="project-dot" style="background-color: {project.color}"></span>
|
||||
{project.name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="option-divider"></div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { Task, Subtask, UpdateTaskInput } from '@todo/shared';
|
||||
import { STATUS_OPTIONS, RECURRENCE_OPTIONS } from '@todo/shared';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Project } from '@todo/shared';
|
||||
import { getActiveProjects } from '$lib/data/task-queries';
|
||||
|
||||
const projectsCtx: { readonly value: Project[] } = getContext('projects');
|
||||
import { contactsStore } from '$lib/stores/contacts.svelte';
|
||||
import { useTaskForm } from '$lib/composables/useTaskForm.svelte';
|
||||
import SubtaskList from './SubtaskList.svelte';
|
||||
|
|
@ -198,19 +193,6 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Projekt -->
|
||||
<div class="form-section">
|
||||
<label class="form-label" for="task-project">Projekt</label>
|
||||
<select id="task-project" class="form-select" bind:value={form.projectId}>
|
||||
<option value={null}>Kein Projekt</option>
|
||||
{#each getActiveProjects(projectsCtx.value) as project}
|
||||
<option value={project.id}>
|
||||
{project.name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="form-section">
|
||||
<label class="form-label">Tags</label>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,6 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import type { TaskPriority } from '@todo/shared';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Project } from '@todo/shared';
|
||||
import { getActiveProjects } from '$lib/data/task-queries';
|
||||
|
||||
const projectsCtx: { readonly value: Project[] } = getContext('projects');
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
|
|
@ -18,13 +14,11 @@
|
|||
|
||||
// Filter state (owned by parent)
|
||||
selectedPriorities: TaskPriority[];
|
||||
selectedProjectId: string | null;
|
||||
selectedLabelIds: string[];
|
||||
searchQuery: string;
|
||||
|
||||
// Callbacks
|
||||
onPrioritiesChange: (priorities: TaskPriority[]) => void;
|
||||
onProjectChange: (projectId: string | null) => void;
|
||||
onLabelsChange: (labelIds: string[]) => void;
|
||||
onSearchChange: (query: string) => void;
|
||||
onClearFilters: () => void;
|
||||
|
|
@ -49,11 +43,9 @@
|
|||
let {
|
||||
variant,
|
||||
selectedPriorities,
|
||||
selectedProjectId,
|
||||
selectedLabelIds,
|
||||
searchQuery,
|
||||
onPrioritiesChange,
|
||||
onProjectChange,
|
||||
onLabelsChange,
|
||||
onSearchChange,
|
||||
onClearFilters,
|
||||
|
|
@ -86,10 +78,7 @@
|
|||
let showLabelsDropdown = $state(false);
|
||||
|
||||
let hasActiveFilters = $derived(
|
||||
selectedPriorities.length > 0 ||
|
||||
selectedProjectId !== null ||
|
||||
selectedLabelIds.length > 0 ||
|
||||
searchQuery.trim() !== ''
|
||||
selectedPriorities.length > 0 || selectedLabelIds.length > 0 || searchQuery.trim() !== ''
|
||||
);
|
||||
|
||||
function togglePriority(priority: TaskPriority) {
|
||||
|
|
@ -166,23 +155,6 @@
|
|||
</button>
|
||||
{/each}
|
||||
|
||||
<!-- Project Filter Pills -->
|
||||
{#if getActiveProjects(projectsCtx.value).length > 0}
|
||||
<span class="strip-divider"></span>
|
||||
{#each getActiveProjects(projectsCtx.value) as project (project.id)}
|
||||
<button
|
||||
class="project-pill glass-pill"
|
||||
class:selected={selectedProjectId === project.id}
|
||||
onclick={() => onProjectChange(selectedProjectId === project.id ? null : project.id)}
|
||||
title={project.name}
|
||||
style="--project-color: {project.color || '#6b7280'}"
|
||||
>
|
||||
<span class="project-dot"></span>
|
||||
<span class="pill-label">{project.name}</span>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<!-- Sort Pills -->
|
||||
{#if showSort && onSortChange}
|
||||
<span class="strip-divider"></span>
|
||||
|
|
@ -276,25 +248,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-6 w-px bg-border hidden sm:block"></div>
|
||||
|
||||
<!-- Project filter -->
|
||||
<div class="filter-group flex items-center gap-2">
|
||||
<span class="text-xs font-medium text-muted-foreground uppercase tracking-wide"
|
||||
>Projekt</span
|
||||
>
|
||||
<select
|
||||
class="px-3 py-1.5 text-sm bg-background border border-border rounded-lg outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all cursor-pointer"
|
||||
value={selectedProjectId || ''}
|
||||
onchange={(e) => onProjectChange(e.currentTarget.value || null)}
|
||||
>
|
||||
<option value="">Alle Projekte</option>
|
||||
{#each getActiveProjects(projectsCtx.value) as project}
|
||||
<option value={project.id}>{project.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Labels filter -->
|
||||
{#if showLabels}
|
||||
<div class="h-6 w-px bg-border hidden sm:block"></div>
|
||||
|
|
@ -538,28 +491,6 @@
|
|||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Project pills */
|
||||
.project-pill .project-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--project-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.project-pill.selected {
|
||||
background: var(--project-color) !important;
|
||||
border-color: var(--project-color) !important;
|
||||
}
|
||||
|
||||
.project-pill.selected .project-dot {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.project-pill.selected .pill-label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Priority pills */
|
||||
.priority-pill.selected {
|
||||
background: var(--priority-color) !important;
|
||||
|
|
|
|||
|
|
@ -6,11 +6,6 @@
|
|||
import { formatDueDate } from '$lib/utils/date-display';
|
||||
import { getSubtaskProgress } from '$lib/utils/task-helpers';
|
||||
import { useTaskForm } from '$lib/composables/useTaskForm.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Project } from '@todo/shared';
|
||||
import { getActiveProjects, getProjectColor } from '$lib/data/task-queries';
|
||||
|
||||
const projectsCtx: { readonly value: Project[] } = getContext('projects');
|
||||
import { contactsStore } from '$lib/stores/contacts.svelte';
|
||||
import { ContactAvatar, ContactSelector } from '@manacore/shared-ui';
|
||||
import SubtaskList from './SubtaskList.svelte';
|
||||
|
|
@ -99,7 +94,6 @@
|
|||
form.startDate,
|
||||
form.priority,
|
||||
form.status,
|
||||
form.projectId,
|
||||
form.selectedLabelIds,
|
||||
form.subtasks,
|
||||
form.recurrenceRule,
|
||||
|
|
@ -229,12 +223,6 @@
|
|||
return isPast(date) && !isToday(date);
|
||||
});
|
||||
|
||||
// Get project color
|
||||
let projectColor = $derived(() => {
|
||||
if (!task.projectId) return null;
|
||||
return getProjectColor(projectsCtx.value, task.projectId);
|
||||
});
|
||||
|
||||
// Subtasks progress
|
||||
let subtaskProgress = $derived(() => getSubtaskProgress(task.subtasks));
|
||||
|
||||
|
|
@ -479,25 +467,14 @@
|
|||
<PrioritySelector value={form.priority} onChange={(p) => (form.priority = p)} />
|
||||
</div>
|
||||
|
||||
<!-- Status & Project row -->
|
||||
<div class="form-row-2">
|
||||
<div class="form-section">
|
||||
<label class="form-label" for="task-status-{task.id}">Status</label>
|
||||
<select id="task-status-{task.id}" class="form-select" bind:value={form.status}>
|
||||
{#each STATUS_OPTIONS as s}
|
||||
<option value={s.value}>{s.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-section">
|
||||
<label class="form-label" for="task-project-{task.id}">Projekt</label>
|
||||
<select id="task-project-{task.id}" class="form-select" bind:value={form.projectId}>
|
||||
<option value={null}>Kein Projekt</option>
|
||||
{#each getActiveProjects(projectsCtx.value) as project}
|
||||
<option value={project.id}>{project.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<!-- Status -->
|
||||
<div class="form-section">
|
||||
<label class="form-label" for="task-status-{task.id}">Status</label>
|
||||
<select id="task-status-{task.id}" class="form-select" bind:value={form.status}>
|
||||
{#each STATUS_OPTIONS as s}
|
||||
<option value={s.value}>{s.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
|
|
|
|||
|
|
@ -4,10 +4,6 @@
|
|||
import TaskItem from './TaskItem.svelte';
|
||||
import { getContext, untrack } from 'svelte';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
import type { Project } from '@todo/shared';
|
||||
import { getActiveProjects } from '$lib/data/task-queries';
|
||||
|
||||
const projectsCtx: { readonly value: Project[] } = getContext('projects');
|
||||
import { ContextMenu, type ContextMenuItem } from '@manacore/shared-ui';
|
||||
|
||||
// Context menu state
|
||||
|
|
@ -67,27 +63,7 @@
|
|||
},
|
||||
];
|
||||
|
||||
// Add project move options if there are projects
|
||||
const projects = getActiveProjects(projectsCtx.value);
|
||||
if (projects.length > 0) {
|
||||
items.push({ id: 'divider-2', label: '', type: 'divider' });
|
||||
items.push({
|
||||
id: 'move-inbox',
|
||||
label: 'In Inbox verschieben',
|
||||
action: () => tasksStore.moveTask(task.id, null),
|
||||
disabled: !task.projectId,
|
||||
});
|
||||
for (const project of projects) {
|
||||
items.push({
|
||||
id: `move-${project.id}`,
|
||||
label: project.name,
|
||||
action: () => tasksStore.moveTask(task.id, project.id),
|
||||
disabled: task.projectId === project.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
items.push({ id: 'divider-3', label: '', type: 'divider' });
|
||||
items.push({ id: 'divider-2', label: '', type: 'divider' });
|
||||
items.push({
|
||||
id: 'delete',
|
||||
label: 'Löschen',
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { TaskPriority } from '@todo/shared';
|
||||
import { PRIORITY_OPTIONS } from '@todo/shared';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Project } from '@todo/shared';
|
||||
import { getActiveProjects } from '$lib/data/task-queries';
|
||||
|
||||
const projectsCtx: { readonly value: Project[] } = getContext('projects');
|
||||
import { viewStore, type SortBy } from '$lib/stores/view.svelte';
|
||||
import { todoSettings } from '$lib/stores/settings.svelte';
|
||||
import { PillToolbarButton, PillToolbarDivider, PillViewSwitcher } from '@manacore/shared-ui';
|
||||
|
|
@ -35,7 +30,6 @@
|
|||
// Filter dropdown states
|
||||
let showFilterDropdown = $state(false);
|
||||
let selectedPriorityFilters = $state<TaskPriority[]>([]);
|
||||
let selectedProjectFilter = $state<string | null>(null);
|
||||
let selectedLabelFilters = $state<string[]>([]);
|
||||
|
||||
const priorities: { value: TaskPriority; label: string; color: string }[] = [
|
||||
|
|
@ -53,9 +47,7 @@
|
|||
];
|
||||
|
||||
// Count active filters
|
||||
let activeFilterCount = $derived(
|
||||
selectedPriorityFilters.length + (selectedProjectFilter ? 1 : 0) + selectedLabelFilters.length
|
||||
);
|
||||
let activeFilterCount = $derived(selectedPriorityFilters.length + selectedLabelFilters.length);
|
||||
|
||||
function handleSortChange(value: string) {
|
||||
onSortChange(value as SortBy);
|
||||
|
|
@ -75,7 +67,6 @@
|
|||
|
||||
function clearAllFilters() {
|
||||
selectedPriorityFilters = [];
|
||||
selectedProjectFilter = null;
|
||||
selectedLabelFilters = [];
|
||||
}
|
||||
</script>
|
||||
|
|
@ -133,20 +124,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-section">
|
||||
<div class="filter-section-header">Projekt</div>
|
||||
<select
|
||||
class="filter-select"
|
||||
value={selectedProjectFilter || ''}
|
||||
onchange={(e) => (selectedProjectFilter = e.currentTarget.value || null)}
|
||||
>
|
||||
<option value="">Alle Projekte</option>
|
||||
{#each getActiveProjects(projectsCtx.value) as project}
|
||||
<option value={project.id}>{project.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{#if activeFilterCount > 0}
|
||||
<button type="button" class="clear-filters-btn" onclick={clearAllFilters}>
|
||||
Filter zurücksetzen
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import type { Task, Project } from '@todo/shared';
|
||||
import type { Task } from '@todo/shared';
|
||||
import type { LocalBoardView } from '$lib/data/local-store';
|
||||
import { groupTasksByView, getDropActionUpdate } from '$lib/data/view-grouping';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
|
|
@ -30,12 +30,11 @@
|
|||
|
||||
let activeLayout = $derived(layoutOverride || view.layout);
|
||||
|
||||
// Get tasks and projects from context (set by layout)
|
||||
// Get tasks from context (set by layout)
|
||||
const tasksCtx: { readonly value: Task[] } = getContext('tasks');
|
||||
const projectsCtx: { readonly value: Project[] } = getContext('projects');
|
||||
|
||||
// Group tasks by the current view configuration
|
||||
let columns = $derived(groupTasksByView(view, tasksCtx.value, projectsCtx.value));
|
||||
let columns = $derived(groupTasksByView(view, tasksCtx.value));
|
||||
|
||||
// ─── Task Callbacks ──────────────────────────────────────
|
||||
|
||||
|
|
@ -65,7 +64,6 @@
|
|||
const updateData: Record<string, unknown> = {};
|
||||
if (data.title !== undefined) updateData.title = data.title;
|
||||
if (data.description !== undefined) updateData.description = data.description;
|
||||
if (data.projectId !== undefined) updateData.projectId = data.projectId;
|
||||
if (data.dueDate !== undefined) {
|
||||
updateData.dueDate = data.dueDate instanceof Date ? data.dueDate.toISOString() : data.dueDate;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,13 +102,10 @@
|
|||
const createData: Record<string, unknown> = { title };
|
||||
if (column.onDrop) {
|
||||
if (column.onDrop.setPriority) createData.priority = column.onDrop.setPriority;
|
||||
if (column.onDrop.setProjectId !== undefined)
|
||||
createData.projectId = column.onDrop.setProjectId;
|
||||
}
|
||||
tasksStore.createTask(
|
||||
createData as {
|
||||
title: string;
|
||||
projectId?: string;
|
||||
priority?: 'low' | 'medium' | 'high' | 'urgent';
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@
|
|||
const updateData: Record<string, unknown> = {};
|
||||
if (data.title !== undefined) updateData.title = data.title;
|
||||
if (data.description !== undefined) updateData.description = data.description;
|
||||
if (data.projectId !== undefined) updateData.projectId = data.projectId;
|
||||
if (data.dueDate !== undefined) {
|
||||
updateData.dueDate = data.dueDate instanceof Date ? data.dueDate.toISOString() : data.dueDate;
|
||||
}
|
||||
|
|
@ -104,14 +103,10 @@
|
|||
if (column.onDrop.setPriority) {
|
||||
createData.priority = column.onDrop.setPriority;
|
||||
}
|
||||
if (column.onDrop.setProjectId !== undefined) {
|
||||
createData.projectId = column.onDrop.setProjectId;
|
||||
}
|
||||
}
|
||||
await tasksStore.createTask(
|
||||
createData as {
|
||||
title: string;
|
||||
projectId?: string;
|
||||
priority?: 'low' | 'medium' | 'high' | 'urgent';
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@
|
|||
const groupByOptions = [
|
||||
{ value: 'status', label: 'Status' },
|
||||
{ value: 'priority', label: 'Priorität' },
|
||||
{ value: 'project', label: 'Projekt' },
|
||||
{ value: 'dueDate', label: 'Fälligkeit' },
|
||||
{ value: 'tag', label: 'Tag' },
|
||||
{ value: 'custom', label: 'Manuell' },
|
||||
|
|
@ -181,8 +180,6 @@
|
|||
onDrop: { setPriority: 'low' },
|
||||
},
|
||||
];
|
||||
case 'project':
|
||||
return []; // dynamically generated
|
||||
case 'dueDate':
|
||||
return [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ export function useTaskForm() {
|
|||
let startDate = $state('');
|
||||
let priority = $state<TaskPriority>('medium');
|
||||
let status = $state<TaskStatus>('pending');
|
||||
let projectId = $state<string | null>(null);
|
||||
let selectedLabelIds = $state<string[]>([]);
|
||||
let subtasks = $state<Subtask[]>([]);
|
||||
let recurrenceRule = $state('');
|
||||
|
|
@ -51,7 +50,6 @@ export function useTaskForm() {
|
|||
startDate = task.startDate ? format(new Date(task.startDate), 'yyyy-MM-dd') : '';
|
||||
priority = task.priority || 'medium';
|
||||
status = task.status || 'pending';
|
||||
projectId = task.projectId || null;
|
||||
selectedLabelIds = task.labels?.map((l) => l.id) || [];
|
||||
subtasks = task.subtasks ? [...task.subtasks] : [];
|
||||
recurrenceRule = task.recurrenceRule || '';
|
||||
|
|
@ -97,7 +95,6 @@ export function useTaskForm() {
|
|||
startDate: startDate ? new Date(startDate).toISOString() : null,
|
||||
priority,
|
||||
status,
|
||||
projectId: projectId || null,
|
||||
subtasks: subtasks.length > 0 ? subtasks : null,
|
||||
recurrenceRule: recurrenceRule || null,
|
||||
metadata: {
|
||||
|
|
@ -157,12 +154,6 @@ export function useTaskForm() {
|
|||
set status(v: TaskStatus) {
|
||||
status = v;
|
||||
},
|
||||
get projectId() {
|
||||
return projectId;
|
||||
},
|
||||
set projectId(v: string | null) {
|
||||
projectId = v;
|
||||
},
|
||||
get selectedLabelIds() {
|
||||
return selectedLabelIds;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ export function getTodoHelpContent(locale: string): HelpContent {
|
|||
id: 'faq-quick-add',
|
||||
question: isDE ? 'Wie funktioniert die Schnelleingabe?' : 'How does quick add work?',
|
||||
answer: isDE
|
||||
? '<p>Die Schnelleingabe erkennt automatisch verschiedene Muster:</p><ul><li><strong>Priorität</strong>: <code>!!!</code> (dringend), <code>!!</code> (hoch), <code>!</code> (mittel)</li><li><strong>Projekt</strong>: <code>@Projektname</code></li><li><strong>Labels</strong>: <code>#label1 #label2</code></li><li><strong>Datum</strong>: heute, morgen, nächsten Montag, 15.12.</li><li><strong>Wiederholung</strong>: täglich, wöchentlich, monatlich</li><li><strong>Unteraufgaben</strong>: <code>Titel: Item1, Item2, Item3</code></li></ul><p>Beispiel: <code>Einkaufen: Milch, Brot morgen !! @Privat #wichtig</code></p>'
|
||||
: '<p>Quick add automatically recognizes various patterns:</p><ul><li><strong>Priority</strong>: <code>!!!</code> (urgent), <code>!!</code> (high), <code>!</code> (medium)</li><li><strong>Project</strong>: <code>@ProjectName</code></li><li><strong>Labels</strong>: <code>#label1 #label2</code></li><li><strong>Date</strong>: today, tomorrow, next Monday</li><li><strong>Recurrence</strong>: daily, weekly, monthly</li><li><strong>Subtasks</strong>: <code>Title: Item1, Item2, Item3</code></li></ul><p>Example: <code>Shopping: Milk, Bread tomorrow !! @Personal #important</code></p>',
|
||||
? '<p>Die Schnelleingabe erkennt automatisch verschiedene Muster:</p><ul><li><strong>Priorität</strong>: <code>!!!</code> (dringend), <code>!!</code> (hoch), <code>!</code> (mittel)</li><li><strong>Labels</strong>: <code>#label1 #label2</code></li><li><strong>Datum</strong>: heute, morgen, nächsten Montag, 15.12.</li><li><strong>Wiederholung</strong>: täglich, wöchentlich, monatlich</li><li><strong>Unteraufgaben</strong>: <code>Titel: Item1, Item2, Item3</code></li></ul><p>Beispiel: <code>Einkaufen: Milch, Brot morgen !! #wichtig</code></p>'
|
||||
: '<p>Quick add automatically recognizes various patterns:</p><ul><li><strong>Priority</strong>: <code>!!!</code> (urgent), <code>!!</code> (high), <code>!</code> (medium)</li><li><strong>Labels</strong>: <code>#label1 #label2</code></li><li><strong>Date</strong>: today, tomorrow, next Monday</li><li><strong>Recurrence</strong>: daily, weekly, monthly</li><li><strong>Subtasks</strong>: <code>Title: Item1, Item2, Item3</code></li></ul><p>Example: <code>Shopping: Milk, Bread tomorrow !! #important</code></p>',
|
||||
category: 'features',
|
||||
order: 1,
|
||||
language: isDE ? 'de' : 'en',
|
||||
|
|
@ -23,19 +23,17 @@ export function getTodoHelpContent(locale: string): HelpContent {
|
|||
tags: isDE ? ['schnelleingabe', 'erstellen', 'syntax'] : ['quick-add', 'create', 'syntax'],
|
||||
},
|
||||
{
|
||||
id: 'faq-projects',
|
||||
id: 'faq-tags',
|
||||
question: isDE
|
||||
? 'Wie organisiere ich Aufgaben in Projekten?'
|
||||
: 'How do I organize tasks in projects?',
|
||||
? 'Wie organisiere ich Aufgaben mit Tags?'
|
||||
: 'How do I organize tasks with tags?',
|
||||
answer: isDE
|
||||
? '<p>Projekte helfen dir, Aufgaben thematisch zu gruppieren:</p><ul><li>Erstelle Projekte über das <strong>+</strong> Symbol in der Seitenleiste</li><li>Weise jeder Aufgabe ein Projekt zu (oder lasse sie im Posteingang)</li><li>Jedes Projekt hat eine eigene Farbe zur visuellen Unterscheidung</li><li>Ziehe Aufgaben per Drag & Drop zwischen Projekten</li></ul>'
|
||||
: '<p>Projects help you group tasks by topic:</p><ul><li>Create projects via the <strong>+</strong> icon in the sidebar</li><li>Assign tasks to a project (or leave them in the inbox)</li><li>Each project has its own color for visual distinction</li><li>Drag and drop tasks between projects</li></ul>',
|
||||
? '<p>Tags helfen dir, Aufgaben flexibel zu organisieren:</p><ul><li>Erstelle Tags in der Tags-Ansicht</li><li>Weise Aufgaben beliebig viele Tags zu</li><li>Jeder Tag hat eine eigene Farbe zur visuellen Unterscheidung</li><li>Filtere Aufgaben über die Filterleiste nach Tags</li></ul>'
|
||||
: '<p>Tags help you organize tasks flexibly:</p><ul><li>Create tags in the Tags view</li><li>Assign any number of tags to tasks</li><li>Each tag has its own color for visual distinction</li><li>Filter tasks by tags using the filter strip</li></ul>',
|
||||
category: 'features',
|
||||
order: 2,
|
||||
language: isDE ? 'de' : 'en',
|
||||
tags: isDE
|
||||
? ['projekte', 'organisation', 'sortierung']
|
||||
: ['projects', 'organize', 'sorting'],
|
||||
tags: isDE ? ['tags', 'organisation', 'sortierung'] : ['tags', 'organize', 'sorting'],
|
||||
},
|
||||
{
|
||||
id: 'faq-kanban',
|
||||
|
|
@ -73,8 +71,8 @@ export function getTodoHelpContent(locale: string): HelpContent {
|
|||
icon: '⚡',
|
||||
category: 'core',
|
||||
highlights: isDE
|
||||
? ['Automatische Erkennung', 'Datum & Priorität', 'Projekte & Labels']
|
||||
: ['Auto-detection', 'Date & priority', 'Projects & labels'],
|
||||
? ['Automatische Erkennung', 'Datum & Priorität', 'Tags & Labels']
|
||||
: ['Auto-detection', 'Date & priority', 'Tags & labels'],
|
||||
content: '',
|
||||
order: 1,
|
||||
language: isDE ? 'de' : 'en',
|
||||
|
|
@ -95,16 +93,16 @@ export function getTodoHelpContent(locale: string): HelpContent {
|
|||
language: isDE ? 'de' : 'en',
|
||||
},
|
||||
{
|
||||
id: 'feature-projects',
|
||||
title: isDE ? 'Projekte' : 'Projects',
|
||||
id: 'feature-tags',
|
||||
title: 'Tags',
|
||||
description: isDE
|
||||
? 'Organisiere Aufgaben in farbcodierten Projekten'
|
||||
: 'Organize tasks in color-coded projects',
|
||||
icon: '📁',
|
||||
? 'Organisiere Aufgaben mit farbcodierten Tags'
|
||||
: 'Organize tasks with color-coded tags',
|
||||
icon: '🏷️',
|
||||
category: 'core',
|
||||
highlights: isDE
|
||||
? ['Farbcodierung', 'Drag & Drop', 'Archivierung']
|
||||
: ['Color coding', 'Drag & drop', 'Archiving'],
|
||||
? ['Farbcodierung', 'Mehrere Tags pro Aufgabe', 'Filterbar']
|
||||
: ['Color coding', 'Multiple tags per task', 'Filterable'],
|
||||
content: '',
|
||||
order: 3,
|
||||
language: isDE ? 'de' : 'en',
|
||||
|
|
|
|||
|
|
@ -5,31 +5,7 @@
|
|||
* They serve as onboarding content that teaches the user how the app works.
|
||||
*/
|
||||
|
||||
import type { LocalTask, LocalProject, LocalLabel, LocalBoardView } from './local-store';
|
||||
|
||||
const ONBOARDING_PROJECT_ID = 'onboarding-project';
|
||||
const PERSONAL_PROJECT_ID = 'personal-project';
|
||||
|
||||
export const guestProjects: LocalProject[] = [
|
||||
{
|
||||
id: ONBOARDING_PROJECT_ID,
|
||||
name: 'Erste Schritte',
|
||||
color: '#3b82f6',
|
||||
icon: 'sparkle',
|
||||
order: 0,
|
||||
isArchived: false,
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
id: PERSONAL_PROJECT_ID,
|
||||
name: 'Persönlich',
|
||||
color: '#10b981',
|
||||
icon: 'home',
|
||||
order: 1,
|
||||
isArchived: false,
|
||||
isDefault: true,
|
||||
},
|
||||
];
|
||||
import type { LocalTask, LocalLabel, LocalBoardView } from './local-store';
|
||||
|
||||
export const guestLabels: LocalLabel[] = [
|
||||
{
|
||||
|
|
@ -147,22 +123,13 @@ export const guestBoardViews: LocalBoardView[] = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'view-project',
|
||||
name: 'Projekte',
|
||||
icon: 'folders',
|
||||
groupBy: 'project',
|
||||
layout: 'kanban',
|
||||
order: 3,
|
||||
columns: [], // dynamically generated from projects
|
||||
},
|
||||
{
|
||||
id: 'view-due',
|
||||
name: 'Fälligkeit',
|
||||
icon: 'calendar',
|
||||
groupBy: 'dueDate',
|
||||
layout: 'kanban',
|
||||
order: 4,
|
||||
order: 3,
|
||||
columns: [
|
||||
{
|
||||
id: 'col-due-overdue',
|
||||
|
|
@ -219,7 +186,6 @@ export const guestTasks: LocalTask[] = [
|
|||
title: 'Willkommen bei Todo! Tippe hier, um diese Aufgabe zu bearbeiten ✏️',
|
||||
description:
|
||||
'Du kannst Titel, Beschreibung, Priorität und Fälligkeitsdatum ändern. Probiere es aus!',
|
||||
projectId: ONBOARDING_PROJECT_ID,
|
||||
priority: 'medium',
|
||||
isCompleted: false,
|
||||
order: 0,
|
||||
|
|
@ -232,7 +198,6 @@ export const guestTasks: LocalTask[] = [
|
|||
{
|
||||
id: 'onboard-2',
|
||||
title: 'Klicke den Kreis links, um diese Aufgabe abzuschließen ✓',
|
||||
projectId: ONBOARDING_PROJECT_ID,
|
||||
priority: 'low',
|
||||
isCompleted: false,
|
||||
order: 1,
|
||||
|
|
@ -240,7 +205,6 @@ export const guestTasks: LocalTask[] = [
|
|||
{
|
||||
id: 'onboard-3',
|
||||
title: 'Erstelle eine neue Aufgabe mit dem + Button oben',
|
||||
projectId: ONBOARDING_PROJECT_ID,
|
||||
priority: 'medium',
|
||||
isCompleted: false,
|
||||
order: 2,
|
||||
|
|
@ -248,7 +212,6 @@ export const guestTasks: LocalTask[] = [
|
|||
{
|
||||
id: 'onboard-4',
|
||||
title: 'Wechsle zur Board-Ansicht über die Navigation',
|
||||
projectId: ONBOARDING_PROJECT_ID,
|
||||
priority: 'low',
|
||||
isCompleted: false,
|
||||
order: 3,
|
||||
|
|
@ -258,7 +221,6 @@ export const guestTasks: LocalTask[] = [
|
|||
title: 'Melde dich an, um deine Aufgaben auf allen Geräten zu synchronisieren',
|
||||
description:
|
||||
'Ohne Anmeldung werden deine Daten nur in diesem Browser gespeichert. Mit einem Account synchronisieren wir sie automatisch.',
|
||||
projectId: ONBOARDING_PROJECT_ID,
|
||||
priority: 'high',
|
||||
isCompleted: false,
|
||||
order: 4,
|
||||
|
|
@ -269,7 +231,6 @@ export const guestTasks: LocalTask[] = [
|
|||
id: 'sample-1',
|
||||
title: 'Einkaufen gehen',
|
||||
description: 'Milch, Brot, Obst',
|
||||
projectId: PERSONAL_PROJECT_ID,
|
||||
priority: 'medium',
|
||||
isCompleted: false,
|
||||
dueDate: tomorrow.toISOString(),
|
||||
|
|
@ -283,7 +244,6 @@ export const guestTasks: LocalTask[] = [
|
|||
{
|
||||
id: 'sample-2',
|
||||
title: 'Wohnung aufräumen',
|
||||
projectId: PERSONAL_PROJECT_ID,
|
||||
priority: 'low',
|
||||
isCompleted: false,
|
||||
dueDate: nextWeek.toISOString(),
|
||||
|
|
|
|||
|
|
@ -7,14 +7,13 @@
|
|||
|
||||
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
|
||||
import type { Subtask as SharedSubtask } from '@todo/shared';
|
||||
import { guestProjects, guestTasks, guestLabels, guestBoardViews } from './guest-seed.js';
|
||||
import { guestTasks, guestLabels, guestBoardViews } from './guest-seed.js';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────
|
||||
|
||||
export interface LocalTask extends BaseRecord {
|
||||
title: string;
|
||||
description?: string;
|
||||
projectId?: string | null;
|
||||
userId?: string;
|
||||
priority: 'low' | 'medium' | 'high' | 'urgent';
|
||||
isCompleted: boolean;
|
||||
|
|
@ -31,16 +30,6 @@ export interface LocalTask extends BaseRecord {
|
|||
|
||||
export type { SharedSubtask as Subtask };
|
||||
|
||||
export interface LocalProject extends BaseRecord {
|
||||
name: string;
|
||||
color: string;
|
||||
icon?: string | null;
|
||||
userId?: string;
|
||||
order: number;
|
||||
isArchived: boolean;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export interface LocalLabel extends BaseRecord {
|
||||
name: string;
|
||||
color: string;
|
||||
|
|
@ -63,7 +52,7 @@ export interface LocalReminder extends BaseRecord {
|
|||
// ─── Board Views ────────────────────────────────────────────
|
||||
|
||||
export interface TaskMatcher {
|
||||
type: 'status' | 'priority' | 'project' | 'tag' | 'dueDate' | 'custom';
|
||||
type: 'status' | 'priority' | 'tag' | 'dueDate' | 'custom';
|
||||
value?: string | null;
|
||||
/** For 'custom' groupBy: manually assigned task IDs */
|
||||
taskIds?: string[];
|
||||
|
|
@ -72,7 +61,6 @@ export interface TaskMatcher {
|
|||
export interface DropAction {
|
||||
setCompleted?: boolean;
|
||||
setPriority?: 'low' | 'medium' | 'high' | 'urgent';
|
||||
setProjectId?: string | null;
|
||||
}
|
||||
|
||||
export interface ViewColumn {
|
||||
|
|
@ -84,7 +72,6 @@ export interface ViewColumn {
|
|||
}
|
||||
|
||||
export interface ViewFilter {
|
||||
projectId?: string;
|
||||
tagIds?: string[];
|
||||
priorities?: string[];
|
||||
}
|
||||
|
|
@ -92,7 +79,7 @@ export interface ViewFilter {
|
|||
export interface LocalBoardView extends BaseRecord {
|
||||
name: string;
|
||||
icon: string;
|
||||
groupBy: 'status' | 'priority' | 'project' | 'dueDate' | 'tag' | 'custom';
|
||||
groupBy: 'status' | 'priority' | 'dueDate' | 'tag' | 'custom';
|
||||
columns: ViewColumn[];
|
||||
filter?: ViewFilter;
|
||||
layout: 'kanban' | 'grid' | 'fokus';
|
||||
|
|
@ -108,22 +95,9 @@ export const todoStore = createLocalStore({
|
|||
collections: [
|
||||
{
|
||||
name: 'tasks',
|
||||
indexes: [
|
||||
'projectId',
|
||||
'dueDate',
|
||||
'isCompleted',
|
||||
'priority',
|
||||
'order',
|
||||
'[isCompleted+order]',
|
||||
'[projectId+order]',
|
||||
],
|
||||
indexes: ['dueDate', 'isCompleted', 'priority', 'order', '[isCompleted+order]'],
|
||||
guestSeed: guestTasks,
|
||||
},
|
||||
{
|
||||
name: 'projects',
|
||||
indexes: ['order', 'isArchived'],
|
||||
guestSeed: guestProjects,
|
||||
},
|
||||
{
|
||||
name: 'labels',
|
||||
indexes: [],
|
||||
|
|
@ -150,7 +124,6 @@ export const todoStore = createLocalStore({
|
|||
|
||||
// Typed collection accessors
|
||||
export const taskCollection = todoStore.collection<LocalTask>('tasks');
|
||||
export const projectCollection = todoStore.collection<LocalProject>('projects');
|
||||
export const labelCollection = todoStore.collection<LocalLabel>('labels');
|
||||
export const taskLabelCollection = todoStore.collection<LocalTaskLabel>('taskLabels');
|
||||
export const reminderCollection = todoStore.collection<LocalReminder>('reminders');
|
||||
|
|
|
|||
|
|
@ -9,13 +9,11 @@
|
|||
import { useLiveQueryWithDefault } from '@manacore/local-store/svelte';
|
||||
import {
|
||||
taskCollection,
|
||||
projectCollection,
|
||||
boardViewCollection,
|
||||
type LocalTask,
|
||||
type LocalProject,
|
||||
type LocalBoardView,
|
||||
} from './local-store';
|
||||
import type { Task, Project } from '@todo/shared';
|
||||
import type { Task } from '@todo/shared';
|
||||
import { isToday, isPast, isFuture, startOfDay, addDays } from 'date-fns';
|
||||
|
||||
// ─── Type Converters ───────────────────────────────────────
|
||||
|
|
@ -23,7 +21,6 @@ import { isToday, isPast, isFuture, startOfDay, addDays } from 'date-fns';
|
|||
export function toTask(local: LocalTask): Task {
|
||||
return {
|
||||
id: local.id,
|
||||
projectId: local.projectId,
|
||||
userId: local.userId ?? 'guest',
|
||||
title: local.title,
|
||||
description: local.description,
|
||||
|
|
@ -44,21 +41,6 @@ export function toTask(local: LocalTask): Task {
|
|||
};
|
||||
}
|
||||
|
||||
export function toProject(local: LocalProject): Project {
|
||||
return {
|
||||
id: local.id,
|
||||
userId: local.userId ?? 'guest',
|
||||
name: local.name,
|
||||
color: local.color,
|
||||
icon: local.icon,
|
||||
order: local.order,
|
||||
isArchived: local.isArchived,
|
||||
isDefault: local.isDefault,
|
||||
createdAt: local.createdAt ?? new Date().toISOString(),
|
||||
updatedAt: local.updatedAt ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Live Query Hooks (call during component init) ─────────
|
||||
|
||||
/** All tasks, sorted by order. Auto-updates on any change. */
|
||||
|
|
@ -72,17 +54,6 @@ export function useAllTasks() {
|
|||
}, [] as Task[]);
|
||||
}
|
||||
|
||||
/** All projects, sorted by order. Auto-updates on any change. */
|
||||
export function useAllProjects() {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await projectCollection.getAll(undefined, {
|
||||
sortBy: 'order',
|
||||
sortDirection: 'asc',
|
||||
});
|
||||
return locals.map(toProject);
|
||||
}, [] as Project[]);
|
||||
}
|
||||
|
||||
/** All board views, sorted by order. Auto-updates on any change. */
|
||||
export function useAllBoardViews() {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
|
|
@ -132,36 +103,6 @@ export function filterUpcoming(tasks: Task[]): Task[] {
|
|||
});
|
||||
}
|
||||
|
||||
export function filterByProject(tasks: Task[], projectId: string | null): Task[] {
|
||||
if (projectId === null) {
|
||||
return tasks.filter((t) => !t.projectId);
|
||||
}
|
||||
return tasks.filter((t) => t.projectId === projectId);
|
||||
}
|
||||
|
||||
export function filterByLabel(tasks: Task[], labelId: string): Task[] {
|
||||
return tasks.filter((t) => t.labels?.some((l) => l.id === labelId));
|
||||
}
|
||||
|
||||
// ─── Pure Project Helpers ──────────────────────────────────
|
||||
|
||||
export function getActiveProjects(projects: Project[]): Project[] {
|
||||
return projects.filter((p) => !p.isArchived).sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
export function getArchivedProjects(projects: Project[]): Project[] {
|
||||
return projects.filter((p) => p.isArchived);
|
||||
}
|
||||
|
||||
export function getInboxProject(projects: Project[]): Project | undefined {
|
||||
return projects.find((p) => p.isDefault);
|
||||
}
|
||||
|
||||
export function getProjectById(projects: Project[], id: string): Project | undefined {
|
||||
return projects.find((p) => p.id === id);
|
||||
}
|
||||
|
||||
export function getProjectColor(projects: Project[], projectId: string): string {
|
||||
const project = projects.find((p) => p.id === projectId);
|
||||
return project?.color || '#6b7280';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* No side effects, no store dependencies — easy to test.
|
||||
*/
|
||||
|
||||
import type { Task, Project } from '@todo/shared';
|
||||
import type { Task } from '@todo/shared';
|
||||
import type { LocalBoardView, ViewColumn, DropAction } from './local-store';
|
||||
import { isToday, isPast, isTomorrow, startOfDay, addDays, isFuture } from 'date-fns';
|
||||
|
||||
|
|
@ -21,11 +21,7 @@ export interface GroupedColumn {
|
|||
|
||||
// ─── Main Grouping Function ────────────────────────────────
|
||||
|
||||
export function groupTasksByView(
|
||||
view: LocalBoardView,
|
||||
tasks: Task[],
|
||||
projects: Project[]
|
||||
): GroupedColumn[] {
|
||||
export function groupTasksByView(view: LocalBoardView, tasks: Task[]): GroupedColumn[] {
|
||||
// Only group incomplete tasks (unless status view includes completed)
|
||||
const activeTasks = view.groupBy === 'status' ? tasks : tasks.filter((t) => !t.isCompleted);
|
||||
|
||||
|
|
@ -37,8 +33,6 @@ export function groupTasksByView(
|
|||
return groupByStatus(filtered, view.columns);
|
||||
case 'priority':
|
||||
return groupByPriority(filtered, view.columns);
|
||||
case 'project':
|
||||
return groupByProject(filtered, view.columns, projects);
|
||||
case 'dueDate':
|
||||
return groupByDueDate(filtered, view.columns);
|
||||
case 'tag':
|
||||
|
|
@ -76,44 +70,6 @@ function groupByPriority(tasks: Task[], columns: ViewColumn[]): GroupedColumn[]
|
|||
}));
|
||||
}
|
||||
|
||||
function groupByProject(
|
||||
tasks: Task[],
|
||||
columns: ViewColumn[],
|
||||
projects: Project[]
|
||||
): GroupedColumn[] {
|
||||
// Dynamic: generate columns from projects
|
||||
if (columns.length === 0) {
|
||||
const activeProjects = projects.filter((p) => !p.isArchived);
|
||||
const dynamicColumns: GroupedColumn[] = [
|
||||
{
|
||||
id: 'col-inbox',
|
||||
name: 'Inbox',
|
||||
color: '#6B7280',
|
||||
tasks: tasks.filter((t) => !t.projectId),
|
||||
onDrop: { setProjectId: null },
|
||||
},
|
||||
...activeProjects.map((p) => ({
|
||||
id: `col-proj-${p.id}`,
|
||||
name: p.name,
|
||||
color: p.color,
|
||||
tasks: tasks.filter((t) => t.projectId === p.id),
|
||||
onDrop: { setProjectId: p.id } as DropAction,
|
||||
})),
|
||||
];
|
||||
return dynamicColumns;
|
||||
}
|
||||
// Static columns from config
|
||||
return columns.map((col) => ({
|
||||
id: col.id,
|
||||
name: col.name,
|
||||
color: col.color,
|
||||
onDrop: col.onDrop,
|
||||
tasks: tasks.filter((t) =>
|
||||
col.match.value === null ? !t.projectId : t.projectId === col.match.value
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
function groupByDueDate(tasks: Task[], columns: ViewColumn[]): GroupedColumn[] {
|
||||
const today = startOfDay(new Date());
|
||||
const tomorrowDate = addDays(today, 1);
|
||||
|
|
@ -152,9 +108,7 @@ function groupByTag(tasks: Task[], columns: ViewColumn[]): GroupedColumn[] {
|
|||
name: col.name,
|
||||
color: col.color,
|
||||
onDrop: col.onDrop,
|
||||
tasks: tasks.filter(
|
||||
(t) => t.labels?.some((l) => l.id === col.match.value) ?? false
|
||||
),
|
||||
tasks: tasks.filter((t) => t.labels?.some((l) => l.id === col.match.value) ?? false),
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -236,13 +190,13 @@ function groupEisenhower(tasks: Task[], columns: ViewColumn[]): GroupedColumn[]
|
|||
|
||||
// ─── Helpers ───────────────────────────────────────────────
|
||||
|
||||
function applyViewFilter(tasks: Task[], filter?: { projectId?: string; tagIds?: string[]; priorities?: string[] }): Task[] {
|
||||
function applyViewFilter(
|
||||
tasks: Task[],
|
||||
filter?: { tagIds?: string[]; priorities?: string[] }
|
||||
): Task[] {
|
||||
if (!filter) return tasks;
|
||||
let result = tasks;
|
||||
|
||||
if (filter.projectId) {
|
||||
result = result.filter((t) => t.projectId === filter.projectId);
|
||||
}
|
||||
if (filter.priorities && filter.priorities.length > 0) {
|
||||
result = result.filter((t) => filter.priorities!.includes(t.priority));
|
||||
}
|
||||
|
|
@ -263,6 +217,5 @@ export function getDropActionUpdate(action: DropAction): Record<string, unknown>
|
|||
update.completedAt = action.setCompleted ? new Date().toISOString() : null;
|
||||
}
|
||||
if (action.setPriority) update.priority = action.setPriority;
|
||||
if (action.setProjectId !== undefined) update.projectId = action.setProjectId;
|
||||
return update;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const todoOnboardingSteps: AppOnboardingStep[] = [
|
|||
{
|
||||
id: 'normal',
|
||||
label: 'Normal',
|
||||
description: 'Mit Projekt, Labels und Fälligkeit',
|
||||
description: 'Mit Tags, Labels und Fälligkeit',
|
||||
emoji: '📝',
|
||||
},
|
||||
{
|
||||
|
|
@ -74,10 +74,10 @@ const todoOnboardingSteps: AppOnboardingStep[] = [
|
|||
emoji: '🎉',
|
||||
gradient: { from: 'primary', to: 'primary/70' },
|
||||
bullets: [
|
||||
'Schnelleingabe: "Meeting morgen 14 Uhr !hoch @Arbeit #wichtig"',
|
||||
'Nutze @Projekt und #Label direkt beim Erstellen',
|
||||
'Schnelleingabe: "Meeting morgen 14 Uhr !hoch #wichtig"',
|
||||
'Nutze #Tags direkt beim Erstellen',
|
||||
'Drücke "F" für den Fokus-Modus ohne Ablenkungen',
|
||||
'Erstelle Projekte, um Aufgaben zu organisieren',
|
||||
'Nutze Tags, um Aufgaben zu organisieren',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
export { authStore } from './auth.svelte';
|
||||
export { projectsStore } from './projects.svelte';
|
||||
export { tasksStore } from './tasks.svelte';
|
||||
export { viewStore } from './view.svelte';
|
||||
export type { ViewType, SortBy, SortOrder } from './view.svelte';
|
||||
|
|
|
|||
|
|
@ -1,103 +1 @@
|
|||
/**
|
||||
* Projects Store — Mutation-Only Service
|
||||
*
|
||||
* All reads are handled by useLiveQuery() hooks in task-queries.ts.
|
||||
* This store only provides write operations (create, update, delete, etc.).
|
||||
* IndexedDB writes automatically trigger UI updates via Dexie liveQuery.
|
||||
*/
|
||||
|
||||
import type { Project } from '@todo/shared';
|
||||
import { projectCollection, type LocalProject } from '$lib/data/local-store';
|
||||
import { toProject } from '$lib/data/task-queries';
|
||||
import { TodoEvents } from '@manacore/shared-utils/analytics';
|
||||
import { withErrorHandling } from './store-helpers';
|
||||
|
||||
let error = $state<string | null>(null);
|
||||
const setError = (e: string | null) => (error = e);
|
||||
|
||||
export const projectsStore = {
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
|
||||
async createProject(data: { name: string; description?: string; color?: string; icon?: string }) {
|
||||
return withErrorHandling(
|
||||
setError,
|
||||
async () => {
|
||||
const count = await projectCollection.count();
|
||||
const newLocal: LocalProject = {
|
||||
id: crypto.randomUUID(),
|
||||
name: data.name,
|
||||
color: data.color ?? '#6b7280',
|
||||
icon: data.icon ?? null,
|
||||
order: count,
|
||||
isArchived: false,
|
||||
isDefault: false,
|
||||
};
|
||||
|
||||
const inserted = await projectCollection.insert(newLocal);
|
||||
TodoEvents.projectCreated();
|
||||
return toProject(inserted);
|
||||
},
|
||||
'Failed to create project'
|
||||
);
|
||||
},
|
||||
|
||||
async updateProject(
|
||||
id: string,
|
||||
data: { name?: string; description?: string; color?: string; icon?: string }
|
||||
) {
|
||||
return withErrorHandling(
|
||||
setError,
|
||||
async () => {
|
||||
const updated = await projectCollection.update(id, data as Partial<LocalProject>);
|
||||
if (updated) {
|
||||
return toProject(updated);
|
||||
}
|
||||
},
|
||||
'Failed to update project'
|
||||
);
|
||||
},
|
||||
|
||||
async deleteProject(id: string) {
|
||||
return withErrorHandling(
|
||||
setError,
|
||||
async () => {
|
||||
await projectCollection.delete(id);
|
||||
TodoEvents.projectDeleted();
|
||||
},
|
||||
'Failed to delete project'
|
||||
);
|
||||
},
|
||||
|
||||
async archiveProject(id: string) {
|
||||
return withErrorHandling(
|
||||
setError,
|
||||
async () => {
|
||||
const updated = await projectCollection.update(id, {
|
||||
isArchived: true,
|
||||
} as Partial<LocalProject>);
|
||||
if (updated) {
|
||||
return toProject(updated);
|
||||
}
|
||||
},
|
||||
'Failed to archive project'
|
||||
);
|
||||
},
|
||||
|
||||
async reorderProjects(projectIds: string[]) {
|
||||
return withErrorHandling(
|
||||
setError,
|
||||
async () => {
|
||||
for (let i = 0; i < projectIds.length; i++) {
|
||||
await projectCollection.update(projectIds[i], { order: i } as Partial<LocalProject>);
|
||||
}
|
||||
},
|
||||
'Failed to reorder projects'
|
||||
);
|
||||
},
|
||||
|
||||
get guestInboxId() {
|
||||
return 'personal-project';
|
||||
},
|
||||
};
|
||||
export {};
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ export const tasksStore = {
|
|||
async createTask(data: {
|
||||
title: string;
|
||||
description?: string;
|
||||
projectId?: string;
|
||||
dueDate?: string;
|
||||
priority?: TaskPriority;
|
||||
labelIds?: string[];
|
||||
|
|
@ -39,7 +38,6 @@ export const tasksStore = {
|
|||
id: crypto.randomUUID(),
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
projectId: data.projectId ?? null,
|
||||
priority: data.priority ?? 'medium',
|
||||
isCompleted: false,
|
||||
dueDate: data.dueDate ?? null,
|
||||
|
|
@ -62,7 +60,6 @@ export const tasksStore = {
|
|||
data: {
|
||||
title?: string;
|
||||
description?: string | null;
|
||||
projectId?: string | null;
|
||||
parentTaskId?: string | null;
|
||||
dueDate?: string | null;
|
||||
dueTime?: string | null;
|
||||
|
|
@ -152,19 +149,6 @@ export const tasksStore = {
|
|||
);
|
||||
},
|
||||
|
||||
async moveTask(id: string, projectId: string | null) {
|
||||
return withErrorHandling(
|
||||
setError,
|
||||
async () => {
|
||||
const updated = await taskCollection.update(id, { projectId } as Partial<LocalTask>);
|
||||
if (updated) {
|
||||
return toTask(updated);
|
||||
}
|
||||
},
|
||||
'Failed to move task'
|
||||
);
|
||||
},
|
||||
|
||||
async updateLabels(id: string, labelIds: string[]) {
|
||||
return withErrorHandling(
|
||||
setError,
|
||||
|
|
|
|||
|
|
@ -4,20 +4,12 @@
|
|||
|
||||
import type { TaskPriority } from '@todo/shared';
|
||||
|
||||
export type ViewType =
|
||||
| 'inbox'
|
||||
| 'today'
|
||||
| 'upcoming'
|
||||
| 'project'
|
||||
| 'label'
|
||||
| 'completed'
|
||||
| 'search';
|
||||
export type ViewType = 'inbox' | 'today' | 'upcoming' | 'label' | 'completed' | 'search';
|
||||
export type SortBy = 'dueDate' | 'priority' | 'title' | 'createdAt' | 'order';
|
||||
export type SortOrder = 'asc' | 'desc';
|
||||
|
||||
// State
|
||||
let currentView = $state<ViewType>('inbox');
|
||||
let currentProjectId = $state<string | null>(null);
|
||||
let currentLabelId = $state<string | null>(null);
|
||||
let searchQuery = $state('');
|
||||
let sortBy = $state<SortBy>('order');
|
||||
|
|
@ -26,7 +18,6 @@ let showCompleted = $state(false);
|
|||
|
||||
// Filter state (used by TaskFilters strip in list view)
|
||||
let filterPriorities = $state<TaskPriority[]>([]);
|
||||
let filterProjectId = $state<string | null>(null);
|
||||
let filterLabelIds = $state<string[]>([]);
|
||||
let filterSearchQuery = $state('');
|
||||
|
||||
|
|
@ -35,9 +26,6 @@ export const viewStore = {
|
|||
get currentView() {
|
||||
return currentView;
|
||||
},
|
||||
get currentProjectId() {
|
||||
return currentProjectId;
|
||||
},
|
||||
get currentLabelId() {
|
||||
return currentLabelId;
|
||||
},
|
||||
|
|
@ -56,9 +44,6 @@ export const viewStore = {
|
|||
get filterPriorities() {
|
||||
return filterPriorities;
|
||||
},
|
||||
get filterProjectId() {
|
||||
return filterProjectId;
|
||||
},
|
||||
get filterLabelIds() {
|
||||
return filterLabelIds;
|
||||
},
|
||||
|
|
@ -71,7 +56,6 @@ export const viewStore = {
|
|||
*/
|
||||
setInbox() {
|
||||
currentView = 'inbox';
|
||||
currentProjectId = null;
|
||||
currentLabelId = null;
|
||||
searchQuery = '';
|
||||
},
|
||||
|
|
@ -81,7 +65,6 @@ export const viewStore = {
|
|||
*/
|
||||
setToday() {
|
||||
currentView = 'today';
|
||||
currentProjectId = null;
|
||||
currentLabelId = null;
|
||||
searchQuery = '';
|
||||
},
|
||||
|
|
@ -91,17 +74,6 @@ export const viewStore = {
|
|||
*/
|
||||
setUpcoming() {
|
||||
currentView = 'upcoming';
|
||||
currentProjectId = null;
|
||||
currentLabelId = null;
|
||||
searchQuery = '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Set current view to a specific project
|
||||
*/
|
||||
setProject(projectId: string) {
|
||||
currentView = 'project';
|
||||
currentProjectId = projectId;
|
||||
currentLabelId = null;
|
||||
searchQuery = '';
|
||||
},
|
||||
|
|
@ -111,7 +83,6 @@ export const viewStore = {
|
|||
*/
|
||||
setLabel(labelId: string) {
|
||||
currentView = 'label';
|
||||
currentProjectId = null;
|
||||
currentLabelId = labelId;
|
||||
searchQuery = '';
|
||||
},
|
||||
|
|
@ -121,7 +92,6 @@ export const viewStore = {
|
|||
*/
|
||||
setCompleted() {
|
||||
currentView = 'completed';
|
||||
currentProjectId = null;
|
||||
currentLabelId = null;
|
||||
searchQuery = '';
|
||||
},
|
||||
|
|
@ -131,7 +101,6 @@ export const viewStore = {
|
|||
*/
|
||||
setSearch(query: string) {
|
||||
currentView = 'search';
|
||||
currentProjectId = null;
|
||||
currentLabelId = null;
|
||||
searchQuery = query;
|
||||
},
|
||||
|
|
@ -172,13 +141,6 @@ export const viewStore = {
|
|||
filterPriorities = priorities;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set filter project
|
||||
*/
|
||||
setFilterProjectId(id: string | null) {
|
||||
filterProjectId = id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set filter label IDs
|
||||
*/
|
||||
|
|
@ -198,7 +160,6 @@ export const viewStore = {
|
|||
*/
|
||||
clearFilters() {
|
||||
filterPriorities = [];
|
||||
filterProjectId = null;
|
||||
filterLabelIds = [];
|
||||
filterSearchQuery = '';
|
||||
},
|
||||
|
|
@ -208,14 +169,12 @@ export const viewStore = {
|
|||
*/
|
||||
reset() {
|
||||
currentView = 'inbox';
|
||||
currentProjectId = null;
|
||||
currentLabelId = null;
|
||||
searchQuery = '';
|
||||
sortBy = 'order';
|
||||
sortOrder = 'asc';
|
||||
showCompleted = false;
|
||||
filterPriorities = [];
|
||||
filterProjectId = null;
|
||||
filterLabelIds = [];
|
||||
filterSearchQuery = '';
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,10 +12,6 @@ describe('viewStore filter methods', () => {
|
|||
expect(viewStore.filterPriorities).toEqual([]);
|
||||
});
|
||||
|
||||
it('has null filter project', () => {
|
||||
expect(viewStore.filterProjectId).toBeNull();
|
||||
});
|
||||
|
||||
it('has empty filter labels', () => {
|
||||
expect(viewStore.filterLabelIds).toEqual([]);
|
||||
});
|
||||
|
|
@ -39,19 +35,6 @@ describe('viewStore filter methods', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('setFilterProjectId', () => {
|
||||
it('sets project ID', () => {
|
||||
viewStore.setFilterProjectId('proj-1');
|
||||
expect(viewStore.filterProjectId).toBe('proj-1');
|
||||
});
|
||||
|
||||
it('can clear to null', () => {
|
||||
viewStore.setFilterProjectId('proj-1');
|
||||
viewStore.setFilterProjectId(null);
|
||||
expect(viewStore.filterProjectId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setFilterLabelIds', () => {
|
||||
it('sets label IDs', () => {
|
||||
viewStore.setFilterLabelIds(['label-1', 'label-2']);
|
||||
|
|
@ -70,14 +53,12 @@ describe('viewStore filter methods', () => {
|
|||
describe('clearFilters', () => {
|
||||
it('resets all filter state', () => {
|
||||
viewStore.setFilterPriorities(['urgent']);
|
||||
viewStore.setFilterProjectId('proj-1');
|
||||
viewStore.setFilterLabelIds(['label-1']);
|
||||
viewStore.setFilterSearchQuery('test');
|
||||
|
||||
viewStore.clearFilters();
|
||||
|
||||
expect(viewStore.filterPriorities).toEqual([]);
|
||||
expect(viewStore.filterProjectId).toBeNull();
|
||||
expect(viewStore.filterLabelIds).toEqual([]);
|
||||
expect(viewStore.filterSearchQuery).toBe('');
|
||||
});
|
||||
|
|
@ -99,13 +80,11 @@ describe('viewStore filter methods', () => {
|
|||
describe('reset', () => {
|
||||
it('resets filter state along with everything else', () => {
|
||||
viewStore.setFilterPriorities(['urgent']);
|
||||
viewStore.setFilterProjectId('proj-1');
|
||||
viewStore.setSort('title', 'desc');
|
||||
|
||||
viewStore.reset();
|
||||
|
||||
expect(viewStore.filterPriorities).toEqual([]);
|
||||
expect(viewStore.filterProjectId).toBeNull();
|
||||
expect(viewStore.sortBy).toBe('order');
|
||||
expect(viewStore.sortOrder).toBe('asc');
|
||||
expect(viewStore.currentView).toBe('inbox');
|
||||
|
|
|
|||
|
|
@ -34,33 +34,30 @@ function makeTask(overrides: Partial<Task> = {}): Task {
|
|||
|
||||
const emptyFilters: TaskFilterCriteria = {
|
||||
priorities: [],
|
||||
projectId: null,
|
||||
labelIds: [],
|
||||
searchQuery: '',
|
||||
};
|
||||
|
||||
describe('applyTaskFilters', () => {
|
||||
const tasks: Task[] = [
|
||||
makeTask({ id: '1', title: 'Buy groceries', priority: 'low', projectId: 'proj-a' }),
|
||||
makeTask({ id: '1', title: 'Buy groceries', priority: 'low' }),
|
||||
makeTask({
|
||||
id: '2',
|
||||
title: 'Urgent meeting',
|
||||
priority: 'urgent',
|
||||
projectId: 'proj-b',
|
||||
labels: [makeLabel({ id: 'label-1', name: 'Work', color: '#f00' })],
|
||||
}),
|
||||
makeTask({
|
||||
id: '3',
|
||||
title: 'Write report',
|
||||
priority: 'high',
|
||||
projectId: 'proj-a',
|
||||
description: 'Quarterly financial report',
|
||||
labels: [
|
||||
makeLabel({ id: 'label-1', name: 'Work', color: '#f00' }),
|
||||
makeLabel({ id: 'label-2', name: 'Important', color: '#0f0' }),
|
||||
],
|
||||
}),
|
||||
makeTask({ id: '4', title: 'Relax', priority: 'low', projectId: null }),
|
||||
makeTask({ id: '4', title: 'Relax', priority: 'low' }),
|
||||
];
|
||||
|
||||
it('returns all tasks when no filters are active', () => {
|
||||
|
|
@ -96,26 +93,6 @@ describe('applyTaskFilters', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// Project filtering
|
||||
describe('project filter', () => {
|
||||
it('filters by project ID', () => {
|
||||
const result = applyTaskFilters(tasks, { ...emptyFilters, projectId: 'proj-a' });
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((t) => t.id).sort()).toEqual(['1', '3']);
|
||||
});
|
||||
|
||||
it('does not match tasks with null projectId when filtering', () => {
|
||||
const result = applyTaskFilters(tasks, { ...emptyFilters, projectId: 'proj-b' });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('2');
|
||||
});
|
||||
|
||||
it('skips project filter when null', () => {
|
||||
const result = applyTaskFilters(tasks, { ...emptyFilters, projectId: null });
|
||||
expect(result).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
// Label filtering
|
||||
describe('label filter', () => {
|
||||
it('filters by single label', () => {
|
||||
|
|
@ -140,7 +117,6 @@ describe('applyTaskFilters', () => {
|
|||
|
||||
it('excludes tasks with no labels', () => {
|
||||
const result = applyTaskFilters(tasks, { ...emptyFilters, labelIds: ['label-1'] });
|
||||
// Tasks 1 and 4 have no labels
|
||||
expect(result.find((t) => t.id === '1')).toBeUndefined();
|
||||
expect(result.find((t) => t.id === '4')).toBeUndefined();
|
||||
});
|
||||
|
|
@ -180,16 +156,6 @@ describe('applyTaskFilters', () => {
|
|||
|
||||
// Combined filters
|
||||
describe('combined filters', () => {
|
||||
it('applies priority + project filter together (AND)', () => {
|
||||
const result = applyTaskFilters(tasks, {
|
||||
...emptyFilters,
|
||||
priorities: ['low'],
|
||||
projectId: 'proj-a',
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('1');
|
||||
});
|
||||
|
||||
it('applies priority + label filter together', () => {
|
||||
const result = applyTaskFilters(tasks, {
|
||||
...emptyFilters,
|
||||
|
|
@ -203,7 +169,6 @@ describe('applyTaskFilters', () => {
|
|||
it('applies all filters together', () => {
|
||||
const result = applyTaskFilters(tasks, {
|
||||
priorities: ['high'],
|
||||
projectId: 'proj-a',
|
||||
labelIds: ['label-1'],
|
||||
searchQuery: 'report',
|
||||
});
|
||||
|
|
@ -213,9 +178,8 @@ describe('applyTaskFilters', () => {
|
|||
|
||||
it('returns empty when combined filters contradict', () => {
|
||||
const result = applyTaskFilters(tasks, {
|
||||
priorities: ['urgent'],
|
||||
projectId: 'proj-a', // task 2 is urgent but in proj-b
|
||||
labelIds: [],
|
||||
priorities: ['low'],
|
||||
labelIds: ['label-1'], // tasks 1,4 are low but have no label-1
|
||||
searchQuery: '',
|
||||
});
|
||||
expect(result).toHaveLength(0);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import type { Task, TaskPriority } from '@todo/shared';
|
|||
|
||||
export interface TaskFilterCriteria {
|
||||
priorities: TaskPriority[];
|
||||
projectId: string | null;
|
||||
labelIds: string[];
|
||||
searchQuery: string;
|
||||
}
|
||||
|
|
@ -18,10 +17,6 @@ export function applyTaskFilters(tasks: Task[], filters: TaskFilterCriteria): Ta
|
|||
filtered = filtered.filter((t) => filters.priorities.includes(t.priority));
|
||||
}
|
||||
|
||||
if (filters.projectId) {
|
||||
filtered = filtered.filter((t) => t.projectId === filters.projectId);
|
||||
}
|
||||
|
||||
if (filters.labelIds.length > 0) {
|
||||
filtered = filtered.filter((t) => t.labels?.some((l) => filters.labelIds.includes(l.id)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ describe('parseTaskInput', () => {
|
|||
const result = parseTaskInput('Einkaufen gehen');
|
||||
expect(result.title).toBe('Einkaufen gehen');
|
||||
expect(result.priority).toBeUndefined();
|
||||
expect(result.projectName).toBeUndefined();
|
||||
expect(result.labelNames).toEqual([]);
|
||||
});
|
||||
|
||||
|
|
@ -48,12 +47,6 @@ describe('parseTaskInput', () => {
|
|||
expect(result.priority).toBe('low');
|
||||
});
|
||||
|
||||
it('should parse @project', () => {
|
||||
const result = parseTaskInput('Task erledigen @Arbeit');
|
||||
expect(result.projectName).toBe('Arbeit');
|
||||
expect(result.title).not.toContain('@Arbeit');
|
||||
});
|
||||
|
||||
it('should parse #labels', () => {
|
||||
const result = parseTaskInput('Anrufen #arbeit #privat');
|
||||
expect(result.labelNames).toEqual(['arbeit', 'privat']);
|
||||
|
|
@ -61,9 +54,8 @@ describe('parseTaskInput', () => {
|
|||
});
|
||||
|
||||
it('should parse complex input with all fields', () => {
|
||||
const result = parseTaskInput('Meeting vorbereiten !!! @Arbeit #wichtig #team');
|
||||
const result = parseTaskInput('Meeting vorbereiten !!! #wichtig #team');
|
||||
expect(result.priority).toBe('urgent');
|
||||
expect(result.projectName).toBe('Arbeit');
|
||||
expect(result.labelNames).toEqual(['wichtig', 'team']);
|
||||
expect(result.title).toContain('Meeting vorbereiten');
|
||||
});
|
||||
|
|
@ -92,9 +84,8 @@ describe('parseTaskInput', () => {
|
|||
});
|
||||
|
||||
it('should parse recurrence "wöchentlich"', () => {
|
||||
const result = parseTaskInput('Review wöchentlich @Arbeit');
|
||||
const result = parseTaskInput('Review wöchentlich');
|
||||
expect(result.recurrenceRule).toBe('FREQ=WEEKLY');
|
||||
expect(result.projectName).toBe('Arbeit');
|
||||
});
|
||||
|
||||
it('should have no recurrence for normal input', () => {
|
||||
|
|
@ -120,57 +111,38 @@ describe('parseTaskInput', () => {
|
|||
});
|
||||
|
||||
it('should parse subtasks with other fields', () => {
|
||||
const result = parseTaskInput('Einkaufen: Milch, Brot morgen !! @Privat');
|
||||
const result = parseTaskInput('Einkaufen: Milch, Brot morgen !!');
|
||||
expect(result.title).toBe('Einkaufen');
|
||||
expect(result.subtasks).toEqual(['Milch', 'Brot']);
|
||||
expect(result.priority).toBe('high');
|
||||
expect(result.projectName).toBe('Privat');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveTaskIds', () => {
|
||||
const projects = [
|
||||
{ id: 'proj-1', name: 'Arbeit' },
|
||||
{ id: 'proj-2', name: 'Privat' },
|
||||
];
|
||||
|
||||
const labels = [
|
||||
{ id: 'label-1', name: 'Wichtig' },
|
||||
{ id: 'label-2', name: 'Team' },
|
||||
{ id: 'label-3', name: 'Bug' },
|
||||
];
|
||||
|
||||
it('should resolve project name to ID (case-insensitive)', () => {
|
||||
const parsed = parseTaskInput('Task @arbeit');
|
||||
const resolved = resolveTaskIds(parsed, projects, labels);
|
||||
expect(resolved.projectId).toBe('proj-1');
|
||||
});
|
||||
|
||||
it('should resolve label names to IDs (case-insensitive)', () => {
|
||||
// Note: "wichtig" is consumed by priority extraction, so use "bug" instead
|
||||
const parsed = parseTaskInput('Task #bug #team');
|
||||
const resolved = resolveTaskIds(parsed, projects, labels);
|
||||
const resolved = resolveTaskIds(parsed, labels);
|
||||
expect(resolved.labelIds).toEqual(['label-3', 'label-2']);
|
||||
});
|
||||
|
||||
it('should skip unknown project', () => {
|
||||
const parsed = parseTaskInput('Task @Unbekannt');
|
||||
const resolved = resolveTaskIds(parsed, projects, labels);
|
||||
expect(resolved.projectId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should skip unknown labels', () => {
|
||||
const parsed = parseTaskInput('Task #nichtda');
|
||||
const resolved = resolveTaskIds(parsed, projects, labels);
|
||||
const resolved = resolveTaskIds(parsed, labels);
|
||||
expect(resolved.labelIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('should preserve title and priority', () => {
|
||||
const parsed = parseTaskInput('Meeting vorbereiten !!! @Arbeit #wichtig');
|
||||
const resolved = resolveTaskIds(parsed, projects, labels);
|
||||
const parsed = parseTaskInput('Meeting vorbereiten !!! #wichtig');
|
||||
const resolved = resolveTaskIds(parsed, labels);
|
||||
expect(resolved.title).toContain('Meeting vorbereiten');
|
||||
expect(resolved.priority).toBe('urgent');
|
||||
expect(resolved.projectId).toBe('proj-1');
|
||||
expect(resolved.labelIds).toEqual(['label-1']);
|
||||
});
|
||||
});
|
||||
|
|
@ -182,12 +154,6 @@ describe('formatParsedTaskPreview', () => {
|
|||
expect(preview).toContain('Dringend');
|
||||
});
|
||||
|
||||
it('should format project', () => {
|
||||
const parsed = parseTaskInput('Task @Arbeit');
|
||||
const preview = formatParsedTaskPreview(parsed);
|
||||
expect(preview).toContain('Arbeit');
|
||||
});
|
||||
|
||||
it('should format labels', () => {
|
||||
const parsed = parseTaskInput('Task #arbeit #team');
|
||||
const preview = formatParsedTaskPreview(parsed);
|
||||
|
|
@ -201,9 +167,9 @@ describe('formatParsedTaskPreview', () => {
|
|||
});
|
||||
|
||||
it('should join parts with separator', () => {
|
||||
const parsed = parseTaskInput('Task !!! @Arbeit');
|
||||
const parsed = parseTaskInput('Task !!!');
|
||||
const preview = formatParsedTaskPreview(parsed);
|
||||
expect(preview).toContain(' · ');
|
||||
expect(preview).not.toBe('');
|
||||
});
|
||||
|
||||
it('should format duration in preview', () => {
|
||||
|
|
@ -248,10 +214,9 @@ describe('duration extraction', () => {
|
|||
});
|
||||
|
||||
it('should work with other fields', () => {
|
||||
const result = parseTaskInput('Meeting 2h morgen !! @Arbeit');
|
||||
const result = parseTaskInput('Meeting 2h morgen !!');
|
||||
expect(result.estimatedDuration).toBe(120);
|
||||
expect(result.priority).toBe('high');
|
||||
expect(result.projectName).toBe('Arbeit');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -293,13 +258,6 @@ describe('parseMultiTaskInput', () => {
|
|||
expect(tasks[0].dueDate!.toDateString()).toBe(tasks[1].dueDate!.toDateString());
|
||||
});
|
||||
|
||||
it('should inherit project context from first task', () => {
|
||||
const tasks = parseMultiTaskInput('Meeting @Arbeit danach Report schreiben');
|
||||
expect(tasks).toHaveLength(2);
|
||||
expect(tasks[0].projectName).toBe('Arbeit');
|
||||
expect(tasks[1].projectName).toBe('Arbeit');
|
||||
});
|
||||
|
||||
it('should offset time when first task has duration', () => {
|
||||
const tasks = parseMultiTaskInput('Meeting 14 Uhr 1h danach Notizen');
|
||||
expect(tasks).toHaveLength(2);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import {
|
||||
parseBaseInput,
|
||||
extractAtReference,
|
||||
extractRecurrence,
|
||||
combineDateAndTime,
|
||||
formatDatePreview,
|
||||
|
|
@ -22,18 +21,12 @@ export interface ParsedTask {
|
|||
dueDate?: Date;
|
||||
dueTime?: string; // HH:mm format
|
||||
priority?: TaskPriority;
|
||||
projectName?: string;
|
||||
labelNames: string[];
|
||||
recurrenceRule?: string;
|
||||
subtasks?: string[];
|
||||
estimatedDuration?: number; // in minutes
|
||||
}
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Label {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -44,7 +37,6 @@ export interface ParsedTaskWithIds {
|
|||
dueDate?: string;
|
||||
dueTime?: string;
|
||||
priority?: TaskPriority;
|
||||
projectId?: string;
|
||||
labelIds: string[];
|
||||
recurrenceRule?: string;
|
||||
subtasks?: string[];
|
||||
|
|
@ -152,7 +144,6 @@ export function parseMultiTaskInput(input: string, locale: ParserLocale = 'de'):
|
|||
const results: ParsedTask[] = [];
|
||||
let contextDate: Date | undefined;
|
||||
let contextTime: string | undefined;
|
||||
let contextProject: string | undefined;
|
||||
let lastEndMinutes: number | undefined; // track end time for "danach" offset
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
|
|
@ -162,7 +153,6 @@ export function parseMultiTaskInput(input: string, locale: ParserLocale = 'de'):
|
|||
// First task sets the context
|
||||
contextDate = parsed.dueDate;
|
||||
contextTime = parsed.dueTime;
|
||||
contextProject = parsed.projectName;
|
||||
|
||||
// Calculate end time if duration is known
|
||||
if (parsed.dueDate && parsed.estimatedDuration) {
|
||||
|
|
@ -185,10 +175,6 @@ export function parseMultiTaskInput(input: string, locale: ParserLocale = 'de'):
|
|||
parsed.dueTime = contextTime;
|
||||
}
|
||||
}
|
||||
if (!parsed.projectName && contextProject) {
|
||||
parsed.projectName = contextProject;
|
||||
}
|
||||
|
||||
// Update end time for next task
|
||||
if (parsed.dueDate && parsed.estimatedDuration) {
|
||||
lastEndMinutes =
|
||||
|
|
@ -289,11 +275,6 @@ export function parseTaskInput(input: string, locale: ParserLocale = 'de'): Pars
|
|||
text = durationResult.remaining;
|
||||
const estimatedDuration = durationResult.duration;
|
||||
|
||||
// Extract project (@ProjectName) - task-specific
|
||||
const projectResult = extractAtReference(text);
|
||||
text = projectResult.remaining;
|
||||
const projectName = projectResult.value;
|
||||
|
||||
// Use base parser for common patterns (date, time, tags)
|
||||
const base = parseBaseInput(text, locale);
|
||||
|
||||
|
|
@ -313,7 +294,6 @@ export function parseTaskInput(input: string, locale: ParserLocale = 'de'): Pars
|
|||
dueDate,
|
||||
dueTime,
|
||||
priority,
|
||||
projectName,
|
||||
labelNames: base.tagNames,
|
||||
recurrenceRule,
|
||||
subtasks: subtaskResult.subtasks,
|
||||
|
|
@ -322,26 +302,11 @@ export function parseTaskInput(input: string, locale: ParserLocale = 'de'): Pars
|
|||
}
|
||||
|
||||
/**
|
||||
* Resolve project and label names to IDs
|
||||
* Resolve label names to IDs
|
||||
*/
|
||||
export function resolveTaskIds(
|
||||
parsed: ParsedTask,
|
||||
projects: Project[],
|
||||
labels: Label[]
|
||||
): ParsedTaskWithIds {
|
||||
let projectId: string | undefined;
|
||||
export function resolveTaskIds(parsed: ParsedTask, labels: Label[]): ParsedTaskWithIds {
|
||||
const labelIds: string[] = [];
|
||||
|
||||
// Find project by name (case-insensitive)
|
||||
if (parsed.projectName) {
|
||||
const project = projects.find(
|
||||
(p) => p.name.toLowerCase() === parsed.projectName!.toLowerCase()
|
||||
);
|
||||
if (project) {
|
||||
projectId = project.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Find labels by name (case-insensitive)
|
||||
for (const labelName of parsed.labelNames) {
|
||||
const label = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
|
||||
|
|
@ -355,7 +320,6 @@ export function resolveTaskIds(
|
|||
dueDate: parsed.dueDate?.toISOString(),
|
||||
dueTime: parsed.dueTime,
|
||||
priority: parsed.priority,
|
||||
projectId,
|
||||
labelIds,
|
||||
recurrenceRule: parsed.recurrenceRule,
|
||||
subtasks: parsed.subtasks,
|
||||
|
|
@ -406,10 +370,6 @@ export function formatParsedTaskPreview(parsed: ParsedTask, locale: ParserLocale
|
|||
parts.push(PRIORITY_LABELS[locale][parsed.priority]);
|
||||
}
|
||||
|
||||
if (parsed.projectName) {
|
||||
parts.push(`📁 ${parsed.projectName}`);
|
||||
}
|
||||
|
||||
if (parsed.recurrenceRule) {
|
||||
parts.push(`🔄 ${parsed.recurrenceRule}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
export interface CompletedTaskData {
|
||||
title: string;
|
||||
projectId?: string | null;
|
||||
labelIds?: string[];
|
||||
priority: string;
|
||||
estimatedDuration?: number | null; // minutes
|
||||
|
|
@ -104,17 +103,12 @@ function titleOverlap(a: string[], b: string[]): number {
|
|||
* Compute similarity score between a new task and a historical task
|
||||
*/
|
||||
function similarity(
|
||||
newTask: { title: string; projectId?: string | null; labelIds?: string[]; priority: string },
|
||||
newTask: { title: string; labelIds?: string[]; priority: string },
|
||||
historical: CompletedTaskData,
|
||||
newTokens: string[]
|
||||
): number {
|
||||
let score = 0;
|
||||
|
||||
// Same project is the strongest signal
|
||||
if (newTask.projectId && historical.projectId && newTask.projectId === historical.projectId) {
|
||||
score += 3;
|
||||
}
|
||||
|
||||
// Shared labels
|
||||
if (newTask.labelIds && historical.labelIds) {
|
||||
const histSet = new Set(historical.labelIds);
|
||||
|
|
@ -168,7 +162,7 @@ function getEffectiveDuration(task: CompletedTaskData): number | null {
|
|||
* @returns Estimate or null if insufficient data
|
||||
*/
|
||||
export function estimateDuration(
|
||||
newTask: { title: string; projectId?: string | null; labelIds?: string[]; priority: string },
|
||||
newTask: { title: string; labelIds?: string[]; priority: string },
|
||||
history: CompletedTaskData[],
|
||||
minSamples = 3
|
||||
): DurationEstimate | null {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { todoSettings } from '$lib/stores/settings.svelte';
|
||||
import { projectsStore } from '$lib/stores/projects.svelte';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
import {
|
||||
tagLocalStore,
|
||||
|
|
@ -48,18 +47,12 @@
|
|||
import { TodoEvents } from '@manacore/shared-utils/analytics';
|
||||
import { todoStore, taskCollection } from '$lib/data/local-store';
|
||||
import type { LocalBoardView } from '$lib/data/local-store';
|
||||
import {
|
||||
useAllTasks,
|
||||
useAllProjects,
|
||||
useAllBoardViews,
|
||||
getActiveProjects,
|
||||
} from '$lib/data/task-queries';
|
||||
import { useAllTasks, useAllBoardViews } from '$lib/data/task-queries';
|
||||
import SyncIndicator from '$lib/components/SyncIndicator.svelte';
|
||||
import { List, X } from '@manacore/shared-icons';
|
||||
|
||||
// Live queries — auto-update when IndexedDB changes (local writes, sync, other tabs)
|
||||
const allTasks = useAllTasks();
|
||||
const allProjects = useAllProjects();
|
||||
const allTags = useAllSharedTags();
|
||||
|
||||
// ─── Board View Management ──────────────────────────────
|
||||
|
|
@ -69,7 +62,6 @@
|
|||
let activeView = $derived(boardViews.value[0] ?? null);
|
||||
|
||||
// Provide data to child components via Svelte context
|
||||
setContext('projects', allProjects);
|
||||
setContext('tasks', allTasks);
|
||||
setContext('tags', allTags);
|
||||
setContext('activeView', {
|
||||
|
|
@ -92,9 +84,6 @@
|
|||
},
|
||||
});
|
||||
|
||||
// Derived active projects for UI
|
||||
let activeProjects = $derived(getActiveProjects(allProjects.value));
|
||||
|
||||
// Guest welcome modal state
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
|
|
@ -162,13 +151,12 @@
|
|||
|
||||
try {
|
||||
const parsed = parseTaskInput(query);
|
||||
const resolved = resolveTaskIds(parsed, allProjects.value, allTags.value);
|
||||
const resolved = resolveTaskIds(parsed, allTags.value);
|
||||
|
||||
await tasksStore.createTask({
|
||||
title: resolved.title,
|
||||
dueDate: resolved.dueDate,
|
||||
priority: resolved.priority,
|
||||
projectId: resolved.projectId,
|
||||
labelIds: resolved.labelIds,
|
||||
});
|
||||
TodoEvents.quickAddUsed();
|
||||
|
|
@ -462,11 +450,9 @@
|
|||
<TaskFilters
|
||||
variant="strip"
|
||||
selectedPriorities={viewStore.filterPriorities}
|
||||
selectedProjectId={viewStore.filterProjectId}
|
||||
selectedLabelIds={viewStore.filterLabelIds}
|
||||
searchQuery={viewStore.filterSearchQuery}
|
||||
onPrioritiesChange={(p: TaskPriority[]) => viewStore.setFilterPriorities(p)}
|
||||
onProjectChange={(id: string | null) => viewStore.setFilterProjectId(id)}
|
||||
onLabelsChange={(ids: string[]) => viewStore.setFilterLabelIds(ids)}
|
||||
onSearchChange={(q: string) => viewStore.setFilterSearchQuery(q)}
|
||||
onClearFilters={() => viewStore.clearFilters()}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@
|
|||
const GROUPBY_OPTIONS = [
|
||||
{ value: 'status', label: 'Status' },
|
||||
{ value: 'priority', label: 'Priorität' },
|
||||
{ value: 'project', label: 'Projekt' },
|
||||
{ value: 'dueDate', label: 'Fälligkeit' },
|
||||
{ value: 'custom', label: 'Benutzerdefiniert' },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -10,10 +10,6 @@
|
|||
type KanbanCardSize,
|
||||
type PageMode,
|
||||
} from '$lib/stores/settings.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Project } from '@todo/shared';
|
||||
|
||||
const projectsCtx: { readonly value: Project[] } = getContext('projects');
|
||||
import type { TaskPriority } from '@todo/shared';
|
||||
import { PRIORITY_OPTIONS } from '@todo/shared';
|
||||
import {
|
||||
|
|
@ -40,7 +36,6 @@
|
|||
EnvelopeSimple,
|
||||
Eye,
|
||||
Fire,
|
||||
FolderSimple,
|
||||
GraduationCap,
|
||||
GridFour,
|
||||
Hash,
|
||||
|
|
@ -56,7 +51,6 @@
|
|||
SignOut,
|
||||
SortAscending,
|
||||
SquaresFour,
|
||||
Stack,
|
||||
Tag,
|
||||
Timer,
|
||||
Trash,
|
||||
|
|
@ -107,12 +101,6 @@
|
|||
{ value: '120', label: '2 Stunden' },
|
||||
];
|
||||
|
||||
// Project options for quick add (computed)
|
||||
let projectOptions = $derived([
|
||||
{ value: null, label: 'Inbox' },
|
||||
...projectsCtx.value.map((p) => ({ value: p.id, label: p.name })),
|
||||
]);
|
||||
|
||||
onMount(async () => {
|
||||
// Load user settings and projects from server (only if authenticated)
|
||||
if (authStore.isAuthenticated) {
|
||||
|
|
@ -227,19 +215,6 @@
|
|||
{/snippet}
|
||||
</SettingsTimeInput>
|
||||
|
||||
<SettingsSelect
|
||||
label="Standard-Projekt"
|
||||
description="Projekt für Quick-Add"
|
||||
options={projectOptions}
|
||||
value={todoSettings.quickAddProject}
|
||||
onchange={(v: string | number | null) =>
|
||||
todoSettings.set('quickAddProject', v as string | null)}
|
||||
>
|
||||
{#snippet icon()}
|
||||
<FolderSimple size={20} />
|
||||
{/snippet}
|
||||
</SettingsSelect>
|
||||
|
||||
<SettingsNumberInput
|
||||
label="Auto-Archivierung"
|
||||
description="Erledigte Tasks nach X Tagen archivieren"
|
||||
|
|
@ -339,21 +314,10 @@
|
|||
description="Fortschrittsbalken für Subtasks anzeigen"
|
||||
isOn={todoSettings.showSubtaskProgress}
|
||||
onToggle={(v) => todoSettings.set('showSubtaskProgress', v)}
|
||||
>
|
||||
{#snippet icon()}
|
||||
<ChartBar size={20} />
|
||||
{/snippet}
|
||||
</SettingsToggle>
|
||||
|
||||
<SettingsToggle
|
||||
label="Nach Projekt gruppieren"
|
||||
description="Tasks nach Projekt gruppieren"
|
||||
isOn={todoSettings.groupByProject}
|
||||
onToggle={(v) => todoSettings.set('groupByProject', v)}
|
||||
border={false}
|
||||
>
|
||||
{#snippet icon()}
|
||||
<Stack size={20} />
|
||||
<ChartBar size={20} />
|
||||
{/snippet}
|
||||
</SettingsToggle>
|
||||
</SettingsCard>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue