import { clientFetch } from '@/lib/api/client'; import { ApiError } from '@/lib/api/errors'; import { unwrap, type ApiEnvelope } from '@/lib/api/types'; import type { AvatarUploadResult, CustomerProfile, CustomerProfileDto, NurseProfile, NurseProfileDto, ProfilesApi, UpsertCustomerProfileInput, UpsertNurseProfileInput, } from '../types'; const BASE = '/api/v1'; // A caller with no profile yet gets a 404 from the GET — map that to `null` (an empty form), // not an error. Any other status propagates. async function orNull(promise: Promise): Promise { try { return await promise; } catch (error) { if (error instanceof ApiError && error.status === 404) return null; throw error; } } /** REQ-006 (delivered): both profile DTOs now carry `avatarUrl`; the customer DTO also carries `preferredLanguage`. */ interface NurseProfileWire extends NurseProfileDto { avatarUrl: string | null; } interface CustomerProfileWire extends CustomerProfileDto { avatarUrl: string | null; preferredLanguage: string | null; } function toNurseProfile(dto: NurseProfileWire): NurseProfile { return { ...dto, avatarUrl: dto.avatarUrl ?? null }; } // The customer name lives on `/me` (REQ-007 design), not on `CustomerProfileDto` — the profile screen // sources first/last name from `useMe`. Here we carry the served `preferredLanguage`; name stays null. function toCustomerProfile(dto: CustomerProfileWire): CustomerProfile { return { ...dto, firstName: null, lastName: null, preferredLanguage: dto.preferredLanguage ?? null }; } /** * Real HTTP implementation of the ProfilesApi seam (b3 action-style routes). PRIMARY once * USE_PROFILES_MOCK is false (refinement-phase-4; REQ-006/007 delivered). Avatar upload posts multipart * to the b3 `nurse_profiles/avatar` route (the JSON-only default is bypassed for `FormData` bodies — * see `lib/api/client.ts`); the customer name/language reach the server via the upsert body. */ export const profilesClientApi: ProfilesApi = { getCustomerProfile: async () => orNull( clientFetch>(`${BASE}/customer_profiles/me`).then((env) => toCustomerProfile(unwrap(env)), ), ), upsertCustomerProfile: async (input: UpsertCustomerProfileInput) => toCustomerProfile( unwrap( await clientFetch>(`${BASE}/customer_profiles/upsert`, { method: 'POST', body: JSON.stringify({ defaultEmergencyContactName: input.defaultEmergencyContactName, defaultEmergencyContactPhone: input.defaultEmergencyContactPhone, // REQ-007 (delivered): name + preferred language are accepted on the upsert command. firstName: input.firstName ?? null, lastName: input.lastName ?? null, preferredLanguage: input.preferredLanguage ?? null, }), }), ), ), getNurseProfile: async () => orNull( clientFetch>(`${BASE}/nurse_profiles/me`).then((env) => toNurseProfile(unwrap(env))), ), upsertNurseProfile: async (input: UpsertNurseProfileInput) => toNurseProfile( unwrap( await clientFetch>(`${BASE}/nurse_profiles/upsert`, { method: 'POST', body: JSON.stringify({ bio: input.bio, yearsOfExperience: input.yearsOfExperience, educationLevel: input.educationLevel, educationField: input.educationField, specializationsJson: input.specializationsJson, }), }), ), ), // REQ-006 (delivered): only the nurse profile screen uploads an avatar today; it persists immediately // via the dedicated multipart route and the returned URL is echoed for display + read back on reload. uploadAvatar: async (file: File): Promise => { const form = new FormData(); form.append('file', file); return unwrap( await clientFetch>(`${BASE}/nurse_profiles/avatar`, { method: 'POST', body: form, }), ); }, // ui-phase-8: the real, previously-unwired go-live switch — flips `is_accepting_bookings` // independently of `is_verified`; the server reindexes the nurse's search rows in-transaction. setAcceptingBookings: async (accepting: boolean): Promise => { await clientFetch>(`${BASE}/nurse_profiles/set_accepting_bookings`, { method: 'POST', body: JSON.stringify({ accepting }), }); }, };