Files
parentcalendar/frontend/src/lib/api.ts
T

82 lines
4.1 KiB
TypeScript

export type Kind = 'no_school' | 'early_release' | 'event' | 'game' | 'practice' | 'deadline' | 'pack_lunch';
export interface Child { id: number; display_name: string; color: string }
export interface Calendar {
id: number; name: string; color: string; child_ids: number[]; source_ids: number[];
alarm_minutes: number | null; last_exported_at: string | null; feed_token: string;
}
export interface Source {
id: number; name: string; layout_type: 'month_grid' | 'menu' | 'list'; double_read: boolean;
ignore_labels: string[]; applies_filter: string | null; prompt_notes: string | null;
default_calendar_id: number | null; child_ids: number[];
}
export interface Household {
timezone: string; show_scan_stats: boolean; public_url: string;
about: { version: string; commit: string; repo_url: string };
children: Child[]; calendars: Calendar[]; sources: Source[];
}
export interface CalEvent {
id: number; uid: string; scan_id: number | null; source_id: number | null;
date: string; end_date: string | null; all_day: boolean; start_time: string | null; end_time: string | null;
title: string; notes: string | null; location: string | null; kind: Kind;
applies: boolean; include: boolean; page: number | null; cell_bbox: number[] | null;
source_text: string | null; confidence: number | null; flags: string[];
status: 'draft' | 'confirmed' | 'deleted'; match: 'new' | 'updated' | 'unchanged' | null;
replaces_id: number | null; sequence: number; child_ids: number[];
replaces?: Partial<CalEvent> | null; // in review: the existing event this draft would update
}
export interface MenuItem {
id: number; scan_id: number; source_id: number; date: string; items: string[]; no_lunch: boolean;
page: number | null; cell_bbox: number[] | null; flags: string[]; source_name?: string;
reads: MenuRead[] | null; // both readings, when a double read disagreed
}
export interface MenuRead { items: string[]; no_lunch: boolean }
export interface Scan {
id: number; source_id: number; source_name: string; layout_type: Source['layout_type']; hint_month: string;
date_start: string | null; date_end: string | null; image_keys: string[];
text: string | null; // pasted text (an email) instead of photos
status: 'queued' | 'parsed' | 'failed' | 'abandoned' | 'reviewed' | 'published';
error: string | null; seconds: number | null; model: string | null; cost_tokens: number | null; created_at: string; published_at: string | null;
events: CalEvent[]; menu_items: MenuItem[]; can_undo: boolean;
}
export interface ScanSummary {
id: number; source_id: number; source_name: string; hint_month: string; status: Scan['status']; error: string | null;
created_at: string; event_count: number;
}
export interface LunchDay {
date: string; menus: MenuItem[]; closed: string | null; no_lunch: boolean;
greyed: boolean; picked: boolean; warning: string | null;
}
/** What the event editor saves. */
export type EventValues = {
title: string; date: string; end_date: string | null; all_day: boolean; start_time: string | null;
end_time: string | null; kind: Kind; notes: string | null; location: string | null; child_ids: number[];
};
export const KINDS: Record<Kind, string> = {
event: 'Event', no_school: 'No school', early_release: 'Early release', deadline: 'Deadline',
game: 'Game', practice: 'Practice', pack_lunch: 'Pack lunch',
};
function message(body: any, fallback: string): string {
const d = body?.detail;
if (typeof d === 'string') return d;
if (Array.isArray(d)) return d.map((x) => `${(x.loc ?? []).slice(1).join('.')}: ${x.msg}`).join('; ');
return fallback;
}
/** fetch() for /api: JSON in and out, and a readable Error when the server says no. */
export async function api<T>(path: string, init: RequestInit & { json?: unknown } = {}): Promise<T> {
const { json, ...rest } = init;
if (json !== undefined) {
rest.body = JSON.stringify(json);
rest.headers = { 'Content-Type': 'application/json' };
}
const res = await fetch('/api' + path, rest);
const body = await res.json().catch(() => null);
if (!res.ok) throw new Error(message(body, `${res.status} ${res.statusText}`));
return body as T;
}