From 6836cf58bc8185c9af296cd9f4bb7706b196e684 Mon Sep 17 00:00:00 2001 From: mARTin Date: Tue, 7 Apr 2026 00:03:09 +0200 Subject: [PATCH] feat: filters in all priority mode right columns + work report CSV MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Priority View: filter panel (project / list / timespan) is now always visible in the right column of every priority mode — Eisenhower, ABCDE, Ivy Lee, and 80/20. Removed the collapse toggle. An 'active' indicator in the header shows when any filter is set. On mobile the filter appears above the main content. Work Report CSV (Settings > Data Export): - Columns: Week / Completed / Scheduled / Created / Task / Project / List / Priority / Delegated to / Notes - Rows grouped by calendar week (ISO week, most recent first), with a section header row showing "KW 14 — 2026 (30 Mär – 05 Apr)" - Priority shows Eisenhower label (Sofort erledigen / Planen / Delegieren / Eliminieren) or ABCDE grade - Includes project name, Someday list name, delegation info and notes - UTF-8 BOM prepended so Excel opens German umlauts correctly - Language-aware column headers (de/en) via ?lang= param - Filename: arbeitsbericht_YYYY-MM-DD_bis_YYYY-MM-DD_date.csv v1.91.0 --- package.json | 2 +- src/app/api/user/export/route.ts | 116 ++++++++++++++++++++++++----- src/components/PriorityView.tsx | 51 +++++++------ src/components/SettingsSidebar.tsx | 10 +-- 4 files changed, 132 insertions(+), 47 deletions(-) diff --git a/package.json b/package.json index 468614b..27979dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.90.0", + "version": "1.91.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/src/app/api/user/export/route.ts b/src/app/api/user/export/route.ts index c497edf..41dca67 100644 --- a/src/app/api/user/export/route.ts +++ b/src/app/api/user/export/route.ts @@ -3,11 +3,55 @@ import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; import { prisma } from '@/lib/prisma'; +/** Return ISO week string like "2026-W14" */ +function isoWeek(date: Date): string { + const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); + const dayNum = d.getUTCDay() || 7; // Mon=1 … Sun=7 + d.setUTCDate(d.getUTCDate() + 4 - dayNum); + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + const weekNo = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7); + return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`; +} + +function weekLabel(date: Date): string { + // "KW 14 — 2026 (Mon 30 Mar – Sun 5 Apr)" + const week = isoWeek(date); + const [year, w] = week.split('-W'); + // Find Monday of that week + const d = new Date(Date.UTC(parseInt(year), 0, 1)); + d.setUTCDate(d.getUTCDate() + (parseInt(w) - 1) * 7 - (d.getUTCDay() || 7) + 1); + const sun = new Date(d); sun.setUTCDate(d.getUTCDate() + 6); + const fmt = (x: Date) => x.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', timeZone: 'UTC' }); + return `KW ${w} — ${year} (${fmt(d)} – ${fmt(sun)})`; +} + +function fmtDate(d: Date | null | undefined): string { + if (!d) return ''; + return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); +} + +function priorityLabel(task: any): string { + if (task.urgency != null && task.importance != null) { + if (task.urgency && task.importance) return 'Sofort erledigen'; + if (!task.urgency && task.importance) return 'Planen'; + if (task.urgency && !task.importance) return 'Delegieren'; + return 'Eliminieren'; + } + if (task.priority) return task.priority; + return ''; +} + +function csvCell(value: string): string { + return `"${(value || '').replace(/"/g, '""').replace(/\n/g, ' ')}"`; +} + export async function GET(request: Request) { const session = await getServerSession(authOptions); const { searchParams } = new URL(request.url); const startDate = searchParams.get('startDate'); const endDate = searchParams.get('endDate'); + const lang = searchParams.get('lang') || 'de'; + const de = lang === 'de'; if (!session || !session.user?.email) { return new NextResponse('Unauthorized', { status: 401 }); @@ -25,6 +69,7 @@ export async function GET(request: Request) { const where: any = { userId: user.id, completed: true, + deletedAt: null, }; if (startDate || endDate) { @@ -37,32 +82,67 @@ export async function GET(request: Request) { } } - // Fetch filtered tasks const tasks = await prisma.task.findMany({ where, - orderBy: { - updatedAt: 'desc', + orderBy: { updatedAt: 'desc' }, + include: { + project: { select: { name: true, color: true } }, + somedayList: { select: { title: true } }, }, }); - // Generate CSV - const headers = ['Title', 'Description', 'Completed Date', 'Created Date']; - const rows = tasks.map((task: any) => [ - task.title, - task.description || '', - task.updatedAt.toISOString(), - task.createdAt.toISOString(), - ]); + // Group by ISO week (most recent first) + const byWeek = new Map(); + for (const task of tasks) { + const key = isoWeek(task.updatedAt); + if (!byWeek.has(key)) byWeek.set(key, []); + byWeek.get(key)!.push(task); + } - const csvContent = [ - headers.join(','), - ...rows.map((row: string[]) => row.map((cell: string) => `"${(cell || '').replace(/"/g, '""')}"`).join(',')) - ].join('\n'); + const headers = de + ? ['Woche', 'Erledigt am', 'Geplant für', 'Erstellt am', 'Aufgabe', 'Projekt', 'Liste', 'Priorität', 'Delegiert an', 'Notiz'] + : ['Week', 'Completed', 'Scheduled', 'Created', 'Task', 'Project', 'List', 'Priority', 'Delegated to', 'Notes']; - return new NextResponse(csvContent, { + const rows: string[][] = [headers]; + + for (const [weekKey, weekTasks] of byWeek) { + // Section header row (empty for all columns except Week) + const weekDisplay = weekLabel(weekTasks[0].updatedAt); + rows.push([weekDisplay, '', '', '', '', '', '', '', '', '']); + + for (const t of weekTasks) { + rows.push([ + '', // no week label on task rows (grouped visually) + fmtDate(t.updatedAt), + fmtDate(t.scheduledDate ?? undefined), + fmtDate(t.createdAt), + t.title || '', + t.project?.name || '', + t.somedayList?.title || '', + priorityLabel(t), + t.delegatedTo || '', + (t.markdownContent || t.description || '').slice(0, 500), + ]); + } + + // Blank spacer between weeks + rows.push(Array(headers.length).fill('')); + } + + const csvContent = rows + .map((row) => row.map(csvCell).join(',')) + .join('\r\n'); + + // Prepend BOM so Excel opens UTF-8 correctly + const bom = '\uFEFF'; + const period = startDate && endDate + ? `_${startDate}_bis_${endDate}` + : startDate ? `_ab_${startDate}` : endDate ? `_bis_${endDate}` : ''; + + return new NextResponse(bom + csvContent, { headers: { - 'Content-Type': 'text/csv', - 'Content-Disposition': `attachment; filename="completed_tasks_${new Date().toISOString().split('T')[0]}.csv"`, + 'Content-Type': 'text/csv; charset=utf-8', + 'Content-Disposition': `attachment; filename="arbeitsbericht${period}_${new Date().toISOString().split('T')[0]}.csv"`, }, }); } catch (error) { diff --git a/src/components/PriorityView.tsx b/src/components/PriorityView.tsx index 2098496..b92f5a9 100644 --- a/src/components/PriorityView.tsx +++ b/src/components/PriorityView.tsx @@ -280,7 +280,7 @@ export default function PriorityView({ { key: "pareto", label: "80/20", icon: }, ]; - // Filter panel — rendered in Eisenhower's right column or above content in other modes + // Filter panel — always rendered in the right column of every mode const filterPanel = (
@@ -352,27 +352,15 @@ export default function PriorityView({ {de ? "Prioritäten" : "Priority View"}
- {/* Filter toggle — only for non-Eisenhower modes (Eisenhower shows filters in its right panel) */} - {method !== "eisenhower" && ( - + {/* Filter active indicator — filters are always in the right column */} + {(filterProject || filterList || filterTimespan !== "all") && ( + + + {de ? "Filter aktiv" : "Filter active"} + )}
- {/* ─── Filters Panel (non-Eisenhower modes only) ─── */} - {method !== "eisenhower" && showFilters && ( -
- {filterPanel} -
- )} {/* ─── Method Tabs ─── */}
@@ -447,6 +435,7 @@ export default function PriorityView({ textPrimary={textPrimary} textSecondary={textSecondary} isMobile={isMobile} + filterPanel={filterPanel} /> )} @@ -467,6 +456,7 @@ export default function PriorityView({ textPrimary={textPrimary} textSecondary={textSecondary} isMobile={isMobile} + filterPanel={filterPanel} /> )} @@ -486,6 +476,7 @@ export default function PriorityView({ textPrimary={textPrimary} textSecondary={textSecondary} isMobile={isMobile} + filterPanel={filterPanel} /> )} @@ -955,9 +946,10 @@ interface AbcdeViewProps { 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 }: AbcdeViewProps) { +function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary, isMobile, filterPanel }: AbcdeViewProps) { const [collapsed, setCollapsed] = useState>({ none: true }); const [dragOver, setDragOver] = useState(null); @@ -1038,6 +1030,8 @@ function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onT // Ungraded tasks panel (right column) const hasUngraded = (groups.none || []).length > 0; const ungradedPanel = ( +
+ {filterPanel}
)}
+
); if (isMobile) { @@ -1107,9 +1102,10 @@ interface IvyLeeViewProps { 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 }: IvyLeeViewProps) { +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)); @@ -1185,7 +1181,9 @@ function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onTog // Available tasks pool (right column) const availablePool = ( -
+
+ {filterPanel} +

{unselected.length > 0 ? (de ? "Verfügbare Aufgaben:" : "Available tasks:") : (de ? "Alle Aufgaben ausgewählt ✓" : "All tasks selected ✓")}

@@ -1214,6 +1212,7 @@ function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onTog
))}
+
); @@ -1221,6 +1220,7 @@ function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onTog return (
{infoBanner} + {filterPanel &&
{filterPanel}
} {top6Panel} {unselected.length > 0 &&
{availablePool}
}
@@ -1255,9 +1255,10 @@ interface ParetoViewProps { textPrimary: string; textSecondary: string; isMobile?: boolean; + filterPanel?: React.ReactNode; } -function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary, isMobile }: ParetoViewProps) { +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; @@ -1311,6 +1312,8 @@ function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplet // Remaining tasks (right column — always visible, no toggle) const remainingPanel = ( +
+ {filterPanel}
@@ -1335,12 +1338,14 @@ function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplet
)}
+
); if (isMobile) { return (
{header} + {filterPanel &&
{filterPanel}
} {importantPanel}
{remainingPanel}
diff --git a/src/components/SettingsSidebar.tsx b/src/components/SettingsSidebar.tsx index f7465c1..91cb4ac 100644 --- a/src/components/SettingsSidebar.tsx +++ b/src/components/SettingsSidebar.tsx @@ -4244,8 +4244,8 @@ function SettingsSidebar({ }} > {profile.language === "de" - ? "Laden Sie eine CSV-Datei Ihrer erledigten Aufgaben herunter." - : "Download a CSV file of your completed tasks."} + ? "CSV-Arbeitsbericht mit erledigten Aufgaben, Projekten und Prioritäten — nach Kalenderwochen gruppiert. Ideal für Freelancer und Mitarbeiter." + : "CSV work report of completed tasks with project, list, priority and notes — grouped by calendar week. Perfect for freelancers and employees."}

{profile.language === "de" - ? "Erledigte Aufgaben exportieren (CSV)" - : "Export Completed Tasks (CSV)"} + ? "Arbeitsbericht exportieren (CSV)" + : "Export Work Report (CSV)"}