My-Weekly-ToDo-List/src/components/PriorityView.tsx
mARTin-B78 7f850bb4e9
Some checks are pending
Playwright Tests / test (push) Waiting to run
Test and SonarQube / quality (push) Waiting to run
fix: URLs im Aufgabentitel als anklickbaren Link darstellen
Eine im Titel getippte URL war bisher reiner Text innerhalb des klickbaren Bereichs
der Aufgabe -- ein Klick darauf hat je nach Ansicht die Aufgabe durchgestrichen
(Tagesraster, GridTaskBlock) oder den Bearbeiten-Modus geoeffnet (Kanban/Someday/
Liste/Prioritaet, weekly/TaskItem.tsx und PriorityView.tsx), statt den Link zu oeffnen.

- src/lib/weekly/format.ts: neue reine Funktion splitTextAndUrls zerlegt einen Titel
  in Text- und URL-Abschnitte (http(s)://, www.-Adressen). Abschliessende Satzzeichen
  (".", ",", ")", ...) bleiben Text, nicht Teil des Links.
- src/components/weekly/LinkifiedText.tsx: rendert diese Abschnitte, URL-Abschnitte als
  echtes <a target="_blank" rel="noopener noreferrer"> mit stopPropagation, damit ein
  Klick ausschliesslich den Link oeffnet und nicht an den umgebenden Klick-Handler
  durchgereicht wird. Kein Titel ohne URL bekommt ein zusaetzliches Wrapper-Element.
- Eingebaut ueberall dort, wo ein Aufgabentitel bislang als reiner Text stand:
  GridTaskBlock.tsx, weekly/TaskItem.tsx, PriorityView.tsx.

Keine Aenderung an Daten, API oder dem separaten task.url-Feld (der eigene "Termin-
Link"-Button in TaskItem.tsx) -- reine Darstellungsaenderung.

8 neue Unit-Tests fuer splitTextAndUrls (Titel ohne URL, nur URL, URL mit Text davor/
danach, www.-Form, Satzzeichen am Ende, Klammern, zwei URLs im selben Titel).

Verifiziert: Typecheck, Lint (0 Fehler), 108 Unit-Tests (8 neu), Produktions-Build.

v1.128.0

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 14:03:17 +02:00

1391 lines
73 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useMemo, useCallback, useEffect } from "react";
import {
AlertTriangle,
Target,
Clock,
Trash2,
ChevronDown,
ChevronUp,
User,
Sparkles,
X,
Check,
Circle,
Star,
ListOrdered,
BarChart2,
Grid,
SlidersHorizontal,
UserCheck,
SendHorizonal,
Tag,
CalendarDays,
FolderOpen,
List,
Pencil,
FileText,
Link2,
ExternalLink,
} from "lucide-react";
import { LinkifiedText } from "./weekly/LinkifiedText";
export interface PriorityTask {
id: string;
title: string;
completed: boolean;
urgency?: boolean | null;
importance?: boolean | null;
priority?: string | null; // A | B | C | D | E
delegatedTo?: string | null;
delegationNote?: string | null;
scheduledDate?: string | null;
somedayListId?: string | null;
projectId?: string | null;
project?: { id: string; name: string; icon?: string | null; color?: string | null } | null;
subTasks?: PriorityTask[];
dayOfWeek?: number | null;
markdownContent?: string | null;
startTime?: string | null;
url?: string | null;
}
export interface PrioritySomedayList {
id: string;
title: string;
}
export interface PriorityProject {
id: string;
name: string;
color?: string | null;
}
type PriorityMethod = "eisenhower" | "abcde" | "ivylee" | "pareto";
interface PriorityViewProps {
tasks: PriorityTask[];
somedayLists: PrioritySomedayList[];
projects: PriorityProject[];
darkMode: boolean;
language?: string;
onUpdateTask: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
initialMethod?: PriorityMethod;
onMethodChange?: (m: PriorityMethod) => void;
}
const ABCDE_LABELS: Record<string, { label: string; desc: string; color: string; bg: string }> = {
A: { label: "A — Critical", desc: "Must do. Serious consequences if not done.", color: "#ef4444", bg: "#fef2f2" },
B: { label: "B — Should do", desc: "Mild consequences if not done.", color: "#f97316", bg: "#fff7ed" },
C: { label: "C — Nice to do", desc: "No consequences if not done.", color: "#eab308", bg: "#fefce8" },
D: { label: "D — Delegate", desc: "Someone else can do this.", color: "#3b82f6", bg: "#eff6ff" },
E: { label: "E — Eliminate", desc: "Delete — no real value.", color: "#6b7280", bg: "#f9fafb" },
};
const ABCDE_LABELS_DARK: Record<string, { bg: string }> = {
A: { bg: "#450a0a" },
B: { bg: "#431407" },
C: { bg: "#422006" },
D: { bg: "#1e3a5f" },
E: { bg: "#1f2937" },
};
const ABCDE_LABELS_DE: Record<string, { label: string; desc: string }> = {
A: { label: "A — Kritisch", desc: "Muss erledigt werden. Ernste Konsequenzen." },
B: { label: "B — Sollte erledigt werden", desc: "Leichte Konsequenzen wenn nicht erledigt." },
C: { label: "C — Schön wenn erledigt", desc: "Keine Konsequenzen wenn nicht erledigt." },
D: { label: "D — Delegieren", desc: "Kann von jemand anderem erledigt werden." },
E: { label: "E — Eliminieren", desc: "Streichen — kein echter Mehrwert." },
};
function today(): string {
return new Date().toISOString().split("T")[0];
}
function getWeekRange(offsetWeeks = 0): [string, string] {
const now = new Date();
const day = now.getDay();
const mon = new Date(now);
mon.setDate(now.getDate() - day + (day === 0 ? -6 : 1) + offsetWeeks * 7);
const sun = new Date(mon);
sun.setDate(mon.getDate() + 6);
return [mon.toISOString().split("T")[0], sun.toISOString().split("T")[0]];
}
function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return "";
const d = new Date(dateStr);
return d.toLocaleDateString("default", { month: "short", day: "numeric" });
}
export default function PriorityView({
tasks,
somedayLists,
projects,
darkMode,
language = "en",
onUpdateTask,
initialMethod,
onMethodChange,
}: PriorityViewProps) {
const [method, setMethodState] = useState<PriorityMethod>(initialMethod || "eisenhower");
const setMethod = useCallback((m: PriorityMethod) => {
setMethodState(m);
onMethodChange?.(m);
}, [onMethodChange]);
const [filterProject, setFilterProject] = useState("");
const [filterList, setFilterList] = useState("");
const [filterTimespan, setFilterTimespan] = useState("all");
// Optimistic set: tasks marked complete inside this view disappear immediately,
// without waiting for the API round-trip + parent re-render.
const [locallyDone, setLocallyDone] = useState<Set<string>>(new Set());
const [delegateModal, setDelegateModal] = useState<PriorityTask | null>(null);
const [delegateTo, setDelegateTo] = useState("");
const [delegateNote, setDelegateNote] = useState("");
const [delegateType, setDelegateType] = useState<"person" | "ai">("person");
const [ivyLeeSelected, setIvyLeeSelected] = useState<Set<string>>(() => {
// Hydrate from any task that already has a numeric priority "1"-"6"
const init = new Set<string>();
const ranked = tasks
.filter((t) => t.priority && /^[1-6]$/.test(t.priority))
.sort((a, b) => Number(a.priority) - Number(b.priority));
for (const t of ranked) init.add(t.id);
return init;
});
const [showFilters, setShowFilters] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const de = language === "de";
useEffect(() => {
const check = () => setIsMobile(window.innerWidth < 768);
check();
window.addEventListener("resize", check);
return () => window.removeEventListener("resize", check);
}, []);
// --- Filter tasks ---
const filteredTasks = useMemo(() => {
let result = tasks.filter((t) => !t.id.startsWith("virtual-"));
// Priority view is "what to do next" — always hide completed tasks.
result = result.filter((t) => !t.completed && !locallyDone.has(t.id));
if (filterProject) result = result.filter((t) => t.projectId === filterProject);
if (filterList) {
if (filterList === "__scheduled__") {
result = result.filter((t) => t.scheduledDate || t.dayOfWeek != null);
} else if (filterList === "__unscheduled__") {
result = result.filter((t) => !t.scheduledDate && t.dayOfWeek == null && !t.somedayListId);
} else {
result = result.filter((t) => t.somedayListId === filterList);
}
}
if (filterTimespan !== "all") {
const todayStr = today();
const [thisWeekStart, thisWeekEnd] = getWeekRange(0);
const [nextWeekStart, nextWeekEnd] = getWeekRange(1);
result = result.filter((t) => {
const d = t.scheduledDate?.split("T")[0];
if (filterTimespan === "today") return d === todayStr;
if (filterTimespan === "this-week") return d && d >= thisWeekStart && d <= thisWeekEnd;
if (filterTimespan === "next-week") return d && d >= nextWeekStart && d <= nextWeekEnd;
if (filterTimespan === "unscheduled") return !t.scheduledDate && t.dayOfWeek == null;
return true;
});
}
return result;
}, [tasks, locallyDone, filterProject, filterList, filterTimespan]);
// --- Eisenhower quadrants ---
const eisenhowerQuadrants = useMemo(() => {
const q1: PriorityTask[] = []; // Urgent + Important → Do
const q2: PriorityTask[] = []; // Not Urgent + Important → Schedule
const q3: PriorityTask[] = []; // Urgent + Not Important → Delegate
const q4: PriorityTask[] = []; // Not Urgent + Not Important → Delete
const unset: PriorityTask[] = [];
for (const t of filteredTasks) {
if (t.urgency == null && t.importance == null) { unset.push(t); continue; }
if (t.urgency && t.importance) q1.push(t);
else if (!t.urgency && t.importance) q2.push(t);
else if (t.urgency && !t.importance) q3.push(t);
else q4.push(t);
}
return { q1, q2, q3, q4, unset };
}, [filteredTasks]);
// --- ABCDE groups ---
const abcdeGroups = useMemo(() => {
const groups: Record<string, PriorityTask[]> = { A: [], B: [], C: [], D: [], E: [], none: [] };
for (const t of filteredTasks) {
if (t.priority && groups[t.priority]) groups[t.priority].push(t);
else groups.none.push(t);
}
return groups;
}, [filteredTasks]);
// --- Pareto: top 20% by importance ---
const paretoSplit = useMemo(() => {
const important = filteredTasks.filter((t) => t.importance === true || t.priority === "A" || t.priority === "B");
const rest = filteredTasks.filter((t) => !(t.importance === true || t.priority === "A" || t.priority === "B"));
return { important, rest };
}, [filteredTasks]);
// --- Handlers ---
const setEisenhower = useCallback(async (task: PriorityTask, urgency: boolean, importance: boolean) => {
await onUpdateTask(task.id, { urgency, importance });
}, [onUpdateTask]);
const clearEisenhower = useCallback(async (task: PriorityTask) => {
await onUpdateTask(task.id, { urgency: null, importance: null });
}, [onUpdateTask]);
const setAbcde = useCallback(async (task: PriorityTask, grade: string | null) => {
await onUpdateTask(task.id, { priority: grade });
}, [onUpdateTask]);
const handleDelegate = useCallback(async () => {
if (!delegateModal) return;
await onUpdateTask(delegateModal.id, {
delegatedTo: delegateTo || (delegateType === "ai" ? "AI" : undefined),
delegationNote: delegateNote || undefined,
});
setDelegateModal(null);
setDelegateTo("");
setDelegateNote("");
}, [delegateModal, delegateTo, delegateNote, delegateType, onUpdateTask]);
const toggleComplete = useCallback(async (task: PriorityTask) => {
if (!task.completed) {
// Optimistically hide immediately so the task doesn't linger
// while the API call + parent re-render cycle completes.
setLocallyDone((prev) => new Set([...prev, task.id]));
} else {
setLocallyDone((prev) => { const n = new Set(prev); n.delete(task.id); return n; });
}
await onUpdateTask(task.id, { completed: !task.completed });
}, [onUpdateTask]);
const toggleIvyLee = useCallback((id: string) => {
setIvyLeeSelected((prev) => {
const next = new Set(prev);
const removing = next.has(id);
if (removing) {
next.delete(id);
onUpdateTask(id, { priority: null });
} else {
if (next.size >= 6) return prev;
next.add(id);
}
// Re-rank all selected tasks 1..N so cross-view badges stay consistent
Array.from(next).forEach((tid, idx) => {
onUpdateTask(tid, { priority: String(idx + 1) });
});
return next;
});
}, [onUpdateTask]);
const openDelegate = useCallback((task: PriorityTask) => {
setDelegateModal(task);
setDelegateTo(task.delegatedTo || "");
setDelegateNote(task.delegationNote || "");
setDelegateType(task.delegatedTo === "AI" ? "ai" : "person");
}, []);
// ---------- STYLES ----------
const bg = darkMode ? "#111827" : "#f9fafb";
const cardBg = darkMode ? "#1f2937" : "#ffffff";
const border = darkMode ? "#374151" : "#e5e7eb";
const textPrimary = darkMode ? "#f9fafb" : "#111827";
const textSecondary = darkMode ? "#9ca3af" : "#6b7280";
const accentColor = "#6366f1";
const methodButtons: { key: PriorityMethod; label: string; icon: React.ReactNode }[] = [
{ key: "eisenhower", label: "Eisenhower", icon: <Grid size={14} /> },
{ key: "abcde", label: "ABCDE", icon: <Tag size={14} /> },
{ key: "ivylee", label: "Ivy Lee", icon: <ListOrdered size={14} /> },
{ key: "pareto", label: "80/20", icon: <BarChart2 size={14} /> },
];
// Filter panel — always rendered in the right column of every mode
const filterPanel = (
<div style={{ background: cardBg, border: `1px solid ${border}`, borderRadius: "10px", padding: "10px", display: "flex", flexWrap: "wrap", gap: "8px" }}>
<div style={{ display: "flex", flexDirection: "column", gap: "3px", flex: "1 1 120px" }}>
<label style={{ fontSize: "0.68rem", fontWeight: 600, color: textSecondary, textTransform: "uppercase", letterSpacing: "0.05em" }}>
<FolderOpen size={9} style={{ display: "inline", marginRight: "3px" }} />
{de ? "Projekt" : "Project"}
</label>
<select
value={filterProject}
onChange={(e) => setFilterProject(e.target.value)}
style={{ padding: "4px 7px", borderRadius: "6px", border: `1px solid ${border}`, background: darkMode ? "#374151" : "#f3f4f6", color: textPrimary, fontSize: "0.8rem", cursor: "pointer" }}
>
<option value="">{de ? "Alle Projekte" : "All projects"}</option>
{projects.map((p) => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "3px", flex: "1 1 130px" }}>
<label style={{ fontSize: "0.68rem", fontWeight: 600, color: textSecondary, textTransform: "uppercase", letterSpacing: "0.05em" }}>
<List size={9} style={{ display: "inline", marginRight: "3px" }} />
{de ? "Liste" : "List"}
</label>
<select
value={filterList}
onChange={(e) => setFilterList(e.target.value)}
style={{ padding: "4px 7px", borderRadius: "6px", border: `1px solid ${border}`, background: darkMode ? "#374151" : "#f3f4f6", color: textPrimary, fontSize: "0.8rem", cursor: "pointer" }}
>
<option value="">{de ? "Alle Listen" : "All lists"}</option>
<option value="__scheduled__">{de ? "Geplante Aufgaben" : "Scheduled"}</option>
<option value="__unscheduled__">{de ? "Ungeplant" : "Unscheduled"}</option>
{somedayLists.map((l) => (
<option key={l.id} value={l.id}>{l.title}</option>
))}
</select>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "3px", flex: "1 1 120px" }}>
<label style={{ fontSize: "0.68rem", fontWeight: 600, color: textSecondary, textTransform: "uppercase", letterSpacing: "0.05em" }}>
<CalendarDays size={9} style={{ display: "inline", marginRight: "3px" }} />
{de ? "Zeitraum" : "Timespan"}
</label>
<select
value={filterTimespan}
onChange={(e) => setFilterTimespan(e.target.value)}
style={{ padding: "4px 7px", borderRadius: "6px", border: `1px solid ${border}`, background: darkMode ? "#374151" : "#f3f4f6", color: textPrimary, fontSize: "0.8rem", cursor: "pointer" }}
>
<option value="all">{de ? "Alle" : "All time"}</option>
<option value="today">{de ? "Heute" : "Today"}</option>
<option value="this-week">{de ? "Diese Woche" : "This week"}</option>
<option value="next-week">{de ? "Nächste Woche" : "Next week"}</option>
<option value="unscheduled">{de ? "Ungeplant" : "Unscheduled"}</option>
</select>
</div>
<div style={{ display: "flex", alignItems: "flex-end", paddingBottom: "2px" }}>
<span style={{ fontSize: "0.75rem", color: textSecondary }}>
{filteredTasks.length} {de ? "Aufgaben" : "tasks"}
</span>
</div>
</div>
);
return (
<div style={{ background: bg, minHeight: "100%", padding: "12px", boxSizing: "border-box", fontFamily: "inherit" }}>
{/* ─── Header ─── */}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "10px", flexWrap: "wrap", gap: "8px" }}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Target size={18} style={{ color: accentColor }} />
<span style={{ fontSize: "1rem", fontWeight: 700, color: textPrimary }}>
{de ? "Prioritäten" : "Priority View"}
</span>
</div>
{/* Filter active indicator — filters are always in the right column */}
{(filterProject || filterList || filterTimespan !== "all") && (
<span style={{ display: "flex", alignItems: "center", gap: "4px", fontSize: "0.75rem", color: accentColor, fontWeight: 600 }}>
<SlidersHorizontal size={12} />
{de ? "Filter aktiv" : "Filter active"}
</span>
)}
</div>
{/* ─── Method Tabs ─── */}
<div style={{ display: "flex", gap: "4px", marginBottom: "14px", background: darkMode ? "#1f2937" : "#e5e7eb", borderRadius: "9px", padding: "3px" }}>
{methodButtons.map((m) => (
<button
key={m.key}
onClick={() => setMethod(m.key)}
style={{
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "5px",
padding: "7px 4px",
borderRadius: "6px",
border: "none",
cursor: "pointer",
fontSize: "0.78rem",
fontWeight: method === m.key ? 700 : 400,
background: method === m.key ? (darkMode ? "#374151" : "#fff") : "transparent",
color: method === m.key ? (darkMode ? "#f9fafb" : "#111827") : textSecondary,
boxShadow: method === m.key ? "0 1px 3px rgba(0,0,0,0.1)" : "none",
transition: "all 0.15s",
}}
>
{m.icon}
<span>{m.label}</span>
</button>
))}
</div>
{/* ─── METHOD: EISENHOWER ─── */}
{method === "eisenhower" && (
<EisenhowerMatrix
q1={eisenhowerQuadrants.q1}
q2={eisenhowerQuadrants.q2}
q3={eisenhowerQuadrants.q3}
q4={eisenhowerQuadrants.q4}
unset={eisenhowerQuadrants.unset}
allTasks={tasks}
darkMode={darkMode}
de={de}
onSetQuadrant={setEisenhower}
onClear={clearEisenhower}
onDelegate={openDelegate}
onToggleComplete={toggleComplete}
onUpdateTask={onUpdateTask}
projects={projects}
cardBg={cardBg}
border={border}
textPrimary={textPrimary}
textSecondary={textSecondary}
isMobile={isMobile}
filterPanel={filterPanel}
/>
)}
{/* ─── METHOD: ABCDE ─── */}
{method === "abcde" && (
<AbcdeView
groups={abcdeGroups}
allTasks={tasks}
darkMode={darkMode}
de={de}
onSetGrade={setAbcde}
onDelegate={openDelegate}
onToggleComplete={toggleComplete}
onUpdateTask={onUpdateTask}
projects={projects}
cardBg={cardBg}
border={border}
textPrimary={textPrimary}
textSecondary={textSecondary}
isMobile={isMobile}
filterPanel={filterPanel}
/>
)}
{/* ─── METHOD: IVY LEE ─── */}
{method === "ivylee" && (
<IvyLeeView
tasks={filteredTasks}
selected={ivyLeeSelected}
darkMode={darkMode}
de={de}
onToggle={toggleIvyLee}
onDelegate={openDelegate}
onToggleComplete={toggleComplete}
onUpdateTask={onUpdateTask}
projects={projects}
cardBg={cardBg}
border={border}
textPrimary={textPrimary}
textSecondary={textSecondary}
isMobile={isMobile}
filterPanel={filterPanel}
/>
)}
{/* ─── METHOD: PARETO ─── */}
{method === "pareto" && (
<ParetoView
important={paretoSplit.important}
rest={paretoSplit.rest}
darkMode={darkMode}
de={de}
onDelegate={openDelegate}
onToggleComplete={toggleComplete}
onUpdateTask={onUpdateTask}
projects={projects}
cardBg={cardBg}
border={border}
textPrimary={textPrimary}
textSecondary={textSecondary}
isMobile={isMobile}
filterPanel={filterPanel}
/>
)}
{/* ─── Delegate Modal ─── */}
{delegateModal && (
<div
style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", zIndex: 9000, display: "flex", alignItems: "center", justifyContent: "center", padding: "16px" }}
onClick={() => setDelegateModal(null)}
>
<div
style={{ background: cardBg, borderRadius: "14px", padding: "24px", width: "100%", maxWidth: "420px", boxShadow: "0 20px 40px rgba(0,0,0,0.3)" }}
onClick={(e) => e.stopPropagation()}
>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "16px" }}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<UserCheck size={18} style={{ color: accentColor }} />
<span style={{ fontWeight: 700, color: textPrimary, fontSize: "1rem" }}>
{de ? "Aufgabe delegieren" : "Delegate task"}
</span>
</div>
<button onClick={() => setDelegateModal(null)} style={{ background: "none", border: "none", cursor: "pointer", color: textSecondary }}>
<X size={18} />
</button>
</div>
<p style={{ fontSize: "0.88rem", color: textSecondary, marginBottom: "16px", lineHeight: 1.4 }}>
&ldquo;{delegateModal.title}&rdquo;
</p>
{/* Type toggle */}
<div style={{ display: "flex", gap: "6px", marginBottom: "14px" }}>
<button
onClick={() => setDelegateType("person")}
style={{ flex: 1, padding: "8px", borderRadius: "8px", border: `1px solid ${delegateType === "person" ? accentColor : border}`, background: delegateType === "person" ? `${accentColor}22` : "transparent", color: delegateType === "person" ? accentColor : textSecondary, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: "6px", fontSize: "0.85rem", fontWeight: delegateType === "person" ? 600 : 400 }}
>
<User size={14} /> {de ? "Person" : "Person"}
</button>
<button
onClick={() => setDelegateType("ai")}
style={{ flex: 1, padding: "8px", borderRadius: "8px", border: `1px solid ${delegateType === "ai" ? "#8b5cf6" : border}`, background: delegateType === "ai" ? "#8b5cf622" : "transparent", color: delegateType === "ai" ? "#8b5cf6" : textSecondary, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: "6px", fontSize: "0.85rem", fontWeight: delegateType === "ai" ? 600 : 400 }}
>
<Sparkles size={14} /> AI
</button>
</div>
{delegateType === "person" ? (
<>
<label style={{ fontSize: "0.78rem", fontWeight: 600, color: textSecondary, display: "block", marginBottom: "6px" }}>
{de ? "Name oder E-Mail" : "Name or email"}
</label>
<input
autoFocus
value={delegateTo}
onChange={(e) => setDelegateTo(e.target.value)}
placeholder={de ? "z.B. Max Mustermann" : "e.g. John Doe"}
style={{ width: "100%", padding: "8px 10px", borderRadius: "7px", border: `1px solid ${border}`, background: darkMode ? "#374151" : "#f9fafb", color: textPrimary, fontSize: "0.9rem", boxSizing: "border-box", marginBottom: "12px", outline: "none" }}
/>
</>
) : (
<div style={{ background: darkMode ? "#374151" : "#f3f4f6", borderRadius: "8px", padding: "10px 12px", marginBottom: "12px" }}>
<p style={{ fontSize: "0.8rem", color: textSecondary, margin: 0, lineHeight: 1.5 }}>
{de
? "Die KI übernimmt diese Aufgabe. Füge eine Notiz hinzu, was sie wissen muss."
: "Assign this task to AI. Add a note about what it needs to know."}
</p>
</div>
)}
<label style={{ fontSize: "0.78rem", fontWeight: 600, color: textSecondary, display: "block", marginBottom: "6px" }}>
{de ? "Hinweis / Anweisung" : "Note / Instructions"}
</label>
<textarea
value={delegateNote}
onChange={(e) => setDelegateNote(e.target.value)}
placeholder={de ? "Was soll die Person / KI wissen?" : "What should the person / AI know?"}
rows={3}
style={{ width: "100%", padding: "8px 10px", borderRadius: "7px", border: `1px solid ${border}`, background: darkMode ? "#374151" : "#f9fafb", color: textPrimary, fontSize: "0.85rem", boxSizing: "border-box", resize: "vertical", outline: "none", marginBottom: "16px", fontFamily: "inherit" }}
/>
<div style={{ display: "flex", gap: "8px" }}>
<button
onClick={() => setDelegateModal(null)}
style={{ flex: 1, padding: "9px", borderRadius: "8px", border: `1px solid ${border}`, background: "transparent", color: textSecondary, cursor: "pointer", fontSize: "0.88rem" }}
>
{de ? "Abbrechen" : "Cancel"}
</button>
{delegateModal.delegatedTo && (
<button
onClick={async () => {
await onUpdateTask(delegateModal.id, { delegatedTo: null, delegationNote: null });
setDelegateModal(null);
}}
style={{ padding: "9px 14px", borderRadius: "8px", border: `1px solid ${border}`, background: "transparent", color: "#ef4444", cursor: "pointer", fontSize: "0.88rem" }}
title={de ? "Delegation entfernen" : "Remove delegation"}
>
<X size={14} />
</button>
)}
<button
onClick={handleDelegate}
disabled={delegateType === "person" && !delegateTo.trim()}
style={{ flex: 1, padding: "9px", borderRadius: "8px", border: "none", background: delegateType === "ai" ? "#8b5cf6" : accentColor, color: "#fff", cursor: "pointer", fontSize: "0.88rem", fontWeight: 600, display: "flex", alignItems: "center", justifyContent: "center", gap: "6px", opacity: (delegateType === "person" && !delegateTo.trim()) ? 0.5 : 1 }}
>
<SendHorizonal size={14} />
{de ? "Delegieren" : "Delegate"}
</button>
</div>
</div>
</div>
)}
</div>
);
}
// ─────────────────────────────────────────────
// SHARED TASK CARD
// ─────────────────────────────────────────────
interface TaskCardProps {
task: PriorityTask;
darkMode: boolean;
cardBg: string;
border: string;
textPrimary: string;
textSecondary: string;
onDelegate: (t: PriorityTask) => void;
onToggleComplete: (t: PriorityTask) => void;
onUpdateTask?: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
projects?: PriorityProject[];
onRemove?: () => void;
children?: React.ReactNode;
de?: boolean;
}
function TaskCard({ task, darkMode, cardBg, border, textPrimary, textSecondary, onDelegate, onToggleComplete, onUpdateTask, projects, onRemove, children, de }: TaskCardProps) {
const [editingTitle, setEditingTitle] = useState(false);
const [titleVal, setTitleVal] = useState(task.title);
const [showNote, setShowNote] = useState(false);
const [noteVal, setNoteVal] = useState(task.markdownContent || "");
const [showUrl, setShowUrl] = useState(false);
const [urlVal, setUrlVal] = useState(task.url || "");
const [showProject, setShowProject] = useState(false);
const saveTitle = () => {
if (onUpdateTask && titleVal.trim() && titleVal.trim() !== task.title) {
onUpdateTask(task.id, { title: titleVal.trim() });
}
setEditingTitle(false);
};
const iconBtn = (onClick: (e: React.MouseEvent) => void, title: string, active: boolean, activeColor: string, icon: React.ReactNode) => (
<button
onClick={(e) => { e.stopPropagation(); onClick(e); }}
title={title}
style={{ background: "none", border: "none", cursor: "pointer", padding: "2px 3px", borderRadius: "3px", color: active ? activeColor : "#9ca3af", flexShrink: 0, display: "flex", alignItems: "center" }}
>
{icon}
</button>
);
return (
<div
draggable
onDragStart={(e) => { e.dataTransfer.setData("text/plain", task.id); e.dataTransfer.effectAllowed = "move"; }}
style={{
background: task.completed ? (darkMode ? "#111827" : "#f9fafb") : cardBg,
border: `1px solid ${border}`,
borderRadius: "7px",
padding: "4px 6px",
opacity: task.completed ? 0.6 : 1,
cursor: "grab",
display: "flex",
flexDirection: "column",
gap: "2px",
}}
>
{/* Main row */}
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
{/* Complete toggle */}
<button
onClick={() => onToggleComplete(task)}
style={{ background: "none", border: "none", cursor: "pointer", padding: 0, flexShrink: 0, display: "flex", alignItems: "center" }}
>
{task.completed ? <Check size={13} style={{ color: "#059669" }} /> : <Circle size={13} style={{ color: "#9ca3af" }} />}
</button>
{/* Date */}
{task.scheduledDate && (
<span style={{ fontSize: "0.63rem", color: "#6b7280", flexShrink: 0, fontVariantNumeric: "tabular-nums" }}>
{formatDate(task.scheduledDate)}
</span>
)}
{/* Title */}
{editingTitle ? (
<input
value={titleVal}
onChange={(e) => setTitleVal(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") saveTitle(); if (e.key === "Escape") { setTitleVal(task.title); setEditingTitle(false); } }}
onBlur={saveTitle}
autoFocus
onClick={(e) => e.stopPropagation()}
style={{ flex: 1, fontSize: "0.82rem", fontWeight: 500, border: `1px solid ${border}`, borderRadius: "4px", padding: "1px 4px", background: darkMode ? "#374151" : "#fff", color: textPrimary, outline: "none", minWidth: 0 }}
/>
) : (
<span
style={{ flex: 1, fontSize: "0.82rem", fontWeight: 500, color: textPrimary, textDecoration: task.completed ? "line-through" : "none", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 }}
title={task.title}
onDoubleClick={() => setEditingTitle(true)}
>
<LinkifiedText text={task.title} />
</span>
)}
{/* Action icons */}
{iconBtn(() => setEditingTitle(true), de ? "Umbenennen" : "Rename", false, "#6366f1", <Pencil size={11} />)}
{iconBtn(() => { setShowNote((v) => !v); if (!showNote) setNoteVal(task.markdownContent || ""); }, de ? "Notiz" : "Note", !!task.markdownContent || showNote, "#6366f1", <FileText size={11} />)}
{iconBtn(() => { setShowUrl((v) => !v); if (!showUrl) setUrlVal(task.url || ""); }, "Link", !!task.url || showUrl, "#3b82f6", <Link2 size={11} />)}
{projects && projects.length > 0 && iconBtn(() => setShowProject((v) => !v), de ? "Projekt" : "Project", !!task.projectId || showProject, "#059669", <FolderOpen size={11} />)}
{iconBtn(() => onDelegate(task), de ? "Delegieren" : "Delegate", !!task.delegatedTo, "#8b5cf6", <UserCheck size={11} />)}
{onRemove && iconBtn(() => onRemove(), de ? "Entfernen" : "Remove", false, "#ef4444", <X size={11} />)}
</div>
{/* Badges */}
{(task.project || task.delegatedTo) && (
<div style={{ display: "flex", gap: "3px", paddingLeft: "17px", flexWrap: "wrap" }}>
{task.project && (
<span style={{ fontSize: "0.62rem", color: task.project.color || "#6366f1", background: `${task.project.color || "#6366f1"}20`, borderRadius: "3px", padding: "0 4px" }}>
{task.project.name}
</span>
)}
{task.delegatedTo && (
<span style={{ fontSize: "0.62rem", color: "#8b5cf6", background: "#8b5cf620", borderRadius: "3px", padding: "0 4px", display: "flex", alignItems: "center", gap: "2px" }}>
{task.delegatedTo === "AI" ? <Sparkles size={8} /> : <User size={8} />}
{task.delegatedTo}
</span>
)}
</div>
)}
{/* Note editor */}
{showNote && (
<textarea
value={noteVal}
onChange={(e) => setNoteVal(e.target.value)}
onBlur={() => { if (onUpdateTask) onUpdateTask(task.id, { markdownContent: noteVal || null }); }}
placeholder={de ? "Notiz..." : "Note..."}
rows={2}
style={{ fontSize: "0.78rem", border: `1px solid ${border}`, borderRadius: "5px", padding: "4px 6px", background: darkMode ? "#374151" : "#f9fafb", color: textPrimary, resize: "vertical", fontFamily: "inherit", outline: "none", width: "100%", boxSizing: "border-box", marginTop: "2px" }}
/>
)}
{/* URL input */}
{showUrl && (
<div style={{ display: "flex", gap: "4px", alignItems: "center", marginTop: "2px" }}>
<input
value={urlVal}
onChange={(e) => setUrlVal(e.target.value)}
onBlur={() => { if (onUpdateTask) onUpdateTask(task.id, { url: urlVal || null }); }}
onKeyDown={(e) => { if (e.key === "Enter" && onUpdateTask) { onUpdateTask(task.id, { url: urlVal || null }); setShowUrl(false); } }}
placeholder="https://..."
style={{ flex: 1, fontSize: "0.78rem", border: `1px solid ${border}`, borderRadius: "5px", padding: "3px 6px", background: darkMode ? "#374151" : "#f9fafb", color: textPrimary, outline: "none" }}
/>
{task.url && (
<a href={task.url} target="_blank" rel="noopener noreferrer" style={{ color: "#3b82f6", flexShrink: 0, display: "flex" }}>
<ExternalLink size={12} />
</a>
)}
</div>
)}
{/* Project selector */}
{showProject && projects && (
<select
value={task.projectId || ""}
onChange={(e) => { if (onUpdateTask) { onUpdateTask(task.id, { projectId: e.target.value || null }); setShowProject(false); } }}
style={{ fontSize: "0.78rem", border: `1px solid ${border}`, borderRadius: "5px", padding: "3px 6px", background: darkMode ? "#374151" : "#f9fafb", color: textPrimary, outline: "none", width: "100%", cursor: "pointer", marginTop: "2px" }}
>
<option value="">{de ? "Kein Projekt" : "No project"}</option>
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
)}
{/* Children (quick-assign buttons for unset/ungraded sections) */}
{children}
</div>
);
}
// ─────────────────────────────────────────────
// EISENHOWER MATRIX
// ─────────────────────────────────────────────
interface EisenhowerProps {
q1: PriorityTask[];
q2: PriorityTask[];
q3: PriorityTask[];
q4: PriorityTask[];
unset: PriorityTask[];
allTasks: PriorityTask[];
darkMode: boolean;
de: boolean;
onSetQuadrant: (t: PriorityTask, urgency: boolean, importance: boolean) => void;
onClear: (t: PriorityTask) => void;
onDelegate: (t: PriorityTask) => void;
onToggleComplete: (t: PriorityTask) => void;
onUpdateTask: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
projects: PriorityProject[];
cardBg: string;
border: string;
textPrimary: string;
textSecondary: string;
isMobile?: boolean;
filterPanel?: React.ReactNode;
}
const QUADRANT_CONFIG = [
{ urgency: true, importance: true, label: "Do", labelDe: "Sofort erledigen", desc: "Urgent & Important", descDe: "Dringend & Wichtig", color: "#ef4444", bgLight: "#fef2f2", bgDark: "#450a0a", icon: <AlertTriangle size={14} /> },
{ urgency: false, importance: true, label: "Schedule", labelDe: "Planen", desc: "Important, Not Urgent", descDe: "Wichtig, nicht dringend", color: "#3b82f6", bgLight: "#eff6ff", bgDark: "#1e3a5f", icon: <Clock size={14} /> },
{ urgency: true, importance: false, label: "Delegate", labelDe: "Delegieren", desc: "Urgent, Not Important", descDe: "Dringend, nicht wichtig", color: "#f97316", bgLight: "#fff7ed", bgDark: "#431407", icon: <User size={14} /> },
{ urgency: false, importance: false, label: "Delete", labelDe: "Eliminieren", desc: "Neither", descDe: "Weder noch", color: "#6b7280", bgLight: "#f9fafb", bgDark: "#1f2937", icon: <Trash2 size={14} /> },
];
function EisenhowerMatrix({ q1, q2, q3, q4, unset, allTasks, darkMode, de, onSetQuadrant, onClear, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary, isMobile, filterPanel }: EisenhowerProps) {
const quadrantTasks = [q1, q2, q3, q4];
const [collapsed, setCollapsed] = useState<Record<number, boolean>>({});
const [dragOver, setDragOver] = useState<number | null>(null);
const handleQuadrantDrop = (e: React.DragEvent, qc: typeof QUADRANT_CONFIG[0]) => {
e.preventDefault();
const taskId = e.dataTransfer.getData("text/plain");
const task = allTasks.find((t) => t.id === taskId);
if (task) onSetQuadrant(task, qc.urgency, qc.importance);
setDragOver(null);
};
// 2×2 quadrant grid
const quadrantGrid = (
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "10px" }}>
{QUADRANT_CONFIG.map((qc, idx) => {
const tasks = quadrantTasks[idx];
const isCollapsed = collapsed[idx];
const isOver = dragOver === idx;
const quadBg = darkMode ? qc.bgDark : qc.bgLight;
return (
<div
key={idx}
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDragOver(idx); }}
onDragLeave={(e) => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setDragOver(null); }}
onDrop={(e) => handleQuadrantDrop(e, qc)}
style={{
background: isOver ? `${qc.color}30` : quadBg,
borderRadius: "10px",
border: isOver ? `2px dashed ${qc.color}` : `1px solid ${border}`,
overflow: "hidden",
transition: "border 0.15s, background 0.15s",
}}
>
{/* Quadrant header */}
<div
style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "8px 12px", cursor: "pointer", borderBottom: isCollapsed ? "none" : `1px solid ${border}` }}
onClick={() => setCollapsed((p) => ({ ...p, [idx]: !p[idx] }))}
>
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
<span style={{ color: qc.color }}>{qc.icon}</span>
<div>
<div style={{ fontSize: "0.82rem", fontWeight: 700, color: qc.color }}>{de ? qc.labelDe : qc.label}</div>
<div style={{ fontSize: "0.68rem", color: textSecondary }}>{de ? qc.descDe : qc.desc}</div>
</div>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: qc.color, background: `${qc.color}22`, borderRadius: "9px", padding: "1px 7px" }}>{tasks.length}</span>
{isCollapsed ? <ChevronDown size={14} style={{ color: textSecondary }} /> : <ChevronUp size={14} style={{ color: textSecondary }} />}
</div>
</div>
{/* Tasks */}
{!isCollapsed && (
<div style={{ padding: "8px", display: "flex", flexDirection: "column", gap: "6px", maxHeight: "260px", overflowY: "auto", minHeight: "44px" }}>
{tasks.length === 0 && (
<p style={{ fontSize: "0.75rem", color: isOver ? qc.color : textSecondary, textAlign: "center", padding: "8px 0", margin: 0 }}>
{isOver ? (de ? "Hier ablegen" : "Drop here") : "—"}
</p>
)}
{tasks.map((t) => (
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} onRemove={() => onClear(t)} de={de} />
))}
</div>
)}
{/* Collapsed drop hint */}
{isCollapsed && isOver && (
<div style={{ padding: "6px 12px", fontSize: "0.72rem", color: qc.color, textAlign: "center" }}>
{de ? "Hier ablegen" : "Drop here"}
</div>
)}
</div>
);
})}
</div>
);
// Right panel: filters + unassigned tasks
const rightPanel = (
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
{/* Filters always visible in Eisenhower right column */}
{filterPanel}
{/* Unassigned tasks */}
<div style={{ background: darkMode ? "#1f2937" : "#f3f4f6", borderRadius: "10px", border: `1px solid ${border}`, padding: "12px" }}>
<p style={{ fontSize: "0.78rem", fontWeight: 600, color: textSecondary, marginBottom: unset.length > 0 ? "10px" : 0, margin: unset.length > 0 ? undefined : 0 }}>
{unset.length > 0
? (de ? `${unset.length} nicht eingestufte Aufgaben — in Quadrant ziehen oder klicken:` : `${unset.length} unclassified — drag to a quadrant or click to assign:`)
: (de ? "Alle Aufgaben sind eingestuft ✓" : "All tasks classified ✓")}
</p>
{unset.length > 0 && (
<div style={{ display: "flex", flexDirection: "column", gap: "8px", maxHeight: "400px", overflowY: "auto" }}>
{unset.map((t) => (
<div key={t.id}>
<TaskCard task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} de={de}>
<div style={{ display: "flex", gap: "4px", marginTop: "5px", flexWrap: "wrap" }}>
{QUADRANT_CONFIG.map((qc, i) => (
<button
key={i}
onClick={() => onSetQuadrant(t, qc.urgency, qc.importance)}
style={{ fontSize: "0.65rem", padding: "2px 6px", borderRadius: "4px", border: `1px solid ${qc.color}`, background: `${qc.color}18`, color: qc.color, cursor: "pointer", display: "flex", alignItems: "center", gap: "3px", fontWeight: 600 }}
>
{qc.icon} {de ? qc.labelDe : qc.label}
</button>
))}
</div>
</TaskCard>
</div>
))}
</div>
)}
</div>
</div>
);
// Mobile: stack vertically
if (isMobile) {
return (
<div>
{quadrantGrid}
<div style={{ marginTop: "12px" }}>{rightPanel}</div>
</div>
);
}
// Desktop: quadrants left (60%), filter+unassigned right (40%)
return (
<div style={{ display: "flex", gap: "14px", alignItems: "flex-start" }}>
<div style={{ flex: 3, minWidth: 0 }}>{quadrantGrid}</div>
<div style={{ flex: 2, minWidth: "220px", maxWidth: "400px" }}>{rightPanel}</div>
</div>
);
}
// ─────────────────────────────────────────────
// ABCDE VIEW
// ─────────────────────────────────────────────
interface AbcdeViewProps {
groups: Record<string, PriorityTask[]>;
allTasks: PriorityTask[];
darkMode: boolean;
de: boolean;
onSetGrade: (t: PriorityTask, grade: string | null) => void;
onDelegate: (t: PriorityTask) => void;
onToggleComplete: (t: PriorityTask) => void;
onUpdateTask: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
projects: PriorityProject[];
cardBg: string;
border: string;
textPrimary: string;
textSecondary: string;
isMobile?: boolean;
filterPanel?: React.ReactNode;
}
function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary, isMobile, filterPanel }: AbcdeViewProps) {
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({ none: true });
const [dragOver, setDragOver] = useState<string | null>(null);
const gradeList = ["A", "B", "C", "D", "E"];
const handleGradeDrop = (e: React.DragEvent, grade: string) => {
e.preventDefault();
const taskId = e.dataTransfer.getData("text/plain");
const task = allTasks.find((t) => t.id === taskId);
if (task) onSetGrade(task, grade);
setDragOver(null);
};
// A–E grade buckets
const gradeBuckets = (
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
{gradeList.map((grade) => {
const cfg = ABCDE_LABELS[grade];
const cfgDe = ABCDE_LABELS_DE[grade];
const cfgDark = ABCDE_LABELS_DARK[grade];
const tasks = groups[grade] || [];
const isCollapsed = collapsed[grade];
const isOver = dragOver === grade;
const bg = isOver ? `${cfg.color}28` : (darkMode ? cfgDark.bg : cfg.bg);
return (
<div
key={grade}
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDragOver(grade); }}
onDragLeave={(e) => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setDragOver(null); }}
onDrop={(e) => handleGradeDrop(e, grade)}
style={{
background: bg,
borderRadius: "10px",
border: isOver ? `2px dashed ${cfg.color}` : `1px solid ${border}`,
overflow: "hidden",
transition: "border 0.15s, background 0.15s",
}}
>
<div
style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "9px 14px", cursor: "pointer", borderBottom: isCollapsed ? "none" : `1px solid ${border}` }}
onClick={() => setCollapsed((p) => ({ ...p, [grade]: !p[grade] }))}
>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span style={{ fontWeight: 800, fontSize: "1.1rem", color: cfg.color, lineHeight: 1 }}>{grade}</span>
<div>
<div style={{ fontSize: "0.82rem", fontWeight: 700, color: cfg.color }}>{de ? cfgDe.label : cfg.label}</div>
<div style={{ fontSize: "0.68rem", color: textSecondary }}>{de ? cfgDe.desc : cfg.desc}</div>
</div>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
<span style={{ fontSize: "0.75rem", fontWeight: 600, color: cfg.color, background: `${cfg.color}22`, borderRadius: "9px", padding: "1px 7px" }}>{tasks.length}</span>
{isCollapsed ? <ChevronDown size={14} style={{ color: textSecondary }} /> : <ChevronUp size={14} style={{ color: textSecondary }} />}
</div>
</div>
{!isCollapsed && (
<div style={{ padding: "10px", display: "flex", flexDirection: "column", gap: "6px", minHeight: "36px" }}>
{tasks.length === 0 && (
<p style={{ fontSize: "0.75rem", color: isOver ? cfg.color : textSecondary, textAlign: "center", padding: "6px 0", margin: 0 }}>
{isOver ? (de ? "Hier ablegen" : "Drop here") : "—"}
</p>
)}
{tasks.map((t) => (
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} onRemove={() => onSetGrade(t, null)} de={de} />
))}
</div>
)}
{isCollapsed && isOver && (
<div style={{ padding: "6px 14px", fontSize: "0.72rem", color: cfg.color, textAlign: "center" }}>
{de ? "Hier ablegen" : "Drop here"}
</div>
)}
</div>
);
})}
</div>
);
// Ungraded tasks panel (right column)
const hasUngraded = (groups.none || []).length > 0;
const ungradedPanel = (
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
{filterPanel}
<div style={{ background: darkMode ? "#1f2937" : "#f3f4f6", borderRadius: "10px", border: `1px solid ${border}`, overflow: "hidden" }}>
<div
style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "9px 14px", cursor: "pointer", borderBottom: collapsed.none ? "none" : `1px solid ${border}` }}
onClick={() => setCollapsed((p) => ({ ...p, none: !p.none }))}
>
<span style={{ fontSize: "0.82rem", fontWeight: 600, color: textSecondary }}>
{hasUngraded
? `${de ? "Nicht eingestuft" : "Ungraded"} (${(groups.none || []).length})`
: (de ? "Alle Aufgaben sind eingestuft ✓" : "All tasks graded ✓")}
</span>
{hasUngraded && (collapsed.none ? <ChevronDown size={14} style={{ color: textSecondary }} /> : <ChevronUp size={14} style={{ color: textSecondary }} />)}
</div>
{!collapsed.none && hasUngraded && (
<div style={{ padding: "10px", display: "flex", flexDirection: "column", gap: "6px", maxHeight: "500px", overflowY: "auto" }}>
{(groups.none || []).map((t) => (
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} de={de}>
<div style={{ display: "flex", gap: "3px", marginTop: "5px", flexWrap: "wrap" }}>
{gradeList.map((g) => (
<button
key={g}
onClick={() => onSetGrade(t, g)}
style={{ fontSize: "0.65rem", padding: "1px 6px", borderRadius: "4px", border: `1px solid ${ABCDE_LABELS[g].color}`, background: `${ABCDE_LABELS[g].color}18`, color: ABCDE_LABELS[g].color, cursor: "pointer", fontWeight: 700 }}
>
{g}
</button>
))}
</div>
</TaskCard>
))}
</div>
)}
</div>
</div>
);
if (isMobile) {
return (
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
{gradeBuckets}
{ungradedPanel}
</div>
);
}
return (
<div style={{ display: "flex", gap: "14px", alignItems: "flex-start" }}>
<div style={{ flex: 3, minWidth: 0 }}>{gradeBuckets}</div>
<div style={{ flex: 2, minWidth: "200px", maxWidth: "380px" }}>{ungradedPanel}</div>
</div>
);
}
// ─────────────────────────────────────────────
// IVY LEE VIEW
// ─────────────────────────────────────────────
interface IvyLeeViewProps {
tasks: PriorityTask[];
selected: Set<string>;
darkMode: boolean;
de: boolean;
onToggle: (id: string) => void;
onDelegate: (t: PriorityTask) => void;
onToggleComplete: (t: PriorityTask) => void;
onUpdateTask: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
projects: PriorityProject[];
cardBg: string;
border: string;
textPrimary: string;
textSecondary: string;
isMobile?: boolean;
filterPanel?: React.ReactNode;
}
function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary, isMobile, filterPanel }: IvyLeeViewProps) {
const accentColor = "#6366f1";
const selectedTasks = tasks.filter((t) => selected.has(t.id));
const unselected = tasks.filter((t) => !selected.has(t.id));
const [dropOverTop6, setDropOverTop6] = useState(false);
const handleTop6Drop = (e: React.DragEvent) => {
e.preventDefault();
const taskId = e.dataTransfer.getData("text/plain");
if (taskId && !selected.has(taskId) && selected.size < 6) {
onToggle(taskId);
}
setDropOverTop6(false);
};
// Info banner
const infoBanner = (
<div style={{ background: darkMode ? "#1e3a5f" : "#eff6ff", borderRadius: "10px", padding: "12px 14px", marginBottom: "14px", display: "flex", alignItems: "flex-start", gap: "10px" }}>
<Star size={16} style={{ color: "#3b82f6", flexShrink: 0, marginTop: "1px" }} />
<div>
<p style={{ fontSize: "0.82rem", color: darkMode ? "#93c5fd" : "#1d4ed8", fontWeight: 600, margin: "0 0 3px" }}>
{de ? "Ivy Lee Methode" : "Ivy Lee Method"}
</p>
<p style={{ fontSize: "0.78rem", color: darkMode ? "#60a5fa" : "#3b82f6", margin: 0, lineHeight: 1.5 }}>
{de
? "Wähle genau 6 wichtige Aufgaben für morgen, geordnet nach Priorität."
: "Select exactly 6 important tasks for tomorrow, ordered by priority."}
</p>
</div>
</div>
);
// Top-6 selection panel (left column)
const top6Panel = (
<div
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = selected.size < 6 ? "move" : "none"; setDropOverTop6(true); }}
onDragLeave={(e) => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setDropOverTop6(false); }}
onDrop={handleTop6Drop}
style={{ borderRadius: "10px", border: dropOverTop6 ? `2px dashed ${accentColor}` : "2px solid transparent", padding: "4px", transition: "border 0.15s" }}
>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "8px" }}>
<span style={{ fontSize: "0.78rem", fontWeight: 700, color: dropOverTop6 ? accentColor : textSecondary, textTransform: "uppercase", letterSpacing: "0.05em" }}>
{dropOverTop6 ? (de ? "Hier ablegen" : "Drop to add") : (de ? "Meine Top-6" : "My Top 6")}
</span>
<span style={{ fontSize: "0.78rem", color: selected.size === 6 ? "#059669" : accentColor, fontWeight: 600 }}>
{selected.size}/6
</span>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
{Array.from({ length: 6 }).map((_, i) => {
const t = selectedTasks[i];
if (t) {
return (
<div key={t.id} style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span style={{ fontSize: "0.78rem", fontWeight: 700, color: accentColor, width: "16px", textAlign: "center", flexShrink: 0 }}>{i + 1}</span>
<div style={{ flex: 1 }}>
<TaskCard task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} onRemove={() => onToggle(t.id)} de={de} />
</div>
</div>
);
}
return (
<div key={`empty-${i}`} style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span style={{ fontSize: "0.78rem", fontWeight: 700, color: textSecondary, width: "16px", textAlign: "center", flexShrink: 0 }}>{i + 1}</span>
<div style={{ flex: 1, background: darkMode ? "#1f2937" : "#f9fafb", border: `1px dashed ${border}`, borderRadius: "8px", padding: "10px 12px" }}>
<span style={{ fontSize: "0.78rem", color: textSecondary }}>{de ? "Leer — wähle eine Aufgabe" : "Empty — pick a task"}</span>
</div>
</div>
);
})}
</div>
</div>
);
// Available tasks pool (right column)
const availablePool = (
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
{filterPanel}
<div>
<p style={{ fontSize: "0.78rem", fontWeight: 600, color: textSecondary, marginBottom: "8px" }}>
{unselected.length > 0 ? (de ? "Verfügbare Aufgaben:" : "Available tasks:") : (de ? "Alle Aufgaben ausgewählt ✓" : "All tasks selected ✓")}
</p>
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
{unselected.map((t) => (
<div
key={t.id}
onClick={() => onToggle(t.id)}
style={{
background: cardBg,
border: `1px solid ${border}`,
borderRadius: "8px",
padding: "8px 12px",
cursor: selected.size >= 6 ? "not-allowed" : "pointer",
opacity: selected.size >= 6 ? 0.5 : 1,
display: "flex",
alignItems: "center",
gap: "8px",
transition: "background 0.1s",
}}
>
<Circle size={14} style={{ color: "#9ca3af", flexShrink: 0 }} />
<span style={{ fontSize: "0.85rem", color: textPrimary, flex: 1 }}>{t.title}</span>
{t.scheduledDate && <span style={{ fontSize: "0.7rem", color: "#6b7280" }}>{formatDate(t.scheduledDate)}</span>}
{t.project && <span style={{ fontSize: "0.7rem", color: t.project.color || "#6366f1" }}>{t.project.name}</span>}
</div>
))}
</div>
</div>
</div>
);
if (isMobile) {
return (
<div>
{infoBanner}
{filterPanel && <div style={{ marginBottom: "12px" }}>{filterPanel}</div>}
{top6Panel}
{unselected.length > 0 && <div style={{ marginTop: "16px" }}>{availablePool}</div>}
</div>
);
}
return (
<div>
{infoBanner}
<div style={{ display: "flex", gap: "14px", alignItems: "flex-start" }}>
<div style={{ flex: 1, minWidth: 0 }}>{top6Panel}</div>
<div style={{ flex: 1, minWidth: "200px" }}>{availablePool}</div>
</div>
</div>
);
}
// ─────────────────────────────────────────────
// PARETO (80/20) VIEW
// ─────────────────────────────────────────────
interface ParetoViewProps {
important: PriorityTask[];
rest: PriorityTask[];
darkMode: boolean;
de: boolean;
onDelegate: (t: PriorityTask) => void;
onToggleComplete: (t: PriorityTask) => void;
onUpdateTask: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
projects: PriorityProject[];
cardBg: string;
border: string;
textPrimary: string;
textSecondary: string;
isMobile?: boolean;
filterPanel?: React.ReactNode;
}
function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary, isMobile, filterPanel }: ParetoViewProps) {
const total = important.length + rest.length;
const pct = total > 0 ? Math.round((important.length / total) * 100) : 0;
// Info banner + progress bar (full-width header)
const header = (
<>
<div style={{ background: darkMode ? "#422006" : "#fff7ed", borderRadius: "10px", padding: "12px 14px", marginBottom: "12px", display: "flex", gap: "10px", alignItems: "flex-start" }}>
<BarChart2 size={16} style={{ color: "#f97316", flexShrink: 0, marginTop: "1px" }} />
<div>
<p style={{ fontSize: "0.82rem", fontWeight: 700, color: "#f97316", margin: "0 0 3px" }}>
{de ? "Pareto-Prinzip (80/20-Regel)" : "Pareto Principle (80/20 Rule)"}
</p>
<p style={{ fontSize: "0.78rem", color: darkMode ? "#fb923c" : "#c2410c", margin: 0, lineHeight: 1.5 }}>
{de
? `${important.length} von ${total} Aufgaben (${pct}%) sind wichtig. Fokussiere dich auf diese für 80% der Ergebnisse.`
: `${important.length} of ${total} tasks (${pct}%) are marked as important. Focus on these to achieve 80% of outcomes.`}
</p>
</div>
</div>
<div style={{ background: darkMode ? "#374151" : "#e5e7eb", borderRadius: "99px", height: "8px", marginBottom: "16px", overflow: "hidden" }}>
<div style={{ width: `${pct}%`, height: "100%", background: "linear-gradient(90deg, #f97316, #ef4444)", borderRadius: "99px", transition: "width 0.4s" }} />
</div>
</>
);
// High-impact tasks (left column)
const importantPanel = (
<div>
<div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "10px" }}>
<span style={{ fontSize: "0.78rem", fontWeight: 700, color: "#f97316", textTransform: "uppercase", letterSpacing: "0.05em" }}>
{de ? "Wichtige Aufgaben (Fokus)" : "High-Impact Tasks (Focus)"}
</span>
<span style={{ fontSize: "0.75rem", color: "#f97316", background: "#f9731622", borderRadius: "9px", padding: "1px 7px" }}>{important.length}</span>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
{important.length === 0 && (
<p style={{ fontSize: "0.8rem", color: textSecondary, padding: "10px 0" }}>
{de
? "Keine wichtigen Aufgaben. Markiere Aufgaben als wichtig im Eisenhower- oder ABCDE-Modus."
: "No important tasks. Mark tasks as important via Eisenhower or ABCDE mode."}
</p>
)}
{important.map((t) => (
<div key={t.id} style={{ borderLeft: "3px solid #f97316", borderRadius: "0 8px 8px 0" }}>
<TaskCard task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} onRemove={() => onUpdateTask(t.id, { importance: false })} de={de} />
</div>
))}
</div>
</div>
);
// Remaining tasks (right column — always visible, no toggle)
const remainingPanel = (
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
{filterPanel}
<div>
<div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "10px" }}>
<span style={{ fontSize: "0.78rem", fontWeight: 600, color: textSecondary, textTransform: "uppercase", letterSpacing: "0.05em" }}>
{de ? "Restliche Aufgaben" : "Remaining tasks"}
</span>
<span style={{ fontSize: "0.75rem", color: textSecondary, background: darkMode ? "#374151" : "#e5e7eb", borderRadius: "9px", padding: "1px 7px" }}>{rest.length}</span>
</div>
{rest.length === 0 ? (
<p style={{ fontSize: "0.78rem", color: textSecondary }}>{de ? "Keine weiteren Aufgaben." : "No remaining tasks."}</p>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
{rest.map((t) => (
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} de={de}>
<button
onClick={() => onUpdateTask(t.id, { importance: true })}
style={{ fontSize: "0.65rem", marginTop: "3px", background: "none", border: "none", color: "#f97316", cursor: "pointer", padding: 0, display: "flex", alignItems: "center", gap: "2px" }}
>
<Star size={9} /> {de ? "Als wichtig markieren" : "Mark as important"}
</button>
</TaskCard>
))}
</div>
)}
</div>
</div>
);
if (isMobile) {
return (
<div>
{header}
{filterPanel && <div style={{ marginBottom: "12px" }}>{filterPanel}</div>}
{importantPanel}
<div style={{ marginTop: "16px" }}>{remainingPanel}</div>
</div>
);
}
return (
<div>
{header}
<div style={{ display: "flex", gap: "14px", alignItems: "flex-start" }}>
<div style={{ flex: 1, minWidth: 0 }}>{importantPanel}</div>
<div style={{ flex: 1, minWidth: "200px" }}>{remainingPanel}</div>
</div>
</div>
);
}