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>
1391 lines
73 KiB
TypeScript
1391 lines
73 KiB
TypeScript
"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 }}>
|
||
“{delegateModal.title}”
|
||
</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>
|
||
);
|
||
}
|