All checks were successful
Deploy production / deploy (push) Successful in 2m22s
Regenerated the (not yet deployed) health migration without hair_progress.
130 lines
4.1 KiB
TypeScript
130 lines
4.1 KiB
TypeScript
// 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<string, { label: string; fields: readonly Field[] }>;
|
|
|
|
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);
|