From 62ebc56c71b1f883a343f47dfcfb1b4e92ee9dab Mon Sep 17 00:00:00 2001 From: mARTin Date: Mon, 13 Apr 2026 10:14:05 +0200 Subject: [PATCH] feat: fix PDF export style, add calendar events, add print button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Query CachedCalendarEvent table for all-day and timed external calendar events (Google/MS/Apple), so GANZ-TAG strip now shows actual events - Regular user tasks: remove white background — transparent with left border only, matching the webapp's simple view style - All-day strip: calendar events show light tinted chip, user tasks show left-border text only (no fill background) - Add 'Im Browser drucken / Print in Browser' button to the print modal (opens PDF inline in new tab so browser print dialog can be triggered) - Fix Content-Disposition to 'inline' when ?inline=1 param is passed - Update modal preview to show left-border task style (not solid blocks) v1.96.0 --- package.json | 2 +- src/app/api/user/export-week-view/route.ts | 102 ++++++++++++++++----- src/components/WeekPrintModal.tsx | 36 ++++++-- 3 files changed, 112 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 9caa1ba..dd0c3f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.95.6", + "version": "1.96.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-week-view/route.ts b/src/app/api/user/export-week-view/route.ts index 1e9f5d2..2330934 100644 --- a/src/app/api/user/export-week-view/route.ts +++ b/src/app/api/user/export-week-view/route.ts @@ -466,26 +466,30 @@ function WeekCalendarPDF({ }, }, ...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); + if (t.completed) { + return React.createElement(View, { + key: t.id, + style: { borderRadius: 2, paddingVertical: 1.5, paddingHorizontal: 4, marginBottom: 2, backgroundColor: DONE_BG }, + }, React.createElement(Text, { style: { fontSize: 6.5, color: DONE_TEXT } }, (t.title || '').slice(0, 36))); + } + if (t.isExternal) { + // Calendar event: light tinted chip with left border + const evColor = t.project?.color || '#6366f1'; + return React.createElement(View, { + key: t.id, + style: { + borderRadius: 2, borderLeftWidth: 2, borderLeftColor: evColor, + paddingVertical: 1.5, paddingHorizontal: 4, marginBottom: 2, + backgroundColor: lighten(evColor, 0.78), + }, + }, React.createElement(Text, { style: { fontFamily: 'Helvetica-Bold', fontSize: 6.5, color: '#1e293b' } }, (t.title || '').slice(0, 36))); + } + // User-created all-day task: left border only, no background fill + const accent = t.project?.color || userStyle.taskColor; return React.createElement(View, { key: t.id, - style: { - borderRadius: 2, - paddingVertical: 1.5, - paddingHorizontal: 4, - marginBottom: 2, - backgroundColor: bg, - }, - }, - React.createElement(Text, { - style: { - fontFamily: 'Helvetica-Bold', - fontSize: 6.5, - color: fg, - }, - }, (t.title || '').slice(0, 36)), - ); + style: { borderLeftWidth: 2, borderLeftColor: accent, paddingVertical: 1.5, paddingHorizontal: 4, marginBottom: 2 }, + }, React.createElement(Text, { style: { fontSize: 6.5, color: '#374151' } }, (t.title || '').slice(0, 36))); }), allDayTasks.length > 5 ? React.createElement(Text, { style: { fontSize: 5.5, color: '#3b82f6', marginTop: 1 }, @@ -608,12 +612,12 @@ function WeekCalendarPDF({ ); } - // Regular user task: white background, thin colored left border + // Regular user task: transparent, thin colored left border (matches webapp simple view) return React.createElement(View, { key: t.id, style: { position: 'absolute', top: topPx + 1, left: 2, right: 2, - height: taskH - 2, backgroundColor: WHITE, + height: taskH - 2, borderLeftWidth: 3, borderLeftColor: accentColor, paddingLeft: 5, paddingRight: 3, paddingVertical: 2, overflow: 'hidden', @@ -820,6 +824,7 @@ export async function GET(request: Request) { const showAllDay = searchParams.get('showAllDay') !== '0'; const showNoTime = searchParams.get('showNoTime') !== '0'; const showWeather = searchParams.get('showWeather') === '1'; + const inline = searchParams.get('inline') === '1'; if (!startDate || !endDate) return new NextResponse('Missing startDate or endDate', { status: 400 }); @@ -895,6 +900,61 @@ export async function GET(request: Request) { } } + // ── Fetch CachedCalendarEvents (Google/MS/Apple Calendar) + const calEvents = await (prisma as any).cachedCalendarEvent.findMany({ + where: { + userId: user.id, + OR: [ + { startDate: { gte: startDate, lte: endDate } }, + { startDateTime: { gte: startDt, lte: endDt } }, + ], + }, + select: { + id: true, title: true, calendarColor: true, + startDate: true, startDateTime: true, endDateTime: true, + }, + }); + + for (const ev of calEvents) { + if (ev.startDate) { + // All-day external event (e.g. Google all-day) + if (!showAllDay) continue; + const dk = ev.startDate as string; + if (dk < startDate || dk > endDate) continue; + if (!tasksByDay.has(dk)) tasksByDay.set(dk, { allDay: [], timed: [] }); + tasksByDay.get(dk)!.allDay.push({ + id: `cal-${ev.id}`, title: ev.title as string | null, + startTime: null, endTime: null, completed: false, isExternal: true, + project: { name: '', color: (ev.calendarColor as string | null) || '#6366f1' }, + }); + } else if (ev.startDateTime) { + // Timed external event + const dk = localDateStr(ev.startDateTime as Date, userTz); + if (dk < startDate || dk > endDate) continue; + if (!tasksByDay.has(dk)) tasksByDay.set(dk, { allDay: [], timed: [] }); + const startTimeStr = (ev.startDateTime as Date).toLocaleTimeString('en-GB', { + hour: '2-digit', minute: '2-digit', timeZone: userTz, + }); + const endTimeStr = ev.endDateTime + ? (ev.endDateTime as Date).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', timeZone: userTz }) + : null; + const h = parseInt(startTimeStr.split(':')[0], 10); + if (h >= startHour && h < endHour) { + tasksByDay.get(dk)!.timed.push({ + id: `cal-${ev.id}`, title: ev.title as string | null, + startTime: startTimeStr, endTime: endTimeStr, + completed: false, isExternal: true, + project: { name: '', color: (ev.calendarColor as string | null) || '#6366f1' }, + }); + } + } + } + + // Sort timed tasks by start time within each day (user tasks + calendar events merged) + for (const dayData of tasksByDay.values()) { + dayData.timed.sort((a, b) => (a.startTime || '').localeCompare(b.startTime || '')); + } + // ── Weather const weatherByDay = new Map(); if (showWeather && (user as any).weatherLat && (user as any).weatherLon) { @@ -965,7 +1025,7 @@ export async function GET(request: Request) { return new NextResponse(new Uint8Array(pdfBuffer), { headers: { 'Content-Type': 'application/pdf', - 'Content-Disposition': `attachment; filename="${filename}"`, + 'Content-Disposition': `${inline ? 'inline' : 'attachment'}; filename="${filename}"`, }, }); } catch (error) { diff --git a/src/components/WeekPrintModal.tsx b/src/components/WeekPrintModal.tsx index b73156f..a4d29fa 100644 --- a/src/components/WeekPrintModal.tsx +++ b/src/components/WeekPrintModal.tsx @@ -152,6 +152,8 @@ export default function WeekPrintModal({ return `/api/user/export-week-view?${params}`; }, [startDate, endDate, startHour, endHour, lang, showAllDay, showNoTime, showWeather]); + const printUrl = useMemo(() => `${pdfUrl}&inline=1`, [pdfUrl]); + const handleStartDateChange = useCallback((val: string) => { setStartDate(val); }, []); @@ -198,6 +200,7 @@ export default function WeekPrintModal({ days: de ? "Tage" : "days", language: de ? "Sprache" : "Language", cancel: de ? "Abbrechen" : "Cancel", + print: de ? "Im Browser drucken" : "Print in Browser", download: de ? "PDF herunterladen" : "Download PDF", kw: de ? "KW" : "W", }; @@ -410,6 +413,18 @@ export default function WeekPrintModal({ > {t.cancel} + {SAMPLE_TASKS.filter(t => t.dayIdx === i && t.hourOffset === 0).slice(0, 1).map((t, j) => (
+ borderLeft: `2px solid ${t.color}`, + backgroundColor: `${t.color}28`, + borderRadius: 2, height: 8, + width: `${t.w * 80}%`, + paddingLeft: 2, display: "flex", alignItems: "center", + }}> +
+
))}
))} @@ -605,10 +625,14 @@ function CalendarPreview({ }}> {tasks.slice(0, 2).map((t, j) => (
+ marginBottom: 1, paddingLeft: 2, + display: "flex", alignItems: "center", + overflow: "hidden", + }}> +
+
))}
);