- import type { Task } from '@todo/shared';
+ import type {
+ Task,
+ Subtask,
+ TaskPriority,
+ TaskStatus,
+ EffectiveDuration,
+ UpdateTaskInput,
+ } from '@todo/shared';
+ import type { ContactReference, ContactOrManual } from '@manacore/shared-types';
+ import { STATUS_OPTIONS, RECURRENCE_OPTIONS } from '@todo/shared';
import { format, isToday, isPast, isTomorrow } from 'date-fns';
import { de } from 'date-fns/locale';
import { projectsStore } from '$lib/stores/projects.svelte';
- import { ContactAvatar } from '@manacore/shared-ui';
+ import { contactsStore } from '$lib/stores/contacts.svelte';
+ import { ContactAvatar, ContactSelector } from '@manacore/shared-ui';
+ import SubtaskList from './SubtaskList.svelte';
+ import {
+ PrioritySelector,
+ StorypointsSelector,
+ DurationPicker,
+ FunRatingPicker,
+ TagSelector,
+ } from './form';
interface Props {
task: Task;
showCompleted?: boolean;
animateComplete?: boolean;
+ isExpanded?: boolean;
onToggleComplete: () => void;
onDelete: () => void;
- onEdit?: () => void;
+ onExpand?: () => void;
+ onCollapse?: () => void;
+ onSave?: (data: UpdateTaskInput) => void;
}
let {
task,
showCompleted = false,
animateComplete = false,
+ isExpanded = false,
onToggleComplete,
onDelete,
- onEdit,
+ onExpand,
+ onCollapse,
+ onSave,
}: Props = $props();
+ // Form state for expanded mode
+ let title = $state('');
+ let description = $state('');
+ let dueDate = $state('');
+ let dueTime = $state('');
+ let startDate = $state('');
+ let priority = $state
('medium');
+ let status = $state('pending');
+ let projectId = $state(null);
+ let selectedLabelIds = $state([]);
+ let subtasks = $state([]);
+ let recurrenceRule = $state('');
+ let notes = $state('');
+ let storyPoints = $state(null);
+ let effectiveDuration = $state(null);
+ let funRating = $state(null);
+ let assignee = $state([]);
+ let involvedContacts = $state([]);
+ let contactsAvailable = $state(null);
+ let isLoading = $state(false);
+ let showDeleteConfirm = $state(false);
+ let titleInputRef = $state(null);
+
+ // Focus title input when expanded
+ $effect(() => {
+ if (isExpanded && titleInputRef) {
+ // Small delay to ensure DOM is ready
+ setTimeout(() => titleInputRef?.focus(), 50);
+ }
+ });
+
+ // Initialize form when expanded
+ $effect(() => {
+ if (isExpanded && task) {
+ title = task.title || '';
+ description = task.description || '';
+ dueDate = task.dueDate ? format(new Date(task.dueDate), 'yyyy-MM-dd') : '';
+ dueTime = task.dueTime || '';
+ 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 || '';
+ notes = task.metadata?.notes || '';
+ storyPoints = task.metadata?.storyPoints ?? null;
+ effectiveDuration = task.metadata?.effectiveDuration ?? null;
+ funRating = task.metadata?.funRating ?? null;
+ assignee = task.metadata?.assignee ? [task.metadata.assignee] : [];
+ involvedContacts = task.metadata?.involvedContacts || [];
+ showDeleteConfirm = false;
+
+ contactsStore.checkAvailability().then((available) => {
+ contactsAvailable = available;
+ });
+ }
+ });
+
// Animation state for completing
let isAnimatingComplete = $state(false);
@@ -48,11 +131,79 @@
}
function handleContentClick() {
- if (onEdit) {
- onEdit();
+ if (onExpand) {
+ onExpand();
}
}
+ function handleKeydown(e: KeyboardEvent) {
+ if (!isExpanded) return;
+ if (e.key === 'Escape') {
+ onCollapse?.();
+ } else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
+ e.preventDefault();
+ handleSave();
+ }
+ }
+
+ function toContactReference(contact: ContactOrManual): ContactReference | null {
+ if ('isManual' in contact && contact.isManual) {
+ return null;
+ }
+ return contact as ContactReference;
+ }
+
+ async function handleSave() {
+ if (!title.trim() || !onSave) return;
+
+ isLoading = true;
+ try {
+ const assigneeRef = assignee.length > 0 ? toContactReference(assignee[0]) : null;
+ const involvedRefs = involvedContacts
+ .map(toContactReference)
+ .filter((c): c is ContactReference => c !== null);
+
+ const data: UpdateTaskInput = {
+ title: title.trim(),
+ description: description.trim() || null,
+ dueDate: dueDate ? new Date(dueDate).toISOString() : null,
+ dueTime: dueTime || null,
+ startDate: startDate ? new Date(startDate).toISOString() : null,
+ priority,
+ status,
+ projectId: projectId || null,
+ subtasks: subtasks.length > 0 ? subtasks : null,
+ recurrenceRule: recurrenceRule || null,
+ metadata: {
+ ...task.metadata,
+ notes: notes.trim() || undefined,
+ storyPoints: storyPoints ?? undefined,
+ effectiveDuration: effectiveDuration ?? undefined,
+ funRating: funRating ?? undefined,
+ assignee: assigneeRef ?? undefined,
+ involvedContacts: involvedRefs.length > 0 ? involvedRefs : undefined,
+ },
+ labelIds: selectedLabelIds,
+ };
+
+ onSave(data);
+ } finally {
+ isLoading = false;
+ }
+ }
+
+ function handleDeleteClick() {
+ if (showDeleteConfirm) {
+ onDelete();
+ } else {
+ showDeleteConfirm = true;
+ }
+ }
+
+ function handleSubtasksChange(newSubtasks: Subtask[]) {
+ subtasks = newSubtasks;
+ }
+
// Priority colors
const priorityColors: Record = {
low: '#22c55e',
@@ -89,140 +240,412 @@
const completed = task.subtasks.filter((s) => s.isCompleted).length;
return `${completed}/${task.subtasks.length}`;
});
+
+ // Only allow drag from the drag handle
+ function handlePointerDown(e: PointerEvent) {
+ const target = e.target as HTMLElement;
+ const isDragHandle = target.closest('.drag-handle');
+ if (!isDragHandle) {
+ // Prevent drag from starting if not on drag handle
+ e.stopPropagation();
+ }
+ }
-
-
-
+
-
+
-
-
-
+
-
-
-
- {task.title}
-
-
-
- {#if subtaskProgress() || (task.labels && task.labels.length > 0)}
-