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