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",
|
"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": {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user