148 lines
5.9 KiB
TypeScript
148 lines
5.9 KiB
TypeScript
'use client';
|
|
import { FunctionComponent } from 'react';
|
|
import { useLocale, useTranslations } from 'next-intl';
|
|
import { Paper, Stack, Typography } from '@mui/material';
|
|
import { AppButton } from '@/components/common';
|
|
import StatusChip from '@/components/StatusChip';
|
|
import { formatIrrToToman, formatShamsiDate } from '@/utils';
|
|
import type { BookingSessionStatus, VisitVerificationStatus } from '@/services/bookings/types';
|
|
import EvvStatusBanner from '../EvvStatusBanner';
|
|
import { formatClock, formatElapsed, formatTimeRange } from '../format';
|
|
import { SESSION_STATUS_KIND } from '../statusKind';
|
|
|
|
export interface SessionCardProps {
|
|
/** Optional heading above the visit row — the patient name on the nurse's day feed (omitted in the detail). */
|
|
title?: string;
|
|
sessionIndex: number;
|
|
/** ISO date `YYYY-MM-DD`. */
|
|
scheduledDate: string;
|
|
/** `HH:mm:ss`. */
|
|
scheduledTimeStart: string;
|
|
scheduledTimeEnd: string;
|
|
status: BookingSessionStatus;
|
|
evvStatus?: VisitVerificationStatus;
|
|
/** Server `checkInAt`/`checkOutAt` — the banner + elapsed render from these, never a client clock. */
|
|
checkInAt?: string | null;
|
|
checkOutAt?: string | null;
|
|
/** Advisory match: `true` in range · `false` out-of-range (under review) · `null` GPS unavailable. */
|
|
checkInAddressMatch?: boolean | null;
|
|
/** IRR digit-string — this session's payout share; shown in the nurse view when `showPayout`. */
|
|
visitPayoutAmount?: string | null;
|
|
showPayout?: boolean;
|
|
/** Render the nurse EVV check-in/out CTA (state-machine driven). */
|
|
showEvvControls?: boolean;
|
|
/** This session's EVV mutation is in flight. */
|
|
evvPending?: boolean;
|
|
/** Acquiring GPS for this session (pre-mutation). */
|
|
acquiringLocation?: boolean;
|
|
onCheckIn?: () => void;
|
|
onCheckOut?: () => void;
|
|
}
|
|
|
|
/**
|
|
* One visit row: the Shamsi schedule, a per-session status chip, the EVV banner once checked in, and —
|
|
* for the assigned nurse — the check-in/out CTA driven by the session + EVV state machine (`scheduled` →
|
|
* «ثبت ورود» · `in_progress`/checked-in → «ثبت خروج» · `completed` → elapsed duration · `missed` → no
|
|
* action). A single-visit booking renders exactly this same card. Presentational: the container supplies
|
|
* the EVV handlers + busy flags; a GPS mismatch never disables the flow (advisory, not a block).
|
|
* @component SessionCard
|
|
*/
|
|
const SessionCard: FunctionComponent<SessionCardProps> = ({
|
|
title,
|
|
sessionIndex,
|
|
scheduledDate,
|
|
scheduledTimeStart,
|
|
scheduledTimeEnd,
|
|
status,
|
|
evvStatus,
|
|
checkInAt,
|
|
checkOutAt,
|
|
checkInAddressMatch = null,
|
|
visitPayoutAmount,
|
|
showPayout = false,
|
|
showEvvControls = false,
|
|
evvPending = false,
|
|
acquiringLocation = false,
|
|
onCheckIn,
|
|
onCheckOut,
|
|
}) => {
|
|
const t = useTranslations('booking');
|
|
const tc = useTranslations('common');
|
|
const locale = useLocale();
|
|
|
|
const dateLabel = formatShamsiDate(scheduledDate, locale);
|
|
const timeLabel = formatTimeRange(scheduledTimeStart, scheduledTimeEnd, locale);
|
|
const showBanner = (evvStatus === 'checked_in' || evvStatus === 'completed') && Boolean(checkInAt);
|
|
const elapsed = formatElapsed(checkInAt ?? null, checkOutAt ?? null, locale);
|
|
const busy = acquiringLocation || evvPending;
|
|
|
|
return (
|
|
<Paper
|
|
elevation={0}
|
|
data-session-status={status}
|
|
sx={{ p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 2 }}
|
|
>
|
|
<Stack sx={{ gap: 1.25 }}>
|
|
{title ? (
|
|
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
|
{title}
|
|
</Typography>
|
|
) : null}
|
|
<Stack direction="row" sx={{ justifyContent: 'space-between', alignItems: 'center', gap: 1 }}>
|
|
<Typography variant="subtitle2" sx={{ fontWeight: title ? 600 : 700, color: title ? 'text.secondary' : 'text.primary' }}>
|
|
{t('session_index', { n: sessionIndex })}
|
|
</Typography>
|
|
<StatusChip status={SESSION_STATUS_KIND[status]} label={t(`sstatus_${status}`)} />
|
|
</Stack>
|
|
|
|
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
|
{dateLabel} · <Typography component="span" dir="ltr" sx={{ fontVariantNumeric: 'tabular-nums' }}>{timeLabel}</Typography>
|
|
</Typography>
|
|
|
|
{showBanner ? <EvvStatusBanner checkInAtIso={checkInAt as string} addressMatch={checkInAddressMatch} /> : null}
|
|
|
|
{checkOutAt ? (
|
|
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
|
{t('evv_checked_out_at', { time: formatClock(checkOutAt, locale) })}
|
|
{elapsed ? ` · ${t('session_elapsed', { duration: elapsed })}` : ''}
|
|
</Typography>
|
|
) : null}
|
|
|
|
{showPayout && visitPayoutAmount ? (
|
|
<Typography variant="caption" sx={{ color: 'var(--bal-secondary-dark)', fontWeight: 600 }}>
|
|
{t('session_payout')}: {formatIrrToToman(visitPayoutAmount, locale)} {tc('currency_toman')}
|
|
</Typography>
|
|
) : null}
|
|
|
|
{showEvvControls && status === 'scheduled' ? (
|
|
<AppButton
|
|
color="secondary"
|
|
variant="contained"
|
|
startIcon={acquiringLocation ? 'gps' : 'check_in'}
|
|
disabled={busy}
|
|
onClick={onCheckIn}
|
|
sx={{ m: 0, alignSelf: 'flex-start', py: 1 }}
|
|
>
|
|
{acquiringLocation ? t('evv_acquiring_location') : evvPending ? t('evv_checking_in') : t('evv_check_in')}
|
|
</AppButton>
|
|
) : null}
|
|
|
|
{showEvvControls && status === 'in_progress' && evvStatus === 'checked_in' ? (
|
|
<AppButton
|
|
color="secondary"
|
|
variant="outlined"
|
|
startIcon={acquiringLocation ? 'gps' : 'check_out'}
|
|
disabled={busy}
|
|
onClick={onCheckOut}
|
|
sx={{ m: 0, alignSelf: 'flex-start', py: 1 }}
|
|
>
|
|
{acquiringLocation ? t('evv_acquiring_location') : evvPending ? t('evv_checking_out') : t('evv_check_out')}
|
|
</AppButton>
|
|
) : null}
|
|
</Stack>
|
|
</Paper>
|
|
);
|
|
};
|
|
|
|
export default SessionCard;
|