From 52d160cceabd858a61f2de74918982c946d9df42 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Fri, 1 May 2026 14:23:56 +0200 Subject: [PATCH] feat: projects tab, priority icons, list/tab visuals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wave-1 user feedback (points 7–10): - Settings: new Projects tab with full CRUD (name, color, icon picker) - Priority Icons render in simple/calendar/list views with style-specific visuals (Eisenhower icons, ABCDE letters, Ivy Lee 1–6, Pareto star). Toggle + style selector in Settings; Ivy Lee ranks now persist to task.priority so cross-view badges stay consistent. - Someday Lists gain optional icon (left of title) and color (left border tint), edited via a pencil-popup with IconPicker + color picker. - Tabs gain optional icon and color, stored in user.viewSettings JSON and edited via the same popup pattern. Schema: SomedayList.color/icon, User.showPriorityIcons/priorityStyle. v1.99.0 Co-Authored-By: Claude Opus 4.7 --- package.json | 2 +- .../migration.sql | 7 + prisma/schema.prisma | 4 + src/app/api/someday-lists/route.ts | 6 +- src/app/api/user/profile/route.ts | 8 +- src/components/GridTaskBlock.tsx | 4 + src/components/PriorityView.tsx | 37 +- src/components/QuickSettingsSidebar.tsx | 17 +- src/components/SettingsSidebar.tsx | 220 ++++++++- src/components/WeeklyView.tsx | 457 ++++++++++++++++-- 10 files changed, 711 insertions(+), 51 deletions(-) create mode 100644 prisma/migrations/20260501_add_priority_settings_and_list_visuals/migration.sql diff --git a/package.json b/package.json index 721c71f..2b7ae82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.97.2", + "version": "1.99.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/migrations/20260501_add_priority_settings_and_list_visuals/migration.sql b/prisma/migrations/20260501_add_priority_settings_and_list_visuals/migration.sql new file mode 100644 index 0000000..3fa2cce --- /dev/null +++ b/prisma/migrations/20260501_add_priority_settings_and_list_visuals/migration.sql @@ -0,0 +1,7 @@ +-- User: priority style + visibility toggle +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "showPriorityIcons" BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "priorityStyle" TEXT NOT NULL DEFAULT 'eisenhower'; + +-- SomedayList: per-list color and icon (used by Punkt 7 + 8) +ALTER TABLE "SomedayList" ADD COLUMN IF NOT EXISTS "color" TEXT; +ALTER TABLE "SomedayList" ADD COLUMN IF NOT EXISTS "icon" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 92f5d49..17df18a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -93,6 +93,8 @@ model User { yearFontWeight String? @default("700") showTaskCheckboxes Boolean @default(false) showProjectIcons Boolean @default(false) + showPriorityIcons Boolean @default(true) + priorityStyle String @default("eisenhower") weekStartDay Int @default(1) emailVerificationCode String? dayHeaderGap String? @default("0.75em") @@ -226,6 +228,8 @@ model SomedayList { title String order Int @default(0) tab String? + color String? + icon String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt externalId String? diff --git a/src/app/api/someday-lists/route.ts b/src/app/api/someday-lists/route.ts index e103c43..8bd6ce6 100644 --- a/src/app/api/someday-lists/route.ts +++ b/src/app/api/someday-lists/route.ts @@ -209,8 +209,8 @@ export async function PATCH(request: NextRequest) { return NextResponse.json({ success: true }); } - // Handle Single Update (Title and/or Tab) - const { id, title, tab } = body; + // Handle Single Update (Title, Tab, Color, Icon) + const { id, title, tab, color, icon } = body; if (!id) { return NextResponse.json( @@ -234,6 +234,8 @@ export async function PATCH(request: NextRequest) { const data: Record = {}; if (title !== undefined) data.title = title; if (tab !== undefined) data.tab = tab; + if (color !== undefined) data.color = color || null; + if (icon !== undefined) data.icon = icon || null; const list = await prisma.somedayList.update({ where: { id }, diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts index 7611a6b..d6d6e79 100644 --- a/src/app/api/user/profile/route.ts +++ b/src/app/api/user/profile/route.ts @@ -34,6 +34,8 @@ export async function GET(request: NextRequest) { showSchedule: true, showTaskCheckboxes: true, showProjectIcons: true, + showPriorityIcons: true, + priorityStyle: true, cellDuration: true, viewStyle: true, viewDays: true, @@ -150,7 +152,7 @@ export async function PATCH(request: NextRequest) { hourLabelFormat, showSubHourSlots, allDayPosition, cwFontFamily, cwFontSize, cwFontWeight, cwColor, yearFontFamily, yearFontSize, yearFontWeight, yearColor, - showTaskCheckboxes, showProjectIcons, dayHeaderGap, + showTaskCheckboxes, showProjectIcons, showPriorityIcons, priorityStyle, dayHeaderGap, showCompletedTasks, showLines, startDayOffset, weekdayFormat, weekdayCase, customWeekdayNames, weekStartDay, quoteSourceUrls, quoteLanguages, kanbanStages, lightTheme, darkTheme, notificationsEnabled, mobileFontScale, weatherEnabled, weatherLat, weatherLon, weatherLocation, weatherRecentCities, viewSettings, @@ -178,6 +180,8 @@ export async function PATCH(request: NextRequest) { ...(showSchedule !== undefined && { showSchedule }), ...(showTaskCheckboxes !== undefined && { showTaskCheckboxes }), ...(showProjectIcons !== undefined && { showProjectIcons }), + ...(showPriorityIcons !== undefined && { showPriorityIcons }), + ...(priorityStyle !== undefined && { priorityStyle }), ...(cellDuration !== undefined && !isNaN(cellDuration) && { cellDuration }), ...(viewStyle !== undefined && { viewStyle }), ...(viewDays !== undefined && !isNaN(viewDays) && { viewDays }), @@ -283,6 +287,8 @@ export async function PATCH(request: NextRequest) { showSchedule: true, showTaskCheckboxes: true, showProjectIcons: true, + showPriorityIcons: true, + priorityStyle: true, cellDuration: true, viewStyle: true, viewDays: true, diff --git a/src/components/GridTaskBlock.tsx b/src/components/GridTaskBlock.tsx index a5a3645..e894bd6 100644 --- a/src/components/GridTaskBlock.tsx +++ b/src/components/GridTaskBlock.tsx @@ -39,6 +39,8 @@ interface GridTaskBlockProps { workingHoursStart: number; showTaskCheckboxes?: boolean; showProjectIcons?: boolean; + showPriorityIcons?: boolean; + priorityStyle?: string; projects?: any[]; onProjectAssign?: (taskId: string, projectId: string | null) => void; kanbanStages?: KanbanStage[]; @@ -76,6 +78,8 @@ export function GridTaskBlock({ workingHoursStart, showTaskCheckboxes, showProjectIcons, + showPriorityIcons = true, + priorityStyle = "eisenhower", projects, onProjectAssign, kanbanStages = [], diff --git a/src/components/PriorityView.tsx b/src/components/PriorityView.tsx index b92f5a9..ce9ec6e 100644 --- a/src/components/PriorityView.tsx +++ b/src/components/PriorityView.tsx @@ -70,6 +70,8 @@ interface PriorityViewProps { darkMode: boolean; language?: string; onUpdateTask: (id: string, fields: Partial) => Promise; + initialMethod?: PriorityMethod; + onMethodChange?: (m: PriorityMethod) => void; } const ABCDE_LABELS: Record = { @@ -123,8 +125,14 @@ export default function PriorityView({ darkMode, language = "en", onUpdateTask, + initialMethod, + onMethodChange, }: PriorityViewProps) { - const [method, setMethod] = useState("eisenhower"); + const [method, setMethodState] = useState(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"); @@ -135,7 +143,15 @@ export default function PriorityView({ const [delegateTo, setDelegateTo] = useState(""); const [delegateNote, setDelegateNote] = useState(""); const [delegateType, setDelegateType] = useState<"person" | "ai">("person"); - const [ivyLeeSelected, setIvyLeeSelected] = useState>(new Set()); + const [ivyLeeSelected, setIvyLeeSelected] = useState>(() => { + // Hydrate from any task that already has a numeric priority "1"-"6" + const init = new Set(); + 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); @@ -251,12 +267,21 @@ export default function PriorityView({ 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); + 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); diff --git a/src/components/QuickSettingsSidebar.tsx b/src/components/QuickSettingsSidebar.tsx index f7ca401..6895505 100644 --- a/src/components/QuickSettingsSidebar.tsx +++ b/src/components/QuickSettingsSidebar.tsx @@ -1,5 +1,5 @@ "use client"; -import { X, Type, Space, CheckSquare, Calendar, Minus } from "lucide-react"; +import { X, Type, Space, CheckSquare, Calendar, Minus, Target } from "lucide-react"; interface QuickSettingsProps { fontSize: string; @@ -12,6 +12,8 @@ interface QuickSettingsProps { onStartDayOffsetChange: (offset: number) => void; showLines: boolean; onShowLinesChange: (show: boolean) => void; + showPriorityIcons?: boolean; + onShowPriorityIconsChange?: (show: boolean) => void; isOpen: boolean; onClose: () => void; darkMode?: boolean; @@ -28,6 +30,8 @@ export default function QuickSettingsSidebar({ onStartDayOffsetChange, showLines, onShowLinesChange, + showPriorityIcons, + onShowPriorityIconsChange, isOpen, onClose, darkMode, @@ -198,6 +202,17 @@ export default function QuickSettingsSidebar({ + + {/* Show Priority Icons */} + {onShowPriorityIconsChange && ( +
+
+ + Priority Icons +
+ +
+ )} diff --git a/src/components/SettingsSidebar.tsx b/src/components/SettingsSidebar.tsx index a9fb158..38def31 100644 --- a/src/components/SettingsSidebar.tsx +++ b/src/components/SettingsSidebar.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect, useRef, useMemo, useCallback } from "react" import { signOut } from "next-auth/react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faApple, faGoogle, faMicrosoft, faNotion } from "@fortawesome/free-brands-svg-icons"; -import { faServer } from "@fortawesome/free-solid-svg-icons"; +import { faServer, faFolder } from "@fortawesome/free-solid-svg-icons"; import CalendarSyncRulesPanel from "./CalendarSyncRulesPanel"; import { ViewStyle, KanbanStage, Task } from "./WeeklyView"; import { AVAILABLE_FONTS, isCustomFont } from "../lib/fontConstants"; @@ -13,14 +13,18 @@ import { WeatherDisplayKey, WEATHER_DISPLAY_DEFAULTS } from "../lib/weeklyViewCo import { EXPORT_FIELDS, type ExportFieldKey } from "../app/api/user/export/route"; import { ArrowLeftRight, + Briefcase, Calendar, CalendarDays, + Check, + FolderOpen, Globe, Info, Kanban, Link, ListTodo, Palette, + Pencil, Play, Plus, Settings, @@ -28,6 +32,25 @@ import { Trash2, User, } from "lucide-react"; +import IconPicker from "./IconPicker"; +import { allIcons } from "./iconRegistry"; +import Icon from "@mdi/react"; + +// Minimal ProjectIcon — resolves an icon name from the unified registry. +function ProjectIcon({ icon, size = 18, color }: { icon?: string | null; size?: number; color?: string }) { + if (!icon) return ; + const normalised = icon.startsWith("fa") && icon.length > 2 && icon[2] === icon[2].toUpperCase() + ? icon.slice(2, 3).toLowerCase() + icon.slice(3) + : icon; + const found = allIcons.find((i) => i.name === normalised || i.name === icon); + if (found) { + if (found.type === "fa") { + return ; + } + return ; + } + return {icon}; +} export interface SomedayList { id: string; @@ -193,7 +216,7 @@ interface SettingsSidebarProps { fetchAvailableTaskLists: ( provider: "google" | "apple" | "outlook" | "synology", ) => Promise; - initialTab?: "calendar" | "general" | "account" | "styling" | "motivation" | "about" | "sync"; + initialTab?: "calendar" | "general" | "account" | "styling" | "motivation" | "about" | "sync" | "projects"; projects: { id: string; name: string; icon?: string | null; color?: string | null }[]; onProjectsChanged: () => void; kanbanStages: KanbanStage[]; @@ -326,7 +349,7 @@ function SettingsSidebar({ onRunSetupAssistant, }: SettingsSidebarProps) { const [activeTab, setActiveTab] = useState< - "calendar" | "general" | "account" | "styling" | "motivation" | "about" | "localisation" | "sync" + "calendar" | "general" | "account" | "styling" | "motivation" | "about" | "localisation" | "sync" | "projects" >(initialTab || "general"); const [isLoading, setIsLoading] = useState(true); const [isSyncing, setIsSyncing] = useState(false); @@ -831,6 +854,7 @@ function SettingsSidebar({ { key: "localisation", icon: , label: t.localisation }, { key: "calendar", icon: , label: t.calendar }, { key: "sync", icon: , label: t.calendarSync || "Sync" }, + { key: "projects", icon: , label: t.projects || "Projects" }, { key: "account", icon: , label: t.account }, { key: "styling", icon: , label: t.styling }, { key: "motivation", icon: , label: t.motivation }, @@ -1365,6 +1389,37 @@ function SettingsSidebar({ +
+ { + saveField("showPriorityIcons", e.target.checked); + perView.saveViewSetting("showPriorityIcons", e.target.checked, false); + }} + style={{ width: "16px", height: "16px" }} /> + +
+ +
+ + +
+ {viewStyle !== "kanban" && (
+ ) : activeTab === "projects" ? ( +
+
+

+ {t.projects || "Projects"} +

+

+ {t.projectsDesc || "Organize tasks with color-coded projects"} +

+
+ + {/* Existing projects list */} + {projects.length === 0 ? ( +
+ +

+ {t.noProjects || "No projects yet"} +

+
+ ) : ( +
+ {projects.map((p) => ( +
+ {editingProjectId === p.id ? ( +
+
+
+ + {showEditProjectIconPicker && ( +
+ { setEditProjectIcon(name); setShowEditProjectIconPicker(false); }} darkMode={false} /> +
+ )} +
+ setEditProjectName(e.target.value)} + className="weekly-input" + style={{ flex: 1, padding: "8px 12px", fontSize: "0.9rem" }} + onKeyDown={(e) => { + if (e.key === "Enter") { + fetch("/api/projects", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: p.id, name: editProjectName, color: editProjectColor, icon: editProjectIcon }) }) + .then(() => { onProjectsChanged(); setEditingProjectId(null); }); + } + if (e.key === "Escape") setEditingProjectId(null); + }} + autoFocus + /> +
+
+ setEditProjectColor(e.target.value)} style={{ width: "30px", height: "30px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }} /> + {profile.language === "de" ? "Farbe" : "Color"} +
+ + +
+
+ ) : ( + <> + +
+ {p.name} +
+ + + + )} +
+ ))} +
+ )} + + {/* Add new project form */} +
+

+ {profile.language === "de" ? "Projekt hinzufügen" : "Add Project"} +

+
+
+ + {showNewProjectIconPicker && ( +
+ { setNewProjectIcon(name); setShowNewProjectIconPicker(false); }} darkMode={false} /> +
+ )} +
+ setNewProjectColor(e.target.value)} style={{ width: "30px", height: "30px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }} /> + setNewProjectName(e.target.value)} + placeholder={profile.language === "de" ? "Name" : "Name"} + className="weekly-input" + style={{ flex: 1, padding: "8px 12px", fontSize: "0.9rem" }} + onKeyDown={(e) => { + if (e.key === "Enter" && newProjectName.trim()) { + fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: newProjectName.trim(), color: newProjectColor, icon: newProjectIcon }) }) + .then(() => { onProjectsChanged(); setNewProjectName(""); setNewProjectIcon("folder"); }); + } + }} + /> + +
+
+
) : activeTab === "about" ? (
html.replace(/<[^>]*>/g, '').trim(); @@ -116,7 +117,7 @@ const PriorityView = dynamic(() => import("./PriorityView"), { ssr: false }); const DEVICE_SETTINGS_KEYS = ["viewDays", "cellDuration", "startHour", "endHour", "fontSize", "showSubHourSlots"]; // Per-view toggle keys that are also device-specific (sidebar eye toggles) -const DEVICE_VIEW_SETTINGS_KEYS = ["showSomeday", "showAllDayEvents", "showTaskCheckboxes", "showProjectIcons", "weatherEnabled"] as const; +const DEVICE_VIEW_SETTINGS_KEYS = ["showSomeday", "showAllDayEvents", "showTaskCheckboxes", "showProjectIcons", "showPriorityIcons", "weatherEnabled"] as const; type DeviceViewSettingKey = typeof DEVICE_VIEW_SETTINGS_KEYS[number]; // Settings that save to DB (cross-device default) AND to cookie (device override wins on load) @@ -222,6 +223,8 @@ export interface SomedayList { title: string; tasks: Task[]; tab?: string | null; + color?: string | null; + icon?: string | null; externalProvider?: string | null; externalId?: string | null; externalListId?: string | null; @@ -712,6 +715,9 @@ export default function WeeklyView() { const [editingTaskId, setEditingTaskId] = useState(null); const [draggingListId, setDraggingListId] = useState(null); const [listToDelete, setListToDelete] = useState(null); + // Punkt 7+8: per-list color/icon edit popover, and per-tab settings popover + const [editingListVisualsId, setEditingListVisualsId] = useState(null); + const [editingTabVisualsName, setEditingTabVisualsName] = useState(null); const [activeSomedayTab, setActiveSomedayTab] = useState(null); const [editingTabName, setEditingTabName] = useState(null); const [renamingTabValue, setRenamingTabValue] = useState(""); @@ -827,6 +833,44 @@ export default function WeeklyView() { } }; + // Update a list's color or icon, and persist to the API. + const updateListVisuals = async (listId: string, updates: { color?: string | null; icon?: string | null }) => { + setSomedayLists(prev => prev.map(l => l.id === listId ? { ...l, ...updates } : l)); + try { + await fetch("/api/someday-lists", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: listId, ...updates }), + }); + } catch (e) { + console.error("Failed to update list visuals:", e); + } + }; + + // Tab visuals are not first-class entities — store color/icon in user.viewSettings JSON. + const getTabVisuals = (tabName: string): { color?: string; icon?: string } => { + const cfg = (viewSettingsRef.current as any).somedayTabConfig || {}; + return cfg[tabName] || {}; + }; + + const updateTabVisuals = (tabName: string, updates: { color?: string | null; icon?: string | null }) => { + const cfg = { ...((viewSettingsRef.current as any).somedayTabConfig || {}) }; + const existing = cfg[tabName] || {}; + const next: any = { ...existing, ...updates }; + if (!next.color) delete next.color; + if (!next.icon) delete next.icon; + if (Object.keys(next).length === 0) delete cfg[tabName]; + else cfg[tabName] = next; + const updated = { ...(viewSettingsRef.current as any), somedayTabConfig: cfg }; + viewSettingsRef.current = updated; + setViewSettings(updated); + fetch("/api/user/profile", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ viewSettings: updated }), + }).catch(e => console.error("[tabs] Failed to save tab visuals:", e)); + }; + const dissolveTab = async (tabName: string) => { const listsToUpdate = somedayLists.filter(l => l.tab === tabName); setSomedayLists(prev => prev.map(l => l.tab === tabName ? { ...l, tab: null } : l)); @@ -1040,6 +1084,7 @@ export default function WeeklyView() { weatherDisplay?: WeatherDisplayKey[]; showTaskCheckboxes?: boolean; showProjectIcons?: boolean; + showPriorityIcons?: boolean; showSomeday?: boolean; showAllDayEvents?: boolean; allDayPosition?: "above" | "below"; @@ -1048,7 +1093,7 @@ export default function WeeklyView() { startHour?: number; endHour?: number; }; - const PER_VIEW_KEYS = ["hourLabelFormat", "showSubHourSlots", "weatherEnabled", "weatherDisplay", "showTaskCheckboxes", "showProjectIcons", "showSomeday", "showAllDayEvents", "allDayPosition", "showCompletedTasks", "cellDuration", "startHour", "endHour"] as const; + const PER_VIEW_KEYS = ["hourLabelFormat", "showSubHourSlots", "weatherEnabled", "weatherDisplay", "showTaskCheckboxes", "showProjectIcons", "showPriorityIcons", "showSomeday", "showAllDayEvents", "allDayPosition", "showCompletedTasks", "cellDuration", "startHour", "endHour"] as const; const [viewSettings, setViewSettings] = useState>({}); const viewSettingsRef = useRef>({}); viewSettingsRef.current = viewSettings; @@ -1139,6 +1184,8 @@ export default function WeeklyView() { const effectiveWeatherDisplay = (getEffective("weatherDisplay", WEATHER_DISPLAY_DEFAULTS) || WEATHER_DISPLAY_DEFAULTS) as WeatherDisplayKey[]; const effectiveShowTaskCheckboxes = getEffective("showTaskCheckboxes", profile.showTaskCheckboxes); const effectiveShowProjectIcons = getEffective("showProjectIcons", profile.showProjectIcons); + const effectiveShowPriorityIcons = getEffective("showPriorityIcons", profile.showPriorityIcons !== false) as boolean; + const effectivePriorityStyle = (profile.priorityStyle || "eisenhower") as string; const effectiveShowSomeday = getEffective("showSomeday", profile.showSomeday ?? true); const effectiveShowAllDay = getEffective("showAllDayEvents", profile.showAllDayEvents ?? true); const effectiveAllDayPosition = getEffective("allDayPosition", profile.allDayPosition ?? "above") || "above"; @@ -2562,6 +2609,8 @@ export default function WeeklyView() { id: l.id, title: l.title, tab: l.tab || null, + color: l.color || null, + icon: l.icon || null, tasks: l.tasks || [], externalId: l.externalId || null, externalProvider: l.externalProvider || null, @@ -2625,6 +2674,8 @@ export default function WeeklyView() { id: l.id, title: l.title, tab: l.tab || null, + color: l.color || null, + icon: l.icon || null, tasks: [], externalId: l.externalId || null, externalProvider: l.externalProvider || null, @@ -6253,6 +6304,11 @@ export default function WeeklyView() { darkMode={darkMode} language={profile.language} onUpdateTask={async (id, fields) => { await updateTaskFields(id, fields as any); }} + initialMethod={(profile.priorityStyle || "eisenhower") as any} + onMethodChange={(m) => { + setProfile((p: any) => ({ ...p, priorityStyle: m })); + saveSetting("priorityStyle", m); + }} />
)} - {(() => { - const pm = getPriorityMeta(task); - return pm ? ( - - + {effectiveShowPriorityIcons && (() => { + const pb = getPriorityBadge(task, profile.priorityStyle || "eisenhower", 12); + return pb ? ( + + {pb.node} ) : null; })()} @@ -7164,6 +7220,8 @@ export default function WeeklyView() { workingHoursStart={workingHoursStart} showTaskCheckboxes={effectiveShowTaskCheckboxes} showProjectIcons={effectiveShowProjectIcons} + showPriorityIcons={effectiveShowPriorityIcons} + priorityStyle={effectivePriorityStyle} projects={projects} onProjectAssign={assignProject} kanbanStages={kanbanStages} @@ -7582,6 +7640,8 @@ export default function WeeklyView() { onSetEditingTaskId={setEditingTaskId} showTaskCheckboxes={effectiveShowTaskCheckboxes} showProjectIcons={effectiveShowProjectIcons} + showPriorityIcons={effectiveShowPriorityIcons} + priorityStyle={effectivePriorityStyle} projects={projects} onProjectAssign={assignProject} kanbanStages={kanbanStages} @@ -7770,6 +7830,7 @@ export default function WeeklyView() {
{ if (e.dataTransfer.types.includes("text/list-id")) { e.preventDefault(); @@ -7790,20 +7851,52 @@ export default function WeeklyView() { setDragOverTab(null); }} > + {(() => { + const tv = getTabVisuals(tab); + return ( + + ); + })()} + onClick={(e) => { e.stopPropagation(); setEditingTabVisualsName(editingTabVisualsName === tab ? null : tab); }} + title={profile.language === "de" ? "Tab-Stil bearbeiten" : "Edit tab style"} + style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: "#bbb", display: "inline-flex", alignItems: "center" }} + > + {editingTabVisualsName === tab && ( + updateTabVisuals(tab, updates)} + onClose={() => setEditingTabVisualsName(null)} + /> + )}
) ))} @@ -7858,6 +7951,8 @@ export default function WeeklyView() { cursor: "text", display: "flex", flexDirection: "column", + position: "relative", + ...(list.color ? { borderLeft: `4px solid ${list.color}`, paddingLeft: "8px" } : {}), }} onMouseDown={(e) => { const target = e.target as HTMLElement; @@ -8012,6 +8107,15 @@ export default function WeeklyView() { >
+ {list.icon && ( + { e.stopPropagation(); setEditingListVisualsId(list.id); }} + title={profile.language === "de" ? "Listen-Stil bearbeiten" : "Edit list style"} + style={{ display: "inline-flex", alignItems: "center", marginRight: "6px", cursor: "pointer", flexShrink: 0 }} + > + + + )} )} +
+ {/* List visuals popover (Punkt 7+8) */} + {editingListVisualsId === list.id && ( + updateListVisuals(list.id, updates)} + onClose={() => setEditingListVisualsId(null)} + /> + )}
void; kanbanStages?: KanbanStage[]; } // ── Priority indicator helper ───────────────────────────────────── -// Returns icon + color for a task based on Eisenhower quadrant (primary) -// or ABCDE grade (fallback). Returns null when no priority is set. -function getPriorityMeta(task: Task): { Icon: React.ElementType; color: string; label: string } | null { - if (task.urgency != null && task.importance != null) { - if (task.urgency && task.importance) return { Icon: Zap, color: "#ef4444", label: "Sofort erledigen" }; - if (!task.urgency && task.importance) return { Icon: CalendarClock, color: "#3b82f6", label: "Planen" }; - if (task.urgency && !task.importance) return { Icon: CornerUpRight, color: "#f97316", label: "Delegieren" }; - return { Icon: Archive, color: "#9ca3af", label: "Eliminieren" }; +// Renders a small priority badge for a task. The visual style is selected +// by the user's `priorityStyle` setting: +// - "eisenhower" → quadrant icons + colors (urgency × importance) +// - "abcde" → letter A–E in a colored circle +// - "ivylee" → digit 1–6 in a colored circle (rank persisted in priority) +// - "pareto" → star for the "vital few" (importance flag) +// Returns null when the task has no priority data for the active style. +type PriorityBadge = { node: React.ReactNode; label: string }; + +const PRIORITY_NUMBER_COLORS: Record = { + "1": "#ef4444", "2": "#f97316", "3": "#eab308", + "4": "#22c55e", "5": "#3b82f6", "6": "#8b5cf6", +}; +const PRIORITY_LETTER_COLORS: Record = { + A: "#ef4444", B: "#3b82f6", C: "#eab308", D: "#f97316", E: "#9ca3af", +}; + +function makeBadgeCircle(text: string, color: string, size: number, label: string): PriorityBadge { + return { + label, + node: ( + {text} + ), + }; +} + +function getPriorityBadge(task: Task, style: string, size = 12): PriorityBadge | null { + if (style === "abcde") { + if (!task.priority || !PRIORITY_LETTER_COLORS[task.priority]) return null; + return makeBadgeCircle(task.priority, PRIORITY_LETTER_COLORS[task.priority], size + 2, task.priority); } - if (task.priority) { - const map: Record = { - A: { Icon: Zap, color: "#ef4444" }, - B: { Icon: CalendarClock, color: "#3b82f6" }, - C: { Icon: Clock, color: "#eab308" }, - D: { Icon: CornerUpRight, color: "#f97316" }, - E: { Icon: Archive, color: "#9ca3af" }, + if (style === "ivylee") { + if (!task.priority || !PRIORITY_NUMBER_COLORS[task.priority]) return null; + return makeBadgeCircle(task.priority, PRIORITY_NUMBER_COLORS[task.priority], size + 2, `Ivy Lee #${task.priority}`); + } + if (style === "pareto") { + if (task.importance !== true && task.priority !== "A" && task.priority !== "B") return null; + return { + label: "Vital Few (80/20)", + node: , }; - const m = map[task.priority]; - return m ? { ...m, label: task.priority } : null; + } + // eisenhower (default) + if (task.urgency != null && task.importance != null) { + if (task.urgency && task.importance) + return { label: "Sofort erledigen", node: }; + if (!task.urgency && task.importance) + return { label: "Planen", node: }; + if (task.urgency && !task.importance) + return { label: "Delegieren", node: }; + return { label: "Eliminieren", node: }; } return null; } @@ -9579,6 +9764,8 @@ function TaskItem({ isSubTask = false, showTaskCheckboxes = false, showProjectIcons = false, + showPriorityIcons = true, + priorityStyle = "eisenhower", projects = [], onProjectAssign, kanbanStages = [], @@ -9842,11 +10029,11 @@ function TaskItem({ flex: 1, }} > - {(() => { - const pm = getPriorityMeta(task); - return pm ? ( - - + {showPriorityIcons && (() => { + const pb = getPriorityBadge(task, priorityStyle, 11); + return pb ? ( + + {pb.node} ) : null; })()} @@ -10444,6 +10631,202 @@ function TaskItem({ ); } +// Punkt 7+8 — popover for editing a someday list's icon and color. +function ListVisualsPopover({ + list, + darkMode, + language, + onChange, + onClose, +}: { + list: { id: string; title: string; color?: string | null; icon?: string | null }; + darkMode: boolean; + language: string; + onChange: (updates: { color?: string | null; icon?: string | null }) => void; + onClose: () => void; +}) { + const [iconPickerOpen, setIconPickerOpen] = useState(false); + const containerRef = useRef(null); + useEffect(() => { + const onDoc = (e: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) onClose(); + }; + document.addEventListener("mousedown", onDoc); + return () => document.removeEventListener("mousedown", onDoc); + }, [onClose]); + const de = language === "de"; + return ( +
e.stopPropagation()} + > +
+ + {de ? "Listen-Stil" : "List style"} + + +
+
+ {de ? "Icon" : "Icon"} + + {list.icon && ( + + )} +
+ {iconPickerOpen && ( +
+ { onChange({ icon: name }); setIconPickerOpen(false); }} + darkMode={darkMode} + /> +
+ )} +
+ {de ? "Farbe" : "Color"} + onChange({ color: e.target.value })} + style={{ width: "32px", height: "32px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }} + /> + {list.color && ( + + )} +
+
+ ); +} + +// Punkt 7 — popover for editing a tab's icon and color (stored in viewSettings JSON). +function TabVisualsPopover({ + tabName, + visuals, + darkMode, + language, + onChange, + onClose, +}: { + tabName: string; + visuals: { color?: string; icon?: string }; + darkMode: boolean; + language: string; + onChange: (updates: { color?: string | null; icon?: string | null }) => void; + onClose: () => void; +}) { + const [iconPickerOpen, setIconPickerOpen] = useState(false); + const containerRef = useRef(null); + useEffect(() => { + const onDoc = (e: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) onClose(); + }; + document.addEventListener("mousedown", onDoc); + return () => document.removeEventListener("mousedown", onDoc); + }, [onClose]); + const de = language === "de"; + return ( +
e.stopPropagation()} + > +
+ + {de ? `Tab-Stil: ${tabName}` : `Tab style: ${tabName}`} + + +
+
+ {de ? "Icon" : "Icon"} + + {visuals.icon && ( + + )} +
+ {iconPickerOpen && ( +
+ { onChange({ icon: name }); setIconPickerOpen(false); }} + darkMode={darkMode} + /> +
+ )} +
+ {de ? "Farbe" : "Color"} + onChange({ color: e.target.value })} + style={{ width: "32px", height: "32px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }} + /> + {visuals.color && ( + + )} +
+
+ ); +} + // Projects Sidebar Component const projectIconsGlobal: { name: string; icon: IconDefinition }[] = [ { name: "folder", icon: faFolder }, { name: "briefcase", icon: faBriefcase },