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:
parent
27ccd275f6
commit
62ebc56c71
@ -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": {
|
||||
|
||||
@ -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,
|
||||
paddingVertical: 1.5,
|
||||
paddingHorizontal: 4,
|
||||
marginBottom: 2,
|
||||
backgroundColor: bg,
|
||||
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: fg,
|
||||
},
|
||||
}, (t.title || '').slice(0, 36)),
|
||||
);
|
||||
}, 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: { 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<string, WeatherDay>();
|
||||
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) {
|
||||
|
||||
@ -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}
|
||||
</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
|
||||
href={pdfUrl}
|
||||
target="_blank"
|
||||
@ -560,9 +575,14 @@ function CalendarPreview({
|
||||
}}>
|
||||
{SAMPLE_TASKS.filter(t => t.dayIdx === i && t.hourOffset === 0).slice(0, 1).map((t, j) => (
|
||||
<div key={j} style={{
|
||||
background: t.color, borderRadius: 2, height: 8,
|
||||
width: `${t.w * 80}%`, opacity: 0.8,
|
||||
}} />
|
||||
borderLeft: `2px solid ${t.color}`,
|
||||
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>
|
||||
))}
|
||||
@ -605,10 +625,14 @@ function CalendarPreview({
|
||||
}}>
|
||||
{tasks.slice(0, 2).map((t, j) => (
|
||||
<div key={j} style={{
|
||||
background: t.color, borderRadius: 2,
|
||||
borderLeft: `2px solid ${t.color}`,
|
||||
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>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user