Browse Source
- Sprint 7: Electronic patient card (MedcardScreen rewritten with hero passport, 5 tabs, bidirectional links with past appointments) - Sprint 8: 5th home variant "Светлая плитка" (HomeSplashScreen) - Sprint 9: Tweaks "Дизайн" section (Клод / Прозрачная карточка) with plate versions of Profile, Appts, Appt details and Medcard in screens-plate.jsx; fallback to Клод for other screens Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>main
8 changed files with 1324 additions and 76 deletions
@ -0,0 +1,619 @@
|
||||
import React, { useState } from 'react'; |
||||
import { I } from '../icons.jsx'; |
||||
import { CLINIC_DATA } from '../data.js'; |
||||
import { Avatar } from '../components.jsx'; |
||||
|
||||
// ---------- Общие plate-компоненты ---------- |
||||
|
||||
function PlateHeader({ title, onBack, right }) { |
||||
return ( |
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 20px 8px' }}> |
||||
{onBack ? ( |
||||
<button onClick={onBack} className="press" style={{ width: 40, height: 40, borderRadius: 999, background: 'var(--c-primary-50)', border: '1px solid var(--c-primary-100)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> |
||||
<I.chevL size={20} style={{ color: 'var(--c-primary-darker)' }} /> |
||||
</button> |
||||
) : <div style={{ width: 40 }} />} |
||||
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--c-fg-1)' }}>{title}</div> |
||||
<div style={{ width: 40, display: 'flex', justifyContent: 'flex-end' }}>{right || null}</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
function PlateCard({ children, onClick, pad = 16, tint = 'soft', style = {} }) { |
||||
const bg = tint === 'warm' ? 'var(--c-warm-100)' : 'var(--c-primary-50)'; |
||||
const br = tint === 'warm' ? 'transparent' : 'var(--c-primary-100)'; |
||||
const base = { |
||||
width: '100%', textAlign: 'left', |
||||
background: bg, border: '1px solid ' + br, |
||||
borderRadius: 16, padding: pad, |
||||
display: 'block', |
||||
...style, |
||||
}; |
||||
return onClick ? ( |
||||
<button onClick={onClick} className="press" style={base}>{children}</button> |
||||
) : ( |
||||
<div style={base}>{children}</div> |
||||
); |
||||
} |
||||
|
||||
function PlateIcon({ icon: Ic, size = 40, bg = 'var(--c-primary-darker)', color = '#fff' }) { |
||||
return ( |
||||
<div style={{ width: size, height: size, borderRadius: 999, background: bg, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> |
||||
<Ic size={Math.round(size * 0.5)} style={{ color }} /> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
function PlateSection({ title, children, action, onAction }) { |
||||
return ( |
||||
<div style={{ padding: '0 20px 8px' }}> |
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 10 }}> |
||||
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--c-fg-1)' }}>{title}</div> |
||||
{action && <button onClick={onAction} style={{ fontSize: 12, fontWeight: 700, color: 'var(--c-primary-darker)' }}>{action}</button>} |
||||
</div> |
||||
{children} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
function PlateH1({ children }) { |
||||
return <h1 style={{ fontSize: 28, fontWeight: 800, lineHeight: 1.15, color: 'var(--c-fg-1)', margin: 0, padding: '4px 20px 14px' }}>{children}</h1>; |
||||
} |
||||
|
||||
const SEVERITY_TINT = { |
||||
high: { bg: 'var(--c-accent-50)', c: 'var(--c-accent-dark)', lb: 'Опасная' }, |
||||
mid: { bg: 'var(--c-warm-100)', c: 'var(--c-warm-text)', lb: 'Средняя' }, |
||||
low: { bg: 'var(--c-primary-50)', c: 'var(--c-primary-darker)', lb: 'Лёгкая' }, |
||||
}; |
||||
|
||||
function shortDoc(d) { |
||||
if (!d) return '—'; |
||||
const parts = d.name.split(' '); |
||||
return parts[0] + ' ' + (parts[1] ? parts[1][0] + '.' : ''); |
||||
} |
||||
|
||||
// ---------- Профиль ---------- |
||||
|
||||
export function ProfilePlateScreen({ nav }) { |
||||
const { patient, medcard, appointments, results } = CLINIC_DATA; |
||||
const pastCount = appointments.filter(a => a.status === 'past').length; |
||||
const activeRxCount = medcard.prescriptions.filter(p => p.active).length; |
||||
|
||||
const sections = [ |
||||
{ |
||||
title: 'Здоровье', |
||||
items: [ |
||||
{ i: I.file, t: 'Электронная карта', s: `${pastCount} посещений · ${medcard.allergies.length} аллергии · ${medcard.chronicConditions.length} диагноза`, go: 'medcard', featured: true }, |
||||
{ i: I.doc, t: 'Анализы', s: `${results.length} результатов`, go: 'results' }, |
||||
{ i: I.pill, t: 'Лекарства', s: `${activeRxCount} активных курса`, go: 'recovery' }, |
||||
{ i: I.hearing, t: 'История тестов слуха', s: '2 аудиограммы', go: 'results' }, |
||||
], |
||||
}, |
||||
{ |
||||
title: 'Оплата и бонусы', |
||||
items: [ |
||||
{ i: I.card, t: 'Способы оплаты', s: 'Mir •••• 4821' }, |
||||
{ i: I.gift, t: 'Бонусы', s: '2 480 баллов · 5%', badge: 'Серебро' }, |
||||
{ i: I.file, t: 'История платежей', s: '12 операций' }, |
||||
], |
||||
}, |
||||
{ |
||||
title: 'Клиника', |
||||
items: [ |
||||
{ i: I.pin, t: 'Адреса и часы работы', s: '3 клиники', go: 'contacts' }, |
||||
{ i: I.phone, t: '(342) 207-03-03', s: 'Ежедневно 9:00–21:00' }, |
||||
], |
||||
}, |
||||
{ |
||||
title: 'Настройки', |
||||
items: [ |
||||
{ i: I.bell, t: 'Уведомления', go: 'notifications' }, |
||||
{ i: I.shield, t: 'Конфиденциальность' }, |
||||
{ i: I.user, t: 'Члены семьи', s: '+ 2 профиля' }, |
||||
], |
||||
}, |
||||
]; |
||||
|
||||
return ( |
||||
<div style={{ paddingBottom: 100 }}> |
||||
<PlateHeader title="Профиль" right={ |
||||
<button onClick={() => nav.push('notifications')} className="press" style={{ width: 40, height: 40, borderRadius: 999, display: 'flex', alignItems: 'center', justifyContent: 'center' }}> |
||||
<I.bell size={20} style={{ color: 'var(--c-primary-darker)' }} /> |
||||
</button> |
||||
} /> |
||||
|
||||
<PlateH1>{patient.shortName}</PlateH1> |
||||
|
||||
{/* Паспорт */} |
||||
<div style={{ padding: '0 20px 16px' }}> |
||||
<PlateCard tint="soft"> |
||||
<div style={{ display: 'flex', gap: 14, alignItems: 'center', marginBottom: 14 }}> |
||||
<Avatar init={patient.init} size={56} style={{ fontSize: 22 }} /> |
||||
<div style={{ flex: 1, minWidth: 0 }}> |
||||
<div style={{ fontSize: 13, color: 'var(--c-primary-darker)', fontWeight: 700 }}>{patient.phone}</div> |
||||
<div className="sub" style={{ fontSize: 12, marginTop: 2 }}>{patient.birthDate} · {patient.age} года</div> |
||||
</div> |
||||
</div> |
||||
<div style={{ display: 'flex', gap: 8 }}> |
||||
<button onClick={() => nav.push('qr')} className="press" style={{ flex: 1, padding: '10px 12px', background: '#fff', borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, fontSize: 13, fontWeight: 600 }}> |
||||
<I.qr size={14} style={{ color: 'var(--c-primary-darker)' }} /> QR пациента |
||||
</button> |
||||
<button onClick={() => nav.push('medcard')} className="press" style={{ flex: 1, padding: '10px 12px', background: '#fff', borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, fontSize: 13, fontWeight: 600 }}> |
||||
<I.file size={14} style={{ color: 'var(--c-primary-darker)' }} /> Карта №{patient.cardNumber.split('-').pop()} |
||||
</button> |
||||
</div> |
||||
</PlateCard> |
||||
</div> |
||||
|
||||
{sections.map((sec, si) => ( |
||||
<PlateSection key={si} title={sec.title}> |
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 18 }}> |
||||
{sec.items.map((it, i) => { |
||||
const II = it.i; |
||||
return ( |
||||
<PlateCard key={i} pad={14} onClick={() => it.go && nav.push(it.go)} style={{ display: 'flex', gap: 12, alignItems: 'center' }}> |
||||
<PlateIcon icon={II} size={it.featured ? 44 : 36} /> |
||||
<div style={{ flex: 1, minWidth: 0 }}> |
||||
<div style={{ fontSize: 14, fontWeight: it.featured ? 700 : 600 }}>{it.t}</div> |
||||
{it.s && <div className="sub" style={{ fontSize: 12, marginTop: 2 }}>{it.s}</div>} |
||||
</div> |
||||
{it.badge && <span className="chip chip-warm" style={{ fontSize: 11 }}>{it.badge}</span>} |
||||
<I.chev size={14} style={{ color: 'var(--c-primary-darker)', opacity: .5, flexShrink: 0 }} /> |
||||
</PlateCard> |
||||
); |
||||
})} |
||||
</div> |
||||
</PlateSection> |
||||
))} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
// ---------- Приёмы ---------- |
||||
|
||||
export function ApptsPlateScreen({ nav }) { |
||||
const { appointments, doctors, clinic } = CLINIC_DATA; |
||||
const [tab, setTab] = useState('upcoming'); |
||||
const items = appointments.filter(a => a.status === tab); |
||||
|
||||
return ( |
||||
<div style={{ paddingBottom: 100 }}> |
||||
<PlateHeader title="Приёмы" right={ |
||||
<button onClick={() => nav.push('notifications')} className="press" style={{ width: 40, height: 40, borderRadius: 999, display: 'flex', alignItems: 'center', justifyContent: 'center' }}> |
||||
<I.bell size={20} style={{ color: 'var(--c-primary-darker)' }} /> |
||||
</button> |
||||
} /> |
||||
|
||||
<PlateH1>Мои приёмы</PlateH1> |
||||
|
||||
<div style={{ padding: '0 20px 16px' }}> |
||||
<div style={{ display: 'flex', gap: 8, background: 'var(--c-primary-50)', border: '1px solid var(--c-primary-100)', borderRadius: 14, padding: 4 }}> |
||||
<button onClick={() => setTab('upcoming')} style={{ |
||||
flex: 1, padding: 10, borderRadius: 10, fontSize: 13, fontWeight: 700, |
||||
background: tab === 'upcoming' ? '#fff' : 'transparent', |
||||
color: tab === 'upcoming' ? 'var(--c-fg-1)' : 'var(--c-fg-3)', |
||||
}}>Предстоящие · {appointments.filter(a => a.status === 'upcoming').length}</button> |
||||
<button onClick={() => setTab('past')} style={{ |
||||
flex: 1, padding: 10, borderRadius: 10, fontSize: 13, fontWeight: 700, |
||||
background: tab === 'past' ? '#fff' : 'transparent', |
||||
color: tab === 'past' ? 'var(--c-fg-1)' : 'var(--c-fg-3)', |
||||
}}>Прошедшие · {appointments.filter(a => a.status === 'past').length}</button> |
||||
</div> |
||||
</div> |
||||
|
||||
<div style={{ padding: '0 20px', display: 'flex', flexDirection: 'column', gap: 10 }}> |
||||
{items.map(a => { |
||||
const d = doctors.find(x => x.id === a.doctor); |
||||
const ad = clinic.addresses.find(x => x.id === a.address); |
||||
const isUp = a.status === 'upcoming'; |
||||
return ( |
||||
<PlateCard key={a.id} onClick={() => nav.push('appt:' + a.id)} tint={isUp ? 'soft' : 'soft'}> |
||||
{isUp && ( |
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}> |
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, fontWeight: 700, letterSpacing: .7, color: 'var(--c-primary-darker)' }}> |
||||
<span style={{ width: 6, height: 6, borderRadius: 999, background: 'var(--c-primary-darker)' }} /> |
||||
{a.date.toUpperCase()} · {a.time} |
||||
</span> |
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, color: 'var(--c-primary-darker)', fontWeight: 700 }}> |
||||
<I.star size={12} /> Активно |
||||
</span> |
||||
</div> |
||||
)} |
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'center', marginBottom: 10 }}> |
||||
<Avatar init={d.init} size={48} /> |
||||
<div style={{ flex: 1, minWidth: 0 }}> |
||||
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--c-fg-1)' }}>{d.name.split(' ').slice(0, 2).join(' ')}</div> |
||||
<div style={{ fontSize: 13, color: 'var(--c-primary-darker)', marginTop: 2 }}>{d.spec.split(' · ')[0]}</div> |
||||
</div> |
||||
</div> |
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}> |
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '6px 12px', background: '#fff', border: '1px solid var(--c-primary-100)', borderRadius: 10, fontSize: 13, fontWeight: 600 }}> |
||||
<I.calendar size={14} style={{ color: 'var(--c-primary-darker)' }} /> {a.date} |
||||
</span> |
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '6px 12px', background: '#fff', border: '1px solid var(--c-primary-100)', borderRadius: 10, fontSize: 13, fontWeight: 600 }}> |
||||
<I.clock size={14} style={{ color: 'var(--c-primary-darker)' }} /> {a.time} |
||||
</span> |
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '6px 12px', background: '#fff', border: '1px solid var(--c-primary-100)', borderRadius: 10, fontSize: 13, fontWeight: 600 }}> |
||||
<I.pin size={14} style={{ color: 'var(--c-primary-darker)' }} /> {ad.short} |
||||
</span> |
||||
{!isUp && a.hasReport && ( |
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '6px 12px', background: '#fff', border: '1px solid var(--c-primary-100)', borderRadius: 10, fontSize: 13, fontWeight: 600 }}> |
||||
<I.doc size={14} style={{ color: 'var(--c-primary-darker)' }} /> Заключение |
||||
</span> |
||||
)} |
||||
</div> |
||||
</PlateCard> |
||||
); |
||||
})} |
||||
{items.length === 0 && ( |
||||
<div style={{ textAlign: 'center', padding: '40px 20px' }} className="sub">Нет приёмов в этой категории</div> |
||||
)} |
||||
</div> |
||||
|
||||
<div style={{ padding: 20, display: 'flex', flexDirection: 'column', gap: 10 }}> |
||||
{tab === 'upcoming' && ( |
||||
<PlateCard tint="warm" onClick={() => nav.push('booking-specs')} style={{ position: 'relative', overflow: 'hidden' }}> |
||||
<div style={{ position: 'absolute', top: -20, right: -20, width: 90, height: 90, borderRadius: 999, background: 'rgba(255,255,255,0.35)' }} /> |
||||
<div style={{ position: 'relative', display: 'flex', gap: 14, alignItems: 'center', marginBottom: 12 }}> |
||||
<PlateIcon icon={I.plus} size={48} bg="var(--c-warm-text)" /> |
||||
<div style={{ fontSize: 16, fontWeight: 700 }}>Записаться на приём</div> |
||||
</div> |
||||
<div style={{ position: 'relative', padding: 12, background: '#fff', borderRadius: 12, textAlign: 'center', fontSize: 14, fontWeight: 600 }}>Выбрать удобное время</div> |
||||
</PlateCard> |
||||
)} |
||||
{tab === 'past' && ( |
||||
<PlateCard onClick={() => nav.push('medcard')} style={{ display: 'flex', gap: 14, alignItems: 'center' }}> |
||||
<PlateIcon icon={I.file} size={48} /> |
||||
<div style={{ flex: 1, minWidth: 0 }}> |
||||
<div style={{ fontSize: 15, fontWeight: 700 }}>Электронная карта</div> |
||||
<div className="sub" style={{ fontSize: 12, marginTop: 2 }}>Все посещения, диагнозы, назначения</div> |
||||
</div> |
||||
<I.chev size={14} style={{ color: 'var(--c-primary-darker)', opacity: .5 }} /> |
||||
</PlateCard> |
||||
)} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
// ---------- Детали приёма ---------- |
||||
|
||||
export function ApptDetailPlateScreen({ nav, apptId }) { |
||||
const a = CLINIC_DATA.appointments.find(x => x.id === apptId); |
||||
const d = CLINIC_DATA.doctors.find(x => x.id === a.doctor); |
||||
const ad = CLINIC_DATA.clinic.addresses.find(x => x.id === a.address); |
||||
const isUp = a.status === 'upcoming'; |
||||
|
||||
return ( |
||||
<div style={{ paddingBottom: 120 }}> |
||||
<PlateHeader title="Приём" onBack={() => nav.pop()} /> |
||||
<div style={{ padding: '0 20px' }}> |
||||
<PlateCard style={{ textAlign: 'center', marginBottom: 12 }}> |
||||
<div style={{ fontSize: 13, color: 'var(--c-primary-darker)', fontWeight: 700, marginBottom: 4 }}>{a.weekday}, {a.date}</div> |
||||
<div style={{ fontSize: 42, fontFamily: 'var(--font-narrow)', fontWeight: 700, lineHeight: 1, marginBottom: 6, color: 'var(--c-fg-1)' }}>{a.time}</div> |
||||
<div className="sub" style={{ fontSize: 13 }}>{a.type}</div> |
||||
</PlateCard> |
||||
|
||||
<PlateCard pad={0} onClick={() => nav.push('doctor:' + d.id)} style={{ marginBottom: 10, display: 'flex', gap: 12, alignItems: 'center', padding: 14 }}> |
||||
<Avatar init={d.init} size={48} /> |
||||
<div style={{ flex: 1, minWidth: 0 }}> |
||||
<div style={{ fontWeight: 700, fontSize: 15 }}>{d.name.split(' ').slice(0, 2).join(' ')}</div> |
||||
<div style={{ fontSize: 12, color: 'var(--c-primary-darker)', marginTop: 2 }}>{d.spec}</div> |
||||
</div> |
||||
<I.chev size={14} style={{ color: 'var(--c-primary-darker)', opacity: .5 }} /> |
||||
</PlateCard> |
||||
|
||||
<PlateCard style={{ marginBottom: 12 }}> |
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 10 }}> |
||||
<PlateIcon icon={I.pin} size={36} /> |
||||
<div style={{ flex: 1 }}> |
||||
<div style={{ fontSize: 14, fontWeight: 700 }}>{ad.full}</div> |
||||
<div className="sub" style={{ fontSize: 12 }}>{a.room}</div> |
||||
</div> |
||||
</div> |
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}> |
||||
<PlateIcon icon={I.phone} size={36} /> |
||||
<div style={{ flex: 1, fontSize: 14 }}>(342) 207-03-03</div> |
||||
</div> |
||||
</PlateCard> |
||||
|
||||
{isUp && ( |
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 12 }}> |
||||
<button className="press" style={{ width: '100%', padding: 14, background: '#fff', border: '1px solid var(--c-primary-100)', borderRadius: 12, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, fontSize: 14, fontWeight: 600 }}> |
||||
<I.calendar size={18} style={{ color: 'var(--c-primary-darker)' }} /> Добавить в календарь |
||||
</button> |
||||
<button className="press" style={{ width: '100%', padding: 14, background: '#fff', border: '1px solid var(--c-primary-100)', borderRadius: 12, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, fontSize: 14, fontWeight: 600 }}> |
||||
<I.bell size={18} style={{ color: 'var(--c-primary-darker)' }} /> Напомнить позже |
||||
</button> |
||||
</div> |
||||
)} |
||||
|
||||
{a.hasReport && ( |
||||
<> |
||||
<div style={{ fontSize: 14, fontWeight: 700, padding: '4px 4px 8px', display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}> |
||||
<span>Заключение врача</span> |
||||
{a.diagnosisCode && <span className="sub" style={{ fontSize: 11, fontFamily: 'var(--font-narrow)' }}>{a.diagnosisCode}</span>} |
||||
</div> |
||||
<PlateCard> |
||||
{a.diagnosis && <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 6 }}>{a.diagnosis}</div>} |
||||
<div style={{ fontSize: 13, lineHeight: 1.55, color: 'var(--c-fg-2)', padding: '10px 12px', background: '#fff', borderRadius: 10 }}> |
||||
{a.conclusion || 'Заключение недоступно.'} |
||||
</div> |
||||
{a.prescriptions && a.prescriptions.length > 0 && ( |
||||
<> |
||||
<div style={{ fontSize: 11, textTransform: 'uppercase', letterSpacing: .5, fontWeight: 700, color: 'var(--c-primary-darker)', marginTop: 12, marginBottom: 6 }}>Назначения</div> |
||||
<ul style={{ margin: 0, padding: 0, listStyle: 'none' }}> |
||||
{a.prescriptions.map((p, i) => ( |
||||
<li key={i} style={{ display: 'flex', gap: 8, fontSize: 13, padding: '4px 0', lineHeight: 1.5 }}> |
||||
<I.pill size={14} style={{ color: 'var(--c-primary-darker)', flexShrink: 0, marginTop: 3 }} /> |
||||
<span>{p}</span> |
||||
</li> |
||||
))} |
||||
</ul> |
||||
</> |
||||
)} |
||||
<div style={{ display: 'flex', gap: 8, marginTop: 12, flexWrap: 'wrap' }}> |
||||
<button className="press" style={{ padding: '8px 14px', fontSize: 13, background: '#fff', border: '1px solid var(--c-primary-100)', borderRadius: 10, display: 'flex', alignItems: 'center', gap: 6, fontWeight: 600 }}> |
||||
<I.doc size={15} style={{ color: 'var(--c-primary-darker)' }} /> PDF |
||||
</button> |
||||
<button onClick={() => nav.push('medcard')} className="press" style={{ padding: '8px 14px', fontSize: 13, background: '#fff', border: '1px solid var(--c-primary-100)', borderRadius: 10, display: 'flex', alignItems: 'center', gap: 6, fontWeight: 600 }}> |
||||
<I.file size={15} style={{ color: 'var(--c-primary-darker)' }} /> В медкарте |
||||
</button> |
||||
</div> |
||||
</PlateCard> |
||||
</> |
||||
)} |
||||
</div> |
||||
|
||||
{isUp && ( |
||||
<div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '14px 20px 34px', background: '#fff', borderTop: '1px solid var(--c-border)', display: 'flex', gap: 10 }}> |
||||
<button className="press" style={{ flex: 1, padding: 14, border: '1px solid var(--c-accent-50)', borderRadius: 12, color: 'var(--c-danger)', background: '#fff', fontWeight: 700, fontSize: 14 }}>Отменить</button> |
||||
<button className="press" style={{ flex: 2, padding: 14, background: 'var(--c-primary-darker)', color: '#fff', borderRadius: 12, fontWeight: 700, fontSize: 14 }}>Перенести</button> |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
// ---------- Медкарта ---------- |
||||
|
||||
const PLATE_MEDCARD_TABS = [ |
||||
{ id: 'summary', lb: 'Общее' }, |
||||
{ id: 'visits', lb: 'Посещения' }, |
||||
{ id: 'rx', lb: 'Назначения' }, |
||||
{ id: 'shots', lb: 'Прививки' }, |
||||
{ id: 'ops', lb: 'Операции' }, |
||||
]; |
||||
|
||||
export function MedcardPlateScreen({ nav }) { |
||||
const { patient, medcard, appointments, doctors, results } = CLINIC_DATA; |
||||
const [tab, setTab] = useState('summary'); |
||||
|
||||
const pastVisits = appointments.filter(a => a.status === 'past').slice().sort((a, b) => (b.year || 2026) - (a.year || 2026)); |
||||
const activeRx = medcard.prescriptions.filter(p => p.active); |
||||
const pastRx = medcard.prescriptions.filter(p => !p.active); |
||||
|
||||
return ( |
||||
<div style={{ paddingBottom: 40 }}> |
||||
<PlateHeader title="Электронная карта" onBack={() => nav.pop()} right={ |
||||
<button className="press" style={{ width: 40, height: 40, borderRadius: 999, display: 'flex', alignItems: 'center', justifyContent: 'center' }}> |
||||
<I.search size={20} style={{ color: 'var(--c-primary-darker)' }} /> |
||||
</button> |
||||
} /> |
||||
|
||||
<PlateH1>{patient.shortName}</PlateH1> |
||||
|
||||
<div style={{ padding: '0 20px 14px' }}> |
||||
<PlateCard> |
||||
<div style={{ display: 'flex', gap: 14, alignItems: 'center', marginBottom: 12 }}> |
||||
<Avatar init={patient.init} size={52} style={{ fontSize: 20 }} /> |
||||
<div style={{ flex: 1, minWidth: 0 }}> |
||||
<div style={{ fontSize: 13, color: 'var(--c-primary-darker)', fontWeight: 700 }}>{patient.birthDate}</div> |
||||
<div className="sub" style={{ fontSize: 12, marginTop: 2 }}>{patient.age} года · {patient.sex}</div> |
||||
</div> |
||||
<button onClick={() => nav.push('qr')} className="press" style={{ width: 40, height: 40, borderRadius: 10, background: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> |
||||
<I.qr size={18} style={{ color: 'var(--c-primary-darker)' }} /> |
||||
</button> |
||||
</div> |
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}> |
||||
<div style={{ padding: '8px 10px', background: '#fff', borderRadius: 10 }}> |
||||
<div className="sub" style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: .5, fontWeight: 700 }}>№ карты</div> |
||||
<div style={{ fontSize: 12, fontWeight: 700, fontFamily: 'var(--font-narrow)', marginTop: 2 }}>{patient.cardNumber}</div> |
||||
</div> |
||||
<div style={{ padding: '8px 10px', background: '#fff', borderRadius: 10 }}> |
||||
<div className="sub" style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: .5, fontWeight: 700 }}>Полис</div> |
||||
<div style={{ fontSize: 12, fontWeight: 700, fontFamily: 'var(--font-narrow)', marginTop: 2 }}>{patient.policy}</div> |
||||
</div> |
||||
</div> |
||||
</PlateCard> |
||||
</div> |
||||
|
||||
<div style={{ padding: '0 20px 14px', overflowX: 'auto' }}> |
||||
<div style={{ display: 'flex', gap: 6, padding: 4, background: 'var(--c-primary-50)', border: '1px solid var(--c-primary-100)', borderRadius: 14, minWidth: '100%' }}> |
||||
{PLATE_MEDCARD_TABS.map(t => ( |
||||
<button key={t.id} onClick={() => setTab(t.id)} style={{ |
||||
flex: 1, whiteSpace: 'nowrap', padding: '8px 10px', borderRadius: 10, fontSize: 12, fontWeight: 700, |
||||
background: tab === t.id ? '#fff' : 'transparent', |
||||
color: tab === t.id ? 'var(--c-fg-1)' : 'var(--c-fg-3)', |
||||
}}>{t.lb}</button> |
||||
))} |
||||
</div> |
||||
</div> |
||||
|
||||
<div style={{ padding: '0 20px' }}> |
||||
{tab === 'summary' && ( |
||||
<> |
||||
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 10 }}>Аллергии · {medcard.allergies.length}</div> |
||||
<PlateCard style={{ marginBottom: 14 }}> |
||||
{medcard.allergies.map((a, i) => { |
||||
const s = SEVERITY_TINT[a.severity] || SEVERITY_TINT.low; |
||||
return ( |
||||
<div key={a.id} style={{ display: 'flex', gap: 12, alignItems: 'flex-start', padding: i === 0 ? 0 : '10px 0 0', marginTop: i === 0 ? 0 : 10, borderTop: i === 0 ? 0 : '1px solid var(--c-primary-100)' }}> |
||||
<div style={{ padding: '4px 10px', borderRadius: 999, background: s.bg, color: s.c, fontSize: 11, fontWeight: 700, flexShrink: 0 }}>{s.lb}</div> |
||||
<div style={{ flex: 1, minWidth: 0 }}> |
||||
<div style={{ fontSize: 14, fontWeight: 700 }}>{a.name}</div> |
||||
<div className="sub" style={{ fontSize: 12, marginTop: 2 }}>{a.reaction} · с {a.noted}</div> |
||||
</div> |
||||
</div> |
||||
); |
||||
})} |
||||
</PlateCard> |
||||
|
||||
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 10 }}>Хронические диагнозы</div> |
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 14 }}> |
||||
{medcard.chronicConditions.map(c => { |
||||
const doc = doctors.find(d => d.id === c.doctorId); |
||||
return ( |
||||
<PlateCard key={c.id}> |
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 4 }}> |
||||
<span style={{ fontSize: 14, fontWeight: 700 }}>{c.name}</span> |
||||
<span className="sub" style={{ fontSize: 11, fontFamily: 'var(--font-narrow)' }}>{c.code}</span> |
||||
</div> |
||||
<div className="sub" style={{ fontSize: 12 }}>{c.stage} · с {c.diagnosed}</div> |
||||
<div className="sub" style={{ fontSize: 12, marginTop: 2 }}>Наблюдает: {shortDoc(doc)}</div> |
||||
</PlateCard> |
||||
); |
||||
})} |
||||
</div> |
||||
|
||||
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 10 }}>Основное</div> |
||||
<PlateCard style={{ marginBottom: 14 }}> |
||||
{[ |
||||
['Рост / Вес', patient.height + ' см · ' + patient.weight + ' кг'], |
||||
['Группа крови', patient.bloodType], |
||||
['СНИЛС', patient.snils], |
||||
['Первое обращение', patient.firstVisit], |
||||
['Лечащий врач', shortDoc(doctors.find(d => d.id === patient.primaryDoctorId))], |
||||
].map(([k, v], i, arr) => ( |
||||
<div key={k} style={{ display: 'flex', justifyContent: 'space-between', padding: i === 0 ? '0 0 8px' : '8px 0', fontSize: 13, borderTop: i === 0 ? 0 : '1px solid var(--c-primary-100)' }}> |
||||
<span className="sub">{k}</span> |
||||
<span style={{ fontWeight: 700 }}>{v}</span> |
||||
</div> |
||||
))} |
||||
</PlateCard> |
||||
|
||||
<PlateCard onClick={() => nav.push('results')} style={{ display: 'flex', gap: 12, alignItems: 'center' }}> |
||||
<PlateIcon icon={I.doc} size={40} /> |
||||
<div style={{ flex: 1 }}> |
||||
<div style={{ fontSize: 14, fontWeight: 700 }}>Анализы и обследования</div> |
||||
<div className="sub" style={{ fontSize: 12 }}>{results.length} результатов</div> |
||||
</div> |
||||
<I.chev size={14} style={{ color: 'var(--c-primary-darker)', opacity: .5 }} /> |
||||
</PlateCard> |
||||
</> |
||||
)} |
||||
|
||||
{tab === 'visits' && ( |
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}> |
||||
{pastVisits.map(v => { |
||||
const doc = doctors.find(d => d.id === v.doctor); |
||||
return ( |
||||
<PlateCard key={v.id} onClick={() => nav.push('appt:' + v.id)}> |
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 6 }}> |
||||
<span style={{ fontSize: 12, fontWeight: 700, color: 'var(--c-primary-darker)' }}>{v.date} {v.year} · {v.time}</span> |
||||
<span style={{ padding: '3px 10px', borderRadius: 999, background: '#fff', fontSize: 11, fontWeight: 600 }}>{v.type}</span> |
||||
</div> |
||||
{v.diagnosis && ( |
||||
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 6 }}>{v.diagnosis}</div> |
||||
)} |
||||
<div style={{ display: 'flex', gap: 10, alignItems: 'center', marginBottom: v.conclusion ? 10 : 0 }}> |
||||
<Avatar init={doc.init} size={28} style={{ fontSize: 12 }} /> |
||||
<div className="sub" style={{ fontSize: 12 }}>{shortDoc(doc)}</div> |
||||
</div> |
||||
{v.conclusion && ( |
||||
<div style={{ fontSize: 12, padding: '10px 12px', background: '#fff', borderRadius: 10, lineHeight: 1.5, color: 'var(--c-fg-2)' }}> |
||||
{v.conclusion} |
||||
</div> |
||||
)} |
||||
</PlateCard> |
||||
); |
||||
})} |
||||
</div> |
||||
)} |
||||
|
||||
{tab === 'rx' && ( |
||||
<> |
||||
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 10 }}>Активный курс · {activeRx.length}</div> |
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 14 }}> |
||||
{activeRx.map(p => { |
||||
const doc = doctors.find(d => d.id === p.prescribedBy); |
||||
return ( |
||||
<PlateCard key={p.id} style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}> |
||||
<PlateIcon icon={I.pill} size={36} /> |
||||
<div style={{ flex: 1, minWidth: 0 }}> |
||||
<div style={{ fontSize: 14, fontWeight: 700 }}>{p.name}</div> |
||||
<div className="sub" style={{ fontSize: 12, marginTop: 2 }}>{p.dose} · {p.course}</div> |
||||
<div className="sub" style={{ fontSize: 11, marginTop: 4 }}>Назначил: {shortDoc(doc)}</div> |
||||
</div> |
||||
</PlateCard> |
||||
); |
||||
})} |
||||
</div> |
||||
{pastRx.length > 0 && ( |
||||
<> |
||||
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 10 }}>Завершённые</div> |
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> |
||||
{pastRx.map(p => { |
||||
const doc = doctors.find(d => d.id === p.prescribedBy); |
||||
return ( |
||||
<PlateCard key={p.id}> |
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}> |
||||
<span style={{ fontSize: 14, fontWeight: 700 }}>{p.name}</span> |
||||
<span className="sub" style={{ fontSize: 11 }}>{p.course}</span> |
||||
</div> |
||||
<div className="sub" style={{ fontSize: 11, marginTop: 2 }}>{shortDoc(doc)}</div> |
||||
</PlateCard> |
||||
); |
||||
})} |
||||
</div> |
||||
</> |
||||
)} |
||||
</> |
||||
)} |
||||
|
||||
{tab === 'shots' && ( |
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> |
||||
{medcard.vaccinations.map(v => ( |
||||
<PlateCard key={v.id} style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}> |
||||
<PlateIcon icon={I.check} size={36} bg="var(--c-success)" /> |
||||
<div style={{ flex: 1, minWidth: 0 }}> |
||||
<div style={{ fontSize: 14, fontWeight: 700 }}>{v.name}</div> |
||||
<div className="sub" style={{ fontSize: 12, marginTop: 2 }}>{v.date} · партия {v.lot}</div> |
||||
</div> |
||||
</PlateCard> |
||||
))} |
||||
</div> |
||||
)} |
||||
|
||||
{tab === 'ops' && ( |
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}> |
||||
{medcard.surgeries.map(s => { |
||||
const doc = doctors.find(d => d.id === s.doctorId); |
||||
return ( |
||||
<PlateCard key={s.id}> |
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 8 }}> |
||||
<span style={{ fontSize: 14, fontWeight: 700 }}>{s.name}</span> |
||||
<span className="sub" style={{ fontSize: 12 }}>{s.date}</span> |
||||
</div> |
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginBottom: 10 }}> |
||||
<div style={{ padding: '8px 10px', background: '#fff', borderRadius: 10 }}> |
||||
<div className="sub" style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: .4, fontWeight: 700 }}>Хирург</div> |
||||
<div style={{ fontSize: 12, fontWeight: 700, marginTop: 2 }}>{shortDoc(doc)}</div> |
||||
</div> |
||||
<div style={{ padding: '8px 10px', background: '#fff', borderRadius: 10 }}> |
||||
<div className="sub" style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: .4, fontWeight: 700 }}>Анестезия</div> |
||||
<div style={{ fontSize: 12, fontWeight: 700, marginTop: 2 }}>{s.anesthesia}</div> |
||||
</div> |
||||
</div> |
||||
<div style={{ fontSize: 12, padding: '8px 12px', background: 'var(--c-success-50)', borderRadius: 10, color: 'var(--c-fg-2)' }}> |
||||
{s.outcome} |
||||
</div> |
||||
</PlateCard> |
||||
); |
||||
})} |
||||
</div> |
||||
)} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
Loading…
Reference in new issue