backend phase 15 & frontend phase 8

This commit is contained in:
hamid
2026-07-10 03:22:29 +03:30
parent 93cc5ecb98
commit cd6c2591a6
154 changed files with 15335 additions and 37 deletions
@@ -1,8 +1,98 @@
import { getTranslations } from 'next-intl/server';
import { PlaceholderScreen } from '@/components';
'use client';
import { useLocale, useTranslations } from 'next-intl';
import { useRouter } from 'next/navigation';
import { Box, Paper, Skeleton, Stack, Typography } from '@mui/material';
import { AppButton, AppIcon } from '@/components';
import { SessionCard, useEvvController } from '@/components/booking';
import { ROUTES } from '@/constants';
import { useSessionEvv, useTodaySessions } from '@/services/bookings';
import type { BookingSessionListItemDto } from '@/services/bookings/types';
export default async function NurseVisitsPage() {
const t = await getTranslations('nav');
const tShell = await getTranslations('shell');
return <PlaceholderScreen icon="visits" title={t('visits')} description={tShell('placeholder_body')} />;
/**
* Nurse ویزیت امروز (E3 top) — the day's operational surface. Lists today's sessions from
* `useTodaySessions`; each renders the shared `SessionCard` with the per-session EVV check-in/out control
* (driven by one `useEvvController`) and the advisory EVV banner once checked in. A GPS mismatch is
* advisory, never a block. Each card also deep-links to the full booking detail (`/nurse/visits/{id}`),
* where the gated care instructions live. Visit-note authoring + task checklist are deferred to f13.
*/
export default function NurseVisitsPage() {
const t = useTranslations('booking');
const { data, isLoading } = useTodaySessions();
const evv = useEvvController();
const items = data?.items ?? [];
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, maxWidth: 640 }}>
<Box>
<Typography variant="h5" component="h1">
{t('evv_visits_title')}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t('evv_visits_subtitle')}
</Typography>
</Box>
{isLoading ? (
<Stack sx={{ gap: 2 }}>
{[0, 1].map((key) => (
<Skeleton key={key} variant="rounded" height={150} />
))}
</Stack>
) : items.length === 0 ? (
<Paper elevation={0} sx={{ p: 4, textAlign: 'center', border: '1px dashed', borderColor: 'divider', borderRadius: 2 }}>
<AppIcon icon="visits" size={40} color="var(--bal-text-secondary)" />
<Typography variant="body2" sx={{ color: 'text.secondary', mt: 1 }}>
{t('evv_no_visits')}
</Typography>
</Paper>
) : (
<Stack sx={{ gap: 2 }}>
{items.map((item) => (
<TodayVisitCard key={item.sessionId} item={item} evv={evv} />
))}
</Stack>
)}
</Box>
);
}
function TodayVisitCard({ item, evv }: { item: BookingSessionListItemDto; evv: ReturnType<typeof useEvvController> }) {
const t = useTranslations('booking');
const locale = useLocale();
const router = useRouter();
// Fetch the EVV detail only once the session has activity, so the banner has the server check-in time.
const hasEvvActivity = item.evvStatus !== 'pending';
const { data: evvDetail } = useSessionEvv(item.sessionId, { enabled: hasEvvActivity });
return (
<Stack sx={{ gap: 0.75 }}>
<SessionCard
title={item.patientName}
sessionIndex={item.sessionIndex}
scheduledDate={item.scheduledDate}
scheduledTimeStart={item.scheduledTimeStart}
scheduledTimeEnd={item.scheduledTimeEnd}
status={item.status}
evvStatus={item.evvStatus}
checkInAt={evvDetail?.checkInAt ?? null}
checkOutAt={evvDetail?.checkOutAt ?? null}
checkInAddressMatch={evvDetail?.checkInAddressMatch ?? null}
showEvvControls
evvPending={evv.busySessionId === item.sessionId}
acquiringLocation={evv.acquiringSessionId === item.sessionId}
onCheckIn={() => evv.checkIn({ sessionId: item.sessionId, bookingId: item.bookingId })}
onCheckOut={() => evv.checkOut({ sessionId: item.sessionId, bookingId: item.bookingId })}
/>
<AppButton
variant="text"
color="primary"
endIcon="bookings"
onClick={() => router.push(`/${locale}${ROUTES.NURSE_VISITS}/${item.bookingId}`)}
sx={{ m: 0, alignSelf: 'flex-start' }}
>
{t('view_booking')}
</AppButton>
</Stack>
);
}