feat: sync calendar colors from all providers and add custom recurrence

- Store calendar colors (backgroundColor) from Apple and Synology
  during connection setup
- Auto-refresh missing colors for existing Apple/Synology connections
- Show color dots next to calendar names in settings
- Add "Custom..." recurrence option with interval (every N days/weeks/
  months/years) and day-of-week selection for weekly recurrence
- Generate proper RRULE with INTERVAL and BYDAY for all providers
- Generate proper Outlook Graph recurrence with custom intervals

v1.61.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-03-23 17:35:22 +01:00
parent 1ab98025a6
commit c5dc315f82
10 changed files with 172 additions and 26 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.60.0", "version": "1.61.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

@ -57,6 +57,7 @@ export async function POST(req: Request) {
id: cal.id, id: cal.id,
title: cal.title, title: cal.title,
isPrimary: cal.isPrimary, isPrimary: cal.isPrimary,
backgroundColor: cal.color || undefined,
selected: true // Default to selected selected: true // Default to selected
})) as any, })) as any,
updatedAt: new Date() updatedAt: new Date()

View File

@ -4,6 +4,7 @@ import { authOptions } from "@/lib/auth";
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
import { refreshConnectionCache, RefreshableConnection } from '@/lib/calendar-cache'; import { refreshConnectionCache, RefreshableConnection } from '@/lib/calendar-cache';
import { getUserCalendars as getSynologyCalendars } from '@/lib/synology-calendar'; import { getUserCalendars as getSynologyCalendars } from '@/lib/synology-calendar';
import { getUserCalendars as getAppleCalendars } from '@/lib/apple-calendar';
// Get user's calendar connections // Get user's calendar connections
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
@ -42,7 +43,7 @@ export async function GET(request: NextRequest) {
); );
} }
// For Synology connections, fire-and-forget prune of deleted calendars // For Synology connections, fire-and-forget prune of deleted calendars + refresh colors
if (conn.provider === 'synology' && Array.isArray(calendars) && calendars.length > 0) { if (conn.provider === 'synology' && Array.isArray(calendars) && calendars.length > 0) {
const connId = conn.id; const connId = conn.id;
const storedCalendars = calendars; const storedCalendars = calendars;
@ -52,14 +53,20 @@ export async function GET(request: NextRequest) {
const serverUrl = conn.refreshToken; const serverUrl = conn.refreshToken;
if (username && password && serverUrl) { if (username && password && serverUrl) {
const freshCalendars = await getSynologyCalendars(serverUrl, username, password); const freshCalendars = await getSynologyCalendars(serverUrl, username, password);
const freshIds = new Set(freshCalendars.map(c => c.id)); const freshMap = new Map(freshCalendars.map(c => [c.id, c]));
const staleIds = storedCalendars.filter((c: any) => !freshIds.has(c.id)).map((c: any) => c.id); const staleIds = storedCalendars.filter((c: any) => !freshMap.has(c.id)).map((c: any) => c.id);
if (staleIds.length > 0) { const hasMissingColors = storedCalendars.some((c: any) => !c.backgroundColor);
console.log('[CONNECTIONS] Synology stale calendars to prune:', staleIds); if (staleIds.length > 0 || hasMissingColors) {
const pruned = storedCalendars.filter((c: any) => freshIds.has(c.id)); console.log('[CONNECTIONS] Synology refresh: pruning', staleIds.length, 'stale, refreshing colors:', hasMissingColors);
const updated = storedCalendars
.filter((c: any) => freshMap.has(c.id))
.map((c: any) => {
const fresh = freshMap.get(c.id);
return { ...c, backgroundColor: c.backgroundColor || fresh?.color || undefined };
});
await prisma.calendarConnection.update({ await prisma.calendarConnection.update({
where: { id: connId }, where: { id: connId },
data: { calendars: pruned }, data: { calendars: updated },
}); });
} }
} }
@ -69,6 +76,35 @@ export async function GET(request: NextRequest) {
})(); })();
} }
// For Apple connections, fire-and-forget refresh of missing colors
if (conn.provider === 'apple' && Array.isArray(calendars) && calendars.length > 0) {
const connId = conn.id;
const storedCalendars = calendars;
const hasMissingColors = storedCalendars.some((c: any) => !c.backgroundColor);
if (hasMissingColors) {
(async () => {
try {
const [email, appPassword] = conn.accessToken.split(':');
if (email && appPassword) {
const freshCalendars = await getAppleCalendars(email, appPassword);
const freshMap = new Map(freshCalendars.map(c => [c.id, c]));
const updated = storedCalendars.map((c: any) => {
const fresh = freshMap.get(c.id);
return { ...c, backgroundColor: c.backgroundColor || fresh?.color || undefined };
});
await prisma.calendarConnection.update({
where: { id: connId },
data: { calendars: updated },
});
console.log('[CONNECTIONS] Apple calendars refreshed with colors');
}
} catch (err) {
console.error('[CONNECTIONS] Apple calendar color refresh failed:', err);
}
})();
}
}
return { return {
id: conn.id, id: conn.id,
provider: conn.provider, provider: conn.provider,

View File

@ -39,7 +39,7 @@ export async function POST(request: NextRequest) {
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const body = await request.json(); const body = await request.json();
const { calendarId, title, description, start, end, location, allDay, recurrence, recurrenceEndDate, recurrenceCount, url, const { calendarId, title, description, start, end, location, allDay, recurrence, recurrenceEndDate, recurrenceCount, recurrenceInterval, recurrenceDays, url,
reminders, busyStatus, visibility, attendees, attachments } = body; reminders, busyStatus, visibility, attendees, attachments } = body;
console.log('[API] Creating event:', { calendarId, title, start, end }); console.log('[API] Creating event:', { calendarId, title, start, end });
@ -65,6 +65,8 @@ export async function POST(request: NextRequest) {
recurrence, recurrence,
recurrenceEndDate, recurrenceEndDate,
recurrenceCount, recurrenceCount,
recurrenceInterval,
recurrenceDays,
url, url,
reminders, reminders,
busyStatus, busyStatus,
@ -94,7 +96,7 @@ export async function PATCH(request: NextRequest) {
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const body = await request.json(); const body = await request.json();
const { calendarId, eventId, title, description, start, end, location, allDay, recurrence, recurrenceEndDate, recurrenceCount, url, const { calendarId, eventId, title, description, start, end, location, allDay, recurrence, recurrenceEndDate, recurrenceCount, recurrenceInterval, recurrenceDays, url,
reminders, busyStatus, visibility, attendees, attachments } = body; reminders, busyStatus, visibility, attendees, attachments } = body;
console.log('[API] Updating event:', { calendarId, eventId, title }); console.log('[API] Updating event:', { calendarId, eventId, title });
@ -120,6 +122,8 @@ export async function PATCH(request: NextRequest) {
recurrence, recurrence,
recurrenceEndDate, recurrenceEndDate,
recurrenceCount, recurrenceCount,
recurrenceInterval,
recurrenceDays,
url, url,
reminders, reminders,
busyStatus, busyStatus,

View File

@ -71,6 +71,7 @@ export async function POST(req: Request) {
id: cal.id, id: cal.id,
title: cal.title, title: cal.title,
isPrimary: cal.isPrimary, isPrimary: cal.isPrimary,
backgroundColor: cal.color || undefined,
selected: true selected: true
})) as any })) as any
} }

View File

@ -77,6 +77,11 @@ export default function CalendarEventModal({
event?.recurrenceEndDate || '' event?.recurrenceEndDate || ''
); );
const [recurrenceCount, setRecurrenceCount] = useState(event?.recurrenceCount || 10); const [recurrenceCount, setRecurrenceCount] = useState(event?.recurrenceCount || 10);
// Custom recurrence fields
const [customInterval, setCustomInterval] = useState(event?.recurrenceInterval || 1);
const [customUnit, setCustomUnit] = useState<'days' | 'weeks' | 'months' | 'years'>(event?.recurrenceUnit || 'weeks');
const startDow = (() => { const d = event?.start?.dateTime ? new Date(event.start.dateTime) : new Date(); return d.getDay(); })();
const [customDays, setCustomDays] = useState<number[]>(event?.recurrenceDays || [startDow]);
const [calendarId, setCalendarId] = useState(event?.calendarId || (availableCalendars.length > 0 ? availableCalendars[0].id : '')); const [calendarId, setCalendarId] = useState(event?.calendarId || (availableCalendars.length > 0 ? availableCalendars[0].id : ''));
// New fields // New fields
@ -179,7 +184,10 @@ export default function CalendarEventModal({
description, description,
location, location,
url: url || undefined, url: url || undefined,
recurrence: recurrence !== 'none' ? recurrence : undefined, recurrence: recurrence === 'custom' ? customUnit.replace(/s$/, '') === 'day' ? 'daily' : customUnit.replace(/s$/, '') === 'week' ? 'weekly' : customUnit.replace(/s$/, '') === 'month' ? 'monthly' : 'yearly'
: recurrence !== 'none' ? recurrence : undefined,
recurrenceInterval: recurrence === 'custom' ? customInterval : undefined,
recurrenceDays: recurrence === 'custom' && customUnit === 'weeks' ? customDays : undefined,
recurrenceEndDate: recurrence !== 'none' && recurrenceEndType === 'date' && recurrenceEndDateValue ? recurrenceEndDateValue : undefined, recurrenceEndDate: recurrence !== 'none' && recurrenceEndType === 'date' && recurrenceEndDateValue ? recurrenceEndDateValue : undefined,
recurrenceCount: recurrence !== 'none' && recurrenceEndType === 'count' && recurrenceCount > 0 ? recurrenceCount : undefined, recurrenceCount: recurrence !== 'none' && recurrenceEndType === 'count' && recurrenceCount > 0 ? recurrenceCount : undefined,
calendarId, calendarId,
@ -443,9 +451,62 @@ export default function CalendarEventModal({
<option value="weekly">Every Week</option> <option value="weekly">Every Week</option>
<option value="monthly">Every Month</option> <option value="monthly">Every Month</option>
<option value="yearly">Every Year</option> <option value="yearly">Every Year</option>
<option value="custom">Custom...</option>
</select> </select>
</div> </div>
{/* Custom recurrence options */}
{recurrence === 'custom' && (
<>
<div style={{ ...rowStyle, gap: '6px' }}>
<span style={labelStyle}>Every</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<input
type="number" min={1} max={99} value={customInterval}
onChange={e => setCustomInterval(Math.max(1, parseInt(e.target.value) || 1))}
style={{
...selectStyle, width: '45px', textAlign: 'center',
background: 'var(--weekly-bg-secondary, #f5f5f5)', borderRadius: '6px', padding: '4px 6px',
}}
/>
<select value={customUnit} onChange={e => setCustomUnit(e.target.value as any)} style={selectStyle}>
<option value="days">{customInterval === 1 ? 'Day' : 'Days'}</option>
<option value="weeks">{customInterval === 1 ? 'Week' : 'Weeks'}</option>
<option value="months">{customInterval === 1 ? 'Month' : 'Months'}</option>
<option value="years">{customInterval === 1 ? 'Year' : 'Years'}</option>
</select>
</div>
</div>
{customUnit === 'weeks' && (
<div style={{ ...rowStyle, flexDirection: 'column', alignItems: 'flex-start', gap: '6px' }}>
<span style={labelStyle}>Repeat on</span>
<div style={{ display: 'flex', gap: '4px', paddingLeft: '0' }}>
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, i) => (
<button
key={i}
onClick={() => {
setCustomDays(prev =>
prev.includes(i) ? (prev.length > 1 ? prev.filter(d => d !== i) : prev) : [...prev, i]
);
}}
style={{
width: '30px', height: '30px', borderRadius: '50%',
border: customDays.includes(i) ? '2px solid #3b82f6' : '1px solid var(--weekly-border, #ddd)',
backgroundColor: customDays.includes(i) ? '#3b82f6' : 'transparent',
color: customDays.includes(i) ? 'white' : 'var(--weekly-text)',
fontSize: '0.75rem', fontWeight: 600, cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
{day}
</button>
))}
</div>
</div>
)}
</>
)}
{/* Recurrence End - only shown when repeat is set */} {/* Recurrence End - only shown when repeat is set */}
{recurrence !== 'none' && ( {recurrence !== 'none' && (
<> <>

View File

@ -12405,7 +12405,7 @@ function SettingsSidebar({
padding: "3px 0", padding: "3px 0",
}} }}
> >
{/* Calendar Name */} {/* Calendar Color + Name */}
<span <span
style={{ style={{
flex: 1, flex: 1,
@ -12414,8 +12414,15 @@ function SettingsSidebar({
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
whiteSpace: "nowrap", whiteSpace: "nowrap",
display: "flex",
alignItems: "center",
gap: "6px",
}} }}
> >
<span style={{
width: "10px", height: "10px", borderRadius: "50%", flexShrink: 0,
backgroundColor: cal.backgroundColor || cal.color || "#3b82f6",
}} />
{cleanTitle} {cleanTitle}
{isShared && ( {isShared && (
<span title="Shared calendar" style={{ marginLeft: "4px", fontSize: "0.75rem", opacity: 0.5 }}> <span title="Shared calendar" style={{ marginLeft: "4px", fontSize: "0.75rem", opacity: 0.5 }}>

View File

@ -392,6 +392,8 @@ export const createEvent = async (
recurrence?: string; recurrence?: string;
recurrenceEndDate?: string; recurrenceEndDate?: string;
recurrenceCount?: number; recurrenceCount?: number;
recurrenceInterval?: number;
recurrenceDays?: number[];
start: { dateTime?: string; date?: string }; start: { dateTime?: string; date?: string };
end: { dateTime?: string; date?: string }; end: { dateTime?: string; date?: string };
reminders?: Array<{ method: string; minutes: number }>; reminders?: Array<{ method: string; minutes: number }>;
@ -458,6 +460,13 @@ export const createEvent = async (
}; };
if (rruleMap[eventData.recurrence]) { if (rruleMap[eventData.recurrence]) {
let rrule = rruleMap[eventData.recurrence]; let rrule = rruleMap[eventData.recurrence];
if (eventData.recurrenceInterval && eventData.recurrenceInterval > 1 && eventData.recurrence !== 'biweekly') {
rrule += `;INTERVAL=${eventData.recurrenceInterval}`;
}
if (eventData.recurrenceDays && eventData.recurrenceDays.length > 0 && eventData.recurrence === 'weekly') {
const dayMap = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
rrule += `;BYDAY=${eventData.recurrenceDays.map(d => dayMap[d]).join(',')}`;
}
if (eventData.recurrenceCount && eventData.recurrenceCount > 0) { if (eventData.recurrenceCount && eventData.recurrenceCount > 0) {
rrule += `;COUNT=${eventData.recurrenceCount}`; rrule += `;COUNT=${eventData.recurrenceCount}`;
} else if (eventData.recurrenceEndDate) { } else if (eventData.recurrenceEndDate) {

View File

@ -43,6 +43,8 @@ export interface CalendarEvent {
recurrence?: string; recurrence?: string;
recurrenceEndDate?: string; recurrenceEndDate?: string;
recurrenceCount?: number; recurrenceCount?: number;
recurrenceInterval?: number;
recurrenceDays?: number[];
recurringEventId?: string; recurringEventId?: string;
isRecurring?: boolean; isRecurring?: boolean;
source: 'google' | 'apple' | 'outlook' | 'synology' | 'notion'; source: 'google' | 'apple' | 'outlook' | 'synology' | 'notion';
@ -60,7 +62,7 @@ export interface CalendarEvent {
/** /**
* Convert friendly recurrence name to RRULE string * Convert friendly recurrence name to RRULE string
*/ */
function toRRule(recurrence?: string, recurrenceEndDate?: string, recurrenceCount?: number): string | null { function toRRule(recurrence?: string, recurrenceEndDate?: string, recurrenceCount?: number, recurrenceInterval?: number, recurrenceDays?: number[]): string | null {
let freq: string; let freq: string;
switch (recurrence) { switch (recurrence) {
case 'daily': freq = 'FREQ=DAILY'; break; case 'daily': freq = 'FREQ=DAILY'; break;
@ -71,10 +73,16 @@ function toRRule(recurrence?: string, recurrenceEndDate?: string, recurrenceCoun
default: return null; default: return null;
} }
let rrule = `RRULE:${freq}`; let rrule = `RRULE:${freq}`;
if (recurrenceInterval && recurrenceInterval > 1 && recurrence !== 'biweekly') {
rrule += `;INTERVAL=${recurrenceInterval}`;
}
if (recurrenceDays && recurrenceDays.length > 0 && recurrence === 'weekly') {
const dayMap = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
rrule += `;BYDAY=${recurrenceDays.map(d => dayMap[d]).join(',')}`;
}
if (recurrenceCount && recurrenceCount > 0) { if (recurrenceCount && recurrenceCount > 0) {
rrule += `;COUNT=${recurrenceCount}`; rrule += `;COUNT=${recurrenceCount}`;
} else if (recurrenceEndDate) { } else if (recurrenceEndDate) {
// UNTIL format: YYYYMMDDTHHMMSSZ
const d = new Date(recurrenceEndDate); const d = new Date(recurrenceEndDate);
d.setHours(23, 59, 59); d.setHours(23, 59, 59);
rrule += `;UNTIL=${d.toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z')}`; rrule += `;UNTIL=${d.toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z')}`;
@ -85,9 +93,11 @@ function toRRule(recurrence?: string, recurrenceEndDate?: string, recurrenceCoun
/** /**
* Convert friendly recurrence name to Outlook Graph recurrence object * Convert friendly recurrence name to Outlook Graph recurrence object
*/ */
function toOutlookRecurrence(recurrence?: string, startDate?: Date, recurrenceEndDate?: string, recurrenceCount?: number): any { function toOutlookRecurrence(recurrence?: string, startDate?: Date, recurrenceEndDate?: string, recurrenceCount?: number, recurrenceInterval?: number, recurrenceDays?: number[]): any {
if (!recurrence || recurrence === 'none') return undefined; if (!recurrence || recurrence === 'none') return undefined;
const start = startDate || new Date(); const start = startDate || new Date();
const interval = recurrenceInterval || (recurrence === 'biweekly' ? 2 : 1);
const dayNames = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
let range: any = { let range: any = {
type: 'noEnd', type: 'noEnd',
startDate: start.toISOString().split('T')[0], startDate: start.toISOString().split('T')[0],
@ -99,15 +109,18 @@ function toOutlookRecurrence(recurrence?: string, startDate?: Date, recurrenceEn
} }
switch (recurrence) { switch (recurrence) {
case 'daily': case 'daily':
return { pattern: { type: 'daily', interval: 1 }, range }; return { pattern: { type: 'daily', interval }, range };
case 'weekly': case 'weekly':
return { pattern: { type: 'weekly', interval: 1, daysOfWeek: [['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'][start.getDay()]] }, range }; case 'biweekly': {
case 'biweekly': const days = recurrenceDays && recurrenceDays.length > 0
return { pattern: { type: 'weekly', interval: 2, daysOfWeek: [['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'][start.getDay()]] }, range }; ? recurrenceDays.map(d => dayNames[d])
: [dayNames[start.getDay()]];
return { pattern: { type: 'weekly', interval, daysOfWeek: days }, range };
}
case 'monthly': case 'monthly':
return { pattern: { type: 'absoluteMonthly', interval: 1, dayOfMonth: start.getDate() }, range }; return { pattern: { type: 'absoluteMonthly', interval, dayOfMonth: start.getDate() }, range };
case 'yearly': case 'yearly':
return { pattern: { type: 'absoluteYearly', interval: 1, dayOfMonth: start.getDate(), month: start.getMonth() + 1 }, range }; return { pattern: { type: 'absoluteYearly', interval, dayOfMonth: start.getDate(), month: start.getMonth() + 1 }, range };
default: return undefined; default: return undefined;
} }
} }
@ -516,6 +529,7 @@ export const getCalendarEvents = async (
console.log(`[CALENDAR] Synology: fetched ${calendarEvents.length} events from calendar ${calendarId}`); console.log(`[CALENDAR] Synology: fetched ${calendarEvents.length} events from calendar ${calendarId}`);
const calendarData = calendars.find(c => c.id === calendarId); const calendarData = calendars.find(c => c.id === calendarId);
const freshCal = freshCalendars.find(c => c.id === calendarId);
events = events.concat(calendarEvents.map((event: any) => { events = events.concat(calendarEvents.map((event: any) => {
const isDateOnly = (s: string) => s && !s.includes('T'); const isDateOnly = (s: string) => s && !s.includes('T');
@ -539,7 +553,7 @@ export const getCalendarEvents = async (
source: 'synology' as const, source: 'synology' as const,
calendarId, calendarId,
calendarTitle: calendarData?.title || 'Synology Calendar', calendarTitle: calendarData?.title || 'Synology Calendar',
backgroundColor: calendarData?.color || '#1b85ff', backgroundColor: calendarData?.backgroundColor || calendarData?.color || freshCal?.color || '#1b85ff',
reminders: event.reminders as EventReminder[] || undefined, reminders: event.reminders as EventReminder[] || undefined,
busyStatus: event.busyStatus as BusyStatus || undefined, busyStatus: event.busyStatus as BusyStatus || undefined,
visibility: event.visibility as EventVisibility || undefined, visibility: event.visibility as EventVisibility || undefined,
@ -825,7 +839,7 @@ export const createCalendarEvent = async (
); );
// Map to Google format // Map to Google format
const rrule = toRRule(event.recurrence, event.recurrenceEndDate, event.recurrenceCount); const rrule = toRRule(event.recurrence, event.recurrenceEndDate, event.recurrenceCount, event.recurrenceInterval, event.recurrenceDays);
const googleEvent: any = { const googleEvent: any = {
summary: event.title, summary: event.title,
description: event.description, description: event.description,
@ -877,7 +891,7 @@ export const createCalendarEvent = async (
end: event.end, end: event.end,
location: event.location, location: event.location,
allDay: event.allDay, allDay: event.allDay,
recurrence: toOutlookRecurrence(event.recurrence, startDate, event.recurrenceEndDate, event.recurrenceCount), recurrence: toOutlookRecurrence(event.recurrence, startDate, event.recurrenceEndDate, event.recurrenceCount, event.recurrenceInterval, event.recurrenceDays),
reminders: event.reminders, reminders: event.reminders,
busyStatus: event.busyStatus, busyStatus: event.busyStatus,
visibility: event.visibility, visibility: event.visibility,
@ -936,6 +950,8 @@ export const createCalendarEvent = async (
recurrence: event.recurrence, recurrence: event.recurrence,
recurrenceEndDate: event.recurrenceEndDate, recurrenceEndDate: event.recurrenceEndDate,
recurrenceCount: event.recurrenceCount, recurrenceCount: event.recurrenceCount,
recurrenceInterval: event.recurrenceInterval,
recurrenceDays: event.recurrenceDays,
start, start,
end, end,
reminders: event.reminders, reminders: event.reminders,
@ -974,6 +990,8 @@ export const createCalendarEvent = async (
recurrence: event.recurrence, recurrence: event.recurrence,
recurrenceEndDate: event.recurrenceEndDate, recurrenceEndDate: event.recurrenceEndDate,
recurrenceCount: event.recurrenceCount, recurrenceCount: event.recurrenceCount,
recurrenceInterval: event.recurrenceInterval,
recurrenceDays: event.recurrenceDays,
start: event.start!, start: event.start!,
end: event.end!, end: event.end!,
reminders: event.reminders, reminders: event.reminders,
@ -1056,7 +1074,7 @@ export const updateCalendarEvent = async (
); );
// Map to Google format // Map to Google format
const rrule = toRRule(event.recurrence, event.recurrenceEndDate, event.recurrenceCount); const rrule = toRRule(event.recurrence, event.recurrenceEndDate, event.recurrenceCount, event.recurrenceInterval, event.recurrenceDays);
const googleEvent: any = {}; const googleEvent: any = {};
if (event.title !== undefined) googleEvent.summary = event.title; if (event.title !== undefined) googleEvent.summary = event.title;
if (event.description !== undefined) googleEvent.description = event.description; if (event.description !== undefined) googleEvent.description = event.description;
@ -1109,7 +1127,7 @@ export const updateCalendarEvent = async (
end: event.end, end: event.end,
location: event.location, location: event.location,
allDay: event.allDay, allDay: event.allDay,
recurrence: toOutlookRecurrence(event.recurrence, startDate, event.recurrenceEndDate, event.recurrenceCount), recurrence: toOutlookRecurrence(event.recurrence, startDate, event.recurrenceEndDate, event.recurrenceCount, event.recurrenceInterval, event.recurrenceDays),
reminders: event.reminders, reminders: event.reminders,
busyStatus: event.busyStatus, busyStatus: event.busyStatus,
visibility: event.visibility, visibility: event.visibility,

View File

@ -402,6 +402,8 @@ export const createEvent = async (
recurrence?: string; recurrence?: string;
recurrenceEndDate?: string; recurrenceEndDate?: string;
recurrenceCount?: number; recurrenceCount?: number;
recurrenceInterval?: number;
recurrenceDays?: number[];
start: { dateTime?: string; date?: string }; start: { dateTime?: string; date?: string };
end: { dateTime?: string; date?: string }; end: { dateTime?: string; date?: string };
reminders?: Array<{ method: string; minutes: number }>; reminders?: Array<{ method: string; minutes: number }>;
@ -458,6 +460,13 @@ export const createEvent = async (
}; };
if (rruleMap[eventData.recurrence]) { if (rruleMap[eventData.recurrence]) {
let rrule = rruleMap[eventData.recurrence]; let rrule = rruleMap[eventData.recurrence];
if (eventData.recurrenceInterval && eventData.recurrenceInterval > 1 && eventData.recurrence !== 'biweekly') {
rrule += `;INTERVAL=${eventData.recurrenceInterval}`;
}
if (eventData.recurrenceDays && eventData.recurrenceDays.length > 0 && eventData.recurrence === 'weekly') {
const dayMap = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
rrule += `;BYDAY=${eventData.recurrenceDays.map(d => dayMap[d]).join(',')}`;
}
if (eventData.recurrenceCount && eventData.recurrenceCount > 0) { if (eventData.recurrenceCount && eventData.recurrenceCount > 0) {
rrule += `;COUNT=${eventData.recurrenceCount}`; rrule += `;COUNT=${eventData.recurrenceCount}`;
} else if (eventData.recurrenceEndDate) { } else if (eventData.recurrenceEndDate) {