fix: match webapp style — solid task blocks, fix all-day events, tab-sorted someday
- 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
This commit is contained in:
parent
3fd738b185
commit
da31128121
@ -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": {
|
||||
|
||||
@ -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) => {
|
||||
@ -416,7 +397,6 @@ function WeekCalendarPDF({
|
||||
: null;
|
||||
|
||||
const allDayTasks = dayData?.allDay || [];
|
||||
const noTimeTasks = dayData?.noTime || [];
|
||||
const timedTasks = dayData?.timed || [];
|
||||
|
||||
const dayName = weekdayLabel(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 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,
|
||||
},
|
||||
},
|
||||
...somedayLists.map(list =>
|
||||
React.createElement(View, {
|
||||
// Group lists by tab, then render tab sections
|
||||
...((() => {
|
||||
// Build ordered tab groups
|
||||
const tabOrder: string[] = [];
|
||||
const tabMap = new Map<string, SomedayList[]>();
|
||||
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: '30%',
|
||||
marginBottom: 12,
|
||||
style: { width: '31%', marginBottom: 10 },
|
||||
},
|
||||
},
|
||||
// List name
|
||||
React.createElement(View, {
|
||||
style: {
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: list.tasks[0]?.project?.color || userStyle.weekdayColor,
|
||||
paddingLeft: 6,
|
||||
marginBottom: 5,
|
||||
paddingLeft: 5, marginBottom: 4,
|
||||
},
|
||||
},
|
||||
React.createElement(Text, {
|
||||
style: {
|
||||
fontFamily: 'Helvetica-Bold',
|
||||
fontSize: 8,
|
||||
color: '#374151',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.4,
|
||||
},
|
||||
style: { fontFamily: 'Helvetica-Bold', fontSize: 7.5, color: '#374151', textTransform: 'uppercase', letterSpacing: 0.3 },
|
||||
}, list.title),
|
||||
),
|
||||
|
||||
// Task rows
|
||||
...list.tasks.slice(0, 20).map(t =>
|
||||
...list.tasks.slice(0, 18).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',
|
||||
},
|
||||
style: { flexDirection: 'row', alignItems: 'flex-start', paddingVertical: 2, paddingHorizontal: 3, 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,
|
||||
},
|
||||
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.5, color: '#374151', lineHeight: 1.3, flex: 1 },
|
||||
}, (t.title || '').slice(0, 55)),
|
||||
style: { fontSize: 7, color: '#374151', lineHeight: 1.3, flex: 1 },
|
||||
}, (t.title || '').slice(0, 52)),
|
||||
)
|
||||
),
|
||||
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,
|
||||
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: {
|
||||
borderBottomWidth: 1, borderBottomColor: BORDER,
|
||||
marginBottom: 8, marginTop: 4, paddingBottom: 3,
|
||||
},
|
||||
},
|
||||
React.createElement(Text, {
|
||||
style: { fontFamily: 'Helvetica-Bold', fontSize: 9, color: userStyle.weekdayColor, textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
}, tab),
|
||||
),
|
||||
// Lists in row
|
||||
React.createElement(View, {
|
||||
style: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, marginBottom: 6 },
|
||||
},
|
||||
...tabMap.get(tab)!.map(renderList),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
})()),
|
||||
|
||||
// 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) {
|
||||
// All date-scheduled tasks with no time → all-day strip
|
||||
if (showAllDay) d.allDay.push(item);
|
||||
} else {
|
||||
if (showNoTime) d.noTime.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' },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user