import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../core/api_client.dart'; import '../core/api_service.dart'; import '../models/automation_rule.dart'; import '../models/device.dart'; import '../models/device_type.dart'; import '../models/zone.dart'; const _operators = ['>', '<', '>=', '<=', '=', '!=']; /// Create/edit automation rule form. Pass an existing [rule] to edit it in /// place; omit it to create a new one. Pops with `true` on success. /// /// Dropdown options (sensor/actuator devices, sensor types, action types) /// are all derived from the backend's device/device-type data, never /// hardcoded — a new device_type with new capabilities shows up here /// automatically, per the platform's "don't hardcode the domain" rule. class AutomationRuleFormScreen extends StatefulWidget { const AutomationRuleFormScreen({super.key, this.rule}); final AutomationRule? rule; bool get isEditing => rule != null; @override State createState() => _AutomationRuleFormScreenState(); } class _AutomationRuleFormScreenState extends State { final _formKey = GlobalKey(); late final _conditionValueController = TextEditingController(text: widget.rule?.conditionValue.toString()); late final _levelController = TextEditingController(text: widget.rule?.level?.toString()); late Future<(List, List, List)> _optionsFuture; int? _zoneId; int? _targetDeviceId; int? _conditionSourceDeviceId; String? _conditionSensorType; late String _conditionOperator; String? _actionType; late bool _isActive; bool _isSubmitting = false; Map> _fieldErrors = {}; @override void initState() { super.initState(); _zoneId = widget.rule?.zone?.id; _targetDeviceId = widget.rule?.targetDevice?.id; _conditionSourceDeviceId = widget.rule?.conditionSourceDevice?.id; _conditionSensorType = widget.rule?.conditionSensorType; _conditionOperator = widget.rule?.conditionOperator ?? _operators.first; _actionType = widget.rule?.actionType; _isActive = widget.rule?.isActive ?? true; final apiService = context.read(); _optionsFuture = (apiService.zones(), apiService.devices(), apiService.deviceTypes()).wait; } @override void dispose() { _conditionValueController.dispose(); _levelController.dispose(); super.dispose(); } Future _submit() async { if (!_formKey.currentState!.validate()) return; final missing = >{ if (_zoneId == null) 'zone_id': ['Выберите зону'], if (_targetDeviceId == null) 'target_device_id': ['Выберите целевое устройство'], if (_conditionSourceDeviceId == null) 'condition_source_device_id': ['Выберите датчик'], if (_conditionSensorType == null) 'condition_sensor_type': ['Выберите тип показания'], if (_actionType == null) 'action_type': ['Выберите действие'], }; if (missing.isNotEmpty) { setState(() => _fieldErrors = missing); return; } setState(() { _isSubmitting = true; _fieldErrors = {}; }); final payload = { 'zone_id': _zoneId, 'target_device_id': _targetDeviceId, 'condition_source_device_id': _conditionSourceDeviceId, 'condition_sensor_type': _conditionSensorType, 'condition_operator': _conditionOperator, 'condition_value': double.parse(_conditionValueController.text), 'action_type': _actionType, 'is_active': _isActive, if (_actionType == 'set_level') 'level': double.parse(_levelController.text), }; final apiService = context.read(); try { if (widget.isEditing) { await apiService.updateAutomationRule(widget.rule!.id, payload); } else { await apiService.createAutomationRule(payload); } if (mounted) Navigator.of(context).pop(true); } on ApiException catch (e) { setState(() => _fieldErrors = e.errors ?? {}); if (e.errors == null && mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.message))); } } finally { if (mounted) setState(() => _isSubmitting = false); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(widget.isEditing ? 'Редактировать правило' : 'Новое правило')), body: FutureBuilder<(List, List, List)>( future: _optionsFuture, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Center(child: Text('${snapshot.error}')); } final (zones, devices, deviceTypes) = snapshot.data!; final sensorDevices = devices.where((d) => d.deviceType?.isSensor ?? false).toList(); final actuatorDevices = devices.where((d) => d.deviceType?.isActuator ?? false).toList(); final sensorTypeOptions = deviceTypes .where((t) => t.isSensor) .expand((t) => t.capabilities) .toSet() .toList() ..sort(); final actionTypeOptions = deviceTypes .where((t) => t.isActuator) .expand((t) => t.capabilities) .toSet() .toList() ..sort(); return Form( key: _formKey, child: ListView( padding: const EdgeInsets.all(16), children: [ DropdownButtonFormField( initialValue: _zoneId, decoration: InputDecoration(labelText: 'Зона', errorText: _fieldErrors['zone_id']?.first), items: zones.map((z) => DropdownMenuItem(value: z.id, child: Text(z.name))).toList(), onChanged: (value) => setState(() => _zoneId = value), ), const SizedBox(height: 24), Text('ЕСЛИ', style: Theme.of(context).textTheme.titleSmall), const SizedBox(height: 8), DropdownButtonFormField( initialValue: _conditionSourceDeviceId, decoration: InputDecoration( labelText: 'Датчик', errorText: _fieldErrors['condition_source_device_id']?.first, ), items: sensorDevices.map((d) => DropdownMenuItem(value: d.id, child: Text(d.name))).toList(), onChanged: (value) => setState(() => _conditionSourceDeviceId = value), ), const SizedBox(height: 16), DropdownButtonFormField( initialValue: _conditionSensorType, decoration: InputDecoration( labelText: 'Тип показания', errorText: _fieldErrors['condition_sensor_type']?.first, ), items: sensorTypeOptions.map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(), onChanged: (value) => setState(() => _conditionSensorType = value), ), const SizedBox(height: 16), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: DropdownButtonFormField( initialValue: _conditionOperator, decoration: const InputDecoration(labelText: 'Оператор'), items: _operators.map((op) => DropdownMenuItem(value: op, child: Text(op))).toList(), onChanged: (value) => setState(() => _conditionOperator = value!), ), ), const SizedBox(width: 12), Expanded( child: TextFormField( controller: _conditionValueController, decoration: const InputDecoration(labelText: 'Значение'), keyboardType: const TextInputType.numberWithOptions(decimal: true), validator: (value) => (value == null || double.tryParse(value) == null) ? 'Введите число' : null, ), ), ], ), const SizedBox(height: 24), Text('ТО', style: Theme.of(context).textTheme.titleSmall), const SizedBox(height: 8), DropdownButtonFormField( initialValue: _actionType, decoration: InputDecoration(labelText: 'Действие', errorText: _fieldErrors['action_type']?.first), items: actionTypeOptions.map((a) => DropdownMenuItem(value: a, child: Text(a))).toList(), onChanged: (value) => setState(() => _actionType = value), ), if (_actionType == 'set_level') ...[ const SizedBox(height: 16), TextFormField( controller: _levelController, decoration: const InputDecoration(labelText: 'Уровень'), keyboardType: const TextInputType.numberWithOptions(decimal: true), validator: (value) => (_actionType == 'set_level' && double.tryParse(value ?? '') == null) ? 'Введите число' : null, ), ], const SizedBox(height: 16), DropdownButtonFormField( initialValue: _targetDeviceId, decoration: InputDecoration( labelText: 'Целевое устройство', errorText: _fieldErrors['target_device_id']?.first, ), items: actuatorDevices.map((d) => DropdownMenuItem(value: d.id, child: Text(d.name))).toList(), onChanged: (value) => setState(() => _targetDeviceId = value), ), const SizedBox(height: 16), SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('Активно'), value: _isActive, onChanged: (value) => setState(() => _isActive = value), ), const SizedBox(height: 24), FilledButton( onPressed: _isSubmitting ? null : _submit, child: _isSubmitting ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2)) : Text(widget.isEditing ? 'Сохранить' : 'Создать'), ), ], ), ); }, ), ); } }