Reposition notes popup to floating vertical container and various integration fixes
This commit is contained in:
parent
8842123caf
commit
368928c016
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.4.0",
|
"version": "1.5.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 } = body;
|
const { calendarId, title, description, start, end, location, allDay, recurrence, url } = body;
|
||||||
|
|
||||||
console.log('[API] Creating event:', { calendarId, title, start, end });
|
console.log('[API] Creating event:', { calendarId, title, start, end });
|
||||||
|
|
||||||
@ -60,8 +60,10 @@ export async function POST(request: NextRequest) {
|
|||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
location,
|
location,
|
||||||
allDay: !!allDay
|
allDay: !!allDay,
|
||||||
});
|
recurrence,
|
||||||
|
url,
|
||||||
|
} as any);
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
upsertCachedEvent(userId, connection.id, connection.provider, event)
|
upsertCachedEvent(userId, connection.id, connection.provider, event)
|
||||||
@ -81,7 +83,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 } = body;
|
const { calendarId, eventId, title, description, start, end, location, allDay, recurrence, url } = body;
|
||||||
|
|
||||||
console.log('[API] Updating event:', { calendarId, eventId, title });
|
console.log('[API] Updating event:', { calendarId, eventId, title });
|
||||||
|
|
||||||
@ -102,8 +104,10 @@ export async function PATCH(request: NextRequest) {
|
|||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
location,
|
location,
|
||||||
allDay: allDay !== undefined ? !!allDay : undefined
|
allDay: allDay !== undefined ? !!allDay : undefined,
|
||||||
});
|
recurrence,
|
||||||
|
url,
|
||||||
|
} as any);
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
upsertCachedEvent(userId, connection.id, connection.provider, event)
|
upsertCachedEvent(userId, connection.id, connection.provider, event)
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
import { authOptions } from "@/lib/auth";
|
import { authOptions } from "@/lib/auth";
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import { createGoogleClient, updateGoogleTaskStatus, updateGoogleTask, deleteGoogleTask, fetchGoogleTasksForSync, GoogleTask } from '@/lib/google-tasks';
|
import { createGoogleClient, createGoogleTask, updateGoogleTaskStatus, updateGoogleTask, deleteGoogleTask, fetchGoogleTasksForSync, GoogleTask } from '@/lib/google-tasks';
|
||||||
import { fetchMsTodoTasksForSync, updateMsTodoTask, deleteMsTodoTask, createMsTodoTask, isMsTodoTaskCompleted } from '@/lib/microsoft-todo';
|
import { fetchMsTodoTasksForSync, updateMsTodoTask, deleteMsTodoTask, createMsTodoTask, isMsTodoTaskCompleted } from '@/lib/microsoft-todo';
|
||||||
import { getOutlookAccessToken } from '@/lib/outlook-token';
|
import { getOutlookAccessToken } from '@/lib/outlook-token';
|
||||||
|
|
||||||
@ -492,6 +492,35 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({ success: true, task: updatedTask });
|
return NextResponse.json({ success: true, task: updatedTask });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider === 'google') {
|
||||||
|
const account = await prisma.account.findFirst({
|
||||||
|
where: { userId: task.userId, provider: 'google' }
|
||||||
|
});
|
||||||
|
if (!account?.access_token) {
|
||||||
|
return NextResponse.json({ error: 'Google token not available' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = createGoogleClient(account.access_token, account.refresh_token || undefined);
|
||||||
|
|
||||||
|
const created = await createGoogleTask(client, listExternalId, {
|
||||||
|
title: task.title,
|
||||||
|
notes: task.description || undefined,
|
||||||
|
due: task.scheduledDate ? task.scheduledDate.toISOString() : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedTask = await prisma.task.update({
|
||||||
|
where: { id: taskId },
|
||||||
|
data: {
|
||||||
|
externalId: created.id,
|
||||||
|
externalProvider: 'google',
|
||||||
|
externalListId: listExternalId,
|
||||||
|
lastSyncedAt: new Date(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, task: updatedTask });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ error: `Provider "${provider}" creation sync not supported yet` }, { status: 400 });
|
return NextResponse.json({ error: `Provider "${provider}" creation sync not supported yet` }, { status: 400 });
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
|||||||
@ -982,6 +982,27 @@ h3 {
|
|||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Time-grid tasks: show actions below task text as a dropdown toolbar */
|
||||||
|
.time-slot-task > .task-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: auto;
|
||||||
|
bottom: auto;
|
||||||
|
left: 0;
|
||||||
|
right: auto;
|
||||||
|
margin-top: 0;
|
||||||
|
background: var(--weekly-bg, white);
|
||||||
|
border: 1px solid var(--weekly-border, #e0e0e0);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
|
||||||
|
padding: 2px 4px;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .time-slot-task > .task-actions {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
.task-action-btn {
|
.task-action-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@ -32,6 +32,8 @@ export default function CalendarEventModal({
|
|||||||
const [title, setTitle] = useState(event?.title || '');
|
const [title, setTitle] = useState(event?.title || '');
|
||||||
const [description, setDescription] = useState(event?.description || '');
|
const [description, setDescription] = useState(event?.description || '');
|
||||||
const [location, setLocation] = useState(event?.location || '');
|
const [location, setLocation] = useState(event?.location || '');
|
||||||
|
const [url, setUrl] = useState(event?.url || '');
|
||||||
|
const [recurrence, setRecurrence] = useState(event?.recurrence || 'none');
|
||||||
const [calendarId, setCalendarId] = useState(event?.calendarId || (availableCalendars.length > 0 ? availableCalendars[0].id : ''));
|
const [calendarId, setCalendarId] = useState(event?.calendarId || (availableCalendars.length > 0 ? availableCalendars[0].id : ''));
|
||||||
|
|
||||||
// Date/Time State
|
// Date/Time State
|
||||||
@ -90,6 +92,8 @@ export default function CalendarEventModal({
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
location,
|
location,
|
||||||
|
url: url || undefined,
|
||||||
|
recurrence: recurrence !== 'none' ? recurrence : undefined,
|
||||||
calendarId,
|
calendarId,
|
||||||
allDay,
|
allDay,
|
||||||
start: { dateTime: startDate.toISOString() },
|
start: { dateTime: startDate.toISOString() },
|
||||||
@ -218,6 +222,23 @@ export default function CalendarEventModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Recurrence */}
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Repeat</label>
|
||||||
|
<select
|
||||||
|
value={recurrence}
|
||||||
|
onChange={e => setRecurrence(e.target.value)}
|
||||||
|
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||||
|
>
|
||||||
|
<option value="none">Never</option>
|
||||||
|
<option value="daily">Every Day</option>
|
||||||
|
<option value="weekly">Every Week</option>
|
||||||
|
<option value="biweekly">Every 2 Weeks</option>
|
||||||
|
<option value="monthly">Every Month</option>
|
||||||
|
<option value="yearly">Every Year</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Location */}
|
{/* Location */}
|
||||||
<div>
|
<div>
|
||||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Location</label>
|
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Location</label>
|
||||||
@ -230,9 +251,21 @@ export default function CalendarEventModal({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* URL */}
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>URL</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={url}
|
||||||
|
onChange={e => setUrl(e.target.value)}
|
||||||
|
placeholder="https://..."
|
||||||
|
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<div>
|
<div>
|
||||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Description</label>
|
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Notes</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={description}
|
value={description}
|
||||||
onChange={e => setDescription(e.target.value)}
|
onChange={e => setDescription(e.target.value)}
|
||||||
|
|||||||
@ -269,21 +269,12 @@ export function GridTaskBlock({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(true) && (
|
</div>
|
||||||
|
|
||||||
|
{/* Task Actions - shown below text on hover */}
|
||||||
<div
|
<div
|
||||||
className="task-actions"
|
className="task-actions"
|
||||||
style={{
|
onClick={(e) => e.stopPropagation()}
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "2px",
|
|
||||||
marginLeft: "4px",
|
|
||||||
flexShrink: 0,
|
|
||||||
flexWrap: "nowrap",
|
|
||||||
background: darkMode ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.8)",
|
|
||||||
padding: "1px 3px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className={`task-action-btn ${task.completed ? "active text-green-600 dark:text-green-500" : ""}`}
|
className={`task-action-btn ${task.completed ? "active text-green-600 dark:text-green-500" : ""}`}
|
||||||
@ -329,6 +320,15 @@ export function GridTaskBlock({
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{!task.completed && (
|
||||||
|
<button
|
||||||
|
className={`task-action-btn ${task.isRecurring ? "active" : ""}`}
|
||||||
|
onClick={(e) => { e.stopPropagation(); setSelectedTaskForRecurrence(task); }}
|
||||||
|
title={task.isRecurring ? "Edit recurrence" : "Make recurring"}
|
||||||
|
>
|
||||||
|
<Repeat size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button className="task-action-btn delete text-red-500 hover:text-red-700 hover:bg-red-100/50 dark:hover:bg-red-900/30 rounded" onClick={(e) => { e.stopPropagation(); deleteTask(task.id); }} title="Delete">
|
<button className="task-action-btn delete text-red-500 hover:text-red-700 hover:bg-red-100/50 dark:hover:bg-red-900/30 rounded" onClick={(e) => { e.stopPropagation(); deleteTask(task.id); }} title="Delete">
|
||||||
<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round">
|
<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
@ -336,8 +336,6 @@ export function GridTaskBlock({
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Inline Expanders Container */}
|
{/* Inline Expanders Container */}
|
||||||
<div style={{ paddingLeft: "4px", paddingRight: "4px", paddingBottom: "10px", marginTop: "4px" }}>
|
<div style={{ paddingLeft: "4px", paddingRight: "4px", paddingBottom: "10px", marginTop: "4px" }}>
|
||||||
|
|||||||
@ -42,6 +42,7 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
Undo2,
|
Undo2,
|
||||||
Redo2,
|
Redo2,
|
||||||
|
AlertCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
@ -96,7 +97,7 @@ interface CalendarEvent {
|
|||||||
title: string;
|
title: string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
source: "google" | "apple";
|
source: "google" | "apple" | "outlook";
|
||||||
calendarId?: string;
|
calendarId?: string;
|
||||||
calendarTitle?: string;
|
calendarTitle?: string;
|
||||||
calendarColor?: string;
|
calendarColor?: string;
|
||||||
@ -487,6 +488,7 @@ export default function WeeklyView() {
|
|||||||
const [viewDays, setViewDays] = useState(7);
|
const [viewDays, setViewDays] = useState(7);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
|
const [syncError, setSyncError] = useState<string | null>(null);
|
||||||
const syncCountRef = useRef(0);
|
const syncCountRef = useRef(0);
|
||||||
const startSync = useCallback(() => { syncCountRef.current++; setIsSyncing(true); }, []);
|
const startSync = useCallback(() => { syncCountRef.current++; setIsSyncing(true); }, []);
|
||||||
const endSync = useCallback(() => { syncCountRef.current = Math.max(0, syncCountRef.current - 1); if (syncCountRef.current === 0) setIsSyncing(false); }, []);
|
const endSync = useCallback(() => { syncCountRef.current = Math.max(0, syncCountRef.current - 1); if (syncCountRef.current === 0) setIsSyncing(false); }, []);
|
||||||
@ -934,13 +936,23 @@ export default function WeeklyView() {
|
|||||||
// Optimistically add/update from API response, then force refresh cache
|
// Optimistically add/update from API response, then force refresh cache
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.event) {
|
if (data.event) {
|
||||||
|
// Transform API shape (start.dateTime/end.dateTime) to frontend shape (startTime/endTime)
|
||||||
|
const ev = data.event;
|
||||||
|
const frontendEvent: CalendarEvent = {
|
||||||
|
id: ev.id,
|
||||||
|
title: ev.title,
|
||||||
|
startTime: ev.start?.dateTime || ev.start?.date || ev.startTime || '',
|
||||||
|
endTime: ev.end?.dateTime || ev.end?.date || ev.endTime || '',
|
||||||
|
source: ev.source,
|
||||||
|
calendarId: ev.calendarId,
|
||||||
|
calendarTitle: ev.calendarTitle,
|
||||||
|
calendarColor: ev.backgroundColor || ev.calendarColor,
|
||||||
|
};
|
||||||
setRawCalendarEvents(prev => {
|
setRawCalendarEvents(prev => {
|
||||||
if (eventData.id) {
|
if (eventData.id) {
|
||||||
// Update existing
|
return prev.map(e => e.id === eventData.id ? frontendEvent : e);
|
||||||
return prev.map(e => e.id === eventData.id ? data.event : e);
|
|
||||||
}
|
}
|
||||||
// Add new
|
return [...prev, frontendEvent];
|
||||||
return [...prev, data.event];
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Also force-refresh from provider to ensure full sync
|
// Also force-refresh from provider to ensure full sync
|
||||||
@ -1072,7 +1084,9 @@ export default function WeeklyView() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silent fail for background sync
|
console.error("[SYNC] Task sync error:", e);
|
||||||
|
setSyncError("Task sync failed");
|
||||||
|
setTimeout(() => setSyncError(null), 10000);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
2 * 60 * 1000,
|
2 * 60 * 1000,
|
||||||
@ -1108,7 +1122,9 @@ export default function WeeklyView() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silent fail for background sync
|
console.error("[SYNC] Calendar sync error:", e);
|
||||||
|
setSyncError("Calendar sync failed");
|
||||||
|
setTimeout(() => setSyncError(null), 10000);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
2 * 60 * 1000,
|
2 * 60 * 1000,
|
||||||
@ -3085,6 +3101,8 @@ export default function WeeklyView() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error syncing:", error);
|
console.error("Error syncing:", error);
|
||||||
setSyncStatus("idle");
|
setSyncStatus("idle");
|
||||||
|
setSyncError("Sync failed");
|
||||||
|
setTimeout(() => setSyncError(null), 10000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -3489,7 +3507,12 @@ export default function WeeklyView() {
|
|||||||
<div className="flex items-center justify-center gap-6 absolute left-1/2 transform -translate-x-1/2 group">
|
<div className="flex items-center justify-center gap-6 absolute left-1/2 transform -translate-x-1/2 group">
|
||||||
{/* Week & Year */}
|
{/* Week & Year */}
|
||||||
<div className="whitespace-nowrap flex items-center gap-2">
|
<div className="whitespace-nowrap flex items-center gap-2">
|
||||||
{(isLoading || isSyncing || syncStatus === "syncing") ? (
|
{syncError ? (
|
||||||
|
<div className="flex items-center gap-1 text-red-500" title={syncError}>
|
||||||
|
<AlertCircle size={14} />
|
||||||
|
<span className="text-xs">{syncError}</span>
|
||||||
|
</div>
|
||||||
|
) : (isLoading || isSyncing || syncStatus === "syncing") ? (
|
||||||
<div className="weekly-spinner" title="Syncing..."></div>
|
<div className="weekly-spinner" title="Syncing..."></div>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
@ -7866,31 +7889,70 @@ function SettingsSidebar({
|
|||||||
{conn.calendars &&
|
{conn.calendars &&
|
||||||
Array.isArray(conn.calendars) &&
|
Array.isArray(conn.calendars) &&
|
||||||
conn.calendars.length > 0 ? (
|
conn.calendars.length > 0 ? (
|
||||||
<ul
|
<div style={{ paddingLeft: "8px" }}>
|
||||||
style={{ paddingLeft: "24px", listStyle: "none" }}
|
{/* Column Headers */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "8px",
|
||||||
|
marginBottom: "6px",
|
||||||
|
paddingBottom: "4px",
|
||||||
|
borderBottom: "1px solid var(--weekly-border, #eee)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<span style={{ flex: 1, fontSize: "0.75rem", color: "var(--weekly-text-light)", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.5px" }}>
|
||||||
|
Calendar
|
||||||
|
</span>
|
||||||
|
<span style={{ width: "50px", textAlign: "center", fontSize: "0.75rem", color: "var(--weekly-text-light)", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.5px" }}>
|
||||||
|
Display
|
||||||
|
</span>
|
||||||
|
<span style={{ width: "50px", textAlign: "center", fontSize: "0.75rem", color: "var(--weekly-text-light)", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.5px" }}>
|
||||||
|
Edit
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Calendar Rows */}
|
||||||
{conn.calendars.map((cal: any) => {
|
{conn.calendars.map((cal: any) => {
|
||||||
const isShared = /⚠/.test(cal.title);
|
const isShared = /⚠/.test(cal.title);
|
||||||
const cleanTitle = cal.title
|
const cleanTitle = cal.title
|
||||||
.replace(/\s*⚠️?\s*/g, "")
|
.replace(/\s*⚠️?\s*/g, "")
|
||||||
.trim();
|
.trim();
|
||||||
return (
|
return (
|
||||||
<li
|
<div
|
||||||
key={cal.id}
|
key={cal.id}
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "16px",
|
|
||||||
marginBottom: "8px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "8px",
|
gap: "8px",
|
||||||
|
padding: "3px 0",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Calendar Name */}
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
color: "var(--weekly-text)",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cleanTitle}
|
||||||
|
{isShared && (
|
||||||
|
<span title="Shared calendar" style={{ marginLeft: "4px", fontSize: "0.75rem", opacity: 0.5 }}>
|
||||||
|
🔗
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{cal.isPrimary && (
|
||||||
|
<span style={{ fontSize: "0.8em", color: "var(--weekly-text-light)", marginLeft: "4px" }}>
|
||||||
|
(Primary)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Display checkbox */}
|
||||||
|
<span style={{ width: "50px", textAlign: "center" }}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={cal.selected !== false}
|
checked={cal.selected !== false}
|
||||||
@ -7901,51 +7963,10 @@ function SettingsSidebar({
|
|||||||
}
|
}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
/>
|
/>
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: "0.9rem",
|
|
||||||
color: "var(--weekly-text)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{cleanTitle}
|
|
||||||
<span style={{ fontSize: '0.85em', color: 'var(--weekly-text-light)', marginLeft: '4px' }}>
|
|
||||||
({conn.provider === 'google' ? 'Google Calendar' : conn.provider === 'apple' ? 'Apple Calendar' : 'Outlook Calendar'})
|
|
||||||
</span>
|
</span>
|
||||||
{isShared && (
|
|
||||||
<span
|
|
||||||
title="Shared calendar"
|
|
||||||
style={{
|
|
||||||
marginLeft: "5px",
|
|
||||||
fontSize: "0.75rem",
|
|
||||||
opacity: 0.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
🔗
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{cal.isPrimary && (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: "0.8em",
|
|
||||||
color: "var(--weekly-text-light)",
|
|
||||||
marginLeft: "4px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
(Primary)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Editable toggle */}
|
{/* Edit checkbox */}
|
||||||
<div
|
<span style={{ width: "50px", textAlign: "center" }}>
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "4px",
|
|
||||||
opacity: 0.8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={cal.editable === true}
|
checked={cal.editable === true}
|
||||||
@ -7954,27 +7975,14 @@ function SettingsSidebar({
|
|||||||
editable: e.target.checked,
|
editable: e.target.checked,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
style={{
|
style={{ cursor: "pointer" }}
|
||||||
cursor: "pointer",
|
|
||||||
width: "12px",
|
|
||||||
height: "12px",
|
|
||||||
}}
|
|
||||||
title="Allow adding/editing events"
|
title="Allow adding/editing events"
|
||||||
/>
|
/>
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: "0.8rem",
|
|
||||||
color: "#888",
|
|
||||||
}}
|
|
||||||
title="Allow adding/editing events"
|
|
||||||
>
|
|
||||||
Editable
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -8095,16 +8103,29 @@ function SettingsSidebar({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul
|
<div
|
||||||
style={{
|
style={{
|
||||||
listStyle: "none",
|
|
||||||
padding: 0,
|
|
||||||
margin: 0,
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "4px",
|
gap: "4px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Column header */}
|
||||||
|
{providerLists.length > 0 && (
|
||||||
|
<div style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "2px 8px",
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--weekly-text-light, #888)",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.05em",
|
||||||
|
}}>
|
||||||
|
<span style={{ flex: 1 }}>List</span>
|
||||||
|
<span style={{ width: "50px", textAlign: "center" }}>Sync</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{providerLists.map((list: { id: string; title: string }) => {
|
{providerLists.map((list: { id: string; title: string }) => {
|
||||||
const isSynced = somedayLists.some(
|
const isSynced = somedayLists.some(
|
||||||
(sl: SomedayList) =>
|
(sl: SomedayList) =>
|
||||||
@ -8112,17 +8133,20 @@ function SettingsSidebar({
|
|||||||
sl.externalProvider === conn.provider,
|
sl.externalProvider === conn.provider,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<li
|
<div
|
||||||
key={list.id}
|
key={list.id}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "10px",
|
|
||||||
padding: "4px 8px",
|
padding: "4px 8px",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
background: "rgba(0,0,0,0.02)",
|
background: "rgba(0,0,0,0.02)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<span style={{ flex: 1, fontSize: "0.9rem" }}>
|
||||||
|
{list.title}
|
||||||
|
</span>
|
||||||
|
<span style={{ width: "50px", textAlign: "center" }}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isSynced}
|
checked={isSynced}
|
||||||
@ -8136,10 +8160,8 @@ function SettingsSidebar({
|
|||||||
}
|
}
|
||||||
disabled={importingTasksState}
|
disabled={importingTasksState}
|
||||||
/>
|
/>
|
||||||
<span style={{ fontSize: "0.9rem" }}>
|
|
||||||
{list.title}
|
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!isFetching && providerLists.length === 0 && (
|
{!isFetching && providerLists.length === 0 && (
|
||||||
@ -8153,7 +8175,7 @@ function SettingsSidebar({
|
|||||||
No task lists found.
|
No task lists found.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -244,6 +244,8 @@ export const createEvent = async (
|
|||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
|
url?: string;
|
||||||
|
recurrence?: string;
|
||||||
start: { dateTime?: string; date?: string };
|
start: { dateTime?: string; date?: string };
|
||||||
end: { dateTime?: string; date?: string };
|
end: { dateTime?: string; date?: string };
|
||||||
}
|
}
|
||||||
@ -293,6 +295,20 @@ export const createEvent = async (
|
|||||||
|
|
||||||
const description = eventData.description ? `DESCRIPTION:${eventData.description.replace(/\n/g, '\\n')}\r\n` : '';
|
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 location = eventData.location ? `LOCATION:${eventData.location.replace(/,/g, '\\,')}\r\n` : '';
|
||||||
|
const url = eventData.url ? `URL:${eventData.url}\r\n` : '';
|
||||||
|
let rruleLine = '';
|
||||||
|
if (eventData.recurrence) {
|
||||||
|
const rruleMap: Record<string, string> = {
|
||||||
|
daily: 'RRULE:FREQ=DAILY',
|
||||||
|
weekly: 'RRULE:FREQ=WEEKLY',
|
||||||
|
biweekly: 'RRULE:FREQ=WEEKLY;INTERVAL=2',
|
||||||
|
monthly: 'RRULE:FREQ=MONTHLY',
|
||||||
|
yearly: 'RRULE:FREQ=YEARLY',
|
||||||
|
};
|
||||||
|
if (rruleMap[eventData.recurrence]) {
|
||||||
|
rruleLine = `${rruleMap[eventData.recurrence]}\r\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const iCalString = `BEGIN:VCALENDAR
|
const iCalString = `BEGIN:VCALENDAR
|
||||||
VERSION:2.0
|
VERSION:2.0
|
||||||
@ -303,7 +319,7 @@ DTSTAMP:${dtStamp}
|
|||||||
DTSTART${dtStartParam}:${dtStart}
|
DTSTART${dtStartParam}:${dtStart}
|
||||||
DTEND${dtEndParam}:${dtEnd}
|
DTEND${dtEndParam}:${dtEnd}
|
||||||
SUMMARY:${eventData.title}
|
SUMMARY:${eventData.title}
|
||||||
${description}${location}END:VEVENT
|
${description}${location}${url}${rruleLine}END:VEVENT
|
||||||
END:VCALENDAR`;
|
END:VCALENDAR`;
|
||||||
|
|
||||||
console.log('[APPLE CALENDAR] Creating event with iCal:', iCalString);
|
console.log('[APPLE CALENDAR] Creating event with iCal:', iCalString);
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
import { prisma } from './prisma';
|
import { prisma } from './prisma';
|
||||||
import { getCalendarEvents, CalendarEvent, CalendarConnection } from './calendar-events';
|
import { getCalendarEvents, CalendarEvent, CalendarConnection } from './calendar-events';
|
||||||
|
|
||||||
const STALE_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes
|
const STALE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes — match background sync interval
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Monday (start of ISO week) for a given date.
|
* Get the Monday (start of ISO week) for a given date.
|
||||||
|
|||||||
@ -19,6 +19,8 @@ export interface CalendarEvent {
|
|||||||
date?: string;
|
date?: string;
|
||||||
};
|
};
|
||||||
location?: string;
|
location?: string;
|
||||||
|
url?: string;
|
||||||
|
recurrence?: string;
|
||||||
source: 'google' | 'apple' | 'outlook';
|
source: 'google' | 'apple' | 'outlook';
|
||||||
calendarId: string;
|
calendarId: string;
|
||||||
calendarTitle: string;
|
calendarTitle: string;
|
||||||
@ -26,6 +28,45 @@ export interface CalendarEvent {
|
|||||||
allDay?: boolean;
|
allDay?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert friendly recurrence name to RRULE string
|
||||||
|
*/
|
||||||
|
function toRRule(recurrence?: string): string | null {
|
||||||
|
switch (recurrence) {
|
||||||
|
case 'daily': return 'RRULE:FREQ=DAILY';
|
||||||
|
case 'weekly': return 'RRULE:FREQ=WEEKLY';
|
||||||
|
case 'biweekly': return 'RRULE:FREQ=WEEKLY;INTERVAL=2';
|
||||||
|
case 'monthly': return 'RRULE:FREQ=MONTHLY';
|
||||||
|
case 'yearly': return 'RRULE:FREQ=YEARLY';
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert friendly recurrence name to Outlook Graph recurrence object
|
||||||
|
*/
|
||||||
|
function toOutlookRecurrence(recurrence?: string, startDate?: Date): any {
|
||||||
|
if (!recurrence || recurrence === 'none') return undefined;
|
||||||
|
const start = startDate || new Date();
|
||||||
|
const range = {
|
||||||
|
type: 'noEnd',
|
||||||
|
startDate: start.toISOString().split('T')[0],
|
||||||
|
};
|
||||||
|
switch (recurrence) {
|
||||||
|
case 'daily':
|
||||||
|
return { pattern: { type: 'daily', interval: 1 }, range };
|
||||||
|
case 'weekly':
|
||||||
|
return { pattern: { type: 'weekly', interval: 1, daysOfWeek: [['sunday','monday','tuesday','wednesday','thursday','friday','saturday'][start.getDay()]] }, range };
|
||||||
|
case 'biweekly':
|
||||||
|
return { pattern: { type: 'weekly', interval: 2, daysOfWeek: [['sunday','monday','tuesday','wednesday','thursday','friday','saturday'][start.getDay()]] }, range };
|
||||||
|
case 'monthly':
|
||||||
|
return { pattern: { type: 'absoluteMonthly', interval: 1, dayOfMonth: start.getDate() }, range };
|
||||||
|
case 'yearly':
|
||||||
|
return { pattern: { type: 'absoluteYearly', interval: 1, dayOfMonth: start.getDate(), month: start.getMonth() + 1 }, range };
|
||||||
|
default: return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Google Calendar event color mapping (colorId -> hex color)
|
// Google Calendar event color mapping (colorId -> hex color)
|
||||||
const GOOGLE_EVENT_COLORS: Record<string, string> = {
|
const GOOGLE_EVENT_COLORS: Record<string, string> = {
|
||||||
'1': '#7986cb', // Lavender
|
'1': '#7986cb', // Lavender
|
||||||
@ -510,12 +551,15 @@ export const createCalendarEvent = async (
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Map to Google format
|
// Map to Google format
|
||||||
|
const rrule = toRRule(event.recurrence);
|
||||||
const googleEvent: any = {
|
const googleEvent: any = {
|
||||||
summary: event.title,
|
summary: event.title,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
start: event.start,
|
start: event.start,
|
||||||
end: event.end,
|
end: event.end,
|
||||||
location: event.location,
|
location: event.location,
|
||||||
|
...(rrule ? { recurrence: [rrule] } : {}),
|
||||||
|
...(event.url ? { source: { url: event.url, title: event.url } } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const createdEvent = await import('./google-calendar').then(m =>
|
const createdEvent = await import('./google-calendar').then(m =>
|
||||||
@ -544,13 +588,15 @@ export const createCalendarEvent = async (
|
|||||||
else throw new Error('Failed to refresh token');
|
else throw new Error('Failed to refresh token');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startDate = event.start?.dateTime ? new Date(event.start.dateTime) : new Date();
|
||||||
const createdEvent = await createOutlookEvent(accessToken, calendarId, {
|
const createdEvent = await createOutlookEvent(accessToken, calendarId, {
|
||||||
summary: event.title,
|
summary: event.title,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
start: event.start,
|
start: event.start,
|
||||||
end: event.end,
|
end: event.end,
|
||||||
location: event.location,
|
location: event.location,
|
||||||
allDay: event.allDay
|
allDay: event.allDay,
|
||||||
|
recurrence: toOutlookRecurrence(event.recurrence, startDate),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -581,6 +627,8 @@ export const createCalendarEvent = async (
|
|||||||
title: event.title!,
|
title: event.title!,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
location: event.location,
|
location: event.location,
|
||||||
|
url: event.url,
|
||||||
|
recurrence: event.recurrence,
|
||||||
start: event.start!,
|
start: event.start!,
|
||||||
end: event.end!
|
end: event.end!
|
||||||
})
|
})
|
||||||
@ -627,12 +675,15 @@ export const updateCalendarEvent = async (
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Map to Google format
|
// Map to Google format
|
||||||
|
const rrule = toRRule(event.recurrence);
|
||||||
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;
|
||||||
if (event.start !== undefined) googleEvent.start = event.start;
|
if (event.start !== undefined) googleEvent.start = event.start;
|
||||||
if (event.end !== undefined) googleEvent.end = event.end;
|
if (event.end !== undefined) googleEvent.end = event.end;
|
||||||
if (event.location !== undefined) googleEvent.location = event.location;
|
if (event.location !== undefined) googleEvent.location = event.location;
|
||||||
|
if (rrule) googleEvent.recurrence = [rrule];
|
||||||
|
if (event.url) googleEvent.source = { url: event.url, title: event.url };
|
||||||
|
|
||||||
const updatedEvent = await import('./google-calendar').then(m =>
|
const updatedEvent = await import('./google-calendar').then(m =>
|
||||||
m.updateEvent(oauth2Client, accessToken, calendarId, eventId, googleEvent)
|
m.updateEvent(oauth2Client, accessToken, calendarId, eventId, googleEvent)
|
||||||
@ -657,13 +708,15 @@ export const updateCalendarEvent = async (
|
|||||||
else throw new Error('Failed to refresh token');
|
else throw new Error('Failed to refresh token');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startDate = event.start?.dateTime ? new Date(event.start.dateTime) : new Date();
|
||||||
const updatedEvent = await updateOutlookEvent(accessToken, calendarId, eventId, {
|
const updatedEvent = await updateOutlookEvent(accessToken, calendarId, eventId, {
|
||||||
summary: event.title,
|
summary: event.title,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
start: event.start,
|
start: event.start,
|
||||||
end: event.end,
|
end: event.end,
|
||||||
location: event.location,
|
location: event.location,
|
||||||
allDay: event.allDay
|
allDay: event.allDay,
|
||||||
|
recurrence: toOutlookRecurrence(event.recurrence, startDate),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -118,15 +118,18 @@ export const createEvent = async (
|
|||||||
oauth2Client.setCredentials({ access_token: accessToken });
|
oauth2Client.setCredentials({ access_token: accessToken });
|
||||||
try {
|
try {
|
||||||
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
||||||
const response = await calendar.events.insert({
|
const requestBody: any = {
|
||||||
calendarId,
|
|
||||||
requestBody: {
|
|
||||||
summary: event.summary,
|
summary: event.summary,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
start: event.start,
|
start: event.start,
|
||||||
end: event.end,
|
end: event.end,
|
||||||
location: event.location,
|
location: event.location,
|
||||||
},
|
};
|
||||||
|
if ((event as any).recurrence) requestBody.recurrence = (event as any).recurrence;
|
||||||
|
if ((event as any).source) requestBody.source = (event as any).source;
|
||||||
|
const response = await calendar.events.insert({
|
||||||
|
calendarId,
|
||||||
|
requestBody,
|
||||||
});
|
});
|
||||||
return response.data as any;
|
return response.data as any;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -148,16 +151,19 @@ export const updateEvent = async (
|
|||||||
oauth2Client.setCredentials({ access_token: accessToken });
|
oauth2Client.setCredentials({ access_token: accessToken });
|
||||||
try {
|
try {
|
||||||
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
||||||
const response = await calendar.events.patch({
|
const requestBody: any = {
|
||||||
calendarId,
|
|
||||||
eventId,
|
|
||||||
requestBody: {
|
|
||||||
summary: event.summary,
|
summary: event.summary,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
start: event.start,
|
start: event.start,
|
||||||
end: event.end,
|
end: event.end,
|
||||||
location: event.location,
|
location: event.location,
|
||||||
},
|
};
|
||||||
|
if ((event as any).recurrence) requestBody.recurrence = (event as any).recurrence;
|
||||||
|
if ((event as any).source) requestBody.source = (event as any).source;
|
||||||
|
const response = await calendar.events.patch({
|
||||||
|
calendarId,
|
||||||
|
eventId,
|
||||||
|
requestBody,
|
||||||
});
|
});
|
||||||
return response.data as any;
|
return response.data as any;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -180,6 +180,40 @@ export const fetchGoogleTasksForSync = async (client: OAuth2Client, taskListId:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new task in a task list
|
||||||
|
*/
|
||||||
|
export const createGoogleTask = async (
|
||||||
|
client: OAuth2Client,
|
||||||
|
taskListId: string,
|
||||||
|
taskData: { title: string; notes?: string; due?: string }
|
||||||
|
): Promise<GoogleTask> => {
|
||||||
|
const service = google.tasks({ version: 'v1', auth: client });
|
||||||
|
try {
|
||||||
|
const requestBody: any = { title: taskData.title };
|
||||||
|
if (taskData.notes) requestBody.notes = taskData.notes;
|
||||||
|
if (taskData.due) requestBody.due = taskData.due;
|
||||||
|
|
||||||
|
const response = await service.tasks.insert({
|
||||||
|
tasklist: taskListId,
|
||||||
|
requestBody,
|
||||||
|
});
|
||||||
|
const item = response.data;
|
||||||
|
return {
|
||||||
|
id: item.id!,
|
||||||
|
title: item.title!,
|
||||||
|
notes: item.notes || undefined,
|
||||||
|
status: item.status!,
|
||||||
|
due: item.due || undefined,
|
||||||
|
updated: item.updated!,
|
||||||
|
parent: (item as any).parent || undefined,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating Google Task:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const updateGoogleTaskStatus = async (client: OAuth2Client, taskListId: string, taskId: string, status: 'needsAction' | 'completed'): Promise<GoogleTask> => {
|
export const updateGoogleTaskStatus = async (client: OAuth2Client, taskListId: string, taskId: string, status: 'needsAction' | 'completed'): Promise<GoogleTask> => {
|
||||||
const service = google.tasks({ version: 'v1', auth: client });
|
const service = google.tasks({ version: 'v1', auth: client });
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -218,7 +218,8 @@ export const createEvent = async (
|
|||||||
isAllDay: !!event.allDay,
|
isAllDay: !!event.allDay,
|
||||||
location: {
|
location: {
|
||||||
displayName: event.location || ''
|
displayName: event.location || ''
|
||||||
}
|
},
|
||||||
|
...(event.recurrence ? { recurrence: event.recurrence } : {}),
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -265,7 +266,8 @@ export const updateEvent = async (
|
|||||||
isAllDay: event.allDay !== undefined ? !!event.allDay : undefined,
|
isAllDay: event.allDay !== undefined ? !!event.allDay : undefined,
|
||||||
location: {
|
location: {
|
||||||
displayName: event.location || ''
|
displayName: event.location || ''
|
||||||
}
|
},
|
||||||
|
...(event.recurrence ? { recurrence: event.recurrence } : {}),
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user