132 lines
4.6 KiB
TypeScript
132 lines
4.6 KiB
TypeScript
'use client';
|
|
import { FunctionComponent } from 'react';
|
|
import Box from '@mui/material/Box';
|
|
import ButtonBase from '@mui/material/ButtonBase';
|
|
import Skeleton from '@mui/material/Skeleton';
|
|
import Stack from '@mui/material/Stack';
|
|
import Typography from '@mui/material/Typography';
|
|
import { AppIcon, AppIconButton, SurfaceCard } from '@/components/common';
|
|
import PatientHeader from '@/components/PatientHeader';
|
|
import type { Patient } from '@/services/patients/types';
|
|
|
|
export interface PatientCardProps {
|
|
patient: Patient;
|
|
/** Translated relation label; omitted when the patient has no relation set. */
|
|
relationLabel?: string;
|
|
/** Translated gender label. */
|
|
genderLabel: string;
|
|
/** Translated age label (e.g. "70 yrs"); omitted when the birth date is unknown. */
|
|
ageLabel?: string;
|
|
/** Translated condition labels; empty renders the "no conditions" line. */
|
|
conditionLabels: string[];
|
|
noConditionsLabel: string;
|
|
onEdit: () => void;
|
|
onArchive: () => void;
|
|
editLabel: string;
|
|
archiveLabel: string;
|
|
/** When provided, tapping the identity area opens the patient's record (E2). The action buttons are unaffected. */
|
|
onOpen?: () => void;
|
|
/** Accessible label for the tappable identity area (required when `onOpen` is set). */
|
|
openLabel?: string;
|
|
/**
|
|
* Pre-formatted "آخرین ویزیت: …" teaser, best-effort from the cached bookings list. Omitted (never
|
|
* fabricated) when no cached booking can be matched to this patient — the server has no
|
|
* `lastVisitAt` field yet (filed as a REQ).
|
|
*/
|
|
lastVisitLabel?: string;
|
|
}
|
|
|
|
/**
|
|
* Patient summary card for the E1 list — the shared `PatientHeader` (relation + name, age/gender, condition
|
|
* chips) plus edit and archive actions. When `onOpen` is passed the identity area becomes a button that opens
|
|
* the E2 record viewer; the edit/archive icon buttons keep their own handlers. All display text is translated
|
|
* by the caller.
|
|
* @component PatientCard
|
|
*/
|
|
const PatientCard: FunctionComponent<PatientCardProps> = ({
|
|
patient,
|
|
relationLabel,
|
|
genderLabel,
|
|
ageLabel,
|
|
conditionLabels,
|
|
noConditionsLabel,
|
|
onEdit,
|
|
onArchive,
|
|
editLabel,
|
|
archiveLabel,
|
|
onOpen,
|
|
openLabel,
|
|
lastVisitLabel,
|
|
}) => {
|
|
const header = (
|
|
<>
|
|
<PatientHeader
|
|
displayName={patient.displayName}
|
|
relationLabel={relationLabel}
|
|
genderLabel={genderLabel}
|
|
ageLabel={ageLabel}
|
|
conditionLabels={conditionLabels}
|
|
noConditionsLabel={noConditionsLabel}
|
|
/>
|
|
{lastVisitLabel ? (
|
|
<Typography variant="caption" sx={{ color: 'text.secondary', display: 'block', mt: 0.75 }}>
|
|
{lastVisitLabel}
|
|
</Typography>
|
|
) : null}
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<SurfaceCard padding="sm">
|
|
<Stack direction="row" sx={{ alignItems: 'flex-start', gap: 1 }}>
|
|
{onOpen ? (
|
|
<ButtonBase
|
|
onClick={onOpen}
|
|
aria-label={openLabel}
|
|
sx={{
|
|
flexGrow: 1,
|
|
minWidth: 0,
|
|
justifyContent: 'flex-start',
|
|
textAlign: 'start',
|
|
borderRadius: 'var(--bal-radius-md)',
|
|
p: 1,
|
|
m: -1,
|
|
gap: 1,
|
|
'&:hover': { bgcolor: 'action.hover' },
|
|
}}
|
|
>
|
|
<Box sx={{ flexGrow: 1, minWidth: 0 }}>{header}</Box>
|
|
<AppIcon icon="forward" size={20} color="var(--bal-text-secondary)" />
|
|
</ButtonBase>
|
|
) : (
|
|
<Box sx={{ flexGrow: 1, minWidth: 0 }}>{header}</Box>
|
|
)}
|
|
|
|
<Stack direction="row" sx={{ flexShrink: 0 }}>
|
|
<AppIconButton icon="edit" title={editLabel} aria-label={editLabel} size="small" onClick={onEdit} />
|
|
<AppIconButton icon="archive" title={archiveLabel} aria-label={archiveLabel} size="small" onClick={onArchive} />
|
|
</Stack>
|
|
</Stack>
|
|
</SurfaceCard>
|
|
);
|
|
};
|
|
|
|
/** Matches the real card's anatomy (identity block + two icon-button actions) for the E1 patient list. */
|
|
const PatientCardSkeleton: FunctionComponent = () => (
|
|
<SurfaceCard padding="sm">
|
|
<Stack direction="row" sx={{ alignItems: 'flex-start', gap: 1 }}>
|
|
<Stack sx={{ gap: 0.75, flexGrow: 1 }}>
|
|
<Skeleton variant="text" width="45%" height={26} />
|
|
<Skeleton variant="text" width="65%" height={20} />
|
|
<Skeleton variant="rounded" width={90} height={22} />
|
|
</Stack>
|
|
<Stack direction="row" sx={{ flexShrink: 0, gap: 0.5 }}>
|
|
<Skeleton variant="circular" width={32} height={32} />
|
|
<Skeleton variant="circular" width={32} height={32} />
|
|
</Stack>
|
|
</Stack>
|
|
</SurfaceCard>
|
|
);
|
|
|
|
export default Object.assign(PatientCard, { Skeleton: PatientCardSkeleton });
|