Add Firestore integration with real-time sync

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