frontend phase 2: onboarding & profiles — customer/patient, nurse profile & bank
Turns a logged-in user into a usable account, consuming the b3 identity-profiles
contract behind the services/{domain} seam.
Services (mock default true; real HTTP clients wired for a one-line flip):
- services/patients: rewritten to b3 PatientDto + client-augmented relation/conditions;
full CRUD, optimistic soft-archive, cache-splice on create, age<->birthDate helper.
- services/profiles: customer + nurse profile get/upsert + avatar (404->null mapping).
- services/nurse: payout bank accounts + IBAN(Sheba) util + pending-only polling.
Screens: A3->A4 onboarding wizard, E1 patients list/CRUD, A5 home (first-login gate +
nudge), customer profile (no national-ID), nurse profile bootstrap (unverified
placeholder), nurse bank settings (pending/verified/mismatch + make-primary).
Shared composites (each tested): GenderToggle, ConditionChips, RelationSelect,
PatientForm, PatientCard, BankStatusPanel; reuses f0 StepperHeader/StatusChip/PhoneField.
Adds onboarding/home/profile/nurseProfile/bank i18n namespaces (both locales, in sync),
the --bal-primary-soft token, and nurse sidebar Profile + Bank entries.
Contract gaps filed: REQ-005 (patient relation/conditions), REQ-006 (avatar route),
REQ-007 (customer name/language). Gate: check + 112 tests + build all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 };
|
||||
}
|
||||
function toCustomerProfile(dto: CustomerProfileDto): CustomerProfile {
|
||||
return { ...dto, firstName: null, lastName: null, 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.
|
||||
*/
|
||||
export const profilesClientApi: ProfilesApi = {
|
||||
getCustomerProfile: async () =>
|
||||
orNull(
|
||||
clientFetch<ApiEnvelope<CustomerProfileDto>>(`${BASE}/customer_profiles/me`).then((env) =>
|
||||
toCustomerProfile(unwrap(env)),
|
||||
),
|
||||
),
|
||||
|
||||
upsertCustomerProfile: async (input: UpsertCustomerProfileInput) =>
|
||||
toCustomerProfile(
|
||||
unwrap(
|
||||
await clientFetch<ApiEnvelope<CustomerProfileDto>>(`${BASE}/customer_profiles/upsert`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
defaultEmergencyContactName: input.defaultEmergencyContactName,
|
||||
defaultEmergencyContactPhone: input.defaultEmergencyContactPhone,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
getNurseProfile: async () =>
|
||||
orNull(
|
||||
clientFetch<ApiEnvelope<NurseProfileDto>>(`${BASE}/nurse_profiles/me`).then((env) => toNurseProfile(unwrap(env))),
|
||||
),
|
||||
|
||||
upsertNurseProfile: async (input: UpsertNurseProfileInput) =>
|
||||
toNurseProfile(
|
||||
unwrap(
|
||||
await clientFetch<ApiEnvelope<NurseProfileDto>>(`${BASE}/nurse_profiles/upsert`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
bio: input.bio,
|
||||
yearsOfExperience: input.yearsOfExperience,
|
||||
educationLevel: input.educationLevel,
|
||||
educationField: input.educationField,
|
||||
specializationsJson: input.specializationsJson,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
uploadAvatar: async (): Promise<AvatarUploadResult> => {
|
||||
throw new ApiError(501, 'Avatar upload has no backend route yet (REQ-006); served by the mock.');
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user