Files
baya-monorepo/client/src/services/profiles/apis/clientApi.ts
T
2026-07-19 13:57:11 +03:30

120 lines
4.5 KiB
TypeScript

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<T>(promise: Promise<T>): Promise<T | null> {
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<ApiEnvelope<CustomerProfileWire>>(`${BASE}/customer_profiles/me`).then((env) =>
toCustomerProfile(unwrap(env)),
),
),
upsertCustomerProfile: async (input: UpsertCustomerProfileInput) =>
toCustomerProfile(
unwrap(
await clientFetch<ApiEnvelope<CustomerProfileWire>>(`${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<ApiEnvelope<NurseProfileWire>>(`${BASE}/nurse_profiles/me`).then((env) => toNurseProfile(unwrap(env))),
),
upsertNurseProfile: async (input: UpsertNurseProfileInput) =>
toNurseProfile(
unwrap(
await clientFetch<ApiEnvelope<NurseProfileWire>>(`${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<AvatarUploadResult> => {
const form = new FormData();
form.append('file', file);
return unwrap(
await clientFetch<ApiEnvelope<AvatarUploadResult>>(`${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<void> => {
await clientFetch<ApiEnvelope<boolean>>(`${BASE}/nurse_profiles/set_accepting_bookings`, {
method: 'POST',
body: JSON.stringify({ accepting }),
});
},
};