Browse Source

Complete Sprint 2 (chat hub) and Sprint 3 (color palettes)

Sprint 2 — chat hub:
- Chat tab becomes a list of three conversations: AI assistant
  (featured, gradient card), doctor Syndaev, and clinic administrator
- data.js: new `chats` array with kind, participants, message history,
  online/unread state, time of last message, AI suggestions
- screens-chats.jsx: ChatsListScreen and ChatConversationScreen with
  per-kind UI — AI gets suggestion chips + AI-badge, doctor gets
  video-call button, operator gets phone button
- Recovery surgeon chat button routes to chat:doctor-syndaev directly
- Tab bar auto-hides on pushed chat:<id> routes
- ChatTabScreen removed from screens-misc.jsx

Sprint 3 — color palettes:
- ACCENT_OPTIONS extended with accent/accentDark/accent50,
  p300/success50/fg4 so palette switches change the full theme
  (primary + warm + accent + muted + success)
- New palette "Лагуна" from the design-system screenshot: primary
  #29AEE3 (sky blue), accent #FFA39C (coral), warm #E9E4D4 (beige)
- New palette "Бриз": Лагуна variant with primary #63BAC3 (muted
  teal) and the bright sky blue #29AEE3 demoted to p300
- All 9 screenshot colors wired: #f2fee6→success-50, #93908f→fg-4,
  #63bac3→p300 (visible as border on Clinic Stats card in Home V2)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
main
parent
commit
6700e96476
  1. 37
      SPRINTS.md
  2. 19
      src/App.jsx
  3. 10
      src/PhoneApp.jsx
  4. 58
      src/data.js
  5. 268
      src/screens/screens-chats.jsx
  6. 63
      src/screens/screens-misc.jsx
  7. 3
      src/screens/screens-v2.jsx

37
SPRINTS.md

@ -32,11 +32,23 @@ _заполнить в конце спринта_
---
## Спринт 2 · даты TBD
## Спринт 2 · 19 апр 2026
**Цель:** _TBD_
**Цель:** превратить одиночный чат в центр всех коммуникаций с клиникой — AI-помощник, врач, администратор.
### Идеи-кандидаты
**Итоги.** Закрыт. Чат стал списком из трёх диалогов (AI-помощник, врач, администратор), каждая карточка ведёт в отдельную конверсацию с разными UI-акцентами. Таббар автоматически скрывается в подэкранах `chat:<id>`.
### План
- [x] Экран `chat` — список диалогов: AI-помощник (featured), врач, администратор
- [x] Данные: три чата в `data.js` (kind, участники, сообщения, online, непрочитанные, время последнего сообщения)
- [x] AI-помощник: расширенный диалог с напоминаниями о лекарствах, чипсы с подсказками ответов
- [x] Чат с регистратурой: запросы справок, переносы приёмов, счета
- [x] Экран `chat:<id>` — конверсация: разный аватар/статус/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, прогресс восстановления, успех-галочка, таббар-бейджи
### Итоги
_заполнить в конце спринта_

19
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;

10
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 <ResultEndoscopyScreen nav={nav} resultId={parts[1]} />;
case 'recovery': return <RecoveryScreen nav={nav} />;
case 'audiotest': return <AudioTestScreen nav={nav} />;
case 'chat': return <ChatTabScreen nav={nav} />;
case 'chat': return parts[1]
? <ChatConversationScreen nav={nav} chatId={parts[1]} />
: <ChatsListScreen nav={nav} />;
case 'profile': return <ProfileTabScreen nav={nav} ctx={ctx} />;
case 'qr': return <QRScreen nav={nav} />;
case 'telemed': return <TelemedScreen nav={nav} />;
@ -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'];

58
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',

268
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: <Avatar init={d.init} size={48} />,
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: (
<div style={{
width: 48, height: 48, borderRadius: 999, background: bg,
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 22,
}}>{c.icon}</div>
),
title: c.name,
subtitle: c.subtitle,
};
};
return (
<div style={{ paddingBottom: 100 }}>
<div style={{ padding: '12px 20px 12px' }}>
<h1 className="h-screen" style={{ marginBottom: 14 }}>Чаты</h1>
<div style={{
background: '#fff', borderRadius: 14, padding: '10px 14px',
display: 'flex', alignItems: 'center', gap: 10,
border: '1px solid var(--c-border)',
}}>
<I.search size={18} style={{ color: 'var(--c-fg-4)' }} />
<input placeholder="Найти в чатах..." style={{ flex: 1, border: 0, outline: 0, fontSize: 15, background: 'transparent' }} />
</div>
</div>
{/* AI — featured */}
{ai && (
<div style={{ padding: '0 16px 18px' }}>
<button onClick={() => nav.push('chat:' + ai.id)} className="press" style={{
width: '100%', padding: 16,
background: 'linear-gradient(135deg, var(--c-primary-darker), var(--c-primary-dark))',
color: '#fff', borderRadius: 20, textAlign: 'left',
display: 'flex', gap: 14, alignItems: 'center',
boxShadow: '0 10px 28px rgba(22,107,99,.22)',
}}>
<div style={{
width: 52, height: 52, borderRadius: 14,
background: 'rgba(255,255,255,0.18)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 26,
}}></div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3 }}>
<span style={{ fontSize: 15, fontWeight: 700 }}>{ai.name}</span>
<span style={{ padding: '2px 6px', borderRadius: 5, background: 'rgba(255,255,255,0.25)', fontSize: 9, fontWeight: 700, letterSpacing: .5 }}>AI</span>
</div>
<div style={{ fontSize: 13, opacity: .85, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{ai.lastMessage}
</div>
</div>
<I.chev size={18} style={{ color: 'rgba(255,255,255,0.8)', flexShrink: 0 }} />
</button>
</div>
)}
{/* Other chats */}
<div style={{ padding: '0 16px' }}>
<div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: .6, color: 'var(--c-fg-3)', padding: '0 4px 10px' }}>Все диалоги · {rest.length}</div>
<div className="card" style={{ padding: 0 }}>
{rest.map((c, i) => {
const s = subjectFor(c);
return (
<React.Fragment key={c.id}>
<button onClick={() => nav.push('chat:' + c.id)} className="press" style={{
width: '100%', padding: '14px 16px', display: 'flex', gap: 12, alignItems: 'center', textAlign: 'left',
}}>
<div style={{ position: 'relative', flexShrink: 0 }}>
{s.avatar}
{c.online && <span style={{
position: 'absolute', bottom: 1, right: 1,
width: 12, height: 12, borderRadius: 999, background: 'var(--c-success)',
border: '2px solid #fff',
}} />}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 10, marginBottom: 2 }}>
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--c-fg-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{s.title}</div>
<div className="sub" style={{ fontSize: 11, flexShrink: 0, color: c.unread > 0 ? 'var(--c-primary-darker)' : 'var(--c-fg-3)', fontWeight: c.unread > 0 ? 700 : 400 }}>{c.lastTime}</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
<div style={{ fontSize: 13, color: c.unread > 0 ? 'var(--c-fg-1)' : 'var(--c-fg-3)', fontWeight: c.unread > 0 ? 600 : 400, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1, minWidth: 0 }}>{c.lastMessage}</div>
{c.unread > 0 && (
<span style={{
background: 'var(--c-accent)', color: '#fff', fontSize: 11, fontWeight: 700,
padding: '2px 7px', borderRadius: 999, flexShrink: 0, minWidth: 20, textAlign: 'center',
}}>{c.unread}</span>
)}
</div>
</div>
</button>
{i < rest.length - 1 && <div style={{ height: 1, background: 'var(--c-divider)', marginLeft: 76 }} />}
</React.Fragment>
);
})}
</div>
<button onClick={() => nav.push('doctors')} className="btn-g" style={{
marginTop: 14, width: '100%', padding: 14, fontSize: 14,
}}>
<I.plus size={18} /> Новый чат с врачом
</button>
<div style={{ marginTop: 14, padding: 12, background: 'var(--c-primary-50)', borderRadius: 12, display: 'flex', gap: 10, alignItems: 'flex-start' }}>
<I.shield size={18} style={{ color: 'var(--c-primary-darker)', flexShrink: 0, marginTop: 2 }} />
<div className="sub" style={{ fontSize: 12, lineHeight: 1.5 }}>
Чаты с врачом доступны в течение 14 дней после приёма. Экстренные случаи по телефону (342) 207-03-03.
</div>
</div>
</div>
</div>
);
}
//
// 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 (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
{/* Header */}
<div style={{
padding: '12px 16px 10px', display: 'flex', gap: 10, alignItems: 'center',
background: 'var(--c-bg)', borderBottom: '1px solid var(--c-border)',
flexShrink: 0,
}}>
<button onClick={() => nav.pop()} className="press" style={{
width: 38, height: 38, borderRadius: 999, background: '#fff',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: 'var(--sh-sm)', border: '1px solid var(--c-border)', flexShrink: 0,
}}>
<I.chevL size={20} />
</button>
<div style={{ position: 'relative', flexShrink: 0 }}>
{isDoc && <Avatar init={doc.init} size={42} />}
{!isDoc && (
<div style={{
width: 42, height: 42, borderRadius: 999,
background: isAI
? 'linear-gradient(135deg, var(--c-primary-darker), var(--c-primary-dark))'
: 'var(--c-warm-100)',
color: isAI ? '#fff' : 'var(--c-warm-text)',
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20,
}}>{chat.icon}</div>
)}
{chat.online && <span style={{
position: 'absolute', bottom: 0, right: 0,
width: 11, height: 11, borderRadius: 999, background: 'var(--c-success)',
border: '2px solid var(--c-bg)',
}} />}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 15, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{title}</span>
{isAI && <span style={{ padding: '1px 6px', borderRadius: 5, background: 'var(--c-primary-100)', color: 'var(--c-primary-darker)', fontSize: 9, fontWeight: 700, letterSpacing: .5 }}>AI</span>}
</div>
<div className="sub" style={{ fontSize: 12, display: 'flex', alignItems: 'center', gap: 4 }}>
{chat.online && <span style={{ width: 6, height: 6, borderRadius: 999, background: 'var(--c-success)' }} />}
{subtitle}
</div>
</div>
{isDoc && (
<button onClick={() => nav.push('telemed')} className="btn-s" style={{ padding: 0, width: 38, height: 38, borderRadius: 999, flexShrink: 0 }}>
<I.video size={16} />
</button>
)}
{isOp && (
<button className="btn-s" style={{ padding: 0, width: 38, height: 38, borderRadius: 999, flexShrink: 0 }}>
<I.phone size={16} />
</button>
)}
</div>
{/* Messages */}
<div style={{ flex: 1, overflowY: 'auto', padding: '16px', display: 'flex', flexDirection: 'column', gap: 10 }}>
{chat.messages.map((m, i) => {
const mine = m.from === 'me';
return (
<div key={i} style={{ alignSelf: mine ? 'flex-end' : 'flex-start', maxWidth: '80%' }}>
<div style={{
background: mine
? 'var(--c-primary-darker)'
: isAI
? 'linear-gradient(135deg, #F2FAF9, #E3F4F2)'
: '#fff',
color: mine ? '#fff' : 'var(--c-fg-1)',
padding: '10px 14px', borderRadius: 16,
borderBottomRightRadius: mine ? 4 : 16,
borderBottomLeftRadius: mine ? 16 : 4,
fontSize: 14, lineHeight: 1.5,
boxShadow: mine ? 'none' : 'var(--sh-sm)',
border: !mine && isAI ? '1px solid var(--c-primary-200)' : 'none',
}}>{m.t}</div>
<div className="sub" style={{ fontSize: 11, marginTop: 3, textAlign: mine ? 'right' : 'left', padding: '0 4px' }}>{m.tm}</div>
</div>
);
})}
</div>
{/* Suggested replies (AI only) */}
{isAI && chat.suggestions && (
<div style={{ padding: '4px 16px 8px', display: 'flex', gap: 8, overflowX: 'auto', flexShrink: 0 }} className="noscroll">
{chat.suggestions.map((s, i) => (
<button key={i} style={{
flexShrink: 0, padding: '8px 14px', borderRadius: 999, fontSize: 13,
background: '#fff', color: 'var(--c-primary-darker)',
border: '1px solid var(--c-primary-200)', fontWeight: 600,
}}>{s}</button>
))}
</div>
)}
{/* Input */}
<div style={{ padding: '10px 16px 100px', borderTop: '1px solid var(--c-border)', background: '#fff', flexShrink: 0 }}>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<button style={{ width: 38, height: 38, borderRadius: 999, background: 'var(--c-bg)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<I.plus size={20} />
</button>
<div style={{
flex: 1, background: 'var(--c-bg)', borderRadius: 999,
padding: '10px 16px', fontSize: 14, color: 'var(--c-fg-4)',
}}>
{isAI ? 'Спросите что-нибудь...' : 'Сообщение...'}
</div>
<button style={{ width: 38, height: 38, borderRadius: 999, background: 'var(--c-primary-darker)', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<I.mic size={20} />
</button>
</div>
</div>
</div>
);
}

63
src/screens/screens-misc.jsx

@ -464,7 +464,7 @@ export function RecoveryScreen({ nav }) {
<div className="sub" style={{ fontSize: 11 }}>Ваш хирург</div>
<div style={{ fontSize: 14, fontWeight: 700 }}>{surgeon.name.split(' ').slice(0,2).join(' ')}</div>
</div>
<button onClick={() => nav.push('chat')} className="btn-s" style={{ padding: '8px 12px' }}>
<button onClick={() => nav.push('chat:doctor-syndaev')} className="btn-s" style={{ padding: '8px 12px' }}>
<I.chat size={15} /> Чат
</button>
</div>
@ -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 (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: '12px 20px 12px' }}>
<h1 className="h-screen">Чат</h1>
</div>
<div style={{ padding: '0 16px 12px' }}>
<div className="card" style={{ display: 'flex', gap: 12, alignItems: 'center', padding: 12, background: 'var(--c-primary-50)' }}>
<Avatar init={doc.init} size={44} />
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 700 }}>{doc.name.split(' ').slice(0,2).join(' ')}</div>
<div className="sub" style={{ fontSize: 12, display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{ width: 6, height: 6, borderRadius: 999, background: 'var(--c-success)' }} />
Онлайн · отвечает 5 мин
</div>
</div>
<button className="btn-s" style={{ padding: 10, borderRadius: 999 }}>
<I.video size={16} />
</button>
</div>
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '8px 16px', display: 'flex', flexDirection: 'column', gap: 8 }}>
{msgs.map((m,i)=>(
<div key={i} style={{ alignSelf: m.from === 'me' ? 'flex-end' : 'flex-start', maxWidth: '78%' }}>
<div style={{
background: m.from === 'me' ? 'var(--c-primary-darker)' : '#fff',
color: m.from === 'me' ? '#fff' : 'var(--c-fg-1)',
padding: '10px 14px', borderRadius: 16,
borderBottomRightRadius: m.from === 'me' ? 4 : 16,
borderBottomLeftRadius: m.from === 'me' ? 16 : 4,
fontSize: 14, lineHeight: 1.45,
boxShadow: m.from === 'me' ? 'none' : 'var(--sh-sm)',
}}>{m.t}</div>
<div className="sub" style={{ fontSize: 11, marginTop: 3, textAlign: m.from === 'me' ? 'right' : 'left', padding: '0 4px' }}>{m.tm}</div>
</div>
))}
</div>
<div style={{ padding: '10px 16px 100px', borderTop: '1px solid var(--c-border)', background: '#fff' }}>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<button style={{ width: 38, height: 38, borderRadius: 999, background: 'var(--c-bg)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<I.plus size={20} />
</button>
<div style={{ flex: 1, background: 'var(--c-bg)', borderRadius: 999, padding: '10px 16px', fontSize: 14, color: 'var(--c-fg-4)' }}>Сообщение...</div>
<button style={{ width: 38, height: 38, borderRadius: 999, background: 'var(--c-primary-darker)', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<I.mic size={20} />
</button>
</div>
</div>
</div>
);
}
export function ProfileTabScreen({ nav }) {
const sections = [
{

3
src/screens/screens-v2.jsx

@ -142,7 +142,8 @@ export function HomeV2Screen({ nav }) {
{/* Clinic stats */}
<div style={{ padding: '0 20px 16px' }}>
<div className="card" style={{
padding: 18, background: 'linear-gradient(135deg, var(--c-primary-100), var(--c-warm-100))', border: 0,
padding: 18, background: 'linear-gradient(135deg, var(--c-primary-100), var(--c-warm-100))',
border: '1.5px solid var(--c-primary-300)',
}}>
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 12 }}>Клиника УГН</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 10 }}>

Loading…
Cancel
Save