Files
ignis_app/lib/screens/schedules_screen.dart
2026-04-22 21:08:02 +07:00

363 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<SchedulesScreen> createState() => _SchedulesScreenState();
}
class _SchedulesScreenState extends ConsumerState<SchedulesScreen> {
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _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) => const _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<String, dynamic>.from(task) : <String, dynamic>{};
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 {
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<String>(
initialValue: _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,
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<void> _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')),
);
}
}
}
}