import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../app/error_message.dart'; import '../app/load_state.dart'; import '../models/schedule_task.dart'; import '../providers/providers.dart'; import '../widgets/load_error_view.dart'; /// Экран управления расписаниями. /// Показывает все задачи (one-shot и cron), позволяет создавать и удалять. class SchedulesScreen extends ConsumerStatefulWidget { const SchedulesScreen({super.key}); @override ConsumerState createState() => _SchedulesScreenState(); } class _SchedulesScreenState extends ConsumerState { @override void initState() { super.initState(); _load(); } Future _load() async { await ref.read(tasksProvider.notifier).load(); } @override Widget build(BuildContext context) { final tasksState = ref.watch(tasksProvider); final tasks = tasksState.data; return Scaffold( appBar: AppBar(title: const Text('РАСПИСАНИЯ')), body: _buildContent(tasksState, tasks), floatingActionButton: FloatingActionButton( backgroundColor: Colors.deepOrange, onPressed: () => _showAddDialog(context), child: const Icon(Icons.add), ), ); } /// Диалог создания расписания void _showAddDialog(BuildContext context) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: const Color(0xFF1E1E1E), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) => const _AddScheduleSheet(), ); } Widget _buildContent( LoadState> tasksState, List tasks, ) { if ((tasksState.isIdle || tasksState.isLoading) && tasks.isEmpty) { return const Center( child: CircularProgressIndicator(color: Colors.deepOrange), ); } if (tasksState.hasError && tasks.isEmpty) { return LoadErrorView( title: 'Не удалось загрузить расписания', message: tasksState.errorMessage, icon: Icons.schedule, onRetry: _load, ); } if (tasks.isEmpty) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.schedule, size: 64, color: Colors.white24), SizedBox(height: 16), Text( 'Нет активных расписаний', style: TextStyle(color: Colors.white54, fontSize: 16), ), ], ), ); } final hasStatusHeader = tasksState.isLoading || tasksState.hasError; final statusHeaderCount = hasStatusHeader ? 1 : 0; return RefreshIndicator( color: Colors.deepOrange, onRefresh: _load, child: ListView.builder( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(12), itemCount: tasks.length + statusHeaderCount, itemBuilder: (context, index) { if (hasStatusHeader && index == 0) { if (tasksState.isLoading) { return const Padding( padding: EdgeInsets.only(bottom: 12), child: LinearProgressIndicator(color: Colors.deepOrange), ); } return LoadErrorBanner( title: 'Не удалось обновить расписания', message: tasksState.errorMessage, onRetry: _load, ); } final taskIndex = index - statusHeaderCount; return _TaskCard(task: tasks[taskIndex]); }, ), ); } } /// Карточка одной задачи расписания class _TaskCard extends ConsumerWidget { final ScheduleTask task; const _TaskCard({required this.task}); @override Widget build(BuildContext context, WidgetRef ref) { return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: Icon( task.isCron ? Icons.repeat : Icons.timer, color: Colors.deepOrange, ), title: Text( task.title, style: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( task.subtitle, style: const TextStyle(fontSize: 12, color: Colors.white54), ), trailing: IconButton( icon: const Icon(Icons.delete_outline, color: Colors.redAccent), onPressed: () => _confirmCancel(context, ref, task.jobId), ), ), ); } void _confirmCancel(BuildContext context, WidgetRef ref, String jobId) { showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Отменить задачу?'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Нет'), ), TextButton( onPressed: () async { Navigator.of(ctx).pop(); try { await ref.read(tasksProvider.notifier).cancel(jobId); } catch (e) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Ошибка отмены задачи: ${describeLoadError(e)}', ), ), ); } }, child: const Text('Да', style: TextStyle(color: Colors.redAccent)), ), ], ), ); } } /// Нижний лист для создания расписания class _AddScheduleSheet extends ConsumerStatefulWidget { const _AddScheduleSheet(); @override ConsumerState<_AddScheduleSheet> createState() => _AddScheduleSheetState(); } class _AddScheduleSheetState extends ConsumerState<_AddScheduleSheet> { /// Тип: 'once' или 'cron' String _type = 'once'; /// Общие поля String? _selectedGroupId; bool _targetState = false; // включить или выключить /// Поля для once int _hoursFromNow = 4; /// Поля для cron final _hourCtrl = TextEditingController(text: '22'); final _minuteCtrl = TextEditingController(text: '00'); final _dowCtrl = TextEditingController(text: '*'); @override void dispose() { _hourCtrl.dispose(); _minuteCtrl.dispose(); _dowCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final groups = ref.watch(groupsProvider); final bottomPadding = MediaQuery.of(context).viewInsets.bottom; final systemPadding = MediaQuery.of(context).padding.bottom; return Padding( padding: EdgeInsets.only( left: 20, right: 20, top: 20, bottom: bottomPadding + systemPadding + 20, ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Заголовок const Text( 'Новое расписание', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), // Тип расписания Row( children: [ ChoiceChip( label: const Text('Таймер'), selected: _type == 'once', selectedColor: Colors.deepOrange, onSelected: (_) => setState(() => _type = 'once'), ), const SizedBox(width: 8), ChoiceChip( label: const Text('Cron (повтор)'), selected: _type == 'cron', selectedColor: Colors.deepOrange, onSelected: (_) => setState(() => _type = 'cron'), ), ], ), const SizedBox(height: 16), // Выбор группы DropdownButtonFormField( initialValue: _selectedGroupId, decoration: const InputDecoration(labelText: 'Группа'), items: groups.map((g) { final id = g.id; final name = g.name; return DropdownMenuItem(value: id, child: Text(name)); }).toList(), onChanged: (v) => setState(() => _selectedGroupId = v), ), const SizedBox(height: 12), // Действие: включить / выключить SwitchListTile( title: Text(_targetState ? 'Включить' : 'Выключить'), subtitle: const Text('Действие при срабатывании'), value: _targetState, activeThumbColor: Colors.deepOrange, onChanged: (v) => setState(() => _targetState = v), contentPadding: EdgeInsets.zero, ), const SizedBox(height: 12), // ─── Once: через сколько часов ─── if (_type == 'once') ...[ Text( 'Через $_hoursFromNow ч.', style: const TextStyle(color: Colors.white70), ), Slider( value: _hoursFromNow.toDouble(), min: 1, max: 24, divisions: 23, label: '$_hoursFromNow ч.', activeColor: Colors.deepOrange, onChanged: (v) => setState(() => _hoursFromNow = v.toInt()), ), ], // ─── Cron: час, минута, дни недели ─── if (_type == 'cron') ...[ Row( children: [ Expanded( child: TextField( controller: _hourCtrl, decoration: const InputDecoration( labelText: 'Час (0-23)', ), keyboardType: TextInputType.number, ), ), const SizedBox(width: 12), Expanded( child: TextField( controller: _minuteCtrl, decoration: const InputDecoration( labelText: 'Минута (0-59)', ), keyboardType: TextInputType.number, ), ), ], ), const SizedBox(height: 12), TextField( controller: _dowCtrl, decoration: const InputDecoration( labelText: 'Дни недели (* = каждый, 0-6 = вс-сб)', helperText: 'Например: 1-5 (пн-пт), 0,6 (выходные)', ), ), ], const SizedBox(height: 20), // Кнопка создания SizedBox( width: double.infinity, height: 48, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.deepOrange, foregroundColor: Colors.white, ), onPressed: _selectedGroupId == null ? null : _save, child: const Text('СОЗДАТЬ'), ), ), ], ), ), ); } Future _save() async { if (_selectedGroupId == null) return; try { if (_type == 'once') { await ref .read(tasksProvider.notifier) .addOnce( targetId: _selectedGroupId!, targetState: _targetState, hoursFromNow: _hoursFromNow, ); } else { await ref .read(tasksProvider.notifier) .addCron( targetId: _selectedGroupId!, hour: _hourCtrl.text.trim(), minute: _minuteCtrl.text.trim(), dayOfWeek: _dowCtrl.text.trim(), targetState: _targetState, ); } if (mounted) Navigator.of(context).pop(); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Ошибка: ${describeLoadError(e)}')), ); } } } }