Add Firestore integration with real-time sync
This commit is contained in:
@@ -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());
|
||||||
@@ -47,6 +53,16 @@ class FirestoreService {
|
|||||||
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());
|
||||||
@@ -66,7 +82,15 @@ 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();
|
||||||
@@ -90,9 +114,27 @@ class FirestoreService {
|
|||||||
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 {
|
||||||
@@ -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());
|
||||||
@@ -135,7 +186,9 @@ class FirestoreService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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());
|
||||||
@@ -180,21 +244,37 @@ class FirestoreService {
|
|||||||
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'] ?? []);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,16 +1,54 @@
|
|||||||
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) {
|
||||||
|
// Загружаем или создаём пользователя в 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(
|
state = AsyncValue.data(UserModel(
|
||||||
id: user.uid,
|
id: user.uid,
|
||||||
email: user.email ?? '',
|
email: user.email ?? '',
|
||||||
@@ -19,6 +57,7 @@ class AuthNotifier extends StateNotifier<AsyncValue<UserModel?>> {
|
|||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
));
|
));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
state = const AsyncValue.data(null);
|
state = const AsyncValue.data(null);
|
||||||
}
|
}
|
||||||
@@ -43,16 +82,10 @@ 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;
|
||||||
|
});
|
||||||
|
|||||||
@@ -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,13 +141,15 @@ 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 ?? [];
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,8 +59,10 @@ 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'),
|
||||||
@@ -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('Выйти'),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user