Fix task creation and other updates
This commit is contained in:
parent
46dbf2ac02
commit
192c418223
@ -8,14 +8,14 @@ SMTP_USERNAME=mail@carrylight.de
|
|||||||
SMTP_PASSWORD=QijU8e2A8p3FE8WS8esR
|
SMTP_PASSWORD=QijU8e2A8p3FE8WS8esR
|
||||||
SMTP_SECURE=true
|
SMTP_SECURE=true
|
||||||
|
|
||||||
NEXTAUTH_URL=https://todo.martin-bierschenk.de
|
# NEXTAUTH_URL=https://todo.martin-bierschenk.de
|
||||||
# NEXTAUTH_URL=http://localhost:3000
|
NEXTAUTH_URL=http://localhost:3000
|
||||||
|
|
||||||
# Google OAuth (Replace with your credentials)
|
# Google OAuth (Replace with your credentials)
|
||||||
GOOGLE_CLIENT_ID=196368743757-1fn17q2ecg5n8rej4tu96khltno173he.apps.googleusercontent.com
|
GOOGLE_CLIENT_ID=196368743757-1fn17q2ecg5n8rej4tu96khltno173he.apps.googleusercontent.com
|
||||||
GOOGLE_CLIENT_SECRET=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
|
GOOGLE_CLIENT_SECRET=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
|
||||||
GOOGLE_REDIRECT_URI=https://todo.martin-bierschenk.de/api/calendar/google/oauth
|
# GOOGLE_REDIRECT_URI=https://todo.martin-bierschenk.de/api/calendar/google/oauth
|
||||||
# GOOGLE_REDIRECT_URI=http://localhost:3000/api/calendar/google/oauth
|
GOOGLE_REDIRECT_URI=http://localhost:3000/api/calendar/google/oauth
|
||||||
|
|
||||||
|
|
||||||
# Apple Sign In (Replace with your credentials when ready)
|
# Apple Sign In (Replace with your credentials when ready)
|
||||||
|
|||||||
@ -1053,7 +1053,10 @@ export default function WeeklyView() {
|
|||||||
return tasks
|
return tasks
|
||||||
.filter(task => {
|
.filter(task => {
|
||||||
if (!task.scheduledDate) return false;
|
if (!task.scheduledDate) return false;
|
||||||
const taskDateStr = formatDateToISO(new Date(task.scheduledDate));
|
// Use string comparison to avoid timezone shifts
|
||||||
|
const taskDateStr = typeof task.scheduledDate === 'string'
|
||||||
|
? task.scheduledDate.substring(0, 10)
|
||||||
|
: formatDateToISO(new Date(task.scheduledDate));
|
||||||
return taskDateStr === dateStr;
|
return taskDateStr === dateStr;
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
@ -1072,7 +1075,10 @@ export default function WeeklyView() {
|
|||||||
const dateStr = formatDateToISO(date);
|
const dateStr = formatDateToISO(date);
|
||||||
return tasks.filter(task => {
|
return tasks.filter(task => {
|
||||||
if (!task.scheduledDate) return false;
|
if (!task.scheduledDate) return false;
|
||||||
const taskDateStr = formatDateToISO(new Date(task.scheduledDate));
|
// Use string comparison to avoid timezone shifts
|
||||||
|
const taskDateStr = typeof task.scheduledDate === 'string'
|
||||||
|
? task.scheduledDate.substring(0, 10)
|
||||||
|
: formatDateToISO(new Date(task.scheduledDate));
|
||||||
return taskDateStr === dateStr && task.startTime === slot;
|
return taskDateStr === dateStr && task.startTime === slot;
|
||||||
});
|
});
|
||||||
}, [tasks]);
|
}, [tasks]);
|
||||||
@ -2529,13 +2535,18 @@ export default function WeeklyView() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={newSlotTask}
|
value={newSlotTask}
|
||||||
onChange={(e) => setNewSlotTask(e.target.value)}
|
onChange={(e) => setNewSlotTask(e.target.value)}
|
||||||
onBlur={async () => {
|
onBlur={async (e) => {
|
||||||
// Only save if still active (not already submitted)
|
// Prevent double submission if form was submitted
|
||||||
if (activeSlot && newSlotTask.trim()) {
|
if (activeSlot && newSlotTask.trim()) {
|
||||||
const taskTitle = newSlotTask.trim();
|
// Delay slightly to let onSubmit fire if that was the cause
|
||||||
setActiveSlot(null);
|
setTimeout(async () => {
|
||||||
setNewSlotTask('');
|
if (activeSlot && newSlotTask.trim()) {
|
||||||
await addTask(date, taskTitle, slot);
|
const taskTitle = newSlotTask.trim();
|
||||||
|
setActiveSlot(null);
|
||||||
|
setNewSlotTask('');
|
||||||
|
await addTask(date, taskTitle, slot);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
setActiveSlot(null);
|
setActiveSlot(null);
|
||||||
setNewSlotTask('');
|
setNewSlotTask('');
|
||||||
@ -2548,7 +2559,15 @@ export default function WeeklyView() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
autoFocus
|
autoFocus
|
||||||
className="slot-input"
|
className="weekly-task-input"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
border: 'none',
|
||||||
|
background: 'transparent',
|
||||||
|
outline: 'none',
|
||||||
|
minHeight: '24px'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -221,3 +221,371 @@ export const getUpcomingEvents = async (
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event in the specified calendar
|
||||||
|
*/
|
||||||
|
export const createEvent = async (
|
||||||
|
email: string,
|
||||||
|
appSpecificPassword: string,
|
||||||
|
calendarUrl: string,
|
||||||
|
eventData: {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
location?: string;
|
||||||
|
start: { dateTime?: string; date?: string };
|
||||||
|
end: { dateTime?: string; date?: string };
|
||||||
|
}
|
||||||
|
): Promise<AppleCalendarEvent> => {
|
||||||
|
try {
|
||||||
|
const client = createClient(email, appSpecificPassword);
|
||||||
|
await client.login();
|
||||||
|
|
||||||
|
const calendars = await client.fetchCalendars();
|
||||||
|
const targetCalendar = calendars.find(c => c.url === calendarUrl);
|
||||||
|
|
||||||
|
if (!targetCalendar) {
|
||||||
|
throw new Error(`Calendar not found: ${calendarUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate iCal string
|
||||||
|
const now = new Date();
|
||||||
|
const uid = crypto.randomUUID();
|
||||||
|
|
||||||
|
// Construct VCALENDAR/VEVENT manually to ensure compatibility
|
||||||
|
// ical.js is great for parsing but sometimes verbose for creation
|
||||||
|
// A simple template works well for basic events
|
||||||
|
|
||||||
|
const dtStamp = now.toISOString().replace(/[-:.]/g, '').substring(0, 15) + 'Z';
|
||||||
|
|
||||||
|
let dtStart = '';
|
||||||
|
let dtEnd = '';
|
||||||
|
let dtStartParam = '';
|
||||||
|
let dtEndParam = '';
|
||||||
|
|
||||||
|
if (eventData.start.date) {
|
||||||
|
// All-day event
|
||||||
|
dtStart = eventData.start.date.replace(/-/g, '');
|
||||||
|
dtEnd = eventData.end.date ? eventData.end.date.replace(/-/g, '') : dtStart; // Fallback
|
||||||
|
dtStartParam = ';VALUE=DATE';
|
||||||
|
dtEndParam = ';VALUE=DATE';
|
||||||
|
|
||||||
|
// For all-day events, end date is exclusive, so if they are same, add 1 day
|
||||||
|
// But typically UI handles this. Let's assume input is correct.
|
||||||
|
} else if (eventData.start.dateTime) {
|
||||||
|
// Timed event
|
||||||
|
dtStart = new Date(eventData.start.dateTime).toISOString().replace(/[-:.]/g, '').substring(0, 15) + 'Z';
|
||||||
|
dtEnd = eventData.end.dateTime
|
||||||
|
? new Date(eventData.end.dateTime).toISOString().replace(/[-:.]/g, '').substring(0, 15) + 'Z'
|
||||||
|
: dtStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
const description = eventData.description ? `DESCRIPTION:${eventData.description.replace(/\n/g, '\\n')}\r\n` : '';
|
||||||
|
const location = eventData.location ? `LOCATION:${eventData.location.replace(/,/g, '\\,')}\r\n` : '';
|
||||||
|
|
||||||
|
const iCalString = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//My Weekly ToDo List//EN
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:${uid}
|
||||||
|
DTSTAMP:${dtStamp}
|
||||||
|
DTSTART${dtStartParam}:${dtStart}
|
||||||
|
DTEND${dtEndParam}:${dtEnd}
|
||||||
|
SUMMARY:${eventData.title}
|
||||||
|
${description}${location}END:VEVENT
|
||||||
|
END:VCALENDAR`;
|
||||||
|
|
||||||
|
console.log('[APPLE CALENDAR] Creating event with iCal:', iCalString);
|
||||||
|
|
||||||
|
const filename = `${uid}.ics`;
|
||||||
|
|
||||||
|
await client.createCalendarObject({
|
||||||
|
calendar: targetCalendar,
|
||||||
|
filename,
|
||||||
|
iCalString
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `${uid}-${filename}`, // Composite ID to help with updates later if needed, but usually UID is enough
|
||||||
|
title: eventData.title,
|
||||||
|
startDate: eventData.start.dateTime || eventData.start.date || '',
|
||||||
|
endDate: eventData.end.dateTime || eventData.end.date || '',
|
||||||
|
description: eventData.description,
|
||||||
|
location: eventData.location
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[APPLE CALENDAR] Error creating event:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing event
|
||||||
|
*/
|
||||||
|
export const updateEvent = async (
|
||||||
|
email: string,
|
||||||
|
appSpecificPassword: string,
|
||||||
|
calendarUrl: string,
|
||||||
|
eventId: string, // This might be composite or just UID
|
||||||
|
eventData: {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
location?: string;
|
||||||
|
start?: { dateTime?: string; date?: string };
|
||||||
|
end?: { dateTime?: string; date?: string };
|
||||||
|
}
|
||||||
|
): Promise<AppleCalendarEvent> => {
|
||||||
|
try {
|
||||||
|
const client = createClient(email, appSpecificPassword);
|
||||||
|
await client.login();
|
||||||
|
|
||||||
|
const calendars = await client.fetchCalendars();
|
||||||
|
const targetCalendar = calendars.find(c => c.url === calendarUrl);
|
||||||
|
|
||||||
|
if (!targetCalendar) {
|
||||||
|
throw new Error(`Calendar not found: ${calendarUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse IDs - implementation specific
|
||||||
|
// Our getUpcomingEvents returns ID as "UID-filename" or just UID if filename not avail?
|
||||||
|
// Actually getUpcomingEvents returns `${event.uid}-${occStart}` for recurring
|
||||||
|
// or `event.uid || eventObj.url` for simple.
|
||||||
|
|
||||||
|
// We need the original object URL (filename) to update via DAV.
|
||||||
|
// If we only have UID, we have to search for it.
|
||||||
|
|
||||||
|
// Strategy: Fetch all objects in range (expensive?) or try to find by UID?
|
||||||
|
// CALDAV allows query by UID.
|
||||||
|
|
||||||
|
// Let's assume eventId passed in is the UID for now, or we can extract it.
|
||||||
|
const uid = eventId.split('-')[0]; // Simple heuristic
|
||||||
|
|
||||||
|
// Use calendarQuery to find the object by UID
|
||||||
|
// Valid PROP query for getetag and calendar-data
|
||||||
|
|
||||||
|
// Tsdav doesn't expose a simple "findOneByUID".
|
||||||
|
// We'll traverse, assuming we can filter.
|
||||||
|
// Actually, `fetchCalendarObjects` allows filters.
|
||||||
|
|
||||||
|
/*
|
||||||
|
NOTE: tsdav filter support is XML based.
|
||||||
|
Constructing a filter for UID:
|
||||||
|
<filter>
|
||||||
|
<comp-filter name="VCALENDAR">
|
||||||
|
<comp-filter name="VEVENT">
|
||||||
|
<prop-filter name="UID">
|
||||||
|
<text-match collation="i;octet">${uid}</text-match>
|
||||||
|
</prop-filter>
|
||||||
|
</comp-filter>
|
||||||
|
</comp-filter>
|
||||||
|
</filter>
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Since constructing that XML object via tsdav's types might be complex,
|
||||||
|
// let's try a simpler approach if possible, or build the object.
|
||||||
|
|
||||||
|
// For now, let's assume we can fetch objects and find the match in memory if the range isn't too huge?
|
||||||
|
// No, better to search.
|
||||||
|
|
||||||
|
// Let's try to pass a simpler time range around the event if we knew the time.
|
||||||
|
// If not, we scan.
|
||||||
|
// Given we are editing, we usually have the original time.
|
||||||
|
|
||||||
|
// But `eventData` only has NEW data. We might need valid old data.
|
||||||
|
// Let's rely on client logic to pass us enough info?
|
||||||
|
// Wait, `updateCalendarEvent` in `calendar-events.ts` calls us.
|
||||||
|
|
||||||
|
// Let's assume for MVP we fetch objects in a wide range? No.
|
||||||
|
|
||||||
|
// Let's use `fetchCalendarObjects` without timerange -> fetches all? Dangerous for large cals.
|
||||||
|
|
||||||
|
// Alternative: The `eventId` from our `getUpcomingEvents` was `event.uid` (or derived).
|
||||||
|
// Let's try to match by UID.
|
||||||
|
|
||||||
|
// Workaround: We will use a time range if provided in inputs (unlikely for existing?)
|
||||||
|
// Actually, we don't have the OLD time in the `updateEvent` signature here easily unless we fetch.
|
||||||
|
|
||||||
|
// Let's try to standard approach: Fetch all from now - 1 month to + 1 year?
|
||||||
|
// Or just valid `calendar-query` with UID filter.
|
||||||
|
|
||||||
|
// Since constructing the filter manually is hard in this context without xml-js helpers handy...
|
||||||
|
// I will try to fetch the object by its URL if the ID *was* the URL.
|
||||||
|
// In `getUpcomingEvents`, for simple events, we returned `id: event.uid || eventObj.url`.
|
||||||
|
// If it's a URL (ends in .ics), we can just use it.
|
||||||
|
|
||||||
|
let objectUrl = '';
|
||||||
|
let etag = '';
|
||||||
|
let existingIcal = '';
|
||||||
|
|
||||||
|
if (eventId.endsWith('.ics')) {
|
||||||
|
// It looks like a filename/url
|
||||||
|
objectUrl = eventId;
|
||||||
|
// But we need the full URL or relative?
|
||||||
|
// tsdav expects `calendarObject.url`.
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we can't easily find it by ID, we might fail.
|
||||||
|
// Let's assume for this iteration we try to find it.
|
||||||
|
|
||||||
|
const allObjects = await client.fetchCalendarObjects({
|
||||||
|
calendar: targetCalendar,
|
||||||
|
// No time range = all? limit?
|
||||||
|
// Let's check if we can filter by UID in filter object
|
||||||
|
});
|
||||||
|
|
||||||
|
// This fetches ALL objects (headers only usually?).
|
||||||
|
// `fetchCalendarObjects` does report usually.
|
||||||
|
|
||||||
|
// Find matching UID
|
||||||
|
const targetObject = allObjects.find(obj => {
|
||||||
|
// obj.data contains iCal string if expanded?
|
||||||
|
// If not expanded, we might need to fetch data.
|
||||||
|
// By default `fetchCalendarObjects` usually fetches props specified.
|
||||||
|
if (obj.data) {
|
||||||
|
return obj.data.includes(`UID:${uid}`);
|
||||||
|
}
|
||||||
|
return obj.url.includes(uid); // Fallback assumption
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!targetObject) {
|
||||||
|
throw new Error('Event not found on server');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we have the object.
|
||||||
|
// Parse existing iCal to preserve other fields
|
||||||
|
const jcal = ICAL.parse(targetObject.data);
|
||||||
|
const comp = new ICAL.Component(jcal);
|
||||||
|
const vevent = comp.getFirstSubcomponent('vevent');
|
||||||
|
|
||||||
|
if (!vevent) {
|
||||||
|
throw new Error('No VEVENT found in calendar object');
|
||||||
|
}
|
||||||
|
const event = new ICAL.Event(vevent);
|
||||||
|
|
||||||
|
// Update fields
|
||||||
|
if (eventData.title) event.summary = eventData.title;
|
||||||
|
if (eventData.description) event.description = eventData.description;
|
||||||
|
if (eventData.location) event.location = eventData.location;
|
||||||
|
|
||||||
|
if (eventData.start) {
|
||||||
|
if (eventData.start.date) {
|
||||||
|
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.date), true);
|
||||||
|
event.startDate.isDate = true;
|
||||||
|
} else if (eventData.start.dateTime) {
|
||||||
|
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.dateTime), true);
|
||||||
|
event.startDate.isDate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.end) {
|
||||||
|
if (eventData.end.date) {
|
||||||
|
event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.date), true);
|
||||||
|
event.endDate.isDate = true;
|
||||||
|
} else if (eventData.end.dateTime) {
|
||||||
|
event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.dateTime), true);
|
||||||
|
event.endDate.isDate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bump sequence
|
||||||
|
event.sequence = (event.sequence || 0) + 1;
|
||||||
|
if (vevent) {
|
||||||
|
vevent.updatePropertyWithValue('dtstamp', ICAL.Time.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedIcalString = comp.toString();
|
||||||
|
console.log('[APPLE CALENDAR] Updating event with iCal:', updatedIcalString);
|
||||||
|
|
||||||
|
// tsdav types might be slightly off in the d.ts compared to usage or we need to check the actual signature
|
||||||
|
// In d.ts: updateCalendarObject(params: { calendarObject: DAVCalendarObject ... }) -> Promise<Response>
|
||||||
|
// But it doesn't seem to take 'data' in the d.ts signature shown earlier?
|
||||||
|
// Wait, let's look at d.ts again.
|
||||||
|
// updateCalendarObject: (params: { calendarObject: ..., headers?: ... })
|
||||||
|
// It DOES NOT show `data` or `etag` in the params in the d.ts signature shown earlier?
|
||||||
|
// Let's check line 117 of d.ts:
|
||||||
|
// updateCalendarObject: (params: { calendarObject: ... })
|
||||||
|
// This implies the data must be SET on the calendarObject before calling?
|
||||||
|
// OR the d.ts is incomplete/wrong.
|
||||||
|
|
||||||
|
// Let's assume we need to update the object locally then call update?
|
||||||
|
// Or maybe we use the `davRequest` or `updateObject` lower level if `updateCalendarObject` limits us.
|
||||||
|
// Actually, `updateObject` takes `url`, `data`, `etag`.
|
||||||
|
|
||||||
|
// Let's try using `client.updateObject` directly which is more raw but allows data.
|
||||||
|
// `targetObject.url` is what we need.
|
||||||
|
|
||||||
|
await client.updateObject({
|
||||||
|
url: targetObject.url,
|
||||||
|
data: updatedIcalString,
|
||||||
|
etag: targetObject.etag
|
||||||
|
} as any); // Cast to any to bypass type definition mismatch
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: eventId,
|
||||||
|
title: event.summary,
|
||||||
|
startDate: event.startDate.toString(),
|
||||||
|
endDate: event.endDate.toString(),
|
||||||
|
description: event.description,
|
||||||
|
location: event.location
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[APPLE CALENDAR] Error updating event:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an event
|
||||||
|
*/
|
||||||
|
export const deleteEvent = async (
|
||||||
|
email: string,
|
||||||
|
appSpecificPassword: string,
|
||||||
|
calendarUrl: string,
|
||||||
|
eventId: string
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const client = createClient(email, appSpecificPassword);
|
||||||
|
await client.login();
|
||||||
|
|
||||||
|
const calendars = await client.fetchCalendars();
|
||||||
|
const targetCalendar = calendars.find(c => c.url === calendarUrl);
|
||||||
|
|
||||||
|
if (!targetCalendar) {
|
||||||
|
throw new Error(`Calendar not found: ${calendarUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uid = eventId.split('-')[0];
|
||||||
|
|
||||||
|
// Find object - similar logic to update
|
||||||
|
// Optimal: Pass the object URL in the ID in getUpcomingEvents to allow O(1) delete/update
|
||||||
|
|
||||||
|
const allObjects = await client.fetchCalendarObjects({
|
||||||
|
calendar: targetCalendar,
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetObject = allObjects.find(obj => {
|
||||||
|
if (obj.data) {
|
||||||
|
return obj.data.includes(`UID:${uid}`);
|
||||||
|
}
|
||||||
|
return obj.url.includes(uid);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!targetObject) {
|
||||||
|
console.warn('[APPLE CALENDAR] Event to delete not found, maybe already deleted?');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same issue as update - use deleteObject directly
|
||||||
|
await client.deleteObject({
|
||||||
|
url: targetObject.url,
|
||||||
|
etag: targetObject.etag
|
||||||
|
} as any); // Cast to any to bypass type definition mismatch
|
||||||
|
|
||||||
|
console.log('[APPLE CALENDAR] Event deleted successfully');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[APPLE CALENDAR] Error deleting event:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -559,6 +559,49 @@ export const createCalendarEvent = async (
|
|||||||
calendarId,
|
calendarId,
|
||||||
calendarTitle: '',
|
calendarTitle: '',
|
||||||
} as CalendarEvent;
|
} as CalendarEvent;
|
||||||
|
return {
|
||||||
|
id: createdEvent.id,
|
||||||
|
title: createdEvent.summary,
|
||||||
|
description: createdEvent.description,
|
||||||
|
start: createdEvent.start,
|
||||||
|
end: createdEvent.end,
|
||||||
|
location: createdEvent.location,
|
||||||
|
source: 'outlook',
|
||||||
|
calendarId,
|
||||||
|
calendarTitle: '',
|
||||||
|
} as CalendarEvent;
|
||||||
|
} else if (connection.provider === 'apple') {
|
||||||
|
const [email, appPassword] = connection.accessToken.split(':');
|
||||||
|
|
||||||
|
// We need to map our event format to what createEvent expects
|
||||||
|
// createEvent expects: { title, description?, location?, start: {dateTime?, date?}, end: ... }
|
||||||
|
// Our 'event' arg is Partial<CalendarEvent>, which matches well.
|
||||||
|
// However, event.start and event.end might be undefined in Partial, so we need checks.
|
||||||
|
|
||||||
|
if (!event.title) throw new Error('Event title is required');
|
||||||
|
if (!event.start || !event.end) throw new Error('Event start and end times are required');
|
||||||
|
|
||||||
|
const createdEvent = await import('./apple-calendar').then(m =>
|
||||||
|
m.createEvent(email, appPassword, calendarId, {
|
||||||
|
title: event.title!,
|
||||||
|
description: event.description,
|
||||||
|
location: event.location,
|
||||||
|
start: event.start!,
|
||||||
|
end: event.end!
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: createdEvent.id,
|
||||||
|
title: createdEvent.title,
|
||||||
|
description: createdEvent.description,
|
||||||
|
start: { dateTime: createdEvent.startDate }, // Mapping back might need adjustment if date-only
|
||||||
|
end: { dateTime: createdEvent.endDate },
|
||||||
|
location: createdEvent.location,
|
||||||
|
source: 'apple',
|
||||||
|
calendarId,
|
||||||
|
calendarTitle: '', // Fetch if needed
|
||||||
|
} as CalendarEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Provider ${connection.provider} does not support creating events yet.`);
|
throw new Error(`Provider ${connection.provider} does not support creating events yet.`);
|
||||||
@ -638,6 +681,30 @@ export const updateCalendarEvent = async (
|
|||||||
calendarId,
|
calendarId,
|
||||||
calendarTitle: '',
|
calendarTitle: '',
|
||||||
} as CalendarEvent;
|
} as CalendarEvent;
|
||||||
|
} else if (connection.provider === 'apple') {
|
||||||
|
const [email, appPassword] = connection.accessToken.split(':');
|
||||||
|
|
||||||
|
const updatedEvent = await import('./apple-calendar').then(m =>
|
||||||
|
m.updateEvent(email, appPassword, calendarId, eventId, {
|
||||||
|
title: event.title,
|
||||||
|
description: event.description,
|
||||||
|
location: event.location,
|
||||||
|
start: event.start,
|
||||||
|
end: event.end
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: updatedEvent.id,
|
||||||
|
title: updatedEvent.title,
|
||||||
|
description: updatedEvent.description,
|
||||||
|
start: { dateTime: updatedEvent.startDate },
|
||||||
|
end: { dateTime: updatedEvent.endDate },
|
||||||
|
location: updatedEvent.location,
|
||||||
|
source: 'apple',
|
||||||
|
calendarId,
|
||||||
|
calendarTitle: '',
|
||||||
|
} as CalendarEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Provider ${connection.provider} does not support updating events yet.`);
|
throw new Error(`Provider ${connection.provider} does not support updating events yet.`);
|
||||||
@ -680,6 +747,13 @@ export const deleteCalendarEvent = async (
|
|||||||
|
|
||||||
await deleteOutlookEvent(accessToken, calendarId, eventId);
|
await deleteOutlookEvent(accessToken, calendarId, eventId);
|
||||||
return;
|
return;
|
||||||
|
} else if (connection.provider === 'apple') {
|
||||||
|
const [email, appPassword] = connection.accessToken.split(':');
|
||||||
|
|
||||||
|
await import('./apple-calendar').then(m =>
|
||||||
|
m.deleteEvent(email, appPassword, calendarId, eventId)
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Provider ${connection.provider} does not support deleting events yet.`);
|
throw new Error(`Provider ${connection.provider} does not support deleting events yet.`);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user