'use client'; import { FunctionComponent, useMemo } from 'react'; import { useLocale, useTranslations } from 'next-intl'; import { Paper, Stack, Typography } from '@mui/material'; import StatusChip, { StatusKind } from '@/components/StatusChip'; import PriceBreakdown from '@/components/PriceBreakdown'; import CountdownTimer from '@/components/CountdownTimer'; import AppButton from '@/components/common/AppButton'; import AppIcon from '@/components/common/AppIcon'; import { formatShamsiDate, parseIrr } from '@/utils'; import type { EarningsState, NurseEarningsItem } from '@/services/payouts/types'; export interface EarningsRowProps { item: NurseEarningsItem; /** Deep-link to the f8 nurse booking detail (`/nurse/visits/{bookingId}`) — never rebuilt here. */ onViewBooking: (bookingId: number) => void; /** Deep-link to this earning's payout detail (paid rows only). */ onViewPayout: (payoutId: number) => void; } /** Each earnings state maps to a distinct semantic chip so the four states read instantly (§5). */ const EARNINGS_STATE_KIND: Record = { pending: 'pending', eligible: 'info', paid: 'verified', clawback_applied: 'rejected', }; /** * One earnings row — a completed booking's contribution to the nurse's pay. Shows the booking reference, * the **three-amount breakdown** framed for the nurse (`gross − commission = your payout`, reconciled by * `PriceBreakdown`), the **earnings-state chip**, and the state-specific affordance: * - `pending` → "in escrow · dispute window open" + a **display-only** countdown off `disputeWindowEndsAt` * (the server owns eligibility — this never gates anything, only renders time remaining); * - `eligible` → "cleared · awaiting the next weekly batch"; * - `paid` → `paidAt` (Shamsi) + `transferReference`, links to the payout detail; * - `clawback_applied` → the net explanation (`original − clawback = net`) so a lower paid total is explained. * * Money is display-only IRR digit-strings through the money util (no float, no client computation). Every * row deep-links to the booking detail. Read-only: no transfer/retry/eligibility action lives here. * @component EarningsRow */ const EarningsRow: FunctionComponent = ({ item, onViewBooking, onViewPayout }) => { const t = useTranslations('payouts'); const locale = useLocale(); // gross + (−commission) = payout — the invariant PriceBreakdown reconciles against. const negativeCommissionIrr = useMemo( () => String(-parseIrr(item.balinyaarCommissionIrr)), [item.balinyaarCommissionIrr], ); return ( {t('booking_ref', { id: item.bookingId })} {item.patientName} · {formatShamsiDate(item.scheduledDate, locale)} {item.state === 'clawback_applied' && item.clawbackAppliedIrr != null && item.netAmountIrr != null ? ( {t('clawback_explainer')} ) : null} onViewBooking(item.bookingId)} > {t('view_booking')} ); }; /** The state-specific explanation block below the money breakdown. */ const StateAffordance: FunctionComponent<{ item: NurseEarningsItem; onViewPayout: (payoutId: number) => void; }> = ({ item, onViewPayout }) => { const t = useTranslations('payouts'); const locale = useLocale(); if (item.state === 'pending') { return ( {t('pending_affordance')} {item.disputeWindowEndsAt ? ( ) : null} ); } if (item.state === 'eligible') { return ( {t('eligible_affordance')} ); } if (item.state === 'paid') { return ( {item.paidAt ? t('paid_on', { date: formatShamsiDate(item.paidAt, locale) }) : t('estate_paid')} {item.transferReference ? ( {t('transfer_reference_label')}: {item.transferReference} ) : null} {item.nursePayoutId != null ? ( onViewPayout(item.nursePayoutId as number)} > {t('view_payout')} ) : null} ); } // clawback_applied — the net explanation block above already carries the "why"; nothing more here. return null; }; export default EarningsRow;