feat: assign events to calendars
This commit is contained in:
parent
f5feffb4c0
commit
24a8185587
10
CHANGELOG.md
10
CHANGELOG.md
@ -2,6 +2,16 @@
|
||||
|
||||
All notable changes to this project are documented in this file.
|
||||
|
||||
## [1.116.3] - 2026-09-05
|
||||
|
||||
### Added
|
||||
- Calendar events can now be reassigned to another editable calendar from the event editor.
|
||||
- Moving an event updates the source and target calendar caches.
|
||||
|
||||
### Changed
|
||||
- Calendar event backgrounds now use the calendar color with a lighter, readable contrast.
|
||||
- Google event-specific colors no longer override the selected calendar's color.
|
||||
|
||||
## [1.116.2] - 2026-09-05
|
||||
|
||||
### Fixed
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "my-weekly-todo-list",
|
||||
"version": "1.116.2",
|
||||
"version": "1.116.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "my-weekly-todo-list",
|
||||
"version": "1.116.2",
|
||||
"version": "1.116.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@auth/prisma-adapter": "^2.11.1",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "my-weekly-todo-list",
|
||||
"version": "1.116.2",
|
||||
"version": "1.116.3",
|
||||
"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": {
|
||||
|
||||
@ -109,7 +109,7 @@ export async function PATCH(request: NextRequest) {
|
||||
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const { calendarId, eventId, title, description, start, end, location, allDay, recurrence, recurrenceEndDate, recurrenceCount, recurrenceInterval, recurrenceDays, url,
|
||||
const { calendarId, originalCalendarId, eventId, title, description, start, end, location, allDay, recurrence, recurrenceEndDate, recurrenceCount, recurrenceInterval, recurrenceDays, timezone, url,
|
||||
reminders, busyStatus, visibility, attendees, attachments, editMode, recurringEventId } = body;
|
||||
|
||||
console.log('[API] Updating event:', { calendarId, eventId, title, editMode });
|
||||
@ -128,6 +128,14 @@ export async function PATCH(request: NextRequest) {
|
||||
const userId = dbUser2?.id;
|
||||
if (!userId) return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
|
||||
const sourceCalendarId = originalCalendarId || calendarId;
|
||||
const sourceConnection = sourceCalendarId === calendarId
|
||||
? connection
|
||||
: await findConnectionForCalendar(session.user.email, sourceCalendarId);
|
||||
if (!sourceConnection) {
|
||||
return NextResponse.json({ error: 'Original calendar connection not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// For "all events" edit mode on recurring events, use the series master ID
|
||||
let targetEventId = eventId;
|
||||
if (editMode === 'all' && recurringEventId) {
|
||||
@ -139,25 +147,35 @@ export async function PATCH(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
const event = await updateCalendarEvent(connection, calendarId, targetEventId, {
|
||||
title,
|
||||
description,
|
||||
start,
|
||||
end,
|
||||
location,
|
||||
const eventData = {
|
||||
title, description, start, end, location,
|
||||
allDay: allDay !== undefined ? !!allDay : undefined,
|
||||
recurrence,
|
||||
recurrenceEndDate,
|
||||
recurrenceCount,
|
||||
recurrenceInterval,
|
||||
recurrenceDays,
|
||||
url,
|
||||
reminders,
|
||||
busyStatus,
|
||||
visibility,
|
||||
attendees,
|
||||
attachments,
|
||||
} as any);
|
||||
recurrence, recurrenceEndDate, recurrenceCount, recurrenceInterval, recurrenceDays, timezone,
|
||||
url, reminders, busyStatus, visibility, attendees, attachments,
|
||||
} as any;
|
||||
|
||||
// Providers do not share a portable move operation: create in the target,
|
||||
// then remove the original event from its source calendar.
|
||||
if (sourceCalendarId !== calendarId) {
|
||||
if (editMode && editMode !== 'all') {
|
||||
return NextResponse.json({ error: 'Recurring event instances must be moved as a whole series' }, { status: 400 });
|
||||
}
|
||||
const movedEvent = await createCalendarEvent(connection, calendarId, eventData);
|
||||
if (!movedEvent.backgroundColor && connection.calendars) {
|
||||
const cal = (connection.calendars as any[]).find((c: any) => c.id === calendarId);
|
||||
if (cal?.backgroundColor || cal?.color) movedEvent.backgroundColor = cal.backgroundColor || cal.color;
|
||||
}
|
||||
await deleteCalendarEvent(sourceConnection, sourceCalendarId, targetEventId, 'all');
|
||||
try {
|
||||
await deleteCachedEvent(userId, targetEventId, sourceConnection.provider);
|
||||
await upsertCachedEvent(userId, connection.id, connection.provider, movedEvent);
|
||||
} catch (e) {
|
||||
console.error('[CACHE] Failed to update cache after moving event:', e);
|
||||
}
|
||||
return NextResponse.json({ event: movedEvent });
|
||||
}
|
||||
|
||||
const event = await updateCalendarEvent(connection, calendarId, targetEventId, eventData);
|
||||
|
||||
// Attach calendar color from connection metadata for cache + frontend
|
||||
if (!event.backgroundColor && connection.calendars) {
|
||||
|
||||
@ -364,6 +364,7 @@ export default function CalendarEventModal({
|
||||
recurrenceEndDate: recurrence !== 'none' && recurrenceEndType === 'date' && recurrenceEndDateValue ? recurrenceEndDateValue : undefined,
|
||||
recurrenceCount: recurrence !== 'none' && recurrenceEndType === 'count' && recurrenceCount > 0 ? recurrenceCount : undefined,
|
||||
calendarId,
|
||||
...(event?.calendarId ? { originalCalendarId: event.calendarId } : {}),
|
||||
allDay,
|
||||
start: { dateTime: startDate.toISOString() },
|
||||
end: { dateTime: endDate.toISOString() },
|
||||
@ -601,22 +602,22 @@ export default function CalendarEventModal({
|
||||
<div ref={calendarSelectorRef} style={{ ...fieldCol, position: 'relative' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => !event && setIsCalendarSelectorOpen(!isCalendarSelectorOpen)}
|
||||
disabled={!!event}
|
||||
onClick={() => availableCalendars.length > 1 && setIsCalendarSelectorOpen(!isCalendarSelectorOpen)}
|
||||
disabled={availableCalendars.length <= 1}
|
||||
aria-label={language === 'de' ? 'Kalender auswählen' : 'Select calendar'}
|
||||
aria-expanded={isCalendarSelectorOpen}
|
||||
aria-haspopup="listbox"
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: '4px',
|
||||
background: 'transparent', border: 'none', fontWeight: 500,
|
||||
cursor: event ? 'default' : 'pointer', outline: 'none',
|
||||
cursor: availableCalendars.length > 1 ? 'pointer' : 'default', outline: 'none',
|
||||
color: 'var(--weekly-text)', fontSize: '0.8rem', padding: '2px 0',
|
||||
}}
|
||||
>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{selectedCal?.summary || selectedCal?.title || (language === 'de' ? 'Kalender wählen' : 'Select Calendar')}
|
||||
</span>
|
||||
{!event && <ChevronDown size={12} style={{ opacity: 0.5 }} />}
|
||||
{availableCalendars.length > 1 && <ChevronDown size={12} style={{ opacity: 0.5 }} />}
|
||||
</button>
|
||||
|
||||
{isCalendarSelectorOpen && (
|
||||
|
||||
@ -1838,7 +1838,7 @@ export default function WeeklyView() {
|
||||
source: ev.source || calInfo?.provider || 'google',
|
||||
calendarId: ev.calendarId || eventData.calendarId,
|
||||
calendarTitle: ev.calendarTitle || calInfo?.summary || calInfo?.title || '',
|
||||
calendarColor: ev.backgroundColor || ev.calendarColor || calInfo?.backgroundColor || calInfo?.color || '#3b82f6',
|
||||
calendarColor: calInfo?.backgroundColor || calInfo?.color || ev.backgroundColor || ev.calendarColor || '#3b82f6',
|
||||
};
|
||||
setRawCalendarEvents(prev => {
|
||||
if (eventData.id) {
|
||||
@ -1860,13 +1860,17 @@ export default function WeeklyView() {
|
||||
source: ev.source || calInfo?.provider || 'google',
|
||||
calendarId: ev.calendarId || eventData.calendarId,
|
||||
calendarTitle: ev.calendarTitle || calInfo?.summary || calInfo?.title || '',
|
||||
calendarColor: ev.backgroundColor || ev.calendarColor || calInfo?.backgroundColor || calInfo?.color || '#3b82f6',
|
||||
calendarColor: calInfo?.backgroundColor || calInfo?.color || ev.backgroundColor || ev.calendarColor || '#3b82f6',
|
||||
};
|
||||
setRawCalendarEvents(prev => prev.map(e => e.id === eventData.id ? frontendEvent : e));
|
||||
}
|
||||
// Force refresh only the affected provider
|
||||
const connId = getConnectionIdForCalendar(eventData.calendarId);
|
||||
await fetchCalendarEvents(true, connId);
|
||||
const targetConnId = getConnectionIdForCalendar(eventData.calendarId);
|
||||
const sourceConnId = getConnectionIdForCalendar(eventData.originalCalendarId);
|
||||
await fetchCalendarEvents(true, targetConnId);
|
||||
if (sourceConnId && sourceConnId !== targetConnId) {
|
||||
await fetchCalendarEvents(true, sourceConnId);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error saving event:", error);
|
||||
if (error.name === "AbortError") {
|
||||
@ -7803,11 +7807,14 @@ export default function WeeklyView() {
|
||||
// Convert hex to rgba for background, or use default
|
||||
const eventColor = event.calendarColor || "#009a9a";
|
||||
const bgColor = eventColor.startsWith("#")
|
||||
? `${eventColor}20` // Add alpha for transparency
|
||||
? `color-mix(in srgb, ${eventColor} 14%, white)`
|
||||
: eventColor;
|
||||
const borderColor = eventColor.startsWith("#")
|
||||
? eventColor
|
||||
: "var(--weekly-teal)";
|
||||
const textColor = eventColor.startsWith("#")
|
||||
? `color-mix(in srgb, ${eventColor} 78%, black)`
|
||||
: borderColor;
|
||||
|
||||
const overlap = eventOverlapLayout[event.id] || { column: 0, totalColumns: 1 };
|
||||
const colWidth = 100 / overlap.totalColumns;
|
||||
@ -7831,7 +7838,7 @@ export default function WeeklyView() {
|
||||
justifyContent: "flex-start",
|
||||
backgroundColor: bgColor,
|
||||
borderLeftColor: borderColor,
|
||||
color: borderColor,
|
||||
color: textColor,
|
||||
cursor: event.editable
|
||||
? (isDragging ? "grabbing" : "grab")
|
||||
: "default",
|
||||
@ -7965,11 +7972,14 @@ export default function WeeklyView() {
|
||||
{getEventsForDate(date).map((event) => {
|
||||
const eventColor = event.calendarColor || "#009a9a";
|
||||
const bgColor = eventColor.startsWith("#")
|
||||
? `${eventColor}20`
|
||||
? `color-mix(in srgb, ${eventColor} 14%, white)`
|
||||
: eventColor;
|
||||
const borderColor = eventColor.startsWith("#")
|
||||
? eventColor
|
||||
: "var(--weekly-teal)";
|
||||
const textColor = eventColor.startsWith("#")
|
||||
? `color-mix(in srgb, ${eventColor} 78%, black)`
|
||||
: borderColor;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -7987,7 +7997,7 @@ export default function WeeklyView() {
|
||||
style={{
|
||||
backgroundColor: bgColor,
|
||||
borderLeftColor: borderColor,
|
||||
color: borderColor,
|
||||
color: textColor,
|
||||
cursor: event.editable ? "pointer" : "default",
|
||||
}}
|
||||
>
|
||||
|
||||
@ -351,7 +351,10 @@ export const getCalendarEvents = async (
|
||||
const calendarData = calendars.find(c => c.id === calendarId);
|
||||
|
||||
events = events.concat(calendarEvents.map((event: any) => {
|
||||
const eventColor = event.colorId ? getGoogleEventColor(event.colorId) : calendarData?.backgroundColor;
|
||||
// Keep the event tone consistent with the calendar selected by the user.
|
||||
// Google can provide an event-specific color which otherwise makes one
|
||||
// item look unrelated to the rest of its calendar.
|
||||
const eventColor = calendarData?.backgroundColor || (event.colorId ? getGoogleEventColor(event.colorId) : undefined);
|
||||
// Map Google reminders to our format
|
||||
const reminders: EventReminder[] | undefined = event.reminders?.overrides?.map((r: any) => ({
|
||||
method: r.method === 'email' ? 'email' : 'popup',
|
||||
@ -1406,4 +1409,4 @@ export const deleteCalendarEvent = async (
|
||||
}
|
||||
|
||||
throw new Error(`Provider ${connection.provider} does not support deleting events yet.`);
|
||||
};
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user