ui phase 11
This commit is contained in:
@@ -8,6 +8,7 @@ import type {
|
||||
PartnerCenterFilters,
|
||||
PartnerCenterInput,
|
||||
SponsoredBooking,
|
||||
SponsoredBookingDetail,
|
||||
SponsoredBookingFilters,
|
||||
SponsoredNurse,
|
||||
} from '../types';
|
||||
@@ -111,6 +112,9 @@ export const partnerCenterClientApi: PartnerCenterApi = {
|
||||
if (filters.status) q.set('status', filters.status);
|
||||
return unwrap(await clientFetch<ApiEnvelope<Paginated<SponsoredBooking>>>(`${API}/centers/me/bookings?${q}`));
|
||||
},
|
||||
// REQ-064 — no single-booking read in the b15 contract yet; proposed shape (sibling of the list route).
|
||||
getMySponsoredBookingDetail: async (bookingId: number) =>
|
||||
unwrap(await clientFetch<ApiEnvelope<SponsoredBookingDetail>>(`${API}/centers/me/bookings/${bookingId}`)),
|
||||
listMySettlement: async (params) => {
|
||||
const q = new URLSearchParams();
|
||||
q.set('page', String(params.page ?? 1));
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
PartnerCenterFilters,
|
||||
PartnerCenterInput,
|
||||
SponsoredBooking,
|
||||
SponsoredBookingDetail,
|
||||
SponsoredBookingFilters,
|
||||
SponsoredNurse,
|
||||
} from '../types';
|
||||
@@ -119,6 +120,44 @@ const BOOKINGS: Record<number, SponsoredBooking[]> = {
|
||||
3: [],
|
||||
};
|
||||
|
||||
/** The normal-path lifecycle ladder a booking climbs before any dispute/cancellation branch. */
|
||||
const BOOKING_STATUS_LADDER = ['confirmed', 'in_progress', 'completed', 'closed'] as const;
|
||||
|
||||
/**
|
||||
* A synthetic 2-4-step status timeline consistent with the booking's current `status` (REQ-064 — the wire
|
||||
* has no timeline endpoint yet). Earlier steps get earlier (larger days-ago) timestamps than later ones.
|
||||
*/
|
||||
function buildBookingTimeline(status: string): { status: string; occurredAt: string }[] {
|
||||
if (status === 'pending_payment') return [{ status: 'pending_payment', occurredAt: isoDaysAgo(1) }];
|
||||
if (status === 'cancelled') {
|
||||
return [
|
||||
{ status: 'confirmed', occurredAt: isoDaysAgo(4) },
|
||||
{ status: 'cancelled', occurredAt: isoDaysAgo(3) },
|
||||
];
|
||||
}
|
||||
if (status === 'disputed') {
|
||||
return [
|
||||
{ status: 'confirmed', occurredAt: isoDaysAgo(6) },
|
||||
{ status: 'in_progress', occurredAt: isoDaysAgo(4) },
|
||||
{ status: 'completed', occurredAt: isoDaysAgo(3) },
|
||||
{ status: 'disputed', occurredAt: isoDaysAgo(1) },
|
||||
];
|
||||
}
|
||||
const stepIndex = BOOKING_STATUS_LADDER.indexOf(status as (typeof BOOKING_STATUS_LADDER)[number]);
|
||||
if (stepIndex === -1) return [{ status, occurredAt: isoDaysAgo(1) }];
|
||||
return BOOKING_STATUS_LADDER.slice(0, stepIndex + 1).map((s, i) => ({
|
||||
status: s,
|
||||
occurredAt: isoDaysAgo(stepIndex - i + 1),
|
||||
}));
|
||||
}
|
||||
|
||||
function findSponsoredBooking(bookingId: number): SponsoredBooking {
|
||||
const all = Object.values(BOOKINGS).flat();
|
||||
const booking = all.find((b) => b.bookingId === bookingId);
|
||||
if (!booking) throw new Error(`Mock sponsored booking ${bookingId} not found`);
|
||||
return booking;
|
||||
}
|
||||
|
||||
/** Build a reconciling commission invoice (VAT on the commission line only; total = comm + bnpl + vat). */
|
||||
function makeInvoice(id: number, bookingId: number, grossIrr: bigint, commissionIrr: bigint, bnplIrr: bigint, vatRate: number, days: number): CenterInvoice {
|
||||
const vatIrr = (commissionIrr * BigInt(Math.round(vatRate * 100))) / BigInt(100);
|
||||
@@ -241,6 +280,10 @@ export const partnerCenterMockApi: PartnerCenterApi = {
|
||||
if (filters.status) items = items.filter((b) => b.status === filters.status);
|
||||
return delay(paginate(items, params));
|
||||
},
|
||||
getMySponsoredBookingDetail: async (bookingId: number): Promise<SponsoredBookingDetail> => {
|
||||
const booking = findSponsoredBooking(bookingId);
|
||||
return delay({ ...booking, timeline: buildBookingTimeline(booking.status) });
|
||||
},
|
||||
listMySettlement: async (params) => {
|
||||
const center = centerById(MOCK_MY_CENTER_ID);
|
||||
// Non-MoR centers issue no commission invoices here — the portal renders the "via Balinyaar" state.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { partnerCenterApi } from '../apis';
|
||||
import { centerKeys } from '../keys';
|
||||
import { PARTNER_DETAIL_STALE_TIME, PARTNER_GC_TIME } from '../constants';
|
||||
|
||||
/**
|
||||
* The scoped, read-only booking detail behind a sponsored-bookings-list row (REQ-064) — dates + a status
|
||||
* timeline only, no clinical content. `bookingId` is `undefined` while the route param hasn't resolved yet
|
||||
* (mirrors the disabled-until-ready pattern used by the nurse payout detail hook).
|
||||
*/
|
||||
export function useMySponsoredBookingDetail(bookingId: number | undefined) {
|
||||
return useQuery({
|
||||
queryKey: centerKeys.mySponsoredBookingDetail(bookingId ?? -1),
|
||||
queryFn: () => partnerCenterApi.getMySponsoredBookingDetail(bookingId as number),
|
||||
enabled: bookingId != null && Number.isFinite(bookingId),
|
||||
staleTime: PARTNER_DETAIL_STALE_TIME,
|
||||
gcTime: PARTNER_GC_TIME,
|
||||
});
|
||||
}
|
||||
@@ -13,4 +13,5 @@ export { useAssignNurseToPartnerCenter } from './hooks/useAssignNurseToPartnerCe
|
||||
export { useMyPartnerCenter } from './hooks/useMyPartnerCenter';
|
||||
export { useMySponsoredNurses } from './hooks/useMySponsoredNurses';
|
||||
export { useMySponsoredBookings } from './hooks/useMySponsoredBookings';
|
||||
export { useMySponsoredBookingDetail } from './hooks/useMySponsoredBookingDetail';
|
||||
export { useMySettlement } from './hooks/useMySettlement';
|
||||
|
||||
@@ -21,5 +21,6 @@ export const centerKeys = {
|
||||
mySponsoredNurses: () => [...centerKeys.myCenter(), 'nurses'] as const,
|
||||
mySponsoredBookings: (filters: SponsoredBookingFilters, params: PageParams) =>
|
||||
[...centerKeys.myCenter(), 'bookings', filters, params] as const,
|
||||
mySponsoredBookingDetail: (bookingId: number) => [...centerKeys.myCenter(), 'bookings', bookingId] as const,
|
||||
mySettlement: (params: PageParams) => [...centerKeys.myCenter(), 'settlement', params] as const,
|
||||
};
|
||||
|
||||
@@ -72,6 +72,16 @@ export interface SponsoredBooking {
|
||||
status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The portal's scoped, read-only booking detail (REQ-064) — `SponsoredBooking` plus a server-truth status
|
||||
* timeline. Deliberately bounded to dates/status/patient display name: no clinical content, no address, no
|
||||
* money — the portal never sees more than a center legally needs to confirm a booking happened.
|
||||
*/
|
||||
export interface SponsoredBookingDetail extends SponsoredBooking {
|
||||
/** Server-truth status timeline for this booking — dates only, no clinical content (portal scope). */
|
||||
timeline: { status: string; occurredAt: string }[];
|
||||
}
|
||||
|
||||
/** `invoices.moadian_status`. */
|
||||
export type MoadianStatus = 'pending' | 'submitted' | 'registered' | 'failed';
|
||||
|
||||
@@ -104,10 +114,14 @@ export interface PartnerCenterFilters {
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
/** Bookings list filter (portal). */
|
||||
export interface SponsoredBookingFilters {
|
||||
/**
|
||||
* Bookings list filter (portal). A `type` (not `interface`) so it satisfies `useAdminListState`'s
|
||||
* `Record<string, unknown>` generic constraint — a plain interface has no implicit index signature and
|
||||
* TS rejects it as a type argument there, even though it's structurally identical.
|
||||
*/
|
||||
export type SponsoredBookingFilters = {
|
||||
status?: string;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The partner-center API seam — admin-side management + the center-scoped portal reads. The real client
|
||||
@@ -127,6 +141,8 @@ export interface PartnerCenterApi {
|
||||
getMyCenter(): Promise<PartnerCenter>;
|
||||
listMySponsoredNurses(): Promise<SponsoredNurse[]>;
|
||||
listMySponsoredBookings(filters: SponsoredBookingFilters, params: PageParams): Promise<Paginated<SponsoredBooking>>;
|
||||
/** REQ-064 — the scoped read-only detail behind a bookings-list row (dates + status timeline only). */
|
||||
getMySponsoredBookingDetail(bookingId: number): Promise<SponsoredBookingDetail>;
|
||||
listMySettlement(params: PageParams): Promise<Paginated<CenterInvoice>>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user