refinement phase 4

This commit is contained in:
hamid
2026-07-13 12:29:00 +03:30
parent 314763f764
commit 64f6aa45c9
27 changed files with 308 additions and 243 deletions
+38 -14
View File
@@ -25,23 +25,34 @@ async function orNull<T>(promise: Promise<T>): Promise<T | null> {
}
}
// The wire DTOs carry no avatar/name yet (REQ-006/007); reads default the augmented fields.
function toNurseProfile(dto: NurseProfileDto): NurseProfile {
return { ...dto, avatarUrl: null };
/** REQ-006 (delivered): both profile DTOs now carry `avatarUrl`; the customer DTO also carries `preferredLanguage`. */
interface NurseProfileWire extends NurseProfileDto {
avatarUrl: string | null;
}
function toCustomerProfile(dto: CustomerProfileDto): CustomerProfile {
return { ...dto, firstName: null, lastName: null, preferredLanguage: 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). Selected once
* USE_PROFILES_MOCK is false and the avatar/name gaps land. `uploadAvatar` has no route yet
* (REQ-006) and the JSON-only fetch layer can't send multipart — it stays mock-only.
* 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<CustomerProfileDto>>(`${BASE}/customer_profiles/me`).then((env) =>
clientFetch<ApiEnvelope<CustomerProfileWire>>(`${BASE}/customer_profiles/me`).then((env) =>
toCustomerProfile(unwrap(env)),
),
),
@@ -49,11 +60,15 @@ export const profilesClientApi: ProfilesApi = {
upsertCustomerProfile: async (input: UpsertCustomerProfileInput) =>
toCustomerProfile(
unwrap(
await clientFetch<ApiEnvelope<CustomerProfileDto>>(`${BASE}/customer_profiles/upsert`, {
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,
}),
}),
),
@@ -61,13 +76,13 @@ export const profilesClientApi: ProfilesApi = {
getNurseProfile: async () =>
orNull(
clientFetch<ApiEnvelope<NurseProfileDto>>(`${BASE}/nurse_profiles/me`).then((env) => toNurseProfile(unwrap(env))),
clientFetch<ApiEnvelope<NurseProfileWire>>(`${BASE}/nurse_profiles/me`).then((env) => toNurseProfile(unwrap(env))),
),
upsertNurseProfile: async (input: UpsertNurseProfileInput) =>
toNurseProfile(
unwrap(
await clientFetch<ApiEnvelope<NurseProfileDto>>(`${BASE}/nurse_profiles/upsert`, {
await clientFetch<ApiEnvelope<NurseProfileWire>>(`${BASE}/nurse_profiles/upsert`, {
method: 'POST',
body: JSON.stringify({
bio: input.bio,
@@ -80,7 +95,16 @@ export const profilesClientApi: ProfilesApi = {
),
),
uploadAvatar: async (): Promise<AvatarUploadResult> => {
throw new ApiError(501, 'Avatar upload has no backend route yet (REQ-006); served by the mock.');
// 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,
}),
);
},
};
+1 -1
View File
@@ -5,7 +5,7 @@
* (REQ-006 / REQ-007), so this phase demos behind the mock. Flip to false once those land —
* no hook/component changes (see dev/shared-working-context/reports/mocks-registry.md).
*/
export const USE_PROFILES_MOCK = true;
export const USE_PROFILES_MOCK = false;
/** Profiles are stable within a session; revisiting a screen shouldn't refetch. */
export const PROFILE_STALE_TIME = 60_000;