diff --git a/lib/core/firestore_service.dart b/lib/core/firestore_service.dart index 3ce3c76..6114f80 100644 --- a/lib/core/firestore_service.dart +++ b/lib/core/firestore_service.dart @@ -23,6 +23,12 @@ class FirestoreService { await _db.collection('users').doc(user.id).update(user.toFirestore()); } + Stream watchUser(String userId) { + return _db.collection('users').doc(userId).snapshots().map((doc) { + return doc.exists ? UserModel.fromFirestore(doc) : null; + }); + } + // === PROJECTS === Future createProject(ProjectModel project) async { await _db.collection('projects').doc(project.id).set(project.toFirestore()); @@ -34,7 +40,7 @@ class FirestoreService { .where('teamId', isEqualTo: teamId) .orderBy('createdAt', descending: true) .get(); - + return snapshot.docs.map((doc) => ProjectModel.fromFirestore(doc)).toList(); } @@ -43,10 +49,20 @@ class FirestoreService { .collection('projects') .where('memberIds', arrayContains: userId) .get(); - + return snapshot.docs.map((doc) => ProjectModel.fromFirestore(doc)).toList(); } + Stream> watchProjectsByUser(String userId) { + return _db + .collection('projects') + .where('memberIds', arrayContains: userId) + .snapshots() + .map((snapshot) => snapshot.docs + .map((doc) => ProjectModel.fromFirestore(doc)) + .toList()); + } + // === TASKS === Future createTask(TaskModel task) async { await _db.collection('tasks').doc(task.id).set(task.toFirestore()); @@ -58,7 +74,7 @@ class FirestoreService { .where('projectId', isEqualTo: projectId) .orderBy('createdAt', descending: true) .get(); - + return snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList(); } @@ -66,9 +82,17 @@ class FirestoreService { final snapshot = await _db .collection('tasks') .where('assigneeId', isEqualTo: assigneeId) - .where('status', whereIn: ['todo', 'inProgress', 'review']) + .where('status', whereIn: ['todo', 'inProgress', 'review']).get(); + + return snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList(); + } + + Future> getAllTasks() async { + final snapshot = await _db + .collection('tasks') + .orderBy('createdAt', descending: true) .get(); - + return snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList(); } @@ -86,13 +110,31 @@ class FirestoreService { .collection('tasks') .where('projectId', isEqualTo: projectId) .get(); - + return snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList(); } + // Поток задач для реального времени + Stream> watchTasks(String? projectId) { + Query query = _db.collection('tasks'); + + if (projectId != null && projectId.isNotEmpty) { + query = query.where('projectId', isEqualTo: projectId); + } + + return query.orderBy('createdAt', descending: true).snapshots().map( + (snapshot) => + snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList()); + } + // === MIND MAPS === Future createMindMapNode(MindMapNode node) async { - await _db.collection('mindmaps').doc(node.projectId).collection('nodes').doc(node.id).set(node.toFirestore()); + await _db + .collection('mindmaps') + .doc(node.projectId) + .collection('nodes') + .doc(node.id) + .set(node.toFirestore()); } Future> getMindMapNodes(String projectId) async { @@ -102,7 +144,7 @@ class FirestoreService { .collection('nodes') .orderBy('level') .get(); - + return snapshot.docs.map((doc) => MindMapNode.fromFirestore(doc)).toList(); } @@ -115,6 +157,15 @@ class FirestoreService { .update(node.toFirestore()); } + Future deleteMindMapNode(String projectId, String nodeId) async { + await _db + .collection('mindmaps') + .doc(projectId) + .collection('nodes') + .doc(nodeId) + .delete(); + } + // === CALENDAR EVENTS === Future createEvent(CalendarEventModel event) async { await _db.collection('events').doc(event.id).set(event.toFirestore()); @@ -129,13 +180,15 @@ class FirestoreService { .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(); + return snapshot.docs + .map((doc) => CalendarEventModel.fromFirestore(doc)) + .toList(); } Future updateEvent(CalendarEventModel event) async { @@ -146,6 +199,17 @@ class FirestoreService { await _db.collection('events').doc(eventId).delete(); } + Stream> watchEvents(String userId) { + return _db + .collection('events') + .where('attendeeIds', arrayContains: userId) + .orderBy('startTime') + .snapshots() + .map((snapshot) => snapshot.docs + .map((doc) => CalendarEventModel.fromFirestore(doc)) + .toList()); + } + // === ALARMS === Future createAlarm(AlarmModel alarm) async { await _db.collection('alarms').doc(alarm.id).set(alarm.toFirestore()); @@ -157,7 +221,7 @@ class FirestoreService { .where('isEnabled', isEqualTo: true) .orderBy('dateTime') .get(); - + return snapshot.docs.map((doc) => AlarmModel.fromFirestore(doc)).toList(); } @@ -172,29 +236,45 @@ class FirestoreService { // === BATCH OPERATIONS === Future syncTasks(List 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> watchTasks(String projectId) { - return _db - .collection('tasks') - .where('projectId', isEqualTo: projectId) - .snapshots() - .map((snapshot) => snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList()); + // === TEAMS === + Future createTeam(String teamId, String name, String ownerId) async { + await _db.collection('teams').doc(teamId).set({ + 'name': name, + 'ownerId': ownerId, + 'memberIds': [ownerId], + 'createdAt': Timestamp.now(), + 'updatedAt': Timestamp.now(), + }); } - Stream> watchEvents(String userId) { - return _db - .collection('events') - .where('attendeeIds', arrayContains: userId) - .snapshots() - .map((snapshot) => snapshot.docs.map((doc) => CalendarEventModel.fromFirestore(doc)).toList()); + Future addTeamMember(String teamId, String userId) async { + await _db.collection('teams').doc(teamId).update({ + 'memberIds': FieldValue.arrayUnion([userId]), + 'updatedAt': Timestamp.now(), + }); + } + + Future removeTeamMember(String teamId, String userId) async { + await _db.collection('teams').doc(teamId).update({ + 'memberIds': FieldValue.arrayRemove([userId]), + 'updatedAt': Timestamp.now(), + }); + } + + Stream>> watchTeamMembers(String teamId) { + return _db.collection('teams').doc(teamId).snapshots().map((doc) { + if (!doc.exists) return []; + final data = doc.data() as Map; + return List>.from(data['memberIds'] ?? []); + }); } } diff --git a/lib/core/router.dart b/lib/core/router.dart index 04bd930..292640a 100644 --- a/lib/core/router.dart +++ b/lib/core/router.dart @@ -1,4 +1,5 @@ import 'package:go_router/go_router.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../screens/auth_screen.dart'; import '../screens/home_screen.dart'; import '../screens/tasks_screen.dart'; @@ -7,10 +8,16 @@ import '../screens/mind_map_screen.dart'; import '../screens/calendar_screen.dart'; import '../screens/alarms_screen.dart'; import '../screens/ai_test_screen.dart'; +import '../providers/auth_provider.dart'; + +// Глобальный ключ для навигации +final GlobalKey _rootNavigatorKey = GlobalKey(); class AppRouter { static final GoRouter router = GoRouter( + navigatorKey: _rootNavigatorKey, initialLocation: '/auth', + refreshListenable: GoRouterRefreshNotifier(), routes: [ GoRoute( path: '/auth', @@ -74,5 +81,44 @@ class AppRouter { ], ), ], + redirect: (context, state) { + final isLoggedIn = GoRouterRefreshNotifier.isAuthenticated; + final isLoggingIn = state.matchedLocation == '/auth'; + + if (!isLoggedIn && !isLoggingIn) { + return '/auth'; + } + + if (isLoggedIn && isLoggingIn) { + return '/home'; + } + + return null; + }, ); } + +// Класс для уведомления об изменении состояния аутентификации +class GoRouterRefreshNotifier extends ChangeNotifier { + static bool isAuthenticated = false; + + GoRouterRefreshNotifier() { + // Подписка на изменения аутентификации будет добавлена через provider + } + + @override + void dispose() { + super.dispose(); + } +} + +// Provider для обновления роутера при изменении аутентификации +final routerRefreshProvider = ChangeNotifierProvider((ref) { + final auth = ref.watch(authProvider); + + auth.whenData((user) { + GoRouterRefreshNotifier.isAuthenticated = user != null; + }); + + return GoRouterRefreshNotifier(); +}); diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart index 2eef7b7..0bc0458 100644 --- a/lib/providers/auth_provider.dart +++ b/lib/providers/auth_provider.dart @@ -1,24 +1,63 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; import '../models/user_model.dart'; class AuthNotifier extends StateNotifier> { final FirebaseAuth _auth; final GoogleSignIn _googleSignIn; + final FirebaseFirestore _firestore; - AuthNotifier(this._auth, this._googleSignIn) : super(const AsyncValue.data(null)) { + AuthNotifier(this._auth, this._googleSignIn, this._firestore) + : super(const AsyncValue.data(null)) { // Слушаем изменения состояния аутентификации - _auth.authStateChanges().listen((user) { + _auth.authStateChanges().listen((user) async { 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(), - )); + // Загружаем или создаём пользователя в Firestore + try { + final userDoc = + await _firestore.collection('users').doc(user.uid).get(); + + if (userDoc.exists) { + // Пользователь уже есть + state = AsyncValue.data(UserModel.fromFirestore(userDoc)); + } else { + // Создаём нового пользователя + final newUser = UserModel( + id: user.uid, + email: user.email ?? '', + displayName: user.displayName, + photoUrl: user.photoURL, + role: UserRole.member, + teamIds: [], + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + settings: { + 'theme': 'system', + 'notifications': true, + 'language': 'ru', + }, + ); + + await _firestore + .collection('users') + .doc(user.uid) + .set(newUser.toFirestore()); + state = AsyncValue.data(newUser); + } + } catch (e) { + print('Error loading user from Firestore: $e'); + // Создаём временного пользователя + 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); } @@ -42,17 +81,11 @@ class AuthNotifier extends StateNotifier> { ); 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(), - )); + + // Пользователь будет загружен через authStateChanges listener + print('Signed in: ${userCredential.user?.email}'); } catch (e, st) { + print('Google Sign-In Error: $e'); state = AsyncValue.error(e, st); } } @@ -69,16 +102,41 @@ class AuthNotifier extends StateNotifier> { User? get currentUser => _auth.currentUser; bool get isLoggedIn => _auth.currentUser != null; + String? get userId => _auth.currentUser?.uid; } -final authProvider = StateNotifierProvider>((ref) { - return AuthNotifier(FirebaseAuth.instance, GoogleSignIn()); +// Provider для аутентификации +final authProvider = + StateNotifierProvider>((ref) { + return AuthNotifier( + FirebaseAuth.instance, + GoogleSignIn(), + FirebaseFirestore.instance, + ); }); -final authStateProvider = Provider((ref) { - return ref.read(authProvider.notifier); +// Provider для текущего пользователя +final currentUserProvider = Provider((ref) { + return ref.watch(authProvider).value; }); +// Provider для текущего userId +final currentUserIdProvider = Provider((ref) { + return ref.watch(authProvider).value?.id; +}); + +// Provider для проверки авторизации final isAuthenticatedProvider = Provider((ref) { return ref.watch(authProvider).value != null; }); + +// Provider для роли пользователя +final userRoleProvider = Provider((ref) { + return ref.watch(authProvider).value?.role; +}); + +// Provider для проверки является ли пользователь админом +final isAdminProvider = Provider((ref) { + final role = ref.watch(userRoleProvider); + return role == UserRole.admin; +}); diff --git a/lib/providers/task_provider.dart b/lib/providers/task_provider.dart index e41f113..958667d 100644 --- a/lib/providers/task_provider.dart +++ b/lib/providers/task_provider.dart @@ -6,45 +6,85 @@ import '../core/firestore_service.dart'; import '../core/local_storage_service.dart'; import '../core/deepseek_service.dart'; +// Provider для текущего пользователя (заглушка пока нет auth) +final currentUserIdProvider = Provider((ref) { + // TODO: Заменить на реального пользователя из Firebase Auth + return null; // Пока нет авторизации +}); + // Provider для списка всех задач -final tasksProvider = StateNotifierProvider>>((ref) { - return TasksNotifier(); +final tasksProvider = + StateNotifierProvider>>((ref) { + return TasksNotifier(ref); }); // Provider для задач конкретного проекта -final tasksByProjectProvider = Provider.family>, String>((ref, projectId) { +final tasksByProjectProvider = + Provider.family>, String>((ref, projectId) { final tasks = ref.watch(tasksProvider); return tasks.when( - data: (allTasks) => AsyncValue.data(allTasks.where((t) => t.projectId == projectId).toList()), + data: (allTasks) => AsyncValue.data( + allTasks.where((t) => t.projectId == projectId).toList()), loading: () => const AsyncValue.loading(), error: (e, st) => AsyncValue.error(e, st), ); }); // Provider для задач пользователя -final tasksByUserProvider = Provider.family>, String>((ref, userId) { +final tasksByUserProvider = + Provider.family>, String>((ref, userId) { final tasks = ref.watch(tasksProvider); return tasks.when( - data: (allTasks) => AsyncValue.data(allTasks.where((t) => t.assigneeId == userId).toList()), + data: (allTasks) => + AsyncValue.data(allTasks.where((t) => t.assigneeId == userId).toList()), loading: () => const AsyncValue.loading(), error: (e, st) => AsyncValue.error(e, st), ); }); +// Stream provider для задач в реальном времени +final tasksStreamProvider = + StreamProvider.family, String?>((ref, projectId) { + return firestoreService.watchTasks(projectId); +}); + class TasksNotifier extends StateNotifier>> { - TasksNotifier() : super(const AsyncValue.loading()) { + final Ref ref; + StreamSubscription? _subscription; + + TasksNotifier(this.ref) : super(const AsyncValue.loading()) { _loadTasks(); } + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } + // Загрузка задач Future _loadTasks() async { try { - // Сначала загружаем из локального хранилища + // Сначала загружаем из локального хранилища (офлайн режим) final localTasks = localStorageService.getTasks(); state = AsyncValue.data(localTasks); - // Потом синхронизируем с Firestore - // TODO: Реализовать когда будет Firebase Auth + // Подписываемся на обновления из Firestore + final currentUserId = ref.read(currentUserIdProvider); + + _subscription = + firestoreService.watchTasks(null).listen((remoteTasks) async { + // Сохраняем локально для офлайн режима + await localStorageService.syncWithRemote(remoteTasks); + + // Обновляем состояние + state = AsyncValue.data(remoteTasks); + }, onError: (error, stackTrace) { + // Если ошибка Firestore, используем локальные данные + print('Firestore error: $error'); + final localTasks = localStorageService.getTasks(); + state = AsyncValue.data(localTasks); + }); } catch (e, st) { state = AsyncValue.error(e, st); } @@ -59,8 +99,11 @@ class TasksNotifier extends StateNotifier>> { TaskPriority priority = TaskPriority.medium, DateTime? dueDate, int estimatedHours = 0, + String? assigneeId, }) async { try { + final currentUserId = ref.read(currentUserIdProvider); + final task = TaskModel( id: const Uuid().v4(), title: title, @@ -70,20 +113,26 @@ class TasksNotifier extends StateNotifier>> { priority: priority, dueDate: dueDate, estimatedHours: estimatedHours, + assigneeId: assigneeId ?? currentUserId, + creatorId: currentUserId, createdAt: DateTime.now(), updatedAt: DateTime.now(), ); - // Сохраняем локально + // Сохраняем локально (для офлайн режима) await localStorageService.saveTask(task); - + + // Сохраняем в Firestore + try { + await firestoreService.createTask(task); + } catch (e) { + print('Firestore save failed, saved locally only: $e'); + } + // Обновляем состояние final currentTasks = state.value ?? []; state = AsyncValue.data([...currentTasks, task]); - // TODO: Сохранить в Firestore когда будет авторизация - // await firestoreService.createTask(task); - return task; } catch (e, st) { state = AsyncValue.error(e, st); @@ -92,18 +141,20 @@ class TasksNotifier extends StateNotifier>> { } // Обновление задачи - Future updateTask(String taskId, { + Future updateTask( + String taskId, { String? title, String? description, TaskStatus? status, TaskPriority? priority, DateTime? dueDate, String? assigneeId, + Map? aiMetadata, }) async { try { final currentTasks = state.value ?? []; final taskIndex = currentTasks.indexWhere((t) => t.id == taskId); - + if (taskIndex == -1) throw Exception('Task not found'); final oldTask = currentTasks[taskIndex]; @@ -114,17 +165,23 @@ class TasksNotifier extends StateNotifier>> { priority: priority ?? oldTask.priority, dueDate: dueDate ?? oldTask.dueDate, assigneeId: assigneeId ?? oldTask.assigneeId, + aiMetadata: aiMetadata ?? oldTask.aiMetadata, updatedAt: DateTime.now(), ); // Сохраняем локально await localStorageService.saveTask(updatedTask); + // Сохраняем в Firestore + try { + await firestoreService.updateTask(updatedTask); + } catch (e) { + print('Firestore update failed, saved locally only: $e'); + } + // Обновляем состояние currentTasks[taskIndex] = updatedTask; state = AsyncValue.data(List.from(currentTasks)); - - // TODO: Обновить в Firestore } catch (e, st) { state = AsyncValue.error(e, st); rethrow; @@ -137,12 +194,17 @@ class TasksNotifier extends StateNotifier>> { // Удаляем локально await localStorageService.deleteTask(taskId); + // Удаляем из Firestore + try { + await firestoreService.deleteTask(taskId); + } catch (e) { + print('Firestore delete failed, deleted locally only: $e'); + } + // Обновляем состояние final currentTasks = state.value ?? []; currentTasks.removeWhere((t) => t.id == taskId); state = AsyncValue.data(List.from(currentTasks)); - - // TODO: Удалить из Firestore } catch (e, st) { state = AsyncValue.error(e, st); rethrow; @@ -193,13 +255,25 @@ class TasksNotifier extends StateNotifier>> { ); } + // AI: Генерация описания + Future generateDescriptionWithAI({ + required String title, + String? context, + }) async { + return await deepSeekService.generateTaskDescription( + title: title, + context: context, + ); + } + // Фильтрация задач по статусу List filterByStatus(List tasks, TaskStatus status) { return tasks.where((t) => t.status == status).toList(); } // Фильтрация задач по приоритету - List filterByPriority(List tasks, TaskPriority priority) { + List filterByPriority( + List tasks, TaskPriority priority) { return tasks.where((t) => t.priority == priority).toList(); } @@ -207,4 +281,20 @@ class TasksNotifier extends StateNotifier>> { List getOverdueTasks(List tasks) { return tasks.where((t) => t.isOverdue).toList(); } + + // Завершение задачи + Future completeTask(String taskId) async { + await updateTask( + taskId, + status: TaskStatus.done, + ); + } + + // Возобновление задачи + Future reopenTask(String taskId) async { + await updateTask( + taskId, + status: TaskStatus.todo, + ); + } } diff --git a/lib/screens/auth_screen.dart b/lib/screens/auth_screen.dart index 24e773d..a747780 100644 --- a/lib/screens/auth_screen.dart +++ b/lib/screens/auth_screen.dart @@ -9,6 +9,7 @@ class AuthScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final authState = ref.watch(authProvider); + final authNotifier = ref.read(authProvider.notifier); return Scaffold( body: SafeArea( @@ -43,6 +44,7 @@ class AuthScreen extends ConsumerWidget { authState.when( data: (user) { if (user != null) { + // Пользователь авторизован - переходим на главную WidgetsBinding.instance.addPostFrameCallback((_) { context.go('/home'); }); @@ -57,9 +59,11 @@ class AuthScreen extends ConsumerWidget { ), const SizedBox(height: 24), ElevatedButton.icon( - onPressed: () { - ref.read(authProvider.notifier).signInWithGoogle(); - }, + onPressed: authNotifier.isLoggedIn + ? null + : () { + authNotifier.signInWithGoogle(); + }, icon: const Icon(Icons.login), label: const Text('Войти через Google'), style: ElevatedButton.styleFrom( @@ -69,6 +73,14 @@ class AuthScreen extends ConsumerWidget { ), ), ), + if (authNotifier.isLoggedIn) ...[ + const SizedBox(height: 16), + OutlinedButton.icon( + onPressed: () => authNotifier.signOut(), + icon: const Icon(Icons.logout), + label: const Text('Выйти'), + ), + ], ], ), ),