refactor(frontend): route protected API access through server session
This commit is contained in:
parent
1d5630ed8c
commit
b11f64d5a1
31 changed files with 727 additions and 249 deletions
18
.sisyphus/evidence/task-T8-protected-nav.md
Normal file
18
.sisyphus/evidence/task-T8-protected-nav.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Task T8 Protected Navigation
|
||||
|
||||
- QA app: `http://127.0.0.1:4175`
|
||||
- Backend: `http://127.0.0.1:8002`
|
||||
- Mock OIDC issuer: `http://127.0.0.1:9100`
|
||||
- Backend DB: `.sisyphus/evidence/task-T8-qa.sqlite`
|
||||
|
||||
Authenticated shell and protected route checks executed with Playwright:
|
||||
|
||||
- `/` -> title `Dashboard - innercontext`, heading `Dashboard`, shell user `Playwright User`, role `Użytkownik`, logout visible `true`
|
||||
- `/products` -> title `Produkty — innercontext`, heading `Produkty`, shell user `Playwright User`, role `Użytkownik`, logout visible `true`
|
||||
- `/profile` -> title `Profil — innercontext`, heading `Profil`, shell user `Playwright User`, role `Użytkownik`, logout visible `true`
|
||||
- `/routines` -> title `Rutyny — innercontext`, heading `Rutyny`, shell user `Playwright User`, role `Użytkownik`, logout visible `true`
|
||||
|
||||
Logout endpoint check executed with Playwright request API:
|
||||
|
||||
- `GET /auth/logout` -> `303`
|
||||
- Location -> `http://127.0.0.1:9100/logout?client_id=innercontext-web&post_logout_redirect_uri=http%3A%2F%2F127.0.0.1%3A4175%2F`
|
||||
10
.sisyphus/evidence/task-T8-signed-out-network.txt
Normal file
10
.sisyphus/evidence/task-T8-signed-out-network.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
Playwright unauthenticated request check
|
||||
|
||||
request: GET http://127.0.0.1:4175/products
|
||||
cookies: none
|
||||
maxRedirects: 0
|
||||
|
||||
status: 303
|
||||
location: /auth/login?returnTo=%2Fproducts
|
||||
|
||||
result: protected page redirects to the login flow before returning page content.
|
||||
|
|
@ -10,6 +10,11 @@
|
|||
"nav_appName": "innercontext",
|
||||
"nav_appSubtitle": "personal health & skincare",
|
||||
|
||||
"auth_signedInAs": "Signed in as",
|
||||
"auth_roleAdmin": "Admin",
|
||||
"auth_roleMember": "Member",
|
||||
"auth_logout": "Log out",
|
||||
|
||||
"common_save": "Save",
|
||||
"common_cancel": "Cancel",
|
||||
"common_add": "Add",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@
|
|||
"nav_appName": "innercontext",
|
||||
"nav_appSubtitle": "zdrowie & pielęgnacja",
|
||||
|
||||
"auth_signedInAs": "Zalogowano jako",
|
||||
"auth_roleAdmin": "Administrator",
|
||||
"auth_roleMember": "Użytkownik",
|
||||
"auth_logout": "Wyloguj",
|
||||
|
||||
"common_save": "Zapisz",
|
||||
"common_cancel": "Anuluj",
|
||||
"common_add": "Dodaj",
|
||||
|
|
|
|||
|
|
@ -20,43 +20,87 @@ import type {
|
|||
UserProfile,
|
||||
} from "./types";
|
||||
|
||||
// ─── Core fetch helpers ──────────────────────────────────────────────────────
|
||||
export interface ApiClientOptions {
|
||||
fetch?: typeof globalThis.fetch;
|
||||
accessToken?: string;
|
||||
}
|
||||
|
||||
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 },
|
||||
function resolveBase(options: ApiClientOptions): string {
|
||||
if (browser && !options.accessToken) {
|
||||
return "/api";
|
||||
}
|
||||
|
||||
return PUBLIC_API_BASE;
|
||||
}
|
||||
|
||||
function buildHeaders(
|
||||
initHeaders: HeadersInit | undefined,
|
||||
body: BodyInit | null | undefined,
|
||||
accessToken?: string,
|
||||
): Headers {
|
||||
const headers = new Headers(initHeaders);
|
||||
|
||||
if (!(body instanceof FormData) && body !== undefined && body !== null) {
|
||||
headers.set("Content-Type", headers.get("Content-Type") ?? "application/json");
|
||||
}
|
||||
|
||||
if (accessToken && !headers.has("Authorization")) {
|
||||
headers.set("Authorization", `Bearer ${accessToken}`);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
options: ApiClientOptions = {},
|
||||
): Promise<T> {
|
||||
const url = `${resolveBase(options)}${path}`;
|
||||
const requestFn = options.fetch ?? fetch;
|
||||
const res = await requestFn(url, {
|
||||
headers: buildHeaders(init.headers, init.body, options.accessToken),
|
||||
...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),
|
||||
export function createApiClient(options: ApiClientOptions = {}) {
|
||||
return {
|
||||
get: <T>(path: string) => request<T>(path, {}, options),
|
||||
post: <T>(path: string, body: unknown) =>
|
||||
request<T>(path, { method: "POST", body: JSON.stringify(body) }),
|
||||
request<T>(path, { method: "POST", body: JSON.stringify(body) }, options),
|
||||
postForm: <T>(path: string, body: FormData) =>
|
||||
request<T>(path, { method: "POST", body }, options),
|
||||
patch: <T>(path: string, body: unknown) =>
|
||||
request<T>(path, { method: "PATCH", body: JSON.stringify(body) }),
|
||||
del: (path: string) => request<void>(path, { method: "DELETE" }),
|
||||
};
|
||||
request<T>(path, { method: "PATCH", body: JSON.stringify(body) }, options),
|
||||
del: (path: string) => request<void>(path, { method: "DELETE" }, options),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Profile ─────────────────────────────────────────────────────────────────
|
||||
export type ApiClient = ReturnType<typeof createApiClient>;
|
||||
|
||||
export const getProfile = (): Promise<UserProfile | null> => api.get("/profile");
|
||||
export const api = createApiClient();
|
||||
|
||||
function resolveClient(options?: ApiClientOptions): ApiClient {
|
||||
return options ? createApiClient(options) : api;
|
||||
}
|
||||
|
||||
export const getProfile = (
|
||||
options?: ApiClientOptions,
|
||||
): Promise<UserProfile | null> => resolveClient(options).get("/profile");
|
||||
|
||||
export const updateProfile = (
|
||||
body: { birth_date?: string; sex_at_birth?: "male" | "female" | "intersex" },
|
||||
): Promise<UserProfile> => api.patch("/profile", body);
|
||||
|
||||
// ─── Products ────────────────────────────────────────────────────────────────
|
||||
options?: ApiClientOptions,
|
||||
): Promise<UserProfile> => resolveClient(options).patch("/profile", body);
|
||||
|
||||
export interface ProductListParams {
|
||||
category?: string;
|
||||
|
|
@ -68,62 +112,87 @@ export interface ProductListParams {
|
|||
|
||||
export function getProducts(
|
||||
params: ProductListParams = {},
|
||||
options?: ApiClientOptions,
|
||||
): 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)
|
||||
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}` : ""}`);
|
||||
return resolveClient(options).get(`/products${qs ? `?${qs}` : ""}`);
|
||||
}
|
||||
|
||||
export function getProductSummaries(
|
||||
params: ProductListParams = {},
|
||||
options?: ApiClientOptions,
|
||||
): Promise<ProductSummary[]> {
|
||||
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)
|
||||
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/summary${qs ? `?${qs}` : ""}`);
|
||||
return resolveClient(options).get(`/products/summary${qs ? `?${qs}` : ""}`);
|
||||
}
|
||||
|
||||
export const getProduct = (id: string): Promise<Product> =>
|
||||
api.get(`/products/${id}`);
|
||||
export const getProduct = (
|
||||
id: string,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<Product> => resolveClient(options).get(`/products/${id}`);
|
||||
|
||||
export const createProduct = (
|
||||
body: Record<string, unknown>,
|
||||
): Promise<Product> => api.post("/products", body);
|
||||
options?: ApiClientOptions,
|
||||
): Promise<Product> => resolveClient(options).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}`);
|
||||
options?: ApiClientOptions,
|
||||
): Promise<Product> => resolveClient(options).patch(`/products/${id}`, body);
|
||||
|
||||
export const deleteProduct = (
|
||||
id: string,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<void> => resolveClient(options).del(`/products/${id}`);
|
||||
|
||||
export const getInventory = (
|
||||
productId: string,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<ProductInventory[]> =>
|
||||
resolveClient(options).get(`/products/${productId}/inventory`);
|
||||
|
||||
export const getInventory = (productId: string): Promise<ProductInventory[]> =>
|
||||
api.get(`/products/${productId}/inventory`);
|
||||
export const createInventory = (
|
||||
productId: string,
|
||||
body: Record<string, unknown>,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<ProductInventory> =>
|
||||
api.post(`/products/${productId}/inventory`, body);
|
||||
resolveClient(options).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}`);
|
||||
options?: ApiClientOptions,
|
||||
): Promise<ProductInventory> =>
|
||||
resolveClient(options).patch(`/inventory/${id}`, body);
|
||||
|
||||
export const parseProductText = (text: string): Promise<ProductParseResponse> =>
|
||||
api.post("/products/parse-text", { text });
|
||||
export const deleteInventory = (
|
||||
id: string,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<void> => resolveClient(options).del(`/inventory/${id}`);
|
||||
|
||||
// ─── Routines ────────────────────────────────────────────────────────────────
|
||||
export const parseProductText = (
|
||||
text: string,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<ProductParseResponse> =>
|
||||
resolveClient(options).post("/products/parse-text", { text });
|
||||
|
||||
export interface RoutineListParams {
|
||||
from_date?: string;
|
||||
|
|
@ -133,68 +202,103 @@ export interface RoutineListParams {
|
|||
|
||||
export function getRoutines(
|
||||
params: RoutineListParams = {},
|
||||
options?: ApiClientOptions,
|
||||
): 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}` : ""}`);
|
||||
return resolveClient(options).get(`/routines${qs ? `?${qs}` : ""}`);
|
||||
}
|
||||
|
||||
export const getRoutine = (id: string): Promise<Routine> =>
|
||||
api.get(`/routines/${id}`);
|
||||
export const getRoutine = (
|
||||
id: string,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<Routine> => resolveClient(options).get(`/routines/${id}`);
|
||||
|
||||
export const createRoutine = (
|
||||
body: Record<string, unknown>,
|
||||
): Promise<Routine> => api.post("/routines", body);
|
||||
options?: ApiClientOptions,
|
||||
): Promise<Routine> => resolveClient(options).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}`);
|
||||
options?: ApiClientOptions,
|
||||
): Promise<Routine> => resolveClient(options).patch(`/routines/${id}`, body);
|
||||
|
||||
export const deleteRoutine = (
|
||||
id: string,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<void> => resolveClient(options).del(`/routines/${id}`);
|
||||
|
||||
export const addRoutineStep = (
|
||||
routineId: string,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<RoutineStep> => api.post(`/routines/${routineId}/steps`, body);
|
||||
options?: ApiClientOptions,
|
||||
): Promise<RoutineStep> =>
|
||||
resolveClient(options).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}`);
|
||||
options?: ApiClientOptions,
|
||||
): Promise<RoutineStep> =>
|
||||
resolveClient(options).patch(`/routines/steps/${stepId}`, body);
|
||||
|
||||
export const suggestRoutine = (body: {
|
||||
export const deleteRoutineStep = (
|
||||
stepId: string,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<void> => resolveClient(options).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);
|
||||
},
|
||||
options?: ApiClientOptions,
|
||||
): Promise<RoutineSuggestion> =>
|
||||
resolveClient(options).post("/routines/suggest", body);
|
||||
|
||||
export const suggestBatch = (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);
|
||||
},
|
||||
options?: ApiClientOptions,
|
||||
): Promise<BatchSuggestion> =>
|
||||
resolveClient(options).post("/routines/suggest-batch", body);
|
||||
|
||||
export const getGroomingSchedule = (
|
||||
options?: ApiClientOptions,
|
||||
): Promise<GroomingSchedule[]> =>
|
||||
resolveClient(options).get("/routines/grooming-schedule");
|
||||
|
||||
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);
|
||||
options?: ApiClientOptions,
|
||||
): Promise<GroomingSchedule> =>
|
||||
resolveClient(options).post("/routines/grooming-schedule", body);
|
||||
|
||||
export const updateGroomingScheduleEntry = (
|
||||
id: string,
|
||||
body: Record<string, unknown>,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<GroomingSchedule> =>
|
||||
api.patch(`/routines/grooming-schedule/${id}`, body);
|
||||
export const deleteGroomingScheduleEntry = (id: string): Promise<void> =>
|
||||
api.del(`/routines/grooming-schedule/${id}`);
|
||||
resolveClient(options).patch(`/routines/grooming-schedule/${id}`, body);
|
||||
|
||||
// ─── Health – Medications ────────────────────────────────────────────────────
|
||||
export const deleteGroomingScheduleEntry = (
|
||||
id: string,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<void> =>
|
||||
resolveClient(options).del(`/routines/grooming-schedule/${id}`);
|
||||
|
||||
export interface MedicationListParams {
|
||||
kind?: string;
|
||||
|
|
@ -203,37 +307,51 @@ export interface MedicationListParams {
|
|||
|
||||
export function getMedications(
|
||||
params: MedicationListParams = {},
|
||||
options?: ApiClientOptions,
|
||||
): 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}` : ""}`);
|
||||
return resolveClient(options).get(`/health/medications${qs ? `?${qs}` : ""}`);
|
||||
}
|
||||
|
||||
export const getMedication = (id: string): Promise<MedicationEntry> =>
|
||||
api.get(`/health/medications/${id}`);
|
||||
export const getMedication = (
|
||||
id: string,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<MedicationEntry> =>
|
||||
resolveClient(options).get(`/health/medications/${id}`);
|
||||
|
||||
export const createMedication = (
|
||||
body: Record<string, unknown>,
|
||||
): Promise<MedicationEntry> => api.post("/health/medications", body);
|
||||
options?: ApiClientOptions,
|
||||
): Promise<MedicationEntry> =>
|
||||
resolveClient(options).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}`);
|
||||
options?: ApiClientOptions,
|
||||
): Promise<MedicationEntry> =>
|
||||
resolveClient(options).patch(`/health/medications/${id}`, body);
|
||||
|
||||
export const deleteMedication = (
|
||||
id: string,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<void> => resolveClient(options).del(`/health/medications/${id}`);
|
||||
|
||||
export const getMedicationUsages = (
|
||||
medicationId: string,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<MedicationUsage[]> =>
|
||||
api.get(`/health/medications/${medicationId}/usages`);
|
||||
resolveClient(options).get(`/health/medications/${medicationId}/usages`);
|
||||
|
||||
export const createMedicationUsage = (
|
||||
medicationId: string,
|
||||
body: Record<string, unknown>,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<MedicationUsage> =>
|
||||
api.post(`/health/medications/${medicationId}/usages`, body);
|
||||
|
||||
// ─── Health – Lab results ────────────────────────────────────────────────────
|
||||
resolveClient(options).post(`/health/medications/${medicationId}/usages`, body);
|
||||
|
||||
export interface LabResultListParams {
|
||||
q?: string;
|
||||
|
|
@ -250,6 +368,7 @@ export interface LabResultListParams {
|
|||
|
||||
export function getLabResults(
|
||||
params: LabResultListParams = {},
|
||||
options?: ApiClientOptions,
|
||||
): Promise<LabResultListResponse> {
|
||||
const q = new URLSearchParams();
|
||||
if (params.q) q.set("q", params.q);
|
||||
|
|
@ -258,29 +377,43 @@ export function getLabResults(
|
|||
if (params.flags?.length) {
|
||||
for (const flag of params.flags) q.append("flags", flag);
|
||||
}
|
||||
if (params.without_flag != null) q.set("without_flag", String(params.without_flag));
|
||||
if (params.without_flag != null) {
|
||||
q.set("without_flag", String(params.without_flag));
|
||||
}
|
||||
if (params.from_date) q.set("from_date", params.from_date);
|
||||
if (params.to_date) q.set("to_date", params.to_date);
|
||||
if (params.latest_only != null) q.set("latest_only", String(params.latest_only));
|
||||
if (params.latest_only != null) {
|
||||
q.set("latest_only", String(params.latest_only));
|
||||
}
|
||||
if (params.limit != null) q.set("limit", String(params.limit));
|
||||
if (params.offset != null) q.set("offset", String(params.offset));
|
||||
const qs = q.toString();
|
||||
return api.get(`/health/lab-results${qs ? `?${qs}` : ""}`);
|
||||
return resolveClient(options).get(`/health/lab-results${qs ? `?${qs}` : ""}`);
|
||||
}
|
||||
|
||||
export const getLabResult = (id: string): Promise<LabResult> =>
|
||||
api.get(`/health/lab-results/${id}`);
|
||||
export const getLabResult = (
|
||||
id: string,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<LabResult> =>
|
||||
resolveClient(options).get(`/health/lab-results/${id}`);
|
||||
|
||||
export const createLabResult = (
|
||||
body: Record<string, unknown>,
|
||||
): Promise<LabResult> => api.post("/health/lab-results", body);
|
||||
options?: ApiClientOptions,
|
||||
): Promise<LabResult> =>
|
||||
resolveClient(options).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}`);
|
||||
options?: ApiClientOptions,
|
||||
): Promise<LabResult> =>
|
||||
resolveClient(options).patch(`/health/lab-results/${id}`, body);
|
||||
|
||||
// ─── Skin ────────────────────────────────────────────────────────────────────
|
||||
export const deleteLabResult = (
|
||||
id: string,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<void> => resolveClient(options).del(`/health/lab-results/${id}`);
|
||||
|
||||
export interface SnapshotListParams {
|
||||
from_date?: string;
|
||||
|
|
@ -290,40 +423,47 @@ export interface SnapshotListParams {
|
|||
|
||||
export function getSkinSnapshots(
|
||||
params: SnapshotListParams = {},
|
||||
options?: ApiClientOptions,
|
||||
): 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}` : ""}`);
|
||||
return resolveClient(options).get(`/skincare${qs ? `?${qs}` : ""}`);
|
||||
}
|
||||
|
||||
export const getSkinSnapshot = (id: string): Promise<SkinConditionSnapshot> =>
|
||||
api.get(`/skincare/${id}`);
|
||||
export const getSkinSnapshot = (
|
||||
id: string,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<SkinConditionSnapshot> => resolveClient(options).get(`/skincare/${id}`);
|
||||
|
||||
export const createSkinSnapshot = (
|
||||
body: Record<string, unknown>,
|
||||
): Promise<SkinConditionSnapshot> => api.post("/skincare", body);
|
||||
options?: ApiClientOptions,
|
||||
): Promise<SkinConditionSnapshot> => resolveClient(options).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}`);
|
||||
options?: ApiClientOptions,
|
||||
): Promise<SkinConditionSnapshot> =>
|
||||
resolveClient(options).patch(`/skincare/${id}`, body);
|
||||
|
||||
export const deleteSkinSnapshot = (
|
||||
id: string,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<void> => resolveClient(options).del(`/skincare/${id}`);
|
||||
|
||||
export async function analyzeSkinPhotos(
|
||||
files: File[],
|
||||
files: File[] | FormData,
|
||||
options?: ApiClientOptions,
|
||||
): Promise<SkinPhotoAnalysisResponse> {
|
||||
const body = new FormData();
|
||||
const body = files instanceof FormData ? files : new FormData();
|
||||
|
||||
if (!(files instanceof 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();
|
||||
|
||||
return resolveClient(options).postForm("/skincare/analyze-photos", body);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<script lang="ts">
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import type { Product, IngredientFunction, ProductParseResponse } from '$lib/types';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
|
|
@ -186,13 +186,27 @@
|
|||
let editSection = $state<'basic' | 'ingredients' | 'assessment' | 'details' | 'notes'>('basic');
|
||||
let activesPanelOpen = $state(true);
|
||||
|
||||
async function requestProductParse(text: string): Promise<ProductParseResponse> {
|
||||
const response = await fetch('/api/products/parse-text', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const detail = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
throw new Error(detail?.detail ?? response.statusText);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function parseWithAi() {
|
||||
if (!aiText.trim()) return;
|
||||
aiLoading = true;
|
||||
aiError = '';
|
||||
try {
|
||||
const { parseProductText } = await import('$lib/api');
|
||||
const r = await parseProductText(aiText);
|
||||
const r = await requestProductParse(aiText);
|
||||
applyAiResult(r);
|
||||
aiModalOpen = false;
|
||||
} catch (e) {
|
||||
|
|
|
|||
18
frontend/src/lib/server/api.ts
Normal file
18
frontend/src/lib/server/api.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import type { ApiClientOptions } from "$lib/api";
|
||||
import { error } from "@sveltejs/kit";
|
||||
import type { RequestEvent } from "@sveltejs/kit";
|
||||
|
||||
type SessionEvent = Pick<RequestEvent, "fetch" | "locals">;
|
||||
|
||||
export function getSessionApiOptions(event: SessionEvent): ApiClientOptions {
|
||||
const accessToken = event.locals.session?.accessToken;
|
||||
|
||||
if (!accessToken) {
|
||||
throw error(401, "Authentication required");
|
||||
}
|
||||
|
||||
return {
|
||||
fetch: event.fetch,
|
||||
accessToken,
|
||||
};
|
||||
}
|
||||
36
frontend/src/routes/+layout.server.ts
Normal file
36
frontend/src/routes/+layout.server.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
function getDisplayName(session: App.Locals['session']): string | null {
|
||||
if (!session) return null;
|
||||
|
||||
const candidates = [
|
||||
session.identity.name,
|
||||
session.identity.preferred_username,
|
||||
session.identity.email
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const value = candidate?.trim();
|
||||
if (value) return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
if (!locals.session) {
|
||||
return {
|
||||
session: null
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
session: {
|
||||
expiresAt: locals.session.expiresAt,
|
||||
user: locals.session.user,
|
||||
identity: locals.session.identity,
|
||||
profile: locals.session.profile,
|
||||
displayName: getDisplayName(locals.session)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
import { page } from '$app/state';
|
||||
import { resolve } from '$app/paths';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import LanguageSwitcher from '$lib/components/LanguageSwitcher.svelte';
|
||||
import {
|
||||
House,
|
||||
|
|
@ -17,34 +18,81 @@
|
|||
Menu,
|
||||
X
|
||||
} from 'lucide-svelte';
|
||||
import type { AuthIdentityPublic, AuthProfilePublic, AuthUserPublic } from '$lib/api/generated/types.gen';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let { children } = $props();
|
||||
type ShellSession = {
|
||||
expiresAt: number;
|
||||
user: AuthUserPublic;
|
||||
identity: AuthIdentityPublic;
|
||||
profile: AuthProfilePublic | null;
|
||||
displayName: string | null;
|
||||
};
|
||||
|
||||
type NavRoute =
|
||||
| '/'
|
||||
| '/routines'
|
||||
| '/routines/grooming-schedule'
|
||||
| '/products'
|
||||
| '/skin'
|
||||
| '/profile'
|
||||
| '/health/medications'
|
||||
| '/health/lab-results';
|
||||
|
||||
type NavItem = {
|
||||
route: NavRoute;
|
||||
label: string;
|
||||
icon: typeof House;
|
||||
};
|
||||
|
||||
let {
|
||||
data,
|
||||
children
|
||||
}: {
|
||||
data: { session: ShellSession | null };
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
|
||||
let mobileMenuOpen = $state(false);
|
||||
const domainClass = $derived(getDomainClass(page.url.pathname));
|
||||
const authMessages = m as typeof m &
|
||||
Record<'auth_roleAdmin' | 'auth_roleMember' | 'auth_signedInAs' | 'auth_logout', () => string>;
|
||||
const session = $derived(data.session);
|
||||
const logoutPath = '/auth/logout';
|
||||
const roleLabel = $derived(
|
||||
session?.user.role === 'admin'
|
||||
? authMessages['auth_roleAdmin']()
|
||||
: session
|
||||
? authMessages['auth_roleMember']()
|
||||
: ''
|
||||
);
|
||||
const secondaryIdentity = $derived(
|
||||
session?.identity.email ?? session?.identity.preferred_username ?? null
|
||||
);
|
||||
const displayName = $derived(session?.displayName ?? secondaryIdentity ?? m.common_unknown());
|
||||
|
||||
afterNavigate(() => {
|
||||
mobileMenuOpen = false;
|
||||
});
|
||||
|
||||
const navItems = $derived([
|
||||
{ href: resolve('/'), label: m.nav_dashboard(), icon: House },
|
||||
{ href: resolve('/routines'), label: m.nav_routines(), icon: ClipboardList },
|
||||
{ href: resolve('/routines/grooming-schedule'), label: m.nav_grooming(), icon: Scissors },
|
||||
{ href: resolve('/products'), label: m.nav_products(), icon: Package },
|
||||
{ href: resolve('/skin'), label: m.nav_skin(), icon: Sparkles },
|
||||
{ href: resolve('/profile'), label: m.nav_profile(), icon: UserRound },
|
||||
{ href: resolve('/health/medications'), label: m.nav_medications(), icon: Pill },
|
||||
{ href: resolve('/health/lab-results'), label: m["nav_labResults"](), icon: FlaskConical }
|
||||
]);
|
||||
{ route: '/', label: m.nav_dashboard(), icon: House },
|
||||
{ route: '/routines', label: m.nav_routines(), icon: ClipboardList },
|
||||
{ route: '/routines/grooming-schedule', label: m.nav_grooming(), icon: Scissors },
|
||||
{ route: '/products', label: m.nav_products(), icon: Package },
|
||||
{ route: '/skin', label: m.nav_skin(), icon: Sparkles },
|
||||
{ route: '/profile', label: m.nav_profile(), icon: UserRound },
|
||||
{ route: '/health/medications', label: m.nav_medications(), icon: Pill },
|
||||
{ route: '/health/lab-results', label: m["nav_labResults"](), icon: FlaskConical }
|
||||
] satisfies NavItem[]);
|
||||
|
||||
function isActive(href: string) {
|
||||
if (href === '/') return page.url.pathname === '/';
|
||||
function isActive(route: string) {
|
||||
if (route === '/') return page.url.pathname === '/';
|
||||
const pathname = page.url.pathname;
|
||||
if (!pathname.startsWith(href)) return false;
|
||||
// Don't mark parent as active if a more-specific nav item also matches
|
||||
if (!pathname.startsWith(route)) return false;
|
||||
const moreSpecific = navItems.some(
|
||||
(item) => item.href !== href && item.href.startsWith(href) && pathname.startsWith(item.href)
|
||||
(item) =>
|
||||
item.route !== route && item.route.startsWith(route) && pathname.startsWith(item.route)
|
||||
);
|
||||
return !moreSpecific;
|
||||
}
|
||||
|
|
@ -65,6 +113,12 @@
|
|||
<div class="app-mobile-titleblock">
|
||||
<p class="app-mobile-overline">{m["nav_appSubtitle"]()}</p>
|
||||
<span class="app-mobile-title">{m["nav_appName"]()}</span>
|
||||
{#if session}
|
||||
<div class="mt-2 flex items-center gap-2 text-[0.68rem] uppercase tracking-[0.22em] text-muted-foreground">
|
||||
<span class="truncate text-foreground">{displayName}</span>
|
||||
<span class="rounded-full border border-border/80 bg-background/80 px-2 py-0.5 text-[0.62rem] text-foreground">{roleLabel}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -104,12 +158,12 @@
|
|||
</button>
|
||||
</div>
|
||||
<ul class="app-nav-list">
|
||||
{#each navItems as item (item.href)}
|
||||
{#each navItems as item (item.route)}
|
||||
<li>
|
||||
<a
|
||||
href={item.href}
|
||||
href={resolve(item.route)}
|
||||
onclick={() => (mobileMenuOpen = false)}
|
||||
class={`app-nav-link ${isActive(item.href) ? 'app-nav-link--active' : ''}`}
|
||||
class={`app-nav-link ${isActive(item.route) ? 'app-nav-link--active' : ''}`}
|
||||
>
|
||||
<item.icon class="size-4 shrink-0" />
|
||||
<span>{item.label}</span>
|
||||
|
|
@ -118,6 +172,23 @@
|
|||
{/each}
|
||||
</ul>
|
||||
<div class="app-sidebar-footer">
|
||||
{#if session}
|
||||
<div class="mb-3 rounded-[1.35rem] border border-border/70 bg-background/88 px-4 py-3 shadow-sm">
|
||||
<p class="text-[0.65rem] font-semibold uppercase tracking-[0.24em] text-muted-foreground">
|
||||
{authMessages['auth_signedInAs']()}
|
||||
</p>
|
||||
<p class="mt-2 text-sm font-semibold text-foreground">{displayName}</p>
|
||||
{#if secondaryIdentity}
|
||||
<p class="mt-1 break-all text-xs text-muted-foreground">{secondaryIdentity}</p>
|
||||
{/if}
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<span class="rounded-full border border-border/80 bg-muted/40 px-2.5 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.16em] text-foreground">
|
||||
{roleLabel}
|
||||
</span>
|
||||
<Button href={logoutPath} data-sveltekit-reload variant="outline" size="sm">{authMessages['auth_logout']()}</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</aside>
|
||||
|
|
@ -131,11 +202,11 @@
|
|||
</div>
|
||||
</div>
|
||||
<ul class="app-nav-list">
|
||||
{#each navItems as item (item.href)}
|
||||
{#each navItems as item (item.route)}
|
||||
<li>
|
||||
<a
|
||||
href={item.href}
|
||||
class={`app-nav-link ${isActive(item.href) ? 'app-nav-link--active' : ''}`}
|
||||
href={resolve(item.route)}
|
||||
class={`app-nav-link ${isActive(item.route) ? 'app-nav-link--active' : ''}`}
|
||||
>
|
||||
<item.icon class="size-4 shrink-0" />
|
||||
<span>{item.label}</span>
|
||||
|
|
@ -144,6 +215,23 @@
|
|||
{/each}
|
||||
</ul>
|
||||
<div class="app-sidebar-footer">
|
||||
{#if session}
|
||||
<div class="mb-3 rounded-[1.35rem] border border-border/70 bg-background/88 px-4 py-3 shadow-sm">
|
||||
<p class="text-[0.65rem] font-semibold uppercase tracking-[0.24em] text-muted-foreground">
|
||||
{authMessages['auth_signedInAs']()}
|
||||
</p>
|
||||
<p class="mt-2 text-sm font-semibold text-foreground">{displayName}</p>
|
||||
{#if secondaryIdentity}
|
||||
<p class="mt-1 break-all text-xs text-muted-foreground">{secondaryIdentity}</p>
|
||||
{/if}
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<span class="rounded-full border border-border/80 bg-muted/40 px-2.5 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.16em] text-foreground">
|
||||
{roleLabel}
|
||||
</span>
|
||||
<Button href={logoutPath} data-sveltekit-reload variant="outline" size="sm">{authMessages['auth_logout']()}</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import { getLabResults, getRoutines, getSkinSnapshots } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import type { Routine, SkinConditionSnapshot } from '$lib/types';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const apiOptions = getSessionApiOptions(event);
|
||||
const [routines, snapshots, labResults] = await Promise.all([
|
||||
getRoutines({ from_date: recentDate(14) }),
|
||||
getSkinSnapshots({ from_date: recentDate(60) }),
|
||||
getLabResults({ latest_only: true, limit: 8 })
|
||||
getRoutines({ from_date: recentDate(14) }, apiOptions),
|
||||
getSkinSnapshots({ from_date: recentDate(60) }, apiOptions),
|
||||
getLabResults({ latest_only: true, limit: 8 }, apiOptions)
|
||||
]);
|
||||
return {
|
||||
recentRoutines: getFreshestRoutines(routines).slice(0, 10),
|
||||
|
|
|
|||
23
frontend/src/routes/api/products/parse-text/+server.ts
Normal file
23
frontend/src/routes/api/products/parse-text/+server.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { parseProductText } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const payload = await event.request.json().catch(() => null);
|
||||
const text =
|
||||
typeof payload === 'object' && payload !== null && 'text' in payload && typeof payload.text === 'string'
|
||||
? payload.text.trim()
|
||||
: '';
|
||||
|
||||
if (!text) {
|
||||
throw error(400, 'Missing product text');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await parseProductText(text, getSessionApiOptions(event));
|
||||
return json(result);
|
||||
} catch (cause) {
|
||||
throw error(502, cause instanceof Error ? cause.message : 'Failed to parse product text');
|
||||
}
|
||||
};
|
||||
23
frontend/src/routes/api/routines/steps/[id]/+server.ts
Normal file
23
frontend/src/routes/api/routines/steps/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { updateRoutineStep } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const PATCH: RequestHandler = async (event) => {
|
||||
const payload = await event.request.json().catch(() => null);
|
||||
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||
throw error(400, 'Invalid routine step payload');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await updateRoutineStep(
|
||||
event.params.id,
|
||||
payload as Record<string, unknown>,
|
||||
getSessionApiOptions(event)
|
||||
);
|
||||
return json(result);
|
||||
} catch (cause) {
|
||||
throw error(502, cause instanceof Error ? cause.message : 'Failed to update routine step');
|
||||
}
|
||||
};
|
||||
19
frontend/src/routes/api/skin/analyze-photos/+server.ts
Normal file
19
frontend/src/routes/api/skin/analyze-photos/+server.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { analyzeSkinPhotos } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
|
||||
if (formData.getAll('photos').length === 0) {
|
||||
throw error(400, 'Missing skin photos');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await analyzeSkinPhotos(formData, getSessionApiOptions(event));
|
||||
return json(result);
|
||||
} catch (cause) {
|
||||
throw error(502, cause instanceof Error ? cause.message : 'Failed to analyze skin photos');
|
||||
}
|
||||
};
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { deleteLabResult, getLabResults, updateLabResult } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
|
|
@ -9,7 +10,9 @@ const STATUS_GROUP_FLAGS = {
|
|||
|
||||
type StatusGroup = 'all' | 'abnormal' | 'normal' | 'uninterpreted';
|
||||
|
||||
export const load: PageServerLoad = async ({ url }) => {
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { url } = event;
|
||||
const q = url.searchParams.get('q') ?? undefined;
|
||||
const test_code = url.searchParams.get('test_code') ?? undefined;
|
||||
const flag = url.searchParams.get('flag') ?? undefined;
|
||||
|
|
@ -41,7 +44,7 @@ export const load: PageServerLoad = async ({ url }) => {
|
|||
latest_only: latestOnly,
|
||||
limit,
|
||||
offset
|
||||
});
|
||||
}, getSessionApiOptions(event));
|
||||
const totalPages = Math.max(1, Math.ceil(resultPage.total / limit));
|
||||
|
||||
return {
|
||||
|
|
@ -64,8 +67,8 @@ function normalizeStatusGroup(value: string | null): StatusGroup {
|
|||
}
|
||||
|
||||
export const actions: Actions = {
|
||||
update: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
update: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const id = form.get('id') as string;
|
||||
const collected_at = form.get('collected_at') as string;
|
||||
const test_code = form.get('test_code') as string;
|
||||
|
|
@ -136,20 +139,20 @@ export const actions: Actions = {
|
|||
}
|
||||
|
||||
try {
|
||||
await updateLabResult(id, body);
|
||||
await updateLabResult(id, body, getSessionApiOptions(event));
|
||||
return { updated: true };
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
}
|
||||
},
|
||||
|
||||
delete: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
delete: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const id = form.get('id') as string;
|
||||
if (!id) return fail(400, { error: 'Missing id' });
|
||||
|
||||
try {
|
||||
await deleteLabResult(id);
|
||||
await deleteLabResult(id, getSessionApiOptions(event));
|
||||
return { deleted: true };
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { createLabResult } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
|
|
@ -7,8 +8,8 @@ export const load: PageServerLoad = async () => {
|
|||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
default: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const collected_at = form.get('collected_at') as string;
|
||||
const test_code = form.get('test_code') as string;
|
||||
const test_name_original = form.get('test_name_original') as string;
|
||||
|
|
@ -32,7 +33,7 @@ export const actions: Actions = {
|
|||
if (lab) body.lab = lab;
|
||||
|
||||
try {
|
||||
await createLabResult(body);
|
||||
await createLabResult(body, getSessionApiOptions(event));
|
||||
} catch (error) {
|
||||
return fail(500, { error: (error as Error).message });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import { getMedications } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ url }) => {
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { url } = event;
|
||||
const kind = url.searchParams.get('kind') ?? undefined;
|
||||
const medications = await getMedications({ kind });
|
||||
const medications = await getMedications({ kind }, getSessionApiOptions(event));
|
||||
return { medications, kind };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { createMedication } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
|
|
@ -7,8 +8,8 @@ export const load: PageServerLoad = async () => {
|
|||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
default: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const kind = form.get('kind') as string;
|
||||
const product_name = form.get('product_name') as string;
|
||||
const active_substance = form.get('active_substance') as string;
|
||||
|
|
@ -24,7 +25,7 @@ export const actions: Actions = {
|
|||
product_name,
|
||||
active_substance: active_substance || undefined,
|
||||
notes: notes || undefined
|
||||
});
|
||||
}, getSessionApiOptions(event));
|
||||
} catch (error) {
|
||||
return fail(500, { error: (error as Error).message });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { getProductSummaries } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
try {
|
||||
const products = await getProductSummaries();
|
||||
const products = await getProductSummaries({}, getSessionApiOptions(event));
|
||||
return { products, loadError: null };
|
||||
} catch (error) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ import {
|
|||
updateInventory as apiUpdateInventory,
|
||||
deleteInventory as apiDeleteInventory
|
||||
} from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import { error, fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { params } = event;
|
||||
try {
|
||||
const product = await getProduct(params.id);
|
||||
const product = await getProduct(params.id, getSessionApiOptions(event));
|
||||
return { product };
|
||||
} catch {
|
||||
error(404, 'Product not found');
|
||||
|
|
@ -78,8 +80,9 @@ function parseContextRules(
|
|||
}
|
||||
|
||||
export const actions: Actions = {
|
||||
update: async ({ params, request }) => {
|
||||
const form = await request.formData();
|
||||
update: async (event) => {
|
||||
const { params } = event;
|
||||
const form = await event.request.formData();
|
||||
|
||||
const name = form.get('name') as string;
|
||||
const brand = form.get('brand') as string;
|
||||
|
|
@ -163,24 +166,25 @@ export const actions: Actions = {
|
|||
body.context_rules = parseContextRules(form) ?? null;
|
||||
|
||||
try {
|
||||
const product = await updateProduct(params.id, body);
|
||||
const product = await updateProduct(params.id, body, getSessionApiOptions(event));
|
||||
return { success: true, product };
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
}
|
||||
},
|
||||
|
||||
delete: async ({ params }) => {
|
||||
delete: async (event) => {
|
||||
try {
|
||||
await deleteProduct(params.id);
|
||||
await deleteProduct(event.params.id, getSessionApiOptions(event));
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
}
|
||||
redirect(303, '/products');
|
||||
},
|
||||
|
||||
addInventory: async ({ params, request }) => {
|
||||
const form = await request.formData();
|
||||
addInventory: async (event) => {
|
||||
const { params } = event;
|
||||
const form = await event.request.formData();
|
||||
const body: Record<string, unknown> = {
|
||||
is_opened: form.get('is_opened') === 'true'
|
||||
};
|
||||
|
|
@ -195,15 +199,15 @@ export const actions: Actions = {
|
|||
const notes = form.get('notes');
|
||||
if (notes) body.notes = notes;
|
||||
try {
|
||||
await createInventory(params.id, body);
|
||||
await createInventory(params.id, body, getSessionApiOptions(event));
|
||||
return { inventoryAdded: true };
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
}
|
||||
},
|
||||
|
||||
updateInventory: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
updateInventory: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const inventoryId = form.get('inventory_id') as string;
|
||||
if (!inventoryId) return fail(400, { error: 'Missing inventory_id' });
|
||||
const body: Record<string, unknown> = {
|
||||
|
|
@ -220,19 +224,19 @@ export const actions: Actions = {
|
|||
const notes = form.get('notes');
|
||||
body.notes = notes || null;
|
||||
try {
|
||||
await apiUpdateInventory(inventoryId, body);
|
||||
await apiUpdateInventory(inventoryId, body, getSessionApiOptions(event));
|
||||
return { inventoryUpdated: true };
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
}
|
||||
},
|
||||
|
||||
deleteInventory: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
deleteInventory: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const inventoryId = form.get('inventory_id') as string;
|
||||
if (!inventoryId) return fail(400, { error: 'Missing inventory_id' });
|
||||
try {
|
||||
await apiDeleteInventory(inventoryId);
|
||||
await apiDeleteInventory(inventoryId, getSessionApiOptions(event));
|
||||
return { inventoryDeleted: true };
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { createProduct } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
|
|
@ -66,8 +67,8 @@ function parseContextRules(
|
|||
}
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
default: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
|
||||
const name = form.get('name') as string;
|
||||
const brand = form.get('brand') as string;
|
||||
|
|
@ -162,7 +163,7 @@ export const actions: Actions = {
|
|||
if (contextRules) payload.context_rules = contextRules;
|
||||
|
||||
try {
|
||||
await createProduct(payload);
|
||||
await createProduct(payload, getSessionApiOptions(event));
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import { getProfile, updateProfile } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const profile = await getProfile();
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const profile = await getProfile(getSessionApiOptions(event));
|
||||
return { profile };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
save: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
save: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const birth_date_raw = String(form.get('birth_date') ?? '').trim();
|
||||
const sex_at_birth_raw = String(form.get('sex_at_birth') ?? '').trim();
|
||||
|
||||
|
|
@ -20,7 +22,7 @@ export const actions: Actions = {
|
|||
}
|
||||
|
||||
try {
|
||||
const profile = await updateProfile(payload);
|
||||
const profile = await updateProfile(payload, getSessionApiOptions(event));
|
||||
return { saved: true, profile };
|
||||
} catch (e) {
|
||||
return fail(502, { error: (e as Error).message });
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { getRoutines } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ url }) => {
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { url } = event;
|
||||
const from_date = url.searchParams.get('from_date') ?? recentDate(30);
|
||||
const routines = await getRoutines({ from_date });
|
||||
const routines = await getRoutines({ from_date }, getSessionApiOptions(event));
|
||||
return { routines };
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import { addRoutineStep, deleteRoutine, deleteRoutineStep, getProductSummaries, getRoutine } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import { error, fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { params } = event;
|
||||
const apiOptions = getSessionApiOptions(event);
|
||||
try {
|
||||
const [routine, products] = await Promise.all([getRoutine(params.id), getProductSummaries()]);
|
||||
const [routine, products] = await Promise.all([
|
||||
getRoutine(params.id, apiOptions),
|
||||
getProductSummaries({}, apiOptions)
|
||||
]);
|
||||
return { routine, products };
|
||||
} catch {
|
||||
error(404, 'Routine not found');
|
||||
|
|
@ -12,8 +18,9 @@ export const load: PageServerLoad = async ({ params }) => {
|
|||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
addStep: async ({ params, request }) => {
|
||||
const form = await request.formData();
|
||||
addStep: async (event) => {
|
||||
const { params } = event;
|
||||
const form = await event.request.formData();
|
||||
const product_id = form.get('product_id') as string;
|
||||
const order_index = Number(form.get('order_index') ?? 0);
|
||||
const dose = form.get('dose') as string;
|
||||
|
|
@ -25,27 +32,27 @@ export const actions: Actions = {
|
|||
if (region) body.region = region;
|
||||
|
||||
try {
|
||||
await addRoutineStep(params.id, body);
|
||||
await addRoutineStep(params.id, body, getSessionApiOptions(event));
|
||||
return { stepAdded: true };
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
}
|
||||
},
|
||||
|
||||
removeStep: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
removeStep: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const step_id = form.get('step_id') as string;
|
||||
try {
|
||||
await deleteRoutineStep(step_id);
|
||||
await deleteRoutineStep(step_id, getSessionApiOptions(event));
|
||||
return { stepRemoved: true };
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
}
|
||||
},
|
||||
|
||||
delete: async ({ params }) => {
|
||||
delete: async (event) => {
|
||||
try {
|
||||
await deleteRoutine(params.id);
|
||||
await deleteRoutine(event.params.id, getSessionApiOptions(event));
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import { enhance } from '$app/forms';
|
||||
import { resolve } from '$app/paths';
|
||||
import { dragHandleZone, dragHandle, type DndEvent } from 'svelte-dnd-action';
|
||||
import { updateRoutineStep } from '$lib/api';
|
||||
import type { GroomingAction, RoutineStep } from '$lib/types';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
|
|
@ -32,6 +31,21 @@
|
|||
// ── Drag & drop reordering ────────────────────────────────────
|
||||
let dndSaving = $state(false);
|
||||
|
||||
async function updateStep(stepId: string, payload: Record<string, unknown>): Promise<RoutineStep> {
|
||||
const response = await fetch(`/api/routines/steps/${stepId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const detail = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
throw new Error(detail?.detail ?? response.statusText);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function handleConsider(e: CustomEvent<DndEvent<RoutineStep>>) {
|
||||
steps = e.detail.items;
|
||||
}
|
||||
|
|
@ -45,9 +59,7 @@
|
|||
if (changed.length) {
|
||||
dndSaving = true;
|
||||
try {
|
||||
await Promise.all(
|
||||
changed.map((s) => updateRoutineStep(s.id, { order_index: s.order_index }))
|
||||
);
|
||||
await Promise.all(changed.map((s) => updateStep(s.id, { order_index: s.order_index })));
|
||||
} finally {
|
||||
dndSaving = false;
|
||||
}
|
||||
|
|
@ -85,7 +97,7 @@
|
|||
payload.action_type = editDraft.action_type;
|
||||
payload.action_notes = editDraft.action_notes || null;
|
||||
}
|
||||
const updatedStep = await updateRoutineStep(step.id, payload);
|
||||
const updatedStep = await updateStep(step.id, payload);
|
||||
steps = steps.map((s) => (s.id === step.id ? { ...s, ...updatedStep } : s));
|
||||
editingStepId = null;
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -3,17 +3,19 @@ import {
|
|||
getGroomingSchedule,
|
||||
updateGroomingScheduleEntry
|
||||
} from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const schedule = await getGroomingSchedule();
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const schedule = await getGroomingSchedule(getSessionApiOptions(event));
|
||||
return { schedule };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
update: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
update: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const id = form.get('id') as string;
|
||||
if (!id) return fail(400, { error: 'Missing id' });
|
||||
const body: Record<string, unknown> = {};
|
||||
|
|
@ -24,19 +26,19 @@ export const actions: Actions = {
|
|||
const notes = (form.get('notes') as string)?.trim();
|
||||
body.notes = notes || null;
|
||||
try {
|
||||
await updateGroomingScheduleEntry(id, body);
|
||||
await updateGroomingScheduleEntry(id, body, getSessionApiOptions(event));
|
||||
return { updated: true };
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
}
|
||||
},
|
||||
|
||||
delete: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
delete: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const id = form.get('id') as string;
|
||||
if (!id) return fail(400, { error: 'Missing id' });
|
||||
try {
|
||||
await deleteGroomingScheduleEntry(id);
|
||||
await deleteGroomingScheduleEntry(id, getSessionApiOptions(event));
|
||||
return { deleted: true };
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { createGroomingScheduleEntry } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
|
|
@ -7,8 +8,8 @@ export const load: PageServerLoad = async () => {
|
|||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
default: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const day_of_week = form.get('day_of_week');
|
||||
const action = form.get('action') as string;
|
||||
|
||||
|
|
@ -24,7 +25,7 @@ export const actions: Actions = {
|
|||
if (notes) body.notes = notes;
|
||||
|
||||
try {
|
||||
await createGroomingScheduleEntry(body);
|
||||
await createGroomingScheduleEntry(body, getSessionApiOptions(event));
|
||||
} catch (error) {
|
||||
return fail(500, { error: (error as Error).message });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { createRoutine } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
|
|
@ -7,8 +8,8 @@ export const load: PageServerLoad = async () => {
|
|||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
default: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const routine_date = form.get('routine_date') as string;
|
||||
const part_of_day = form.get('part_of_day') as string;
|
||||
const notes = form.get('notes') as string;
|
||||
|
|
@ -19,7 +20,10 @@ export const actions: Actions = {
|
|||
|
||||
let routine;
|
||||
try {
|
||||
routine = await createRoutine({ routine_date, part_of_day, notes: notes || undefined });
|
||||
routine = await createRoutine(
|
||||
{ routine_date, part_of_day, notes: notes || undefined },
|
||||
getSessionApiOptions(event)
|
||||
);
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import { addRoutineStep, createRoutine, getProductSummaries, suggestBatch, suggestRoutine } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const products = await getProductSummaries();
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const apiOptions = getSessionApiOptions(event);
|
||||
const products = await getProductSummaries({}, apiOptions);
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
return { products, today };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
suggest: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
suggest: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const routine_date = form.get('routine_date') as string;
|
||||
const part_of_day = form.get('part_of_day') as 'am' | 'pm';
|
||||
const notes = (form.get('notes') as string) || undefined;
|
||||
|
|
@ -29,15 +32,15 @@ export const actions: Actions = {
|
|||
notes,
|
||||
include_minoxidil_beard,
|
||||
leaving_home
|
||||
});
|
||||
}, getSessionApiOptions(event));
|
||||
return { suggestion, routine_date, part_of_day };
|
||||
} catch (e) {
|
||||
return fail(502, { error: (e as Error).message });
|
||||
}
|
||||
},
|
||||
|
||||
suggestBatch: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
suggestBatch: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const from_date = form.get('from_date') as string;
|
||||
const to_date = form.get('to_date') as string;
|
||||
const notes = (form.get('notes') as string) || undefined;
|
||||
|
|
@ -55,15 +58,18 @@ export const actions: Actions = {
|
|||
}
|
||||
|
||||
try {
|
||||
const batch = await suggestBatch({ from_date, to_date, notes, include_minoxidil_beard, minimize_products });
|
||||
const batch = await suggestBatch(
|
||||
{ from_date, to_date, notes, include_minoxidil_beard, minimize_products },
|
||||
getSessionApiOptions(event)
|
||||
);
|
||||
return { batch, from_date, to_date };
|
||||
} catch (e) {
|
||||
return fail(502, { error: (e as Error).message });
|
||||
}
|
||||
},
|
||||
|
||||
save: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
save: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const routine_date = form.get('routine_date') as string;
|
||||
const part_of_day = form.get('part_of_day') as string;
|
||||
const steps_json = form.get('steps') as string;
|
||||
|
|
@ -86,7 +92,8 @@ export const actions: Actions = {
|
|||
|
||||
let routineId: string;
|
||||
try {
|
||||
const routine = await createRoutine({ routine_date, part_of_day });
|
||||
const apiOptions = getSessionApiOptions(event);
|
||||
const routine = await createRoutine({ routine_date, part_of_day }, apiOptions);
|
||||
routineId = routine.id;
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const s = steps[i];
|
||||
|
|
@ -96,7 +103,7 @@ export const actions: Actions = {
|
|||
action_type: s.action_type || undefined,
|
||||
action_notes: s.action_notes || undefined,
|
||||
region: s.region || undefined
|
||||
});
|
||||
}, apiOptions);
|
||||
}
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
|
|
@ -104,8 +111,8 @@ export const actions: Actions = {
|
|||
redirect(303, `/routines/${routineId}`);
|
||||
},
|
||||
|
||||
saveBatch: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
saveBatch: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const days_json = form.get('days') as string;
|
||||
|
||||
if (!days_json) {
|
||||
|
|
@ -134,11 +141,12 @@ export const actions: Actions = {
|
|||
}
|
||||
|
||||
try {
|
||||
const apiOptions = getSessionApiOptions(event);
|
||||
for (const day of days) {
|
||||
for (const part_of_day of ['am', 'pm'] as const) {
|
||||
const steps = part_of_day === 'am' ? day.am_steps : day.pm_steps;
|
||||
if (steps.length === 0) continue;
|
||||
const routine = await createRoutine({ routine_date: day.date, part_of_day });
|
||||
const routine = await createRoutine({ routine_date: day.date, part_of_day }, apiOptions);
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const s = steps[i];
|
||||
await addRoutineStep(routine.id, {
|
||||
|
|
@ -147,7 +155,7 @@ export const actions: Actions = {
|
|||
action_type: s.action_type || undefined,
|
||||
action_notes: s.action_notes || undefined,
|
||||
region: s.region || undefined
|
||||
});
|
||||
}, apiOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import { deleteSkinSnapshot, getSkinSnapshots, updateSkinSnapshot } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ url }) => {
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { url } = event;
|
||||
const overall_state = url.searchParams.get('overall_state') ?? undefined;
|
||||
const snapshots = await getSkinSnapshots({ overall_state });
|
||||
const snapshots = await getSkinSnapshots({ overall_state }, getSessionApiOptions(event));
|
||||
return { snapshots, overall_state };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
update: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
update: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const id = form.get('id') as string;
|
||||
const snapshot_date = form.get('snapshot_date') as string;
|
||||
const overall_state = form.get('overall_state') as string;
|
||||
|
|
@ -50,19 +53,19 @@ export const actions: Actions = {
|
|||
if (sebum_cheeks) body.sebum_cheeks = Number(sebum_cheeks);
|
||||
|
||||
try {
|
||||
await updateSkinSnapshot(id, body);
|
||||
await updateSkinSnapshot(id, body, getSessionApiOptions(event));
|
||||
return { updated: true };
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
}
|
||||
},
|
||||
|
||||
delete: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
delete: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const id = form.get('id') as string;
|
||||
if (!id) return fail(400, { error: 'Missing id' });
|
||||
try {
|
||||
await deleteSkinSnapshot(id);
|
||||
await deleteSkinSnapshot(id, getSessionApiOptions(event));
|
||||
return { deleted: true };
|
||||
} catch (e) {
|
||||
return fail(500, { error: (e as Error).message });
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { createSkinSnapshot } from '$lib/api';
|
||||
import { getSessionApiOptions } from '$lib/server/api';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
|
|
@ -7,8 +8,8 @@ export const load: PageServerLoad = async () => {
|
|||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
default: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const snapshot_date = form.get('snapshot_date') as string;
|
||||
const overall_state = form.get('overall_state') as string;
|
||||
const texture = form.get('texture') as string;
|
||||
|
|
@ -52,7 +53,7 @@ export const actions: Actions = {
|
|||
if (sebum_cheeks) body.sebum_cheeks = Number(sebum_cheeks);
|
||||
|
||||
try {
|
||||
await createSkinSnapshot(body);
|
||||
await createSkinSnapshot(body, getSessionApiOptions(event));
|
||||
} catch (error) {
|
||||
return fail(500, { error: (error as Error).message });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { analyzeSkinPhotos } from '$lib/api';
|
||||
import FlashMessages from '$lib/components/FlashMessages.svelte';
|
||||
import LabeledInputField from '$lib/components/forms/LabeledInputField.svelte';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
|
|
@ -73,6 +72,20 @@
|
|||
let aiLoading = $state(false);
|
||||
let aiError = $state('');
|
||||
|
||||
async function requestSkinAnalysis(formData: FormData) {
|
||||
const response = await fetch('/api/skin/analyze-photos', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const detail = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
throw new Error(detail?.detail ?? response.statusText);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const flashMessages = $derived(form?.error ? [{ kind: 'error' as const, text: form.error }] : []);
|
||||
|
||||
$effect(() => {
|
||||
|
|
@ -103,7 +116,12 @@
|
|||
aiLoading = true;
|
||||
aiError = '';
|
||||
try {
|
||||
const result = await analyzeSkinPhotos(selectedFiles);
|
||||
const formData = new FormData();
|
||||
for (const file of selectedFiles) {
|
||||
formData.append('photos', file);
|
||||
}
|
||||
|
||||
const result = await requestSkinAnalysis(formData);
|
||||
if (result.overall_state) overallState = result.overall_state;
|
||||
if (result.texture) texture = result.texture;
|
||||
if (result.skin_type) skinType = result.skin_type;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue