120 lines
4.5 KiB
TypeScript
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 }),
|
|
});
|
|
},
|
|
};
|