From da31128121969e780c54b266b2071d52da2968cd Mon Sep 17 00:00:00 2001 From: mARTin Date: Mon, 13 Apr 2026 09:36:38 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20match=20webapp=20style=20=E2=80=94=20sol?= =?UTF-8?q?id=20task=20blocks,=20fix=20all-day=20events,=20tab-sorted=20so?= =?UTF-8?q?meday?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix all-day events: remove externalProvider distinction — ALL tasks with scheduledDate but no startTime belong in the GANZ-TAG strip regardless of source; was incorrectly routing them to a separate noTime strip that didn't show external-provider=null events - Remove noTime calendar strip: it was a wrong concept; tasks outside the time window are simply not shown; someday page handles unscheduled tasks - Solid task blocks: revert from light+border back to solid fill (matching the webapp); use contrastColor() helper to pick white or dark text automatically based on background luminance - All-day strip also uses solid blocks (same style as webapp's ALL DAY bars) - Someday page: group lists by their tab with a tab header row; query now sorts by [tab, order]; lists without a tab get a General/Allgemein header - Show up to 5 all-day tasks per column (was 4) v1.95.4 --- package.json | 2 +- src/app/api/user/export-week-view/route.ts | 274 ++++++++------------- 2 files changed, 98 insertions(+), 178 deletions(-) diff --git a/package.json b/package.json index 1d374fd..f6fbee7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.95.3", + "version": "1.95.4", "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-week-view/route.ts b/src/app/api/user/export-week-view/route.ts index d5725d9..05ebf6f 100644 --- a/src/app/api/user/export-week-view/route.ts +++ b/src/app/api/user/export-week-view/route.ts @@ -167,16 +167,24 @@ type TaskItem = { startTime: string | null; endTime: string | null; completed: boolean; - externalProvider: string | null; project: { name: string; color: string | null } | null; }; type DayData = { - allDay: TaskItem[]; // external all-day calendar events (showAllDay) - noTime: TaskItem[]; // user tasks with no time (showNoTime) - timed: TaskItem[]; + allDay: TaskItem[]; // all tasks without a startTime (shown in GANZ-TAG strip) + timed: TaskItem[]; // tasks with startTime within [startHour, endHour) }; +// Compute a contrasting text color (white or near-black) for a given bg hex +function contrastColor(hex: string): string { + const h = hex.replace('#', ''); + const r = parseInt(h.slice(0, 2), 16) / 255; + const g = parseInt(h.slice(2, 4), 16) / 255; + const b = parseInt(h.slice(4, 6), 16) / 255; + const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b; + return lum > 0.42 ? '#1e293b' : '#ffffff'; +} + type WeatherDay = { maxTemp: number | null; code: number | null; }; // User-specific style settings passed to the PDF @@ -203,6 +211,7 @@ function dayColor(d: Date, isToday: boolean, us: UserStyle): string { type SomedayList = { id: string; title: string; + tab: string | null; tasks: { id: string; title: string; completed: boolean; project: { name: string; color: string | null } | null }[]; }; @@ -244,10 +253,8 @@ function WeekCalendarPDF({ ? (parseInt(userStyle.headlineFontWeight) >= 600 ? 700 : 400) : undefined; - // GRID_H = remaining space after headers and optional strips - const GRID_H = CONTENT_H - DAY_HDR_H - - (showAllDay ? ALL_DAY_H : 0) - - (showNoTime ? NO_TIME_H : 0); + // GRID_H = remaining space after day header and optional all-day strip + const GRID_H = CONTENT_H - DAY_HDR_H - (showAllDay ? ALL_DAY_H : 0); function minutesToY(mins: number): number { return Math.max(0, (mins / totalMins) * GRID_H); @@ -267,7 +274,7 @@ function WeekCalendarPDF({ // Adaptive font size — full names always, size scales with available column width const dayNameSize = numDays <= 2 ? 16 : numDays <= 3 ? 13 : numDays <= 5 ? 10 : 8.5; - const calendarH = DAY_HDR_H + (showAllDay ? ALL_DAY_H : 0) + (showNoTime ? NO_TIME_H : 0) + GRID_H; + const calendarH = DAY_HDR_H + (showAllDay ? ALL_DAY_H : 0) + GRID_H; const firstDay = days[0]; const lastDay = days[days.length - 1]; @@ -358,32 +365,6 @@ function WeekCalendarPDF({ ), ] : []), - // Anyday label spacer - ...(showNoTime ? [ - React.createElement(View, { - style: { - height: NO_TIME_H, - borderBottomWidth: 1, - borderBottomColor: BORDER, - backgroundColor: NOTIME_BG, - alignItems: 'flex-end', - justifyContent: 'flex-start', - paddingRight: 4, - paddingTop: 3, - }, - }, - React.createElement(Text, { - style: { - fontSize: 5.5, - fontFamily: 'Helvetica-Bold', - color: MUTED, - textTransform: 'uppercase', - letterSpacing: 0.3, - }, - }, de ? 'Jederzeit' : 'Any Day'), - ), - ] : []), - // Hour labels (absolute within container) React.createElement(View, { style: { height: GRID_H, position: 'relative' } }, ...Array.from({ length: numHours + 1 }, (_, i) => { @@ -415,9 +396,8 @@ function WeekCalendarPDF({ ? `${de ? (WMO_DE[weather.code] || '') : (WMO_EN[weather.code] || '')}${weather.maxTemp != null ? ` ${Math.round(weather.maxTemp)}°` : ''}` : null; - const allDayTasks = dayData?.allDay || []; - const noTimeTasks = dayData?.noTime || []; - const timedTasks = dayData?.timed || []; + const allDayTasks = dayData?.allDay || []; + const timedTasks = dayData?.timed || []; const dayName = weekdayLabel(d, de); const dateStr = dayDateLabel(d, de); @@ -484,8 +464,9 @@ function WeekCalendarPDF({ overflow: 'hidden', }, }, - ...allDayTasks.slice(0, 4).map(t => { - const c = t.completed ? DONE_BG : (t.project?.color || '#3b82f6'); + ...allDayTasks.slice(0, 5).map(t => { + const bg = t.completed ? DONE_BG : (t.project?.color || userStyle.taskColor); + const fg = t.completed ? DONE_TEXT : contrastColor(bg); return React.createElement(View, { key: t.id, style: { @@ -493,69 +474,25 @@ function WeekCalendarPDF({ paddingVertical: 1.5, paddingHorizontal: 4, marginBottom: 2, - backgroundColor: t.completed ? DONE_BG : lighten(c, 0.7), - borderLeftWidth: 3, - borderLeftColor: c, + backgroundColor: bg, }, }, React.createElement(Text, { style: { fontFamily: 'Helvetica-Bold', fontSize: 6.5, - color: t.completed ? DONE_TEXT : '#1e293b', + color: fg, }, }, (t.title || '').slice(0, 36)), ); }), - allDayTasks.length > 4 ? React.createElement(Text, { - style: { fontSize: 6, color: '#3b82f6', marginTop: 1 }, - }, `+${allDayTasks.length - 4} ${de ? 'weitere' : 'more'}`) : null, + allDayTasks.length > 5 ? React.createElement(Text, { + style: { fontSize: 5.5, color: '#3b82f6', marginTop: 1 }, + }, `+${allDayTasks.length - 5} ${de ? 'weitere' : 'more'}`) : null, ), ] : []), - // ── No-time cell (anyday tasks) - ...(showNoTime ? [ - React.createElement(View, { - style: { - height: NO_TIME_H, - backgroundColor: NOTIME_BG, - borderBottomWidth: 1, - borderBottomColor: BORDER, - paddingHorizontal: 3, - paddingTop: 3, - overflow: 'hidden', - }, - }, - ...noTimeTasks.slice(0, 4).map(t => { - const c = t.completed ? DONE_BG : (t.project?.color || userStyle.taskColor); - return React.createElement(View, { - key: t.id, - style: { - borderRadius: 2, - paddingVertical: 1.5, - paddingHorizontal: 4, - marginBottom: 2, - backgroundColor: t.completed ? DONE_BG : lighten(c, 0.75), - borderLeftWidth: 3, - borderLeftColor: c, - }, - }, - React.createElement(Text, { - style: { - fontFamily: 'Helvetica-Bold', - fontSize: 6.5, - color: t.completed ? DONE_TEXT : '#1e293b', - }, - }, (t.title || '').slice(0, 36)), - ); - }), - noTimeTasks.length > 4 ? React.createElement(Text, { - style: { fontSize: 6, color: MUTED, marginTop: 1 }, - }, `+${noTimeTasks.length - 4} ${de ? 'weitere' : 'more'}`) : null, - ), - ] : []), - - // ── Time grid + // ── Time grid (solid colored blocks, matching webapp style) React.createElement(View, { style: { height: GRID_H, @@ -593,7 +530,7 @@ function WeekCalendarPDF({ }) ), - // Half-hour tick lines + // Half-hour dashes ...Array.from({ length: numHours }, (_, i) => React.createElement(View, { key: `hh${i}`, @@ -607,8 +544,7 @@ function WeekCalendarPDF({ }) ), - // Task blocks (absolute, spanning actual duration) - // Style: light tinted background + colored left border (print-friendly) + // Task blocks — solid fill matching webapp style ...timedTasks.map(t => { const startMins = parseTimeMinutes(t.startTime)!; const endMins = t.endTime @@ -625,14 +561,10 @@ function WeekCalendarPDF({ const rawH = minutesToY(ceMin - csMin); const taskH = Math.max(rawH, 14); - const accentColor = t.completed - ? DONE_BG - : (t.project?.color || userStyle.taskColor); - const bgColor = t.completed ? DONE_BG : lighten(accentColor, 0.78); - const textColor = t.completed ? DONE_TEXT : '#1e293b'; - const subColor = t.completed ? DONE_TEXT : accentColor; - const hasMinutes = t.startTime && !t.startTime.endsWith(':00'); - const isShort = (ceMin - csMin) <= 30; + const bgColor = t.completed ? DONE_BG : (t.project?.color || userStyle.taskColor); + const textColor = t.completed ? DONE_TEXT : contrastColor(bgColor); + const isShort = (ceMin - csMin) <= 30; + const showTime = t.startTime && (!t.startTime.endsWith(':00') || isShort); return React.createElement(View, { key: t.id, @@ -644,10 +576,7 @@ function WeekCalendarPDF({ height: taskH - 2, backgroundColor: bgColor, borderRadius: 3, - borderLeftWidth: 3, - borderLeftColor: accentColor, - paddingLeft: 4, - paddingRight: 3, + paddingHorizontal: 4, paddingVertical: 2, overflow: 'hidden', }, @@ -660,10 +589,10 @@ function WeekCalendarPDF({ lineHeight: 1.25, }, }, (t.title || '').slice(0, 44)), - (hasMinutes || isShort) && t.startTime + showTime ? React.createElement(Text, { - style: { fontSize: 6, color: subColor, marginTop: 0.5 }, - }, fmtTime(t.startTime)) + style: { fontSize: 6, color: textColor, opacity: 0.8, marginTop: 0.5 }, + }, fmtTime(t.startTime!)) : null, ); }).filter(Boolean), @@ -743,77 +672,72 @@ function WeekCalendarPDF({ }, userName), ), - // Lists in columns (flex wrap) - React.createElement(View, { - style: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, + // Group lists by tab, then render tab sections + ...((() => { + // Build ordered tab groups + const tabOrder: string[] = []; + const tabMap = new Map(); + for (const list of somedayLists) { + const tab = list.tab || (de ? 'Allgemein' : 'General'); + if (!tabMap.has(tab)) { tabMap.set(tab, []); tabOrder.push(tab); } + tabMap.get(tab)!.push(list); + } + + const renderList = (list: SomedayList) => React.createElement(View, { + key: list.id, + style: { width: '31%', marginBottom: 10 }, }, - }, - ...somedayLists.map(list => React.createElement(View, { - key: list.id, style: { - width: '30%', - marginBottom: 12, + borderLeftWidth: 3, + borderLeftColor: list.tasks[0]?.project?.color || userStyle.weekdayColor, + paddingLeft: 5, marginBottom: 4, }, }, - // List name + React.createElement(Text, { + style: { fontFamily: 'Helvetica-Bold', fontSize: 7.5, color: '#374151', textTransform: 'uppercase', letterSpacing: 0.3 }, + }, list.title), + ), + ...list.tasks.slice(0, 18).map(t => + React.createElement(View, { + key: t.id, + style: { flexDirection: 'row', alignItems: 'flex-start', paddingVertical: 2, paddingHorizontal: 3, marginBottom: 1, borderRadius: 2, backgroundColor: '#f8fafc' }, + }, + React.createElement(View, { + style: { width: 5, height: 5, borderRadius: 3, borderWidth: 1, borderColor: t.project?.color || MUTED, marginRight: 4, marginTop: 1.5, flexShrink: 0 }, + }), + React.createElement(Text, { + style: { fontSize: 7, color: '#374151', lineHeight: 1.3, flex: 1 }, + }, (t.title || '').slice(0, 52)), + ) + ), + list.tasks.length > 18 ? React.createElement(Text, { + style: { fontSize: 6, color: MUTED, marginTop: 1, paddingLeft: 3 }, + }, `+${list.tasks.length - 18} ${de ? 'weitere' : 'more'}`) : null, + ); + + return tabOrder.map(tab => + React.createElement(View, { key: tab }, + // Tab header React.createElement(View, { style: { - borderLeftWidth: 3, - borderLeftColor: list.tasks[0]?.project?.color || userStyle.weekdayColor, - paddingLeft: 6, - marginBottom: 5, + borderBottomWidth: 1, borderBottomColor: BORDER, + marginBottom: 8, marginTop: 4, paddingBottom: 3, }, }, React.createElement(Text, { - style: { - fontFamily: 'Helvetica-Bold', - fontSize: 8, - color: '#374151', - textTransform: 'uppercase', - letterSpacing: 0.4, - }, - }, list.title), + style: { fontFamily: 'Helvetica-Bold', fontSize: 9, color: userStyle.weekdayColor, textTransform: 'uppercase', letterSpacing: 0.5 }, + }, tab), ), - - // Task rows - ...list.tasks.slice(0, 20).map(t => - React.createElement(View, { - key: t.id, - style: { - flexDirection: 'row', - alignItems: 'flex-start', - paddingVertical: 2.5, - paddingHorizontal: 4, - marginBottom: 1, - borderRadius: 2, - backgroundColor: '#f8fafc', - }, - }, - React.createElement(View, { - style: { - width: 6, height: 6, borderRadius: 3, - borderWidth: 1, - borderColor: t.project?.color || MUTED, - marginRight: 5, - marginTop: 0.5, - flexShrink: 0, - }, - }), - React.createElement(Text, { - style: { fontSize: 7.5, color: '#374151', lineHeight: 1.3, flex: 1 }, - }, (t.title || '').slice(0, 55)), - ) + // Lists in row + React.createElement(View, { + style: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, marginBottom: 6 }, + }, + ...tabMap.get(tab)!.map(renderList), ), - list.tasks.length > 20 ? React.createElement(Text, { - style: { fontSize: 6.5, color: MUTED, marginTop: 2, paddingLeft: 4 }, - }, `+${list.tasks.length - 20} ${de ? 'weitere' : 'more'}`) : null, ) - ), - ), + ); + })()), // Footer React.createElement(View, { @@ -919,29 +843,23 @@ export async function GET(request: Request) { const dk = localDateStr(task.scheduledDate, userTz); if (dk < startDate || dk > endDate) continue; - if (!tasksByDay.has(dk)) tasksByDay.set(dk, { allDay: [], noTime: [], timed: [] }); + if (!tasksByDay.has(dk)) tasksByDay.set(dk, { allDay: [], timed: [] }); const d = tasksByDay.get(dk)!; const item: TaskItem = { id: task.id, title: task.title, startTime: task.startTime, endTime: task.endTime ?? null, completed: task.completed, - externalProvider: (task as any).externalProvider ?? null, project: task.project, }; if (!task.startTime) { - // External calendar all-day events → allDay strip; user tasks → noTime strip - if (item.externalProvider) { - if (showAllDay) d.allDay.push(item); - } else { - if (showNoTime) d.noTime.push(item); - } + // All date-scheduled tasks with no time → all-day strip + if (showAllDay) d.allDay.push(item); } else { const h = parseInt(task.startTime.split(':')[0], 10); if (h >= startHour && h < endHour) { d.timed.push(item); - } else if (showNoTime) { - d.noTime.push(item); // tasks outside the time range → anyday strip } + // Tasks outside the time window are simply not shown in the grid } } @@ -965,16 +883,18 @@ export async function GET(request: Request) { type SomedayListWithTasks = { id: string; title: string; + tab: string | null; tasks: { id: string; title: string; completed: boolean; project: { name: string; color: string | null } | null }[]; }; let somedayLists: SomedayListWithTasks[] = []; if (showNoTime) { const raw = await prisma.somedayList.findMany({ where: { userId: user.id }, - orderBy: { order: 'asc' }, + orderBy: [{ tab: 'asc' }, { order: 'asc' }], select: { id: true, title: true, + tab: true, tasks: { where: { deletedAt: null, completed: false }, orderBy: { order: 'asc' },