backend phase 15 & frontend phase 8
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
'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;
|
||||
Reference in New Issue
Block a user