Screenshotting the pages under dark mode and compact density (rather than trusting that design tokens were enough) turned up two mistakes repeated across every page this feature set added: - `.card` carries only the surface, border and radius — padding comes from the separate `.card-pad`. Fifteen cards were rendering with their content flush against the edges. - `.field` *is* the input box, a 40px-tall flex row. Wrapping a label plus a control in it produced a joined addon rather than a label above its field. `.field-block` is the label-above layout, and thirty-seven wrappers now use it. Both were invisible to type-checking and to the tests, which is exactly why the visual pass was worth running. Numbers in the new UI now go through formatNumber so they render as Persian digits, and the utilization page's header no longer repeats the sentence that appears under its filters verbatim. The QA driver gained a `--ui` flag: theme and density live in localStorage['clinicpro-ui'], so without seeding them dark mode and compact density cannot be screenshotted at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
65 lines
2.7 KiB
TypeScript
65 lines
2.7 KiB
TypeScript
import React from 'react';
|
||
import { useAppointmentSegments } from '../hooks/useResourceBooking';
|
||
import { formatNumber } from '../lib/utils';
|
||
|
||
const timeOf = (ts: number) =>
|
||
new Date(ts * 1000).toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' });
|
||
|
||
/**
|
||
* بخشهای ثبتشدهٔ نوبت — همان چیزی که لحظهٔ رزرو تثبیت شد، نه الگوی امروزِ خدمت.
|
||
*
|
||
* بخشی که بیمار در آن حاضر نیست کمرنگ میآید: اپراتوری که «۹۰ دقیقه» را روی نوبت
|
||
* میبیند باید بفهمد بیمار همهٔ آن مدت روی صندلی نیست.
|
||
*
|
||
* نوبت اسلاتی بخشی ندارد و کارت اصلاً رندر نمیشود — جدول خالی یعنی چیزی خراب است.
|
||
*/
|
||
export default function AppointmentSegmentsCard({ appointmentUuid }: { appointmentUuid: string }) {
|
||
const { segments, loading } = useAppointmentSegments(appointmentUuid);
|
||
|
||
if (loading || segments.length === 0) return null;
|
||
|
||
const total = segments.reduce((sum, s) => sum + s.duration_minutes, 0);
|
||
|
||
return (
|
||
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h3 className="font-semibold text-[var(--text)]">بخشهای نوبت</h3>
|
||
<span className="text-sm text-[var(--text-2)]">{formatNumber(total)} دقیقه</span>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: 2, marginBottom: 14 }}>
|
||
{segments.map((s) => (
|
||
<div
|
||
key={s.sequence}
|
||
title={`${s.name} — ${formatNumber(s.duration_minutes)} دقیقه`}
|
||
style={{
|
||
flex: s.duration_minutes,
|
||
height: 10,
|
||
borderRadius: 'var(--r-pill)',
|
||
background: 'var(--primary)',
|
||
opacity: s.patient_present ? 1 : 0.35,
|
||
}}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
<ol style={{ display: 'flex', flexDirection: 'column', gap: 8, fontSize: 13 }}>
|
||
{segments.map((s) => (
|
||
<li key={s.sequence} style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
|
||
<span style={{ color: 'var(--text-3)', minWidth: 18 }}>{formatNumber(s.sequence)}</span>
|
||
<span style={{ flex: 1 }}>{s.name}</span>
|
||
<span dir="ltr" style={{ color: 'var(--text-2)' }}>
|
||
{timeOf(s.starts_at)} – {timeOf(s.ends_at)}
|
||
</span>
|
||
{!s.patient_present && (
|
||
<span className="badge" style={{ fontSize: 11 }}>
|
||
بدون حضور بیمار
|
||
</span>
|
||
)}
|
||
</li>
|
||
))}
|
||
</ol>
|
||
</div>
|
||
);
|
||
}
|