feat: add Outlook calendar colors and recurrence end date/count
- Fetch and display Outlook calendar colors from Microsoft Graph API instead of hardcoded blue - Add recurrence end options to calendar event modal: Never, On Date (UNTIL), or After X occurrences (COUNT) - Pass recurrence end parameters through to RRULE generation for all providers (Google, Apple, Synology, Outlook) v1.60.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6fc0ccef74
commit
1ab98025a6
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.59.4",
|
"version": "1.60.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": {
|
||||||
|
|||||||
@ -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, url,
|
const { calendarId, title, description, start, end, location, allDay, recurrence, recurrenceEndDate, recurrenceCount, 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 });
|
||||||
@ -63,6 +63,8 @@ export async function POST(request: NextRequest) {
|
|||||||
location,
|
location,
|
||||||
allDay: !!allDay,
|
allDay: !!allDay,
|
||||||
recurrence,
|
recurrence,
|
||||||
|
recurrenceEndDate,
|
||||||
|
recurrenceCount,
|
||||||
url,
|
url,
|
||||||
reminders,
|
reminders,
|
||||||
busyStatus,
|
busyStatus,
|
||||||
@ -92,7 +94,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, url,
|
const { calendarId, eventId, title, description, start, end, location, allDay, recurrence, recurrenceEndDate, recurrenceCount, 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 });
|
||||||
@ -116,6 +118,8 @@ export async function PATCH(request: NextRequest) {
|
|||||||
location,
|
location,
|
||||||
allDay: allDay !== undefined ? !!allDay : undefined,
|
allDay: allDay !== undefined ? !!allDay : undefined,
|
||||||
recurrence,
|
recurrence,
|
||||||
|
recurrenceEndDate,
|
||||||
|
recurrenceCount,
|
||||||
url,
|
url,
|
||||||
reminders,
|
reminders,
|
||||||
busyStatus,
|
busyStatus,
|
||||||
|
|||||||
@ -64,7 +64,8 @@ export async function GET(request: NextRequest) {
|
|||||||
title: cal.name,
|
title: cal.name,
|
||||||
isPrimary: cal.isDefaultCalendar,
|
isPrimary: cal.isDefaultCalendar,
|
||||||
selected: true,
|
selected: true,
|
||||||
editable: cal.canEdit
|
editable: cal.canEdit,
|
||||||
|
backgroundColor: cal.hexColor || '#0078d4',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (existingConnection) {
|
if (existingConnection) {
|
||||||
|
|||||||
@ -70,6 +70,13 @@ export default function CalendarEventModal({
|
|||||||
const [location, setLocation] = useState(event?.location || '');
|
const [location, setLocation] = useState(event?.location || '');
|
||||||
const [url, setUrl] = useState(event?.url || '');
|
const [url, setUrl] = useState(event?.url || '');
|
||||||
const [recurrence, setRecurrence] = useState(event?.recurrence || 'none');
|
const [recurrence, setRecurrence] = useState(event?.recurrence || 'none');
|
||||||
|
const [recurrenceEndType, setRecurrenceEndType] = useState<'never' | 'date' | 'count'>(
|
||||||
|
event?.recurrenceCount ? 'count' : event?.recurrenceEndDate ? 'date' : 'never'
|
||||||
|
);
|
||||||
|
const [recurrenceEndDateValue, setRecurrenceEndDateValue] = useState(
|
||||||
|
event?.recurrenceEndDate || ''
|
||||||
|
);
|
||||||
|
const [recurrenceCount, setRecurrenceCount] = useState(event?.recurrenceCount || 10);
|
||||||
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
|
||||||
@ -173,6 +180,8 @@ export default function CalendarEventModal({
|
|||||||
location,
|
location,
|
||||||
url: url || undefined,
|
url: url || undefined,
|
||||||
recurrence: recurrence !== 'none' ? recurrence : undefined,
|
recurrence: recurrence !== 'none' ? recurrence : undefined,
|
||||||
|
recurrenceEndDate: recurrence !== 'none' && recurrenceEndType === 'date' && recurrenceEndDateValue ? recurrenceEndDateValue : undefined,
|
||||||
|
recurrenceCount: recurrence !== 'none' && recurrenceEndType === 'count' && recurrenceCount > 0 ? recurrenceCount : undefined,
|
||||||
calendarId,
|
calendarId,
|
||||||
allDay,
|
allDay,
|
||||||
start: { dateTime: startDate.toISOString() },
|
start: { dateTime: startDate.toISOString() },
|
||||||
@ -437,6 +446,63 @@ export default function CalendarEventModal({
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Recurrence End - only shown when repeat is set */}
|
||||||
|
{recurrence !== 'none' && (
|
||||||
|
<>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span style={labelStyle}>End</span>
|
||||||
|
<select
|
||||||
|
value={recurrenceEndType}
|
||||||
|
onChange={e => setRecurrenceEndType(e.target.value as 'never' | 'date' | 'count')}
|
||||||
|
style={selectStyle}
|
||||||
|
>
|
||||||
|
<option value="never">Never</option>
|
||||||
|
<option value="date">On Date</option>
|
||||||
|
<option value="count">After...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{recurrenceEndType === 'date' && (
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span style={labelStyle}></span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={recurrenceEndDateValue}
|
||||||
|
onChange={e => setRecurrenceEndDateValue(e.target.value)}
|
||||||
|
style={{
|
||||||
|
...selectStyle,
|
||||||
|
background: 'var(--weekly-bg-secondary, #f5f5f5)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '4px 8px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{recurrenceEndType === 'count' && (
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<span style={labelStyle}></span>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={999}
|
||||||
|
value={recurrenceCount}
|
||||||
|
onChange={e => setRecurrenceCount(Math.max(1, parseInt(e.target.value) || 1))}
|
||||||
|
style={{
|
||||||
|
...selectStyle,
|
||||||
|
width: '50px',
|
||||||
|
background: 'var(--weekly-bg-secondary, #f5f5f5)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '4px 8px',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: '0.82rem', color: 'var(--weekly-text)' }}>times</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Alert */}
|
{/* Alert */}
|
||||||
<div style={{ padding: '0 10px' }}>
|
<div style={{ padding: '0 10px' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', minHeight: '32px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', minHeight: '32px' }}>
|
||||||
|
|||||||
@ -390,6 +390,8 @@ export const createEvent = async (
|
|||||||
location?: string;
|
location?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
recurrence?: string;
|
recurrence?: string;
|
||||||
|
recurrenceEndDate?: string;
|
||||||
|
recurrenceCount?: 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 }>;
|
||||||
@ -455,7 +457,15 @@ export const createEvent = async (
|
|||||||
yearly: 'RRULE:FREQ=YEARLY',
|
yearly: 'RRULE:FREQ=YEARLY',
|
||||||
};
|
};
|
||||||
if (rruleMap[eventData.recurrence]) {
|
if (rruleMap[eventData.recurrence]) {
|
||||||
rruleLine = `${rruleMap[eventData.recurrence]}\r\n`;
|
let rrule = rruleMap[eventData.recurrence];
|
||||||
|
if (eventData.recurrenceCount && eventData.recurrenceCount > 0) {
|
||||||
|
rrule += `;COUNT=${eventData.recurrenceCount}`;
|
||||||
|
} else if (eventData.recurrenceEndDate) {
|
||||||
|
const d = new Date(eventData.recurrenceEndDate);
|
||||||
|
d.setHours(23, 59, 59);
|
||||||
|
rrule += `;UNTIL=${d.toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z')}`;
|
||||||
|
}
|
||||||
|
rruleLine = `${rrule}\r\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,8 @@ export interface CalendarEvent {
|
|||||||
location?: string;
|
location?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
recurrence?: string;
|
recurrence?: string;
|
||||||
|
recurrenceEndDate?: string;
|
||||||
|
recurrenceCount?: number;
|
||||||
recurringEventId?: string;
|
recurringEventId?: string;
|
||||||
isRecurring?: boolean;
|
isRecurring?: boolean;
|
||||||
source: 'google' | 'apple' | 'outlook' | 'synology' | 'notion';
|
source: 'google' | 'apple' | 'outlook' | 'synology' | 'notion';
|
||||||
@ -58,27 +60,43 @@ export interface CalendarEvent {
|
|||||||
/**
|
/**
|
||||||
* Convert friendly recurrence name to RRULE string
|
* Convert friendly recurrence name to RRULE string
|
||||||
*/
|
*/
|
||||||
function toRRule(recurrence?: string): string | null {
|
function toRRule(recurrence?: string, recurrenceEndDate?: string, recurrenceCount?: number): string | null {
|
||||||
|
let freq: string;
|
||||||
switch (recurrence) {
|
switch (recurrence) {
|
||||||
case 'daily': return 'RRULE:FREQ=DAILY';
|
case 'daily': freq = 'FREQ=DAILY'; break;
|
||||||
case 'weekly': return 'RRULE:FREQ=WEEKLY';
|
case 'weekly': freq = 'FREQ=WEEKLY'; break;
|
||||||
case 'biweekly': return 'RRULE:FREQ=WEEKLY;INTERVAL=2';
|
case 'biweekly': freq = 'FREQ=WEEKLY;INTERVAL=2'; break;
|
||||||
case 'monthly': return 'RRULE:FREQ=MONTHLY';
|
case 'monthly': freq = 'FREQ=MONTHLY'; break;
|
||||||
case 'yearly': return 'RRULE:FREQ=YEARLY';
|
case 'yearly': freq = 'FREQ=YEARLY'; break;
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
|
let rrule = `RRULE:${freq}`;
|
||||||
|
if (recurrenceCount && recurrenceCount > 0) {
|
||||||
|
rrule += `;COUNT=${recurrenceCount}`;
|
||||||
|
} else if (recurrenceEndDate) {
|
||||||
|
// UNTIL format: YYYYMMDDTHHMMSSZ
|
||||||
|
const d = new Date(recurrenceEndDate);
|
||||||
|
d.setHours(23, 59, 59);
|
||||||
|
rrule += `;UNTIL=${d.toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z')}`;
|
||||||
|
}
|
||||||
|
return rrule;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert friendly recurrence name to Outlook Graph recurrence object
|
* Convert friendly recurrence name to Outlook Graph recurrence object
|
||||||
*/
|
*/
|
||||||
function toOutlookRecurrence(recurrence?: string, startDate?: Date): any {
|
function toOutlookRecurrence(recurrence?: string, startDate?: Date, recurrenceEndDate?: string, recurrenceCount?: 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 range = {
|
let range: any = {
|
||||||
type: 'noEnd',
|
type: 'noEnd',
|
||||||
startDate: start.toISOString().split('T')[0],
|
startDate: start.toISOString().split('T')[0],
|
||||||
};
|
};
|
||||||
|
if (recurrenceCount && recurrenceCount > 0) {
|
||||||
|
range = { type: 'numbered', startDate: start.toISOString().split('T')[0], numberOfOccurrences: recurrenceCount };
|
||||||
|
} else if (recurrenceEndDate) {
|
||||||
|
range = { type: 'endDate', startDate: start.toISOString().split('T')[0], endDate: recurrenceEndDate };
|
||||||
|
}
|
||||||
switch (recurrence) {
|
switch (recurrence) {
|
||||||
case 'daily':
|
case 'daily':
|
||||||
return { pattern: { type: 'daily', interval: 1 }, range };
|
return { pattern: { type: 'daily', interval: 1 }, range };
|
||||||
@ -595,7 +613,7 @@ export const getCalendarEvents = async (
|
|||||||
source: 'outlook' as const,
|
source: 'outlook' as const,
|
||||||
calendarId,
|
calendarId,
|
||||||
calendarTitle: calendarData?.title || 'Outlook Calendar',
|
calendarTitle: calendarData?.title || 'Outlook Calendar',
|
||||||
backgroundColor: '#0078d4',
|
backgroundColor: calendarData?.backgroundColor || calendarData?.color || '#0078d4',
|
||||||
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,
|
||||||
@ -807,7 +825,7 @@ export const createCalendarEvent = async (
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Map to Google format
|
// Map to Google format
|
||||||
const rrule = toRRule(event.recurrence);
|
const rrule = toRRule(event.recurrence, event.recurrenceEndDate, event.recurrenceCount);
|
||||||
const googleEvent: any = {
|
const googleEvent: any = {
|
||||||
summary: event.title,
|
summary: event.title,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
@ -859,7 +877,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),
|
recurrence: toOutlookRecurrence(event.recurrence, startDate, event.recurrenceEndDate, event.recurrenceCount),
|
||||||
reminders: event.reminders,
|
reminders: event.reminders,
|
||||||
busyStatus: event.busyStatus,
|
busyStatus: event.busyStatus,
|
||||||
visibility: event.visibility,
|
visibility: event.visibility,
|
||||||
@ -916,6 +934,8 @@ export const createCalendarEvent = async (
|
|||||||
location: event.location,
|
location: event.location,
|
||||||
url: event.url,
|
url: event.url,
|
||||||
recurrence: event.recurrence,
|
recurrence: event.recurrence,
|
||||||
|
recurrenceEndDate: event.recurrenceEndDate,
|
||||||
|
recurrenceCount: event.recurrenceCount,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
reminders: event.reminders,
|
reminders: event.reminders,
|
||||||
@ -952,6 +972,8 @@ export const createCalendarEvent = async (
|
|||||||
location: event.location,
|
location: event.location,
|
||||||
url: event.url,
|
url: event.url,
|
||||||
recurrence: event.recurrence,
|
recurrence: event.recurrence,
|
||||||
|
recurrenceEndDate: event.recurrenceEndDate,
|
||||||
|
recurrenceCount: event.recurrenceCount,
|
||||||
start: event.start!,
|
start: event.start!,
|
||||||
end: event.end!,
|
end: event.end!,
|
||||||
reminders: event.reminders,
|
reminders: event.reminders,
|
||||||
@ -1034,7 +1056,7 @@ export const updateCalendarEvent = async (
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Map to Google format
|
// Map to Google format
|
||||||
const rrule = toRRule(event.recurrence);
|
const rrule = toRRule(event.recurrence, event.recurrenceEndDate, event.recurrenceCount);
|
||||||
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;
|
||||||
@ -1087,7 +1109,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),
|
recurrence: toOutlookRecurrence(event.recurrence, startDate, event.recurrenceEndDate, event.recurrenceCount),
|
||||||
reminders: event.reminders,
|
reminders: event.reminders,
|
||||||
busyStatus: event.busyStatus,
|
busyStatus: event.busyStatus,
|
||||||
visibility: event.visibility,
|
visibility: event.visibility,
|
||||||
|
|||||||
@ -7,12 +7,29 @@ export interface OutlookCalendar {
|
|||||||
name: string;
|
name: string;
|
||||||
isDefaultCalendar: boolean;
|
isDefaultCalendar: boolean;
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
|
color?: string;
|
||||||
|
hexColor?: string;
|
||||||
owner: {
|
owner: {
|
||||||
name: string;
|
name: string;
|
||||||
address: string;
|
address: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Microsoft Graph API calendar color names → hex values
|
||||||
|
const OUTLOOK_COLOR_MAP: Record<string, string> = {
|
||||||
|
auto: '#0078d4',
|
||||||
|
lightBlue: '#69afe5',
|
||||||
|
lightGreen: '#7bd148',
|
||||||
|
lightOrange: '#ffb878',
|
||||||
|
lightGray: '#b3b3b3',
|
||||||
|
lightYellow: '#fbd75b',
|
||||||
|
lightTeal: '#92e1c0',
|
||||||
|
lightPink: '#f691b2',
|
||||||
|
lightBrown: '#c2a282',
|
||||||
|
lightRed: '#ff887c',
|
||||||
|
maxColor: '#0078d4',
|
||||||
|
};
|
||||||
|
|
||||||
const GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0';
|
const GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0';
|
||||||
const REDIRECT_URI = process.env.MICROSOFT_REDIRECT_URI || `${process.env.NEXTAUTH_URL}/api/calendar/outlook/callback`;
|
const REDIRECT_URI = process.env.MICROSOFT_REDIRECT_URI || `${process.env.NEXTAUTH_URL}/api/calendar/outlook/callback`;
|
||||||
|
|
||||||
@ -132,7 +149,10 @@ export const getUserCalendars = async (accessToken: string): Promise<OutlookCale
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.value;
|
return (data.value || []).map((cal: any) => ({
|
||||||
|
...cal,
|
||||||
|
hexColor: cal.hexColor || OUTLOOK_COLOR_MAP[cal.color] || OUTLOOK_COLOR_MAP['auto'],
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -400,6 +400,8 @@ export const createEvent = async (
|
|||||||
location?: string;
|
location?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
recurrence?: string;
|
recurrence?: string;
|
||||||
|
recurrenceEndDate?: string;
|
||||||
|
recurrenceCount?: 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 }>;
|
||||||
@ -455,7 +457,15 @@ export const createEvent = async (
|
|||||||
yearly: 'RRULE:FREQ=YEARLY',
|
yearly: 'RRULE:FREQ=YEARLY',
|
||||||
};
|
};
|
||||||
if (rruleMap[eventData.recurrence]) {
|
if (rruleMap[eventData.recurrence]) {
|
||||||
rruleLine = `${rruleMap[eventData.recurrence]}\r\n`;
|
let rrule = rruleMap[eventData.recurrence];
|
||||||
|
if (eventData.recurrenceCount && eventData.recurrenceCount > 0) {
|
||||||
|
rrule += `;COUNT=${eventData.recurrenceCount}`;
|
||||||
|
} else if (eventData.recurrenceEndDate) {
|
||||||
|
const d = new Date(eventData.recurrenceEndDate);
|
||||||
|
d.setHours(23, 59, 59);
|
||||||
|
rrule += `;UNTIL=${d.toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z')}`;
|
||||||
|
}
|
||||||
|
rruleLine = `${rrule}\r\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user