349 lines
12 KiB
TypeScript
349 lines
12 KiB
TypeScript
import { browser } from "$app/environment";
|
||
import { PUBLIC_API_BASE } from "$env/static/public";
|
||
import type {
|
||
ActiveIngredient,
|
||
BatchSuggestion,
|
||
GroomingSchedule,
|
||
LabResult,
|
||
MedicationEntry,
|
||
MedicationUsage,
|
||
PartOfDay,
|
||
Product,
|
||
ProductContext,
|
||
ProductEffectProfile,
|
||
ProductInteraction,
|
||
ProductInventory,
|
||
Routine,
|
||
RoutineSuggestion,
|
||
RoutineStep,
|
||
SkinConditionSnapshot,
|
||
} from "./types";
|
||
|
||
// ─── Core fetch helpers ──────────────────────────────────────────────────────
|
||
|
||
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||
// Server-side uses PUBLIC_API_BASE (e.g. http://localhost:8000).
|
||
// Browser-side uses /api so nginx proxies the request on the correct host.
|
||
const base = browser ? "/api" : PUBLIC_API_BASE;
|
||
const url = `${base}${path}`;
|
||
const res = await fetch(url, {
|
||
headers: { "Content-Type": "application/json", ...init.headers },
|
||
...init,
|
||
});
|
||
if (!res.ok) {
|
||
const detail = await res.json().catch(() => ({ detail: res.statusText }));
|
||
throw new Error(detail?.detail ?? res.statusText);
|
||
}
|
||
if (res.status === 204) return undefined as T;
|
||
return res.json();
|
||
}
|
||
|
||
export const api = {
|
||
get: <T>(path: string) => request<T>(path),
|
||
post: <T>(path: string, body: unknown) =>
|
||
request<T>(path, { method: "POST", body: JSON.stringify(body) }),
|
||
patch: <T>(path: string, body: unknown) =>
|
||
request<T>(path, { method: "PATCH", body: JSON.stringify(body) }),
|
||
del: (path: string) => request<void>(path, { method: "DELETE" }),
|
||
};
|
||
|
||
// ─── Products ────────────────────────────────────────────────────────────────
|
||
|
||
export interface ProductListParams {
|
||
category?: string;
|
||
brand?: string;
|
||
targets?: string[];
|
||
is_medication?: boolean;
|
||
is_tool?: boolean;
|
||
}
|
||
|
||
export function getProducts(
|
||
params: ProductListParams = {},
|
||
): Promise<Product[]> {
|
||
const q = new URLSearchParams();
|
||
if (params.category) q.set("category", params.category);
|
||
if (params.brand) q.set("brand", params.brand);
|
||
if (params.targets) params.targets.forEach((t) => q.append("targets", t));
|
||
if (params.is_medication != null)
|
||
q.set("is_medication", String(params.is_medication));
|
||
if (params.is_tool != null) q.set("is_tool", String(params.is_tool));
|
||
const qs = q.toString();
|
||
return api.get(`/products${qs ? `?${qs}` : ""}`);
|
||
}
|
||
|
||
export const getProduct = (id: string): Promise<Product> =>
|
||
api.get(`/products/${id}`);
|
||
export const createProduct = (
|
||
body: Record<string, unknown>,
|
||
): Promise<Product> => api.post("/products", body);
|
||
export const updateProduct = (
|
||
id: string,
|
||
body: Record<string, unknown>,
|
||
): Promise<Product> => api.patch(`/products/${id}`, body);
|
||
export const deleteProduct = (id: string): Promise<void> =>
|
||
api.del(`/products/${id}`);
|
||
|
||
export const getInventory = (productId: string): Promise<ProductInventory[]> =>
|
||
api.get(`/products/${productId}/inventory`);
|
||
export const createInventory = (
|
||
productId: string,
|
||
body: Record<string, unknown>,
|
||
): Promise<ProductInventory> =>
|
||
api.post(`/products/${productId}/inventory`, body);
|
||
export const updateInventory = (
|
||
id: string,
|
||
body: Record<string, unknown>,
|
||
): Promise<ProductInventory> => api.patch(`/inventory/${id}`, body);
|
||
export const deleteInventory = (id: string): Promise<void> =>
|
||
api.del(`/inventory/${id}`);
|
||
|
||
export interface ProductParseResponse {
|
||
name?: string;
|
||
brand?: string;
|
||
line_name?: string;
|
||
sku?: string;
|
||
url?: string;
|
||
barcode?: string;
|
||
category?: string;
|
||
recommended_time?: string;
|
||
texture?: string;
|
||
absorption_speed?: string;
|
||
leave_on?: boolean;
|
||
price_tier?: string;
|
||
size_ml?: number;
|
||
full_weight_g?: number;
|
||
empty_weight_g?: number;
|
||
pao_months?: number;
|
||
inci?: string[];
|
||
actives?: ActiveIngredient[];
|
||
recommended_for?: string[];
|
||
targets?: string[];
|
||
contraindications?: string[];
|
||
usage_notes?: string;
|
||
fragrance_free?: boolean;
|
||
essential_oils_free?: boolean;
|
||
alcohol_denat_free?: boolean;
|
||
pregnancy_safe?: boolean;
|
||
product_effect_profile?: ProductEffectProfile;
|
||
ph_min?: number;
|
||
ph_max?: number;
|
||
incompatible_with?: ProductInteraction[];
|
||
synergizes_with?: string[];
|
||
context_rules?: ProductContext;
|
||
min_interval_hours?: number;
|
||
max_frequency_per_week?: number;
|
||
is_medication?: boolean;
|
||
is_tool?: boolean;
|
||
needle_length_mm?: number;
|
||
}
|
||
|
||
export const parseProductText = (text: string): Promise<ProductParseResponse> =>
|
||
api.post("/products/parse-text", { text });
|
||
|
||
// ─── Routines ────────────────────────────────────────────────────────────────
|
||
|
||
export interface RoutineListParams {
|
||
from_date?: string;
|
||
to_date?: string;
|
||
part_of_day?: string;
|
||
}
|
||
|
||
export function getRoutines(
|
||
params: RoutineListParams = {},
|
||
): Promise<Routine[]> {
|
||
const q = new URLSearchParams();
|
||
if (params.from_date) q.set("from_date", params.from_date);
|
||
if (params.to_date) q.set("to_date", params.to_date);
|
||
if (params.part_of_day) q.set("part_of_day", params.part_of_day);
|
||
const qs = q.toString();
|
||
return api.get(`/routines${qs ? `?${qs}` : ""}`);
|
||
}
|
||
|
||
export const getRoutine = (id: string): Promise<Routine> =>
|
||
api.get(`/routines/${id}`);
|
||
export const createRoutine = (
|
||
body: Record<string, unknown>,
|
||
): Promise<Routine> => api.post("/routines", body);
|
||
export const updateRoutine = (
|
||
id: string,
|
||
body: Record<string, unknown>,
|
||
): Promise<Routine> => api.patch(`/routines/${id}`, body);
|
||
export const deleteRoutine = (id: string): Promise<void> =>
|
||
api.del(`/routines/${id}`);
|
||
|
||
export const addRoutineStep = (
|
||
routineId: string,
|
||
body: Record<string, unknown>,
|
||
): Promise<RoutineStep> => api.post(`/routines/${routineId}/steps`, body);
|
||
export const updateRoutineStep = (
|
||
stepId: string,
|
||
body: Record<string, unknown>,
|
||
): Promise<RoutineStep> => api.patch(`/routines/steps/${stepId}`, body);
|
||
export const deleteRoutineStep = (stepId: string): Promise<void> =>
|
||
api.del(`/routines/steps/${stepId}`);
|
||
|
||
export const suggestRoutine = (body: {
|
||
routine_date: string;
|
||
part_of_day: PartOfDay;
|
||
notes?: string;
|
||
include_minoxidil_beard?: boolean;
|
||
leaving_home?: boolean;
|
||
}): Promise<RoutineSuggestion> => api.post("/routines/suggest", body);
|
||
|
||
export const suggestBatch = (body: {
|
||
from_date: string;
|
||
to_date: string;
|
||
notes?: string;
|
||
include_minoxidil_beard?: boolean;
|
||
minimize_products?: boolean;
|
||
}): Promise<BatchSuggestion> => api.post("/routines/suggest-batch", body);
|
||
|
||
export const getGroomingSchedule = (): Promise<GroomingSchedule[]> =>
|
||
api.get("/routines/grooming-schedule");
|
||
export const createGroomingScheduleEntry = (
|
||
body: Record<string, unknown>,
|
||
): Promise<GroomingSchedule> => api.post("/routines/grooming-schedule", body);
|
||
export const updateGroomingScheduleEntry = (
|
||
id: string,
|
||
body: Record<string, unknown>,
|
||
): Promise<GroomingSchedule> =>
|
||
api.patch(`/routines/grooming-schedule/${id}`, body);
|
||
export const deleteGroomingScheduleEntry = (id: string): Promise<void> =>
|
||
api.del(`/routines/grooming-schedule/${id}`);
|
||
|
||
// ─── Health – Medications ────────────────────────────────────────────────────
|
||
|
||
export interface MedicationListParams {
|
||
kind?: string;
|
||
product_name?: string;
|
||
}
|
||
|
||
export function getMedications(
|
||
params: MedicationListParams = {},
|
||
): Promise<MedicationEntry[]> {
|
||
const q = new URLSearchParams();
|
||
if (params.kind) q.set("kind", params.kind);
|
||
if (params.product_name) q.set("product_name", params.product_name);
|
||
const qs = q.toString();
|
||
return api.get(`/health/medications${qs ? `?${qs}` : ""}`);
|
||
}
|
||
|
||
export const getMedication = (id: string): Promise<MedicationEntry> =>
|
||
api.get(`/health/medications/${id}`);
|
||
export const createMedication = (
|
||
body: Record<string, unknown>,
|
||
): Promise<MedicationEntry> => api.post("/health/medications", body);
|
||
export const updateMedication = (
|
||
id: string,
|
||
body: Record<string, unknown>,
|
||
): Promise<MedicationEntry> => api.patch(`/health/medications/${id}`, body);
|
||
export const deleteMedication = (id: string): Promise<void> =>
|
||
api.del(`/health/medications/${id}`);
|
||
|
||
export const getMedicationUsages = (
|
||
medicationId: string,
|
||
): Promise<MedicationUsage[]> =>
|
||
api.get(`/health/medications/${medicationId}/usages`);
|
||
export const createMedicationUsage = (
|
||
medicationId: string,
|
||
body: Record<string, unknown>,
|
||
): Promise<MedicationUsage> =>
|
||
api.post(`/health/medications/${medicationId}/usages`, body);
|
||
|
||
// ─── Health – Lab results ────────────────────────────────────────────────────
|
||
|
||
export interface LabResultListParams {
|
||
test_code?: string;
|
||
flag?: string;
|
||
lab?: string;
|
||
from_date?: string;
|
||
to_date?: string;
|
||
}
|
||
|
||
export function getLabResults(
|
||
params: LabResultListParams = {},
|
||
): Promise<LabResult[]> {
|
||
const q = new URLSearchParams();
|
||
if (params.test_code) q.set("test_code", params.test_code);
|
||
if (params.flag) q.set("flag", params.flag);
|
||
if (params.lab) q.set("lab", params.lab);
|
||
if (params.from_date) q.set("from_date", params.from_date);
|
||
if (params.to_date) q.set("to_date", params.to_date);
|
||
const qs = q.toString();
|
||
return api.get(`/health/lab-results${qs ? `?${qs}` : ""}`);
|
||
}
|
||
|
||
export const getLabResult = (id: string): Promise<LabResult> =>
|
||
api.get(`/health/lab-results/${id}`);
|
||
export const createLabResult = (
|
||
body: Record<string, unknown>,
|
||
): Promise<LabResult> => api.post("/health/lab-results", body);
|
||
export const updateLabResult = (
|
||
id: string,
|
||
body: Record<string, unknown>,
|
||
): Promise<LabResult> => api.patch(`/health/lab-results/${id}`, body);
|
||
export const deleteLabResult = (id: string): Promise<void> =>
|
||
api.del(`/health/lab-results/${id}`);
|
||
|
||
// ─── Skin ────────────────────────────────────────────────────────────────────
|
||
|
||
export interface SnapshotListParams {
|
||
from_date?: string;
|
||
to_date?: string;
|
||
overall_state?: string;
|
||
}
|
||
|
||
export function getSkinSnapshots(
|
||
params: SnapshotListParams = {},
|
||
): Promise<SkinConditionSnapshot[]> {
|
||
const q = new URLSearchParams();
|
||
if (params.from_date) q.set("from_date", params.from_date);
|
||
if (params.to_date) q.set("to_date", params.to_date);
|
||
if (params.overall_state) q.set("overall_state", params.overall_state);
|
||
const qs = q.toString();
|
||
return api.get(`/skincare${qs ? `?${qs}` : ""}`);
|
||
}
|
||
|
||
export const getSkinSnapshot = (id: string): Promise<SkinConditionSnapshot> =>
|
||
api.get(`/skincare/${id}`);
|
||
export const createSkinSnapshot = (
|
||
body: Record<string, unknown>,
|
||
): Promise<SkinConditionSnapshot> => api.post("/skincare", body);
|
||
export const updateSkinSnapshot = (
|
||
id: string,
|
||
body: Record<string, unknown>,
|
||
): Promise<SkinConditionSnapshot> => api.patch(`/skincare/${id}`, body);
|
||
export const deleteSkinSnapshot = (id: string): Promise<void> =>
|
||
api.del(`/skincare/${id}`);
|
||
|
||
export interface SkinPhotoAnalysisResponse {
|
||
overall_state?: string;
|
||
texture?: string;
|
||
skin_type?: string;
|
||
hydration_level?: number;
|
||
sebum_tzone?: number;
|
||
sebum_cheeks?: number;
|
||
sensitivity_level?: number;
|
||
barrier_state?: string;
|
||
active_concerns?: string[];
|
||
risks?: string[];
|
||
priorities?: string[];
|
||
notes?: string;
|
||
}
|
||
|
||
export async function analyzeSkinPhotos(
|
||
files: File[],
|
||
): Promise<SkinPhotoAnalysisResponse> {
|
||
const body = new FormData();
|
||
for (const file of files) body.append("photos", file);
|
||
const base = browser ? "/api" : PUBLIC_API_BASE;
|
||
const res = await fetch(`${base}/skincare/analyze-photos`, {
|
||
method: "POST",
|
||
body,
|
||
});
|
||
if (!res.ok) {
|
||
const detail = await res.json().catch(() => ({ detail: res.statusText }));
|
||
throw new Error(detail?.detail ?? res.statusText);
|
||
}
|
||
return res.json();
|
||
}
|