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