- DB: tasks table with scheduled_datetime, reminder_minutes_before, advance_notified, status - API: CRUD routes GET/POST /api/tasks, PATCH/DELETE /api/tasks/<id> - Scheduler: check_task_reminders() fires advance + at-time notifications, tracks advance_notified to prevent double-fire - Bot: handle_task() with add/list/done/cancel/delete actions + datetime resolution helper - AI: task interaction type + examples added to command_parser - Web: task list page with overdue/notified color coding + new task form with datetime-local picker - Nav: replaced Templates with Tasks in bottom nav Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1089 lines
29 KiB
TypeScript
1089 lines
29 KiB
TypeScript
const API_URL = '';
|
|
|
|
export interface Task {
|
|
id: string;
|
|
user_uuid: string;
|
|
title: string;
|
|
description?: string;
|
|
scheduled_datetime: string;
|
|
reminder_minutes_before: number;
|
|
advance_notified: boolean;
|
|
status: 'pending' | 'notified' | 'completed' | 'cancelled';
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
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');
|
|
localStorage.removeItem('refresh_token');
|
|
}
|
|
|
|
function getRefreshToken(): string | null {
|
|
if (typeof window === 'undefined') return null;
|
|
return localStorage.getItem('refresh_token');
|
|
}
|
|
|
|
function setRefreshToken(token: string): void {
|
|
localStorage.setItem('refresh_token', token);
|
|
}
|
|
|
|
let refreshPromise: Promise<boolean> | null = null;
|
|
|
|
async function tryRefreshToken(): Promise<boolean> {
|
|
// Deduplicate concurrent refresh attempts
|
|
if (refreshPromise) return refreshPromise;
|
|
|
|
refreshPromise = (async () => {
|
|
const refreshToken = getRefreshToken();
|
|
if (!refreshToken) return false;
|
|
try {
|
|
const resp = await fetch(`${API_URL}/api/refresh`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
});
|
|
if (resp.ok) {
|
|
const data = await resp.json();
|
|
if (data.token) {
|
|
setToken(data.token);
|
|
return true;
|
|
}
|
|
}
|
|
// Refresh token is invalid/expired - clear everything
|
|
clearToken();
|
|
return false;
|
|
} catch {
|
|
return false;
|
|
} finally {
|
|
refreshPromise = null;
|
|
}
|
|
})();
|
|
|
|
return refreshPromise;
|
|
}
|
|
|
|
async function request<T>(
|
|
endpoint: string,
|
|
options: RequestInit = {},
|
|
_retried = false,
|
|
): Promise<T> {
|
|
const token = getToken();
|
|
const headers: HeadersInit = {
|
|
'Content-Type': 'application/json',
|
|
'X-Timezone-Offset': String(new Date().getTimezoneOffset()),
|
|
'X-Timezone-Name': Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
...options.headers,
|
|
};
|
|
|
|
const response = await fetch(`${API_URL}${endpoint}`, {
|
|
...options,
|
|
headers,
|
|
});
|
|
|
|
// Auto-refresh on 401
|
|
if (response.status === 401 && !_retried) {
|
|
const refreshed = await tryRefreshToken();
|
|
if (refreshed) {
|
|
return request<T>(endpoint, options, true);
|
|
}
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const body = await response.text();
|
|
let errorMsg = 'Request failed';
|
|
try {
|
|
const error = JSON.parse(body);
|
|
errorMsg = error.error || error.message || body;
|
|
} catch {
|
|
errorMsg = body || errorMsg;
|
|
}
|
|
throw new Error(errorMsg);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
export const api = {
|
|
// Auth
|
|
auth: {
|
|
login: async (username: string, password: string, trustDevice = false) => {
|
|
const result = await request<{ token: string; refresh_token?: string }>('/api/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ username, password, trust_device: trustDevice }),
|
|
});
|
|
setToken(result.token);
|
|
if (result.refresh_token) {
|
|
setRefreshToken(result.refresh_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' }
|
|
);
|
|
},
|
|
|
|
listAllSchedules: async () => {
|
|
return request<Array<{
|
|
routine_id: string;
|
|
routine_name: string;
|
|
routine_icon: string;
|
|
days: string[];
|
|
time: string;
|
|
remind: boolean;
|
|
total_duration_minutes: number;
|
|
}>>('/api/routines/schedules', { method: 'GET' });
|
|
},
|
|
|
|
// 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;
|
|
celebration?: {
|
|
streak_current: number;
|
|
streak_longest: number;
|
|
session_duration_minutes: number;
|
|
total_completions: number;
|
|
steps_completed: number;
|
|
steps_skipped: number;
|
|
};
|
|
}>(`/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;
|
|
celebration?: {
|
|
streak_current: number;
|
|
streak_longest: number;
|
|
session_duration_minutes: number;
|
|
total_completions: number;
|
|
steps_completed: number;
|
|
steps_skipped: number;
|
|
};
|
|
}>(`/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' });
|
|
},
|
|
},
|
|
|
|
// Victories
|
|
victories: {
|
|
get: async (days = 30) => {
|
|
return request<Array<{
|
|
type: string;
|
|
message: string;
|
|
date?: string;
|
|
}>>(`/api/victories?days=${days}`, { method: 'GET' });
|
|
},
|
|
},
|
|
|
|
// Rewards
|
|
rewards: {
|
|
getRandom: async (context = 'completion') => {
|
|
return request<{
|
|
reward: {
|
|
id: string;
|
|
category: string;
|
|
content: string;
|
|
emoji?: string;
|
|
rarity: string;
|
|
} | null;
|
|
}>(`/api/rewards/random?context=${context}`, { method: 'GET' });
|
|
},
|
|
|
|
getHistory: async () => {
|
|
return request<Array<{
|
|
earned_at: string;
|
|
context?: string;
|
|
category: string;
|
|
content: string;
|
|
emoji?: string;
|
|
rarity?: string;
|
|
}>>('/api/rewards/history', { method: 'GET' });
|
|
},
|
|
},
|
|
|
|
// Preferences
|
|
preferences: {
|
|
get: async () => {
|
|
return request<{
|
|
sound_enabled: boolean;
|
|
haptic_enabled: boolean;
|
|
show_launch_screen: boolean;
|
|
celebration_style: string;
|
|
}>('/api/preferences', { method: 'GET' });
|
|
},
|
|
|
|
update: async (data: {
|
|
sound_enabled?: boolean;
|
|
haptic_enabled?: boolean;
|
|
show_launch_screen?: boolean;
|
|
celebration_style?: string;
|
|
timezone_offset?: number;
|
|
timezone_name?: string;
|
|
}) => {
|
|
return request<Record<string, unknown>>('/api/preferences', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
},
|
|
|
|
// Notifications
|
|
notifications: {
|
|
getVapidPublicKey: async () => {
|
|
return request<{ public_key: string }>('/api/notifications/vapid-public-key', {
|
|
method: 'GET',
|
|
});
|
|
},
|
|
|
|
subscribe: async (subscription: PushSubscriptionJSON) => {
|
|
return request<{ subscribed: boolean }>('/api/notifications/subscribe', {
|
|
method: 'POST',
|
|
body: JSON.stringify(subscription),
|
|
});
|
|
},
|
|
|
|
unsubscribe: async (endpoint: string) => {
|
|
return request<{ unsubscribed: boolean }>('/api/notifications/subscribe', {
|
|
method: 'DELETE',
|
|
body: JSON.stringify({ endpoint }),
|
|
});
|
|
},
|
|
|
|
getSettings: async () => {
|
|
return request<{
|
|
discord_user_id: string;
|
|
discord_enabled: boolean;
|
|
ntfy_topic: string;
|
|
ntfy_enabled: boolean;
|
|
web_push_enabled: boolean;
|
|
}>('/api/notifications/settings', { method: 'GET' });
|
|
},
|
|
|
|
updateSettings: async (data: {
|
|
discord_user_id?: string;
|
|
discord_enabled?: boolean;
|
|
ntfy_topic?: string;
|
|
ntfy_enabled?: boolean;
|
|
}) => {
|
|
return request<{ updated: boolean }>('/api/notifications/settings', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
},
|
|
|
|
// Adaptive Medications
|
|
adaptiveMeds: {
|
|
getSettings: async () => {
|
|
return request<{
|
|
adaptive_timing_enabled: boolean;
|
|
adaptive_mode: string;
|
|
presence_tracking_enabled: boolean;
|
|
nagging_enabled: boolean;
|
|
nag_interval_minutes: number;
|
|
max_nag_count: number;
|
|
quiet_hours_start: string | null;
|
|
quiet_hours_end: string | null;
|
|
}>('/api/adaptive-meds/settings', { method: 'GET' });
|
|
},
|
|
|
|
updateSettings: async (data: {
|
|
adaptive_timing_enabled?: boolean;
|
|
adaptive_mode?: string;
|
|
presence_tracking_enabled?: boolean;
|
|
nagging_enabled?: boolean;
|
|
nag_interval_minutes?: number;
|
|
max_nag_count?: number;
|
|
quiet_hours_start?: string | null;
|
|
quiet_hours_end?: string | null;
|
|
}) => {
|
|
return request<{ success: boolean }>('/api/adaptive-meds/settings', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
getPresence: async () => {
|
|
return request<{
|
|
is_online: boolean;
|
|
last_online_at: string | null;
|
|
typical_wake_time: string | null;
|
|
}>('/api/adaptive-meds/presence', { method: 'GET' });
|
|
},
|
|
|
|
getSchedule: async () => {
|
|
return request<Array<{
|
|
medication_id: string;
|
|
medication_name: string;
|
|
base_time: string;
|
|
adjusted_time: string;
|
|
adjustment_minutes: number;
|
|
status: string;
|
|
nag_count: number;
|
|
}>>('/api/adaptive-meds/schedule', { method: 'GET' });
|
|
},
|
|
},
|
|
|
|
// Snitch System
|
|
snitch: {
|
|
getSettings: async () => {
|
|
return request<{
|
|
snitch_enabled: boolean;
|
|
trigger_after_nags: number;
|
|
trigger_after_missed_doses: number;
|
|
max_snitches_per_day: number;
|
|
require_consent: boolean;
|
|
consent_given: boolean;
|
|
snitch_cooldown_hours: number;
|
|
}>('/api/snitch/settings', { method: 'GET' });
|
|
},
|
|
|
|
updateSettings: async (data: {
|
|
snitch_enabled?: boolean;
|
|
trigger_after_nags?: number;
|
|
trigger_after_missed_doses?: number;
|
|
max_snitches_per_day?: number;
|
|
require_consent?: boolean;
|
|
consent_given?: boolean;
|
|
snitch_cooldown_hours?: number;
|
|
}) => {
|
|
return request<{ success: boolean }>('/api/snitch/settings', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
giveConsent: async (consent_given: boolean) => {
|
|
return request<{ success: boolean; consent_given: boolean }>('/api/snitch/consent', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ consent_given }),
|
|
});
|
|
},
|
|
|
|
getContacts: async () => {
|
|
return request<Array<{
|
|
id: string;
|
|
contact_name: string;
|
|
contact_type: string;
|
|
contact_value: string;
|
|
priority: number;
|
|
notify_all: boolean;
|
|
is_active: boolean;
|
|
}>>('/api/snitch/contacts', { method: 'GET' });
|
|
},
|
|
|
|
addContact: async (data: {
|
|
contact_name: string;
|
|
contact_type: string;
|
|
contact_value: string;
|
|
priority?: number;
|
|
notify_all?: boolean;
|
|
is_active?: boolean;
|
|
}) => {
|
|
return request<{ success: boolean; contact_id: string }>('/api/snitch/contacts', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
updateContact: async (contactId: string, data: {
|
|
contact_name?: string;
|
|
contact_type?: string;
|
|
contact_value?: string;
|
|
priority?: number;
|
|
notify_all?: boolean;
|
|
is_active?: boolean;
|
|
}) => {
|
|
return request<{ success: boolean }>(`/api/snitch/contacts/${contactId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
deleteContact: async (contactId: string) => {
|
|
return request<{ success: boolean }>(`/api/snitch/contacts/${contactId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
},
|
|
|
|
getHistory: async (days?: number) => {
|
|
return request<Array<{
|
|
id: string;
|
|
contact_id: string;
|
|
medication_id: string;
|
|
trigger_reason: string;
|
|
snitch_count_today: number;
|
|
sent_at: string;
|
|
delivered: boolean;
|
|
}>>(`/api/snitch/history?days=${days || 7}`, { method: 'GET' });
|
|
},
|
|
|
|
test: async () => {
|
|
return request<{ success: boolean; message: string }>('/api/snitch/test', {
|
|
method: 'POST',
|
|
});
|
|
},
|
|
},
|
|
|
|
// 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[];
|
|
skipped_times?: string[];
|
|
is_prn?: boolean;
|
|
is_next_day?: boolean;
|
|
is_previous_day?: boolean;
|
|
}>>('/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),
|
|
});
|
|
},
|
|
},
|
|
|
|
tasks: {
|
|
list: (status = 'pending') =>
|
|
request<Task[]>(`/api/tasks?status=${status}`, { method: 'GET' }),
|
|
create: (data: { title: string; description?: string; scheduled_datetime: string; reminder_minutes_before?: number }) =>
|
|
request<Task>('/api/tasks', { method: 'POST', body: JSON.stringify(data) }),
|
|
update: (id: string, data: Partial<Pick<Task, 'title' | 'description' | 'scheduled_datetime' | 'reminder_minutes_before' | 'status'>>) =>
|
|
request<Task>(`/api/tasks/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
|
|
delete: (id: string) =>
|
|
request<{ success: boolean }>(`/api/tasks/${id}`, { method: 'DELETE' }),
|
|
},
|
|
|
|
ai: {
|
|
generateSteps: (goal: string) =>
|
|
request<{ steps: { name: string; duration_minutes: number }[] }>(
|
|
'/api/ai/generate-steps',
|
|
{ method: 'POST', body: JSON.stringify({ goal }) }
|
|
),
|
|
},
|
|
};
|
|
|
|
export default api;
|