diff --git a/package.json b/package.json index 96937a8..4cac9a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.86.0", + "version": "1.87.0", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6471cf4..92f5d49 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -200,6 +200,11 @@ model Task { projectId String? kanbanStage String? url String? + urgency Boolean? + importance Boolean? + priority String? + delegatedTo String? + delegationNote String? parent Task? @relation("SubTasks", fields: [parentTaskId], references: [id], onDelete: Cascade) subTasks Task[] @relation("SubTasks") project Project? @relation(fields: [projectId], references: [id]) diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index 5f838e3..8c3b647 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -247,7 +247,7 @@ export async function POST(request: NextRequest) { const body = await request.json(); - const { title, description, dayOfWeek, order, markdownContent, somedayListId, somedaySlotIndex, startTime, scheduledDate, recurrenceInterval, recurrenceUnit, recurrenceTime, recurrenceEndDate, recurrenceDays, parentTaskId, projectId, kanbanStage, url } = body; + const { title, description, dayOfWeek, order, markdownContent, somedayListId, somedaySlotIndex, startTime, scheduledDate, recurrenceInterval, recurrenceUnit, recurrenceTime, recurrenceEndDate, recurrenceDays, parentTaskId, projectId, kanbanStage, url, urgency, importance, priority, delegatedTo, delegationNote } = body; let { isRolling } = body; const { isRecurring } = body; @@ -360,6 +360,11 @@ export async function POST(request: NextRequest) { ...(projectId !== undefined && { projectId: projectId || null }), ...(kanbanStage !== undefined && { kanbanStage: kanbanStage || null }), ...(url !== undefined && { url: sanitizeUrl(url) }), + ...(urgency !== undefined && { urgency: urgency === null ? null : Boolean(urgency) }), + ...(importance !== undefined && { importance: importance === null ? null : Boolean(importance) }), + ...(priority !== undefined && { priority: priority || null }), + ...(delegatedTo !== undefined && { delegatedTo: delegatedTo || null }), + ...(delegationNote !== undefined && { delegationNote: delegationNote || null }), ...(externalId && { externalId, externalProvider, externalListId }), }, }); @@ -404,7 +409,7 @@ export async function PATCH(request: NextRequest) { const body = await request.json(); const { id } = body; - const { title, description, completed, dayOfWeek, order, markdownContent, scheduledDate, startTime, somedayListId, somedaySlotIndex, isRecurring, recurrenceInterval, recurrenceUnit, recurrenceTime, recurrenceEndDate, recurrenceDays, restore, parentTaskId, projectId, kanbanStage, externalProvider, url } = body; + const { title, description, completed, dayOfWeek, order, markdownContent, scheduledDate, startTime, somedayListId, somedaySlotIndex, isRecurring, recurrenceInterval, recurrenceUnit, recurrenceTime, recurrenceEndDate, recurrenceDays, restore, parentTaskId, projectId, kanbanStage, externalProvider, url, urgency, importance, priority, delegatedTo, delegationNote } = body; if (!id) { return NextResponse.json( @@ -497,6 +502,11 @@ export async function PATCH(request: NextRequest) { ...(kanbanStage !== undefined && { kanbanStage: kanbanStage || null }), ...(externalProvider !== undefined && { externalProvider: externalProvider || null }), ...(url !== undefined && { url: sanitizeUrl(url) }), + ...(urgency !== undefined && { urgency: urgency === null ? null : Boolean(urgency) }), + ...(importance !== undefined && { importance: importance === null ? null : Boolean(importance) }), + ...(priority !== undefined && { priority: priority || null }), + ...(delegatedTo !== undefined && { delegatedTo: delegatedTo || null }), + ...(delegationNote !== undefined && { delegationNote: delegationNote || null }), }, }); diff --git a/src/components/PriorityView.tsx b/src/components/PriorityView.tsx new file mode 100644 index 0000000..5963db0 --- /dev/null +++ b/src/components/PriorityView.tsx @@ -0,0 +1,1080 @@ +"use client"; + +import React, { useState, useMemo, useCallback } 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, +} from "lucide-react"; + +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; +} + +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) => Promise; +} + +const ABCDE_LABELS: Record = { + 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 = { + A: { bg: "#450a0a" }, + B: { bg: "#431407" }, + C: { bg: "#422006" }, + D: { bg: "#1e3a5f" }, + E: { bg: "#1f2937" }, +}; + +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, +}: PriorityViewProps) { + const [method, setMethod] = useState("eisenhower"); + const [filterProject, setFilterProject] = useState(""); + const [filterList, setFilterList] = useState(""); + const [filterTimespan, setFilterTimespan] = useState("all"); + const [showCompleted, setShowCompleted] = useState(false); + const [delegateModal, setDelegateModal] = useState(null); + const [delegateTo, setDelegateTo] = useState(""); + const [delegateNote, setDelegateNote] = useState(""); + const [delegateType, setDelegateType] = useState<"person" | "ai">("person"); + const [ivyLeeSelected, setIvyLeeSelected] = useState>(new Set()); + const [showFilters, setShowFilters] = useState(false); + + const de = language === "de"; + + // --- Filter tasks --- + const filteredTasks = useMemo(() => { + let result = tasks.filter((t) => !t.id.startsWith("virtual-")); + if (!showCompleted) result = result.filter((t) => !t.completed); + 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, showCompleted, 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 = { 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) => { + await onUpdateTask(task.id, { completed: !task.completed }); + }, [onUpdateTask]); + + const toggleIvyLee = useCallback((id: string) => { + setIvyLeeSelected((prev) => { + const next = new Set(prev); + if (next.has(id)) { next.delete(id); return next; } + if (next.size >= 6) return prev; // max 6 + next.add(id); + return next; + }); + }, []); + + 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: de ? "Eisenhower" : "Eisenhower", icon: }, + { key: "abcde", label: "ABCDE", icon: }, + { key: "ivylee", label: de ? "Ivy Lee" : "Ivy Lee", icon: }, + { key: "pareto", label: de ? "80/20" : "80/20", icon: }, + ]; + + return ( +
+ {/* ─── Header ─── */} +
+
+ + + {de ? "Prioritäten" : "Priority View"} + +
+
+ + +
+
+ + {/* ─── Filters Panel ─── */} + {showFilters && ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + {filteredTasks.length} {de ? "Aufgaben" : "tasks"} + +
+
+ )} + + {/* ─── Method Tabs ─── */} +
+ {methodButtons.map((m) => ( + + ))} +
+ + {/* ─── METHOD: EISENHOWER ─── */} + {method === "eisenhower" && ( + + )} + + {/* ─── METHOD: ABCDE ─── */} + {method === "abcde" && ( + + )} + + {/* ─── METHOD: IVY LEE ─── */} + {method === "ivylee" && ( + + )} + + {/* ─── METHOD: PARETO ─── */} + {method === "pareto" && ( + + )} + + {/* ─── Delegate Modal ─── */} + {delegateModal && ( +
setDelegateModal(null)} + > +
e.stopPropagation()} + > +
+
+ + + {de ? "Aufgabe delegieren" : "Delegate task"} + +
+ +
+

+ “{delegateModal.title}” +

+ + {/* Type toggle */} +
+ + +
+ + {delegateType === "person" ? ( + <> + + 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" }} + /> + + ) : ( +
+

+ {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."} +

+
+ )} + + +