feat: add Priority View — Eisenhower, ABCDE, Ivy Lee, Pareto, delegation

New "Priority View" (Target icon in toolbar) fusing four prioritization
frameworks into a single, filterable view:

- Eisenhower Matrix: 4 quadrants (Do/Schedule/Delegate/Delete) based on
  urgency × importance; unclassified tasks shown below for quick assignment
- ABCDE Method: five collapsible groups (A=Critical → E=Eliminate) with
  inline grade assignment buttons
- Ivy Lee Method: pick exactly 6 tasks for tomorrow, ordered by priority
- Pareto 80/20: highlights the important 20% of tasks with a progress bar
  and one-click "mark as important" for the rest

New DB fields: urgency (Boolean), importance (Boolean), priority (A–E),
delegatedTo, delegationNote — all persisted via Prisma + API.

Filters: by project, someday list (incl. scheduled / unscheduled), and
timespan (today / this week / next week / all).

Delegation modal: assign to a named person or to AI with optional
instruction notes; displayed as inline badge on task cards.

v1.87.0
This commit is contained in:
mARTin 2026-04-06 02:07:05 +02:00
parent 8ffb6a0690
commit 46a05a87c3
5 changed files with 1132 additions and 9 deletions

View File

@ -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": {

View File

@ -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])

View File

@ -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 }),
},
});

File diff suppressed because it is too large Load Diff

View File

@ -103,6 +103,7 @@ import { translations } from "../lib/weeklyViewTranslations";
import { WeatherDisplayKey, WEATHER_DISPLAY_DEFAULTS } from "../lib/weeklyViewConstants";
const SettingsSidebar = dynamic(() => import("./SettingsSidebar"), { ssr: false });
const RichTextEditor = dynamic(() => import("./RichTextEditor"), { ssr: false });
const PriorityView = dynamic(() => import("./PriorityView"), { ssr: false });
// Cookie helpers for per-device settings
const DEVICE_SETTINGS_KEYS = ["viewDays", "cellDuration", "startHour", "endHour"];
@ -119,7 +120,7 @@ function setCookie(name: string, value: string, days: number = 365) {
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`;
}
export type ViewStyle = "simple" | "calendar" | "list" | "grid" | "kanban";
export type ViewStyle = "simple" | "calendar" | "list" | "grid" | "kanban" | "priority";
export interface KanbanStage {
id: string;
@ -168,6 +169,11 @@ export interface Task {
project?: { id: string; name: string; icon?: string | null; color?: string | null } | null;
kanbanStage?: string | null;
url?: string | null;
urgency?: boolean | null;
importance?: boolean | null;
priority?: string | null;
delegatedTo?: string | null;
delegationNote?: string | null;
}
interface CalendarEvent {
@ -5498,8 +5504,9 @@ export default function WeeklyView() {
{ key: "calendar", icon: <CalendarDays size={13} /> },
{ key: "list", icon: <ListTodo size={13} /> },
{ key: "kanban", icon: <Kanban size={13} /> },
{ key: "priority", icon: <Target size={13} /> },
].map((v) => (
<button key={v.key} onClick={() => { setViewStyle(v.key as any); saveSetting("viewStyle", v.key); if (v.key === "list") { setShowTimeGrid(false); saveSetting("showTimeGrid", false); } if (v.key === "simple" || v.key === "calendar") { setShowTimeGrid(true); saveSetting("showTimeGrid", true); } }}
<button key={v.key} onClick={() => { setViewStyle(v.key as any); saveSetting("viewStyle", v.key); if (v.key === "list") { setShowTimeGrid(false); saveSetting("showTimeGrid", false); } if (v.key === "simple" || v.key === "calendar") { setShowTimeGrid(true); saveSetting("showTimeGrid", true); } if (v.key === "priority" || v.key === "kanban") { setShowTimeGrid(false); saveSetting("showTimeGrid", false); } }}
style={{ flex: 1, padding: "5px 0", borderRadius: "6px", border: "none", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: profile.viewStyle ===v.key ? 700 : 400, background: profile.viewStyle ===v.key ? "#0ea5e9" : (darkMode ? "#1f2937" : "#e5e7eb"), color: profile.viewStyle ===v.key ? "#fff" : (darkMode ? "#9ca3af" : "#6b7280") }}>
{v.icon}
</button>
@ -5788,12 +5795,19 @@ export default function WeeklyView() {
<ListTodo size={16} />
</button>
<button
onClick={() => { setViewStyle("kanban"); saveSetting("viewStyle", "kanban"); }}
onClick={() => { setViewStyle("kanban"); saveSetting("viewStyle", "kanban"); setShowTimeGrid(false); saveSetting("showTimeGrid", false); }}
className={`p-1.5 rounded transition-colors ${profile.viewStyle ==="kanban" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
title={t.kanbanView}
>
<Kanban size={16} />
</button>
<button
onClick={() => { setViewStyle("priority"); saveSetting("viewStyle", "priority"); setShowTimeGrid(false); saveSetting("showTimeGrid", false); }}
className={`p-1.5 rounded transition-colors ${profile.viewStyle ==="priority" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
title={profile.language === "de" ? "Prioritäten" : "Priority View"}
>
<Target size={16} />
</button>
</div>
{/* Days to Show — desktop only (available in sidebar on tablet) */}
@ -6103,8 +6117,22 @@ export default function WeeklyView() {
</div>
</header>
{/* All-Day Events Section (above position) — hidden in kanban */}
{profile.viewStyle !== "kanban" && effectiveAllDayPosition === "above" && allDaySection}
{/* All-Day Events Section (above position) — hidden in kanban/priority */}
{profile.viewStyle !== "kanban" && profile.viewStyle !== "priority" && effectiveAllDayPosition === "above" && allDaySection}
{/* Priority View */}
{profile.viewStyle === "priority" && (
<div style={{ flex: 1, overflowY: "auto" }}>
<PriorityView
tasks={[...tasks, ...somedayLists.flatMap(l => l.tasks)]}
somedayLists={somedayLists.map(l => ({ id: l.id, title: l.title }))}
projects={projects}
darkMode={darkMode}
language={profile.language}
onUpdateTask={async (id, fields) => { await updateTaskFields(id, fields as any); }}
/>
</div>
)}
{/* Kanban Board View */}
{profile.viewStyle ==="kanban" && (() => {
@ -7441,7 +7469,7 @@ export default function WeeklyView() {
</div>}
{/* All-Day Events Section (below position) — hidden in kanban */}
{profile.viewStyle !== "kanban" && effectiveAllDayPosition === "below" && allDaySection}
{profile.viewStyle !== "kanban" && profile.viewStyle !== "priority" && effectiveAllDayPosition === "below" && allDaySection}
{/* Someday Section */}
{effectiveShowSomeday && (<>