MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 53s
1106 lines
46 KiB
TypeScript
1106 lines
46 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
Check,
|
|
Plus,
|
|
Calendar,
|
|
Trash2,
|
|
RotateCcw,
|
|
FileText,
|
|
LayoutList,
|
|
Columns,
|
|
ArrowRight,
|
|
ArrowLeft,
|
|
Edit3,
|
|
X,
|
|
Layers,
|
|
Share2,
|
|
Users,
|
|
} from 'lucide-react';
|
|
import { TaskList, Thought, Space } from '../../lib/types';
|
|
import { api, triggerHaptic } from '../../lib/api';
|
|
|
|
const getInitials = (name?: string): string => {
|
|
if (!name) return '?';
|
|
const parts = name.trim().split(/\s+/);
|
|
if (parts.length >= 2) {
|
|
return (parts[0][0] + parts[1][0]).toUpperCase();
|
|
}
|
|
return name.slice(0, 2).toUpperCase();
|
|
};
|
|
|
|
interface TasksViewProps {
|
|
onRefreshStats: () => void;
|
|
onOpenDocument: (docId: string) => void;
|
|
refreshTrigger?: number;
|
|
}
|
|
|
|
export const TasksView: React.FC<TasksViewProps> = ({
|
|
onRefreshStats,
|
|
onOpenDocument,
|
|
refreshTrigger = 0,
|
|
}) => {
|
|
const [viewMode, setViewMode] = useState<'list' | 'kanban'>('list');
|
|
const [filter, setFilter] = useState<'open' | 'done' | 'discarded'>('open');
|
|
const [tasks, setTasks] = useState<Thought[]>([]);
|
|
const [taskLists, setTaskLists] = useState<TaskList[]>([]);
|
|
const [selectedListId, setSelectedListId] = useState<string>('all');
|
|
const [spaces, setSpaces] = useState<Space[]>([]);
|
|
const [discarded, setDiscarded] = useState<Thought[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// Quick Task Add state
|
|
const [quickTitle, setQuickTitle] = useState('');
|
|
const [quickDueDate, setQuickDueDate] = useState('');
|
|
|
|
// New TaskList modal state
|
|
const [showNewListModal, setShowNewListModal] = useState(false);
|
|
const [newListTitle, setNewListTitle] = useState('');
|
|
const [newListIcon, setNewListIcon] = useState('📋');
|
|
const [newListSpaceId, setNewListSpaceId] = useState('');
|
|
|
|
// Share TaskList modal state
|
|
const [showShareListModal, setShowShareListModal] = useState(false);
|
|
const [sharingTaskList, setSharingTaskList] = useState<TaskList | null>(null);
|
|
const [shareTargetSpaceId, setShareTargetSpaceId] = useState('');
|
|
|
|
// Active notes inline edit ID
|
|
const [activeNotesTaskId, setActiveNotesTaskId] = useState<string | null>(null);
|
|
const [notesDraft, setNotesDraft] = useState('');
|
|
|
|
const loadData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [allTasks, allLists, allSpaces, allDiscarded] = await Promise.all([
|
|
api.getTasks(selectedListId === 'all' ? undefined : selectedListId).catch(() => []),
|
|
api.getTaskLists().catch(() => []),
|
|
api.getSpaces().catch(() => []),
|
|
api.getThoughts('discarded').catch(() => []),
|
|
]);
|
|
setTasks(Array.isArray(allTasks) ? allTasks : []);
|
|
setTaskLists(Array.isArray(allLists) ? allLists : []);
|
|
setSpaces(Array.isArray(allSpaces) ? allSpaces : []);
|
|
setDiscarded(Array.isArray(allDiscarded) ? allDiscarded : []);
|
|
} catch {
|
|
// Prevent unhandled promise rejection from crashing view
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, [refreshTrigger, selectedListId]);
|
|
|
|
const handleToggleDone = async (task: Thought) => {
|
|
triggerHaptic(15);
|
|
const nextState = !task.isCompleted;
|
|
const nextKanban = nextState ? 'done' : 'todo';
|
|
setTasks((prev) =>
|
|
prev.map((t) =>
|
|
t.id === task.id ? { ...t, isCompleted: nextState, kanbanStatus: nextKanban } : t
|
|
)
|
|
);
|
|
await api.updateThought(task.id, { isCompleted: nextState, kanbanStatus: nextKanban });
|
|
onRefreshStats();
|
|
};
|
|
|
|
const handleKanbanMove = async (task: Thought, nextStatus: 'todo' | 'in_progress' | 'done') => {
|
|
triggerHaptic(15);
|
|
const isCompleted = nextStatus === 'done';
|
|
setTasks((prev) =>
|
|
prev.map((t) => (t.id === task.id ? { ...t, kanbanStatus: nextStatus, isCompleted } : t))
|
|
);
|
|
await api.updateThought(task.id, { kanbanStatus: nextStatus, isCompleted });
|
|
onRefreshStats();
|
|
};
|
|
|
|
const handleDueDateChange = async (task: Thought, newDate: string) => {
|
|
setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, dueDate: newDate } : t)));
|
|
await api.updateThought(task.id, { dueDate: newDate });
|
|
};
|
|
|
|
const handleSaveNotes = async (task: Thought) => {
|
|
setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, notes: notesDraft } : t)));
|
|
await api.updateThought(task.id, { notes: notesDraft });
|
|
setActiveNotesTaskId(null);
|
|
};
|
|
|
|
const handleConvertToDoc = async (task: Thought) => {
|
|
triggerHaptic(20);
|
|
const updated = await api.triageAction(task.id, 'keep', { convertTo: 'document' });
|
|
onRefreshStats();
|
|
if (updated.convertedRefId) {
|
|
onOpenDocument(updated.convertedRefId);
|
|
} else {
|
|
loadData();
|
|
}
|
|
};
|
|
|
|
const handleRestore = async (item: Thought) => {
|
|
triggerHaptic(15);
|
|
await api.triageAction(item.id, 'restore');
|
|
await loadData();
|
|
onRefreshStats();
|
|
};
|
|
|
|
const handleDeletePermanent = async (id: string) => {
|
|
triggerHaptic(20);
|
|
await api.deleteThought(id);
|
|
await loadData();
|
|
onRefreshStats();
|
|
};
|
|
|
|
const handlePurgeAllDiscarded = async () => {
|
|
await api.purgeDiscarded();
|
|
await loadData();
|
|
onRefreshStats();
|
|
};
|
|
|
|
const handleQuickAddTask = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
const title = quickTitle.trim();
|
|
if (!title) return;
|
|
triggerHaptic(10);
|
|
setQuickTitle('');
|
|
|
|
try {
|
|
const created = await api.captureThought(title, 'normal', 'manual', quickDueDate, {
|
|
target: 'task',
|
|
taskListId: selectedListId === 'all' ? '' : selectedListId,
|
|
});
|
|
setTasks((prev) => [created, ...prev]);
|
|
onRefreshStats();
|
|
} catch {
|
|
loadData();
|
|
}
|
|
};
|
|
|
|
const handleCreateTaskList = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
const title = newListTitle.trim();
|
|
if (!title) return;
|
|
try {
|
|
const created = await api.createTaskList({
|
|
title,
|
|
icon: newListIcon,
|
|
spaceId: newListSpaceId,
|
|
});
|
|
setTaskLists((prev) => [...prev, created]);
|
|
setSelectedListId(created.id);
|
|
setShowNewListModal(false);
|
|
setNewListTitle('');
|
|
} catch (err: any) {
|
|
alert(err.message || 'Fehler beim Erstellen der Aufgabenliste');
|
|
}
|
|
};
|
|
|
|
const handleDeleteTaskList = async (listId: string) => {
|
|
if (
|
|
!window.confirm(
|
|
'Möchten Sie diese Aufgabenliste wirklich löschen? Die enthaltenen Aufgaben bleiben erhalten.'
|
|
)
|
|
)
|
|
return;
|
|
try {
|
|
await api.deleteTaskList(listId);
|
|
setTaskLists((prev) => prev.filter((l) => l.id !== listId));
|
|
setSelectedListId('all');
|
|
} catch (err: any) {
|
|
alert(err.message);
|
|
}
|
|
};
|
|
|
|
const handleOpenShareModal = (tl: TaskList) => {
|
|
setSharingTaskList(tl);
|
|
setShareTargetSpaceId(tl.spaceId || '');
|
|
setShowShareListModal(true);
|
|
};
|
|
|
|
const handleSaveShareList = async () => {
|
|
if (!sharingTaskList) return;
|
|
triggerHaptic(15);
|
|
try {
|
|
const updated = await api.updateTaskList(sharingTaskList.id, {
|
|
spaceId: shareTargetSpaceId,
|
|
});
|
|
setTaskLists((prev) =>
|
|
prev.map((l) => (l.id === updated.id ? { ...l, ...updated } : l))
|
|
);
|
|
setShowShareListModal(false);
|
|
setSharingTaskList(null);
|
|
await loadData();
|
|
} catch (err: any) {
|
|
alert(err.message || 'Fehler beim Speichern der Freigabe');
|
|
}
|
|
};
|
|
|
|
const safeTasks = Array.isArray(tasks) ? tasks : [];
|
|
const safeTaskLists = Array.isArray(taskLists) ? taskLists : [];
|
|
const safeSpaces = Array.isArray(spaces) ? spaces : [];
|
|
const safeDiscarded = Array.isArray(discarded) ? discarded : [];
|
|
|
|
const openTasks = safeTasks.filter((t) => !t.isCompleted);
|
|
const doneTasks = safeTasks.filter((t) => t.isCompleted);
|
|
|
|
const todoKanban = safeTasks.filter(
|
|
(t) => (!t.kanbanStatus || t.kanbanStatus === 'todo') && !t.isCompleted
|
|
);
|
|
const inProgressKanban = safeTasks.filter(
|
|
(t) => t.kanbanStatus === 'in_progress' && !t.isCompleted
|
|
);
|
|
const doneKanban = safeTasks.filter((t) => t.kanbanStatus === 'done' || t.isCompleted);
|
|
|
|
const currentSelectedList = safeTaskLists.find((l) => l.id === selectedListId);
|
|
|
|
return (
|
|
<div className="w-full space-y-3">
|
|
{/* Top Header: Task Lists Tabs & View Mode Switcher */}
|
|
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-border pb-2">
|
|
{/* Task Lists selector tabs */}
|
|
<div className="flex flex-wrap items-center gap-1">
|
|
<button
|
|
onClick={() => setSelectedListId('all')}
|
|
className={`h-8 px-3 text-xs font-medium border rounded-btn transition-colors ${
|
|
selectedListId === 'all'
|
|
? 'bg-panel border-accent text-main'
|
|
: 'bg-canvas border-border text-muted hover:text-main'
|
|
}`}
|
|
>
|
|
Alle Aufgaben
|
|
</button>
|
|
|
|
{safeTaskLists.map((tl) => (
|
|
<div key={tl.id} className="relative group flex items-center">
|
|
<button
|
|
onClick={() => setSelectedListId(tl.id)}
|
|
className={`h-8 px-3 text-xs font-medium border rounded-btn flex items-center gap-1.5 transition-colors ${
|
|
selectedListId === tl.id
|
|
? 'bg-panel border-accent text-main'
|
|
: 'bg-canvas border-border text-muted hover:text-main'
|
|
}`}
|
|
>
|
|
<span>{tl.icon}</span>
|
|
<span>{tl.title}</span>
|
|
{tl.tasksCount !== undefined && tl.tasksCount > 0 && (
|
|
<span className="ml-1 badge-neutral">{tl.tasksCount}</span>
|
|
)}
|
|
{tl.members && tl.members.length > 0 ? (
|
|
<div className="flex items-center -space-x-1 ml-0.5" title={`Geteilt (${tl.members.length} Mitglieder)`}>
|
|
{tl.members.slice(0, 2).map((m) => (
|
|
<span key={m.userId} className="w-3.5 h-3.5 rounded-pill bg-accent/20 border border-panel text-[8px] font-bold text-accent flex items-center justify-center">
|
|
{getInitials(m.displayName || m.email)[0]}
|
|
</span>
|
|
))}
|
|
</div>
|
|
) : tl.spaceId ? (
|
|
<span title="Geteilt mit Team/Space">
|
|
<Share2 className="w-2.5 h-2.5 text-accent ml-0.5" />
|
|
</span>
|
|
) : null}
|
|
</button>
|
|
{selectedListId === tl.id && (
|
|
<button
|
|
onClick={() => handleDeleteTaskList(tl.id)}
|
|
title="Liste löschen"
|
|
className="h-8 px-1.5 bg-panel border-y border-r border-accent text-faint hover:text-status-err"
|
|
>
|
|
<X className="w-3 h-3" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
<button
|
|
onClick={() => setShowNewListModal(true)}
|
|
className="h-8 px-2.5 bg-canvas hover:bg-panel border border-border border-dashed rounded-btn text-xs text-muted hover:text-main flex items-center gap-1 transition-colors"
|
|
title="Neue Aufgabenliste anlegen"
|
|
>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
<span>Neue Liste</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Right Header Section: Sharing, Avatar Stack & View Switcher */}
|
|
<div className="flex items-center gap-2">
|
|
{currentSelectedList && (
|
|
<div className="flex items-center gap-1.5">
|
|
<button
|
|
type="button"
|
|
onClick={() => handleOpenShareModal(currentSelectedList)}
|
|
className="btn-action h-8 px-2.5 flex items-center gap-1.5 text-xs"
|
|
title={`Liste '${currentSelectedList.title}' mit Team oder Nutzern teilen`}
|
|
>
|
|
<Users className="w-3.5 h-3.5 text-accent" />
|
|
<span>Teilen</span>
|
|
</button>
|
|
|
|
{/* Avatar Stack in the corner */}
|
|
{currentSelectedList.members && currentSelectedList.members.length > 0 ? (
|
|
<div
|
|
onClick={() => handleOpenShareModal(currentSelectedList)}
|
|
className="flex items-center -space-x-1.5 cursor-pointer hover:opacity-90 transition-opacity pl-0.5"
|
|
title={`Geteilt mit ${currentSelectedList.members.length} Mitgliedern (${currentSelectedList.spaceName || 'Team'}). Klick zum Verwalten.`}
|
|
>
|
|
{currentSelectedList.members.slice(0, 3).map((m) => (
|
|
<div
|
|
key={m.userId}
|
|
className="w-7 h-7 rounded-pill border-2 border-panel bg-accent/20 text-accent flex items-center justify-center text-[10px] font-bold shadow-sm"
|
|
title={`${m.displayName || m.email} (${m.role})`}
|
|
>
|
|
{getInitials(m.displayName || m.email)}
|
|
</div>
|
|
))}
|
|
{currentSelectedList.members.length > 3 && (
|
|
<div className="w-7 h-7 rounded-pill border-2 border-panel bg-accent text-on-accent flex items-center justify-center text-[10px] font-bold shadow-sm">
|
|
+{currentSelectedList.members.length - 3}
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div
|
|
onClick={() => handleOpenShareModal(currentSelectedList)}
|
|
className="flex items-center cursor-pointer pl-0.5"
|
|
title="Privat (nur du). Klick zum Teilen."
|
|
>
|
|
<div className="w-7 h-7 rounded-pill border border-dashed border-border bg-canvas text-muted hover:text-accent hover:border-accent flex items-center justify-center text-[11px] font-bold transition-colors">
|
|
🔒
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* View Switcher: List vs Kanban */}
|
|
<div className="flex border border-border rounded-btn bg-canvas overflow-hidden">
|
|
<button
|
|
onClick={() => setViewMode('list')}
|
|
className={`h-8 px-2.5 flex items-center gap-1.5 text-xs transition-colors border-r border-border ${
|
|
viewMode === 'list' ? 'bg-panel text-main font-medium' : 'text-muted hover:text-main'
|
|
}`}
|
|
title="Listenansicht"
|
|
>
|
|
<LayoutList className="w-3.5 h-3.5" />
|
|
<span className="hidden sm:inline">Liste</span>
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode('kanban')}
|
|
className={`h-8 px-2.5 flex items-center gap-1.5 text-xs transition-colors ${
|
|
viewMode === 'kanban'
|
|
? 'bg-panel text-main font-medium'
|
|
: 'text-muted hover:text-main'
|
|
}`}
|
|
title="Kanban-Board"
|
|
>
|
|
<Columns className="w-3.5 h-3.5" />
|
|
<span className="hidden sm:inline">Kanban</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Add Task Input in active list */}
|
|
<form
|
|
onSubmit={handleQuickAddTask}
|
|
className="flex items-center gap-1.5 panel-surface p-1.5"
|
|
>
|
|
<Plus className="w-4 h-4 text-accent ml-1 flex-shrink-0" />
|
|
<input
|
|
type="text"
|
|
value={quickTitle}
|
|
onChange={(e) => setQuickTitle(e.target.value)}
|
|
placeholder={
|
|
currentSelectedList
|
|
? `Aufgabe zu "${currentSelectedList.title}" hinzufügen… (Enter)`
|
|
: 'Neue Aufgabe anlegen… (Enter)'
|
|
}
|
|
className="flex-1 bg-transparent px-2 py-1 text-sm text-main placeholder-faint focus:outline-none"
|
|
/>
|
|
<div className="flex items-center gap-1">
|
|
<input
|
|
type="date"
|
|
value={quickDueDate}
|
|
onChange={(e) => setQuickDueDate(e.target.value)}
|
|
className="h-7 px-2 input-field text-xs"
|
|
title="Fälligkeitsdatum"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={!quickTitle.trim()}
|
|
className="btn-primary h-7 px-3 text-xs disabled:opacity-40"
|
|
>
|
|
Hinzufügen
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
{/* NEW LIST MODAL */}
|
|
{showNewListModal && (
|
|
<div className="fixed inset-0 modal-overlay z-50 flex items-center justify-center p-4">
|
|
<div className="w-full max-w-sm panel-surface shadow-panel p-4 space-y-3">
|
|
<div className="flex items-center justify-between border-b border-border pb-2">
|
|
<h3 className="text-sm font-semibold text-main flex items-center gap-1.5">
|
|
<Layers className="w-4 h-4 text-accent" />
|
|
<span>Neue Aufgabenliste erstellen</span>
|
|
</h3>
|
|
<button
|
|
onClick={() => setShowNewListModal(false)}
|
|
className="text-muted hover:text-main"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleCreateTaskList} className="space-y-3">
|
|
<div>
|
|
<label className="block text-xs text-muted mb-1">Titel der Liste</label>
|
|
<div className="flex items-center gap-1.5">
|
|
<input
|
|
type="text"
|
|
value={newListIcon}
|
|
onChange={(e) => setNewListIcon(e.target.value)}
|
|
maxLength={2}
|
|
className="w-10 h-8 text-center input-field text-sm"
|
|
title="Icon (Emoji)"
|
|
/>
|
|
<input
|
|
type="text"
|
|
value={newListTitle}
|
|
onChange={(e) => setNewListTitle(e.target.value)}
|
|
placeholder="z.B. Homelab, Einkäufe, Steuern"
|
|
autoFocus
|
|
required
|
|
className="flex-1 h-8 input-field px-2.5 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{safeSpaces.length > 0 && (
|
|
<div>
|
|
<label className="block text-xs text-muted mb-1">
|
|
Geteilt mit Space (optional)
|
|
</label>
|
|
<select
|
|
value={newListSpaceId}
|
|
onChange={(e) => setNewListSpaceId(e.target.value)}
|
|
className="w-full h-8 input-field px-2 text-xs"
|
|
>
|
|
<option value="">Privat (nur ich)</option>
|
|
{safeSpaces.map((sp) => (
|
|
<option key={sp.id} value={sp.id}>
|
|
{sp.icon} {sp.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-end gap-2 pt-2 border-t border-border">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowNewListModal(false)}
|
|
className="btn-action h-7 px-3 text-xs"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button type="submit" className="btn-primary h-7 px-3 text-xs">
|
|
Erstellen
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* SHARE LIST MODAL */}
|
|
{showShareListModal && sharingTaskList && (
|
|
<div className="fixed inset-0 modal-overlay z-50 flex items-center justify-center p-4">
|
|
<div className="w-full max-w-md panel-surface shadow-panel p-4 space-y-4">
|
|
<div className="flex items-center justify-between border-b border-border pb-2">
|
|
<h3 className="text-sm font-semibold text-main flex items-center gap-1.5">
|
|
<Users className="w-4 h-4 text-accent" />
|
|
<span>Liste teilen: {sharingTaskList.title}</span>
|
|
</h3>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setShowShareListModal(false);
|
|
setSharingTaskList(null);
|
|
}}
|
|
className="text-muted hover:text-main"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label className="block text-xs text-muted mb-1 font-medium">
|
|
Freigeben für Bereich / Team:
|
|
</label>
|
|
<select
|
|
value={shareTargetSpaceId}
|
|
onChange={(e) => setShareTargetSpaceId(e.target.value)}
|
|
className="w-full h-8 px-2.5 input-field text-xs bg-panel"
|
|
>
|
|
<option value="">🔒 Privat (Nicht geteilt - nur für mich)</option>
|
|
{safeSpaces.map((s) => (
|
|
<option key={s.id} value={s.id}>
|
|
{s.icon} {s.name} ({s.members?.length || 1} Mitglieder)
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Members preview */}
|
|
{(() => {
|
|
const selectedSpace = safeSpaces.find((s) => s.id === shareTargetSpaceId);
|
|
const members = selectedSpace?.members || [];
|
|
if (members.length > 0) {
|
|
return (
|
|
<div className="space-y-2 pt-2 border-t border-border">
|
|
<p className="text-xs font-semibold text-main">
|
|
Personen mit Zugriff ({members.length}):
|
|
</p>
|
|
<div className="max-h-48 overflow-y-auto space-y-1.5 pr-1">
|
|
{members.map((m) => (
|
|
<div
|
|
key={m.userId}
|
|
className="p-2 bg-canvas border border-border rounded-card flex items-center justify-between text-xs"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-6 h-6 rounded-pill bg-accent/20 text-accent font-bold text-[10px] flex items-center justify-center">
|
|
{getInitials(m.displayName || m.email)}
|
|
</div>
|
|
<div>
|
|
<p className="font-semibold text-main">{m.displayName || m.email}</p>
|
|
<p className="text-[10px] text-muted">{m.email}</p>
|
|
</div>
|
|
</div>
|
|
<span className="badge-neutral text-[10px]">
|
|
{m.role === 'owner' ? 'Eigentümer' : m.role === 'editor' ? 'Bearbeiter' : 'Mitglied'}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
} else if (shareTargetSpaceId) {
|
|
return (
|
|
<p className="text-xs text-muted italic">Keine Mitglieder in diesem Bereich.</p>
|
|
);
|
|
} else {
|
|
return (
|
|
<div className="p-3 bg-canvas border border-border rounded-card text-xs text-muted space-y-1">
|
|
<p className="font-medium text-main">Diese Aufgabenliste ist privat.</p>
|
|
<p className="text-[11px] text-faint">
|
|
Wähle oben einen Familien- oder Team-Space aus, um die Liste und alle enthaltenen Aufgaben automatisch mit den Mitgliedern zu teilen.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
})()}
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-2 border-t border-border">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setShowShareListModal(false);
|
|
setSharingTaskList(null);
|
|
}}
|
|
className="btn-action text-xs"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleSaveShareList}
|
|
className="btn-primary text-xs"
|
|
>
|
|
Freigabe speichern
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* VIEW MODES: LIST OR KANBAN */}
|
|
{viewMode === 'kanban' ? (
|
|
/* KANBAN BOARD (3 COLUMNS) */
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-2.5 pt-1">
|
|
{/* COLUMN 1: TODO */}
|
|
<div className="bg-canvas border border-border flex flex-col">
|
|
<div className="px-3 py-2 bg-panel border-b border-border flex items-center justify-between">
|
|
<span className="text-xs font-semibold text-main flex items-center gap-1.5">
|
|
<span className="w-2 h-2 bg-faint inline-block" />
|
|
<span>Zu erledigen</span>
|
|
</span>
|
|
<span className="badge-neutral font-mono">{todoKanban.length}</span>
|
|
</div>
|
|
|
|
<div className="p-2 space-y-2 min-h-[360px] flex-1 overflow-y-auto">
|
|
{todoKanban.map((task) => (
|
|
<div
|
|
key={task.id}
|
|
className="panel-surface p-2.5 space-y-2 hover:border-strong transition-colors"
|
|
>
|
|
<div className="flex items-start justify-between gap-2">
|
|
<p className="text-xs font-medium text-main break-words flex-1 leading-snug whitespace-pre-wrap">
|
|
{task.content}
|
|
</p>
|
|
<button
|
|
onClick={() => handleConvertToDoc(task)}
|
|
title="Als Notiz vertiefen"
|
|
className="text-faint hover:text-accent flex-shrink-0"
|
|
>
|
|
<FileText className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Notes Preview / Inline Editor */}
|
|
{activeNotesTaskId === task.id ? (
|
|
<div className="space-y-1">
|
|
<textarea
|
|
value={notesDraft}
|
|
onChange={(e) => setNotesDraft(e.target.value)}
|
|
placeholder="Detailnotizen eingeben…"
|
|
rows={2}
|
|
className="w-full input-field p-1.5 text-xs resize-none"
|
|
/>
|
|
<div className="flex justify-end gap-1">
|
|
<button
|
|
onClick={() => setActiveNotesTaskId(null)}
|
|
className="btn-action h-5 px-1.5 text-[10px]"
|
|
>
|
|
Abbr.
|
|
</button>
|
|
<button
|
|
onClick={() => handleSaveNotes(task)}
|
|
className="btn-primary h-5 px-2 text-[10px]"
|
|
>
|
|
Speichern
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : task.notes ? (
|
|
<div
|
|
onClick={() => {
|
|
setActiveNotesTaskId(task.id);
|
|
setNotesDraft(task.notes || '');
|
|
}}
|
|
className="p-1.5 bg-canvas border border-border text-[11px] text-muted break-words cursor-pointer hover:border-strong"
|
|
title="Klicken zum Bearbeiten der Notizen"
|
|
>
|
|
{task.notes}
|
|
</div>
|
|
) : null}
|
|
|
|
{/* Footer & Movement Controls */}
|
|
<div className="flex items-center justify-between gap-1 pt-1 border-t border-subtle text-[11px]">
|
|
<div className="flex items-center gap-1.5 text-muted">
|
|
{task.dueDate && (
|
|
<span className="flex items-center gap-1 text-[10px] text-muted">
|
|
<Calendar className="w-2.5 h-2.5" /> {task.dueDate}
|
|
</span>
|
|
)}
|
|
{!task.notes && (
|
|
<button
|
|
onClick={() => {
|
|
setActiveNotesTaskId(task.id);
|
|
setNotesDraft('');
|
|
}}
|
|
className="text-[10px] text-faint hover:text-main"
|
|
>
|
|
+ Notiz
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
onClick={() => handleKanbanMove(task, 'in_progress')}
|
|
className="btn-action h-6 px-1.5 text-[10px] flex items-center gap-1"
|
|
title="In Bearbeitung verschieben"
|
|
>
|
|
<span>Starten</span>
|
|
<ArrowRight className="w-3 h-3 text-accent" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{todoKanban.length === 0 && (
|
|
<div className="p-4 text-center text-faint text-xs">Keine Aufgaben offen</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* COLUMN 2: IN PROGRESS */}
|
|
<div className="bg-canvas border border-border flex flex-col">
|
|
<div className="px-3 py-2 bg-panel border-b border-border flex items-center justify-between">
|
|
<span className="text-xs font-semibold text-accent flex items-center gap-1.5">
|
|
<span className="w-2 h-2 bg-accent inline-block" />
|
|
<span>In Bearbeitung</span>
|
|
</span>
|
|
<span className="badge-neutral font-mono">{inProgressKanban.length}</span>
|
|
</div>
|
|
|
|
<div className="p-2 space-y-2 min-h-[360px] flex-1 overflow-y-auto">
|
|
{inProgressKanban.map((task) => (
|
|
<div
|
|
key={task.id}
|
|
className="bg-panel border border-accent-border p-2.5 space-y-2 hover:border-accent transition-colors"
|
|
>
|
|
<div className="flex items-start justify-between gap-2">
|
|
<p className="text-xs font-medium text-main break-words flex-1 leading-snug whitespace-pre-wrap">
|
|
{task.content}
|
|
</p>
|
|
<button
|
|
onClick={() => handleConvertToDoc(task)}
|
|
title="Als Notiz vertiefen"
|
|
className="text-faint hover:text-accent flex-shrink-0"
|
|
>
|
|
<FileText className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Notes Preview / Inline Editor */}
|
|
{activeNotesTaskId === task.id ? (
|
|
<div className="space-y-1">
|
|
<textarea
|
|
value={notesDraft}
|
|
onChange={(e) => setNotesDraft(e.target.value)}
|
|
placeholder="Detailnotizen eingeben…"
|
|
rows={2}
|
|
className="w-full input-field p-1.5 text-xs resize-none"
|
|
/>
|
|
<div className="flex justify-end gap-1">
|
|
<button
|
|
onClick={() => setActiveNotesTaskId(null)}
|
|
className="btn-action h-5 px-1.5 text-[10px]"
|
|
>
|
|
Abbr.
|
|
</button>
|
|
<button
|
|
onClick={() => handleSaveNotes(task)}
|
|
className="btn-primary h-5 px-2 text-[10px]"
|
|
>
|
|
Speichern
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : task.notes ? (
|
|
<div
|
|
onClick={() => {
|
|
setActiveNotesTaskId(task.id);
|
|
setNotesDraft(task.notes || '');
|
|
}}
|
|
className="p-1.5 bg-canvas border border-border text-[11px] text-muted break-words cursor-pointer hover:border-strong"
|
|
>
|
|
{task.notes}
|
|
</div>
|
|
) : null}
|
|
|
|
{/* Movement Controls */}
|
|
<div className="flex items-center justify-between gap-1 pt-1 border-t border-subtle text-[11px]">
|
|
<button
|
|
onClick={() => handleKanbanMove(task, 'todo')}
|
|
className="btn-action h-6 px-1.5 text-[10px] flex items-center gap-1"
|
|
title="Zurück zu 'Zu erledigen'"
|
|
>
|
|
<ArrowLeft className="w-3 h-3 text-muted" />
|
|
<span>Zurück</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleKanbanMove(task, 'done')}
|
|
className="btn-primary h-6 px-2 text-[10px] flex items-center gap-1"
|
|
title="Als Erledigt markieren"
|
|
>
|
|
<span>Fertig</span>
|
|
<Check className="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{inProgressKanban.length === 0 && (
|
|
<div className="p-4 text-center text-faint text-xs">Keine aktiven Aufgaben</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* COLUMN 3: DONE */}
|
|
<div className="bg-canvas border border-border flex flex-col">
|
|
<div className="px-3 py-2 bg-panel border-b border-border flex items-center justify-between">
|
|
<span className="text-xs font-semibold text-status-ok flex items-center gap-1.5">
|
|
<span className="w-2 h-2 bg-status-ok inline-block" />
|
|
<span>Erledigt</span>
|
|
</span>
|
|
<span className="badge-neutral font-mono">{doneKanban.length}</span>
|
|
</div>
|
|
|
|
<div className="p-2 space-y-2 min-h-[360px] flex-1 overflow-y-auto">
|
|
{doneKanban.map((task) => (
|
|
<div
|
|
key={task.id}
|
|
className="panel-surface p-2.5 space-y-2 opacity-70 hover:opacity-100 transition-opacity"
|
|
>
|
|
<p className="text-xs font-medium text-muted line-through break-words leading-snug">
|
|
{task.content}
|
|
</p>
|
|
|
|
<div className="flex items-center justify-between gap-1 pt-1 border-t border-subtle text-[11px]">
|
|
<button
|
|
onClick={() => handleKanbanMove(task, 'todo')}
|
|
className="btn-action h-6 px-1.5 text-[10px] flex items-center gap-1"
|
|
title="Wiedereröffnen"
|
|
>
|
|
<RotateCcw className="w-3 h-3 text-accent" />
|
|
<span>Wiedereröffnen</span>
|
|
</button>
|
|
<button
|
|
onClick={() => handleDeletePermanent(task.id)}
|
|
className="text-faint hover:text-status-err p-1"
|
|
title="Löschen"
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{doneKanban.length === 0 && (
|
|
<div className="p-4 text-center text-faint text-xs">
|
|
Noch keine erledigten Aufgaben
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
/* STANDARD LIST VIEW */
|
|
<div className="space-y-2">
|
|
{/* Subfilter tabs for List View */}
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="flex items-center border border-border rounded-btn bg-canvas overflow-hidden">
|
|
<button
|
|
onClick={() => setFilter('open')}
|
|
className={`h-8 px-3.5 text-xs font-medium border-r border-border transition-colors ${
|
|
filter === 'open' ? 'bg-panel text-main' : 'text-muted hover:text-main'
|
|
}`}
|
|
>
|
|
Offen ({openTasks.length})
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setFilter('done')}
|
|
className={`h-8 px-3.5 text-xs font-medium border-r border-border transition-colors ${
|
|
filter === 'done' ? 'bg-panel text-main' : 'text-muted hover:text-main'
|
|
}`}
|
|
>
|
|
Erledigt ({doneTasks.length})
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setFilter('discarded')}
|
|
className={`h-8 px-3.5 text-xs font-medium transition-colors ${
|
|
filter === 'discarded' ? 'bg-panel text-main' : 'text-muted hover:text-main'
|
|
}`}
|
|
>
|
|
Papierkorb ({safeDiscarded.length})
|
|
</button>
|
|
</div>
|
|
|
|
{filter === 'discarded' && safeDiscarded.length > 0 && (
|
|
<button onClick={handlePurgeAllDiscarded} className="btn-action">
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
Papierkorb leeren
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="p-6 text-center text-faint text-sm">Lade Aufgaben…</div>
|
|
) : filter === 'discarded' ? (
|
|
<div className="panel-surface divide-y divide-border">
|
|
{safeDiscarded.length === 0 ? (
|
|
<div className="px-4 py-6 text-center text-muted text-sm">
|
|
Keine verworfenen Gedanken im Papierkorb.
|
|
</div>
|
|
) : (
|
|
safeDiscarded.map((item) => (
|
|
<div
|
|
key={item.id}
|
|
className="px-3 py-2 flex items-center justify-between gap-3 hover:bg-panel-hover"
|
|
>
|
|
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
|
<Trash2 className="w-4 h-4 text-faint flex-shrink-0" />
|
|
<p className="text-sm text-muted line-through truncate">{item.content}</p>
|
|
</div>
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
<button onClick={() => handleRestore(item)} className="btn-action">
|
|
<RotateCcw className="w-3 h-3 text-accent" /> Zurück
|
|
</button>
|
|
<button
|
|
onClick={() => handleDeletePermanent(item.id)}
|
|
className="btn-icon btn-icon-danger"
|
|
title="Endgültig löschen"
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="panel-surface divide-y divide-border">
|
|
{(filter === 'open' ? openTasks : doneTasks).length === 0 ? (
|
|
<div className="px-4 py-6 text-center text-muted text-sm">
|
|
{filter === 'open'
|
|
? 'Keine offenen Aufgaben vorhanden.'
|
|
: 'Noch keine erledigten Aufgaben.'}
|
|
</div>
|
|
) : (
|
|
(filter === 'open' ? openTasks : doneTasks).map((task) => (
|
|
<div
|
|
key={task.id}
|
|
className={`px-3 py-2.5 flex flex-col sm:flex-row sm:items-center justify-between gap-2.5 transition-colors hover:bg-panel-hover ${
|
|
task.isCompleted ? 'opacity-65' : ''
|
|
}`}
|
|
>
|
|
<div className="flex items-start gap-3 min-w-0 flex-1">
|
|
{/* Theme-controlled checkbox (--radius-checkbox) */}
|
|
<button
|
|
onClick={() => handleToggleDone(task)}
|
|
className={`w-4 h-4 mt-0.5 checkbox-square flex items-center justify-center flex-shrink-0 transition-colors border ${
|
|
task.isCompleted
|
|
? 'bg-accent border-accent text-on-accent'
|
|
: 'border-strong hover:border-accent text-transparent'
|
|
}`}
|
|
title={task.isCompleted ? 'Als offen markieren' : 'Als erledigt abhaken'}
|
|
>
|
|
<Check className="w-3 h-3 stroke-[3]" />
|
|
</button>
|
|
|
|
<div className="min-w-0 flex-1 space-y-1">
|
|
<p
|
|
className={`text-sm break-words leading-tight ${
|
|
task.isCompleted ? 'line-through text-faint' : 'text-main'
|
|
}`}
|
|
>
|
|
{task.content}
|
|
</p>
|
|
|
|
{/* Inline Notes or Editor */}
|
|
{activeNotesTaskId === task.id ? (
|
|
<div className="space-y-1 pt-1">
|
|
<textarea
|
|
value={notesDraft}
|
|
onChange={(e) => setNotesDraft(e.target.value)}
|
|
placeholder="Detailnotizen eingeben…"
|
|
rows={2}
|
|
className="w-full input-field p-2 text-xs resize-none"
|
|
/>
|
|
<div className="flex justify-end gap-1.5">
|
|
<button
|
|
onClick={() => setActiveNotesTaskId(null)}
|
|
className="btn-action h-6 px-2 text-xs"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
onClick={() => handleSaveNotes(task)}
|
|
className="btn-primary h-6 px-2.5 text-xs"
|
|
>
|
|
Speichern
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : task.notes ? (
|
|
<p
|
|
onClick={() => {
|
|
setActiveNotesTaskId(task.id);
|
|
setNotesDraft(task.notes || '');
|
|
}}
|
|
className="text-xs text-muted bg-canvas p-1.5 border border-border cursor-pointer hover:border-strong"
|
|
title="Klicken zum Bearbeiten"
|
|
>
|
|
{task.notes}
|
|
</p>
|
|
) : null}
|
|
|
|
<div className="flex flex-wrap items-center gap-1.5 mt-0.5 empty:hidden">
|
|
{task.priority === 'urgent' && (
|
|
<span className="badge-accent">Dringend</span>
|
|
)}
|
|
{task.priority === 'high' && (
|
|
<span className="px-1.5 py-0.5 border border-strong rounded-btn text-main text-[10px] font-medium">
|
|
Wichtig
|
|
</span>
|
|
)}
|
|
{task.kanbanStatus === 'in_progress' && (
|
|
<span className="badge-accent">⚡ In Bearbeitung</span>
|
|
)}
|
|
{task.tags?.map((tag) => (
|
|
<span key={tag} className="badge-neutral">
|
|
#{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Controls */}
|
|
<div className="flex items-center gap-2 self-end sm:self-center flex-shrink-0">
|
|
{!task.notes && activeNotesTaskId !== task.id && (
|
|
<button
|
|
onClick={() => {
|
|
setActiveNotesTaskId(task.id);
|
|
setNotesDraft('');
|
|
}}
|
|
className="btn-action text-xs"
|
|
title="Notizen zur Aufgabe hinzufügen"
|
|
>
|
|
<Edit3 className="w-3 h-3" />
|
|
<span className="hidden md:inline">Notiz</span>
|
|
</button>
|
|
)}
|
|
|
|
<div className="relative flex items-center">
|
|
<Calendar className="w-3 h-3 text-muted absolute left-2 pointer-events-none" />
|
|
<input
|
|
type="date"
|
|
value={task.dueDate || ''}
|
|
onChange={(e) => handleDueDateChange(task, e.target.value)}
|
|
className="h-7 pl-6 pr-2 input-field text-xs"
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => handleConvertToDoc(task)}
|
|
title="Als Notiz / Projekt vertiefen"
|
|
className="btn-action"
|
|
>
|
|
<FileText className="w-3 h-3 text-accent" />
|
|
<span className="hidden md:inline">Vertiefen</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleDeletePermanent(task.id)}
|
|
title="Löschen"
|
|
className="btn-icon btn-icon-danger"
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|