diff --git a/SPRINTS.md b/SPRINTS.md index f64ce3d..a0c45c0 100644 --- a/SPRINTS.md +++ b/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:` +- [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-решения по любому экрану. diff --git a/src/App.jsx b/src/App.jsx index 2f59285..1e2e247 100644 --- a/src/App.jsx +++ b/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 }) { , , ])} + {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(() => { diff --git a/src/PhoneApp.jsx b/src/PhoneApp.jsx index 320cae9..df63408 100644 --- a/src/PhoneApp.jsx +++ b/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 ; + case 'appts': return ; + case 'appt': return ; + case 'medcard': return ; + } + } + switch (id) { case 'home': return ; case 'home-v2': return ; diff --git a/src/data.js b/src/data.js index 94de649..210ecc3 100644 --- a/src/data.js +++ b/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' }, diff --git a/src/docs.js b/src/docs.js index 09ccdb4..df0f1db 100644 --- a/src/docs.js +++ b/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: 'Уведомления', diff --git a/src/screens/screens-home.jsx b/src/screens/screens-home.jsx index 410266c..acda5e4 100644 --- a/src/screens/screens-home.jsx +++ b/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 ( +
+
+ + +
+ +
+

Добрый день, {firstName}!

+
+ +
+ +
+ +
+
Записи на прием
+ + {upcoming && upDoc && ( + + )} + + + + +
+ +
+
Услуги и консультации
+
+ nav.push('booking-specs')} /> + nav.set('chat')} /> +
+
+ +
+
Полезная информация
+
+ nav.push('article:' + article.id)} /> + nav.push('articles')} /> + nav.push('prices')} /> + nav.push('contacts')} /> +
+
+
+ ); +} + +function SplashTile({ icon: Ic, sub, main, onClick }) { + return ( + + ); +} + export function HomeFeedScreen({ nav }) { const { doctors, appointments, clinic, articles, recovery } = CLINIC_DATA; const upcoming = appointments.find(a => a.status === 'upcoming'); diff --git a/src/screens/screens-misc.jsx b/src/screens/screens-misc.jsx index 4981bd8..08ff43d 100644 --- a/src/screens/screens-misc.jsx +++ b/src/screens/screens-misc.jsx @@ -29,10 +29,17 @@ export function ApptsTabScreen({ nav }) { )} -
- +
+ {tab === 'upcoming' && ( + + )} + {tab === 'past' && ( + + )}
); @@ -92,15 +99,42 @@ export function ApptDetailScreen({ nav, apptId }) { {a.hasReport && (
-
Заключение врача
+
+ Заключение врача + {a.diagnosisCode && {a.diagnosisCode}} +
+ {a.diagnosis &&
{a.diagnosis}
}
- Диагноз: хронический риносинусит, обострение. Назначено: Аква Марис, Назонекс 14 дней. Контрольный осмотр через 2 недели. + {a.conclusion || 'Заключение недоступно.'} +
+ {a.prescriptions && a.prescriptions.length > 0 && ( + <> +
Назначения
+
    + {a.prescriptions.map((p, i) => ( +
  • + + {p} +
  • + ))} +
+ + )} +
+ +
-
+ {a.resultIds && a.resultIds.length > 0 && ( + + )}
)} @@ -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 }) {

Профиль

- -
-
Анна Сергеевна
-
+7 (912) 485-••-•• · 42 года
- + +
+
{patient.shortName}
+
{patient.phone} · {patient.age} года
+
+ + +
@@ -665,16 +707,16 @@ export function ProfileTabScreen({ nav }) { const II = it.i; return ( - {i < sec.items.length - 1 &&
} @@ -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 (
- nav.pop()} /> -
-
-
Основное
- {[['Пол','Женский'],['Возраст','42 года'],['Рост / Вес','168 см · 62 кг'],['Группа крови','II (A), Rh+']].map(([k,v])=>( -
- {k} - {v} + nav.pop()} rightIcon={I.search} /> + + {/* Hero — паспорт пациента */} +
+
+
+ +
+
{patient.fullName}
+
{patient.birthDate} · {patient.age} года · {patient.sex}
- ))} + +
+
+
+
№ карты
+
{patient.cardNumber}
+
+
+
Полис
+
{patient.policy}
+
+
+
-
-
Аллергии
-
- Пенициллин - Пыльца берёзы - + добавить -
+ {/* Табы-сегмент */} +
+
+ {MEDCARD_TABS.map(t => ( + + ))}
+
-
История диагнозов
-
- {[ - { d: '12 апр 2026', t: 'Искривление носовой перегородки', doc: 'Синдяев А.В.', tag: 'Операция' }, - { d: '8 апр 2026', t: 'Хронический риносинусит', doc: 'Макарова Л.Г.', tag: 'Приём' }, - { d: '15 ноя 2025', t: 'ОРВИ', doc: 'Суворова С.В.', tag: 'Приём' }, - ].map((r,i,a)=>( -
-
-
- {r.d} - {r.tag} +
+ {tab === 'summary' && ( + <> + {/* Аллергии */} +
Аллергии · {medcard.allergies.length}
+
+ {medcard.allergies.map((a, i) => { + const s = SEVERITY_STYLE[a.severity] || SEVERITY_STYLE.low; + return ( +
+
{s.lb}
+
+
{a.name}
+
{a.reaction} · с {a.noted}
+
+
+ ); + })} + +
+ + {/* Хронические */} +
Хронические диагнозы
+
+ {medcard.chronicConditions.map((c, i, a) => { + const doc = doctors.find(d => d.id === c.doctorId); + return ( + +
+
+ {c.name} + {c.code} +
+
{c.stage} · с {c.diagnosed}
+
Наблюдает: {shortDoctor(doc)}
+
+ {i < a.length - 1 &&
} + + ); + })} +
+ + {/* Антропометрия и кровь */} +
Основное
+
+ {[ + ['Рост / Вес', patient.height + ' см · ' + patient.weight + ' кг'], + ['Группа крови', patient.bloodType], + ['СНИЛС', patient.snils], + ['Первое обращение', patient.firstVisit], + ['Лечащий врач', shortDoctor(doctors.find(d => d.id === patient.primaryDoctorId))], + ].map(([k, v]) => ( +
+ {k} + {v}
-
{r.t}
-
{r.doc}
-
- {i < a.length - 1 &&
} + ))}
- ))} -
+ + + + )} + + {tab === 'visits' && ( + <> +
Посещения · {pastVisits.length}
+
+ {pastVisits.map(v => { + const doc = doctors.find(d => d.id === v.doctor); + return ( + + ); + })} +
+ + )} + + {tab === 'rx' && ( + <> +
Активный курс · {activeRx.length}
+
+ {activeRx.map((p, i, a) => { + const doc = doctors.find(d => d.id === p.prescribedBy); + return ( + +
+
+ +
+
+
{p.name}
+
{p.dose} · {p.course}
+
Назначил: {shortDoctor(doc)}
+
+
+ {i < a.length - 1 &&
} + + ); + })} + {activeRx.length === 0 &&
Активных назначений нет
} +
+ + + + {pastRx.length > 0 && ( + <> +
Завершённые
+
+ {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 ( + +
+
+ {p.name} + {p.course} +
+
{shortDoctor(doc)}
+ {appt && ( + + )} +
+ {i < a.length - 1 &&
} + + ); + })} +
+ + )} + + )} + + {tab === 'shots' && ( + <> +
Прививки · {medcard.vaccinations.length}
+
+ {medcard.vaccinations.map((v, i, a) => ( + +
+
+ +
+
+
{v.name}
+
{v.date} · партия {v.lot}
+
+
+ {i < a.length - 1 &&
} + + ))} +
+ + + )} + + {tab === 'ops' && ( + <> +
Операции · {medcard.surgeries.length}
+
+ {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 ( +
+
+ {s.name} + {s.date} +
+
+
+
Хирург
+
{shortDoctor(doc)}
+
+
+
Анестезия
+
{s.anesthesia}
+
+
+
+ {s.outcome} +
+ {relatedAppt && ( + + )} +
+ ); + })} +
+ + )}
); diff --git a/src/screens/screens-plate.jsx b/src/screens/screens-plate.jsx new file mode 100644 index 0000000..b3cc78f --- /dev/null +++ b/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 ( +
+ {onBack ? ( + + ) :
} +
{title}
+
{right || null}
+
+ ); +} + +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 ? ( + + ) : ( +
{children}
+ ); +} + +function PlateIcon({ icon: Ic, size = 40, bg = 'var(--c-primary-darker)', color = '#fff' }) { + return ( +
+ +
+ ); +} + +function PlateSection({ title, children, action, onAction }) { + return ( +
+
+
{title}
+ {action && } +
+ {children} +
+ ); +} + +function PlateH1({ children }) { + return

{children}

; +} + +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 ( +
+ nav.push('notifications')} className="press" style={{ width: 40, height: 40, borderRadius: 999, display: 'flex', alignItems: 'center', justifyContent: 'center' }}> + + + } /> + + {patient.shortName} + + {/* Паспорт */} +
+ +
+ +
+
{patient.phone}
+
{patient.birthDate} · {patient.age} года
+
+
+
+ + +
+
+
+ + {sections.map((sec, si) => ( + +
+ {sec.items.map((it, i) => { + const II = it.i; + return ( + it.go && nav.push(it.go)} style={{ display: 'flex', gap: 12, alignItems: 'center' }}> + +
+
{it.t}
+ {it.s &&
{it.s}
} +
+ {it.badge && {it.badge}} + +
+ ); + })} +
+
+ ))} +
+ ); +} + +// ---------- Приёмы ---------- + +export function ApptsPlateScreen({ nav }) { + const { appointments, doctors, clinic } = CLINIC_DATA; + const [tab, setTab] = useState('upcoming'); + const items = appointments.filter(a => a.status === tab); + + return ( +
+ nav.push('notifications')} className="press" style={{ width: 40, height: 40, borderRadius: 999, display: 'flex', alignItems: 'center', justifyContent: 'center' }}> + + + } /> + + Мои приёмы + +
+
+ + +
+
+ +
+ {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 ( + nav.push('appt:' + a.id)} tint={isUp ? 'soft' : 'soft'}> + {isUp && ( +
+ + + {a.date.toUpperCase()} · {a.time} + + + Активно + +
+ )} +
+ +
+
{d.name.split(' ').slice(0, 2).join(' ')}
+
{d.spec.split(' · ')[0]}
+
+
+
+ + {a.date} + + + {a.time} + + + {ad.short} + + {!isUp && a.hasReport && ( + + Заключение + + )} +
+
+ ); + })} + {items.length === 0 && ( +
Нет приёмов в этой категории
+ )} +
+ +
+ {tab === 'upcoming' && ( + nav.push('booking-specs')} style={{ position: 'relative', overflow: 'hidden' }}> +
+
+ +
Записаться на приём
+
+
Выбрать удобное время
+ + )} + {tab === 'past' && ( + nav.push('medcard')} style={{ display: 'flex', gap: 14, alignItems: 'center' }}> + +
+
Электронная карта
+
Все посещения, диагнозы, назначения
+
+ +
+ )} +
+
+ ); +} + +// ---------- Детали приёма ---------- + +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 ( +
+ nav.pop()} /> +
+ +
{a.weekday}, {a.date}
+
{a.time}
+
{a.type}
+
+ + nav.push('doctor:' + d.id)} style={{ marginBottom: 10, display: 'flex', gap: 12, alignItems: 'center', padding: 14 }}> + +
+
{d.name.split(' ').slice(0, 2).join(' ')}
+
{d.spec}
+
+ +
+ + +
+ +
+
{ad.full}
+
{a.room}
+
+
+
+ +
(342) 207-03-03
+
+
+ + {isUp && ( +
+ + +
+ )} + + {a.hasReport && ( + <> +
+ Заключение врача + {a.diagnosisCode && {a.diagnosisCode}} +
+ + {a.diagnosis &&
{a.diagnosis}
} +
+ {a.conclusion || 'Заключение недоступно.'} +
+ {a.prescriptions && a.prescriptions.length > 0 && ( + <> +
Назначения
+
    + {a.prescriptions.map((p, i) => ( +
  • + + {p} +
  • + ))} +
+ + )} +
+ + +
+
+ + )} +
+ + {isUp && ( +
+ + +
+ )} +
+ ); +} + +// ---------- Медкарта ---------- + +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 ( +
+ nav.pop()} right={ + + } /> + + {patient.shortName} + +
+ +
+ +
+
{patient.birthDate}
+
{patient.age} года · {patient.sex}
+
+ +
+
+
+
№ карты
+
{patient.cardNumber}
+
+
+
Полис
+
{patient.policy}
+
+
+
+
+ +
+
+ {PLATE_MEDCARD_TABS.map(t => ( + + ))} +
+
+ +
+ {tab === 'summary' && ( + <> +
Аллергии · {medcard.allergies.length}
+ + {medcard.allergies.map((a, i) => { + const s = SEVERITY_TINT[a.severity] || SEVERITY_TINT.low; + return ( +
+
{s.lb}
+
+
{a.name}
+
{a.reaction} · с {a.noted}
+
+
+ ); + })} +
+ +
Хронические диагнозы
+
+ {medcard.chronicConditions.map(c => { + const doc = doctors.find(d => d.id === c.doctorId); + return ( + +
+ {c.name} + {c.code} +
+
{c.stage} · с {c.diagnosed}
+
Наблюдает: {shortDoc(doc)}
+
+ ); + })} +
+ +
Основное
+ + {[ + ['Рост / Вес', patient.height + ' см · ' + patient.weight + ' кг'], + ['Группа крови', patient.bloodType], + ['СНИЛС', patient.snils], + ['Первое обращение', patient.firstVisit], + ['Лечащий врач', shortDoc(doctors.find(d => d.id === patient.primaryDoctorId))], + ].map(([k, v], i, arr) => ( +
+ {k} + {v} +
+ ))} +
+ + nav.push('results')} style={{ display: 'flex', gap: 12, alignItems: 'center' }}> + +
+
Анализы и обследования
+
{results.length} результатов
+
+ +
+ + )} + + {tab === 'visits' && ( +
+ {pastVisits.map(v => { + const doc = doctors.find(d => d.id === v.doctor); + return ( + nav.push('appt:' + v.id)}> +
+ {v.date} {v.year} · {v.time} + {v.type} +
+ {v.diagnosis && ( +
{v.diagnosis}
+ )} +
+ +
{shortDoc(doc)}
+
+ {v.conclusion && ( +
+ {v.conclusion} +
+ )} +
+ ); + })} +
+ )} + + {tab === 'rx' && ( + <> +
Активный курс · {activeRx.length}
+
+ {activeRx.map(p => { + const doc = doctors.find(d => d.id === p.prescribedBy); + return ( + + +
+
{p.name}
+
{p.dose} · {p.course}
+
Назначил: {shortDoc(doc)}
+
+
+ ); + })} +
+ {pastRx.length > 0 && ( + <> +
Завершённые
+
+ {pastRx.map(p => { + const doc = doctors.find(d => d.id === p.prescribedBy); + return ( + +
+ {p.name} + {p.course} +
+
{shortDoc(doc)}
+
+ ); + })} +
+ + )} + + )} + + {tab === 'shots' && ( +
+ {medcard.vaccinations.map(v => ( + + +
+
{v.name}
+
{v.date} · партия {v.lot}
+
+
+ ))} +
+ )} + + {tab === 'ops' && ( +
+ {medcard.surgeries.map(s => { + const doc = doctors.find(d => d.id === s.doctorId); + return ( + +
+ {s.name} + {s.date} +
+
+
+
Хирург
+
{shortDoc(doc)}
+
+
+
Анестезия
+
{s.anesthesia}
+
+
+
+ {s.outcome} +
+
+ ); + })} +
+ )} +
+
+ ); +}