feat: fix PDF export style, add calendar events, add print button

- 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
This commit is contained in:
mARTin 2026-04-13 10:14:05 +02:00
parent 27ccd275f6
commit 62ebc56c71
3 changed files with 112 additions and 28 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "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", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -466,26 +466,30 @@ function WeekCalendarPDF({
}, },
}, },
...allDayTasks.slice(0, 5).map(t => { ...allDayTasks.slice(0, 5).map(t => {
const bg = t.completed ? DONE_BG : (t.project?.color || userStyle.taskColor); if (t.completed) {
const fg = t.completed ? DONE_TEXT : contrastColor(bg); 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, { return React.createElement(View, {
key: t.id, key: t.id,
style: { style: { borderLeftWidth: 2, borderLeftColor: accent, paddingVertical: 1.5, paddingHorizontal: 4, marginBottom: 2 },
borderRadius: 2, }, React.createElement(Text, { style: { fontSize: 6.5, color: '#374151' } }, (t.title || '').slice(0, 36)));
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)),
);
}), }),
allDayTasks.length > 5 ? React.createElement(Text, { allDayTasks.length > 5 ? React.createElement(Text, {
style: { fontSize: 5.5, color: '#3b82f6', marginTop: 1 }, 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, { return React.createElement(View, {
key: t.id, key: t.id,
style: { style: {
position: 'absolute', top: topPx + 1, left: 2, right: 2, position: 'absolute', top: topPx + 1, left: 2, right: 2,
height: taskH - 2, backgroundColor: WHITE, height: taskH - 2,
borderLeftWidth: 3, borderLeftColor: accentColor, borderLeftWidth: 3, borderLeftColor: accentColor,
paddingLeft: 5, paddingRight: 3, paddingVertical: 2, paddingLeft: 5, paddingRight: 3, paddingVertical: 2,
overflow: 'hidden', overflow: 'hidden',
@ -820,6 +824,7 @@ export async function GET(request: Request) {
const showAllDay = searchParams.get('showAllDay') !== '0'; const showAllDay = searchParams.get('showAllDay') !== '0';
const showNoTime = searchParams.get('showNoTime') !== '0'; const showNoTime = searchParams.get('showNoTime') !== '0';
const showWeather = searchParams.get('showWeather') === '1'; const showWeather = searchParams.get('showWeather') === '1';
const inline = searchParams.get('inline') === '1';
if (!startDate || !endDate) return new NextResponse('Missing startDate or endDate', { status: 400 }); 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 // ── Weather
const weatherByDay = new Map<string, WeatherDay>(); const weatherByDay = new Map<string, WeatherDay>();
if (showWeather && (user as any).weatherLat && (user as any).weatherLon) { 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), { return new NextResponse(new Uint8Array(pdfBuffer), {
headers: { headers: {
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${filename}"`, 'Content-Disposition': `${inline ? 'inline' : 'attachment'}; filename="${filename}"`,
}, },
}); });
} catch (error) { } catch (error) {

View File

@ -152,6 +152,8 @@ export default function WeekPrintModal({
return `/api/user/export-week-view?${params}`; return `/api/user/export-week-view?${params}`;
}, [startDate, endDate, startHour, endHour, lang, showAllDay, showNoTime, showWeather]); }, [startDate, endDate, startHour, endHour, lang, showAllDay, showNoTime, showWeather]);
const printUrl = useMemo(() => `${pdfUrl}&inline=1`, [pdfUrl]);
const handleStartDateChange = useCallback((val: string) => { const handleStartDateChange = useCallback((val: string) => {
setStartDate(val); setStartDate(val);
}, []); }, []);
@ -198,6 +200,7 @@ export default function WeekPrintModal({
days: de ? "Tage" : "days", days: de ? "Tage" : "days",
language: de ? "Sprache" : "Language", language: de ? "Sprache" : "Language",
cancel: de ? "Abbrechen" : "Cancel", cancel: de ? "Abbrechen" : "Cancel",
print: de ? "Im Browser drucken" : "Print in Browser",
download: de ? "PDF herunterladen" : "Download PDF", download: de ? "PDF herunterladen" : "Download PDF",
kw: de ? "KW" : "W", kw: de ? "KW" : "W",
}; };
@ -410,6 +413,18 @@ export default function WeekPrintModal({
> >
{t.cancel} {t.cancel}
</button> </button>
<button
onClick={() => window.open(printUrl, "_blank")}
style={{
padding: "9px 20px", borderRadius: 8, border: "none",
background: "#1e293b", color: "#fff", fontWeight: 600,
fontSize: "0.88rem",
display: "flex", alignItems: "center", gap: 7, cursor: "pointer",
}}
>
<Printer size={15} />
{t.print}
</button>
<a <a
href={pdfUrl} href={pdfUrl}
target="_blank" target="_blank"
@ -560,9 +575,14 @@ function CalendarPreview({
}}> }}>
{SAMPLE_TASKS.filter(t => t.dayIdx === i && t.hourOffset === 0).slice(0, 1).map((t, j) => ( {SAMPLE_TASKS.filter(t => t.dayIdx === i && t.hourOffset === 0).slice(0, 1).map((t, j) => (
<div key={j} style={{ <div key={j} style={{
background: t.color, borderRadius: 2, height: 8, borderLeft: `2px solid ${t.color}`,
width: `${t.w * 80}%`, opacity: 0.8, backgroundColor: `${t.color}28`,
}} /> borderRadius: 2, height: 8,
width: `${t.w * 80}%`,
paddingLeft: 2, display: "flex", alignItems: "center",
}}>
<div style={{ height: 3, flex: 1, backgroundColor: `${t.color}66`, borderRadius: 1 }} />
</div>
))} ))}
</div> </div>
))} ))}
@ -605,10 +625,14 @@ function CalendarPreview({
}}> }}>
{tasks.slice(0, 2).map((t, j) => ( {tasks.slice(0, 2).map((t, j) => (
<div key={j} style={{ <div key={j} style={{
background: t.color, borderRadius: 2, borderLeft: `2px solid ${t.color}`,
height: 9, width: `${t.w * 90}%`, height: 9, width: `${t.w * 90}%`,
marginBottom: 1, opacity: 0.85, marginBottom: 1, paddingLeft: 2,
}} /> display: "flex", alignItems: "center",
overflow: "hidden",
}}>
<div style={{ height: 4, flex: 1, backgroundColor: "#d1d5db", borderRadius: 1 }} />
</div>
))} ))}
</div> </div>
); );