// Client-safe description of the health tables: used by the forms on /trackers/health // and by the server-side validator in #lib/server/health-import.ts. Keys are the // snake_case column names, which are also the import (JSON/CSV) field names. export type FieldType = 'date' | 'text' | 'textarea' | 'integer' | 'number' | 'datetime' | 'select' | 'uuid'; export type Field = { key: string; label: string; type: FieldType; required?: boolean; options?: readonly string[]; min?: number; max?: number; step?: number; maxLength?: number; placeholder?: string; }; export const medicationKinds = ['medication', 'supplement'] as const; export const appointmentTypes = [ 'appointment', 'checkup', 'condition', 'test', 'procedure', 'other' ] as const; const notes: Field = { key: 'notes', label: 'Notes', type: 'textarea', maxLength: 4000 }; const date: Field = { key: 'date', label: 'Date', type: 'date', required: true }; export const healthTables = { vaccinations: { label: 'Vaccinations', fields: [ { key: 'vaccine', label: 'Vaccine', type: 'text', required: true, maxLength: 200 }, { key: 'target', label: 'Disease / target', type: 'text', maxLength: 200 }, { key: 'dose_no', label: 'Dose no.', type: 'integer', min: 1, max: 20 }, date, { key: 'batch_lot', label: 'Batch / lot', type: 'text', maxLength: 100 }, { key: 'provider', label: 'Provider', type: 'text', maxLength: 200 }, { key: 'country', label: 'Country (ISO code)', type: 'text', maxLength: 2, placeholder: 'NL' }, notes ] }, body_measurements: { label: 'Body measurements', fields: [ date, { key: 'weight_kg', label: 'Weight (kg)', type: 'number', min: 20, max: 400, step: 0.01 }, { key: 'waist_cm', label: 'Waist at navel (cm)', type: 'number', min: 30, max: 250, step: 0.1 }, { key: 'body_fat_pct', label: 'Body fat (%)', type: 'number', min: 1, max: 75, step: 0.1 }, notes ] }, workouts: { label: 'Workouts', fields: [ date, { key: 'exercise', label: 'Exercise', type: 'text', required: true, maxLength: 200 }, { key: 'sets', label: 'Sets', type: 'integer', min: 1, max: 100 }, { key: 'reps', label: 'Reps (per set)', type: 'integer', min: 0, max: 1000 }, { key: 'load_kg', label: 'Load (kg)', type: 'number', min: 0, max: 1000, step: 0.25 }, { key: 'rpe', label: 'RPE', type: 'number', min: 1, max: 10, step: 0.5 }, { key: 'session_id', label: 'Session id (optional)', type: 'uuid' }, notes ] }, nutrition_daily: { label: 'Nutrition', fields: [ date, { key: 'kcal', label: 'kcal', type: 'integer', min: 0, max: 20000 }, { key: 'protein_g', label: 'Protein (g)', type: 'number', min: 0, max: 1000, step: 0.1 }, notes ] }, sleep: { label: 'Sleep', fields: [ { ...date, label: 'Date (wake-up day)' }, { key: 'bedtime', label: 'Bedtime', type: 'datetime', required: true }, { key: 'wake_time', label: 'Wake time', type: 'datetime', required: true }, notes ] }, medications: { label: 'Medications & supplements', fields: [ { key: 'name', label: 'Name', type: 'text', required: true, maxLength: 200 }, { key: 'kind', label: 'Kind', type: 'select', options: medicationKinds }, { key: 'dose', label: 'Dose', type: 'text', maxLength: 200, placeholder: '1 mg' }, { key: 'frequency', label: 'Frequency', type: 'text', maxLength: 200, placeholder: 'daily' }, { key: 'start_date', label: 'Start date', type: 'date' }, { key: 'stop_date', label: 'Stop date', type: 'date' }, notes ] }, appointments: { label: 'Appointments & conditions', fields: [ date, { key: 'type', label: 'Type', type: 'select', options: appointmentTypes }, { key: 'provider', label: 'Provider', type: 'text', maxLength: 200 }, notes ] } } as const satisfies Record; export type HealthTable = keyof typeof healthTables; export const healthTableNames = Object.keys(healthTables) as HealthTable[]; export const isHealthTable = (value: unknown): value is HealthTable => typeof value === 'string' && Object.hasOwn(healthTables, value);