import { useState } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { useClient } from 'hooks/useClient'; /** * Creating, changing and deleting one DAV object, in FIELDS. * * Nothing here knows what an .ics looks like, and that is the point. This used to read the object's * text, patch it in the browser and put it back, which meant the browser carried its own copy of the * iCalendar escaping and 75-octet folding rules — a second place to be subtly wrong about a format * neither screen displays. That work moved to the sidecar (see sidecar/patch.ts), which held the only * other copy, and the agent skill now calls the same routes instead of writing a third one in bash. * * The guarantee it makes is unchanged: a patch replaces only the lines it owns, so alarms, attendees, * recurrence rules and every unknown `X-` property survive an edit made from a form that cannot show them. */ export type NewEvent = { summary: string; location?: string; /** An ISO instant for a timed event; `YYYY-MM-DD` when `allDay`. */ start: string; end?: string; allDay: boolean; }; export type NewContact = { fullName: string; emails?: string[]; phones?: string[]; org?: string; title?: string; }; /** What a patch may set. Absent leaves a field alone; `null` removes it. */ export type ItemPatch = { summary?: string; location?: string | null; description?: string | null; start?: string; end?: string; allDay?: boolean; fullName?: string; emails?: string[]; phones?: string[]; org?: string | null; title?: string | null; }; /** `busy` holds the href being written; a create has no href yet, so it gets a sentinel of its own. */ export const CREATING = 'creating'; export function useDavEdit(invalidateKey: unknown[]) { const client = useClient(); const qc = useQueryClient(); const [busy, setBusy] = useState(null); const [error, setError] = useState(null); const refresh = () => qc.invalidateQueries({ queryKey: invalidateKey }); /** Every verb has the same shape: mark busy, do it, refresh, and report rather than throw. */ const run = async (key: string, fallback: string, work: () => Promise) => { setBusy(key); setError(null); try { await work(); await refresh(); return true; } catch (err) { setError(err instanceof Error ? err.message : fallback); return false; } finally { setBusy(null); } }; return { busy, error, save: (href: string, patch: ItemPatch) => run(href, 'Could not save.', () => client.patch('/caldav/_officer/item', { href, ...patch })), remove: (href: string) => run(href, 'Could not delete.', () => client.delete('/caldav/_officer/item', { href })), createEvent: (collection: string, event: NewEvent) => run(CREATING, 'Could not create.', () => client.post('/caldav/_officer/event', { collection, ...event })), createContact: (collection: string, contact: NewContact) => run(CREATING, 'Could not create.', () => client.post('/caldav/_officer/contact', { collection, ...contact })), }; }