Fix medication system and rename to Synculous.
- Add all 14 missing database tables (medications, med_logs, routines, etc.) - Rewrite medication scheduling: support specific days, every N days, as-needed (PRN) - Fix taken_times matching: match by created_at date, not scheduled_time string - Fix adherence calculation: taken / expected doses, not taken / (taken + skipped) - Add formatSchedule() helper for readable display - Update client types and API layer - Rename brilli-ins-client → synculous-client - Make client PWA: add manifest, service worker, icons - Bind dev server to 0.0.0.0 for network access - Fix SVG icon bugs in Icons.tsx - Add .dockerignore for client npm caching Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
697
synculous-client/src/lib/api.ts
Normal file
697
synculous-client/src/lib/api.ts
Normal file
@@ -0,0 +1,697 @@
|
||||
const API_URL = '';
|
||||
|
||||
function getToken(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem('token');
|
||||
}
|
||||
|
||||
function setToken(token: string): void {
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
|
||||
function clearToken(): void {
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const token = getToken();
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_URL}${endpoint}`, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Request failed' }));
|
||||
throw new Error(error.error || 'Request failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
// Auth
|
||||
auth: {
|
||||
login: async (username: string, password: string) => {
|
||||
const result = await request<{ token: string }>('/api/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
setToken(result.token);
|
||||
return result;
|
||||
},
|
||||
|
||||
register: async (username: string, password: string) => {
|
||||
return request<{ success: boolean }>('/api/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
clearToken();
|
||||
},
|
||||
|
||||
getToken,
|
||||
},
|
||||
|
||||
// User
|
||||
user: {
|
||||
get: async (userId: string) => {
|
||||
return request<{ id: string; username: string; created_at: string }>(
|
||||
`/api/user/${userId}`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
},
|
||||
|
||||
getUUID: async (username: string) => {
|
||||
return request<{ id: string }>(`/api/getUserUUID/${username}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
},
|
||||
|
||||
update: async (userId: string, data: Record<string, unknown>) => {
|
||||
return request<{ success: boolean }>(`/api/user/${userId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
// Routines
|
||||
routines: {
|
||||
list: async () => {
|
||||
return request<Array<{
|
||||
id: string;
|
||||
user_uuid: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
created_at: string;
|
||||
}>>('/api/routines', { method: 'GET' });
|
||||
},
|
||||
|
||||
get: async (routineId: string) => {
|
||||
return request<{
|
||||
routine: {
|
||||
id: string;
|
||||
user_uuid: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
};
|
||||
steps: Array<{
|
||||
id: string;
|
||||
routine_id: string;
|
||||
name: string;
|
||||
instructions?: string;
|
||||
step_type: string;
|
||||
duration_minutes?: number;
|
||||
media_url?: string;
|
||||
position: number;
|
||||
}>;
|
||||
}>(`/api/routines/${routineId}`, { method: 'GET' });
|
||||
},
|
||||
|
||||
create: async (data: { name: string; description?: string; icon?: string }) => {
|
||||
return request<{ id: string }>('/api/routines', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
},
|
||||
|
||||
update: async (routineId: string, data: Record<string, unknown>) => {
|
||||
return request<{ id: string }>(`/api/routines/${routineId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
},
|
||||
|
||||
delete: async (routineId: string) => {
|
||||
return request<{ deleted: boolean }>(`/api/routines/${routineId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
},
|
||||
|
||||
// Steps
|
||||
getSteps: async (routineId: string) => {
|
||||
return request<Array<{
|
||||
id: string;
|
||||
routine_id: string;
|
||||
name: string;
|
||||
instructions?: string;
|
||||
step_type: string;
|
||||
duration_minutes?: number;
|
||||
media_url?: string;
|
||||
position: number;
|
||||
}>>(`/api/routines/${routineId}/steps`, { method: 'GET' });
|
||||
},
|
||||
|
||||
addStep: async (
|
||||
routineId: string,
|
||||
data: { name: string; duration_minutes?: number; position?: number }
|
||||
) => {
|
||||
return request<{ id: string }>(`/api/routines/${routineId}/steps`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
},
|
||||
|
||||
updateStep: async (
|
||||
routineId: string,
|
||||
stepId: string,
|
||||
data: Record<string, unknown>
|
||||
) => {
|
||||
return request<{ id: string }>(`/api/routines/${routineId}/steps/${stepId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
},
|
||||
|
||||
deleteStep: async (routineId: string, stepId: string) => {
|
||||
return request<{ deleted: boolean }>(
|
||||
`/api/routines/${routineId}/steps/${stepId}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
},
|
||||
|
||||
reorderSteps: async (routineId: string, stepIds: string[]) => {
|
||||
return request<Array<{ id: string }>>(
|
||||
`/api/routines/${routineId}/steps/reorder`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ step_ids: stepIds }),
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
// Step extended
|
||||
updateStepInstructions: async (
|
||||
routineId: string,
|
||||
stepId: string,
|
||||
instructions: string
|
||||
) => {
|
||||
return request<{ id: string }>(
|
||||
`/api/routines/${routineId}/steps/${stepId}/instructions`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ instructions }),
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
updateStepType: async (
|
||||
routineId: string,
|
||||
stepId: string,
|
||||
stepType: string
|
||||
) => {
|
||||
return request<{ id: string }>(
|
||||
`/api/routines/${routineId}/steps/${stepId}/type`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ step_type: stepType }),
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
updateStepMedia: async (
|
||||
routineId: string,
|
||||
stepId: string,
|
||||
mediaUrl: string
|
||||
) => {
|
||||
return request<{ id: string }>(
|
||||
`/api/routines/${routineId}/steps/${stepId}/media`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ media_url: mediaUrl }),
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
// Scheduling
|
||||
getSchedule: async (routineId: string) => {
|
||||
return request<{
|
||||
id: string;
|
||||
routine_id: string;
|
||||
days: string[];
|
||||
time: string;
|
||||
remind: boolean;
|
||||
}>(`/api/routines/${routineId}/schedule`, { method: 'GET' });
|
||||
},
|
||||
|
||||
setSchedule: async (
|
||||
routineId: string,
|
||||
data: { days: string[]; time: string; remind?: boolean }
|
||||
) => {
|
||||
return request<{ id: string }>(`/api/routines/${routineId}/schedule`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
},
|
||||
|
||||
deleteSchedule: async (routineId: string) => {
|
||||
return request<{ deleted: boolean }>(
|
||||
`/api/routines/${routineId}/schedule`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
},
|
||||
|
||||
// History
|
||||
getHistory: async (routineId: string, days = 7) => {
|
||||
return request<Array<{
|
||||
id: string;
|
||||
routine_id: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
completed_at?: string;
|
||||
}>>(`/api/routines/${routineId}/history?days=${days}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
},
|
||||
|
||||
// Stats
|
||||
getStats: async (routineId: string, days = 30) => {
|
||||
return request<{
|
||||
routine_id: string;
|
||||
routine_name: string;
|
||||
period_days: number;
|
||||
total_sessions: number;
|
||||
completed: number;
|
||||
aborted: number;
|
||||
completion_rate_percent: number;
|
||||
avg_duration_minutes: number;
|
||||
total_time_minutes: number;
|
||||
}>(`/api/routines/${routineId}/stats?days=${days}`, { method: 'GET' });
|
||||
},
|
||||
|
||||
getStreak: async (routineId: string) => {
|
||||
return request<{
|
||||
routine_id: string;
|
||||
routine_name: string;
|
||||
current_streak: number;
|
||||
longest_streak: number;
|
||||
last_completed_date?: string;
|
||||
}>(`/api/routines/${routineId}/streak`, { method: 'GET' });
|
||||
},
|
||||
|
||||
// Tags
|
||||
getTags: async (routineId: string) => {
|
||||
return request<Array<{ id: string; name: string; color: string }>>(
|
||||
`/api/routines/${routineId}/tags`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
},
|
||||
|
||||
addTags: async (routineId: string, tagIds: string[]) => {
|
||||
return request<Array<{ id: string; name: string; color: string }>>(
|
||||
`/api/routines/${routineId}/tags`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ tag_ids: tagIds }),
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
removeTag: async (routineId: string, tagId: string) => {
|
||||
return request<{ removed: boolean }>(
|
||||
`/api/routines/${routineId}/tags/${tagId}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
// Sessions
|
||||
sessions: {
|
||||
start: async (routineId: string) => {
|
||||
return request<{
|
||||
session: { id: string; status: string };
|
||||
current_step: {
|
||||
id: string;
|
||||
name: string;
|
||||
instructions?: string;
|
||||
step_type: string;
|
||||
duration_minutes?: number;
|
||||
position: number;
|
||||
};
|
||||
}>(`/api/routines/${routineId}/start`, { method: 'POST' });
|
||||
},
|
||||
|
||||
getActive: async () => {
|
||||
return request<{
|
||||
session: {
|
||||
id: string;
|
||||
routine_id: string;
|
||||
status: string;
|
||||
current_step_index: number;
|
||||
};
|
||||
routine: { id: string; name: string; icon?: string };
|
||||
current_step: {
|
||||
id: string;
|
||||
name: string;
|
||||
instructions?: string;
|
||||
step_type: string;
|
||||
duration_minutes?: number;
|
||||
position: number;
|
||||
} | null;
|
||||
}>('/api/sessions/active', { method: 'GET' });
|
||||
},
|
||||
|
||||
completeStep: async (sessionId: string, stepId: string) => {
|
||||
return request<{
|
||||
session: { status: string; current_step_index?: number };
|
||||
next_step: {
|
||||
id: string;
|
||||
name: string;
|
||||
instructions?: string;
|
||||
step_type: string;
|
||||
duration_minutes?: number;
|
||||
position: number;
|
||||
} | null;
|
||||
}>(`/api/sessions/${sessionId}/complete-step`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ step_id: stepId }),
|
||||
});
|
||||
},
|
||||
|
||||
skipStep: async (sessionId: string, stepId: string) => {
|
||||
return request<{
|
||||
session: { status: string; current_step_index?: number };
|
||||
next_step: {
|
||||
id: string;
|
||||
name: string;
|
||||
instructions?: string;
|
||||
step_type: string;
|
||||
duration_minutes?: number;
|
||||
position: number;
|
||||
} | null;
|
||||
}>(`/api/sessions/${sessionId}/skip-step`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ step_id: stepId }),
|
||||
});
|
||||
},
|
||||
|
||||
pause: async (sessionId: string) => {
|
||||
return request<{ status: string }>(`/api/sessions/${sessionId}/pause`, {
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
|
||||
resume: async (sessionId: string) => {
|
||||
return request<{ status: string }>(`/api/sessions/${sessionId}/resume`, {
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
|
||||
cancel: async (sessionId: string) => {
|
||||
return request<{ status: string }>(`/api/sessions/${sessionId}/cancel`, {
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
|
||||
abort: async (sessionId: string, reason?: string) => {
|
||||
return request<{ status: string; reason: string }>(
|
||||
`/api/sessions/${sessionId}/abort`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ reason }),
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
addNote: async (
|
||||
sessionId: string,
|
||||
stepIndex: number | undefined,
|
||||
note: string
|
||||
) => {
|
||||
return request<{ id: string }>(`/api/sessions/${sessionId}/note`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ step_index: stepIndex, note }),
|
||||
});
|
||||
},
|
||||
|
||||
setDuration: async (sessionId: string, durationMinutes: number) => {
|
||||
return request<{ id: string }>(`/api/sessions/${sessionId}/duration`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ actual_duration_minutes: durationMinutes }),
|
||||
});
|
||||
},
|
||||
|
||||
getDetails: async (sessionId: string) => {
|
||||
return request<{
|
||||
session: {
|
||||
id: string;
|
||||
routine_id: string;
|
||||
status: string;
|
||||
current_step_index: number;
|
||||
};
|
||||
routine: { id: string; name: string };
|
||||
steps: Array<{ id: string; name: string; position: number }>;
|
||||
notes: Array<{ id: string; step_index?: number; note: string }>;
|
||||
}>(`/api/sessions/${sessionId}`, { method: 'GET' });
|
||||
},
|
||||
},
|
||||
|
||||
// Templates
|
||||
templates: {
|
||||
list: async () => {
|
||||
return request<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
step_count: number;
|
||||
}>>('/api/templates', { method: 'GET' });
|
||||
},
|
||||
|
||||
get: async (templateId: string) => {
|
||||
return request<{
|
||||
template: {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
};
|
||||
steps: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
instructions?: string;
|
||||
step_type: string;
|
||||
duration_minutes?: number;
|
||||
position: number;
|
||||
}>;
|
||||
}>(`/api/templates/${templateId}`, { method: 'GET' });
|
||||
},
|
||||
|
||||
clone: async (templateId: string) => {
|
||||
return request<{ id: string }>(`/api/templates/${templateId}/clone`, {
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
// Tags
|
||||
tags: {
|
||||
list: async () => {
|
||||
return request<Array<{ id: string; name: string; color: string }>>(
|
||||
'/api/tags',
|
||||
{ method: 'GET' }
|
||||
);
|
||||
},
|
||||
|
||||
create: async (data: { name: string; color?: string }) => {
|
||||
return request<{ id: string }>('/api/tags', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
},
|
||||
|
||||
delete: async (tagId: string) => {
|
||||
return request<{ deleted: boolean }>(`/api/tags/${tagId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
// Stats
|
||||
stats: {
|
||||
getWeeklySummary: async () => {
|
||||
return request<{
|
||||
total_completed: number;
|
||||
total_time_minutes: number;
|
||||
routines_started: number;
|
||||
routines: Array<{
|
||||
routine_id: string;
|
||||
name: string;
|
||||
completed_this_week: number;
|
||||
}>;
|
||||
}>('/api/routines/weekly-summary', { method: 'GET' });
|
||||
},
|
||||
|
||||
getStreaks: async () => {
|
||||
return request<Array<{
|
||||
routine_id: string;
|
||||
routine_name: string;
|
||||
current_streak: number;
|
||||
longest_streak: number;
|
||||
last_completed_date?: string;
|
||||
}>>('/api/routines/streaks', { method: 'GET' });
|
||||
},
|
||||
},
|
||||
|
||||
// Medications
|
||||
medications: {
|
||||
list: async () => {
|
||||
return request<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
dosage: string;
|
||||
unit: string;
|
||||
frequency: string;
|
||||
times: string[];
|
||||
notes?: string;
|
||||
active: boolean;
|
||||
quantity_remaining?: number;
|
||||
refill_date?: string;
|
||||
}>>('/api/medications', { method: 'GET' });
|
||||
},
|
||||
|
||||
get: async (medId: string) => {
|
||||
return request<{
|
||||
id: string;
|
||||
name: string;
|
||||
dosage: string;
|
||||
unit: string;
|
||||
frequency: string;
|
||||
times: string[];
|
||||
notes?: string;
|
||||
active: boolean;
|
||||
quantity_remaining?: number;
|
||||
refill_date?: string;
|
||||
}>(`/api/medications/${medId}`, { method: 'GET' });
|
||||
},
|
||||
|
||||
create: async (data: {
|
||||
name: string;
|
||||
dosage: string;
|
||||
unit: string;
|
||||
frequency: string;
|
||||
times?: string[];
|
||||
days_of_week?: string[];
|
||||
interval_days?: number;
|
||||
start_date?: string;
|
||||
notes?: string;
|
||||
}) => {
|
||||
return request<{ id: string }>('/api/medications', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
},
|
||||
|
||||
update: async (medId: string, data: Record<string, unknown>) => {
|
||||
return request<{ id: string }>(`/api/medications/${medId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
},
|
||||
|
||||
delete: async (medId: string) => {
|
||||
return request<{ deleted: boolean }>(`/api/medications/${medId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
},
|
||||
|
||||
take: async (medId: string, scheduledTime?: string, notes?: string) => {
|
||||
return request<{ id: string }>(`/api/medications/${medId}/take`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ scheduled_time: scheduledTime, notes }),
|
||||
});
|
||||
},
|
||||
|
||||
skip: async (medId: string, scheduledTime?: string, reason?: string) => {
|
||||
return request<{ id: string }>(`/api/medications/${medId}/skip`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ scheduled_time: scheduledTime, reason }),
|
||||
});
|
||||
},
|
||||
|
||||
snooze: async (medId: string, minutes = 15) => {
|
||||
return request<{ snoozed_until_minutes: number }>(
|
||||
`/api/medications/${medId}/snooze`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ minutes }),
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
getLog: async (medId: string, days = 30) => {
|
||||
return request<Array<{
|
||||
id: string;
|
||||
medication_id: string;
|
||||
action: string;
|
||||
scheduled_time?: string;
|
||||
notes?: string;
|
||||
created_at: string;
|
||||
}>>(`/api/medications/${medId}/log?days=${days}`, { method: 'GET' });
|
||||
},
|
||||
|
||||
getToday: async () => {
|
||||
return request<Array<{
|
||||
medication: {
|
||||
id: string;
|
||||
name: string;
|
||||
dosage: string;
|
||||
unit: string;
|
||||
};
|
||||
scheduled_times: string[];
|
||||
taken_times: string[];
|
||||
}>>('/api/medications/today', { method: 'GET' });
|
||||
},
|
||||
|
||||
getAdherence: async (days = 30) => {
|
||||
return request<Array<{
|
||||
medication_id: string;
|
||||
name: string;
|
||||
taken: number;
|
||||
skipped: number;
|
||||
adherence_percent: number;
|
||||
}>>(`/api/medications/adherence?days=${days}`, { method: 'GET' });
|
||||
},
|
||||
|
||||
getRefillsDue: async (daysAhead = 7) => {
|
||||
return request<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
dosage: string;
|
||||
quantity_remaining?: number;
|
||||
refill_date?: string;
|
||||
}>>(`/api/medications/refills-due?days_ahead=${daysAhead}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
},
|
||||
|
||||
setRefill: async (
|
||||
medId: string,
|
||||
data: {
|
||||
quantity_remaining?: number;
|
||||
refill_date?: string;
|
||||
pharmacy_notes?: string;
|
||||
}
|
||||
) => {
|
||||
return request<{ id: string }>(`/api/medications/${medId}/refill`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
Reference in New Issue
Block a user