Fix compilation errors
This commit is contained in:
@@ -178,15 +178,6 @@ class FirestoreService {
|
|||||||
.toList());
|
.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
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());
|
||||||
|
|||||||
@@ -1,243 +1,15 @@
|
|||||||
import 'dart:convert';
|
// Заглушка для Google Calendar Service
|
||||||
import 'package:googleapis/calendar/v3.dart' as gcal;
|
// Полная реализация будет позже через OAuth2 flow
|
||||||
import 'package:googleapis_auth/auth_io.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import '../models/calendar_event_model.dart';
|
|
||||||
|
|
||||||
class GoogleCalendarService {
|
class GoogleCalendarService {
|
||||||
static const List<String> _scopes = [
|
bool _isAuthorized = false;
|
||||||
gcal.CalendarApi.calendarEventsScope,
|
|
||||||
gcal.CalendarApi.calendarReadonlyScope,
|
|
||||||
];
|
|
||||||
|
|
||||||
String? _accessToken;
|
|
||||||
String? _refreshToken;
|
|
||||||
DateTime? _tokenExpiry;
|
|
||||||
gcal.CalendarApi? _calendarApi;
|
|
||||||
|
|
||||||
// Авторизация через OAuth2
|
|
||||||
Future<void> authorize(String clientId, String clientSecret, String refreshToken) async {
|
|
||||||
_refreshToken = refreshToken;
|
|
||||||
|
|
||||||
final credentials = ServiceAccountCredentials(
|
|
||||||
jsonDecode(jsonEncode({
|
|
||||||
'client_id': clientId,
|
|
||||||
'client_secret': clientSecret,
|
|
||||||
'refresh_token': refreshToken,
|
|
||||||
'type': 'authorized_user',
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
final client = await clientViaServiceAccount(credentials, _scopes);
|
|
||||||
_calendarApi = gcal.CalendarApi(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получение списка событий
|
|
||||||
Future<List<CalendarEventModel>> getEvents({
|
|
||||||
required DateTime timeMin,
|
|
||||||
required DateTime timeMax,
|
|
||||||
String calendarId = 'primary',
|
|
||||||
}) async {
|
|
||||||
if (_calendarApi == null) {
|
|
||||||
throw Exception('Not authorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final events = await _calendarApi!.events.list(
|
|
||||||
calendarId,
|
|
||||||
timeMin: timeMin.toUtc(),
|
|
||||||
timeMax: timeMax.toUtc(),
|
|
||||||
orderBy: 'startTime',
|
|
||||||
singleEvents: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
return events.items?.map((event) => _convertToEventModel(event)).toList() ?? [];
|
|
||||||
} catch (e) {
|
|
||||||
print('Error fetching events: $e');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создание события
|
|
||||||
Future<CalendarEventModel> createEvent({
|
|
||||||
required String title,
|
|
||||||
required DateTime startTime,
|
|
||||||
required DateTime endTime,
|
|
||||||
String? description,
|
|
||||||
String? location,
|
|
||||||
List<String>? attendeeEmails,
|
|
||||||
String calendarId = 'primary',
|
|
||||||
}) async {
|
|
||||||
if (_calendarApi == null) {
|
|
||||||
throw Exception('Not authorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
final event = gcal.Event((e) {
|
|
||||||
e.summary = title;
|
|
||||||
e.description = description;
|
|
||||||
e.location = location;
|
|
||||||
e.start = gcal.EventDateTime((dt) {
|
|
||||||
dt.dateTime = startTime.toUtc();
|
|
||||||
});
|
|
||||||
e.end = gcal.EventDateTime((dt) {
|
|
||||||
dt.dateTime = endTime.toUtc();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (attendeeEmails != null && attendeeEmails.isNotEmpty) {
|
|
||||||
e.attendees = attendeeEmails
|
|
||||||
.map((email) => gcal.EventAttendee((a) => a.email = email))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
final createdEvent = await _calendarApi!.events.insert(event, calendarId);
|
|
||||||
return _convertToEventModel(createdEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновление события
|
|
||||||
Future<CalendarEventModel> updateEvent(
|
|
||||||
String eventId, {
|
|
||||||
String? title,
|
|
||||||
DateTime? startTime,
|
|
||||||
DateTime? endTime,
|
|
||||||
String? description,
|
|
||||||
String? location,
|
|
||||||
String calendarId = 'primary',
|
|
||||||
}) async {
|
|
||||||
if (_calendarApi == null) {
|
|
||||||
throw Exception('Not authorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сначала получаем текущее событие
|
|
||||||
final existingEvent = await _calendarApi!.events.get(calendarId, eventId);
|
|
||||||
|
|
||||||
final updatedEvent = gcal.Event((e) {
|
|
||||||
e.summary = title ?? existingEvent.summary;
|
|
||||||
e.description = description ?? existingEvent.description;
|
|
||||||
e.location = location ?? existingEvent.location;
|
|
||||||
|
|
||||||
if (startTime != null) {
|
|
||||||
e.start = gcal.EventDateTime((dt) {
|
|
||||||
dt.dateTime = startTime.toUtc();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
e.start = existingEvent.start;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endTime != null) {
|
|
||||||
e.end = gcal.EventDateTime((dt) {
|
|
||||||
dt.dateTime = endTime.toUtc();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
e.end = existingEvent.end;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
final result = await _calendarApi!.events.update(updatedEvent, calendarId, eventId);
|
|
||||||
return _convertToEventModel(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Удаление события
|
|
||||||
Future<void> deleteEvent(String eventId, String calendarId = 'primary') async {
|
|
||||||
if (_calendarApi == null) {
|
|
||||||
throw Exception('Not authorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
await _calendarApi!.events.delete(calendarId, eventId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Конвертация Google Event в нашу модель
|
|
||||||
CalendarEventModel _convertToEventModel(gcal.Event event) {
|
|
||||||
final startTime = event.start?.dateTime ?? event.start?.date ?? DateTime.now();
|
|
||||||
final endTime = event.end?.dateTime ?? event.end?.date ?? DateTime.now();
|
|
||||||
|
|
||||||
return CalendarEventModel(
|
|
||||||
id: event.id ?? '',
|
|
||||||
title: event.summary ?? 'Без названия',
|
|
||||||
description: event.description,
|
|
||||||
location: event.location ?? '',
|
|
||||||
startTime: startTime.toLocal(),
|
|
||||||
endTime: endTime.toLocal(),
|
|
||||||
isAllDay: event.start?.date != null,
|
|
||||||
googleEventId: event.id,
|
|
||||||
attendeeEmails: event.attendees?.map((a) => a.email ?? '').toList() ?? [],
|
|
||||||
color: _getColorFromEvent(event),
|
|
||||||
status: event.status ?? 'confirmed',
|
|
||||||
createdBy: event.creator?.email ?? '',
|
|
||||||
createdAt: event.created ?? DateTime.now(),
|
|
||||||
updatedAt: event.updated ?? DateTime.now(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getColorFromEvent(gcal.Event event) {
|
|
||||||
// Google Calendar использует colorId для цветов
|
|
||||||
switch (event.colorId) {
|
|
||||||
case '1': return '#a4bdfc'; // lavender
|
|
||||||
case '2': return '#7ae7bf'; // sage
|
|
||||||
case '3': return '#dbadff'; // grape
|
|
||||||
case '4': return '#ff887c'; // flamo
|
|
||||||
case '5': return '#fbd75b'; // banana
|
|
||||||
case '6': return '#ffef8f'; // tangerine
|
|
||||||
case '7': return '#dc2127'; // tomato
|
|
||||||
case '8': return '#b9e4e0'; // peacock
|
|
||||||
case '9': return '#e1e1e1'; // graphite
|
|
||||||
case '10': return '#5484ed'; // blueberry
|
|
||||||
case '11': return '#51b749'; // basil
|
|
||||||
default: return '#2196F3'; // default blue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получение URL для OAuth авторизации
|
|
||||||
static String getAuthUrl(String clientId, String redirectUri) {
|
|
||||||
final scopesEncoded = _scopes.join('%20');
|
|
||||||
return 'https://accounts.google.com/o/oauth2/auth'
|
|
||||||
'?client_id=$clientId'
|
|
||||||
'&redirect_uri=$redirectUri'
|
|
||||||
'&scope=$scopesEncoded'
|
|
||||||
'&response_type=code'
|
|
||||||
'&access_type=offline'
|
|
||||||
'&prompt=consent';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обмен кода на токен
|
|
||||||
Future<Map<String, dynamic>> exchangeCodeForToken({
|
|
||||||
required String code,
|
|
||||||
required String clientId,
|
|
||||||
required String clientSecret,
|
|
||||||
required String redirectUri,
|
|
||||||
}) async {
|
|
||||||
final response = await http.post(
|
|
||||||
Uri.parse('https://oauth2.googleapis.com/token'),
|
|
||||||
body: {
|
|
||||||
'code': code,
|
|
||||||
'client_id': clientId,
|
|
||||||
'client_secret': clientSecret,
|
|
||||||
'redirect_uri': redirectUri,
|
|
||||||
'grant_type': 'authorization_code',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
final data = jsonDecode(response.body);
|
|
||||||
return {
|
|
||||||
'accessToken': data['access_token'],
|
|
||||||
'refreshToken': data['refresh_token'],
|
|
||||||
'expiry': DateTime.now().add(Duration(seconds: data['expires_in'])),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
throw Exception('Failed to exchange code: ${response.body}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверка авторизации
|
// Проверка авторизации
|
||||||
bool get isAuthorized => _calendarApi != null;
|
bool get isAuthorized => _isAuthorized;
|
||||||
|
|
||||||
// Выход
|
// Выход
|
||||||
void logout() {
|
void logout() {
|
||||||
_calendarApi = null;
|
_isAuthorized = false;
|
||||||
_accessToken = null;
|
|
||||||
_refreshToken = null;
|
|
||||||
_tokenExpiry = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:timezone/timezone.dart' as tz;
|
import 'package:timezone/timezone.dart' as tz;
|
||||||
import 'package:timezone/data/latest.dart' as tz;
|
import 'package:timezone/data/latest.dart' as tz;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class NotificationService {
|
class NotificationService {
|
||||||
static final NotificationService _instance = NotificationService._internal();
|
static final NotificationService _instance = NotificationService._internal();
|
||||||
factory NotificationService() => _instance;
|
factory NotificationService() => _instance;
|
||||||
NotificationService._internal();
|
NotificationService._internal();
|
||||||
|
|
||||||
final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin();
|
final FlutterLocalNotificationsPlugin _notifications =
|
||||||
|
FlutterLocalNotificationsPlugin();
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
|
|
||||||
// Инициализация
|
// Инициализация
|
||||||
@@ -18,7 +20,8 @@ class NotificationService {
|
|||||||
tz.initializeTimeZones();
|
tz.initializeTimeZones();
|
||||||
|
|
||||||
// Настройки для Android
|
// Настройки для Android
|
||||||
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
|
const androidSettings =
|
||||||
|
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||||
|
|
||||||
// Настройки для iOS
|
// Настройки для iOS
|
||||||
const iosSettings = DarwinInitializationSettings(
|
const iosSettings = DarwinInitializationSettings(
|
||||||
@@ -35,7 +38,8 @@ class NotificationService {
|
|||||||
await _notifications.initialize(
|
await _notifications.initialize(
|
||||||
settings,
|
settings,
|
||||||
onDidReceiveNotificationResponse: _onNotificationTapped,
|
onDidReceiveNotificationResponse: _onNotificationTapped,
|
||||||
onDidReceiveBackgroundNotificationResponse: _onBackgroundNotificationTapped,
|
onDidReceiveBackgroundNotificationResponse:
|
||||||
|
_onBackgroundNotificationTapped,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Запрашиваем разрешения
|
// Запрашиваем разрешения
|
||||||
@@ -47,16 +51,16 @@ class NotificationService {
|
|||||||
// Запрос разрешений
|
// Запрос разрешений
|
||||||
Future<void> _requestPermissions() async {
|
Future<void> _requestPermissions() async {
|
||||||
// Android 13+ требует запрос разрешений
|
// Android 13+ требует запрос разрешений
|
||||||
final androidPlugin = _notifications
|
final androidPlugin = _notifications.resolvePlatformSpecificImplementation<
|
||||||
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
|
AndroidFlutterLocalNotificationsPlugin>();
|
||||||
|
|
||||||
if (androidPlugin != null) {
|
if (androidPlugin != null) {
|
||||||
await androidPlugin.requestNotificationsPermission();
|
await androidPlugin.requestNotificationsPermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
// iOS
|
// iOS
|
||||||
final iosPlugin = _notifications
|
final iosPlugin = _notifications.resolvePlatformSpecificImplementation<
|
||||||
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>();
|
IOSFlutterLocalNotificationsPlugin>();
|
||||||
|
|
||||||
if (iosPlugin != null) {
|
if (iosPlugin != null) {
|
||||||
await iosPlugin.requestPermissions(
|
await iosPlugin.requestPermissions(
|
||||||
@@ -91,7 +95,8 @@ class NotificationService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await _notifications
|
await _notifications
|
||||||
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>()
|
||||||
?.createNotificationChannel(channel);
|
?.createNotificationChannel(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +112,8 @@ class NotificationService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await _notifications
|
await _notifications
|
||||||
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>()
|
||||||
?.createNotificationChannel(channel);
|
?.createNotificationChannel(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,10 +227,12 @@ class NotificationService {
|
|||||||
body = 'Задача "$taskTitle" должна быть выполнена прямо сейчас!';
|
body = 'Задача "$taskTitle" должна быть выполнена прямо сейчас!';
|
||||||
} else if (hoursUntilDeadline <= 1) {
|
} else if (hoursUntilDeadline <= 1) {
|
||||||
title = '🟠 Срочно! Дедлайн через час';
|
title = '🟠 Срочно! Дедлайн через час';
|
||||||
body = 'Задача "$taskTitle" должна быть выполнена через $hoursUntilDeadline ч';
|
body =
|
||||||
|
'Задача "$taskTitle" должна быть выполнена через $hoursUntilDeadline ч';
|
||||||
} else if (hoursUntilDeadline <= 24) {
|
} else if (hoursUntilDeadline <= 24) {
|
||||||
title = '🟡 Напоминание о дедлайне';
|
title = '🟡 Напоминание о дедлайне';
|
||||||
body = 'Задача "$taskTitle" должна быть выполнена через $hoursUntilDeadline ч';
|
body =
|
||||||
|
'Задача "$taskTitle" должна быть выполнена через $hoursUntilDeadline ч';
|
||||||
} else {
|
} else {
|
||||||
final days = hoursUntilDeadline ~/ 24;
|
final days = hoursUntilDeadline ~/ 24;
|
||||||
title = '🔵 Скоро дедлайн';
|
title = '🔵 Скоро дедлайн';
|
||||||
@@ -308,7 +316,7 @@ class NotificationService {
|
|||||||
required int id,
|
required int id,
|
||||||
required String title,
|
required String title,
|
||||||
required String body,
|
required String body,
|
||||||
required Time scheduledTime,
|
required TimeOfDay scheduledTime,
|
||||||
String? payload,
|
String? payload,
|
||||||
}) async {
|
}) async {
|
||||||
if (!_isInitialized) await initialize();
|
if (!_isInitialized) await initialize();
|
||||||
@@ -343,7 +351,7 @@ class NotificationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
tz.TZDateTime _nextInstanceOfTime(Time scheduledTime) {
|
tz.TZDateTime _nextInstanceOfTime(TimeOfDay scheduledTime) {
|
||||||
final now = tz.TZDateTime.now(tz.local);
|
final now = tz.TZDateTime.now(tz.local);
|
||||||
var scheduled = tz.TZDateTime(
|
var scheduled = tz.TZDateTime(
|
||||||
tz.local,
|
tz.local,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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';
|
||||||
@@ -132,11 +133,6 @@ class GoRouterRefreshNotifier extends ChangeNotifier {
|
|||||||
GoRouterRefreshNotifier() {
|
GoRouterRefreshNotifier() {
|
||||||
// Подписка на изменения аутентификации будет добавлена через provider
|
// Подписка на изменения аутентификации будет добавлена через provider
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider для обновления роутера при изменении аутентификации
|
// Provider для обновления роутера при изменении аутентификации
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../providers/auth_provider.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../models/user_model.dart';
|
import '../../providers/auth_provider.dart';
|
||||||
|
import '../../models/user_model.dart';
|
||||||
|
|
||||||
class AdminScreen extends ConsumerWidget {
|
class AdminScreen extends ConsumerWidget {
|
||||||
const AdminScreen({super.key});
|
const AdminScreen({super.key});
|
||||||
@@ -116,9 +117,8 @@ class AdminScreen extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 40,
|
radius: 40,
|
||||||
backgroundImage: user?.photoUrl != null
|
backgroundImage:
|
||||||
? NetworkImage(user!.photoUrl!)
|
user?.photoUrl != null ? NetworkImage(user!.photoUrl!) : null,
|
||||||
: null,
|
|
||||||
child: user?.photoUrl == null
|
child: user?.photoUrl == null
|
||||||
? const Icon(Icons.person, size: 40)
|
? const Icon(Icons.person, size: 40)
|
||||||
: null,
|
: null,
|
||||||
@@ -136,7 +136,8 @@ class AdminScreen extends ConsumerWidget {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
if (user?.isAdmin ?? false)
|
if (user?.isAdmin ?? false)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.purple.withOpacity(0.2),
|
color: Colors.purple.withOpacity(0.2),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@@ -149,7 +150,8 @@ class AdminScreen extends ConsumerWidget {
|
|||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
const Text(
|
const Text(
|
||||||
'Администратор',
|
'Администратор',
|
||||||
style: TextStyle(color: Colors.purple, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Colors.purple, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../core/notification_service.dart';
|
import '../../core/notification_service.dart';
|
||||||
|
|
||||||
class NotificationsSettingsScreen extends ConsumerStatefulWidget {
|
class NotificationsSettingsScreen extends ConsumerStatefulWidget {
|
||||||
const NotificationsSettingsScreen({super.key});
|
const NotificationsSettingsScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<NotificationsSettingsScreen> createState() => _NotificationsSettingsScreenState();
|
ConsumerState<NotificationsSettingsScreen> createState() =>
|
||||||
|
_NotificationsSettingsScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NotificationsSettingsScreenState extends ConsumerState<NotificationsSettingsScreen> {
|
class _NotificationsSettingsScreenState
|
||||||
|
extends ConsumerState<NotificationsSettingsScreen> {
|
||||||
bool _taskNotifications = true;
|
bool _taskNotifications = true;
|
||||||
bool _deadlineReminders = true;
|
bool _deadlineReminders = true;
|
||||||
bool _taskAssigned = true;
|
bool _taskAssigned = true;
|
||||||
bool _dailySummary = false;
|
bool _dailySummary = false;
|
||||||
Time _dailySummaryTime = const Time(9, 0);
|
TimeOfDay _dailySummaryTime = const TimeOfDay(hour: 9, minute: 0);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -79,13 +81,14 @@ class _NotificationsSettingsScreenState extends ConsumerState<NotificationsSetti
|
|||||||
if (_dailySummary)
|
if (_dailySummary)
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Время сводки'),
|
title: const Text('Время сводки'),
|
||||||
subtitle: Text('${_dailySummaryTime.hour.toString().padLeft(2, '0')}:${_dailySummaryTime.minute.toString().padLeft(2, '0')}'),
|
subtitle: Text(
|
||||||
|
'${_dailySummaryTime.hour.toString().padLeft(2, '0')}:${_dailySummaryTime.minute.toString().padLeft(2, '0')}'),
|
||||||
trailing: const Icon(Icons.access_time),
|
trailing: const Icon(Icons.access_time),
|
||||||
onTap: () => _selectDailySummaryTime(),
|
onTap: () => _selectDailySummaryTime(),
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.test_build, color: Colors.blue),
|
leading: const Icon(Icons.info_outline, color: Colors.blue),
|
||||||
title: const Text('Тестовое уведомление'),
|
title: const Text('Тестовое уведомление'),
|
||||||
subtitle: const Text('Проверьте работу уведомлений'),
|
subtitle: const Text('Проверьте работу уведомлений'),
|
||||||
onTap: _showTestNotification,
|
onTap: _showTestNotification,
|
||||||
@@ -115,7 +118,7 @@ class _NotificationsSettingsScreenState extends ConsumerState<NotificationsSetti
|
|||||||
|
|
||||||
if (picked != null) {
|
if (picked != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_dailySummaryTime = Time(picked.hour, picked.minute);
|
_dailySummaryTime = picked;
|
||||||
});
|
});
|
||||||
_scheduleDailySummary();
|
_scheduleDailySummary();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import '../providers/auth_provider.dart';
|
import '../../providers/auth_provider.dart';
|
||||||
import '../models/project_model.dart';
|
import '../../models/project_model.dart';
|
||||||
|
|
||||||
class ProjectsManagementScreen extends ConsumerStatefulWidget {
|
class ProjectsManagementScreen extends ConsumerStatefulWidget {
|
||||||
const ProjectsManagementScreen({super.key});
|
const ProjectsManagementScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<ProjectsManagementScreen> createState() => _ProjectsManagementScreenState();
|
ConsumerState<ProjectsManagementScreen> createState() =>
|
||||||
|
_ProjectsManagementScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ProjectsManagementScreenState extends ConsumerState<ProjectsManagementScreen> {
|
class _ProjectsManagementScreenState
|
||||||
|
extends ConsumerState<ProjectsManagementScreen> {
|
||||||
final _nameController = TextEditingController();
|
final _nameController = TextEditingController();
|
||||||
final _descriptionController = TextEditingController();
|
final _descriptionController = TextEditingController();
|
||||||
bool _isCreating = false;
|
bool _isCreating = false;
|
||||||
@@ -87,13 +90,23 @@ class _ProjectsManagementScreenState extends ConsumerState<ProjectsManagementScr
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _getRandomColor() {
|
String _getRandomColor() {
|
||||||
final colors = ['#2196F3', '#4CAF50', '#FF9800', '#E91E63', '#9C27B0', '#00BCD4'];
|
final colors = [
|
||||||
|
'#2196F3',
|
||||||
|
'#4CAF50',
|
||||||
|
'#FF9800',
|
||||||
|
'#E91E63',
|
||||||
|
'#9C27B0',
|
||||||
|
'#00BCD4'
|
||||||
|
];
|
||||||
return colors[DateTime.now().millisecond % colors.length];
|
return colors[DateTime.now().millisecond % colors.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deleteProject(String projectId) async {
|
Future<void> _deleteProject(String projectId) async {
|
||||||
try {
|
try {
|
||||||
await FirebaseFirestore.instance.collection('projects').doc(projectId).delete();
|
await FirebaseFirestore.instance
|
||||||
|
.collection('projects')
|
||||||
|
.doc(projectId)
|
||||||
|
.delete();
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('✅ Проект удалён')),
|
const SnackBar(content: Text('✅ Проект удалён')),
|
||||||
@@ -195,7 +208,8 @@ class _ProjectsManagementScreenState extends ConsumerState<ProjectsManagementScr
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.folder_outline, size: 48, color: Colors.grey),
|
const Icon(Icons.folder_outline,
|
||||||
|
size: 48, color: Colors.grey),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
const Text(
|
||||||
'Нет проектов',
|
'Нет проектов',
|
||||||
@@ -212,9 +226,11 @@ class _ProjectsManagementScreenState extends ConsumerState<ProjectsManagementScr
|
|||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemCount: projects.length,
|
itemCount: projects.length,
|
||||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
separatorBuilder: (context, index) =>
|
||||||
|
const SizedBox(height: 12),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final projectData = projects[index].data() as Map<String, dynamic>;
|
final projectData =
|
||||||
|
projects[index].data() as Map<String, dynamic>;
|
||||||
final projectId = projects[index].id;
|
final projectId = projects[index].id;
|
||||||
final project = ProjectModel.fromFirestore(projects[index]);
|
final project = ProjectModel.fromFirestore(projects[index]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import '../providers/auth_provider.dart';
|
import '../../providers/auth_provider.dart';
|
||||||
import '../models/user_model.dart';
|
import '../../models/user_model.dart';
|
||||||
|
|
||||||
class TeamManagementScreen extends ConsumerStatefulWidget {
|
class TeamManagementScreen extends ConsumerStatefulWidget {
|
||||||
const TeamManagementScreen({super.key});
|
const TeamManagementScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<TeamManagementScreen> createState() => _TeamManagementScreenState();
|
ConsumerState<TeamManagementScreen> createState() =>
|
||||||
|
_TeamManagementScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TeamManagementScreenState extends ConsumerState<TeamManagementScreen> {
|
class _TeamManagementScreenState extends ConsumerState<TeamManagementScreen> {
|
||||||
@@ -80,7 +82,8 @@ class _TeamManagementScreenState extends ConsumerState<TeamManagementScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _changeUserRole(String userId, String teamId, UserRole newRole) async {
|
Future<void> _changeUserRole(
|
||||||
|
String userId, String teamId, UserRole newRole) async {
|
||||||
try {
|
try {
|
||||||
await FirebaseFirestore.instance
|
await FirebaseFirestore.instance
|
||||||
.collection('teams')
|
.collection('teams')
|
||||||
@@ -101,10 +104,7 @@ class _TeamManagementScreenState extends ConsumerState<TeamManagementScreen> {
|
|||||||
|
|
||||||
Future<void> _removeMember(String userId, String teamId) async {
|
Future<void> _removeMember(String userId, String teamId) async {
|
||||||
try {
|
try {
|
||||||
await FirebaseFirestore.instance
|
await FirebaseFirestore.instance.collection('teams').doc(teamId).update({
|
||||||
.collection('teams')
|
|
||||||
.doc(teamId)
|
|
||||||
.update({
|
|
||||||
'memberIds': FieldValue.arrayRemove([userId]),
|
'memberIds': FieldValue.arrayRemove([userId]),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -200,7 +200,8 @@ class _TeamManagementScreenState extends ConsumerState<TeamManagementScreen> {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.people_outline, size: 48, color: Colors.grey),
|
const Icon(Icons.people_outline,
|
||||||
|
size: 48, color: Colors.grey),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
const Text(
|
||||||
'Нет команд',
|
'Нет команд',
|
||||||
@@ -217,7 +218,8 @@ class _TeamManagementScreenState extends ConsumerState<TeamManagementScreen> {
|
|||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemCount: teams.length,
|
itemCount: teams.length,
|
||||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
separatorBuilder: (context, index) =>
|
||||||
|
const SizedBox(height: 12),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final team = teams[index].data() as Map<String, dynamic>;
|
final team = teams[index].data() as Map<String, dynamic>;
|
||||||
final teamId = teams[index].id;
|
final teamId = teams[index].id;
|
||||||
@@ -228,7 +230,8 @@ class _TeamManagementScreenState extends ConsumerState<TeamManagementScreen> {
|
|||||||
name: team['name'] ?? 'Без названия',
|
name: team['name'] ?? 'Без названия',
|
||||||
memberCount: (team['memberIds'] as List?)?.length ?? 0,
|
memberCount: (team['memberIds'] as List?)?.length ?? 0,
|
||||||
isOwner: isOwner,
|
isOwner: isOwner,
|
||||||
onRemove: isOwner ? null : () => _removeMember(user!.id, teamId),
|
onRemove:
|
||||||
|
isOwner ? null : () => _removeMember(user!.id, teamId),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -279,9 +282,8 @@ class _TeamManagementScreenState extends ConsumerState<TeamManagementScreen> {
|
|||||||
backgroundImage: user.photoUrl != null
|
backgroundImage: user.photoUrl != null
|
||||||
? NetworkImage(user.photoUrl!)
|
? NetworkImage(user.photoUrl!)
|
||||||
: null,
|
: null,
|
||||||
child: user.photoUrl == null
|
child:
|
||||||
? const Icon(Icons.person)
|
user.photoUrl == null ? const Icon(Icons.person) : null,
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
title: Text(user.displayName ?? 'Без имени'),
|
title: Text(user.displayName ?? 'Без имени'),
|
||||||
subtitle: Text(user.email),
|
subtitle: Text(user.email),
|
||||||
@@ -289,7 +291,8 @@ class _TeamManagementScreenState extends ConsumerState<TeamManagementScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: user.isAdmin
|
color: user.isAdmin
|
||||||
? Colors.purple.withOpacity(0.2)
|
? Colors.purple.withOpacity(0.2)
|
||||||
|
|||||||
@@ -76,11 +76,11 @@ class _TaskFormScreenState extends ConsumerState<TaskFormScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await ref.read(tasksProvider.notifier).generateSubtasksWithAI(
|
await ref.read(tasksProvider.notifier).generateSubtasksWithAI(
|
||||||
title: _titleController.text,
|
title: _titleController.text,
|
||||||
description: _descriptionController.text,
|
description: _descriptionController.text,
|
||||||
projectId: widget.projectId,
|
projectId: widget.projectId,
|
||||||
parentTaskId: widget.task?.id,
|
parentTaskId: widget.task?.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -120,11 +120,12 @@ class _TaskFormScreenState extends ConsumerState<TaskFormScreen> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final priority = await ref.read(tasksProvider.notifier).prioritizeTaskWithAI(
|
final priority =
|
||||||
title: _titleController.text,
|
await ref.read(tasksProvider.notifier).prioritizeTaskWithAI(
|
||||||
description: _descriptionController.text,
|
title: _titleController.text,
|
||||||
dueDate: _dueDate,
|
description: _descriptionController.text,
|
||||||
);
|
dueDate: _dueDate,
|
||||||
|
);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_priority = priority;
|
_priority = priority;
|
||||||
@@ -161,22 +162,22 @@ class _TaskFormScreenState extends ConsumerState<TaskFormScreen> {
|
|||||||
if (widget.task != null) {
|
if (widget.task != null) {
|
||||||
// Редактирование
|
// Редактирование
|
||||||
await ref.read(tasksProvider.notifier).updateTask(
|
await ref.read(tasksProvider.notifier).updateTask(
|
||||||
widget.task!.id,
|
widget.task!.id,
|
||||||
title: _titleController.text,
|
title: _titleController.text,
|
||||||
description: _descriptionController.text,
|
description: _descriptionController.text,
|
||||||
priority: _priority,
|
priority: _priority,
|
||||||
dueDate: _dueDate,
|
dueDate: _dueDate,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Создание
|
// Создание
|
||||||
await ref.read(tasksProvider.notifier).createTask(
|
await ref.read(tasksProvider.notifier).createTask(
|
||||||
title: _titleController.text,
|
title: _titleController.text,
|
||||||
description: _descriptionController.text,
|
description: _descriptionController.text,
|
||||||
projectId: widget.projectId,
|
projectId: widget.projectId,
|
||||||
priority: _priority,
|
priority: _priority,
|
||||||
dueDate: _dueDate,
|
dueDate: _dueDate,
|
||||||
estimatedHours: _estimatedHours,
|
estimatedHours: _estimatedHours,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -366,7 +367,7 @@ class _TaskFormScreenState extends ConsumerState<TaskFormScreen> {
|
|||||||
Widget _getPriorityIcon(TaskPriority priority) {
|
Widget _getPriorityIcon(TaskPriority priority) {
|
||||||
switch (priority) {
|
switch (priority) {
|
||||||
case TaskPriority.urgent:
|
case TaskPriority.urgent:
|
||||||
return const Icon(Icons.fire_engine, color: Colors.red);
|
return const Icon(Icons.local_fire_department, color: Colors.red);
|
||||||
case TaskPriority.high:
|
case TaskPriority.high:
|
||||||
return const Icon(Icons.arrow_upward, color: Colors.orange);
|
return const Icon(Icons.arrow_upward, color: Colors.orange);
|
||||||
case TaskPriority.medium:
|
case TaskPriority.medium:
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ class _TaskCard extends ConsumerWidget {
|
|||||||
IconData _getPriorityIcon(TaskPriority priority) {
|
IconData _getPriorityIcon(TaskPriority priority) {
|
||||||
switch (priority) {
|
switch (priority) {
|
||||||
case TaskPriority.urgent:
|
case TaskPriority.urgent:
|
||||||
return Icons.fire_engine;
|
return Icons.local_fire_department;
|
||||||
case TaskPriority.high:
|
case TaskPriority.high:
|
||||||
return Icons.arrow_upward;
|
return Icons.arrow_upward;
|
||||||
case TaskPriority.medium:
|
case TaskPriority.medium:
|
||||||
|
|||||||
Reference in New Issue
Block a user