Browse Source

Sprints 7–9: electronic medical card, splash home, plate design system

- 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
AR 15 M4 3 weeks ago
parent
commit
e9a8171252
  1. 68
      SPRINTS.md
  2. 10
      src/App.jsx
  3. 19
      src/PhoneApp.jsx
  4. 62
      src/data.js
  5. 54
      src/docs.js
  6. 150
      src/screens/screens-home.jsx
  7. 384
      src/screens/screens-misc.jsx
  8. 619
      src/screens/screens-plate.jsx

68
SPRINTS.md

@ -113,6 +113,74 @@ _заполнить в конце спринта_
---
## Спринт 9 · 21 апр 2026
**Цель:** ввести переключатель дизайн-системы в Tweaks (раздел **«Дизайн»**: «Клод» / «Прозрачная карточка») и начать перерабатывать экраны в стиле «Светлой плитки» (primary-50 карточки, круглые teal-иконки, крупные h1, тонкие бордеры primary-100, минимум градиентов).
**Идея.** «Клод» — текущий нейтральный дизайн (Spacious gradient + разные tint-карточки). «Прозрачная карточка» — унифицированный стиль на базе «Светлой плитки»: каждая сущность в primary-50 карточке со скруглением 16, шапки — белая кнопка-назад в primary-50 кружке, плитка-первее-списка.
### Инфраструктура
- [x] `TWEAKS_DEFAULT.design = 'claude'` + `DESIGN_OPTIONS` (App.jsx)
- [x] Группа «Дизайн» в TweaksPanel (2 кнопки)
- [x] `ctx.design` прокинут в PhoneApp.renderScreen
- [x] В plate-режиме главная всегда = `HomeSplashScreen` (homeVariant форсится на `splash`)
- [x] Механизм fallback: экраны без plate-версии показываются в Клоде
### Plate-версии экранов
- [x] Главная — `HomeSplashScreen` (уже готов из Спринта 8, базовый эталон)
- [x] Профиль — `ProfilePlateScreen` (паспорт + 2 быстрые кнопки QR/Карта + 4 секции списком plate-карточек)
- [x] Приёмы — `ApptsPlateScreen` (табы как pill-сегмент, карточка приёма с «Активно», чипы даты/времени/адреса)
- [x] Детали приёма — `ApptDetailPlateScreen` (крупное время в plate-hero + заключение с белыми вложенными pane)
- [x] Электронная карта — `MedcardPlateScreen` (паспорт + pill-табы + все 5 табов в plate-стиле)
### Задел на следующий спринт
- [ ] Врачи (список + карточка врача) в plate-стиле
- [ ] Флоу записи (specs/doctor/time/confirm/success) в plate-стиле
- [ ] Чаты (список + конверсация) в plate-стиле
- [ ] Результаты (список + аудио + эндоскопия) в plate-стиле
- [ ] Восстановление, тест слуха — в plate-стиле
- [ ] Уведомления, QR — в plate-стиле
- [ ] Статьи, контакты, цены, поиск — в plate-стиле
- [ ] docs.js — отдельные записи для plate-вариантов если стилистика существенно отличается от Клода
### Итоги
_заполнить в конце спринта_
---
## Спринт 8 · 21 апр 2026
**Цель:** добавить в Tweaks 5-й вариант главной по макету Stitch — приветственный экран с тёплой карточкой записи и плиткой 2×2 «Полезная информация».
**Итоги.** Закрыт. Реализован `HomeSplashScreen` (screens-home.jsx) + вспомогательный `SplashTile`. В процессе название варианта переименовано из «Сплэш» → **«Светлая плитка»** (id=`splash`) — более понятно на русском. Имя пациента тянется из `patient.shortName`, ближайший приём из `appointments`. Доступ: Tweaks → «Главный экран» → «Светлая плитка». Стал базой для дизайн-системы «Прозрачная карточка» в Спринте 9.
### План
- [x] `HomeSplashScreen` в `screens-home.jsx`: шапка (аватар + «Главная» + колокольчик), крупный h1 «Добрый день, {имя}!», поиск врача, секция «Записи на прием» (ближайшая + мои приёмы + warm-CTA записи), «Услуги и консультации» 2×1, «Полезная информация» 2×2
- [x] Вспомогательный компонент `SplashTile` — круглая teal-иконка + sub/main
- [x] Имя пациента — из `patient.shortName` (единый источник правды с Профилем и Медкартой)
- [x] Подключить в `HOME_OPTIONS` (App.jsx) и в HOME-map (PhoneApp.jsx) как `splash` → «Светлая плитка»
- [x] Описание `home:splash` в `src/docs.js` (цель, задачи, design-решения, варианты)
---
## Спринт 7 · 21 апр 2026
**Цель:** превратить экран «Медкарта» в полноценную электронную карту пациента — единую точку правды о здоровье, связанную двусторонне с карточками прошедших приёмов и с Профилем.
### План
- [x] Расширить `data.js`: блок `patient` (ФИО, ДР, полис, СНИЛС, № карты, первый визит, лечащий врач) + блок `medcard` (allergies с severity/reaction, chronicConditions с МКБ-кодом, vaccinations, surgeries, prescriptions active/past)
- [x] Дополнить `appointments` past-визиты полями `diagnosis`, `diagnosisCode`, `conclusion`, `prescriptions[]`, `resultIds[]` + добавить визит `a5` (септопластика Синдяева 12 апр)
- [x] Переписать `MedcardScreen`: hero-паспорт с QR-кнопкой, 5 табов (Общее / Посещения / Назначения / Прививки / Операции), карточки посещений ведут в `appt:<id>`
- [x] `ApptDetailScreen`: блок «Заключение» теперь тянет данные из `a.diagnosis/conclusion/prescriptions/diagnosisCode`, добавлены CTA «В медкарте» и «Результаты обследований»
- [x] `ApptsTabScreen`: на вкладке «Прошедшие» primary-CTA «Электронная карта» (вместо «Записаться»)
- [x] `ProfileTabScreen`: шапка читает `patient`, пункт «Электронная карта» с featured-стилем и счётчиками (посещения / аллергии / диагнозы), новый чип «Карта №XXX» рядом с QR
- [x] Обновить `docs.js`: `medcard` и `appt` — отразить двустороннюю связь и новую структуру карты
### Итоги
_заполнить в конце спринта_
---
## Спринт 5 · 20 апр 2026
**Цель:** документация прототипа внутри самого прототипа — чтобы на ревью с коллегами можно было сразу увидеть цель и design-решения по любому экрану.

10
src/App.jsx

@ -16,8 +16,14 @@ const TWEAKS_DEFAULT = {
scale: 'auto',
showIntro: true,
docsEnabled: false,
design: 'claude',
};
const DESIGN_OPTIONS = [
{ id: 'claude', lb: 'Клод' },
{ id: 'plate', lb: 'Прозрачная карточка' },
];
const SCALE_OPTIONS = [
{ id: 'auto', lb: 'Авто' },
{ id: '0.5', lb: '50%' },
@ -30,6 +36,7 @@ const HOME_OPTIONS = [
{ id: 'list', lb: 'Лента' },
{ id: 'feed', lb: 'Таймлайн' },
{ id: 'timelineX', lb: 'Таймлайн X' },
{ id: 'splash', lb: 'Светлая плитка' },
];
const DOC_OPTIONS = [
{ id: 'rich', lb: 'Карточки+' },
@ -159,6 +166,7 @@ function TweaksPanel({ tw, setTw, onClose }) {
<button key="on" onClick={() => setTw({ ...tw, docsEnabled: true })} className={tw.docsEnabled ? 'on' : ''}>Вкл</button>,
<button key="off" onClick={() => setTw({ ...tw, docsEnabled: false })} className={!tw.docsEnabled ? 'on' : ''}>Выкл</button>,
])}
{group('Дизайн', opts(DESIGN_OPTIONS, 'design'))}
{tw.layout === 'single' && group('Масштаб', opts(SCALE_OPTIONS, 'scale'))}
{group('Компоновка', opts([
{ id:'single', lb:'1 телефон' },
@ -327,7 +335,7 @@ export default function App() {
useEffect(() => { setInnerScreen(tw.screen); }, [tw.screen]);
const palette = ACCENT_OPTIONS.find(a => a.id === tw.accent) || ACCENT_OPTIONS[0];
const ctx = { homeVariant: tw.homeVariant, docVariant: tw.docVariant, density: tw.density, palette };
const ctx = { homeVariant: tw.homeVariant, docVariant: tw.docVariant, density: tw.density, palette, design: tw.design };
const currentDoc = tw.docsEnabled ? getScreenDoc(innerScreen, ctx) : null;
const content = useMemo(() => {

19
src/PhoneApp.jsx

@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import { TabBar } from './components.jsx';
import { HomeCardsScreen, HomeListScreen, HomeFeedScreen, HomeTimelineXScreen } from './screens/screens-home.jsx';
import { HomeCardsScreen, HomeListScreen, HomeFeedScreen, HomeTimelineXScreen, HomeSplashScreen } from './screens/screens-home.jsx';
import {
BookingSpecsScreen, BookingDoctorScreen, BookingTimeScreen,
BookingConfirmScreen, BookingSuccessScreen,
@ -18,11 +18,26 @@ import { ArticlesScreen, ArticleDetailScreen } from './screens/screens-articles.
import { HomeV2Screen, SearchScreen, ContactsScreen, PricesScreen } from './screens/screens-v2.jsx';
import { DevColorsScreen, DevExamplesScreen } from './screens/screens-dev.jsx';
import { DocsScreen } from './screens/screens-docs.jsx';
import { ProfilePlateScreen, ApptsPlateScreen, ApptDetailPlateScreen, MedcardPlateScreen } from './screens/screens-plate.jsx';
function renderScreen(screenId, nav, ctx) {
const parts = screenId.split(':');
const id = parts[0];
const HOME = { cards: HomeCardsScreen, list: HomeListScreen, feed: HomeFeedScreen, timelineX: HomeTimelineXScreen }[ctx.homeVariant] || HomeCardsScreen;
const plate = ctx.design === 'plate';
// В plate-режиме главная всегда «Светлая плитка», независимо от homeVariant
const homeVariant = plate ? 'splash' : ctx.homeVariant;
const HOME = { cards: HomeCardsScreen, list: HomeListScreen, feed: HomeFeedScreen, timelineX: HomeTimelineXScreen, splash: HomeSplashScreen }[homeVariant] || HomeCardsScreen;
// Plate-подмены (fallback на Клод если plate-версии нет)
if (plate) {
switch (id) {
case 'profile': return <ProfilePlateScreen nav={nav} ctx={ctx} />;
case 'appts': return <ApptsPlateScreen nav={nav} />;
case 'appt': return <ApptDetailPlateScreen nav={nav} apptId={parts[1]} />;
case 'medcard': return <MedcardPlateScreen nav={nav} />;
}
}
switch (id) {
case 'home': return <HOME nav={nav} ctx={ctx} />;
case 'home-v2': return <HomeV2Screen nav={nav} ctx={ctx} />;

62
src/data.js

@ -2,6 +2,48 @@
// (oclinica.ru/lor) — врачи, услуги, цены.
export const CLINIC_DATA = {
patient: {
fullName: 'Разорвина Анна Сергеевна',
shortName: 'Анна Сергеевна',
init: 'АС',
birthDate: '14 июня 1983',
age: 42,
sex: 'Женский',
height: 168,
weight: 62,
bloodType: 'II (A), Rh+',
phone: '+7 (912) 485-••-••',
email: 'arazor72@gmail.com',
policy: 'ОМС 5590 1234 5678 9012',
snils: '123-456-789 01',
cardNumber: 'УГН-2014-00482',
firstVisit: '14 фев 2014',
primaryDoctorId: 'makarova',
},
medcard: {
allergies: [
{ id: 'a1', name: 'Пенициллин', severity: 'high', reaction: 'крапивница, отёк Квинке', noted: '2019' },
{ id: 'a2', name: 'Пыльца берёзы', severity: 'mid', reaction: 'ринит, слезотечение', noted: '2021' },
],
chronicConditions: [
{ id: 'c1', name: 'Хронический тонзиллит', stage: 'Компенсированная форма', diagnosed: '2022', doctorId: 'makarova', code: 'J35.0' },
{ id: 'c2', name: 'Искривление носовой перегородки', stage: 'Прооперировано', diagnosed: '2025', doctorId: 'syndaev', code: 'J34.2' },
],
vaccinations: [
{ id: 'v1', name: 'Грипп (Совигрипп)', date: '12 окт 2025', lot: 'SV-2025-44' },
{ id: 'v2', name: 'Пневмококк (Превенар 13)', date: '4 мая 2024', lot: 'PV-0424' },
{ id: 'v3', name: 'COVID-19 (Спутник V)', date: '18 сен 2022', lot: 'SP-5582' },
],
surgeries: [
{ id: 's1', name: 'Септопластика', date: '12 апр 2026', doctorId: 'syndaev', anesthesia: 'Общая', outcome: 'Без осложнений', apptId: null },
{ id: 's2', name: 'Тонзиллотомия (частичная)', date: '22 мар 2018', doctorId: 'makarova', anesthesia: 'Местная', outcome: 'Ремиссия 6 лет' },
],
prescriptions: [
{ id: 'm1', name: 'Аква Марис', dose: '4 раза в день', course: 'с 12 апр, 14 дней', prescribedBy: 'syndaev', active: true, forApptId: null },
{ id: 'm2', name: 'Амоксиклав 625 мг', dose: '2 раза в день', course: 'с 12 апр, 7 дней', prescribedBy: 'syndaev', active: true, forApptId: null },
{ id: 'm3', name: 'Назонекс', dose: '2 впрыск./сут', course: '14 дней', prescribedBy: 'makarova', active: false, forApptId: 'a3' },
],
},
clinic: {
name: 'Клиника УГН',
full: 'Клиника ухо, горло, нос им. проф. Е.Н. Оленевой',
@ -49,8 +91,24 @@ export const CLINIC_DATA = {
appointments: [
{ id: 'a1', status: 'upcoming', doctor: 'semerikova', date: '21 апр', weekday: 'понедельник', time: '16:00', room: 'Каб. 204', address: 'tsetkin', type: 'Первичный приём' },
{ id: 'a2', status: 'upcoming', doctor: 'torsunova', date: '25 апр', weekday: 'пятница', time: '11:00', room: 'Каб. 118', address: 'tsetkin', type: 'Аудиометрия' },
{ id: 'a3', status: 'past', doctor: 'makarova', date: '8 апр', weekday: 'среда', time: '10:30', room: 'Каб. 202', address: 'tsetkin', type: 'Первичный приём', hasReport: true },
{ id: 'a4', status: 'past', doctor: 'zykin', date: '28 мар', weekday: 'пятница', time: '15:00', room: 'Каб. 210', address: 'tsetkin', type: 'Консультация', hasReport: true },
{ id: 'a3', status: 'past', doctor: 'makarova', date: '8 апр', year: 2026, weekday: 'среда', time: '10:30', room: 'Каб. 202', address: 'tsetkin', type: 'Первичный приём', hasReport: true,
diagnosis: 'Хронический риносинусит, обострение',
diagnosisCode: 'J32.4',
conclusion: 'Слизистая гиперемирована, отёчна. Выделений гнойных нет. Рекомендую медикаментозное лечение, контрольный осмотр через 2 недели.',
prescriptions: ['Аква Марис — промывание 4 р/д, 14 дней', 'Назонекс — 2 впрыск./сут, 14 дней'],
resultIds: ['r1', 'r2'] },
{ id: 'a4', status: 'past', doctor: 'zykin', date: '28 мар', year: 2026, weekday: 'пятница', time: '15:00', room: 'Каб. 210', address: 'tsetkin', type: 'Консультация', hasReport: true,
diagnosis: 'Состояние после ОРВИ, осложнений нет',
diagnosisCode: 'J06.9',
conclusion: 'Жалоб активно нет. Осмотр ЛОР-органов без патологии. Голосовая функция сохранна.',
prescriptions: ['Полоскание солевым раствором по необходимости'],
resultIds: ['r4'] },
{ id: 'a5', status: 'past', doctor: 'syndaev', date: '12 апр', year: 2026, weekday: 'воскресенье', time: '09:00', room: 'Опер. 1', address: 'tsetkin', type: 'Операция: септопластика', hasReport: true,
diagnosis: 'Искривление носовой перегородки',
diagnosisCode: 'J34.2',
conclusion: 'Септопластика. Восстановление функции носового дыхания. Осложнений нет. На 14 дней — стационар/амбулаторно по схеме.',
prescriptions: ['Амоксиклав 625 мг × 2 р/д, 7 дней', 'Аква Марис 4 р/д, 14 дней', 'Нурофен при боли'],
resultIds: [] },
],
results: [
{ id: 'r1', name: 'Аудиограмма', date: '8 апр 2026', doctor: 'makarova', status: 'ready', kind: 'audio' },

54
src/docs.js

@ -75,6 +75,28 @@ export const SCREEN_DOCS = {
'Рекомендации emoji-карточками горизонтально — лёгкое lifestyle-чтение, не давит на верхние задачи',
],
},
'home:splash': {
title: 'Главная 1 · Светлая плитка',
category: 'Главная',
goal: 'Приветственный дом с крупной типографикой и тёплыми карточками. Фокус на первой записи на приём: ближайшая запись → мои приёмы → запись новой. Ниже — услуги и полезная информация 2×N.',
tasks: [
'Увидеть персональное приветствие и открыть поиск врача',
'Попасть в ближайшую запись (фото врача, дата, время, локация, «Активно»)',
'Перейти в «Мои приёмы» из контекстной карточки',
'Нажать тёплый CTA «Записаться на приём → Выбрать удобное время»',
'Открыть связаться с врачом в чате одним тапом',
'Пролистать полезную информацию (статья дня, все статьи, цены, контакты)',
],
rationale: [
'Минималистичная шапка (аватар + «Главная» + колокольчик) вместо градиента — акцент сразу на h1-приветствии',
'Ближайшая запись как выделенная primary-50 карточка с caps-label «БЛИЖАЙШАЯ ЗАПИСЬ» + «★ Активно» — ясный статус и сканируемая структура',
'Чипы даты/времени белым поверх primary-50 — читаемость и тактильная приглашённость к нажатию',
'Тёплая warm-100 карточка записи с декоративными кругами и белой вложенной кнопкой — единственный визуальный «горячий» CTA на экране, притягивает глаз',
'Сетки 2×1 и 2×2 с одинаковыми тайлами (круглая teal-иконка, sub/main тексты) — ритмичная, предсказуемая плитка, легко сканируется',
'Все 8 CTA-карточек видны без скролла или в 1 свайп — плотная, но не перегруженная сетка',
],
variants: 'В Tweaks «Главный экран»: Карточки / Лента / Таймлайн / Таймлайн X / Светлая плитка — разные приоритеты контента.',
},
'home-v2': {
title: 'Главная 2',
category: 'Главная',
@ -223,20 +245,23 @@ export const SCREEN_DOCS = {
'appt': {
title: 'Детали приёма',
category: 'Приёмы и результаты',
goal: 'Полная карточка приёма: дата/время, врач, адрес, контакты, заключение (для прошедших).',
goal: 'Полная карточка приёма: дата/время, врач, адрес, контакты, заключение с диагнозом, назначениями и связкой с медкартой (для прошедших).',
tasks: [
'Увидеть дату, время, тип приёма',
'Открыть карточку врача',
'Посмотреть адрес на карте',
'Позвонить в клинику',
'Отменить или перенести (для предстоящих)',
'Открыть заключение PDF (для прошедших)',
'Прочитать заключение, диагноз с кодом МКБ, назначения (для прошедших)',
'Перейти в электронную карту или к результатам обследований',
],
rationale: [
'Крупное время 42px monospace-narrow — главное, что пациент ищет',
'Адрес отдельной секцией с кнопкой карты — частый re-check',
'Кнопка «Отменить» приглушённым danger — чтобы случайно не нажать',
'Перенос primary — предполагаемое действие',
'Блок заключения: диагноз+код МКБ, conclusion-текст, список назначений с иконкой — та же структура, что в «Посещениях» медкарты',
'CTA «В медкарте» и «Результаты» — двунаправленная связь с электронной картой',
],
},
'results': {
@ -438,20 +463,27 @@ export const SCREEN_DOCS = {
],
},
'medcard': {
title: 'Медицинская карта',
title: 'Электронная карта пациента',
category: 'Здоровье',
goal: 'Медкарта: основное, аллергии, история диагнозов.',
goal: 'Полная амбулаторная карта в телефоне: паспорт пациента, аллергии, диагнозы, история посещений с заключениями, активные назначения, прививки, операции.',
tasks: [
'Увидеть пол, возраст, рост/вес, группу крови',
'Проверить аллергии',
'Просмотреть историю диагнозов с датами',
'Добавить аллергию',
'Увидеть паспорт: ФИО, ДР, № карты, полис, группа крови',
'Проверить аллергии и их реакции перед назначением препарата',
'Просмотреть хронические диагнозы с кодом МКБ и лечащим врачом',
'Открыть посещение → попасть в детали приёма с заключением и назначениями',
'Увидеть активный курс лекарств и перейти к расписанию приёма',
'Просмотреть историю прививок с партиями и операций с исходами',
],
rationale: [
'Основные данные в label/value списке — табличная структура',
'Аллергии как красные чипы — критическая инфа',
'История — плоская лента с датой, диагнозом, врачом',
'Hero-блок с паспортом + QR + № карты — сразу понятен контекст («моя карта»)',
'Сегмент-табы (Общее / Посещения / Назначения / Прививки / Операции) — разделяем 5 разных типов данных без вертикального скролла в одну ленту',
'Аллергии в первой секции Общего — критическая инфа видна без переключения табов',
'Чипсы severity (Опасная/Средняя/Лёгкая) — быстрое считывание риска',
'Каждое посещение — тап-область с заключением и связью «Открыть» на карточку приёма (двунаправленная навигация)',
'Назначения разделены на активные (с крупной иконкой-таблеткой) и завершённые (компактный список) — фокус на актуальном',
'Операции с плашкой «Исход» в success-tone — позитивное закрытие эпизода',
],
variants: 'Данные пациента едины с Профилем (patient в data.js) — изменение в карте отражается в шапке Профиля, QR и шапке Приёмов.',
},
'notifications': {
title: 'Уведомления',

150
src/screens/screens-home.jsx

@ -438,6 +438,156 @@ export function HomeTimelineXScreen({ nav }) {
);
}
export function HomeSplashScreen({ nav }) {
const { patient, doctors, appointments, articles } = CLINIC_DATA;
const firstName = patient.shortName.split(' ')[0];
const upcoming = appointments.find(a => a.status === 'upcoming');
const upDoc = upcoming && doctors.find(d => d.id === upcoming.doctor);
const article = articles[0];
return (
<div style={{ paddingBottom: 100 }}>
<div style={{ padding: '8px 20px 8px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<button onClick={() => nav.push('profile')} className="press" style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 34, height: 34, borderRadius: 999, background: 'var(--c-primary-200)' }} />
<span style={{ fontSize: 16, fontWeight: 600, color: 'var(--c-fg-1)' }}>Главная</span>
</button>
<button onClick={() => nav.push('notifications')} className="press" style={{ width: 40, height: 40, borderRadius: 999, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<I.bell size={22} style={{ color: 'var(--c-primary-darker)' }} />
</button>
</div>
<div style={{ padding: '4px 20px 14px' }}>
<h1 style={{ fontSize: 28, fontWeight: 800, lineHeight: 1.15, color: 'var(--c-fg-1)', margin: 0 }}>Добрый день, {firstName}!</h1>
</div>
<div style={{ padding: '0 20px 20px' }}>
<button onClick={() => nav.push('search')} className="press" style={{
width: '100%', display: 'flex', alignItems: 'center', gap: 12,
padding: '14px 16px', background: '#fff',
border: '1px solid var(--c-border)', borderRadius: 14,
textAlign: 'left',
}}>
<I.search size={18} style={{ color: 'var(--c-fg-3)' }} />
<span style={{ color: 'var(--c-fg-3)', fontSize: 15 }}>Поиск врача...</span>
</button>
</div>
<div style={{ padding: '0 20px 8px' }}>
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--c-fg-1)', marginBottom: 10 }}>Записи на прием</div>
{upcoming && upDoc && (
<button onClick={() => nav.push('appt:' + upcoming.id)} className="press" style={{
width: '100%', textAlign: 'left',
background: 'var(--c-primary-50)',
border: '1px solid var(--c-primary-100)',
borderRadius: 16, padding: 16, marginBottom: 10,
}}>
<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)' }} />
БЛИЖАЙШАЯ ЗАПИСЬ
</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={upDoc.init} size={48} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--c-fg-1)' }}>{upDoc.name}</div>
<div style={{ fontSize: 13, color: 'var(--c-primary-darker)', marginTop: 2 }}>{upDoc.spec.split(' · ')[0]}</div>
</div>
</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 10 }}>
<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)' }} /> {upcoming.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)' }} /> {upcoming.time}
</span>
</div>
<I.pin size={16} style={{ color: 'var(--c-primary-darker)', opacity: .6 }} />
</button>
)}
<button onClick={() => nav.set('appts')} className="press" style={{
width: '100%', textAlign: 'left',
background: 'var(--c-primary-50)',
border: '1px solid var(--c-primary-100)',
borderRadius: 16, padding: 16, marginBottom: 10,
display: 'flex', gap: 14, alignItems: 'center',
}}>
<div style={{ width: 54, height: 54, borderRadius: 999, background: 'var(--c-primary-darker)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<I.calendar size={24} style={{ color: '#fff' }} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: .7, color: 'var(--c-primary-darker)', marginBottom: 4 }}>МОИ ПРИЕМЫ</div>
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--c-fg-1)', marginBottom: 2 }}>Ближайшие приемы</div>
<div style={{ fontSize: 12, color: 'var(--c-fg-3)', lineHeight: 1.35 }}>Просмотрите ваши предстоящие прием</div>
</div>
</button>
<button onClick={() => nav.push('booking-specs')} className="press" style={{
width: '100%', textAlign: 'left', position: 'relative', overflow: 'hidden',
background: 'var(--c-warm-100)',
borderRadius: 16, padding: 16, marginBottom: 18,
}}>
<div style={{ position: 'absolute', top: -20, right: -20, width: 90, height: 90, borderRadius: 999, background: 'rgba(255,255,255,0.35)' }} />
<div style={{ position: 'absolute', top: 30, right: 30, width: 40, height: 40, borderRadius: 999, background: 'rgba(255,255,255,0.25)' }} />
<div style={{ position: 'relative', display: 'flex', gap: 14, alignItems: 'center', marginBottom: 14 }}>
<div style={{ width: 48, height: 48, borderRadius: 999, background: 'var(--c-warm-text)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<I.calendar size={22} style={{ color: '#fff' }} />
</div>
<div style={{ fontSize: 16, fontWeight: 700, color: 'var(--c-fg-1)' }}>Записаться на прием</div>
</div>
<div style={{ position: 'relative', padding: 12, background: '#fff', borderRadius: 12, textAlign: 'center', fontSize: 14, fontWeight: 600, color: 'var(--c-fg-1)' }}>
Выбрать удобное время
</div>
</button>
</div>
<div style={{ padding: '0 20px 8px' }}>
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--c-fg-1)', marginBottom: 10 }}>Услуги и консультации</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2,1fr)', gap: 10, marginBottom: 18 }}>
<SplashTile icon={I.calendar} sub="Записаться на прием" main="К врачу" onClick={() => nav.push('booking-specs')} />
<SplashTile icon={I.chat} sub="Связаться с врачом в" main="Чате" onClick={() => nav.set('chat')} />
</div>
</div>
<div style={{ padding: '0 20px 20px' }}>
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--c-fg-1)', marginBottom: 10 }}>Полезная информация</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2,1fr)', gap: 10 }}>
<SplashTile icon={I.doc} sub="Статья" main={article.title.split(' ').slice(0,2).join(' ')} onClick={() => nav.push('article:' + article.id)} />
<SplashTile icon={I.file} sub="Все" main="Статьи" onClick={() => nav.push('articles')} />
<SplashTile icon={I.shield} sub="Информация" main="Цены" onClick={() => nav.push('prices')} />
<SplashTile icon={I.phone} sub="Информация" main="Контакты" onClick={() => nav.push('contacts')} />
</div>
</div>
</div>
);
}
function SplashTile({ icon: Ic, sub, main, onClick }) {
return (
<button onClick={onClick} className="press" style={{
background: 'var(--c-primary-50)',
border: '1px solid var(--c-primary-100)',
borderRadius: 16, padding: 16,
textAlign: 'left', minHeight: 120,
display: 'flex', flexDirection: 'column', justifyContent: 'space-between', gap: 12,
}}>
<div style={{ width: 40, height: 40, borderRadius: 999, background: 'var(--c-primary-darker)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Ic size={20} style={{ color: '#fff' }} />
</div>
<div>
<div style={{ fontSize: 12, color: 'var(--c-primary-darker)', marginBottom: 2 }}>{sub}</div>
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--c-fg-1)' }}>{main}</div>
</div>
</button>
);
}
export function HomeFeedScreen({ nav }) {
const { doctors, appointments, clinic, articles, recovery } = CLINIC_DATA;
const upcoming = appointments.find(a => a.status === 'upcoming');

384
src/screens/screens-misc.jsx

@ -29,10 +29,17 @@ export function ApptsTabScreen({ nav }) {
</div>
)}
</div>
<div style={{ padding: 16, marginTop: 12 }}>
<div style={{ padding: 16, marginTop: 12, display: 'flex', flexDirection: 'column', gap: 10 }}>
{tab === 'upcoming' && (
<button onClick={() => nav.push('booking-specs')} className="btn-p block">
<I.plus size={18} /> Записаться на приём
</button>
)}
{tab === 'past' && (
<button onClick={() => nav.push('medcard')} className="btn-p block">
<I.file size={18} /> Электронная карта
</button>
)}
</div>
</div>
);
@ -92,16 +99,43 @@ export function ApptDetailScreen({ nav, apptId }) {
{a.hasReport && (
<div style={{ marginTop: 16 }}>
<div className="h-sec" style={{ marginBottom: 10 }}>Заключение врача</div>
<div className="h-sec" style={{ marginBottom: 10, 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>
<div className="card">
{a.diagnosis && <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 6 }}>{a.diagnosis}</div>}
<div style={{ fontSize: 14, lineHeight: 1.55, color: 'var(--c-fg-2)' }}>
Диагноз: хронический риносинусит, обострение. Назначено: Аква Марис, Назонекс 14 дней. Контрольный осмотр через 2 недели.
{a.conclusion || 'Заключение недоступно.'}
</div>
<button className="btn-s" style={{ marginTop: 12, padding: '8px 14px', fontSize: 13 }}>
{a.prescriptions && a.prescriptions.length > 0 && (
<>
<div style={{ fontSize: 11, textTransform: 'uppercase', letterSpacing: .5, fontWeight: 700, color: 'var(--c-fg-3)', marginTop: 14, 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, color: 'var(--c-fg-2)', 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="btn-s" style={{ padding: '8px 14px', fontSize: 13 }}>
<I.doc size={15} /> Открыть PDF
</button>
<button onClick={() => nav.push('medcard')} className="btn-s" style={{ padding: '8px 14px', fontSize: 13 }}>
<I.file size={15} /> В медкарте
</button>
</div>
</div>
{a.resultIds && a.resultIds.length > 0 && (
<button onClick={() => nav.push('results')} className="btn-g" style={{ width: '100%', padding: 12, fontSize: 13, marginTop: 10 }}>
<I.doc size={15} /> Результаты обследований · {a.resultIds.length}
</button>
)}
</div>
)}
</div>
@ -607,13 +641,16 @@ export function AudioTestScreen({ nav }) {
}
export function ProfileTabScreen({ 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: 'История, диагнозы', go: 'medcard' },
{ i: I.doc, t: 'Анализы', s: '5 результатов', go: 'results' },
{ i: I.pill, t: 'Лекарства', s: '3 активных курса', go: 'recovery' },
{ 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' },
]
},
@ -646,13 +683,18 @@ export function ProfileTabScreen({ nav }) {
<div style={{ padding: '12px 20px 16px' }}>
<h1 className="h-screen" style={{ marginBottom: 18 }}>Профиль</h1>
<div className="card" style={{ padding: 18, display: 'flex', gap: 14, alignItems: 'center', background: 'linear-gradient(135deg, var(--c-primary-100), var(--c-warm-100))', border: 0 }}>
<Avatar init="АС" size={64} style={{ fontSize: 24, boxShadow: 'var(--sh-sm)' }} />
<div style={{ flex: 1 }}>
<div style={{ fontSize: 18, fontWeight: 700 }}>Анна Сергеевна</div>
<div className="sub" style={{ fontSize: 13, marginBottom: 6 }}>+7 (912) 485-- · 42 года</div>
<Avatar init={patient.init} size={64} style={{ fontSize: 24, boxShadow: 'var(--sh-sm)' }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 18, fontWeight: 700 }}>{patient.shortName}</div>
<div className="sub" style={{ fontSize: 13, marginBottom: 6 }}>{patient.phone} · {patient.age} года</div>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
<button onClick={() => nav.push('qr')} className="chip" style={{ fontSize: 12, fontWeight: 700 }}>
<I.qr size={12} /> QR пациента
</button>
<button onClick={() => nav.push('medcard')} className="chip" style={{ fontSize: 12, fontWeight: 700 }}>
<I.file size={12} /> Карта {patient.cardNumber.split('-').pop()}
</button>
</div>
</div>
</div>
</div>
@ -665,16 +707,16 @@ export function ProfileTabScreen({ nav }) {
const II = it.i;
return (
<React.Fragment key={i}>
<button onClick={() => it.go && nav.push(it.go)} className="press" style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 14, padding: '13px 16px', textAlign: 'left' }}>
<div style={{ width: 34, height: 34, borderRadius: 9, background: 'var(--c-primary-100)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<II size={18} style={{ color: 'var(--c-primary-darker)' }} />
<button onClick={() => it.go && nav.push(it.go)} className="press" style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 14, padding: '13px 16px', textAlign: 'left', background: it.featured ? 'var(--c-primary-50)' : 'transparent' }}>
<div style={{ width: 34, height: 34, borderRadius: 9, background: it.featured ? 'var(--c-primary-darker)' : 'var(--c-primary-100)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<II size={18} style={{ color: it.featured ? '#fff' : 'var(--c-primary-darker)' }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 600 }}>{it.t}</div>
{it.s && <div className="sub" style={{ fontSize: 12 }}>{it.s}</div>}
<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, lineHeight: 1.35 }}>{it.s}</div>}
</div>
{it.badge && <span className="chip chip-warm" style={{ fontSize: 11 }}>{it.badge}</span>}
<I.chev size={15} style={{ color: 'var(--c-fg-4)' }} />
<I.chev size={15} style={{ color: 'var(--c-fg-4)', flexShrink: 0 }} />
</button>
{i < sec.items.length - 1 && <div style={{ height: 1, background: 'var(--c-divider)', marginLeft: 64 }} />}
</React.Fragment>
@ -782,50 +824,306 @@ export function TelemedScreen({ nav }) {
);
}
const MEDCARD_TABS = [
{ id: 'summary', lb: 'Общее' },
{ id: 'visits', lb: 'Посещения' },
{ id: 'rx', lb: 'Назначения' },
{ id: 'shots', lb: 'Прививки' },
{ id: 'ops', lb: 'Операции' },
];
function shortDoctor(d) {
if (!d) return '—';
const parts = d.name.split(' ');
return parts[0] + ' ' + (parts[1] ? parts[1][0] + '.' : '');
}
const SEVERITY_STYLE = {
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: 'Лёгкая' },
};
export function MedcardScreen({ 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) || 0); // простая сортировка
const activeRx = medcard.prescriptions.filter(p => p.active);
const pastRx = medcard.prescriptions.filter(p => !p.active);
return (
<div style={{ paddingBottom: 40 }}>
<ScreenHeader title="Медицинская карта" onBack={() => nav.pop()} />
<ScreenHeader title="Электронная карта" onBack={() => nav.pop()} rightIcon={I.search} />
{/* Hero — паспорт пациента */}
<div style={{ padding: '0 20px 14px' }}>
<div className="card" style={{ padding: 18, background: 'linear-gradient(135deg, var(--c-primary-100), var(--c-warm-100))', border: 0 }}>
<div style={{ display: 'flex', gap: 14, alignItems: 'center', marginBottom: 14 }}>
<Avatar init={patient.init} size={56} style={{ fontSize: 22, boxShadow: 'var(--sh-sm)' }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 17, fontWeight: 700, lineHeight: 1.25 }}>{patient.fullName}</div>
<div className="sub" style={{ fontSize: 12, marginTop: 3 }}>{patient.birthDate} · {patient.age} года · {patient.sex}</div>
</div>
<button onClick={() => nav.push('qr')} className="press" style={{ width: 38, height: 38, borderRadius: 10, background: 'rgba(255,255,255,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center' }} title="QR пациента">
<I.qr size={18} style={{ color: 'var(--c-primary-darker)' }} />
</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, fontSize: 12 }}>
<div>
<div className="sub" style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: .5, fontWeight: 700, marginBottom: 2 }}> карты</div>
<div style={{ fontWeight: 700, fontFamily: 'var(--font-narrow)' }}>{patient.cardNumber}</div>
</div>
<div>
<div className="sub" style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: .5, fontWeight: 700, marginBottom: 2 }}>Полис</div>
<div style={{ fontWeight: 700, fontFamily: 'var(--font-narrow)' }}>{patient.policy}</div>
</div>
</div>
</div>
</div>
{/* Табы-сегмент */}
<div style={{ padding: '0 16px 12px', overflowX: 'auto' }}>
<div className="seg" style={{ display: 'inline-flex', gap: 0, minWidth: '100%' }}>
{MEDCARD_TABS.map(t => (
<button key={t.id} onClick={() => setTab(t.id)} className={tab === t.id ? 'on' : ''} style={{ flex: 1, whiteSpace: 'nowrap', padding: '8px 12px' }}>{t.lb}</button>
))}
</div>
</div>
<div style={{ padding: '0 20px' }}>
{tab === 'summary' && (
<>
{/* Аллергии */}
<div className="h-sec" style={{ padding: '4px 4px 8px' }}>Аллергии · {medcard.allergies.length}</div>
<div className="card" style={{ padding: 14, marginBottom: 14 }}>
{medcard.allergies.map((a, i) => {
const s = SEVERITY_STYLE[a.severity] || SEVERITY_STYLE.low;
return (
<div key={a.id} style={{ display: 'flex', gap: 12, alignItems: 'flex-start', padding: i === 0 ? '0 0 10px' : '10px 0', borderTop: i === 0 ? 0 : '1px solid var(--c-divider)' }}>
<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>
);
})}
<button className="btn-g" style={{ width: '100%', padding: 10, fontSize: 13, marginTop: 10 }}>
<I.plus size={14} /> Добавить аллергию
</button>
</div>
{/* Хронические */}
<div className="h-sec" style={{ padding: '4px 4px 8px' }}>Хронические диагнозы</div>
<div className="card" style={{ padding: 0, marginBottom: 14 }}>
{medcard.chronicConditions.map((c, i, a) => {
const doc = doctors.find(d => d.id === c.doctorId);
return (
<React.Fragment key={c.id}>
<div style={{ padding: '14px 16px' }}>
<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 }}>Наблюдает: {shortDoctor(doc)}</div>
</div>
{i < a.length - 1 && <div className="divider" />}
</React.Fragment>
);
})}
</div>
{/* Антропометрия и кровь */}
<div className="h-sec" style={{ padding: '4px 4px 8px' }}>Основное</div>
<div className="card" style={{ marginBottom: 14 }}>
<div className="h-row" style={{ marginBottom: 10 }}>Основное</div>
{[['Пол','Женский'],['Возраст','42 года'],['Рост / Вес','168 см · 62 кг'],['Группа крови','II (A), Rh+']].map(([k,v])=>(
<div key={k} style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0' }}>
{[
['Рост / Вес', patient.height + ' см · ' + patient.weight + ' кг'],
['Группа крови', patient.bloodType],
['СНИЛС', patient.snils],
['Первое обращение', patient.firstVisit],
['Лечащий врач', shortDoctor(doctors.find(d => d.id === patient.primaryDoctorId))],
].map(([k, v]) => (
<div key={k} style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0', fontSize: 13 }}>
<span className="sub">{k}</span>
<span style={{ fontSize: 14, fontWeight: 700 }}>{v}</span>
<span style={{ fontWeight: 700, textAlign: 'right' }}>{v}</span>
</div>
))}
</div>
<div className="card" style={{ marginBottom: 14 }}>
<div className="h-row" style={{ marginBottom: 10 }}>Аллергии</div>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
<span className="chip chip-danger">Пенициллин</span>
<span className="chip chip-danger">Пыльца берёзы</span>
<span className="chip chip-soft">+ добавить</span>
<button onClick={() => nav.push('results')} className="btn-g" style={{ width: '100%', padding: 12, fontSize: 13, marginBottom: 10 }}>
<I.doc size={15} /> Анализы и обследования · {results.length}
</button>
</>
)}
{tab === 'visits' && (
<>
<div className="h-sec" style={{ padding: '4px 4px 8px' }}>Посещения · {pastVisits.length}</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{pastVisits.map(v => {
const doc = doctors.find(d => d.id === v.doctor);
return (
<button key={v.id} onClick={() => nav.push('appt:' + v.id)} className="card press" style={{ padding: 14, textAlign: 'left', display: 'block' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 6 }}>
<span className="sub" style={{ fontSize: 12, fontWeight: 700 }}>{v.date} {v.year} · {v.time}</span>
<span className="chip chip-soft" style={{ fontSize: 10 }}>{v.type}</span>
</div>
{v.diagnosis && (
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 4, display: 'flex', gap: 6, alignItems: 'baseline' }}>
<span>{v.diagnosis}</span>
{v.diagnosisCode && <span className="sub" style={{ fontSize: 10, fontFamily: 'var(--font-narrow)' }}>{v.diagnosisCode}</span>}
</div>
)}
<div style={{ display: 'flex', gap: 10, alignItems: 'center', marginBottom: v.conclusion ? 8 : 0 }}>
<Avatar init={doc.init} size={26} style={{ fontSize: 12 }} />
<div className="sub" style={{ fontSize: 12 }}>{shortDoctor(doc)} · {doc.spec.split(' · ')[0]}</div>
</div>
{v.conclusion && (
<div style={{ fontSize: 12, color: 'var(--c-fg-2)', lineHeight: 1.45, padding: '8px 10px', background: 'var(--c-primary-50)', borderRadius: 8 }}>
{v.conclusion}
</div>
)}
<div style={{ display: 'flex', gap: 8, marginTop: 10, flexWrap: 'wrap' }}>
{v.resultIds && v.resultIds.length > 0 && (
<span className="chip" style={{ fontSize: 11 }}><I.doc size={11} /> {v.resultIds.length} результат(а)</span>
)}
{v.prescriptions && v.prescriptions.length > 0 && (
<span className="chip" style={{ fontSize: 11 }}><I.pill size={11} /> Назначения · {v.prescriptions.length}</span>
)}
<span className="chip chip-soft" style={{ fontSize: 11, marginLeft: 'auto' }}>Открыть <I.chev size={11} /></span>
</div>
</button>
);
})}
</div>
</>
)}
<div className="h-sec" style={{ padding: '4px 4px 10px' }}>История диагнозов</div>
<div className="card" style={{ padding: 0 }}>
{[
{ d: '12 апр 2026', t: 'Искривление носовой перегородки', doc: 'Синдяев А.В.', tag: 'Операция' },
{ d: '8 апр 2026', t: 'Хронический риносинусит', doc: 'Макарова Л.Г.', tag: 'Приём' },
{ d: '15 ноя 2025', t: 'ОРВИ', doc: 'Суворова С.В.', tag: 'Приём' },
].map((r,i,a)=>(
<div key={i}>
<div style={{ padding: '14px 16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span className="sub" style={{ fontSize: 12 }}>{r.d}</span>
<span className="chip chip-soft" style={{ fontSize: 10 }}>{r.tag}</span>
{tab === 'rx' && (
<>
<div className="h-sec" style={{ padding: '4px 4px 8px' }}>Активный курс · {activeRx.length}</div>
<div className="card" style={{ padding: 0, marginBottom: 14 }}>
{activeRx.map((p, i, a) => {
const doc = doctors.find(d => d.id === p.prescribedBy);
return (
<React.Fragment key={p.id}>
<div style={{ padding: '14px 16px', display: 'flex', gap: 12, alignItems: 'flex-start' }}>
<div style={{ width: 34, height: 34, borderRadius: 9, background: 'var(--c-primary-100)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<I.pill size={16} style={{ color: 'var(--c-primary-darker)' }} />
</div>
<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 }}>Назначил: {shortDoctor(doc)}</div>
</div>
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 3 }}>{r.t}</div>
<div className="sub" style={{ fontSize: 12 }}>{r.doc}</div>
</div>
{i < a.length - 1 && <div className="divider" />}
</React.Fragment>
);
})}
{activeRx.length === 0 && <div style={{ padding: 20, textAlign: 'center' }} className="sub">Активных назначений нет</div>}
</div>
<button onClick={() => nav.push('recovery')} className="btn-g" style={{ width: '100%', padding: 12, fontSize: 13, marginBottom: 14 }}>
<I.clock size={15} /> Расписание приёма
</button>
{pastRx.length > 0 && (
<>
<div className="h-sec" style={{ padding: '4px 4px 8px' }}>Завершённые</div>
<div className="card" style={{ padding: 0, marginBottom: 14 }}>
{pastRx.map((p, i, a) => {
const doc = doctors.find(d => d.id === p.prescribedBy);
const appt = p.forApptId ? appointments.find(ap => ap.id === p.forApptId) : null;
return (
<React.Fragment key={p.id}>
<div style={{ padding: '12px 16px' }}>
<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 }}>{shortDoctor(doc)}</div>
{appt && (
<button onClick={() => nav.push('appt:' + appt.id)} className="chip chip-soft" style={{ marginTop: 6, fontSize: 11 }}>
Приём {appt.date} <I.chev size={10} />
</button>
)}
</div>
{i < a.length - 1 && <div className="divider" />}
</React.Fragment>
);
})}
</div>
</>
)}
</>
)}
{tab === 'shots' && (
<>
<div className="h-sec" style={{ padding: '4px 4px 8px' }}>Прививки · {medcard.vaccinations.length}</div>
<div className="card" style={{ padding: 0, marginBottom: 14 }}>
{medcard.vaccinations.map((v, i, a) => (
<React.Fragment key={v.id}>
<div style={{ padding: '14px 16px', display: 'flex', gap: 12, alignItems: 'flex-start' }}>
<div style={{ width: 34, height: 34, borderRadius: 9, background: 'var(--c-success-50)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, color: 'var(--c-success)' }}>
<I.check size={18} />
</div>
<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>
</div>
{i < a.length - 1 && <div className="divider" />}
</React.Fragment>
))}
</div>
<button className="btn-g" style={{ width: '100%', padding: 12, fontSize: 13 }}>
<I.plus size={14} /> Добавить прививку
</button>
</>
)}
{tab === 'ops' && (
<>
<div className="h-sec" style={{ padding: '4px 4px 8px' }}>Операции · {medcard.surgeries.length}</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{medcard.surgeries.map(s => {
const doc = doctors.find(d => d.id === s.doctorId);
const relatedAppt = appointments.find(ap => ap.doctor === s.doctorId && ap.date === s.date.split(' ').slice(0,2).join(' '));
return (
<div key={s.id} className="card" style={{ padding: 14 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 6 }}>
<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, fontSize: 12, marginBottom: 8 }}>
<div>
<div className="sub" style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: .4, fontWeight: 700 }}>Хирург</div>
<div style={{ fontWeight: 700 }}>{shortDoctor(doc)}</div>
</div>
<div>
<div className="sub" style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: .4, fontWeight: 700 }}>Анестезия</div>
<div style={{ fontWeight: 700 }}>{s.anesthesia}</div>
</div>
</div>
<div style={{ fontSize: 12, color: 'var(--c-fg-2)', padding: '8px 10px', background: 'var(--c-success-50)', borderRadius: 8 }}>
{s.outcome}
</div>
{relatedAppt && (
<button onClick={() => nav.push('appt:' + relatedAppt.id)} className="chip chip-soft" style={{ marginTop: 10, fontSize: 11 }}>
Карточка приёма <I.chev size={10} />
</button>
)}
</div>
);
})}
</div>
</>
)}
</div>
</div>
);

619
src/screens/screens-plate.jsx

@ -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…
Cancel
Save