Add Local Notifications with deadline reminders
This commit is contained in:
@@ -0,0 +1,365 @@
|
|||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:timezone/timezone.dart' as tz;
|
||||||
|
import 'package:timezone/data/latest.dart' as tz;
|
||||||
|
|
||||||
|
class NotificationService {
|
||||||
|
static final NotificationService _instance = NotificationService._internal();
|
||||||
|
factory NotificationService() => _instance;
|
||||||
|
NotificationService._internal();
|
||||||
|
|
||||||
|
final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin();
|
||||||
|
bool _isInitialized = false;
|
||||||
|
|
||||||
|
// Инициализация
|
||||||
|
Future<void> initialize() async {
|
||||||
|
if (_isInitialized) return;
|
||||||
|
|
||||||
|
// Инициализируем timezone
|
||||||
|
tz.initializeTimeZones();
|
||||||
|
|
||||||
|
// Настройки для Android
|
||||||
|
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||||
|
|
||||||
|
// Настройки для iOS
|
||||||
|
const iosSettings = DarwinInitializationSettings(
|
||||||
|
requestAlertPermission: true,
|
||||||
|
requestBadgePermission: true,
|
||||||
|
requestSoundPermission: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const settings = InitializationSettings(
|
||||||
|
android: androidSettings,
|
||||||
|
iOS: iosSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notifications.initialize(
|
||||||
|
settings,
|
||||||
|
onDidReceiveNotificationResponse: _onNotificationTapped,
|
||||||
|
onDidReceiveBackgroundNotificationResponse: _onBackgroundNotificationTapped,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Запрашиваем разрешения
|
||||||
|
await _requestPermissions();
|
||||||
|
|
||||||
|
_isInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запрос разрешений
|
||||||
|
Future<void> _requestPermissions() async {
|
||||||
|
// Android 13+ требует запрос разрешений
|
||||||
|
final androidPlugin = _notifications
|
||||||
|
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
|
||||||
|
|
||||||
|
if (androidPlugin != null) {
|
||||||
|
await androidPlugin.requestNotificationsPermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
// iOS
|
||||||
|
final iosPlugin = _notifications
|
||||||
|
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>();
|
||||||
|
|
||||||
|
if (iosPlugin != null) {
|
||||||
|
await iosPlugin.requestPermissions(
|
||||||
|
alert: true,
|
||||||
|
badge: true,
|
||||||
|
sound: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка нажатия на уведомление
|
||||||
|
void _onNotificationTapped(NotificationResponse response) {
|
||||||
|
print('Notification tapped: ${response.payload}');
|
||||||
|
// TODO: Навигация к задаче
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка нажатия в фоне
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
static void _onBackgroundNotificationTapped(NotificationResponse response) {
|
||||||
|
print('Background notification tapped: ${response.payload}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создание канала для уведомлений о задачах
|
||||||
|
Future<void> _createTaskChannel() async {
|
||||||
|
const channel = AndroidNotificationChannel(
|
||||||
|
'tasks_channel',
|
||||||
|
'Задачи',
|
||||||
|
description: 'Уведомления о задачах и дедлайнах',
|
||||||
|
importance: Importance.high,
|
||||||
|
enableVibration: true,
|
||||||
|
playSound: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notifications
|
||||||
|
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
|
||||||
|
?.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создание канала для будильников
|
||||||
|
Future<void> _createAlarmChannel() async {
|
||||||
|
const channel = AndroidNotificationChannel(
|
||||||
|
'alarms_channel',
|
||||||
|
'Будильники',
|
||||||
|
description: 'Будильники и напоминания',
|
||||||
|
importance: Importance.max,
|
||||||
|
enableVibration: true,
|
||||||
|
playSound: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notifications
|
||||||
|
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
|
||||||
|
?.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показ немедленного уведомления
|
||||||
|
Future<void> showNotification({
|
||||||
|
required int id,
|
||||||
|
required String title,
|
||||||
|
required String body,
|
||||||
|
String? payload,
|
||||||
|
String channelId = 'tasks_channel',
|
||||||
|
}) async {
|
||||||
|
if (!_isInitialized) await initialize();
|
||||||
|
|
||||||
|
const details = NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
'tasks_channel',
|
||||||
|
'Задачи',
|
||||||
|
channelDescription: 'Уведомления о задачах',
|
||||||
|
importance: Importance.high,
|
||||||
|
priority: Priority.high,
|
||||||
|
icon: '@mipmap/ic_launcher',
|
||||||
|
enableVibration: true,
|
||||||
|
playSound: true,
|
||||||
|
),
|
||||||
|
iOS: DarwinNotificationDetails(
|
||||||
|
presentAlert: true,
|
||||||
|
presentBadge: true,
|
||||||
|
presentSound: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notifications.show(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
details,
|
||||||
|
payload: payload,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Планирование уведомления
|
||||||
|
Future<void> scheduleNotification({
|
||||||
|
required int id,
|
||||||
|
required String title,
|
||||||
|
required String body,
|
||||||
|
required DateTime scheduledDate,
|
||||||
|
String? payload,
|
||||||
|
String channelId = 'tasks_channel',
|
||||||
|
}) async {
|
||||||
|
if (!_isInitialized) await initialize();
|
||||||
|
|
||||||
|
// Не планируем в прошлом
|
||||||
|
if (scheduledDate.isBefore(DateTime.now())) {
|
||||||
|
scheduledDate = DateTime.now().add(const Duration(minutes: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
const details = NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
'tasks_channel',
|
||||||
|
'Задачи',
|
||||||
|
channelDescription: 'Уведомления о задачах',
|
||||||
|
importance: Importance.high,
|
||||||
|
priority: Priority.high,
|
||||||
|
icon: '@mipmap/ic_launcher',
|
||||||
|
enableVibration: true,
|
||||||
|
playSound: true,
|
||||||
|
),
|
||||||
|
iOS: DarwinNotificationDetails(
|
||||||
|
presentAlert: true,
|
||||||
|
presentBadge: true,
|
||||||
|
presentSound: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notifications.zonedSchedule(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
tz.TZDateTime.from(scheduledDate, tz.local),
|
||||||
|
details,
|
||||||
|
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
|
||||||
|
uiLocalNotificationDateInterpretation:
|
||||||
|
UILocalNotificationDateInterpretation.absoluteTime,
|
||||||
|
payload: payload,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отмена уведомления
|
||||||
|
Future<void> cancelNotification(int id) async {
|
||||||
|
await _notifications.cancel(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отмена всех уведомлений
|
||||||
|
Future<void> cancelAllNotifications() async {
|
||||||
|
await _notifications.cancelAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Уведомление о дедлайне задачи
|
||||||
|
Future<void> showTaskDeadlineReminder({
|
||||||
|
required String taskTitle,
|
||||||
|
required DateTime deadline,
|
||||||
|
String? taskId,
|
||||||
|
}) async {
|
||||||
|
final hoursUntilDeadline = deadline.difference(DateTime.now()).inHours;
|
||||||
|
|
||||||
|
String title;
|
||||||
|
String body;
|
||||||
|
|
||||||
|
if (hoursUntilDeadline <= 0) {
|
||||||
|
title = '🔴 Дедлайн задачи!';
|
||||||
|
body = 'Задача "$taskTitle" должна быть выполнена прямо сейчас!';
|
||||||
|
} else if (hoursUntilDeadline <= 1) {
|
||||||
|
title = '🟠 Срочно! Дедлайн через час';
|
||||||
|
body = 'Задача "$taskTitle" должна быть выполнена через $hoursUntilDeadline ч';
|
||||||
|
} else if (hoursUntilDeadline <= 24) {
|
||||||
|
title = '🟡 Напоминание о дедлайне';
|
||||||
|
body = 'Задача "$taskTitle" должна быть выполнена через $hoursUntilDeadline ч';
|
||||||
|
} else {
|
||||||
|
final days = hoursUntilDeadline ~/ 24;
|
||||||
|
title = '🔵 Скоро дедлайн';
|
||||||
|
body = 'Задача "$taskTitle" должна быть выполнена через $days дн';
|
||||||
|
}
|
||||||
|
|
||||||
|
await showNotification(
|
||||||
|
id: taskId.hashCode,
|
||||||
|
title: title,
|
||||||
|
body: body,
|
||||||
|
payload: taskId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Уведомление о новой задаче
|
||||||
|
Future<void> showNewTaskNotification({
|
||||||
|
required String taskTitle,
|
||||||
|
String? taskId,
|
||||||
|
}) async {
|
||||||
|
await showNotification(
|
||||||
|
id: taskId.hashCode,
|
||||||
|
title: '📋 Новая задача',
|
||||||
|
body: 'Задача "$taskTitle" создана',
|
||||||
|
payload: taskId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Уведомление о назначении задачи
|
||||||
|
Future<void> showTaskAssignedNotification({
|
||||||
|
required String taskTitle,
|
||||||
|
required String assignedBy,
|
||||||
|
String? taskId,
|
||||||
|
}) async {
|
||||||
|
await showNotification(
|
||||||
|
id: taskId.hashCode,
|
||||||
|
title: '🎯 Вам назначена задача',
|
||||||
|
body: '$assignedBy назначил вас на задачу "$taskTitle"',
|
||||||
|
payload: taskId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Будильник
|
||||||
|
Future<void> showAlarmNotification({
|
||||||
|
required int id,
|
||||||
|
required String title,
|
||||||
|
required String body,
|
||||||
|
}) async {
|
||||||
|
if (!_isInitialized) await initialize();
|
||||||
|
|
||||||
|
const details = NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
'alarms_channel',
|
||||||
|
'Будильники',
|
||||||
|
channelDescription: 'Будильники и напоминания',
|
||||||
|
importance: Importance.max,
|
||||||
|
priority: Priority.max,
|
||||||
|
icon: '@mipmap/ic_launcher',
|
||||||
|
enableVibration: true,
|
||||||
|
playSound: true,
|
||||||
|
fullScreenIntent: true,
|
||||||
|
category: AndroidNotificationCategory.alarm,
|
||||||
|
),
|
||||||
|
iOS: DarwinNotificationDetails(
|
||||||
|
presentAlert: true,
|
||||||
|
presentBadge: true,
|
||||||
|
presentSound: true,
|
||||||
|
interruptionLevel: InterruptionLevel.timeSensitive,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notifications.show(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
details,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Повторяющееся уведомление (ежедневное)
|
||||||
|
Future<void> scheduleDailyNotification({
|
||||||
|
required int id,
|
||||||
|
required String title,
|
||||||
|
required String body,
|
||||||
|
required Time scheduledTime,
|
||||||
|
String? payload,
|
||||||
|
}) async {
|
||||||
|
if (!_isInitialized) await initialize();
|
||||||
|
|
||||||
|
const details = NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
'daily_reminders',
|
||||||
|
'Ежедневные напоминания',
|
||||||
|
channelDescription: 'Ежедневные уведомления',
|
||||||
|
importance: Importance.defaultImportance,
|
||||||
|
priority: Priority.defaultPriority,
|
||||||
|
icon: '@mipmap/ic_launcher',
|
||||||
|
),
|
||||||
|
iOS: DarwinNotificationDetails(
|
||||||
|
presentAlert: true,
|
||||||
|
presentBadge: true,
|
||||||
|
presentSound: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notifications.zonedSchedule(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
_nextInstanceOfTime(scheduledTime),
|
||||||
|
details,
|
||||||
|
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
|
||||||
|
uiLocalNotificationDateInterpretation:
|
||||||
|
UILocalNotificationDateInterpretation.absoluteTime,
|
||||||
|
matchDateTimeComponents: DateTimeComponents.time,
|
||||||
|
payload: payload,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tz.TZDateTime _nextInstanceOfTime(Time scheduledTime) {
|
||||||
|
final now = tz.TZDateTime.now(tz.local);
|
||||||
|
var scheduled = tz.TZDateTime(
|
||||||
|
tz.local,
|
||||||
|
now.year,
|
||||||
|
now.month,
|
||||||
|
now.day,
|
||||||
|
scheduledTime.hour,
|
||||||
|
scheduledTime.minute,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (scheduled.isBefore(now)) {
|
||||||
|
scheduled = scheduled.add(const Duration(days: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return scheduled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final notificationService = NotificationService();
|
||||||
@@ -11,6 +11,7 @@ import '../screens/ai_test_screen.dart';
|
|||||||
import '../screens/admin/admin_screen.dart';
|
import '../screens/admin/admin_screen.dart';
|
||||||
import '../screens/admin/team_management_screen.dart';
|
import '../screens/admin/team_management_screen.dart';
|
||||||
import '../screens/admin/projects_management_screen.dart';
|
import '../screens/admin/projects_management_screen.dart';
|
||||||
|
import '../screens/admin/notifications_settings_screen.dart';
|
||||||
import '../providers/auth_provider.dart';
|
import '../providers/auth_provider.dart';
|
||||||
|
|
||||||
// Глобальный ключ для навигации
|
// Глобальный ключ для навигации
|
||||||
@@ -96,6 +97,12 @@ class AppRouter {
|
|||||||
name: 'admin-projects',
|
name: 'admin-projects',
|
||||||
builder: (context, state) => const ProjectsManagementScreen(),
|
builder: (context, state) => const ProjectsManagementScreen(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'notifications',
|
||||||
|
name: 'admin-notifications',
|
||||||
|
builder: (context, state) =>
|
||||||
|
const NotificationsSettingsScreen(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'core/firebase_config.dart';
|
import 'core/firebase_config.dart';
|
||||||
import 'core/local_storage_service.dart';
|
import 'core/local_storage_service.dart';
|
||||||
|
import 'core/notification_service.dart';
|
||||||
import 'core/theme.dart';
|
import 'core/theme.dart';
|
||||||
import 'core/router.dart';
|
import 'core/router.dart';
|
||||||
|
|
||||||
@@ -18,6 +19,9 @@ void main() async {
|
|||||||
// Инициализация локального хранилища
|
// Инициализация локального хранилища
|
||||||
await localStorageService.initialize();
|
await localStorageService.initialize();
|
||||||
|
|
||||||
|
// Инициализация уведомлений
|
||||||
|
await notificationService.initialize();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
const ProviderScope(
|
const ProviderScope(
|
||||||
child: NanaxeCrmApp(),
|
child: NanaxeCrmApp(),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import '../models/project_model.dart';
|
|||||||
import '../core/firestore_service.dart';
|
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';
|
||||||
|
import '../core/notification_service.dart';
|
||||||
|
|
||||||
// Provider для текущего пользователя (заглушка пока нет auth)
|
// Provider для текущего пользователя (заглушка пока нет auth)
|
||||||
final currentUserIdProvider = Provider<String?>((ref) {
|
final currentUserIdProvider = Provider<String?>((ref) {
|
||||||
@@ -100,6 +101,7 @@ class TasksNotifier extends StateNotifier<AsyncValue<List<TaskModel>>> {
|
|||||||
DateTime? dueDate,
|
DateTime? dueDate,
|
||||||
int estimatedHours = 0,
|
int estimatedHours = 0,
|
||||||
String? assigneeId,
|
String? assigneeId,
|
||||||
|
bool sendNotification = true,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final currentUserId = ref.read(currentUserIdProvider);
|
final currentUserId = ref.read(currentUserIdProvider);
|
||||||
@@ -129,6 +131,19 @@ class TasksNotifier extends StateNotifier<AsyncValue<List<TaskModel>>> {
|
|||||||
print('Firestore save failed, saved locally only: $e');
|
print('Firestore save failed, saved locally only: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Отправляем уведомление
|
||||||
|
if (sendNotification) {
|
||||||
|
await notificationService.showNewTaskNotification(
|
||||||
|
taskTitle: title,
|
||||||
|
taskId: task.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Планируем напоминание о дедлайне
|
||||||
|
if (dueDate != null) {
|
||||||
|
await scheduleDeadlineReminder(task);
|
||||||
|
}
|
||||||
|
|
||||||
// Обновляем состояние
|
// Обновляем состояние
|
||||||
final currentTasks = state.value ?? [];
|
final currentTasks = state.value ?? [];
|
||||||
state = AsyncValue.data([...currentTasks, task]);
|
state = AsyncValue.data([...currentTasks, task]);
|
||||||
@@ -297,4 +312,46 @@ class TasksNotifier extends StateNotifier<AsyncValue<List<TaskModel>>> {
|
|||||||
status: TaskStatus.todo,
|
status: TaskStatus.todo,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Планирование напоминания о дедлайне
|
||||||
|
Future<void> scheduleDeadlineReminder(TaskModel task) async {
|
||||||
|
if (task.dueDate == null) return;
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
final dueDate = task.dueDate!;
|
||||||
|
|
||||||
|
// Напоминание за 24 часа
|
||||||
|
if (dueDate.difference(now).inHours > 24) {
|
||||||
|
final reminder24h = dueDate.subtract(const Duration(hours: 24));
|
||||||
|
if (reminder24h.isAfter(now)) {
|
||||||
|
await notificationService.scheduleNotification(
|
||||||
|
id: '${task.id}_24h'.hashCode,
|
||||||
|
title: '🔔 Напоминание о задаче',
|
||||||
|
body: 'Задача "${task.title}" должна быть выполнена завтра',
|
||||||
|
scheduledDate: reminder24h,
|
||||||
|
payload: task.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Напоминание за 1 час
|
||||||
|
if (dueDate.difference(now).inHours > 1) {
|
||||||
|
final reminder1h = dueDate.subtract(const Duration(hours: 1));
|
||||||
|
if (reminder1h.isAfter(now)) {
|
||||||
|
await notificationService.scheduleNotification(
|
||||||
|
id: '${task.id}_1h'.hashCode,
|
||||||
|
title: '⏰ Скоро дедлайн!',
|
||||||
|
body: 'Задача "${task.title}" должна быть выполнена через час',
|
||||||
|
scheduledDate: reminder1h,
|
||||||
|
payload: task.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отмена напоминаний о задаче
|
||||||
|
Future<void> cancelTaskReminders(String taskId) async {
|
||||||
|
await notificationService.cancelNotification('${taskId}_24h'.hashCode);
|
||||||
|
await notificationService.cancelNotification('${taskId}_1h'.hashCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../core/notification_service.dart';
|
||||||
|
|
||||||
|
class NotificationsSettingsScreen extends ConsumerStatefulWidget {
|
||||||
|
const NotificationsSettingsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<NotificationsSettingsScreen> createState() => _NotificationsSettingsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NotificationsSettingsScreenState extends ConsumerState<NotificationsSettingsScreen> {
|
||||||
|
bool _taskNotifications = true;
|
||||||
|
bool _deadlineReminders = true;
|
||||||
|
bool _taskAssigned = true;
|
||||||
|
bool _dailySummary = false;
|
||||||
|
Time _dailySummaryTime = const Time(9, 0);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Уведомления'),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
const ListTile(
|
||||||
|
title: Text('Типы уведомлений'),
|
||||||
|
subtitle: Text('Настройте какие уведомления показывать'),
|
||||||
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
title: const Text('Задачи'),
|
||||||
|
subtitle: const Text('Уведомления о создании и изменении задач'),
|
||||||
|
value: _taskNotifications,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_taskNotifications = value;
|
||||||
|
});
|
||||||
|
// TODO: Сохранить в настройки
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
title: const Text('Напоминания о дедлайнах'),
|
||||||
|
subtitle: const Text('За 24 часа и за 1 час до дедлайна'),
|
||||||
|
value: _deadlineReminders,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_deadlineReminders = value;
|
||||||
|
});
|
||||||
|
// TODO: Сохранить в настройки
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
title: const Text('Назначение задач'),
|
||||||
|
subtitle: const Text('Когда вас назначают исполнителем'),
|
||||||
|
value: _taskAssigned,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_taskAssigned = value;
|
||||||
|
});
|
||||||
|
// TODO: Сохранить в настройки
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
SwitchListTile(
|
||||||
|
title: const Text('Ежедневная сводка'),
|
||||||
|
subtitle: const Text('Краткий отчёт о задачах на день'),
|
||||||
|
value: _dailySummary,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_dailySummary = value;
|
||||||
|
if (value) {
|
||||||
|
_scheduleDailySummary();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// TODO: Сохранить в настройки
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (_dailySummary)
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Время сводки'),
|
||||||
|
subtitle: Text('${_dailySummaryTime.hour.toString().padLeft(2, '0')}:${_dailySummaryTime.minute.toString().padLeft(2, '0')}'),
|
||||||
|
trailing: const Icon(Icons.access_time),
|
||||||
|
onTap: () => _selectDailySummaryTime(),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.test_build, color: Colors.blue),
|
||||||
|
title: const Text('Тестовое уведомление'),
|
||||||
|
subtitle: const Text('Проверьте работу уведомлений'),
|
||||||
|
onTap: _showTestNotification,
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Text(
|
||||||
|
'Уведомления будут приходить даже когда приложение закрыто. Убедитесь, что разрешения на уведомления включены в настройках устройства.',
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _selectDailySummaryTime() async {
|
||||||
|
final TimeOfDay? picked = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.fromDateTime(
|
||||||
|
DateTime.now().copyWith(
|
||||||
|
hour: _dailySummaryTime.hour,
|
||||||
|
minute: _dailySummaryTime.minute,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (picked != null) {
|
||||||
|
setState(() {
|
||||||
|
_dailySummaryTime = Time(picked.hour, picked.minute);
|
||||||
|
});
|
||||||
|
_scheduleDailySummary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _scheduleDailySummary() async {
|
||||||
|
if (_dailySummary) {
|
||||||
|
await notificationService.scheduleDailyNotification(
|
||||||
|
id: 'daily_summary'.hashCode,
|
||||||
|
title: '📊 Ежедневная сводка',
|
||||||
|
body: 'Ваши задачи на сегодня',
|
||||||
|
scheduledTime: _dailySummaryTime,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await notificationService.cancelNotification('daily_summary'.hashCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showTestNotification() async {
|
||||||
|
await notificationService.showNotification(
|
||||||
|
id: DateTime.now().millisecondsSinceEpoch,
|
||||||
|
title: '🔔 Тестовое уведомление',
|
||||||
|
body: 'Если вы видите это - уведомления работают!',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('✅ Уведомление отправлено')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user