import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/providers.dart'; /// Экран управления расписаниями. /// Показывает все задачи (one-shot и cron), позволяет создавать и удалять. class SchedulesScreen extends ConsumerStatefulWidget { const SchedulesScreen({super.key}); @override ConsumerState createState() => _SchedulesScreenState(); } class _SchedulesScreenState extends ConsumerState { bool _loading = true; @override void initState() { super.initState(); _load(); } Future _load() async { await ref.read(tasksProvider.notifier).load(); if (mounted) setState(() => _loading = false); } @override Widget build(BuildContext context) { final tasks = ref.watch(tasksProvider); return Scaffold( appBar: AppBar( title: const Text('РАСПИСАНИЯ'), ), body: _loading ? const Center(child: CircularProgressIndicator(color: Colors.deepOrange)) : tasks.isEmpty ? 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), ), ], ), ) : RefreshIndicator( color: Colors.deepOrange, onRefresh: () => ref.read(tasksProvider.notifier).load(), child: ListView.builder( padding: const EdgeInsets.all(12), itemCount: tasks.length, itemBuilder: (context, index) { final task = tasks[index]; return _TaskCard(task: task); }, ), ), 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) => _AddScheduleSheet(), ); } } /// Карточка одной задачи расписания class _TaskCard extends ConsumerWidget { final dynamic task; const _TaskCard({required this.task}); @override Widget build(BuildContext context, WidgetRef ref) { final map = task is Map ? Map.from(task) : {}; final jobId = (map['id'] ?? map['job_id'] ?? '').toString(); final targetId = (map['target_id'] ?? map['target'] ?? '').toString(); final state = map['state']; final runAt = map['run_at'] ?? map['next_run'] ?? map['next_run_time']; final type = map['type'] ?? (map['cron'] != null ? 'cron' : 'once'); // Формирование описания final stateStr = state == true ? 'Включить' : state == false ? 'Выключить' : '?'; String subtitle = 'Цель: $targetId'; if (runAt != null) subtitle += '\nЗапуск: $runAt'; if (map['cron'] != null) subtitle += '\nCron: ${map['cron']}'; if (map['hour'] != null && map['minute'] != null) { subtitle += '\nВремя: ${map['hour']}:${map['minute']}'; } if (map['day_of_week'] != null && map['day_of_week'] != '*') { subtitle += ' (дни: ${map['day_of_week']})'; } return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: Icon( type == 'cron' ? Icons.repeat : Icons.timer, color: Colors.deepOrange, ), title: Text( '$stateStr - $targetId', style: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( subtitle, style: const TextStyle(fontSize: 12, color: Colors.white54), ), trailing: IconButton( icon: const Icon(Icons.delete_outline, color: Colors.redAccent), onPressed: () => _confirmCancel(context, ref, 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: () { Navigator.of(ctx).pop(); ref.read(tasksProvider.notifier).cancel(jobId); }, child: const Text('Да', style: TextStyle(color: Colors.redAccent)), ), ], ), ); } } /// Нижний лист для создания расписания class _AddScheduleSheet extends ConsumerStatefulWidget { @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); return Padding( padding: EdgeInsets.only( left: 20, right: 20, top: 20, bottom: MediaQuery.of(context).viewInsets.bottom + 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( value: _selectedGroupId, decoration: const InputDecoration(labelText: 'Группа'), items: groups.map((g) { final id = g['id'].toString(); final name = g['name']?.toString() ?? id; 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, activeColor: 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('Ошибка: $e')), ); } } } }