82 lines
4.1 KiB
TypeScript
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;
|
|
}
|