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>
This commit is contained in:
+33
-4
@@ -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, прогресс восстановления, успех-галочка, таббар-бейджи
|
||||
|
||||
### Итоги
|
||||
_заполнить в конце спринта_
|
||||
|
||||
+15
-4
@@ -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;
|
||||
|
||||
+7
-3
@@ -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
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
Reference in New Issue
Block a user