Compare commits
No commits in common. "main" and "gitea-pages" have entirely different histories.
main
...
gitea-page
33
.gitignore
vendored
33
.gitignore
vendored
@ -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/
|
|
||||||
321
PRD.md
321
PRD.md
@ -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. Главная ценность продукта
|
|
||||||
|
|
||||||
«Приложение, которое помнит о семье вместо тебя.»
|
|
||||||
|
|
||||||
---
|
|
||||||
26
README.md
26
README.md
@ -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
752
TRD.md
@ -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
|
|
||||||
@ -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 можно было собрать и пощёлкать без бэкенда.
|
|
||||||
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -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),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@ -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 [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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]';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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')),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 {}
|
|
||||||
}
|
|
||||||
@ -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(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 {}
|
|
||||||
}
|
|
||||||
@ -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)',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 {}
|
|
||||||
}
|
|
||||||
@ -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)}'),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
@ -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(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 {}
|
|
||||||
}
|
|
||||||
@ -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',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 {}
|
|
||||||
}
|
|
||||||
@ -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!)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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]';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()));
|
|
||||||
}
|
|
||||||
@ -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()),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
@ -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
|
|
||||||
Reference in New Issue
Block a user