Compare commits

..

No commits in common. "main" and "gitea-pages" have entirely different histories.

35 changed files with 0 additions and 2469 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