Initial commit: NANAHUI CRM - Flutter Firebase CRM assistant
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/task_model.dart';
|
||||
|
||||
class AIService {
|
||||
final String? openaiApiKey;
|
||||
final String? anthropicApiKey;
|
||||
final String? googleAiApiKey;
|
||||
|
||||
AIService({
|
||||
this.openaiApiKey,
|
||||
this.anthropicApiKey,
|
||||
this.googleAiApiKey,
|
||||
});
|
||||
|
||||
// Генерация подзадач из основной задачи
|
||||
Future<List<Map<String, dynamic>>> generateSubtasks({
|
||||
required String title,
|
||||
required String description,
|
||||
}) async {
|
||||
if (openaiApiKey == null) {
|
||||
return _mockGenerateSubtasks(title, description);
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('https://api.openai.com/v1/chat/completions'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $openaiApiKey',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'model': 'gpt-4o-mini',
|
||||
'messages': [
|
||||
{
|
||||
'role': 'system',
|
||||
'content': 'You are a task planning assistant. Break down tasks into logical subtasks.',
|
||||
},
|
||||
{
|
||||
'role': 'user',
|
||||
'content': 'Break down this task into 3-7 subtasks:\n\nTitle: $title\nDescription: $description\n\nReturn ONLY a JSON array of subtasks with format: [{"title": "...", "estimatedHours": 0}]',
|
||||
},
|
||||
],
|
||||
'temperature': 0.7,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
final content = data['choices'][0]['message']['content'];
|
||||
final List<dynamic> subtasks = jsonDecode(content);
|
||||
return subtasks.cast<Map<String, dynamic>>();
|
||||
}
|
||||
} catch (e) {
|
||||
print('AI Service Error: $e');
|
||||
}
|
||||
|
||||
return _mockGenerateSubtasks(title, description);
|
||||
}
|
||||
|
||||
// AI приоритизация задач
|
||||
Future<TaskPriority> prioritizeTask({
|
||||
required String title,
|
||||
required String description,
|
||||
DateTime? dueDate,
|
||||
}) async {
|
||||
if (openaiApiKey == null) {
|
||||
return _mockPrioritizeTask(dueDate);
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('https://api.openai.com/v1/chat/completions'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $openaiApiKey',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'model': 'gpt-4o-mini',
|
||||
'messages': [
|
||||
{
|
||||
'role': 'system',
|
||||
'content': 'You are a task prioritization assistant. Determine task priority: low, medium, high, or urgent.',
|
||||
},
|
||||
{
|
||||
'role': 'user',
|
||||
'content': 'What priority should this task have?\n\nTitle: $title\nDescription: $description\nDue: ${dueDate ?? "Not specified"}\n\nReply with ONE word: low, medium, high, or urgent',
|
||||
},
|
||||
],
|
||||
'temperature': 0.3,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
final content = data['choices'][0]['message']['content'].trim().toLowerCase();
|
||||
|
||||
switch (content) {
|
||||
case 'urgent': return TaskPriority.urgent;
|
||||
case 'high': return TaskPriority.high;
|
||||
case 'low': return TaskPriority.low;
|
||||
default: return TaskPriority.medium;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('AI Service Error: $e');
|
||||
}
|
||||
|
||||
return _mockPrioritizeTask(dueDate);
|
||||
}
|
||||
|
||||
// Умное планирование - распределение задач по времени
|
||||
Future<List<Map<String, dynamic>>> smartSchedule({
|
||||
required List<TaskModel> tasks,
|
||||
required DateTime startDate,
|
||||
required DateTime endDate,
|
||||
required int availableHoursPerDay,
|
||||
}) async {
|
||||
// Mock implementation для начала
|
||||
final schedule = <Map<String, dynamic>>[];
|
||||
var currentDate = startDate;
|
||||
var remainingHours = availableHoursPerDay;
|
||||
|
||||
for (final task in tasks) {
|
||||
if (currentDate.isAfter(endDate)) break;
|
||||
|
||||
if (task.estimatedHours <= remainingHours) {
|
||||
schedule.add({
|
||||
'taskId': task.id,
|
||||
'date': currentDate,
|
||||
'hours': task.estimatedHours,
|
||||
});
|
||||
remainingHours -= task.estimatedHours;
|
||||
} else {
|
||||
// Переходим на следующий день
|
||||
currentDate = currentDate.add(const Duration(days: 1));
|
||||
remainingHours = availableHoursPerDay;
|
||||
|
||||
if (currentDate.isAfter(endDate)) break;
|
||||
|
||||
schedule.add({
|
||||
'taskId': task.id,
|
||||
'date': currentDate,
|
||||
'hours': task.estimatedHours > remainingHours
|
||||
? remainingHours
|
||||
: task.estimatedHours,
|
||||
});
|
||||
remainingHours -= task.estimatedHours > remainingHours
|
||||
? remainingHours
|
||||
: task.estimatedHours;
|
||||
}
|
||||
}
|
||||
|
||||
return schedule;
|
||||
}
|
||||
|
||||
// Генерация описания задачи
|
||||
Future<String> generateTaskDescription({
|
||||
required String title,
|
||||
String? context,
|
||||
}) async {
|
||||
if (openaiApiKey == null) {
|
||||
return 'Описание для задачи: $title';
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('https://api.openai.com/v1/chat/completions'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $openaiApiKey',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'model': 'gpt-4o-mini',
|
||||
'messages': [
|
||||
{
|
||||
'role': 'system',
|
||||
'content': 'You are a task description writer. Write clear, concise task descriptions.',
|
||||
},
|
||||
{
|
||||
'role': 'user',
|
||||
'content': 'Write a brief description for this task:\n\nTitle: $title\nContext: ${context ?? "No additional context"}\n\nKeep it under 100 words.',
|
||||
},
|
||||
],
|
||||
'temperature': 0.5,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
return data['choices'][0]['message']['content'];
|
||||
}
|
||||
} catch (e) {
|
||||
print('AI Service Error: $e');
|
||||
}
|
||||
|
||||
return 'Описание для задачи: $title';
|
||||
}
|
||||
|
||||
// Mock данные для тестирования
|
||||
List<Map<String, dynamic>> _mockGenerateSubtasks(String title, String description) {
|
||||
return [
|
||||
{'title': 'Анализ требований', 'estimatedHours': 2},
|
||||
{'title': 'Проектирование', 'estimatedHours': 3},
|
||||
{'title': 'Реализация', 'estimatedHours': 8},
|
||||
{'title': 'Тестирование', 'estimatedHours': 3},
|
||||
{'title': 'Документирование', 'estimatedHours': 2},
|
||||
];
|
||||
}
|
||||
|
||||
TaskPriority _mockPrioritizeTask(DateTime? dueDate) {
|
||||
if (dueDate == null) return TaskPriority.medium;
|
||||
|
||||
final daysUntilDue = dueDate.difference(DateTime.now()).inDays;
|
||||
|
||||
if (daysUntilDue <= 1) return TaskPriority.urgent;
|
||||
if (daysUntilDue <= 3) return TaskPriority.high;
|
||||
if (daysUntilDue <= 7) return TaskPriority.medium;
|
||||
return TaskPriority.low;
|
||||
}
|
||||
}
|
||||
|
||||
final aiService = AIService(
|
||||
// API ключи лучше хранить в .env файле или Firebase Remote Config
|
||||
openaiApiKey: null, // Добавить ключ здесь или через env
|
||||
anthropicApiKey: null,
|
||||
googleAiApiKey: null,
|
||||
);
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class FirebaseConfig {
|
||||
static Future<void> initialize() async {
|
||||
await Firebase.initializeApp();
|
||||
debugPrint('Firebase initialized successfully');
|
||||
}
|
||||
|
||||
static Future<void> setupFirestore() async {
|
||||
// Настройки Firestore для лучшей производительности
|
||||
// Будут применены после инициализации
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import '../models/task_model.dart';
|
||||
import '../models/project_model.dart';
|
||||
import '../models/mind_map_model.dart';
|
||||
import '../models/calendar_event_model.dart';
|
||||
import '../models/alarm_model.dart';
|
||||
import '../models/user_model.dart';
|
||||
|
||||
class FirestoreService {
|
||||
final FirebaseFirestore _db = FirebaseFirestore.instance;
|
||||
|
||||
// === USERS ===
|
||||
Future<void> createUser(UserModel user) async {
|
||||
await _db.collection('users').doc(user.id).set(user.toFirestore());
|
||||
}
|
||||
|
||||
Future<UserModel?> getUser(String userId) async {
|
||||
final doc = await _db.collection('users').doc(userId).get();
|
||||
return doc.exists ? UserModel.fromFirestore(doc) : null;
|
||||
}
|
||||
|
||||
Future<void> updateUser(UserModel user) async {
|
||||
await _db.collection('users').doc(user.id).update(user.toFirestore());
|
||||
}
|
||||
|
||||
// === PROJECTS ===
|
||||
Future<void> createProject(ProjectModel project) async {
|
||||
await _db.collection('projects').doc(project.id).set(project.toFirestore());
|
||||
}
|
||||
|
||||
Future<List<ProjectModel>> getProjectsByTeam(String teamId) async {
|
||||
final snapshot = await _db
|
||||
.collection('projects')
|
||||
.where('teamId', isEqualTo: teamId)
|
||||
.orderBy('createdAt', descending: true)
|
||||
.get();
|
||||
|
||||
return snapshot.docs.map((doc) => ProjectModel.fromFirestore(doc)).toList();
|
||||
}
|
||||
|
||||
Future<List<ProjectModel>> getProjectsByUser(String userId) async {
|
||||
final snapshot = await _db
|
||||
.collection('projects')
|
||||
.where('memberIds', arrayContains: userId)
|
||||
.get();
|
||||
|
||||
return snapshot.docs.map((doc) => ProjectModel.fromFirestore(doc)).toList();
|
||||
}
|
||||
|
||||
// === TASKS ===
|
||||
Future<void> createTask(TaskModel task) async {
|
||||
await _db.collection('tasks').doc(task.id).set(task.toFirestore());
|
||||
}
|
||||
|
||||
Future<List<TaskModel>> getTasksByProject(String projectId) async {
|
||||
final snapshot = await _db
|
||||
.collection('tasks')
|
||||
.where('projectId', isEqualTo: projectId)
|
||||
.orderBy('createdAt', descending: true)
|
||||
.get();
|
||||
|
||||
return snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList();
|
||||
}
|
||||
|
||||
Future<List<TaskModel>> getTasksByAssignee(String assigneeId) async {
|
||||
final snapshot = await _db
|
||||
.collection('tasks')
|
||||
.where('assigneeId', isEqualTo: assigneeId)
|
||||
.where('status', whereIn: ['todo', 'inProgress', 'review'])
|
||||
.get();
|
||||
|
||||
return snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList();
|
||||
}
|
||||
|
||||
Future<void> updateTask(TaskModel task) async {
|
||||
await _db.collection('tasks').doc(task.id).update(task.toFirestore());
|
||||
}
|
||||
|
||||
Future<void> deleteTask(String taskId) async {
|
||||
await _db.collection('tasks').doc(taskId).delete();
|
||||
}
|
||||
|
||||
// Задачи с поддержкой иерархии (для mind-map)
|
||||
Future<List<TaskModel>> getTaskHierarchy(String projectId) async {
|
||||
final snapshot = await _db
|
||||
.collection('tasks')
|
||||
.where('projectId', isEqualTo: projectId)
|
||||
.get();
|
||||
|
||||
return snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList();
|
||||
}
|
||||
|
||||
// === MIND MAPS ===
|
||||
Future<void> createMindMapNode(MindMapNode node) async {
|
||||
await _db.collection('mindmaps').doc(node.projectId).collection('nodes').doc(node.id).set(node.toFirestore());
|
||||
}
|
||||
|
||||
Future<List<MindMapNode>> getMindMapNodes(String projectId) async {
|
||||
final snapshot = await _db
|
||||
.collection('mindmaps')
|
||||
.doc(projectId)
|
||||
.collection('nodes')
|
||||
.orderBy('level')
|
||||
.get();
|
||||
|
||||
return snapshot.docs.map((doc) => MindMapNode.fromFirestore(doc)).toList();
|
||||
}
|
||||
|
||||
Future<void> updateMindMapNode(MindMapNode node) async {
|
||||
await _db
|
||||
.collection('mindmaps')
|
||||
.doc(node.projectId)
|
||||
.collection('nodes')
|
||||
.doc(node.id)
|
||||
.update(node.toFirestore());
|
||||
}
|
||||
|
||||
// === CALENDAR EVENTS ===
|
||||
Future<void> createEvent(CalendarEventModel event) async {
|
||||
await _db.collection('events').doc(event.id).set(event.toFirestore());
|
||||
}
|
||||
|
||||
Future<List<CalendarEventModel>> getEventsByDateRange({
|
||||
required DateTime start,
|
||||
required DateTime end,
|
||||
String? userId,
|
||||
}) async {
|
||||
Query query = _db
|
||||
.collection('events')
|
||||
.where('startTime', isLessThanOrEqualTo: Timestamp.fromDate(end))
|
||||
.where('endTime', isGreaterThanOrEqualTo: Timestamp.fromDate(start));
|
||||
|
||||
if (userId != null) {
|
||||
query = query.where('attendeeIds', arrayContains: userId);
|
||||
}
|
||||
|
||||
final snapshot = await query.get();
|
||||
return snapshot.docs.map((doc) => CalendarEventModel.fromFirestore(doc)).toList();
|
||||
}
|
||||
|
||||
Future<void> updateEvent(CalendarEventModel event) async {
|
||||
await _db.collection('events').doc(event.id).update(event.toFirestore());
|
||||
}
|
||||
|
||||
Future<void> deleteEvent(String eventId) async {
|
||||
await _db.collection('events').doc(eventId).delete();
|
||||
}
|
||||
|
||||
// === ALARMS ===
|
||||
Future<void> createAlarm(AlarmModel alarm) async {
|
||||
await _db.collection('alarms').doc(alarm.id).set(alarm.toFirestore());
|
||||
}
|
||||
|
||||
Future<List<AlarmModel>> getAlarmsByUser(String userId) async {
|
||||
final snapshot = await _db
|
||||
.collection('alarms')
|
||||
.where('isEnabled', isEqualTo: true)
|
||||
.orderBy('dateTime')
|
||||
.get();
|
||||
|
||||
return snapshot.docs.map((doc) => AlarmModel.fromFirestore(doc)).toList();
|
||||
}
|
||||
|
||||
Future<void> updateAlarm(AlarmModel alarm) async {
|
||||
await _db.collection('alarms').doc(alarm.id).update(alarm.toFirestore());
|
||||
}
|
||||
|
||||
Future<void> deleteAlarm(String alarmId) async {
|
||||
await _db.collection('alarms').doc(alarmId).delete();
|
||||
}
|
||||
|
||||
// === BATCH OPERATIONS ===
|
||||
Future<void> syncTasks(List<TaskModel> tasks) async {
|
||||
final batch = _db.batch();
|
||||
|
||||
for (final task in tasks) {
|
||||
batch.set(_db.collection('tasks').doc(task.id), task.toFirestore());
|
||||
}
|
||||
|
||||
await batch.commit();
|
||||
}
|
||||
|
||||
// === STREAMS (для реального времени) ===
|
||||
Stream<List<TaskModel>> watchTasks(String projectId) {
|
||||
return _db
|
||||
.collection('tasks')
|
||||
.where('projectId', isEqualTo: projectId)
|
||||
.snapshots()
|
||||
.map((snapshot) => snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList());
|
||||
}
|
||||
|
||||
Stream<List<CalendarEventModel>> watchEvents(String userId) {
|
||||
return _db
|
||||
.collection('events')
|
||||
.where('attendeeIds', arrayContains: userId)
|
||||
.snapshots()
|
||||
.map((snapshot) => snapshot.docs.map((doc) => CalendarEventModel.fromFirestore(doc)).toList());
|
||||
}
|
||||
}
|
||||
|
||||
final firestoreService = FirestoreService();
|
||||
@@ -0,0 +1,138 @@
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import '../models/task_model.dart';
|
||||
import '../models/alarm_model.dart';
|
||||
import '../models/mind_map_model.dart';
|
||||
import '../models/calendar_event_model.dart';
|
||||
|
||||
class LocalStorageService {
|
||||
static const String _tasksBoxName = 'tasks';
|
||||
static const String _alarmsBoxName = 'alarms';
|
||||
static const String _mindMapsBoxName = 'mindMaps';
|
||||
static const String _eventsBoxName = 'events';
|
||||
static const String _settingsBoxName = 'settings';
|
||||
|
||||
late Box<Map> _tasksBox;
|
||||
late Box<Map> _alarmsBox;
|
||||
late Box<Map> _mindMapsBox;
|
||||
late Box<Map> _eventsBox;
|
||||
late Box<Object?> _settingsBox;
|
||||
|
||||
Future<void> initialize() async {
|
||||
await Hive.initFlutter();
|
||||
|
||||
_tasksBox = await Hive.openBox<Map>(_tasksBoxName);
|
||||
_alarmsBox = await Hive.openBox<Map>(_alarmsBoxName);
|
||||
_mindMapsBox = await Hive.openBox<Map>(_mindMapsBoxName);
|
||||
_eventsBox = await Hive.openBox<Map>(_eventsBoxName);
|
||||
_settingsBox = await Hive.openBox<Object?>(_settingsBoxName);
|
||||
}
|
||||
|
||||
// Tasks
|
||||
Future<void> saveTask(TaskModel task) async {
|
||||
await _tasksBox.put(task.id, task.toFirestore());
|
||||
}
|
||||
|
||||
Future<void> saveTasks(List<TaskModel> tasks) async {
|
||||
for (final task in tasks) {
|
||||
await _tasksBox.put(task.id, task.toFirestore());
|
||||
}
|
||||
}
|
||||
|
||||
List<TaskModel> getTasks() {
|
||||
return _tasksBox.values.map((data) {
|
||||
return TaskModel.fromFirestore(
|
||||
_createMockDocument(data as Map<String, dynamic>),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Future<void> deleteTask(String taskId) async {
|
||||
await _tasksBox.delete(taskId);
|
||||
}
|
||||
|
||||
// Alarms
|
||||
Future<void> saveAlarm(AlarmModel alarm) async {
|
||||
await _alarmsBox.put(alarm.id, alarm.toFirestore());
|
||||
}
|
||||
|
||||
List<AlarmModel> getAlarms() {
|
||||
return _alarmsBox.values.map((data) {
|
||||
return AlarmModel.fromFirestore(
|
||||
_createMockDocument(data as Map<String, dynamic>),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Future<void> deleteAlarm(String alarmId) async {
|
||||
await _alarmsBox.delete(alarmId);
|
||||
}
|
||||
|
||||
// Mind Maps
|
||||
Future<void> saveMindMapNodes(List<MindMapNode> nodes) async {
|
||||
for (final node in nodes) {
|
||||
await _mindMapsBox.put(node.id, node.toFirestore());
|
||||
}
|
||||
}
|
||||
|
||||
List<MindMapNode> getMindMapNodes() {
|
||||
return _mindMapsBox.values.map((data) {
|
||||
return MindMapNode.fromFirestore(
|
||||
_createMockDocument(data as Map<String, dynamic>),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Events
|
||||
Future<void> saveEvent(CalendarEventModel event) async {
|
||||
await _eventsBox.put(event.id, event.toFirestore());
|
||||
}
|
||||
|
||||
List<CalendarEventModel> getEvents() {
|
||||
return _eventsBox.values.map((data) {
|
||||
return CalendarEventModel.fromFirestore(
|
||||
_createMockDocument(data as Map<String, dynamic>),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Settings
|
||||
Future<void> saveSetting(String key, Object? value) async {
|
||||
await _settingsBox.put(key, value);
|
||||
}
|
||||
|
||||
T? getSetting<T>(String key, {T? defaultValue}) {
|
||||
return _settingsBox.get(key, defaultValue: defaultValue) as T?;
|
||||
}
|
||||
|
||||
// Clear all local data
|
||||
Future<void> clearAll() async {
|
||||
await _tasksBox.clear();
|
||||
await _alarmsBox.clear();
|
||||
await _mindMapsBox.clear();
|
||||
await _eventsBox.clear();
|
||||
await _settingsBox.clear();
|
||||
}
|
||||
|
||||
// Sync with Firebase (merge remote and local)
|
||||
Future<void> syncWithRemote(List<TaskModel> remoteTasks) async {
|
||||
final localTasks = getTasks();
|
||||
final localMap = {for (var t in localTasks) t.id: t};
|
||||
final remoteMap = {for (var t in remoteTasks) t.id: t};
|
||||
|
||||
// Merge: remote takes precedence
|
||||
final merged = {...localMap, ...remoteMap};
|
||||
|
||||
await _tasksBox.clear();
|
||||
for (final task in merged.values) {
|
||||
await _tasksBox.put(task.id, task.toFirestore());
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to create mock DocumentSnapshot
|
||||
dynamic _createMockDocument(Map<String, dynamic> data) {
|
||||
// Простая эмуляция для конвертации
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
final localStorageService = LocalStorageService();
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../screens/auth_screen.dart';
|
||||
import '../screens/home_screen.dart';
|
||||
import '../screens/tasks_screen.dart';
|
||||
import '../screens/mind_map_screen.dart';
|
||||
import '../screens/calendar_screen.dart';
|
||||
import '../screens/alarms_screen.dart';
|
||||
|
||||
class AppRouter {
|
||||
static final GoRouter router = GoRouter(
|
||||
initialLocation: '/auth',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/auth',
|
||||
name: 'auth',
|
||||
builder: (context, state) => const AuthScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/home',
|
||||
name: 'home',
|
||||
builder: (context, state) => const HomeScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'tasks',
|
||||
name: 'tasks',
|
||||
builder: (context, state) => const TasksScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'mindmap',
|
||||
name: 'mindmap',
|
||||
builder: (context, state) => const MindMapScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'calendar',
|
||||
name: 'calendar',
|
||||
builder: (context, state) => const CalendarScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'alarms',
|
||||
name: 'alarms',
|
||||
builder: (context, state) => const AlarmsScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
static ThemeData get lightTheme {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.blue,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||
elevation: 4,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static ThemeData get darkTheme {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.blue,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||
elevation: 4,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'core/firebase_config.dart';
|
||||
import 'core/local_storage_service.dart';
|
||||
import 'core/theme.dart';
|
||||
import 'core/router.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Инициализация Firebase
|
||||
await FirebaseConfig.initialize();
|
||||
|
||||
// Инициализация локального хранилища
|
||||
await localStorageService.initialize();
|
||||
|
||||
runApp(
|
||||
const ProviderScope(
|
||||
child: NanahuiCrmApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class NanahuiCrmApp extends StatelessWidget {
|
||||
const NanahuiCrmApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
title: 'NANAHUI CRM',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
themeMode: ThemeMode.system,
|
||||
routerConfig: AppRouter.router,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
class AlarmModel {
|
||||
final String id;
|
||||
final String title;
|
||||
final String? taskId;
|
||||
final String? eventId;
|
||||
final DateTime dateTime;
|
||||
final bool isRepeating;
|
||||
final String repeatPattern; // daily, weekly, monthly, custom
|
||||
final List<int> repeatDays; // For custom (e.g., [1, 3, 5] for Mon, Wed, Fri)
|
||||
final bool isEnabled;
|
||||
final String sound;
|
||||
final int snoozeMinutes;
|
||||
final Map<String, dynamic> metadata;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
AlarmModel({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.taskId,
|
||||
this.eventId,
|
||||
required this.dateTime,
|
||||
this.isRepeating = false,
|
||||
this.repeatPattern = 'daily',
|
||||
this.repeatDays = const [],
|
||||
this.isEnabled = true,
|
||||
this.sound = 'default',
|
||||
this.snoozeMinutes = 5,
|
||||
this.metadata = const {},
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory AlarmModel.fromFirestore(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
return AlarmModel(
|
||||
id: doc.id,
|
||||
title: data['title'] ?? '',
|
||||
taskId: data['taskId'],
|
||||
eventId: data['eventId'],
|
||||
dateTime: (data['dateTime'] as Timestamp).toDate(),
|
||||
isRepeating: data['isRepeating'] ?? false,
|
||||
repeatPattern: data['repeatPattern'] ?? 'daily',
|
||||
repeatDays: List<int>.from(data['repeatDays'] ?? []),
|
||||
isEnabled: data['isEnabled'] ?? true,
|
||||
sound: data['sound'] ?? 'default',
|
||||
snoozeMinutes: data['snoozeMinutes'] ?? 5,
|
||||
metadata: data['metadata'] ?? {},
|
||||
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'title': title,
|
||||
'taskId': taskId,
|
||||
'eventId': eventId,
|
||||
'dateTime': Timestamp.fromDate(dateTime),
|
||||
'isRepeating': isRepeating,
|
||||
'repeatPattern': repeatPattern,
|
||||
'repeatDays': repeatDays,
|
||||
'isEnabled': isEnabled,
|
||||
'sound': sound,
|
||||
'snoozeMinutes': snoozeMinutes,
|
||||
'metadata': metadata,
|
||||
'createdAt': Timestamp.fromDate(createdAt),
|
||||
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
AlarmModel copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? taskId,
|
||||
String? eventId,
|
||||
DateTime? dateTime,
|
||||
bool? isRepeating,
|
||||
String? repeatPattern,
|
||||
List<int>? repeatDays,
|
||||
bool? isEnabled,
|
||||
String? sound,
|
||||
int? snoozeMinutes,
|
||||
Map<String, dynamic>? metadata,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return AlarmModel(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
taskId: taskId ?? this.taskId,
|
||||
eventId: eventId ?? this.eventId,
|
||||
dateTime: dateTime ?? this.dateTime,
|
||||
isRepeating: isRepeating ?? this.isRepeating,
|
||||
repeatPattern: repeatPattern ?? this.repeatPattern,
|
||||
repeatDays: repeatDays ?? this.repeatDays,
|
||||
isEnabled: isEnabled ?? this.isEnabled,
|
||||
sound: sound ?? this.sound,
|
||||
snoozeMinutes: snoozeMinutes ?? this.snoozeMinutes,
|
||||
metadata: metadata ?? this.metadata,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
DateTime get nextOccurrence {
|
||||
if (!isRepeating) return dateTime;
|
||||
|
||||
final now = DateTime.now();
|
||||
if (dateTime.isAfter(now)) return dateTime;
|
||||
|
||||
switch (repeatPattern) {
|
||||
case 'daily':
|
||||
return dateTime.add(const Duration(days: 1));
|
||||
case 'weekly':
|
||||
return dateTime.add(const Duration(days: 7));
|
||||
case 'monthly':
|
||||
return DateTime(dateTime.year, dateTime.month + 1, dateTime.day);
|
||||
case 'custom':
|
||||
// Find next weekday in repeatDays
|
||||
var next = dateTime.add(const Duration(days: 1));
|
||||
while (!repeatDays.contains(next.weekday)) {
|
||||
next = next.add(const Duration(days: 1));
|
||||
}
|
||||
return DateTime(next.year, next.month, next.day, dateTime.hour, dateTime.minute);
|
||||
default:
|
||||
return dateTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
class CalendarEventModel {
|
||||
final String id;
|
||||
final String title;
|
||||
final String? description;
|
||||
final String location;
|
||||
final DateTime startTime;
|
||||
final DateTime endTime;
|
||||
final bool isAllDay;
|
||||
final String? googleEventId;
|
||||
final String? taskId;
|
||||
final List<String> attendeeIds;
|
||||
final List<String> attendeeEmails;
|
||||
final String color;
|
||||
final String? recurringRuleId;
|
||||
final Map<String, dynamic> reminders;
|
||||
final String status; // confirmed, tentative, cancelled
|
||||
final String createdBy;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
CalendarEventModel({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.description,
|
||||
this.location = '',
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
this.isAllDay = false,
|
||||
this.googleEventId,
|
||||
this.taskId,
|
||||
this.attendeeIds = const [],
|
||||
this.attendeeEmails = const [],
|
||||
this.color = '#2196F3',
|
||||
this.recurringRuleId,
|
||||
this.reminders = const {},
|
||||
this.status = 'confirmed',
|
||||
required this.createdBy,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory CalendarEventModel.fromFirestore(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
return CalendarEventModel(
|
||||
id: doc.id,
|
||||
title: data['title'] ?? '',
|
||||
description: data['description'],
|
||||
location: data['location'] ?? '',
|
||||
startTime: (data['startTime'] as Timestamp).toDate(),
|
||||
endTime: (data['endTime'] as Timestamp).toDate(),
|
||||
isAllDay: data['isAllDay'] ?? false,
|
||||
googleEventId: data['googleEventId'],
|
||||
taskId: data['taskId'],
|
||||
attendeeIds: List<String>.from(data['attendeeIds'] ?? []),
|
||||
attendeeEmails: List<String>.from(data['attendeeEmails'] ?? []),
|
||||
color: data['color'] ?? '#2196F3',
|
||||
recurringRuleId: data['recurringRuleId'],
|
||||
reminders: data['reminders'] ?? {},
|
||||
status: data['status'] ?? 'confirmed',
|
||||
createdBy: data['createdBy'] ?? '',
|
||||
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'title': title,
|
||||
'description': description,
|
||||
'location': location,
|
||||
'startTime': Timestamp.fromDate(startTime),
|
||||
'endTime': Timestamp.fromDate(endTime),
|
||||
'isAllDay': isAllDay,
|
||||
'googleEventId': googleEventId,
|
||||
'taskId': taskId,
|
||||
'attendeeIds': attendeeIds,
|
||||
'attendeeEmails': attendeeEmails,
|
||||
'color': color,
|
||||
'recurringRuleId': recurringRuleId,
|
||||
'reminders': reminders,
|
||||
'status': status,
|
||||
'createdBy': createdBy,
|
||||
'createdAt': Timestamp.fromDate(createdAt),
|
||||
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
CalendarEventModel copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? description,
|
||||
String? location,
|
||||
DateTime? startTime,
|
||||
DateTime? endTime,
|
||||
bool? isAllDay,
|
||||
String? googleEventId,
|
||||
String? taskId,
|
||||
List<String>? attendeeIds,
|
||||
List<String>? attendeeEmails,
|
||||
String? color,
|
||||
String? recurringRuleId,
|
||||
Map<String, dynamic>? reminders,
|
||||
String? status,
|
||||
String? createdBy,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return CalendarEventModel(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
description: description ?? this.description,
|
||||
location: location ?? this.location,
|
||||
startTime: startTime ?? this.startTime,
|
||||
endTime: endTime ?? this.endTime,
|
||||
isAllDay: isAllDay ?? this.isAllDay,
|
||||
googleEventId: googleEventId ?? this.googleEventId,
|
||||
taskId: taskId ?? this.taskId,
|
||||
attendeeIds: attendeeIds ?? this.attendeeIds,
|
||||
attendeeEmails: attendeeEmails ?? this.attendeeEmails,
|
||||
color: color ?? this.color,
|
||||
recurringRuleId: recurringRuleId ?? this.recurringRuleId,
|
||||
reminders: reminders ?? this.reminders,
|
||||
status: status ?? this.status,
|
||||
createdBy: createdBy ?? this.createdBy,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
Duration get duration => endTime.difference(startTime);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
class MindMapNode {
|
||||
final String id;
|
||||
final String projectId;
|
||||
final String? parentId;
|
||||
final int level;
|
||||
final String title;
|
||||
final String? description;
|
||||
final String? taskId; // Link to task
|
||||
final String color;
|
||||
final String icon;
|
||||
final Map<String, dynamic> metadata;
|
||||
final double x;
|
||||
final double y;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
MindMapNode({
|
||||
required this.id,
|
||||
required this.projectId,
|
||||
this.parentId,
|
||||
required this.level,
|
||||
required this.title,
|
||||
this.description,
|
||||
this.taskId,
|
||||
this.color = '#2196F3',
|
||||
this.icon = 'folder',
|
||||
this.metadata = const {},
|
||||
this.x = 0,
|
||||
this.y = 0,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory MindMapNode.fromFirestore(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
return MindMapNode(
|
||||
id: doc.id,
|
||||
projectId: data['projectId'] ?? '',
|
||||
parentId: data['parentId'],
|
||||
level: data['level'] ?? 0,
|
||||
title: data['title'] ?? '',
|
||||
description: data['description'],
|
||||
taskId: data['taskId'],
|
||||
color: data['color'] ?? '#2196F3',
|
||||
icon: data['icon'] ?? 'folder',
|
||||
metadata: data['metadata'] ?? {},
|
||||
x: (data['x'] ?? 0).toDouble(),
|
||||
y: (data['y'] ?? 0).toDouble(),
|
||||
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'projectId': projectId,
|
||||
'parentId': parentId,
|
||||
'level': level,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'taskId': taskId,
|
||||
'color': color,
|
||||
'icon': icon,
|
||||
'metadata': metadata,
|
||||
'x': x,
|
||||
'y': y,
|
||||
'createdAt': Timestamp.fromDate(createdAt),
|
||||
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
MindMapNode copyWith({
|
||||
String? id,
|
||||
String? projectId,
|
||||
String? parentId,
|
||||
int? level,
|
||||
String? title,
|
||||
String? description,
|
||||
String? taskId,
|
||||
String? color,
|
||||
String? icon,
|
||||
Map<String, dynamic>? metadata,
|
||||
double? x,
|
||||
double? y,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return MindMapNode(
|
||||
id: id ?? this.id,
|
||||
projectId: projectId ?? this.projectId,
|
||||
parentId: parentId ?? this.parentId,
|
||||
level: level ?? this.level,
|
||||
title: title ?? this.title,
|
||||
description: description ?? this.description,
|
||||
taskId: taskId ?? this.taskId,
|
||||
color: color ?? this.color,
|
||||
icon: icon ?? this.icon,
|
||||
metadata: metadata ?? this.metadata,
|
||||
x: x ?? this.x,
|
||||
y: y ?? this.y,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MindMapModel {
|
||||
final String id;
|
||||
final String projectId;
|
||||
final String title;
|
||||
final String description;
|
||||
final String rootNodeId;
|
||||
final List<MindMapNode> nodes;
|
||||
final Map<String, dynamic> layout;
|
||||
final String createdBy;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
MindMapModel({
|
||||
required this.id,
|
||||
required this.projectId,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.rootNodeId,
|
||||
required this.nodes,
|
||||
required this.layout,
|
||||
required this.createdBy,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory MindMapModel.fromFirestore({
|
||||
required String id,
|
||||
required Map<String, dynamic> data,
|
||||
required List<MindMapNode> nodes,
|
||||
}) {
|
||||
return MindMapModel(
|
||||
id: id,
|
||||
projectId: data['projectId'] ?? '',
|
||||
title: data['title'] ?? '',
|
||||
description: data['description'] ?? '',
|
||||
rootNodeId: data['rootNodeId'] ?? '',
|
||||
nodes: nodes,
|
||||
layout: data['layout'] ?? {},
|
||||
createdBy: data['createdBy'] ?? '',
|
||||
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'projectId': projectId,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'rootNodeId': rootNodeId,
|
||||
'layout': layout,
|
||||
'createdBy': createdBy,
|
||||
'createdAt': Timestamp.fromDate(createdAt),
|
||||
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
class ProjectModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final String description;
|
||||
final String teamId;
|
||||
final String ownerId;
|
||||
final List<String> memberIds;
|
||||
final String status;
|
||||
final DateTime startDate;
|
||||
final DateTime? endDate;
|
||||
final String color;
|
||||
final String icon;
|
||||
final Map<String, dynamic> settings;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
ProjectModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.teamId,
|
||||
required this.ownerId,
|
||||
this.memberIds = const [],
|
||||
this.status = 'active',
|
||||
required this.startDate,
|
||||
this.endDate,
|
||||
this.color = '#1976D2',
|
||||
this.icon = 'project',
|
||||
this.settings = const {},
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory ProjectModel.fromFirestore(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
return ProjectModel(
|
||||
id: doc.id,
|
||||
name: data['name'] ?? '',
|
||||
description: data['description'] ?? '',
|
||||
teamId: data['teamId'] ?? '',
|
||||
ownerId: data['ownerId'] ?? '',
|
||||
memberIds: List<String>.from(data['memberIds'] ?? []),
|
||||
status: data['status'] ?? 'active',
|
||||
startDate: (data['startDate'] as Timestamp).toDate(),
|
||||
endDate: data['endDate'] != null
|
||||
? (data['endDate'] as Timestamp).toDate()
|
||||
: null,
|
||||
color: data['color'] ?? '#1976D2',
|
||||
icon: data['icon'] ?? 'project',
|
||||
settings: data['settings'] ?? {},
|
||||
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'name': name,
|
||||
'description': description,
|
||||
'teamId': teamId,
|
||||
'ownerId': ownerId,
|
||||
'memberIds': memberIds,
|
||||
'status': status,
|
||||
'startDate': Timestamp.fromDate(startDate),
|
||||
'endDate': endDate != null ? Timestamp.fromDate(endDate!) : null,
|
||||
'color': color,
|
||||
'icon': icon,
|
||||
'settings': settings,
|
||||
'createdAt': Timestamp.fromDate(createdAt),
|
||||
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
ProjectModel copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? description,
|
||||
String? teamId,
|
||||
String? ownerId,
|
||||
List<String>? memberIds,
|
||||
String? status,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
String? color,
|
||||
String? icon,
|
||||
Map<String, dynamic>? settings,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return ProjectModel(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
teamId: teamId ?? this.teamId,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
memberIds: memberIds ?? this.memberIds,
|
||||
status: status ?? this.status,
|
||||
startDate: startDate ?? this.startDate,
|
||||
endDate: endDate ?? this.endDate,
|
||||
color: color ?? this.color,
|
||||
icon: icon ?? this.icon,
|
||||
settings: settings ?? this.settings,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
bool isMember(String userId) {
|
||||
return ownerId == userId || memberIds.contains(userId);
|
||||
}
|
||||
|
||||
bool isOwner(String userId) {
|
||||
return ownerId == userId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
enum TaskStatus {
|
||||
todo,
|
||||
inProgress,
|
||||
review,
|
||||
done,
|
||||
archived,
|
||||
}
|
||||
|
||||
enum TaskPriority {
|
||||
low,
|
||||
medium,
|
||||
high,
|
||||
urgent,
|
||||
}
|
||||
|
||||
class TaskModel {
|
||||
final String id;
|
||||
final String title;
|
||||
final String? description;
|
||||
final String projectId;
|
||||
final String? parentTaskId; // For mind-map hierarchy
|
||||
final TaskStatus status;
|
||||
final TaskPriority priority;
|
||||
final String? assigneeId;
|
||||
final String? creatorId;
|
||||
final List<String> tags;
|
||||
final DateTime? dueDate;
|
||||
final DateTime? startDate;
|
||||
final DateTime? completedDate;
|
||||
final int estimatedHours;
|
||||
final int spentHours;
|
||||
final Map<String, dynamic> aiMetadata; // AI-generated suggestions
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
TaskModel({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.projectId,
|
||||
this.parentTaskId,
|
||||
this.status = TaskStatus.todo,
|
||||
this.priority = TaskPriority.medium,
|
||||
this.assigneeId,
|
||||
this.creatorId,
|
||||
this.tags = const [],
|
||||
this.dueDate,
|
||||
this.startDate,
|
||||
this.completedDate,
|
||||
this.estimatedHours = 0,
|
||||
this.spentHours = 0,
|
||||
this.aiMetadata = const {},
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory TaskModel.fromFirestore(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
return TaskModel(
|
||||
id: doc.id,
|
||||
title: data['title'] ?? '',
|
||||
description: data['description'],
|
||||
projectId: data['projectId'] ?? '',
|
||||
parentTaskId: data['parentTaskId'],
|
||||
status: TaskStatus.values.firstWhere(
|
||||
(e) => e.name == data['status'],
|
||||
orElse: () => TaskStatus.todo,
|
||||
),
|
||||
priority: TaskPriority.values.firstWhere(
|
||||
(e) => e.name == data['priority'],
|
||||
orElse: () => TaskPriority.medium,
|
||||
),
|
||||
assigneeId: data['assigneeId'],
|
||||
creatorId: data['creatorId'],
|
||||
tags: List<String>.from(data['tags'] ?? []),
|
||||
dueDate: data['dueDate'] != null
|
||||
? (data['dueDate'] as Timestamp).toDate()
|
||||
: null,
|
||||
startDate: data['startDate'] != null
|
||||
? (data['startDate'] as Timestamp).toDate()
|
||||
: null,
|
||||
completedDate: data['completedDate'] != null
|
||||
? (data['completedDate'] as Timestamp).toDate()
|
||||
: null,
|
||||
estimatedHours: data['estimatedHours'] ?? 0,
|
||||
spentHours: data['spentHours'] ?? 0,
|
||||
aiMetadata: data['aiMetadata'] ?? {},
|
||||
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'title': title,
|
||||
'description': description,
|
||||
'projectId': projectId,
|
||||
'parentTaskId': parentTaskId,
|
||||
'status': status.name,
|
||||
'priority': priority.name,
|
||||
'assigneeId': assigneeId,
|
||||
'creatorId': creatorId,
|
||||
'tags': tags,
|
||||
'dueDate': dueDate != null ? Timestamp.fromDate(dueDate!) : null,
|
||||
'startDate': startDate != null ? Timestamp.fromDate(startDate!) : null,
|
||||
'completedDate': completedDate != null
|
||||
? Timestamp.fromDate(completedDate!)
|
||||
: null,
|
||||
'estimatedHours': estimatedHours,
|
||||
'spentHours': spentHours,
|
||||
'aiMetadata': aiMetadata,
|
||||
'createdAt': Timestamp.fromDate(createdAt),
|
||||
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
TaskModel copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? description,
|
||||
String? projectId,
|
||||
String? parentTaskId,
|
||||
TaskStatus? status,
|
||||
TaskPriority? priority,
|
||||
String? assigneeId,
|
||||
String? creatorId,
|
||||
List<String>? tags,
|
||||
DateTime? dueDate,
|
||||
DateTime? startDate,
|
||||
DateTime? completedDate,
|
||||
int? estimatedHours,
|
||||
int? spentHours,
|
||||
Map<String, dynamic>? aiMetadata,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return TaskModel(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
description: description ?? this.description,
|
||||
projectId: projectId ?? this.projectId,
|
||||
parentTaskId: parentTaskId ?? this.parentTaskId,
|
||||
status: status ?? this.status,
|
||||
priority: priority ?? this.priority,
|
||||
assigneeId: assigneeId ?? this.assigneeId,
|
||||
creatorId: creatorId ?? this.creatorId,
|
||||
tags: tags ?? this.tags,
|
||||
dueDate: dueDate ?? this.dueDate,
|
||||
startDate: startDate ?? this.startDate,
|
||||
completedDate: completedDate ?? this.completedDate,
|
||||
estimatedHours: estimatedHours ?? this.estimatedHours,
|
||||
spentHours: spentHours ?? this.spentHours,
|
||||
aiMetadata: aiMetadata ?? this.aiMetadata,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isOverdue {
|
||||
if (dueDate == null) return false;
|
||||
return DateTime.now().isAfter(dueDate!) && status != TaskStatus.done;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
enum UserRole {
|
||||
admin,
|
||||
member,
|
||||
viewer,
|
||||
}
|
||||
|
||||
class UserModel {
|
||||
final String id;
|
||||
final String email;
|
||||
final String? displayName;
|
||||
final String? photoUrl;
|
||||
final UserRole role;
|
||||
final List<String> teamIds;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final Map<String, dynamic> settings;
|
||||
|
||||
UserModel({
|
||||
required this.id,
|
||||
required this.email,
|
||||
this.displayName,
|
||||
this.photoUrl,
|
||||
this.role = UserRole.member,
|
||||
this.teamIds = const [],
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.settings = const {},
|
||||
});
|
||||
|
||||
factory UserModel.fromFirestore(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
return UserModel(
|
||||
id: doc.id,
|
||||
email: data['email'] ?? '',
|
||||
displayName: data['displayName'],
|
||||
photoUrl: data['photoUrl'],
|
||||
role: UserRole.values.firstWhere(
|
||||
(e) => e.name == data['role'],
|
||||
orElse: () => UserRole.member,
|
||||
),
|
||||
teamIds: List<String>.from(data['teamIds'] ?? []),
|
||||
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||
settings: data['settings'] ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'email': email,
|
||||
'displayName': displayName,
|
||||
'photoUrl': photoUrl,
|
||||
'role': role.name,
|
||||
'teamIds': teamIds,
|
||||
'createdAt': Timestamp.fromDate(createdAt),
|
||||
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||
'settings': settings,
|
||||
};
|
||||
}
|
||||
|
||||
UserModel copyWith({
|
||||
String? id,
|
||||
String? email,
|
||||
String? displayName,
|
||||
String? photoUrl,
|
||||
UserRole? role,
|
||||
List<String>? teamIds,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
Map<String, dynamic>? settings,
|
||||
}) {
|
||||
return UserModel(
|
||||
id: id ?? this.id,
|
||||
email: email ?? this.email,
|
||||
displayName: displayName ?? this.displayName,
|
||||
photoUrl: photoUrl ?? this.photoUrl,
|
||||
role: role ?? this.role,
|
||||
teamIds: teamIds ?? this.teamIds,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
settings: settings ?? this.settings,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isAdmin => role == UserRole.admin;
|
||||
bool get isMember => role == UserRole.member;
|
||||
bool get isViewer => role == UserRole.viewer;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import '../models/user_model.dart';
|
||||
|
||||
class AuthNotifier extends StateNotifier<AsyncValue<UserModel?>> {
|
||||
final FirebaseAuth _auth;
|
||||
final GoogleSignIn _googleSignIn;
|
||||
|
||||
AuthNotifier(this._auth, this._googleSignIn) : super(const AsyncValue.data(null)) {
|
||||
// Слушаем изменения состояния аутентификации
|
||||
_auth.authStateChanges().listen((user) {
|
||||
if (user != null) {
|
||||
state = AsyncValue.data(UserModel(
|
||||
id: user.uid,
|
||||
email: user.email ?? '',
|
||||
displayName: user.displayName,
|
||||
photoUrl: user.photoURL,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
));
|
||||
} else {
|
||||
state = const AsyncValue.data(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> signInWithGoogle() async {
|
||||
try {
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
final googleUser = await _googleSignIn.signIn();
|
||||
if (googleUser == null) {
|
||||
state = const AsyncValue.data(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final googleAuth = await googleUser.authentication;
|
||||
final credential = GoogleAuthProvider.credential(
|
||||
accessToken: googleAuth.accessToken,
|
||||
idToken: googleAuth.idToken,
|
||||
);
|
||||
|
||||
final userCredential = await _auth.signInWithCredential(credential);
|
||||
|
||||
// Здесь можно создать/обновить пользователя в Firestore
|
||||
state = AsyncValue.data(UserModel(
|
||||
id: userCredential.user!.uid,
|
||||
email: userCredential.user!.email ?? '',
|
||||
displayName: userCredential.user!.displayName,
|
||||
photoUrl: userCredential.user!.photoURL,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
));
|
||||
} catch (e, st) {
|
||||
state = AsyncValue.error(e, st);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> signOut() async {
|
||||
try {
|
||||
await _googleSignIn.signOut();
|
||||
await _auth.signOut();
|
||||
state = const AsyncValue.data(null);
|
||||
} catch (e, st) {
|
||||
state = AsyncValue.error(e, st);
|
||||
}
|
||||
}
|
||||
|
||||
User? get currentUser => _auth.currentUser;
|
||||
bool get isLoggedIn => _auth.currentUser != null;
|
||||
}
|
||||
|
||||
final authProvider = StateNotifierProvider<AuthNotifier, AsyncValue<UserModel?>>((ref) {
|
||||
return AuthNotifier(FirebaseAuth.instance, GoogleSignIn());
|
||||
});
|
||||
|
||||
final authStateProvider = Provider<AuthNotifier>((ref) {
|
||||
return ref.read(authProvider.notifier);
|
||||
});
|
||||
|
||||
final isAuthenticatedProvider = Provider<bool>((ref) {
|
||||
return ref.watch(authProvider).value != null;
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AlarmsScreen extends StatelessWidget {
|
||||
const AlarmsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Будильники'),
|
||||
),
|
||||
body: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: 5,
|
||||
itemBuilder: (context, index) {
|
||||
return _AlarmCard(index: index);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Новый будильник'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlarmCard extends StatelessWidget {
|
||||
final int index;
|
||||
|
||||
const _AlarmCard({required this.index});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: index % 2 == 0 ? Colors.blue : Colors.grey,
|
||||
child: const Icon(Icons.alarm, color: Colors.white),
|
||||
),
|
||||
title: Text('Будильник #${index + 1}'),
|
||||
subtitle: Text('${8 + index}:00'),
|
||||
trailing: Switch(
|
||||
value: index % 2 == 0,
|
||||
onChanged: (value) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
|
||||
class AuthScreen extends ConsumerWidget {
|
||||
const AuthScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final authState = ref.watch(authProvider);
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.business_center,
|
||||
size: 120,
|
||||
color: Colors.blue,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const Text(
|
||||
'NANAHUI CRM',
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'CRM-ассистент для команд',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
authState.when(
|
||||
data: (user) {
|
||||
if (user != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/home');
|
||||
});
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (error, stack) => Text(
|
||||
'Error: $error',
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
ref.read(authProvider.notifier).signInWithGoogle();
|
||||
},
|
||||
icon: const Icon(Icons.login),
|
||||
label: const Text('Войти через Google'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CalendarScreen extends StatelessWidget {
|
||||
const CalendarScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Календарь'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sync),
|
||||
onPressed: () {
|
||||
// Синхронизация с Google Calendar
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildCalendarHeader(),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: 5,
|
||||
itemBuilder: (context, index) {
|
||||
return _CalendarEventCard(index: index);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCalendarHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
onPressed: () {},
|
||||
),
|
||||
const Text(
|
||||
'Март 2026',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CalendarEventCard extends StatelessWidget {
|
||||
final int index;
|
||||
|
||||
const _CalendarEventCard({required this.index});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 4,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: _getEventColor(index),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
title: Text('Событие #${index + 1}'),
|
||||
subtitle: Text('10:00 - 11:00'),
|
||||
trailing: const Icon(Icons.more_vert),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getEventColor(int index) {
|
||||
final colors = [Colors.blue, Colors.green, Colors.orange, Colors.purple];
|
||||
return colors[index % colors.length];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class DashboardScreen extends StatelessWidget {
|
||||
const DashboardScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('NANAHUI CRM'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildWelcomeCard(),
|
||||
const SizedBox(height: 24),
|
||||
_buildQuickStats(),
|
||||
const SizedBox(height: 24),
|
||||
_buildQuickActions(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWelcomeCard() {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Добро пожаловать!',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Управляйте задачами команды и планируйте работу с помощью AI',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Новая задача'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.auto_awesome),
|
||||
label: const Text('AI План'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickStats() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Быстрая статистика',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _StatCard(
|
||||
icon: Icons.task,
|
||||
label: 'Задачи',
|
||||
value: '12',
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _StatCard(
|
||||
icon: Icons.pending_actions,
|
||||
label: 'В работе',
|
||||
value: '5',
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _StatCard(
|
||||
icon: Icons.check_circle,
|
||||
label: 'Готово',
|
||||
value: '48',
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActions(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Быстрые действия',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
_ActionChip(
|
||||
icon: Icons.calendar_today,
|
||||
label: 'Планирование',
|
||||
onTap: () => context.go('/home/calendar'),
|
||||
),
|
||||
_ActionChip(
|
||||
icon: Icons.account_tree,
|
||||
label: 'Mind-карта',
|
||||
onTap: () => context.go('/home/mindmap'),
|
||||
),
|
||||
_ActionChip(
|
||||
icon: Icons.insights,
|
||||
label: 'Аналитика',
|
||||
onTap: () {},
|
||||
),
|
||||
_ActionChip(
|
||||
icon: Icons.people,
|
||||
label: 'Команда',
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String value;
|
||||
final Color color;
|
||||
|
||||
const _StatCard({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 32),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ActionChip({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(label),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dashboard_screen.dart';
|
||||
import 'tasks_screen.dart';
|
||||
import 'mind_map_screen.dart';
|
||||
import 'calendar_screen.dart';
|
||||
import 'alarms_screen.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
int _selectedIndex = 0;
|
||||
|
||||
final List<Widget> _screens = [
|
||||
const DashboardScreen(),
|
||||
const TasksScreen(),
|
||||
const MindMapScreen(),
|
||||
const CalendarScreen(),
|
||||
const AlarmsScreen(),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: _screens[_selectedIndex],
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: _selectedIndex,
|
||||
onDestinationSelected: (index) {
|
||||
setState(() {
|
||||
_selectedIndex = index;
|
||||
});
|
||||
},
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.dashboard_outlined),
|
||||
selectedIcon: Icon(Icons.dashboard),
|
||||
label: 'Главная',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.task_outlined),
|
||||
selectedIcon: Icon(Icons.task),
|
||||
label: 'Задачи',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.account_tree_outlined),
|
||||
selectedIcon: Icon(Icons.account_tree),
|
||||
label: 'Mind-карта',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.calendar_today_outlined),
|
||||
selectedIcon: Icon(Icons.calendar_today),
|
||||
label: 'Календарь',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.alarm_outlined),
|
||||
selectedIcon: Icon(Icons.alarm),
|
||||
label: 'Будильники',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MindMapScreen extends StatelessWidget {
|
||||
const MindMapScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Mind-карта'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
// Edit mode - только для админов
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.download),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.account_tree,
|
||||
size: 100,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'Mind-карта проекта',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Создайте структуру задач в виде дерева',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Создать mind-карту'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
// AI генерация mind-карты
|
||||
},
|
||||
child: const Icon(Icons.auto_awesome),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TasksScreen extends StatelessWidget {
|
||||
const TasksScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Задачи'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.filter_list),
|
||||
onPressed: () {},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: 10,
|
||||
itemBuilder: (context, index) {
|
||||
return _TaskCard(index: index);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Новая задача'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TaskCard extends StatelessWidget {
|
||||
final int index;
|
||||
|
||||
const _TaskCard({required this.index});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: _getPriorityColor(index),
|
||||
child: Text('${index + 1}'),
|
||||
),
|
||||
title: Text('Задача #${index + 1}'),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text('Описание задачи...'),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.calendar_today, size: 12, color: Colors.grey),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'До: ${DateTime.now().add(Duration(days: index)).toString().split(' ')[0]}',
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(value: 'edit', child: Text('Редактировать')),
|
||||
const PopupMenuItem(value: 'delete', child: Text('Удалить')),
|
||||
],
|
||||
),
|
||||
isThreeLine: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getPriorityColor(int index) {
|
||||
final colors = [Colors.red, Colors.orange, Colors.blue, Colors.green];
|
||||
return colors[index % colors.length];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user