Compare commits

..

No commits in common. "main" and "ca12ce8ad47aaaea0fbc1d5096302768a32e2a26" have entirely different histories.

36 changed files with 886 additions and 4286 deletions

33
.gitignore vendored
View File

@ -1,33 +0,0 @@
# secrets
.env
.env.local
.jwt-secret
credentials*.json
secrets.json
*.pem
*.key
id_rsa*
id_ed25519*
# build artifacts
node_modules/
.venv/
venv/
__pycache__/
*.pyc
.next/
dist/
build/
out/
target/
# data
*.sqlite*
*.db
*.log
# OS / IDE
.DS_Store
.claude/
.vscode/
.idea/

View File

@ -1 +0,0 @@
/Users/telecom/Dev/_hack22may/GITEA.md

321
PRD.md
View File

@ -1,321 +0,0 @@
# PRD — Семейный Органайзер (Family Life Assistant)
## Версия
v1.0
## Автор
Зауреш Абдиназарова
## Дата
22 мая 2026
---
# 1. Обзор продукта
## Название приложения
**Family Life Assistant**
(рабочее название: «Семейный Органайзер»)
## Идея
Мобильное приложение, которое помогает управлять семейными задачами, документами, напоминаниями и бытовыми процессами в одном месте.
Приложение должно стать «умной семейной записной книжкой», которая:
- напоминает о важных делах;
- хранит семейную информацию;
- помогает ничего не забывать;
- снижает эмоциональную нагрузку и хаос.
---
# 2. Цель продукта
Помочь семьям:
- не забывать важные дела;
- организовать семейную жизнь;
- сократить стресс;
- централизовать документы, события и задачи.
---
# 3. Целевая аудитория
## Основная аудитория
- Мамы с детьми
- Семьи с высокой бытовой нагрузкой
- Работающие родители
- Люди, ведущие домашние дела
## Дополнительная аудитория
- Пожилые родители
- Семейные пары
- Одинокие родители
---
# 4. Основные проблемы пользователей
## Пользовательские боли
- Забываются записи к врачу
- Теряются школьные документы
- Нет напоминаний о важных датах
- Много информации в разных чатах
- Сложно вести семейные финансы
- Перегруженность бытовыми задачами
- Эмоциональная усталость
---
# 5. Основные функции MVP
## 5.1 Напоминания
### Возможности:
- Уведомления о:
- записи к врачу
- лекарствах
- родительских собраниях
- оплатах
- днях рождения
- кружках и секциях
- дедлайнах документов
### Типы уведомлений:
- Push-уведомления
- Повторяющиеся напоминания
- Срочные уведомления
- Голосовые напоминания
---
## 5.2 Семейный календарь
### Возможности:
- Общий календарь семьи
- Цвета для каждого члена семьи
- События по категориям:
- медицина
- школа
- работа
- финансы
- личное
---
## 5.3 Медицинский раздел
### Возможности:
- История посещений врачей
- Напоминания о лекарствах
- Хранение анализов
- Вакцинация детей
- Карточка здоровья
### Дополнительно:
- Напоминание:
- «купить лекарства»
- «закончится препарат через 3 дня»
---
## 5.4 Школа и детский сад
### Возможности:
- Шаблоны заявлений
- Напоминания:
- принести форму
- оплатить кружок
- подготовить поделку
- Домашние задания
- Расписание уроков
### AI-функции:
- Автогенерация заявлений:
- «по болезни»
- «отпуск»
- «освобождение от физкультуры»
---
## 5.5 Дни рождения и праздники
### Возможности:
- Напоминания заранее
- Идеи подарков
- Список гостей
- Напоминания купить подарок
---
## 5.6 Семейные документы
### Хранение:
- Свидетельства
- Полисы
- Справки
- Фото документов
### Функции:
- Быстрый поиск
- OCR распознавание текста
- Напоминания об окончании срока документов
---
## 5.7 Заметки и списки
### Возможности:
- Список покупок
- Семейные заметки
- Голосовые заметки
- Чек-листы
---
## 5.8 Финансы семьи
### Возможности:
- Напоминания об оплатах
- Коммунальные услуги
- Интернет
- Школа
- Кредиты
### Будущее:
- Аналитика расходов
- Семейный бюджет
---
# 6. Умные AI-функции
## AI-помощник
### Примеры:
- «Напомни записать ребенка к стоматологу»
- «Создай заявление в школу»
- «Что купить на неделю?»
- «Составь список дел на завтра»
---
## Умные сценарии
### Пример:
Если у ребенка температура:
- напомнить дать лекарство;
- предложить записаться к врачу;
- напомнить взять справку в школу.
---
# 7. Дополнительные функции для будущих версий
## V2
- Семейный чат
- Общие задачи
- Геолокация детей
- Интеграция с Google Calendar
- Telegram-бот
- WhatsApp уведомления
## V3
- AI-анализ семейной нагрузки
- Эмоциональный трекер
- Семейный психолог AI
- Автопланирование дел
---
# 8. Пользовательские роли
## Мама
Полный доступ
## Папа
Ограниченный или полный доступ
## Дети
Только задачи и расписание
## Бабушка/дедушка
Просмотр событий
---
# 9. Платформы
## MVP
- Android
## Далее
- iOS
- Web версия
---
# 10. Технологии
## Frontend
- Flutter
## Backend
- Firebase
или
- Node.js + PostgreSQL
## Notifications
- Firebase Cloud Messaging
## AI
- OpenAI API
---
# 11. Монетизация
## Бесплатно
- Базовые напоминания
- Календарь
- Заметки
## Premium
- AI-функции
- Семейное облако
- Расширенное хранение
- Голосовой помощник
---
# 12. UX/UI стиль
## Стиль
- Теплый
- Спокойный
- Семейный
- Минималистичный
## Цвета
- Светло-зеленый
- Бежевый
- Пастельный голубой
---
# 13. Главный экран
## Виджеты:
- Сегодняшние дела
- Срочные напоминания
- Календарь
- Дни рождения
- Заметки
- Покупки
---
# 14. Метрики успеха
## KPI
- Daily Active Users
- Количество созданных напоминаний
- Retention 30 days
- Количество семейных аккаунтов
---
# 15. Главная ценность продукта
«Приложение, которое помнит о семье вместо тебя.»
---

View File

@ -1,26 +0,0 @@
# zauresh
Привет, zauresh!
Это твоя личная папка в среде telecom.quest.
- VS Code в браузере: https://code.telecom.quest/?folder=/Users/telecom/dev/_hack22may/zauresh/
- Бэкап на Gitea: https://git.telecom.quest/kt-innovators/zauresh
## Action Plan (как пользоваться)
В этой папке уже лежат три стартовых файла:
- `PRD.md` — здесь ты простыми словами описываешь, что хочешь автоматизировать
- `GITEA.md` — справочник по системе Gitea (для Claude, не для тебя)
- `TRD.md` — пустой; Claude заполнит его после того, как ты напишешь PRD
**Шаг 1.** Открой `PRD.md` и напиши свою идею.
**Шаг 2.** Открой Claude (Desktop или web). Дай ему `PRD.md` + `GITEA.md` и попроси: «Сделай TRD.md на основе PRD и GITEA».
**Шаг 3.** Сохрани результат в `TRD.md`.
**Шаг 4.** Запусти Claude Code в этой папке. Скажи: «Реализуй TRD.md». Получишь рабочий MVP.
## Как сохранять работу
В VS Code открой панель "Source Control" (слева), напиши краткое описание, нажми галочку → "Commit and Push".
Изменения сохранятся в Gitea и будут доступны при следующем входе.
В этой папке уже инициализирован git и подключен remote `origin` → Gitea.

752
TRD.md
View File

@ -1,752 +0,0 @@
# TRD — Family Life Assistant (Семейный Органайзер)
## Версия
v1.0
## Автор
Зауреш Абдиназарова
## Дата
22 мая 2026
## Связанный документ
[PRD.md](PRD.md) v1.0
---
# 1. Обзор
Технические требования и архитектура для MVP мобильного приложения **Family Life Assistant**. Документ описывает технологический стек, архитектуру системы, модели данных, API, безопасность, инфраструктуру и нефункциональные требования.
## Цели TRD
- Зафиксировать архитектурные решения MVP
- Определить контракты между клиентом и сервером
- Описать модели данных и схемы хранения
- Задать требования к безопасности, производительности и масштабируемости
---
# 2. Архитектура системы
## 2.1 Высокоуровневая схема
```
┌────────────────────────┐ ┌──────────────────────────┐
│ Flutter App (Android) │◀──────▶│ Firebase / Backend API │
└────────────────────────┘ └─────────────┬────────────┘
│ │
│ FCM Push │
▼ ▼
┌────────────────────────┐ ┌──────────────────────────┐
│ Firebase Cloud Msg │ │ Firestore / PostgreSQL │
└────────────────────────┘ └──────────────────────────┘
│
▼
┌──────────────────────────┐
│ Firebase Storage / S3 │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ OpenAI API (AI features)│
└──────────────────────────┘
```
## 2.2 Архитектурный паттерн
- Клиент: **MVVM + Clean Architecture** (Flutter)
- Сервер: **BaaS-first** (Firebase) с возможностью миграции на собственный backend (Node.js + PostgreSQL) для V2
- Слои клиента:
- `presentation` (UI, виджеты, экраны)
- `domain` (use-cases, бизнес-сущности)
- `data` (репозитории, источники данных, маппинг DTO ↔ entity)
## 2.3 Принципы
- Offline-first для заметок, списков, документов (локальный кэш через Hive/Isar)
- Eventual consistency через Firestore listeners
- Sync на фоне при появлении сети
- Все AI-вызовы проксируются через backend для защиты ключей OpenAI
---
# 3. Технологический стек
## 3.1 Frontend
| Компонент | Технология |
|--------------------|-----------------------------|
| Framework | Flutter 3.22+ |
| Язык | Dart 3.4+ |
| State management | Riverpod 2.x |
| Локальное хранение | Isar / Hive |
| HTTP клиент | Dio |
| DI | get_it / Riverpod providers |
| Навигация | go_router |
| Уведомления | firebase_messaging |
| Push (локальные) | flutter_local_notifications |
## 3.2 Backend (MVP — Firebase)
| Компонент | Технология |
|-----------------|---------------------------|
| Auth | Firebase Auth |
| База данных | Cloud Firestore |
| Файлы | Firebase Storage |
| Функции | Cloud Functions (Node.js) |
| Push | Firebase Cloud Messaging |
| Аналитика | Firebase Analytics |
| Crash reporting | Firebase Crashlytics |
| Remote Config | Firebase Remote Config |
## 3.3 Backend (Альтернатива V2 — Self-hosted)
| Компонент | Технология |
|--------------|------------------------|
| Runtime | Node.js 20 LTS |
| Framework | NestJS |
| Database | PostgreSQL 16 |
| ORM | Prisma |
| Cache/Queue | Redis |
| Object store | S3-compatible (MinIO) |
| Auth | JWT + refresh tokens |
## 3.4 AI слой
- OpenAI API (gpt-4o-mini для текстов, whisper для голоса)
- Прокси через Cloud Function `aiProxy` для контроля квот, кэширования, фильтрации PII
- Возможный fallback: Anthropic Claude API
## 3.5 OCR
- Google ML Kit Text Recognition (on-device) для документов
- Cloud Vision API как fallback при низкой уверенности
## 3.6 DevOps
| Компонент | Технология |
|-------------|----------------------------------|
| CI/CD | GitHub Actions |
| Дистрибуция | Firebase App Distribution / Play |
| Мониторинг | Firebase + Sentry |
| Логи | Cloud Logging |
---
# 4. Модели данных
## 4.1 Основные сущности
### User
```json
{
"uid": "string",
"email": "string",
"phone": "string",
"displayName": "string",
"photoUrl": "string",
"familyId": "string",
"role": "owner | parent | child | grandparent",
"color": "#HEX",
"locale": "ru | en | kk",
"createdAt": "timestamp",
"updatedAt": "timestamp"
}
```
### Family
```json
{
"id": "string",
"name": "string",
"ownerUid": "string",
"memberUids": ["string"],
"plan": "free | premium",
"createdAt": "timestamp"
}
```
### Reminder
```json
{
"id": "string",
"familyId": "string",
"createdBy": "uid",
"assigneeUid": "uid",
"title": "string",
"description": "string",
"category": "medical | school | finance | birthday | personal | other",
"dueAt": "timestamp",
"repeat": {
"type": "none | daily | weekly | monthly | yearly | custom",
"interval": "int",
"until": "timestamp"
},
"priority": "low | normal | urgent",
"notificationChannels": ["push", "voice"],
"status": "pending | done | snoozed | cancelled",
"createdAt": "timestamp"
}
```
### CalendarEvent
```json
{
"id": "string",
"familyId": "string",
"title": "string",
"category": "medical | school | work | finance | personal",
"startAt": "timestamp",
"endAt": "timestamp",
"allDay": "bool",
"participants": ["uid"],
"location": "string",
"color": "#HEX",
"linkedReminderId": "string?",
"createdAt": "timestamp"
}
```
### MedicalRecord
```json
{
"id": "string",
"familyId": "string",
"patientUid": "uid",
"type": "visit | analysis | vaccination | prescription",
"title": "string",
"doctor": "string",
"clinic": "string",
"date": "timestamp",
"attachments": ["storagePath"],
"notes": "string"
}
```
### Medication
```json
{
"id": "string",
"familyId": "string",
"patientUid": "uid",
"name": "string",
"dosage": "string",
"schedule": [{"time": "HH:mm", "days": ["mon","tue"]}],
"stockCount": "int",
"lowStockThreshold": "int",
"endDate": "timestamp"
}
```
### Document
```json
{
"id": "string",
"familyId": "string",
"ownerUid": "uid",
"title": "string",
"category": "id | insurance | certificate | school | medical | other",
"storagePath": "string",
"thumbnailPath": "string",
"ocrText": "string",
"expiresAt": "timestamp?",
"tags": ["string"],
"uploadedAt": "timestamp"
}
```
### Note / ShoppingList
```json
{
"id": "string",
"familyId": "string",
"type": "note | checklist | shopping | voice",
"title": "string",
"content": "string",
"items": [{"text": "string", "done": "bool"}],
"audioPath": "string?",
"sharedWith": ["uid"],
"createdAt": "timestamp"
}
```
### FinancePayment
```json
{
"id": "string",
"familyId": "string",
"title": "string",
"category": "utilities | internet | school | loan | subscription | other",
"amount": "decimal",
"currency": "KZT | RUB | USD",
"dueDate": "timestamp",
"recurrence": "none | monthly | yearly",
"status": "pending | paid | overdue"
}
```
### Birthday
```json
{
"id": "string",
"familyId": "string",
"personName": "string",
"relation": "string",
"date": "MM-DD",
"yearBorn": "int?",
"giftIdeas": ["string"],
"reminderDaysBefore": [1, 7, 30]
}
```
## 4.2 Структура Firestore
```
/families/{familyId}
/members/{uid}
/reminders/{reminderId}
/events/{eventId}
/medical/{recordId}
/medications/{medId}
/documents/{docId}
/notes/{noteId}
/payments/{paymentId}
/birthdays/{birthdayId}
/audit/{logId}
/users/{uid}
/invites/{inviteId}
```
## 4.3 Индексы (Firestore)
- `reminders`: composite (`familyId`, `status`, `dueAt`)
- `events`: composite (`familyId`, `startAt`)
- `payments`: composite (`familyId`, `status`, `dueDate`)
- `documents`: composite (`familyId`, `category`, `expiresAt`)
---
# 5. API контракты
## 5.1 Cloud Functions (REST/Callable)
| Endpoint | Метод | Назначение |
|---------------------------|-------|------------------------------------------------|
| `/auth/registerFamily` | POST | Создать семью при регистрации |
| `/auth/invite` | POST | Пригласить члена семьи (генерация ссылки) |
| `/auth/acceptInvite` | POST | Принять приглашение |
| `/ai/chat` | POST | Общий AI-помощник |
| `/ai/generateApplication` | POST | Генерация школьного заявления |
| `/ai/parseReminder` | POST | NLU: текст → структурированное напоминание |
| `/ai/weeklyPlan` | POST | Составление плана на неделю |
| `/ocr/document` | POST | OCR документа, возврат текста + метаданных |
| `/notifications/schedule` | POST | Регистрация серверного напоминания |
| `/notifications/test` | POST | Тестовый push |
## 5.2 Пример запроса AI
```http
POST /ai/parseReminder
Authorization: Bearer <Firebase ID Token>
Content-Type: application/json
{
"text": "Напомни записать Алину к стоматологу в пятницу в 15:00",
"familyId": "fam_123",
"timezone": "Asia/Almaty"
}
```
Ответ:
```json
{
"reminder": {
"title": "Записать Алину к стоматологу",
"category": "medical",
"assigneeUid": "uid_mom",
"dueAt": "2026-05-29T15:00:00+05:00",
"priority": "normal"
},
"confidence": 0.92
}
```
## 5.3 Realtime (Firestore listeners)
- Подписка на `/families/{familyId}/reminders` с фильтром `status == pending`
- Подписка на `/families/{familyId}/events` для календаря
- Offline persistence включён (`PersistenceSettings`)
---
# 6. Уведомления
## 6.1 Каналы
- **Push (FCM)** — основной канал
- **Local notifications** — для оффлайн расписаний
- **Voice reminders** — TTS через `flutter_tts` + предзапись аудио
- **Email** (V2) — для критичных платежей
## 6.2 Логика планирования
- Локальные напоминания планируются на клиенте через `flutter_local_notifications`
- Серверные (для всех членов семьи) — через Cloud Scheduler + Firestore-триггер
- Дедупликация по `reminderId`
## 6.3 Приоритеты
| Priority | Behavior |
|----------|-----------------------------------------|
| low | Тихое уведомление |
| normal | Звук + вибрация |
| urgent | Полноэкранный alert, повтор каждые 5мин |
---
# 7. Безопасность
## 7.1 Аутентификация
- Firebase Auth: email/password, phone OTP, Google Sign-In
- ID Token обязателен для всех Cloud Functions
- Refresh через Firebase SDK автоматически
## 7.2 Авторизация (Firestore Security Rules)
```js
match /families/{familyId}/{document=**} {
allow read, write: if request.auth != null
&& request.auth.uid in resource.data.memberUids;
}
match /families/{familyId}/medical/{doc} {
allow read: if isFamilyMember(familyId)
&& (hasRole('owner') || hasRole('parent'));
allow write: if hasRole('owner') || hasRole('parent');
}
```
## 7.3 Роли и доступы
| Роль | Reminders | Events | Medical | Documents | Finance |
|-------------|-----------|--------|---------|-----------|---------|
| owner | RW | RW | RW | RW | RW |
| parent | RW | RW | RW | RW | RW |
| child | R (свои) | R | — | — | — |
| grandparent | R | R | — | R | — |
## 7.4 Хранение чувствительных данных
- Медицинские документы шифруются на стороне клиента (AES-256-GCM) перед загрузкой
- Ключи шифрования хранятся в Android Keystore
- PII не передаётся в OpenAI без явного согласия (флаг `aiConsent`)
- Логи AI-запросов хранятся 30 дней, затем удаляются
## 7.5 Compliance
- GDPR-ready: экспорт данных, удаление аккаунта
- Согласие на обработку данных при регистрации
- Возрастные ограничения: дети младше 13 лет — только через родителя
---
# 8. Производительность
## 8.1 Целевые метрики
| Метрика | Цель |
|-------------------------------|-------------|
| Холодный старт приложения | < 2.5 сек |
| Открытие экрана | < 300 мс |
| Sync календаря на 30 дней | < 1 сек |
| Загрузка документа (1 МБ) | < 3 сек |
| AI-ответ (`parseReminder`) | < 2 сек p95 |
| Доставка push-уведомления | < 5 сек p95 |
## 8.2 Оптимизации
- Lazy loading списков (пагинация по 20 элементов)
- Кэширование thumbnails документов
- Дебаунс поиска (300 мс)
- Изображения сжимаются до 1080p перед загрузкой
---
# 9. Offline и синхронизация
- Firestore offline persistence: включён
- Очередь исходящих изменений в Isar при отсутствии сети
- Конфликты разрешаются по `updatedAt` (last-write-wins)
- Для документов: загрузка возобновляется через `resumable upload`
---
# 10. Локализация
- Языки MVP: **ru, kk** (казахский), **en**
- Формат через `flutter_intl` + ARB-файлы
- Таймзоны: хранение в UTC, отображение по `Asia/Almaty` (по умолчанию)
- Форматы дат: локализованные через `intl` пакет
---
# 11. Инфраструктура и деплой
## 11.1 Окружения
| Env | Назначение | Firebase project |
|---------|--------------------|------------------------|
| dev | Локальная разработка | `fla-dev` |
| staging | QA + бета-тест | `fla-staging` |
| prod | Production | `fla-prod` |
## 11.2 CI/CD
- GitHub Actions workflows:
- `flutter-ci.yml` — lint, test, build APK на каждый PR
- `deploy-functions.yml` — деплой Cloud Functions при merge в `main`
- `release-android.yml` — сборка release APK + загрузка в Firebase App Distribution
## 11.3 Версионирование
- SemVer: `MAJOR.MINOR.PATCH`
- Build number: автоинкремент из GitHub Actions
---
# 12. Тестирование
## 12.1 Уровни
| Уровень | Инструмент | Покрытие цель |
|---------------|-------------------------------|---------------|
| Unit | `flutter_test` | ≥ 70% |
| Widget | `flutter_test` | ключевые UI |
| Golden | `flutter_test` + `alchemist` | per-screen × per-locale |
| Integration | `integration_test` + Patrol | 5 сценариев |
| E2E на устройствах | Firebase Test Lab | Robo + smoke |
| Cloud Funcs | Jest + Firebase Emulator | ≥ 80% |
## 12.2 Ключевые E2E сценарии
1. Регистрация → создание семьи → приглашение второго пользователя
2. Создание напоминания через AI → получение push
3. Загрузка документа → OCR → поиск по тексту
4. Создание события в календаре → отображение у второго члена семьи
5. Переключение локали (ru/kk/en) на лету без потери состояния
## 12.3 Android API levels
| Параметр | Значение | Обоснование |
|-----------------|--------------------|-----------------------------------------------------|
| `minSdkVersion` | **31** (Android 12)| Решение product owner. ~75% покрытия в KZ/RU |
| `targetSdkVersion` | **35** (Android 15) | Требование Google Play 2026 |
| `compileSdkVersion` | **35** | Соответствие target |
### Технические следствия minSdk 31
- Splash Screen API — нативный
- Material You (dynamic colors) доступен
- Foreground service types обязательны (`mediaPlayback`, `dataSync`)
- Notification trampolines запрещены — FCM handlers стартуют Activity напрямую
- `SCHEDULE_EXACT_ALARM` permission для точных напоминаний
- Approximate location + runtime Bluetooth permissions
### Риск
Аудитория «бабушки/дедушки» из PRD §3 часто на Android 10-11. При жалобах — рассмотреть снижение `minSdk` до 28.
## 12.4 Firebase Test Lab
### Что запускаем
| Тип теста | Триггер | Девайсов | Длительность |
|-------------------|---------------------|----------|--------------|
| Robo (smoke) | PR в `main` | 3 | ~5 мин |
| Instrumentation | Nightly cron | 8 | ~15 мин |
| Full matrix | Pre-release tag | 10+ | ~30 мин |
### Тестовая матрица устройств
| Бренд | Модель | API | Локаль | Назначение |
|---------|------------------|-----|--------|-----------------------------|
| Samsung | Galaxy A52 | 31 | ru-RU | минимум API, массовый KZ/RU |
| Samsung | Galaxy S23 | 34 | kk-KZ | флагман, проверка kk |
| Xiaomi | Redmi Note 11 | 31 | ru-RU | MIUI специфика |
| Xiaomi | Redmi Note 13 | 34 | kk-KZ | актуальная Xiaomi |
| Google | Pixel 7 | 34 | en-US | чистый Android, en |
| Google | Pixel 8 | 35 | ru-RU | latest target SDK |
| Google | Pixel Tablet | 34 | ru-RU | tablet layout |
### Конфиг GitHub Actions
```yaml
# .github/workflows/test-lab.yml
name: Firebase Test Lab
on:
pull_request:
branches: [main]
schedule:
- cron: '0 2 * * *' # nightly 02:00 UTC
jobs:
test-lab:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.22.x'
- run: flutter build apk --debug
- run: |
cd android
./gradlew app:assembleAndroidTest -PtestBuildType=debug
- uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: Run Test Lab
run: |
gcloud firebase test android run \
--type instrumentation \
--app build/app/outputs/apk/debug/app-debug.apk \
--test android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
--device model=a52,version=31,locale=ru,orientation=portrait \
--device model=husky,version=34,locale=en,orientation=portrait \
--device model=tangorpro,version=34,locale=kk,orientation=landscape \
--timeout 15m \
--use-orchestrator \
--num-flaky-test-attempts 1
```
### Бюджет Test Lab
- Free tier: 10 virtual + 5 physical тестов/день
- MVP оценка: **$50-100/мес** при PR + nightly
- Лимит установить через GCP budget alerts
## 12.5 Локализация (ru / kk / en)
### Что проверяется per locale
| Проверка | Инструмент |
|---------------------------|--------------------------------------------|
| Все строки переведены | `intl_utils:generate` + lint missing keys |
| Overflow длинных текстов | Widget tests с `TextOverflow.fail` |
| Форматы дат/чисел/валют | `intl` `DateFormat`, `NumberFormat` |
| Плюрализация (kk специфика) | ICU MessageFormat в ARB |
| Покрытие шрифта | проверка глифов ә, ғ, қ, ң, ө, ұ, ү, і |
| Системный locale switch | runtime через `Localizations.override` |
| AI ответы на языке user | E2E тест `/ai/chat` с разными locale |
### Golden tests per locale
```dart
// test/golden/home_screen_test.dart
import 'package:alchemist/alchemist.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
goldenTest(
'home screen across locales',
fileName: 'home_screen',
builder: () => GoldenTestGroup(
children: [
for (final locale in [Locale('ru'), Locale('kk'), Locale('en')])
GoldenTestScenario(
name: locale.languageCode,
child: MaterialApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomeScreen(),
),
),
],
),
);
}
```
### ARB структура
```
lib/l10n/
app_en.arb # source of truth
app_ru.arb
app_kk.arb # казахский — носитель ревьюит
```
CI проверка отсутствующих ключей:
```bash
flutter pub run intl_utils:generate --verify
```
### Test Lab локали
`--device model=X,version=Y,locale=ru` — Test Lab выставляет системный язык, проверяется правильность реакции приложения на смену locale на лету и при старте.
---
# 13. Мониторинг и аналитика
## 13.1 Метрики продукта
- DAU / MAU
- Retention D1, D7, D30
- Количество напоминаний на семью
- AI-запросов в день
- Conversion free → premium
## 13.2 Технические метрики
- Crash-free users (цель > 99.5%)
- API latency (p50, p95, p99)
- FCM delivery rate
- OpenAI cost per user
## 13.3 Инструменты
- Firebase Analytics — продуктовые события
- Crashlytics — крэши
- Sentry — JS ошибки в Cloud Functions
- Cloud Monitoring — инфраструктура
---
# 14. Монетизация (техническая реализация)
- Подписки через **Google Play Billing**
- Валидация чеков на сервере через Cloud Function `verifyPurchase`
- Хранение статуса подписки в `/users/{uid}.subscription`
- Feature flags через Firebase Remote Config (`premiumFeaturesEnabled`)
---
# 15. Безопасность AI
- Все промпты к OpenAI проходят через серверный прокси
- System prompt с ограничениями (никаких медицинских диагнозов, юридических консультаций)
- Rate limit: 50 запросов/день для free, 1000/день для premium
- Логирование запросов без сохранения content (только метрики)
---
# 16. Roadmap технических задач MVP
| Спринт | Задачи |
|--------|-----------------------------------------------------------------------|
| 1 | Setup Flutter проекта, Firebase init, Auth flow |
| 2 | Family/User модели, приглашения, роли |
| 3 | Напоминания (CRUD + локальные уведомления) |
| 4 | Календарь + FCM push |
| 5 | Документы (upload, OCR, поиск) |
| 6 | Медицинский раздел + медикаменты |
| 7 | Финансы + заметки + списки |
| 8 | AI-помощник (`parseReminder`, `chat`) |
| 9 | Дни рождения, главный экран (виджеты) |
| 10 | Премиум, биллинг, аналитика |
| 11 | Стабилизация, UAT, релиз в Google Play closed beta |
---
# 17. Открытые вопросы
- Выбор основной БД для V2: остаться на Firestore или мигрировать на PostgreSQL
- Поддержка iOS — оценка через 3 месяца после Android релиза
- Интеграция с Каспи/национальными платёжными системами для KZ рынка
- Голосовой ассистент — собственный или Yandex Speech Kit / Google Speech-to-Text
- E2E шифрование медицинских данных — насколько обязательно для MVP
---
# 18. Глоссарий
| Термин | Значение |
|--------|-------------------------------------------------------|
| FCM | Firebase Cloud Messaging |
| OCR | Optical Character Recognition |
| TTS | Text-to-Speech |
| NLU | Natural Language Understanding |
| PII | Personally Identifiable Information |
| BaaS | Backend as a Service |
| MVVM | Model-View-ViewModel |
| RBAC | Role-Based Access Control |
---
# 19. Ссылки
- [PRD.md](PRD.md) — продуктовые требования
- [README.md](README.md) — описание репозитория
- Firebase docs: https://firebase.google.com/docs
- Flutter docs: https://docs.flutter.dev
- OpenAI API: https://platform.openai.com/docs

View File

@ -1,48 +0,0 @@
# Family Life Assistant — App (stubs)
Flutter скелет MVP. Все 10 систем из TRD §4 реализованы как stub.
## Запуск
```bash
cd app
flutter pub get
flutter run
```
## Структура
```
lib/
main.dart # entry
app.dart # MaterialApp + theme
router.dart # go_router
home_screen.dart # сетка из 10 систем
core/
models.dart # все entity из TRD §4
mock_data.dart # фейк-данные для всех модулей
list_screen.dart # общий stub-экран
features/
auth/ # семья, роли, инвайты
reminders/ # напоминания
calendar/ # календарь
medical/ # визиты, лекарства, low-stock
school/ # расписание, домашка, AI-заявления
birthdays/ # ДР + идеи подарков
documents/ # docs + expiry alerts
notes/ # заметки, чек-листы, покупки
finance/ # платежи, авто-списания
ai/ # AI-чат, parseReminder
```
Каждый feature: `*_service.dart` (stub методы → mock data) + `*_screen.dart`.
## Что НЕ сделано (специально)
- Firebase init — будут реальные `firebase_core`, `cloud_firestore`, `firebase_auth`
- Локальные пуши — `flutter_local_notifications`
- Реальный OpenAI proxy — Cloud Function `/ai/chat`
- Persistence — Isar/Hive
- Локализация (ru/kk/en) — ARB файлы
Stub-сервисы возвращают захардкоженные данные, чтобы UI можно было собрать и пощёлкать без бэкенда.

View File

@ -1,23 +0,0 @@
import 'package:flutter/material.dart';
import 'router.dart';
class FamilyLifeApp extends StatelessWidget {
const FamilyLifeApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Family Life Assistant',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF8BB89A),
brightness: Brightness.light,
),
scaffoldBackgroundColor: const Color(0xFFF5F1EA),
),
routerConfig: appRouter,
);
}
}

View File

@ -1,78 +0,0 @@
import 'package:flutter/material.dart';
class StubListScreen extends StatelessWidget {
final String title;
final String emoji;
final List<StubItem> items;
final String? hint;
const StubListScreen({
super.key,
required this.title,
required this.emoji,
required this.items,
this.hint,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('$emoji $title')),
body: items.isEmpty
? Center(child: Text('Пусто', style: Theme.of(context).textTheme.bodyLarge))
: ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: items.length + (hint != null ? 1 : 0),
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (ctx, i) {
if (hint != null && i == 0) {
return Card(
color: Theme.of(ctx).colorScheme.primaryContainer,
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(hint!, style: const TextStyle(fontStyle: FontStyle.italic)),
),
);
}
final item = items[i - (hint != null ? 1 : 0)];
return Card(
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(ctx).colorScheme.secondaryContainer,
child: Text(item.leading, style: const TextStyle(fontSize: 18)),
),
title: Text(item.title),
subtitle: item.subtitle == null ? null : Text(item.subtitle!),
trailing: item.trailing == null
? null
: Text(item.trailing!, style: TextStyle(
color: item.urgent ? Colors.red : Colors.green.shade700,
fontWeight: FontWeight.bold,
)),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Stub: создание не реализовано')),
),
child: const Icon(Icons.add),
),
);
}
}
class StubItem {
final String leading;
final String title;
final String? subtitle;
final String? trailing;
final bool urgent;
const StubItem({
required this.leading,
required this.title,
this.subtitle,
this.trailing,
this.urgent = false,
});
}

View File

@ -1,121 +0,0 @@
import 'models.dart';
const _famId = 'fam_1';
final _now = DateTime.now();
DateTime _at(int dayOffset, [int hour = 9, int minute = 0]) =>
DateTime(_now.year, _now.month, _now.day + dayOffset, hour, minute);
class MockData {
static final family = Family(
id: _famId,
name: 'Семья Иванчуковых',
ownerUid: 'u_mom',
memberUids: ['u_mom', 'u_dad', 'u_alina', 'u_timur'],
);
static final users = [
AppUser(uid: 'u_mom', displayName: 'Мама', familyId: _famId, role: UserRole.owner, color: '#8BB89A'),
AppUser(uid: 'u_dad', displayName: 'Папа', familyId: _famId, role: UserRole.parent, color: '#A8C8E0'),
AppUser(uid: 'u_alina', displayName: 'Алина', familyId: _famId, role: UserRole.child, color: '#E8DCC4'),
AppUser(uid: 'u_timur', displayName: 'Тимур', familyId: _famId, role: UserRole.child, color: '#E89B8B'),
];
static final reminders = [
Reminder(id: 'r1', familyId: _famId, createdBy: 'u_mom', assigneeUid: 'u_alina',
title: 'Записать Алину к стоматологу', category: ReminderCategory.medical,
dueAt: _at(0, 15, 0), priority: ReminderPriority.urgent),
Reminder(id: 'r2', familyId: _famId, createdBy: 'u_mom',
title: 'Оплатить коммуналку', category: ReminderCategory.finance, dueAt: _at(1, 18, 0)),
Reminder(id: 'r3', familyId: _famId, createdBy: 'u_mom', assigneeUid: 'u_timur',
title: 'Принести поделку в садик', category: ReminderCategory.school, dueAt: _at(1, 8, 0)),
Reminder(id: 'r4', familyId: _famId, createdBy: 'u_mom',
title: 'Купить подарок бабушке', category: ReminderCategory.birthday, dueAt: _at(5, 10, 0)),
];
static final events = [
CalendarEvent(id: 'e1', familyId: _famId, title: 'Стоматолог — Алина',
category: EventCategory.medical, startAt: _at(0, 15, 0), endAt: _at(0, 15, 45),
participants: ['u_alina', 'u_mom'], location: 'Клиника «Улыбка»', color: '#E89B8B'),
CalendarEvent(id: 'e2', familyId: _famId, title: 'Родительское собрание',
category: EventCategory.school, startAt: _at(1, 18, 0), endAt: _at(1, 19, 30),
participants: ['u_mom'], location: 'Школа №42', color: '#A8C8E0'),
CalendarEvent(id: 'e3', familyId: _famId, title: 'Йога — мама',
category: EventCategory.personal, startAt: _at(3, 9, 0), endAt: _at(3, 10, 0),
participants: ['u_mom'], color: '#8BB89A'),
CalendarEvent(id: 'e4', familyId: _famId, title: 'ДР бабушки Айгуль',
category: EventCategory.personal, startAt: _at(6), endAt: _at(6), allDay: true, color: '#F3D6FF'),
];
static final medicalRecords = [
MedicalRecord(id: 'm1', familyId: _famId, patientUid: 'u_timur', type: MedicalType.visit,
title: 'Педиатр — ОРВИ', date: _at(-17), doctor: 'Иванова А.С.', clinic: 'ДП №3', notes: 'выписан'),
MedicalRecord(id: 'm2', familyId: _famId, patientUid: 'u_alina', type: MedicalType.visit,
title: 'Стоматолог профилактика', date: _at(-34), doctor: 'Сатов Б.К.', clinic: 'Улыбка'),
MedicalRecord(id: 'm3', familyId: _famId, patientUid: 'u_mom', type: MedicalType.analysis,
title: 'Анализ крови — норма', date: _at(-50)),
MedicalRecord(id: 'm4', familyId: _famId, patientUid: 'u_timur', type: MedicalType.vaccination,
title: 'КПК', date: _at(-90)),
];
static final medications = [
Medication(id: 'med1', familyId: _famId, patientUid: 'u_timur',
name: 'Парацетамол', dosage: '5 мл', stockCount: 2, lowStockThreshold: 3),
Medication(id: 'med2', familyId: _famId, patientUid: 'u_alina',
name: 'Витамин D', dosage: '1 капля', stockCount: 30),
];
static final documents = [
FamilyDocument(id: 'd1', familyId: _famId, ownerUid: 'u_mom', title: 'Удостоверение мамы',
category: DocCategory.id, uploadedAt: _at(-200), expiresAt: _at(1800)),
FamilyDocument(id: 'd2', familyId: _famId, ownerUid: 'u_alina', title: 'Свидетельство Алины',
category: DocCategory.school, uploadedAt: _at(-180)),
FamilyDocument(id: 'd3', familyId: _famId, ownerUid: 'u_dad', title: 'Тех. паспорт авто',
category: DocCategory.insurance, uploadedAt: _at(-60), expiresAt: _at(12)),
FamilyDocument(id: 'd4', familyId: _famId, ownerUid: 'u_mom', title: 'Полис ДМС',
category: DocCategory.medical, uploadedAt: _at(-300), expiresAt: _at(80)),
];
static final notes = [
FamilyNote(id: 'n1', familyId: _famId, type: NoteType.shopping, title: 'Список покупок',
createdAt: _at(-1), items: const [
NoteItem('Молоко 2.5%'),
NoteItem('Хлеб серый'),
NoteItem('Яблоки', done: true),
NoteItem('Парацетамол детский'),
NoteItem('Тетради ×3'),
]),
FamilyNote(id: 'n2', familyId: _famId, type: NoteType.note, title: 'Идеи подарка бабушке',
createdAt: _at(-2), content: 'плед, книга о саде, медовик'),
FamilyNote(id: 'n3', familyId: _famId, type: NoteType.checklist, title: 'На дачу',
createdAt: _at(-5), items: const [
NoteItem('Палатка', done: true),
NoteItem('Спальники', done: true),
NoteItem('Аптечка'),
NoteItem('Продукты'),
]),
];
static final payments = [
FinancePayment(id: 'p1', familyId: _famId, title: 'Коммуналка',
category: FinanceCategory.utilities, amount: 18400, dueDate: _at(1)),
FinancePayment(id: 'p2', familyId: _famId, title: 'Интернет Beeline',
category: FinanceCategory.internet, amount: 5990, dueDate: _at(3)),
FinancePayment(id: 'p3', familyId: _famId, title: 'Кружок Алины',
category: FinanceCategory.school, amount: 12000, dueDate: _at(8)),
FinancePayment(id: 'p4', familyId: _famId, title: 'Ипотека',
category: FinanceCategory.loan, amount: 28500, dueDate: _at(14)),
];
static final birthdays = [
Birthday(id: 'b1', familyId: _famId, personName: 'Бабушка Айгуль',
relation: 'бабушка', date: '05-28', yearBorn: 1959,
giftIdeas: ['Тёплый плед', 'Книга о саде', 'Медовик']),
Birthday(id: 'b2', familyId: _famId, personName: 'Айбек', relation: 'папа',
date: '06-12', yearBorn: 1988),
Birthday(id: 'b3', familyId: _famId, personName: 'Алина', relation: 'дочь',
date: '07-03', yearBorn: 2017),
Birthday(id: 'b4', familyId: _famId, personName: 'Тимур', relation: 'сын',
date: '09-18', yearBorn: 2022),
];
}

View File

@ -1,229 +0,0 @@
enum UserRole { owner, parent, child, grandparent }
enum ReminderCategory { medical, school, finance, birthday, personal, other }
enum ReminderPriority { low, normal, urgent }
enum ReminderStatus { pending, done, snoozed, cancelled }
enum EventCategory { medical, school, work, finance, personal }
enum MedicalType { visit, analysis, vaccination, prescription }
enum DocCategory { id, insurance, certificate, school, medical, other }
enum NoteType { note, checklist, shopping, voice }
enum FinanceCategory { utilities, internet, school, loan, subscription, other }
enum FinanceStatus { pending, paid, overdue }
enum FamilyPlan { free, premium }
class AppUser {
final String uid;
final String displayName;
final String? email;
final String? photoUrl;
final String familyId;
final UserRole role;
final String color;
final String locale;
AppUser({
required this.uid,
required this.displayName,
required this.familyId,
required this.role,
this.email,
this.photoUrl,
this.color = '#8BB89A',
this.locale = 'ru',
});
}
class Family {
final String id;
final String name;
final String ownerUid;
final List<String> memberUids;
final FamilyPlan plan;
Family({
required this.id,
required this.name,
required this.ownerUid,
required this.memberUids,
this.plan = FamilyPlan.free,
});
}
class Reminder {
final String id;
final String familyId;
final String createdBy;
final String? assigneeUid;
final String title;
final String? description;
final ReminderCategory category;
final DateTime dueAt;
final ReminderPriority priority;
final ReminderStatus status;
Reminder({
required this.id,
required this.familyId,
required this.createdBy,
required this.title,
required this.category,
required this.dueAt,
this.assigneeUid,
this.description,
this.priority = ReminderPriority.normal,
this.status = ReminderStatus.pending,
});
}
class CalendarEvent {
final String id;
final String familyId;
final String title;
final EventCategory category;
final DateTime startAt;
final DateTime endAt;
final bool allDay;
final List<String> participants;
final String? location;
final String color;
CalendarEvent({
required this.id,
required this.familyId,
required this.title,
required this.category,
required this.startAt,
required this.endAt,
this.allDay = false,
this.participants = const [],
this.location,
this.color = '#8BB89A',
});
}
class MedicalRecord {
final String id;
final String familyId;
final String patientUid;
final MedicalType type;
final String title;
final String? doctor;
final String? clinic;
final DateTime date;
final String? notes;
MedicalRecord({
required this.id,
required this.familyId,
required this.patientUid,
required this.type,
required this.title,
required this.date,
this.doctor,
this.clinic,
this.notes,
});
}
class Medication {
final String id;
final String familyId;
final String patientUid;
final String name;
final String dosage;
final int stockCount;
final int lowStockThreshold;
Medication({
required this.id,
required this.familyId,
required this.patientUid,
required this.name,
required this.dosage,
this.stockCount = 0,
this.lowStockThreshold = 5,
});
}
class FamilyDocument {
final String id;
final String familyId;
final String ownerUid;
final String title;
final DocCategory category;
final String? storagePath;
final String? ocrText;
final DateTime? expiresAt;
final List<String> tags;
final DateTime uploadedAt;
FamilyDocument({
required this.id,
required this.familyId,
required this.ownerUid,
required this.title,
required this.category,
required this.uploadedAt,
this.storagePath,
this.ocrText,
this.expiresAt,
this.tags = const [],
});
}
class NoteItem {
final String text;
final bool done;
const NoteItem(this.text, {this.done = false});
}
class FamilyNote {
final String id;
final String familyId;
final NoteType type;
final String title;
final String? content;
final List<NoteItem> items;
final DateTime createdAt;
FamilyNote({
required this.id,
required this.familyId,
required this.type,
required this.title,
required this.createdAt,
this.content,
this.items = const [],
});
}
class FinancePayment {
final String id;
final String familyId;
final String title;
final FinanceCategory category;
final double amount;
final String currency;
final DateTime dueDate;
final FinanceStatus status;
FinancePayment({
required this.id,
required this.familyId,
required this.title,
required this.category,
required this.amount,
required this.dueDate,
this.currency = 'KZT',
this.status = FinanceStatus.pending,
});
}
class Birthday {
final String id;
final String familyId;
final String personName;
final String? relation;
final String date;
final int? yearBorn;
final List<String> giftIdeas;
Birthday({
required this.id,
required this.familyId,
required this.personName,
required this.date,
this.relation,
this.yearBorn,
this.giftIdeas = const [],
});
}

View File

@ -1,107 +0,0 @@
import 'package:flutter/material.dart';
import 'ai_service.dart';
class _Msg {
final String text;
final bool fromUser;
_Msg(this.text, this.fromUser);
}
class AiScreen extends StatefulWidget {
const AiScreen({super.key});
@override
State<AiScreen> createState() => _AiScreenState();
}
class _AiScreenState extends State<AiScreen> {
final _svc = AiService();
final _ctrl = TextEditingController();
final List<_Msg> _msgs = [
_Msg('Привет! Я AI-помощник. Спроси что угодно — напоминания, заявления, план на неделю.', false),
];
bool _loading = false;
Future<void> _send() async {
final text = _ctrl.text.trim();
if (text.isEmpty) return;
setState(() {
_msgs.add(_Msg(text, true));
_ctrl.clear();
_loading = true;
});
final reply = await _svc.chat(text);
setState(() {
_msgs.add(_Msg(reply, false));
_loading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('🤖 AI-помощник')),
body: Column(
children: [
Expanded(
child: ListView.builder(
reverse: false,
padding: const EdgeInsets.all(12),
itemCount: _msgs.length,
itemBuilder: (ctx, i) {
final m = _msgs[i];
return Align(
alignment: m.fromUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.all(12),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.78,
),
decoration: BoxDecoration(
color: m.fromUser
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
),
child: Text(
m.text,
style: TextStyle(
color: m.fromUser ? Colors.white : null,
),
),
),
);
},
),
),
if (_loading)
const Padding(
padding: EdgeInsets.all(8),
child: LinearProgressIndicator(),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
Expanded(
child: TextField(
controller: _ctrl,
decoration: const InputDecoration(
hintText: 'Например: «Напомни купить молоко завтра в 9»',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _send(),
),
),
IconButton(icon: const Icon(Icons.send), onPressed: _send),
],
),
),
),
],
),
);
}
}

View File

@ -1,35 +0,0 @@
import '../../core/models.dart';
class AiService {
Future<String> chat(String text) async {
await Future.delayed(const Duration(milliseconds: 400));
return '[stub AI] Понял запрос: "$text". В реальности — proxy → OpenAI API через Cloud Function /ai/chat.';
}
Future<Reminder> parseReminder({
required String text,
required String familyId,
String timezone = 'Asia/Almaty',
}) async {
await Future.delayed(const Duration(milliseconds: 300));
return Reminder(
id: 'r_${DateTime.now().millisecondsSinceEpoch}',
familyId: familyId,
createdBy: 'u_mom',
title: text,
category: ReminderCategory.other,
dueAt: DateTime.now().add(const Duration(hours: 1)),
);
}
Future<String> generateApplication({
required String type,
required String childName,
}) async {
return 'Заявление ($type) для $childName. [stub]';
}
Future<String> weeklyPlan(String familyId) async {
return 'План на неделю: 1) понедельник — врач, 2) среда — оплаты, ... [stub]';
}
}

View File

@ -1,49 +0,0 @@
import 'package:flutter/material.dart';
import '../../core/mock_data.dart';
class AuthScreen extends StatelessWidget {
const AuthScreen({super.key});
@override
Widget build(BuildContext context) {
final fam = MockData.family;
return Scaffold(
appBar: AppBar(title: const Text('👤 Семья')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(fam.name, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 4),
Text('План: ${fam.plan.name}'),
Text('Участников: ${fam.memberUids.length}'),
],
),
),
),
const SizedBox(height: 12),
...MockData.users.map((u) => Card(
child: ListTile(
leading: CircleAvatar(child: Text(u.displayName[0])),
title: Text(u.displayName),
subtitle: Text('Роль: ${u.role.name}'),
),
)),
const SizedBox(height: 12),
FilledButton.icon(
icon: const Icon(Icons.person_add),
label: const Text('Пригласить (stub)'),
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Stub: invite link → /auth/invite')),
),
),
],
),
);
}
}

View File

@ -1,24 +0,0 @@
import '../../core/models.dart';
import '../../core/mock_data.dart';
class AuthService {
AppUser? _current = MockData.users.first;
AppUser? get currentUser => _current;
Future<AppUser> signInWithEmail(String email, String password) async {
await Future.delayed(const Duration(milliseconds: 200));
_current = MockData.users.first;
return _current!;
}
Future<void> signOut() async {
_current = null;
}
Future<String> generateInviteLink(String familyId) async {
return 'https://fla.app/invite/$familyId/${DateTime.now().millisecondsSinceEpoch}';
}
Future<void> acceptInvite(String inviteCode) async {}
}

View File

@ -1,37 +0,0 @@
import 'package:flutter/material.dart';
import '../../core/mock_data.dart';
class BirthdaysScreen extends StatelessWidget {
const BirthdaysScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('🎂 Дни рождения')),
body: ListView(
padding: const EdgeInsets.all(12),
children: MockData.birthdays.map((b) {
return Card(
child: ExpansionTile(
leading: const Text('🎂', style: TextStyle(fontSize: 24)),
title: Text(b.personName),
subtitle: Text('${b.date}${b.relation == null ? '' : ' · ${b.relation}'}'),
children: [
if (b.giftIdeas.isEmpty)
const Padding(
padding: EdgeInsets.all(12),
child: Text('Идеи подарка не добавлены'),
)
else
...b.giftIdeas.map((g) => ListTile(
leading: const Text('🎁'),
title: Text(g),
)),
],
),
);
}).toList(),
),
);
}
}

View File

@ -1,11 +0,0 @@
import '../../core/models.dart';
import '../../core/mock_data.dart';
class BirthdaysService {
Future<List<Birthday>> list() async => MockData.birthdays;
Future<List<String>> giftIdeas(String birthdayId) async {
final b = MockData.birthdays.firstWhere((x) => x.id == birthdayId);
return b.giftIdeas;
}
}

View File

@ -1,36 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../core/list_screen.dart';
import '../../core/mock_data.dart';
import '../../core/models.dart';
class CalendarScreen extends StatelessWidget {
const CalendarScreen({super.key});
String _icon(EventCategory c) => switch (c) {
EventCategory.medical => '💊',
EventCategory.school => '🎒',
EventCategory.work => '💼',
EventCategory.finance => '💰',
EventCategory.personal => '🏃',
};
@override
Widget build(BuildContext context) {
final fmt = DateFormat('dd MMM HH:mm', 'ru');
final items = MockData.events
.map((e) => StubItem(
leading: _icon(e.category),
title: e.title,
subtitle: e.allDay ? 'Весь день · ${fmt.format(e.startAt)}' : fmt.format(e.startAt),
trailing: e.location,
))
.toList();
return StubListScreen(
title: 'Календарь',
emoji: '📅',
items: items,
hint: 'Stub: Firestore listener на /families/{id}/events',
);
}
}

View File

@ -1,15 +0,0 @@
import '../../core/models.dart';
import '../../core/mock_data.dart';
class CalendarService {
Future<List<CalendarEvent>> list({DateTime? from, DateTime? to}) async {
return MockData.events.where((e) {
if (from != null && e.startAt.isBefore(from)) return false;
if (to != null && e.startAt.isAfter(to)) return false;
return true;
}).toList();
}
Future<CalendarEvent> create(CalendarEvent e) async => e;
Future<void> delete(String id) async {}
}

View File

@ -1,51 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../core/list_screen.dart';
import '../../core/mock_data.dart';
import '../../core/models.dart';
class DocumentsScreen extends StatelessWidget {
const DocumentsScreen({super.key});
String _icon(DocCategory c) => switch (c) {
DocCategory.id => '🪪',
DocCategory.insurance => '🛡',
DocCategory.certificate => '📄',
DocCategory.school => '🎒',
DocCategory.medical => '🏥',
DocCategory.other => '📂',
};
@override
Widget build(BuildContext context) {
final fmt = DateFormat('dd.MM.yyyy');
final now = DateTime.now();
final items = MockData.documents.map((d) {
final exp = d.expiresAt;
String? trailing;
bool urgent = false;
if (exp != null) {
final daysLeft = exp.difference(now).inDays;
if (daysLeft < 30) {
trailing = '${daysLeft}д';
urgent = daysLeft < 14;
} else {
trailing = fmt.format(exp);
}
}
return StubItem(
leading: _icon(d.category),
title: d.title,
subtitle: d.category.name,
trailing: trailing,
urgent: urgent,
);
}).toList();
return StubListScreen(
title: 'Документы',
emoji: '📂',
items: items,
hint: 'Stub: загрузка → Firebase Storage → OCR (ML Kit)',
);
}
}

View File

@ -1,23 +0,0 @@
import '../../core/models.dart';
import '../../core/mock_data.dart';
class DocumentsService {
Future<List<FamilyDocument>> list({DocCategory? category}) async {
return MockData.documents
.where((d) => category == null || d.category == category)
.toList();
}
Future<FamilyDocument> upload(FamilyDocument d) async => d;
Future<String> ocr(String storagePath) async {
return '[stub OCR text]';
}
Future<List<FamilyDocument>> expiringSoon({Duration within = const Duration(days: 30)}) async {
final cutoff = DateTime.now().add(within);
return MockData.documents
.where((d) => d.expiresAt != null && d.expiresAt!.isBefore(cutoff))
.toList();
}
}

View File

@ -1,57 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../core/mock_data.dart';
class FinanceScreen extends StatelessWidget {
const FinanceScreen({super.key});
@override
Widget build(BuildContext context) {
final fmt = DateFormat('dd.MM');
final money = NumberFormat('#,###', 'ru');
final total = MockData.payments.fold<double>(0, (s, p) => s + p.amount);
final now = DateTime.now();
return Scaffold(
appBar: AppBar(title: const Text('💰 Финансы')),
body: ListView(
padding: const EdgeInsets.all(12),
children: [
Card(
color: Theme.of(context).colorScheme.primaryContainer,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('К оплате'),
const SizedBox(height: 4),
Text('${money.format(total)} ₸',
style: Theme.of(context).textTheme.headlineMedium),
],
),
),
),
const SizedBox(height: 12),
...MockData.payments.map((p) {
final daysLeft = p.dueDate.difference(now).inDays;
final urgent = daysLeft <= 3;
return Card(
child: ListTile(
leading: const Text('💸', style: TextStyle(fontSize: 24)),
title: Text(p.title),
subtitle: Text('${p.category.name} · до ${fmt.format(p.dueDate)}'),
trailing: Text(
'${money.format(p.amount)} ₸',
style: TextStyle(
color: urgent ? Colors.red : null,
fontWeight: FontWeight.bold,
),
),
),
);
}),
],
),
);
}
}

View File

@ -1,18 +0,0 @@
import '../../core/models.dart';
import '../../core/mock_data.dart';
class FinanceService {
Future<List<FinancePayment>> payments({FinanceStatus? status}) async {
return MockData.payments
.where((p) => status == null || p.status == status)
.toList();
}
Future<double> totalDue() async {
return MockData.payments
.where((p) => p.status == FinanceStatus.pending)
.fold(0.0, (sum, p) => sum + p.amount);
}
Future<void> markPaid(String id) async {}
}

View File

@ -1,55 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../core/mock_data.dart';
import '../../core/models.dart';
class MedicalScreen extends StatelessWidget {
const MedicalScreen({super.key});
String _userName(String uid) =>
MockData.users.firstWhere((u) => u.uid == uid, orElse: () => MockData.users.first).displayName;
@override
Widget build(BuildContext context) {
final fmt = DateFormat('dd.MM.yyyy');
return Scaffold(
appBar: AppBar(title: const Text('💊 Медицина')),
body: ListView(
padding: const EdgeInsets.all(12),
children: [
Text('Лекарства', style: Theme.of(context).textTheme.titleMedium),
...MockData.medications.map((m) {
final low = m.stockCount <= m.lowStockThreshold;
return Card(
child: ListTile(
leading: const Text('💊', style: TextStyle(fontSize: 24)),
title: Text('${m.name} · ${_userName(m.patientUid)}'),
subtitle: Text('Остаток: ${m.stockCount} · ${m.dosage}'),
trailing: low
? const Text('LOW', style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold))
: null,
),
);
}),
const SizedBox(height: 12),
Text('История визитов', style: Theme.of(context).textTheme.titleMedium),
...MockData.medicalRecords.map((r) => Card(
child: ListTile(
leading: Text(
switch (r.type) {
MedicalType.visit => '🩺',
MedicalType.analysis => '🧪',
MedicalType.vaccination => '💉',
MedicalType.prescription => '📝',
},
style: const TextStyle(fontSize: 24),
),
title: Text(r.title),
subtitle: Text('${_userName(r.patientUid)} · ${fmt.format(r.date)}'),
),
)),
],
),
);
}
}

View File

@ -1,15 +0,0 @@
import '../../core/models.dart';
import '../../core/mock_data.dart';
class MedicalService {
Future<List<MedicalRecord>> records({String? patientUid}) async {
return MockData.medicalRecords
.where((r) => patientUid == null || r.patientUid == patientUid)
.toList();
}
Future<List<Medication>> medications() async => MockData.medications;
Future<List<Medication>> lowStock() async =>
MockData.medications.where((m) => m.stockCount <= m.lowStockThreshold).toList();
}

View File

@ -1,47 +0,0 @@
import 'package:flutter/material.dart';
import '../../core/mock_data.dart';
import '../../core/models.dart';
class NotesScreen extends StatelessWidget {
const NotesScreen({super.key});
String _icon(NoteType t) => switch (t) {
NoteType.note => '📝',
NoteType.checklist => '📋',
NoteType.shopping => '🛒',
NoteType.voice => '🎙',
};
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('📝 Заметки и покупки')),
body: ListView(
padding: const EdgeInsets.all(12),
children: MockData.notes.map((n) {
return Card(
child: ExpansionTile(
leading: Text(_icon(n.type), style: const TextStyle(fontSize: 24)),
title: Text(n.title),
subtitle: Text('${n.type.name} · ${n.items.length} пунктов'),
children: [
if (n.content != null)
Padding(padding: const EdgeInsets.all(12), child: Text(n.content!)),
...n.items.asMap().entries.map((e) => CheckboxListTile(
value: e.value.done,
onChanged: (_) {},
title: Text(
e.value.text,
style: TextStyle(
decoration: e.value.done ? TextDecoration.lineThrough : null,
),
),
)),
],
),
);
}).toList(),
),
);
}
}

View File

@ -1,13 +0,0 @@
import '../../core/models.dart';
import '../../core/mock_data.dart';
class NotesService {
Future<List<FamilyNote>> list({NoteType? type}) async {
return MockData.notes
.where((n) => type == null || n.type == type)
.toList();
}
Future<FamilyNote> create(FamilyNote n) async => n;
Future<void> toggleItem(String noteId, int itemIndex) async {}
}

View File

@ -1,38 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../core/list_screen.dart';
import '../../core/mock_data.dart';
import '../../core/models.dart';
class RemindersScreen extends StatelessWidget {
const RemindersScreen({super.key});
String _icon(ReminderCategory c) => switch (c) {
ReminderCategory.medical => '💊',
ReminderCategory.school => '🎒',
ReminderCategory.finance => '💰',
ReminderCategory.birthday => '🎂',
ReminderCategory.personal => '🏃',
ReminderCategory.other => '🔔',
};
@override
Widget build(BuildContext context) {
final fmt = DateFormat('dd MMM HH:mm', 'ru');
final items = MockData.reminders
.map((r) => StubItem(
leading: _icon(r.category),
title: r.title,
subtitle: fmt.format(r.dueAt),
trailing: r.priority == ReminderPriority.urgent ? 'СРОЧ' : null,
urgent: r.priority == ReminderPriority.urgent,
))
.toList();
return StubListScreen(
title: 'Напоминания',
emoji: '🔔',
items: items,
hint: 'Stub: CRUD + локальные пуши через flutter_local_notifications',
);
}
}

View File

@ -1,15 +0,0 @@
import '../../core/models.dart';
import '../../core/mock_data.dart';
class RemindersService {
Future<List<Reminder>> list({ReminderStatus? status}) async {
return MockData.reminders
.where((r) => status == null || r.status == status)
.toList();
}
Future<Reminder> create(Reminder r) async => r;
Future<void> markDone(String id) async {}
Future<void> snooze(String id, Duration d) async {}
Future<void> delete(String id) async {}
}

View File

@ -1,53 +0,0 @@
import 'package:flutter/material.dart';
import 'school_service.dart';
class SchoolScreen extends StatefulWidget {
const SchoolScreen({super.key});
@override
State<SchoolScreen> createState() => _SchoolScreenState();
}
class _SchoolScreenState extends State<SchoolScreen> {
final _svc = SchoolService();
String? _generated;
Future<void> _generate() async {
final txt = await _svc.generateApplication(
type: 'по болезни',
childName: 'Алина',
);
setState(() => _generated = txt);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('🎒 Школа и сад')),
body: ListView(
padding: const EdgeInsets.all(12),
children: [
Text('Расписание (Алина · пт)', style: Theme.of(context).textTheme.titleMedium),
const Card(child: ListTile(leading: Text('1', style: TextStyle(fontSize: 18)), title: Text('Математика'), subtitle: Text('08:30–09:15'))),
const Card(child: ListTile(leading: Text('2', style: TextStyle(fontSize: 18)), title: Text('Русский язык'), subtitle: Text('09:25–10:10'))),
const Card(child: ListTile(leading: Text('3', style: TextStyle(fontSize: 18)), title: Text('Физкультура'), subtitle: Text('10:20–11:05 · форма!'))),
const SizedBox(height: 12),
Text('Домашка', style: Theme.of(context).textTheme.titleMedium),
const Card(child: CheckboxListTile(value: false, onChanged: null, title: Text('Мат. с.34 №5,6'))),
const Card(child: CheckboxListTile(value: false, onChanged: null, title: Text('Рус. упр. 142'))),
const SizedBox(height: 12),
FilledButton.icon(
icon: const Icon(Icons.auto_awesome),
label: const Text('AI · Сгенерировать заявление'),
onPressed: _generate,
),
if (_generated != null)
Card(
color: Theme.of(context).colorScheme.primaryContainer,
child: Padding(padding: const EdgeInsets.all(12), child: Text(_generated!)),
),
],
),
);
}
}

View File

@ -1,20 +0,0 @@
class SchoolService {
Future<List<String>> homework(String childUid) async => const [
'Математика с.34 №5,6',
'Русский упр. 142',
];
Future<List<String>> schedule(String childUid, int weekday) async => const [
'08:30 — Математика',
'09:25 — Русский',
'10:20 — Физкультура',
];
Future<String> generateApplication({
required String type,
required String childName,
String? reason,
}) async {
return 'Заявление ($type) для $childName${reason == null ? '' : ': $reason'}.\n[stub AI output]';
}
}

View File

@ -1,57 +0,0 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class _Tile {
final String emoji;
final String label;
final String route;
const _Tile(this.emoji, this.label, this.route);
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
static const _tiles = [
_Tile('🔔', 'Напоминания', '/reminders'),
_Tile('📅', 'Календарь', '/calendar'),
_Tile('💊', 'Медицина', '/medical'),
_Tile('🎒', 'Школа/сад', '/school'),
_Tile('🎂', 'Дни рождения', '/birthdays'),
_Tile('📂', 'Документы', '/documents'),
_Tile('📝', 'Заметки/покупки', '/notes'),
_Tile('💰', 'Финансы', '/finance'),
_Tile('🤖', 'AI-помощник', '/ai'),
_Tile('👤', 'Аккаунт/семья', '/auth'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('🏡 Family Life Assistant')),
body: GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(16),
crossAxisSpacing: 12,
mainAxisSpacing: 12,
children: _tiles.map((t) {
return Card(
child: InkWell(
onTap: () => context.push(t.route),
borderRadius: BorderRadius.circular(12),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(t.emoji, style: const TextStyle(fontSize: 40)),
const SizedBox(height: 8),
Text(t.label, style: Theme.of(context).textTheme.titleMedium),
],
),
),
),
);
}).toList(),
),
);
}
}

View File

@ -1,7 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'app.dart';
void main() {
runApp(const ProviderScope(child: FamilyLifeApp()));
}

View File

@ -1,29 +0,0 @@
import 'package:go_router/go_router.dart';
import 'features/auth/auth_screen.dart';
import 'features/reminders/reminders_screen.dart';
import 'features/calendar/calendar_screen.dart';
import 'features/medical/medical_screen.dart';
import 'features/school/school_screen.dart';
import 'features/birthdays/birthdays_screen.dart';
import 'features/documents/documents_screen.dart';
import 'features/notes/notes_screen.dart';
import 'features/finance/finance_screen.dart';
import 'features/ai/ai_screen.dart';
import 'home_screen.dart';
final appRouter = GoRouter(
initialLocation: '/',
routes: [
GoRoute(path: '/', builder: (_, __) => const HomeScreen()),
GoRoute(path: '/auth', builder: (_, __) => const AuthScreen()),
GoRoute(path: '/reminders', builder: (_, __) => const RemindersScreen()),
GoRoute(path: '/calendar', builder: (_, __) => const CalendarScreen()),
GoRoute(path: '/medical', builder: (_, __) => const MedicalScreen()),
GoRoute(path: '/school', builder: (_, __) => const SchoolScreen()),
GoRoute(path: '/birthdays', builder: (_, __) => const BirthdaysScreen()),
GoRoute(path: '/documents', builder: (_, __) => const DocumentsScreen()),
GoRoute(path: '/notes', builder: (_, __) => const NotesScreen()),
GoRoute(path: '/finance', builder: (_, __) => const FinanceScreen()),
GoRoute(path: '/ai', builder: (_, __) => const AiScreen()),
],
);

View File

@ -1,25 +0,0 @@
name: family_life_assistant
description: Family Life Assistant — MVP scaffold (stubs only)
publish_to: 'none'
version: 0.1.0+1
environment:
sdk: ">=3.4.0 <4.0.0"
flutter: ">=3.22.0"
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
go_router: ^14.0.0
flutter_riverpod: ^2.5.0
intl: ^0.19.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
flutter:
uses-material-design: true

886
index.html Normal file
View File

@ -0,0 +1,886 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Family Life Assistant — Mock</title>
<style>
:root {
--bg: #f5f1ea;
--card: #ffffff;
--accent: #8bb89a;
--accent-dark: #5d8a6d;
--blue: #a8c8e0;
--beige: #e8dcc4;
--text: #2d3a32;
--muted: #8a9b8f;
--urgent: #e89b8b;
--shadow: 0 2px 8px rgba(45,58,50,0.08);
}
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #d4e4d8 0%, #e8dcc4 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: var(--text);
}
.device-wrap {
display: flex;
gap: 40px;
align-items: flex-start;
flex-wrap: wrap;
justify-content: center;
}
.phone {
width: 390px;
height: 800px;
background: #1a1a1a;
border-radius: 48px;
padding: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
position: relative;
}
.screen {
width: 100%;
height: 100%;
background: var(--bg);
border-radius: 36px;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
}
.notch {
position: absolute;
top: 12px;
left: 50%;
transform: translateX(-50%);
width: 120px;
height: 28px;
background: #1a1a1a;
border-radius: 0 0 18px 18px;
z-index: 10;
}
.status-bar {
height: 44px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 24px;
font-size: 14px;
font-weight: 600;
color: var(--text);
}
.header {
padding: 16px 20px 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 24px;
font-weight: 700;
}
.header h1 .emoji { margin-right: 6px; }
.avatar {
width: 40px; height: 40px;
border-radius: 50%;
background: var(--accent);
color: white;
display: flex; align-items: center; justify-content: center;
font-weight: 700;
}
.content {
flex: 1;
overflow-y: auto;
padding: 0 16px 80px;
}
.content::-webkit-scrollbar { display: none; }
.greeting {
font-size: 15px;
color: var(--muted);
padding: 0 4px 16px;
}
.widget {
background: var(--card);
border-radius: 20px;
padding: 16px;
margin-bottom: 12px;
box-shadow: var(--shadow);
}
.widget-title {
font-size: 12px;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
}
.widget-title .count {
background: var(--accent);
color: white;
border-radius: 10px;
padding: 2px 8px;
font-size: 11px;
}
.reminder-item {
display: flex;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #f0ebe2;
gap: 12px;
}
.reminder-item:last-child { border-bottom: none; }
.reminder-icon {
width: 36px; height: 36px;
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.icon-medical { background: #ffe1d6; }
.icon-school { background: #d6e8ff; }
.icon-finance { background: #fff3d6; }
.icon-birthday { background: #f3d6ff; }
.icon-personal { background: #d6ffe1; }
.icon-food { background: #ffd6d6; }
.reminder-text { flex: 1; min-width: 0; }
.reminder-title-r {
font-size: 15px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.reminder-meta {
font-size: 12px;
color: var(--muted);
margin-top: 2px;
}
.badge-urgent {
background: var(--urgent);
color: white;
padding: 2px 6px;
border-radius: 6px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
}
.calendar-strip {
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 4px;
}
.calendar-strip::-webkit-scrollbar { display: none; }
.day {
min-width: 48px;
text-align: center;
padding: 8px 4px;
border-radius: 14px;
background: #f8f4ec;
cursor: pointer;
}
.day.active {
background: var(--accent);
color: white;
}
.day .dow { font-size: 10px; text-transform: uppercase; opacity: 0.7; }
.day .num { font-size: 18px; font-weight: 700; margin-top: 2px; }
.day .dot { width: 4px; height: 4px; border-radius: 50%; background: var(--urgent); margin: 4px auto 0; }
.ai-card {
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%);
color: white;
border-radius: 20px;
padding: 16px;
margin-bottom: 12px;
}
.ai-card .label {
font-size: 11px;
text-transform: uppercase;
opacity: 0.8;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
.ai-card .question {
font-size: 15px;
margin-bottom: 12px;
line-height: 1.4;
}
.ai-input {
background: rgba(255,255,255,0.2);
border-radius: 14px;
padding: 10px 14px;
font-size: 13px;
display: flex;
align-items: center;
gap: 8px;
}
.quick-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-bottom: 12px;
}
.quick-item {
background: var(--card);
border-radius: 16px;
padding: 10px 6px;
text-align: center;
box-shadow: var(--shadow);
cursor: pointer;
transition: transform 0.1s;
}
.quick-item:active { transform: scale(0.95); }
.quick-emoji { font-size: 24px; margin-bottom: 4px; }
.quick-label { font-size: 10px; font-weight: 600; color: var(--text); }
.nav {
position: absolute;
bottom: 0; left: 0; right: 0;
height: 70px;
background: var(--card);
border-top: 1px solid #f0ebe2;
display: flex;
justify-content: space-around;
align-items: center;
padding: 0 8px 12px;
z-index: 5;
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
color: var(--muted);
cursor: pointer;
padding: 6px 12px;
border-radius: 12px;
font-size: 10px;
transition: all 0.15s;
}
.nav-item.active { color: var(--accent-dark); }
.nav-item.active .nav-icon { transform: scale(1.1); }
.nav-icon { font-size: 22px; }
.home-indicator {
position: absolute;
bottom: 6px; left: 50%;
transform: translateX(-50%);
width: 120px; height: 4px;
background: #333;
border-radius: 2px;
}
.screen-label {
text-align: center;
color: var(--text);
font-weight: 600;
margin-top: 12px;
font-size: 13px;
opacity: 0.7;
}
.doc-item, .note-item, .pay-item, .med-item, .bday-item, .event-item {
background: var(--card);
border-radius: 16px;
padding: 14px;
margin-bottom: 10px;
box-shadow: var(--shadow);
display: flex;
align-items: center;
gap: 12px;
}
.doc-thumb {
width: 44px; height: 44px;
border-radius: 10px;
background: var(--beige);
display: flex; align-items: center; justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.item-main { flex: 1; min-width: 0; }
.item-title { font-size: 14px; font-weight: 600; }
.item-sub { font-size: 11px; color: var(--muted); margin-top: 2px; }
.item-right { font-size: 13px; font-weight: 600; color: var(--accent-dark); flex-shrink: 0; }
.item-right.urgent { color: var(--urgent); }
.chip-row { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; padding: 0 4px; }
.chip {
background: var(--card);
padding: 6px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 500;
box-shadow: var(--shadow);
cursor: pointer;
}
.chip.active { background: var(--accent); color: white; }
.fab {
position: absolute;
bottom: 86px; right: 20px;
width: 56px; height: 56px;
border-radius: 50%;
background: var(--accent-dark);
color: white;
display: flex; align-items: center; justify-content: center;
font-size: 28px;
box-shadow: 0 4px 16px rgba(93,138,109,0.4);
cursor: pointer;
z-index: 4;
}
.checkbox {
width: 22px; height: 22px;
border: 2px solid var(--accent);
border-radius: 6px;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
cursor: pointer;
}
.checkbox.checked { background: var(--accent); color: white; }
.checked-text { text-decoration: line-through; opacity: 0.5; }
.ai-chat-bubble {
background: var(--card);
border-radius: 18px;
padding: 12px 14px;
margin-bottom: 8px;
box-shadow: var(--shadow);
max-width: 80%;
font-size: 14px;
line-height: 1.4;
}
.ai-chat-bubble.user {
background: var(--accent);
color: white;
margin-left: auto;
border-bottom-right-radius: 6px;
}
.ai-chat-bubble.bot {
border-bottom-left-radius: 6px;
}
.ai-suggestion {
font-size: 12px;
background: rgba(255,255,255,0.3);
padding: 8px 12px;
border-radius: 12px;
margin-top: 6px;
display: inline-block;
cursor: pointer;
}
.legend {
max-width: 320px;
background: white;
border-radius: 20px;
padding: 24px;
box-shadow: var(--shadow);
font-size: 13px;
line-height: 1.6;
}
.legend h2 { font-size: 18px; margin-bottom: 12px; color: var(--accent-dark); }
.legend h3 { font-size: 13px; margin-top: 14px; margin-bottom: 4px; color: var(--text); }
.legend kbd {
background: var(--beige);
padding: 1px 6px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
}
.legend ul { padding-left: 18px; color: var(--muted); }
.legend li { margin-bottom: 2px; }
.toast {
position: absolute;
bottom: 100px; left: 50%;
transform: translateX(-50%);
background: rgba(45,58,50,0.95);
color: white;
padding: 10px 16px;
border-radius: 12px;
font-size: 13px;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
z-index: 20;
}
.toast.show { opacity: 1; }
.hidden { display: none !important; }
.empty {
text-align: center;
color: var(--muted);
padding: 40px 20px;
font-size: 13px;
}
</style>
</head>
<body>
<div class="device-wrap">
<div>
<div class="phone">
<div class="notch"></div>
<div class="screen">
<div class="status-bar">
<span>9:41</span>
<span>● ● ● 🔋</span>
</div>
<!-- HOME -->
<div class="content" data-tab="home">
<div class="header">
<h1><span class="emoji">🏡</span>Семья Иванчуковых</h1>
<div class="avatar">М</div>
</div>
<div class="greeting">Доброе утро, Зауреш! Сегодня <b>22 мая</b>, пятница</div>
<div class="ai-card" onclick="switchTab('ai')">
<div class="label">🤖 AI-помощник</div>
<div class="question">«Что важного сегодня и что не забыть купить?»</div>
<div class="ai-input">
<span>🎙</span><span style="opacity:0.85">Спросить или сказать...</span>
</div>
</div>
<div class="quick-grid">
<div class="quick-item" onclick="switchTab('calendar')"><div class="quick-emoji">📅</div><div class="quick-label">Календарь</div></div>
<div class="quick-item" onclick="switchTab('medical')"><div class="quick-emoji">💊</div><div class="quick-label">Медицина</div></div>
<div class="quick-item" onclick="switchTab('docs')"><div class="quick-emoji">📂</div><div class="quick-label">Документы</div></div>
<div class="quick-item" onclick="switchTab('finance')"><div class="quick-emoji">💰</div><div class="quick-label">Финансы</div></div>
<div class="quick-item" onclick="switchTab('notes')"><div class="quick-emoji">📝</div><div class="quick-label">Заметки</div></div>
<div class="quick-item" onclick="switchTab('shopping')"><div class="quick-emoji">🛒</div><div class="quick-label">Покупки</div></div>
<div class="quick-item" onclick="switchTab('school')"><div class="quick-emoji">🎒</div><div class="quick-label">Школа</div></div>
<div class="quick-item" onclick="switchTab('bday')"><div class="quick-emoji">🎂</div><div class="quick-label">Праздники</div></div>
</div>
<div class="widget">
<div class="widget-title">Срочно сегодня <span class="count">3</span></div>
<div class="reminder-item">
<div class="reminder-icon icon-medical">💉</div>
<div class="reminder-text">
<div class="reminder-title-r">Записать Алину к стоматологу</div>
<div class="reminder-meta">Сегодня 15:00 · клиника «Улыбка»</div>
</div>
<span class="badge-urgent">сроч</span>
</div>
<div class="reminder-item">
<div class="reminder-icon icon-finance">💡</div>
<div class="reminder-text">
<div class="reminder-title-r">Оплатить коммуналку</div>
<div class="reminder-meta">До 23 мая · 18 400 ₸</div>
</div>
</div>
<div class="reminder-item">
<div class="reminder-icon icon-school">🎒</div>
<div class="reminder-text">
<div class="reminder-title-r">Принести поделку в садик</div>
<div class="reminder-meta">Тимур, завтра утром</div>
</div>
</div>
</div>
<div class="widget">
<div class="widget-title">Календарь — следующие дни</div>
<div class="calendar-strip">
<div class="day active"><div class="dow">Пт</div><div class="num">22</div><div class="dot"></div></div>
<div class="day"><div class="dow">Сб</div><div class="num">23</div><div class="dot"></div></div>
<div class="day"><div class="dow">Вс</div><div class="num">24</div></div>
<div class="day"><div class="dow">Пн</div><div class="num">25</div><div class="dot"></div></div>
<div class="day"><div class="dow">Вт</div><div class="num">26</div></div>
<div class="day"><div class="dow">Ср</div><div class="num">27</div><div class="dot"></div></div>
<div class="day"><div class="dow">Чт</div><div class="num">28</div></div>
</div>
</div>
<div class="widget">
<div class="widget-title">🎂 Ближайший день рождения</div>
<div class="reminder-item" style="padding-top:4px">
<div class="reminder-icon icon-birthday">🎁</div>
<div class="reminder-text">
<div class="reminder-title-r">Бабушка Айгуль — 28 мая</div>
<div class="reminder-meta">Через 6 дней · подарок не куплен</div>
</div>
</div>
</div>
<div class="widget">
<div class="widget-title">🛒 Список покупок <span class="count">5</span></div>
<div class="reminder-item" style="padding:6px 0">
<div class="checkbox"></div>
<div class="reminder-text"><div class="reminder-title-r" style="font-weight:500;font-size:14px">Молоко, хлеб, яблоки</div></div>
</div>
<div class="reminder-item" style="padding:6px 0">
<div class="checkbox checked">✓</div>
<div class="reminder-text"><div class="reminder-title-r checked-text" style="font-weight:500;font-size:14px">Парацетамол детский</div></div>
</div>
<div class="reminder-item" style="padding:6px 0">
<div class="checkbox"></div>
<div class="reminder-text"><div class="reminder-title-r" style="font-weight:500;font-size:14px">Тетради для Алины (3 шт)</div></div>
</div>
</div>
</div>
<!-- CALENDAR -->
<div class="content hidden" data-tab="calendar">
<div class="header"><h1><span class="emoji">📅</span>Календарь</h1><div class="avatar">М</div></div>
<div class="calendar-strip" style="padding: 0 4px 16px">
<div class="day"><div class="dow">Пт</div><div class="num">22</div><div class="dot"></div></div>
<div class="day active"><div class="dow">Сб</div><div class="num">23</div><div class="dot"></div></div>
<div class="day"><div class="dow">Вс</div><div class="num">24</div></div>
<div class="day"><div class="dow">Пн</div><div class="num">25</div><div class="dot"></div></div>
<div class="day"><div class="dow">Вт</div><div class="num">26</div></div>
<div class="day"><div class="dow">Ср</div><div class="num">27</div><div class="dot"></div></div>
<div class="day"><div class="dow">Чт</div><div class="num">28</div><div class="dot"></div></div>
</div>
<div class="chip-row">
<div class="chip active">Все</div><div class="chip">Медицина</div><div class="chip">Школа</div><div class="chip">Работа</div><div class="chip">Личное</div>
</div>
<div class="event-item" style="border-left: 4px solid var(--urgent)">
<div class="doc-thumb icon-medical">💉</div>
<div class="item-main"><div class="item-title">Стоматолог — Алина</div><div class="item-sub">Сегодня · 15:00–15:45 · клиника «Улыбка»</div></div>
</div>
<div class="event-item" style="border-left: 4px solid var(--blue)">
<div class="doc-thumb icon-school">📚</div>
<div class="item-main"><div class="item-title">Родительское собрание</div><div class="item-sub">Сб 23 мая · 18:00 · школа №42</div></div>
</div>
<div class="event-item" style="border-left: 4px solid var(--accent)">
<div class="doc-thumb icon-personal">🧘</div>
<div class="item-main"><div class="item-title">Йога — мама</div><div class="item-sub">Пн 25 мая · 09:00</div></div>
</div>
<div class="event-item" style="border-left: 4px solid var(--beige)">
<div class="doc-thumb icon-finance">💼</div>
<div class="item-main"><div class="item-title">Зарплата папы</div><div class="item-sub">Ср 27 мая</div></div>
</div>
<div class="event-item" style="border-left: 4px solid #f3d6ff">
<div class="doc-thumb icon-birthday">🎂</div>
<div class="item-main"><div class="item-title">Бабушка Айгуль — ДР</div><div class="item-sub">Чт 28 мая</div></div>
</div>
</div>
<!-- MEDICAL -->
<div class="content hidden" data-tab="medical">
<div class="header"><h1><span class="emoji">💊</span>Медицина</h1><div class="avatar">М</div></div>
<div class="chip-row"><div class="chip active">Все</div><div class="chip">Мама</div><div class="chip">Папа</div><div class="chip">Алина</div><div class="chip">Тимур</div></div>
<div class="widget">
<div class="widget-title">💊 Лекарства <span class="count">2</span></div>
<div class="reminder-item">
<div class="reminder-icon icon-medical">🟠</div>
<div class="reminder-text"><div class="reminder-title-r">Парацетамол · Тимур</div><div class="reminder-meta">Закончится через 2 дня · купить!</div></div>
<span class="badge-urgent">low</span>
</div>
<div class="reminder-item">
<div class="reminder-icon icon-medical">💊</div>
<div class="reminder-text"><div class="reminder-title-r">Витамин D · все дети</div><div class="reminder-meta">Каждый день 09:00</div></div>
</div>
</div>
<div class="widget">
<div class="widget-title">🩺 История визитов</div>
<div class="reminder-item">
<div class="reminder-icon icon-medical">👶</div>
<div class="reminder-text"><div class="reminder-title-r">Педиатр · Тимур</div><div class="reminder-meta">05.05.2026 · ОРВИ, выписан</div></div>
</div>
<div class="reminder-item">
<div class="reminder-icon icon-medical">🦷</div>
<div class="reminder-text"><div class="reminder-title-r">Стоматолог · Алина</div><div class="reminder-meta">18.04.2026 · профилактика</div></div>
</div>
<div class="reminder-item">
<div class="reminder-icon icon-medical">🧪</div>
<div class="reminder-text"><div class="reminder-title-r">Анализ крови · мама</div><div class="reminder-meta">02.04.2026 · норма</div></div>
</div>
</div>
<div class="widget">
<div class="widget-title">💉 Вакцинация</div>
<div class="reminder-item">
<div class="reminder-icon icon-medical">💉</div>
<div class="reminder-text"><div class="reminder-title-r">КПК · Тимур</div><div class="reminder-meta">След. прививка через 4 мес</div></div>
</div>
</div>
</div>
<!-- DOCS -->
<div class="content hidden" data-tab="docs">
<div class="header"><h1><span class="emoji">📂</span>Документы</h1><div class="avatar">М</div></div>
<div class="ai-input" style="background: white; box-shadow: var(--shadow); margin-bottom: 12px; padding: 10px 14px">
<span>🔍</span><span style="color: var(--muted)">Поиск по тексту документа...</span>
</div>
<div class="chip-row"><div class="chip active">Все</div><div class="chip">Удостоверения</div><div class="chip">Страховки</div><div class="chip">Школа</div><div class="chip">Мед</div></div>
<div class="doc-item">
<div class="doc-thumb">🪪</div>
<div class="item-main"><div class="item-title">Удостоверение мамы</div><div class="item-sub">ID · действует до 2031</div></div>
<span class="item-right">✓</span>
</div>
<div class="doc-item">
<div class="doc-thumb">📄</div>
<div class="item-main"><div class="item-title">Свидетельство Алины</div><div class="item-sub">Школа · OCR ✓</div></div>
<span class="item-right">✓</span>
</div>
<div class="doc-item">
<div class="doc-thumb">🚗</div>
<div class="item-main"><div class="item-title">Тех. паспорт авто</div><div class="item-sub">Страховка истекает через 12 дней</div></div>
<span class="item-right urgent">⏰</span>
</div>
<div class="doc-item">
<div class="doc-thumb">🏥</div>
<div class="item-main"><div class="item-title">Полис ДМС</div><div class="item-sub">Действует до 12.08.2026</div></div>
<span class="item-right">✓</span>
</div>
<div class="doc-item">
<div class="doc-thumb">📋</div>
<div class="item-main"><div class="item-title">Справка из садика</div><div class="item-sub">Тимур · 15.04.2026</div></div>
<span class="item-right">✓</span>
</div>
</div>
<!-- FINANCE -->
<div class="content hidden" data-tab="finance">
<div class="header"><h1><span class="emoji">💰</span>Финансы</h1><div class="avatar">М</div></div>
<div class="widget" style="background: linear-gradient(135deg, var(--beige), #f5e8c8)">
<div class="widget-title">К оплате в этом месяце</div>
<div style="font-size: 28px; font-weight: 700; margin-bottom: 4px">68 200 ₸</div>
<div style="font-size: 12px; color: var(--muted)">из них просрочено: <b style="color: var(--urgent)">0 ₸</b></div>
</div>
<div class="pay-item">
<div class="doc-thumb icon-finance">💡</div>
<div class="item-main"><div class="item-title">Коммуналка</div><div class="item-sub">До 23 мая</div></div>
<span class="item-right urgent">18 400 ₸</span>
</div>
<div class="pay-item">
<div class="doc-thumb icon-finance">📡</div>
<div class="item-main"><div class="item-title">Интернет (Beeline)</div><div class="item-sub">До 25 мая · авто</div></div>
<span class="item-right">5 990 ₸</span>
</div>
<div class="pay-item">
<div class="doc-thumb icon-finance">🎒</div>
<div class="item-main"><div class="item-title">Кружок Алины (танцы)</div><div class="item-sub">До 30 мая</div></div>
<span class="item-right">12 000 ₸</span>
</div>
<div class="pay-item">
<div class="doc-thumb icon-finance">🏦</div>
<div class="item-main"><div class="item-title">Ипотека</div><div class="item-sub">До 5 июня · авто</div></div>
<span class="item-right">28 500 ₸</span>
</div>
<div class="pay-item">
<div class="doc-thumb icon-finance">🎬</div>
<div class="item-main"><div class="item-title">Подписки (Netflix, Spotify)</div><div class="item-sub">До 10 июня · авто</div></div>
<span class="item-right">3 310 ₸</span>
</div>
</div>
<!-- NOTES -->
<div class="content hidden" data-tab="notes">
<div class="header"><h1><span class="emoji">📝</span>Заметки</h1><div class="avatar">М</div></div>
<div class="chip-row"><div class="chip active">Все</div><div class="chip">Личные</div><div class="chip">Общие</div><div class="chip">Голос</div></div>
<div class="note-item">
<div class="doc-thumb">📝</div>
<div class="item-main"><div class="item-title">Идеи на подарок бабушке</div><div class="item-sub">3 идеи · обновлено вчера</div></div>
</div>
<div class="note-item">
<div class="doc-thumb">🎙</div>
<div class="item-main"><div class="item-title">Голос: «спросить у врача»</div><div class="item-sub">0:42 · сегодня 08:15</div></div>
</div>
<div class="note-item">
<div class="doc-thumb">📋</div>
<div class="item-main"><div class="item-title">Чек-лист на дачу</div><div class="item-sub">12 пунктов · 4 готово</div></div>
</div>
<div class="note-item">
<div class="doc-thumb">🍳</div>
<div class="item-main"><div class="item-title">Рецепт пирога от мамы</div><div class="item-sub">Семейная</div></div>
</div>
</div>
<!-- SHOPPING -->
<div class="content hidden" data-tab="shopping">
<div class="header"><h1><span class="emoji">🛒</span>Покупки</h1><div class="avatar">М</div></div>
<div class="widget">
<div class="widget-title">Список на эту неделю</div>
<div class="reminder-item"><div class="checkbox"></div><div class="reminder-text"><div class="reminder-title-r" style="font-weight:500">Молоко 2.5% · 2 л</div></div></div>
<div class="reminder-item"><div class="checkbox"></div><div class="reminder-text"><div class="reminder-title-r" style="font-weight:500">Хлеб серый</div></div></div>
<div class="reminder-item"><div class="checkbox checked">✓</div><div class="reminder-text"><div class="reminder-title-r checked-text" style="font-weight:500">Яблоки</div></div></div>
<div class="reminder-item"><div class="checkbox"></div><div class="reminder-text"><div class="reminder-title-r" style="font-weight:500">Парацетамол детский</div></div></div>
<div class="reminder-item"><div class="checkbox"></div><div class="reminder-text"><div class="reminder-title-r" style="font-weight:500">Тетради 12 листов × 3</div></div></div>
<div class="reminder-item"><div class="checkbox checked">✓</div><div class="reminder-text"><div class="reminder-title-r checked-text" style="font-weight:500">Шампунь</div></div></div>
<div class="reminder-item"><div class="checkbox"></div><div class="reminder-text"><div class="reminder-title-r" style="font-weight:500">Корм коту 1кг</div></div></div>
</div>
</div>
<!-- SCHOOL -->
<div class="content hidden" data-tab="school">
<div class="header"><h1><span class="emoji">🎒</span>Школа и сад</h1><div class="avatar">М</div></div>
<div class="chip-row"><div class="chip active">Алина (3 кл)</div><div class="chip">Тимур (сад)</div></div>
<div class="widget">
<div class="widget-title">📚 Расписание · Пятница</div>
<div class="reminder-item"><div class="reminder-icon icon-school">1</div><div class="reminder-text"><div class="reminder-title-r" style="font-size:14px">Математика</div><div class="reminder-meta">08:30–09:15</div></div></div>
<div class="reminder-item"><div class="reminder-icon icon-school">2</div><div class="reminder-text"><div class="reminder-title-r" style="font-size:14px">Русский язык</div><div class="reminder-meta">09:25–10:10</div></div></div>
<div class="reminder-item"><div class="reminder-icon icon-school">3</div><div class="reminder-text"><div class="reminder-title-r" style="font-size:14px">Физкультура</div><div class="reminder-meta">10:20–11:05 · форма!</div></div></div>
</div>
<div class="widget">
<div class="widget-title">📝 Домашка</div>
<div class="reminder-item"><div class="checkbox"></div><div class="reminder-text"><div class="reminder-title-r" style="font-size:14px">Мат. с.34 №5,6</div><div class="reminder-meta">К понедельнику</div></div></div>
<div class="reminder-item"><div class="checkbox"></div><div class="reminder-text"><div class="reminder-title-r" style="font-size:14px">Рус. упр. 142</div><div class="reminder-meta">К понедельнику</div></div></div>
</div>
<div class="widget" style="background: linear-gradient(135deg, var(--accent), var(--accent-dark)); color: white">
<div class="widget-title" style="color: rgba(255,255,255,0.85)">✨ AI · Сгенерировать заявление</div>
<div style="font-size: 13px; margin: 8px 0; opacity: 0.95">По болезни · отпуск · освобождение от физкультуры</div>
<div class="ai-input" style="background: rgba(255,255,255,0.2)">
<span>📄</span><span>Создать за 5 секунд</span>
</div>
</div>
</div>
<!-- BIRTHDAY -->
<div class="content hidden" data-tab="bday">
<div class="header"><h1><span class="emoji">🎂</span>Дни рождения</h1><div class="avatar">М</div></div>
<div class="bday-item" style="background: linear-gradient(135deg, #f3d6ff, #ffe1d6)">
<div class="doc-thumb" style="background: white">👵</div>
<div class="item-main"><div class="item-title">Бабушка Айгуль</div><div class="item-sub">28 мая · через 6 дней · 67 лет</div></div>
<span class="item-right">🎁</span>
</div>
<div class="bday-item">
<div class="doc-thumb icon-birthday">👨</div>
<div class="item-main"><div class="item-title">Папа · Айбек</div><div class="item-sub">12 июня · через 21 день</div></div>
</div>
<div class="bday-item">
<div class="doc-thumb icon-birthday">👧</div>
<div class="item-main"><div class="item-title">Алина</div><div class="item-sub">3 июля · через 42 дня · 9 лет</div></div>
</div>
<div class="bday-item">
<div class="doc-thumb icon-birthday">👶</div>
<div class="item-main"><div class="item-title">Тимур</div><div class="item-sub">18 сентября · через 119 дней</div></div>
</div>
<div class="widget" style="margin-top: 12px">
<div class="widget-title">💡 Идеи подарков · Бабушка</div>
<div class="reminder-item"><div class="reminder-icon icon-birthday">🧣</div><div class="reminder-text"><div class="reminder-title-r" style="font-size:14px">Тёплый плед</div></div></div>
<div class="reminder-item"><div class="reminder-icon icon-birthday">📚</div><div class="reminder-text"><div class="reminder-title-r" style="font-size:14px">Книга «Сад на подоконнике»</div></div></div>
<div class="reminder-item"><div class="reminder-icon icon-birthday">🍰</div><div class="reminder-text"><div class="reminder-title-r" style="font-size:14px">Торт «Медовик» от Алины</div></div></div>
</div>
</div>
<!-- AI CHAT -->
<div class="content hidden" data-tab="ai">
<div class="header"><h1><span class="emoji">🤖</span>AI-помощник</h1><div class="avatar">М</div></div>
<div style="padding: 8px 4px">
<div class="ai-chat-bubble bot">Доброе утро, Зауреш! Я просмотрела семейный календарь — сегодня важных 3 дела.</div>
<div class="ai-chat-bubble user">Напомни записать Алину к стоматологу в пятницу в 15:00</div>
<div class="ai-chat-bubble bot">
✅ Создала напоминание:<br>
<b>«Записать Алину к стоматологу»</b><br>
📅 Пт 22 мая · 15:00<br>
🏥 категория: медицина<br>
<div class="ai-suggestion">Добавить в календарь?</div>
</div>
<div class="ai-chat-bubble user">Составь список покупок на неделю</div>
<div class="ai-chat-bubble bot">
По истории покупок и тому что у вас заканчивается:<br><br>
🥛 молоко · 🍞 хлеб · 🍎 яблоки<br>
💊 парацетамол детский · 📓 тетради ×3<br>
🐱 корм коту · 🧴 шампунь<br><br>
<div class="ai-suggestion">Добавить в «Покупки»</div>
</div>
<div class="ai-chat-bubble user">У Тимура температура 38</div>
<div class="ai-chat-bubble bot">
🌡 Понимаю. Включаю сценарий «ребёнок заболел»:<br><br>
⏰ напомню дать парацетамол через 4 часа<br>
📞 нашла педиатра в истории — записать?<br>
📋 готова справка-заявление для садика
<div class="ai-suggestion">Записать к врачу</div>
<div class="ai-suggestion">Создать заявление в сад</div>
</div>
</div>
</div>
<div class="fab" onclick="showToast('+ Новое напоминание')">+</div>
<div class="toast" id="toast">Готово</div>
<div class="nav">
<div class="nav-item active" data-nav="home" onclick="switchTab('home')"><div class="nav-icon">🏠</div><span>Главная</span></div>
<div class="nav-item" data-nav="calendar" onclick="switchTab('calendar')"><div class="nav-icon">📅</div><span>Календарь</span></div>
<div class="nav-item" data-nav="ai" onclick="switchTab('ai')"><div class="nav-icon">🤖</div><span>AI</span></div>
<div class="nav-item" data-nav="notes" onclick="switchTab('notes')"><div class="nav-icon">📝</div><span>Заметки</span></div>
<div class="nav-item" data-nav="more" onclick="switchTab('home')"><div class="nav-icon">≡</div><span>Ещё</span></div>
</div>
<div class="home-indicator"></div>
</div>
</div>
<div class="screen-label">Family Life Assistant · MVP mock</div>
</div>
<div class="legend">
<h2>🏡 Что показано</h2>
<p>Кликабельный прототип всех экранов MVP по PRD/TRD. Открой <kbd>index.html</kbd> в браузере.</p>
<h3>Экраны</h3>
<ul>
<li>🏠 Главная — виджеты «сегодня»</li>
<li>📅 Календарь — события по цветам ролей</li>
<li>💊 Медицина — визиты, лекарства, low-stock</li>
<li>📂 Документы — OCR, expiry-напоминания</li>
<li>💰 Финансы — платежи, авто-списания</li>
<li>📝 Заметки и 🛒 Покупки</li>
<li>🎒 Школа — расписание + AI заявления</li>
<li>🎂 Дни рождения + идеи подарков</li>
<li>🤖 AI-помощник — parseReminder, сценарии</li>
</ul>
<h3>Кликабельно</h3>
<ul>
<li>Нижняя навигация</li>
<li>Quick-grid на главной</li>
<li>AI-карточка → AI чат</li>
<li>FAB (+) → toast</li>
</ul>
<h3>Дальше</h3>
<p>Если зайдёт визуал — перенести в Flutter (Riverpod + Material 3, как в TRD §3.1). Цвета и компоненты уже подобраны по PRD §12.</p>
</div>
</div>
<script>
function switchTab(name) {
document.querySelectorAll('[data-tab]').forEach(el => el.classList.add('hidden'));
const target = document.querySelector(`[data-tab="${name}"]`);
if (target) target.classList.remove('hidden');
document.querySelectorAll('[data-nav]').forEach(el => el.classList.remove('active'));
const navMap = { home: 'home', calendar: 'calendar', ai: 'ai', notes: 'notes' };
const navTarget = document.querySelector(`[data-nav="${navMap[name] || 'more'}"]`);
if (navTarget) navTarget.classList.add('active');
document.querySelector(`[data-tab="${name}"]`).scrollTop = 0;
}
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 1800);
}
document.querySelectorAll('.checkbox').forEach(cb => {
cb.addEventListener('click', e => {
cb.classList.toggle('checked');
cb.textContent = cb.classList.contains('checked') ? '✓' : '';
const txt = cb.parentElement.querySelector('.reminder-title-r');
if (txt) txt.classList.toggle('checked-text');
});
});
document.querySelectorAll('.chip').forEach(ch => {
ch.addEventListener('click', e => {
ch.parentElement.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
ch.classList.add('active');
});
});
document.querySelectorAll('.day').forEach(d => {
d.addEventListener('click', e => {
d.parentElement.querySelectorAll('.day').forEach(x => x.classList.remove('active'));
d.classList.add('active');
});
});
document.querySelectorAll('.ai-suggestion').forEach(s => {
s.addEventListener('click', () => showToast('✓ ' + s.textContent));
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff