diff --git a/SPRINTS.md b/SPRINTS.md index e63789d..c31fb97 100644 --- a/SPRINTS.md +++ b/SPRINTS.md @@ -32,11 +32,23 @@ _заполнить в конце спринта_ --- -## Спринт 2 · даты TBD +## Спринт 2 · 19 апр 2026 -**Цель:** _TBD_ +**Цель:** превратить одиночный чат в центр всех коммуникаций с клиникой — AI-помощник, врач, администратор. -### Идеи-кандидаты +**Итоги.** Закрыт. Чат стал списком из трёх диалогов (AI-помощник, врач, администратор), каждая карточка ведёт в отдельную конверсацию с разными UI-акцентами. Таббар автоматически скрывается в подэкранах `chat:`. + +### План +- [x] Экран `chat` — список диалогов: AI-помощник (featured), врач, администратор +- [x] Данные: три чата в `data.js` (kind, участники, сообщения, online, непрочитанные, время последнего сообщения) +- [x] AI-помощник: расширенный диалог с напоминаниями о лекарствах, чипсы с подсказками ответов +- [x] Чат с регистратурой: запросы справок, переносы приёмов, счета +- [x] Экран `chat:` — конверсация: разный аватар/статус/UI-акценты для AI, врача, оператора +- [x] Видеозвонок-кнопка только у врача; suggested replies только у AI +- [x] Переадресация: сурджен в «Восстановлении» → `chat:doctor-syndaev` +- [x] Tweaks: добавить три варианта чата в список экранов + +### Задел на Спринт 3 - [ ] Экран онбординга (первый запуск) - [ ] Пустые состояния для всех вкладок - [ ] Анимации переходов между экранами @@ -47,8 +59,25 @@ _заполнить в конце спринта_ - [ ] Форма обратной связи / отзыв о враче - [ ] Тёмная тема +### Итоги +_заполнить в конце спринта_ + +--- + +## Спринт 3 · 20 апр 2026 + +**Цель:** применить новую палитру из дизайн-системы (скрин от 20.04) и добавить её как 4-й вариант в Tweaks. + +Входные цвета: `#ffffff #fffde4 #f2fee6 #d4f6f8 #e9e4d4 #ffa39c #63bac3 #29aee3 #93908f` — sky-blue primary + coral accent + warm pastels. + ### План -- [ ] _выбрать и зафиксировать задачи на спринт_ +- [x] Расширить схему `ACCENT_OPTIONS`: добавить `accent`, `accentDark`, `accent50` (чтобы палитра меняла и красный акцент, а не только primary + warm) +- [x] Обновить `applyTheme` — устанавливать `--c-accent`, `--c-accent-dark`, `--c-accent-50` из палитры +- [x] Проставить accent-поля в существующих палитрах (тил/терра/марин) → сохранить текущий красный `#E04E44` +- [x] Добавить 4-ю палитру **Лагуна**: primary `#29AEE3`, accent `#FFA39C`, warm `#E9E4D4` +- [x] Пристроить три оставшихся цвета из скрина: `#f2fee6` → `success-50`, `#93908f` → `fg-4`, `#63bac3` → `primary-300` (с видимым применением в бордере Clinic Stats card на Home V2) +- [x] Добавить 5-ю палитру **Бриз** — вариант Лагуны с приглушённым primary `#63BAC3` (яркий `#29AEE3` переехал в p300, warm/accent/success/fg-4 наследуются от Лагуны) +- [ ] Визуальная проверка всех экранов в новой палитре: кнопки, чипы, CTA, прогресс восстановления, успех-галочка, таббар-бейджи ### Итоги _заполнить в конце спринта_ diff --git a/src/App.jsx b/src/App.jsx index 3aae517..5929e90 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -38,9 +38,11 @@ const DENSITY_OPTIONS = [ { id: 'compact', lb: 'Плотно' }, ]; const ACCENT_OPTIONS = [ - { id: 'teal', lb: 'Тил', primary: '#1F8F85', darker: '#166B63', dark: '#0F4A44', p50: '#E3F4F2', p100: '#C7E8E4', p200: '#9BD6CE', warm50: '#FDF8EE', warm100: '#F5EDDF' }, - { id: 'terra', lb: 'Терра', primary: '#C77A4C', darker: '#A65C33', dark: '#7F4426', p50: '#FBEFE4', p100: '#F5DDC9', p200: '#EBC19F', warm50: '#F4F7F3', warm100: '#E5ECE4' }, - { id: 'marine', lb: 'Марин', primary: '#3C6EA8', darker: '#23538B', dark: '#193C66', p50: '#E4EDF8', p100: '#C8DAEE', p200: '#9DBDDE', warm50: '#FBF6EE', warm100: '#F2E8D5' }, + { id: 'teal', lb: 'Тил', primary: '#1F8F85', darker: '#166B63', dark: '#0F4A44', p50: '#E3F4F2', p100: '#C7E8E4', p200: '#9BD6CE', p300: '#9ED8D1', warm50: '#FDF8EE', warm100: '#F5EDDF', accent: '#E04E44', accentDark: '#B63D35', accent50: '#FCF1F0', success50: '#E8F5EE', fg4: '#9AA7B4' }, + { id: 'terra', lb: 'Терра', primary: '#C77A4C', darker: '#A65C33', dark: '#7F4426', p50: '#FBEFE4', p100: '#F5DDC9', p200: '#EBC19F', p300: '#D9A07A', warm50: '#F4F7F3', warm100: '#E5ECE4', accent: '#E04E44', accentDark: '#B63D35', accent50: '#FCF1F0', success50: '#E8F5EE', fg4: '#9AA7B4' }, + { id: 'marine', lb: 'Марин', primary: '#3C6EA8', darker: '#23538B', dark: '#193C66', p50: '#E4EDF8', p100: '#C8DAEE', p200: '#9DBDDE', p300: '#7FA8D4', warm50: '#FBF6EE', warm100: '#F2E8D5', accent: '#E04E44', accentDark: '#B63D35', accent50: '#FCF1F0', success50: '#E8F5EE', fg4: '#9AA7B4' }, + { id: 'laguna', lb: 'Лагуна',primary: '#29AEE3', darker: '#1E8FBD', dark: '#155E7A', p50: '#EDF9FD', p100: '#D4F6F8', p200: '#9FDDEB', p300: '#63BAC3', warm50: '#FFFDF0', warm100: '#E9E4D4', accent: '#FFA39C', accentDark: '#E07B73', accent50: '#FFEDEA', success50: '#F2FEE6', fg4: '#93908F' }, + { id: 'briz', lb: 'Бриз', primary: '#63BAC3', darker: '#4A9DA6', dark: '#2F6670', p50: '#F0F9FB', p100: '#D8ECEF', p200: '#A3D4DB', p300: '#29AEE3', warm50: '#FFFDF0', warm100: '#E9E4D4', accent: '#FFA39C', accentDark: '#E07B73', accent50: '#FFEDEA', success50: '#F2FEE6', fg4: '#93908F' }, ]; const FONT_OPTIONS = [ { id: 'manrope', lb: 'Manrope', base: '"Manrope", system-ui, sans-serif', narrow: '"Oswald", sans-serif' }, @@ -64,7 +66,10 @@ const SCREEN_OPTIONS = [ { id: 'result:r2', lb: 'Эндоскопия носоглотки' }, { id: 'recovery', lb: 'Восстановление' }, { id: 'audiotest', lb: 'Тест слуха' }, - { id: 'chat', lb: 'Чат' }, + { id: 'chat', lb: 'Чаты · список' }, + { id: 'chat:ai', lb: 'Чат: AI-помощник' }, + { id: 'chat:doctor-syndaev', lb: 'Чат: Синдяев' }, + { id: 'chat:operator', lb: 'Чат: администратор' }, { id: 'profile', lb: 'Профиль' }, { id: 'qr', lb: 'QR' }, { id: 'telemed', lb: 'Телемед' }, @@ -92,6 +97,12 @@ function applyTheme(tw) { r.setProperty('--c-primary-200', a.p200); r.setProperty('--c-warm-50', a.warm50); r.setProperty('--c-warm-100', a.warm100); + r.setProperty('--c-accent', a.accent); + r.setProperty('--c-accent-dark', a.accentDark); + r.setProperty('--c-accent-50', a.accent50); + r.setProperty('--c-primary-300', a.p300); + r.setProperty('--c-success-50', a.success50); + r.setProperty('--c-fg-4', a.fg4); r.setProperty('--font-base', f.base); r.setProperty('--font-narrow', f.narrow); document.body.style.fontFamily = f.base; diff --git a/src/PhoneApp.jsx b/src/PhoneApp.jsx index 13bdd14..513be9d 100644 --- a/src/PhoneApp.jsx +++ b/src/PhoneApp.jsx @@ -10,9 +10,10 @@ import { ApptsTabScreen, ApptDetailScreen, ResultsScreen, ResultAudioScreen, ResultEndoscopyScreen, RecoveryScreen, AudioTestScreen, - ChatTabScreen, ProfileTabScreen, QRScreen, + ProfileTabScreen, QRScreen, TelemedScreen, MedcardScreen, NotificationsScreen, } from './screens/screens-misc.jsx'; +import { ChatsListScreen, ChatConversationScreen } from './screens/screens-chats.jsx'; import { ArticlesScreen, ArticleDetailScreen } from './screens/screens-articles.jsx'; import { HomeV2Screen, SearchScreen, ContactsScreen, PricesScreen } from './screens/screens-v2.jsx'; @@ -37,7 +38,9 @@ function renderScreen(screenId, nav, ctx) { case 'result': return ; case 'recovery': return ; case 'audiotest': return ; - case 'chat': return ; + case 'chat': return parts[1] + ? + : ; case 'profile': return ; case 'qr': return ; case 'telemed': return ; @@ -68,7 +71,8 @@ export function PhoneApp({ initialScreen, ctx }) { const current = stack[stack.length - 1]; const rootId = current.split(':')[0]; - const tabId = rootId === 'home-v2' ? 'home' : (TAB_IDS.includes(rootId) ? rootId : null); + const hasSubId = current.includes(':'); + const tabId = hasSubId ? null : (rootId === 'home-v2' ? 'home' : (TAB_IDS.includes(rootId) ? rootId : null)); const showTabBar = tabId !== null; const modalScreens = ['qr', 'telemed', 'booking-success', 'audiotest']; diff --git a/src/data.js b/src/data.js index 18d9faf..3f3cdc0 100644 --- a/src/data.js +++ b/src/data.js @@ -168,6 +168,64 @@ export const CLINIC_DATA = { ], }, ], + chats: [ + { + id: 'ai', + kind: 'ai', + name: 'Умный помощник', + subtitle: 'Бот клиники УГН · отвечает мгновенно', + icon: '✨', + pinned: true, + lastMessage: 'Отлично! Отметил приём ✓', + lastTime: 'Сейчас', + unread: 0, + online: true, + messages: [ + { from: 'ai', t: 'Добрый день, Анна! Я помощник клиники УГН. Могу подсказать с записью, напомнить о лекарствах, объяснить заключение врача или помочь с тестом слуха.', tm: '09:00' }, + { from: 'me', t: 'Когда следующий контроль?', tm: '09:12' }, + { from: 'ai', t: 'Следующий контрольный осмотр — на 10-й день после операции, это 22 апреля. К этому дню запланирована эндоскопия полости носа у Синдяева А.В.', tm: '09:12' }, + { from: 'ai', t: '⏰ Напоминание: приём Амоксиклава в 20:00 — через 2 часа', tm: '18:00' }, + { from: 'me', t: 'Принял', tm: '20:03' }, + { from: 'ai', t: 'Отлично! Отметил приём ✓ Следующая доза — завтра в 08:00. Осталось 3 дня курса.', tm: '20:03' }, + ], + suggestions: ['Что показала аудиограмма?', 'Можно ли в баню?', 'Перенести приём 21 апреля'], + }, + { + id: 'doctor-syndaev', + kind: 'doctor', + doctorId: 'syndaev', + lastMessage: 'Отлично, жду. Если что-то изменится — напишите.', + lastTime: '14:15', + unread: 2, + online: true, + messages: [ + { from: 'doc', t: 'Добрый день, Анна! Как самочувствие после операции?', tm: '14:02' }, + { from: 'me', t: 'Здравствуйте! В целом хорошо, немного саднит в носу по утрам.', tm: '14:08' }, + { from: 'doc', t: 'Это нормально на 6-й день. Продолжайте промывания Аква Марис 4 раза в день.', tm: '14:10' }, + { from: 'doc', t: 'Выходите на осмотр сегодня? Я свободен после 15:00.', tm: '14:11' }, + { from: 'me', t: 'Да, буду в 16:00 как запланировано.', tm: '14:14' }, + { from: 'doc', t: 'Отлично, жду. Если что-то изменится — напишите.', tm: '14:15' }, + ], + }, + { + id: 'operator', + kind: 'operator', + name: 'Администратор', + subtitle: 'Регистратура · Пн–Вс 9:00–21:00', + icon: '📞', + lastMessage: 'Справка отправлена на почту.', + lastTime: 'Вчера', + unread: 0, + online: false, + messages: [ + { from: 'op', t: 'Добрый день, Анна! Это регистратура Клиники УГН. Чем могу помочь?', tm: 'Вчера, 11:30' }, + { from: 'me', t: 'Здравствуйте. Нужна справка о прохождении аудиометрии для работы.', tm: 'Вчера, 11:45' }, + { from: 'op', t: 'Подготовим за час. Подъехать в клинику за оригиналом или отправить PDF на почту?', tm: 'Вчера, 11:48' }, + { from: 'me', t: 'На почту, пожалуйста.', tm: 'Вчера, 11:50' }, + { from: 'op', t: 'Готово. Справка отправлена на arazor72@gmail.com. Если нужен оригинал с печатью — забирайте на ресепшене в любое время.', tm: 'Вчера, 12:40' }, + ], + }, + ], recovery: { op: 'Септопластика', surgeon: 'syndaev', diff --git a/src/screens/screens-chats.jsx b/src/screens/screens-chats.jsx new file mode 100644 index 0000000..2522c09 --- /dev/null +++ b/src/screens/screens-chats.jsx @@ -0,0 +1,268 @@ +import React from 'react'; +import { I } from '../icons.jsx'; +import { CLINIC_DATA } from '../data.js'; +import { Avatar } from '../components.jsx'; + +// ───────────────────────────────────────────────────────────── +// Chats list — the main chat tab +// ───────────────────────────────────────────────────────────── +export function ChatsListScreen({ nav }) { + const { chats, doctors } = CLINIC_DATA; + const ai = chats.find(c => c.kind === 'ai'); + const rest = chats.filter(c => c.kind !== 'ai'); + + const subjectFor = (c) => { + if (c.kind === 'doctor') { + const d = doctors.find(x => x.id === c.doctorId); + return { + avatar: , + title: d.name.split(' ').slice(0, 2).join(' '), + subtitle: d.spec, + }; + } + const bg = c.kind === 'operator' ? 'var(--c-warm-100)' : 'var(--c-primary-100)'; + return { + avatar: ( +
{c.icon}
+ ), + title: c.name, + subtitle: c.subtitle, + }; + }; + + return ( +
+
+

Чаты

+
+ + +
+
+ + {/* AI — featured */} + {ai && ( +
+ +
+ )} + + {/* Other chats */} +
+
Все диалоги · {rest.length}
+
+ {rest.map((c, i) => { + const s = subjectFor(c); + return ( + + + {i < rest.length - 1 &&
} + + ); + })} +
+ + + +
+ +
+ Чаты с врачом доступны в течение 14 дней после приёма. Экстренные случаи — по телефону (342) 207-03-03. +
+
+
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Chat conversation +// ───────────────────────────────────────────────────────────── +export function ChatConversationScreen({ nav, chatId }) { + const chat = CLINIC_DATA.chats.find(c => c.id === chatId) || CLINIC_DATA.chats[0]; + const isAI = chat.kind === 'ai'; + const isOp = chat.kind === 'operator'; + const isDoc = chat.kind === 'doctor'; + const doc = isDoc ? CLINIC_DATA.doctors.find(d => d.id === chat.doctorId) : null; + + const title = isDoc ? doc.name.split(' ').slice(0, 2).join(' ') : chat.name; + const subtitle = isDoc + ? (chat.online ? 'Онлайн · отвечает 5 мин' : 'Был(а) в сети недавно') + : isAI + ? (chat.online ? 'Онлайн · отвечает мгновенно' : chat.subtitle) + : chat.subtitle; + + return ( +
+ {/* Header */} +
+ +
+ {isDoc && } + {!isDoc && ( +
{chat.icon}
+ )} + {chat.online && } +
+
+
+ {title} + {isAI && AI} +
+
+ {chat.online && } + {subtitle} +
+
+ {isDoc && ( + + )} + {isOp && ( + + )} +
+ + {/* Messages */} +
+ {chat.messages.map((m, i) => { + const mine = m.from === 'me'; + return ( +
+
{m.t}
+
{m.tm}
+
+ ); + })} +
+ + {/* Suggested replies (AI only) */} + {isAI && chat.suggestions && ( +
+ {chat.suggestions.map((s, i) => ( + + ))} +
+ )} + + {/* Input */} +
+
+ +
+ {isAI ? 'Спросите что-нибудь...' : 'Сообщение...'} +
+ +
+
+
+ ); +} diff --git a/src/screens/screens-misc.jsx b/src/screens/screens-misc.jsx index 042a50a..4981bd8 100644 --- a/src/screens/screens-misc.jsx +++ b/src/screens/screens-misc.jsx @@ -464,7 +464,7 @@ export function RecoveryScreen({ nav }) {
Ваш хирург
{surgeon.name.split(' ').slice(0,2).join(' ')}
- @@ -606,67 +606,6 @@ export function AudioTestScreen({ nav }) { ); } -export function ChatTabScreen() { - const msgs = [ - { from: 'doc', t: 'Добрый день, Анна! Как самочувствие после операции?', tm: '14:02' }, - { from: 'me', t: 'Здравствуйте! В целом хорошо, немного саднит в носу по утрам.', tm: '14:08' }, - { from: 'doc', t: 'Это нормально на 6-й день. Продолжайте промывания Аква Марис 4 раза в день.', tm: '14:10' }, - { from: 'doc', t: 'Выходите на осмотр сегодня? Я свободен после 15:00.', tm: '14:11' }, - { from: 'me', t: 'Да, буду в 16:00 как запланировано.', tm: '14:14' }, - { from: 'doc', t: 'Отлично, жду. Если что-то изменится — напишите.', tm: '14:15' }, - ]; - const doc = CLINIC_DATA.doctors.find(d => d.id === 'syndaev'); - return ( -
-
-

Чат

-
-
-
- -
-
{doc.name.split(' ').slice(0,2).join(' ')}
-
- - Онлайн · отвечает 5 мин -
-
- -
-
-
- {msgs.map((m,i)=>( -
-
{m.t}
-
{m.tm}
-
- ))} -
-
-
- -
Сообщение...
- -
-
-
- ); -} - export function ProfileTabScreen({ nav }) { const sections = [ { diff --git a/src/screens/screens-v2.jsx b/src/screens/screens-v2.jsx index 64f0601..fe9b7cc 100644 --- a/src/screens/screens-v2.jsx +++ b/src/screens/screens-v2.jsx @@ -142,7 +142,8 @@ export function HomeV2Screen({ nav }) { {/* Clinic stats */}
Клиника УГН