backend phase 13 & frontend phase 6
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { clientFetch } from '@/lib/api/client';
|
||||
import { unwrap, type ApiEnvelope, type Paginated } from '@/lib/api/types';
|
||||
import type { PriceUnit } from '@/services/catalog/types';
|
||||
import type { TrustBadge } from '@/services/verification/types';
|
||||
import { SEARCH_PAGE_SIZE } from '../constants';
|
||||
import type {
|
||||
NurseGender,
|
||||
NurseProfile,
|
||||
NurseSearchFilters,
|
||||
NurseSearchResult,
|
||||
SearchApi,
|
||||
} from '../types';
|
||||
|
||||
const SEARCH_BASE = '/api/v1/search';
|
||||
const NURSES_BASE = '/api/v1/nurses';
|
||||
|
||||
/** The b7 `NurseSearchResultDto` (the projected index row) — the exact wire shape we map from. */
|
||||
interface NurseSearchResultDto {
|
||||
variantId: number;
|
||||
nurseId: number;
|
||||
serviceCategoryId: number;
|
||||
price: string;
|
||||
priceUnit: PriceUnit;
|
||||
nurseGender: NurseGender;
|
||||
averageRating: number;
|
||||
totalReviews: number;
|
||||
totalCompletedBookings: number;
|
||||
cityId: number;
|
||||
districtId: number | null;
|
||||
}
|
||||
|
||||
/** The INO-membership credential type code (see b6 verification). */
|
||||
const INO_MEMBERSHIP_CODE = 'ino_membership';
|
||||
|
||||
/**
|
||||
* Real HTTP implementation of the `SearchApi` seam (b7 `search/nurses`, b6 trust badge). Routes are
|
||||
* action-style + snake_case; query params are snake_case per the contract; JSON fields are camelCase and
|
||||
* `clientFetch` returns the raw envelope, so we `unwrap()`.
|
||||
*
|
||||
* NOT the primary implementation this phase (`USE_SEARCH_MOCK = true`): b7's index row omits the nurse
|
||||
* **display name, avatar, and distance** the C2 card renders, and there is **no** aggregated
|
||||
* nurse-profile endpoint (name/bio/specialties/full services list/latest review) for C3 — only the b6
|
||||
* trust badge is public. Both gaps are filed in
|
||||
* `dev/shared-working-context/frontend/requests/for-backend.md`. This client maps everything b7/b6
|
||||
* currently provide (leaving the missing fields blank) so the swap is a single config flip once the
|
||||
* backend lands the join + profile route.
|
||||
*/
|
||||
export const searchClientApi: SearchApi = {
|
||||
searchNurses: async (filters: NurseSearchFilters): Promise<Paginated<NurseSearchResult>> => {
|
||||
const query = new URLSearchParams();
|
||||
query.set('service_category_id', String(filters.serviceCategoryId));
|
||||
query.set('city_id', String(filters.cityId));
|
||||
if (filters.districtId != null) query.set('district_id', String(filters.districtId));
|
||||
if (filters.nurseGender) query.set('nurse_gender', filters.nurseGender);
|
||||
if (filters.priceMin) query.set('min_price', filters.priceMin);
|
||||
if (filters.priceMax) query.set('max_price', filters.priceMax);
|
||||
if (filters.priceUnit) query.set('price_unit', filters.priceUnit);
|
||||
query.set('page', String(filters.page || 1));
|
||||
query.set('page_size', String(filters.pageSize || SEARCH_PAGE_SIZE));
|
||||
|
||||
const paged = unwrap(
|
||||
await clientFetch<ApiEnvelope<Paginated<NurseSearchResultDto>>>(
|
||||
`${SEARCH_BASE}/nurses?${query.toString()}`,
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
...paged,
|
||||
items: paged.items.map((dto) => ({
|
||||
nurseId: dto.nurseId,
|
||||
variantId: dto.variantId,
|
||||
serviceCategoryId: dto.serviceCategoryId,
|
||||
// Gap (filed): b7 does not yet join the nurse's name/avatar; the card falls back to a label.
|
||||
nurseName: '',
|
||||
avatarUrl: null,
|
||||
// Every returned row is searchable by the index invariant.
|
||||
isVerified: true,
|
||||
averageRating: dto.averageRating,
|
||||
totalReviews: dto.totalReviews,
|
||||
totalCompletedBookings: dto.totalCompletedBookings,
|
||||
// Gap (filed): no geo-distance in the index row yet.
|
||||
distanceKm: null,
|
||||
priceFromIrr: dto.price,
|
||||
priceUnit: dto.priceUnit,
|
||||
nurseGender: dto.nurseGender,
|
||||
cityId: dto.cityId,
|
||||
districtId: dto.districtId,
|
||||
})),
|
||||
};
|
||||
},
|
||||
|
||||
getNurseProfile: async (nurseId: number): Promise<NurseProfile> => {
|
||||
// Only the public trust badge is available today; the aggregated profile (name/bio/specialties/
|
||||
// services list/latest review) is filed for the backend. Compose what b6 exposes; leave the rest blank.
|
||||
const badge = unwrap(
|
||||
await clientFetch<ApiEnvelope<TrustBadge>>(`${NURSES_BASE}/${nurseId}/trust_badge`),
|
||||
);
|
||||
|
||||
return {
|
||||
nurseId: badge.nurseId,
|
||||
nurseName: '',
|
||||
avatarUrl: null,
|
||||
bio: null,
|
||||
yearsExperience: null,
|
||||
averageRating: 0,
|
||||
totalReviews: 0,
|
||||
totalCompletedBookings: 0,
|
||||
isVerified: badge.isVerified,
|
||||
inoMembership: badge.credentialTypes.includes(INO_MEMBERSHIP_CODE),
|
||||
attributeChips: badge.credentialTypes,
|
||||
services: [],
|
||||
latestReview: null,
|
||||
nurseGender: 'female',
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user