ui phase 8

This commit is contained in:
hamid
2026-07-19 13:57:11 +03:30
parent edc38543fd
commit 1ef4feb911
41 changed files with 2113 additions and 667 deletions
@@ -107,4 +107,13 @@ export const profilesClientApi: ProfilesApi = {
}),
);
},
// 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 }),
});
},
};
@@ -70,4 +70,11 @@ export const profilesMockApi: ProfilesApi = {
// object-storage URL (REQ-006).
return { url: URL.createObjectURL(file) };
},
// Mirrors the real endpoint's flip: `isVerified` is never touched, matching the guarded server field.
setAcceptingBookings: async (accepting: boolean): Promise<void> => {
await sleep(MOCK_LATENCY_MS);
if (!nurseProfile) return;
nurseProfile = { ...nurseProfile, isAcceptingBookings: accepting };
},
};
@@ -0,0 +1,17 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { profilesApi } from '../apis';
import { profileKeys } from '../keys';
/**
* The real go-live switch (`POST nurse_profiles/set_accepting_bookings`) — flips
* `isAcceptingBookings` independently of verification. Invalidates the nurse profile so the
* activation checklist / publish gate re-render the live state from the server, never an optimistic
* "published" toast before the mutation actually succeeds.
*/
export function useSetAcceptingBookings() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (accepting: boolean) => profilesApi.setAcceptingBookings(accepting),
onSuccess: () => queryClient.invalidateQueries({ queryKey: profileKeys.nurse() }),
});
}
+1
View File
@@ -3,3 +3,4 @@ export { useUpsertCustomerProfile } from './hooks/useUpsertCustomerProfile';
export { useNurseProfile } from './hooks/useNurseProfile';
export { useUpsertNurseProfile } from './hooks/useUpsertNurseProfile';
export { useUploadAvatar } from './hooks/useUploadAvatar';
export { useSetAcceptingBookings } from './hooks/useSetAcceptingBookings';
+6
View File
@@ -73,4 +73,10 @@ export interface ProfilesApi {
getNurseProfile(): Promise<NurseProfile | null>;
upsertNurseProfile(input: UpsertNurseProfileInput): Promise<NurseProfile>;
uploadAvatar(file: File): Promise<AvatarUploadResult>;
/**
* The real, previously-unwired `POST nurse_profiles/set_accepting_bookings` — flips the
* nurse's go-live switch independently of `isVerified` (the server reindexes every one of the
* nurse's search rows in the same transaction). Never a client-side simulation of "publish".
*/
setAcceptingBookings(accepting: boolean): Promise<void>;
}
@@ -42,6 +42,12 @@ let nextStepId = 1;
let nextDocId = 1;
let boundNationalId = '';
let approvedAt: string | null = null;
// REQ-055: stamped once when the nurse first leaves `not_started` — stands in for the not-yet-served
// nurse-facing `submittedAt` (B6's timestamp line).
let submittedAt: string | null = null;
// REQ-056: the structured B5 fields, kept so a returning nurse's form hydrates instead of re-prompting
// blank — never the raw INO number, which the real server would never re-serve either.
let credentialSubmission: NonNullable<VerificationStatus['credentialSubmission']> | null = null;
const nationalIdShape = new RegExp(`^\\d{${NATIONAL_ID_LENGTH}}$`);
@@ -56,13 +62,20 @@ function setStepStatus(code: string, status: VerificationStepStatus, failureReas
/** Re-aggregate exactly as the server would: approved only when every step passes; blockers are the rest. */
function aggregate(): VerificationStatus {
if (steps.length === 0) {
return { status: 'not_started', isBookable: false, blockingSteps: [], steps: [] };
return { status: 'not_started', isBookable: false, blockingSteps: [], steps: [], submittedAt: null };
}
const blockingSteps = steps.filter((step) => step.status !== 'passed').map((step) => step.code);
const allPassed = blockingSteps.length === 0;
const anyInReview = steps.some((step) => step.status === 'in_review');
const status = allPassed ? 'approved' : anyInReview ? 'in_review' : 'pending';
return { status, isBookable: allPassed, blockingSteps, steps: steps.map((step) => ({ ...step })) };
return {
status,
isBookable: allPassed,
blockingSteps,
steps: steps.map((step) => ({ ...step })),
submittedAt,
credentialSubmission,
};
}
function seedSteps(): void {
@@ -76,6 +89,7 @@ function seedSteps(): void {
expiresAt: null,
failureReason: null,
}));
submittedAt = new Date().toISOString();
}
/* --- Admin review queue fixtures + state ---------------------------------------------------------
@@ -351,6 +365,14 @@ export const verificationMockApi: VerificationApi = {
if (input.inoNumber.trim().length === 0) {
throw new ApiError(400, 'INO number is required', 'ino_number_required');
}
// Stands in for REQ-056 — the mock persists what a real read-back would serve (never the raw number).
credentialSubmission = {
inoNumberSubmitted: true,
specialties: input.specialties,
issuingAuthority: input.issuingAuthority ?? null,
issuedAt: input.issuedAt ?? null,
expiresAt: input.expiresAt ?? null,
};
},
getTrustBadge: async (nurseId) => {
+21
View File
@@ -77,6 +77,27 @@ export interface VerificationStatus {
/** Step codes still blocking go-live. */
blockingSteps: string[];
steps: VerificationStep[];
/**
* When the nurse first submitted (left `not_started`). **Not on the nurse-facing wire yet** — the
* admin queue DTO already serves it, but `VerificationStatusDto` doesn't (REQ-055, filed by
* ui-phase-8). Mock-tolerant: `undefined` on the real path omits the B6 timestamp line rather than
* fake one.
*/
submittedAt?: string | null;
/**
* A nurse-facing read-back of the structured credential details B5 collects — so a returning nurse
* sees a submitted summary instead of blank fields. **Not on the wire yet** (REQ-011 delivered only
* the write; there is no nurse-facing read — REQ-056, filed by ui-phase-8). `inoNumberSubmitted` is a
* boolean, never the number itself — the raw value is encrypted server-side by design and never
* re-served. Mock-tolerant: `undefined` on the real path renders every B5 field blank, as today.
*/
credentialSubmission?: {
inoNumberSubmitted: boolean;
specialties: string[];
issuingAuthority: string | null;
issuedAt: string | null;
expiresAt: string | null;
} | null;
}
/** `UploadUrlResult` — a signed PUT target for a manual step's document. */