ZoneFormScreen/DeviceFormScreen/AutomationRuleFormScreen — одна форма на создание и редактирование, показ ошибок валидации Laravel (422) по полям. Списки зон/устройств/правил дополнены FAB "добавить", свайпом/меню удаления и переходом в форму по тапу — всё только для owner (RBAC), viewer видит те же списки в режиме только чтения. Опции дропдаунов (типы устройств, датчики/актуаторы, capabilities) берутся из данных бэкенда, не хардкодятся.
99 lines
3.2 KiB
Dart
99 lines
3.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../core/api_client.dart';
|
|
import '../core/api_service.dart';
|
|
import '../models/zone.dart';
|
|
|
|
/// Create/edit zone form. Pass an existing [zone] to edit it in place;
|
|
/// omit it to create a new one. Pops with `true` on success so the caller
|
|
/// knows to refresh its list.
|
|
class ZoneFormScreen extends StatefulWidget {
|
|
const ZoneFormScreen({super.key, this.zone});
|
|
|
|
final Zone? zone;
|
|
|
|
bool get isEditing => zone != null;
|
|
|
|
@override
|
|
State<ZoneFormScreen> createState() => _ZoneFormScreenState();
|
|
}
|
|
|
|
class _ZoneFormScreenState extends State<ZoneFormScreen> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
late final _nameController = TextEditingController(text: widget.zone?.name);
|
|
late final _descriptionController = TextEditingController(text: widget.zone?.description);
|
|
|
|
bool _isSubmitting = false;
|
|
Map<String, List<String>> _fieldErrors = {};
|
|
|
|
@override
|
|
void dispose() {
|
|
_nameController.dispose();
|
|
_descriptionController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _submit() async {
|
|
if (!_formKey.currentState!.validate()) return;
|
|
|
|
setState(() {
|
|
_isSubmitting = true;
|
|
_fieldErrors = {};
|
|
});
|
|
|
|
final apiService = context.read<ApiService>();
|
|
final name = _nameController.text.trim();
|
|
final description = _descriptionController.text.trim();
|
|
|
|
try {
|
|
if (widget.isEditing) {
|
|
await apiService.updateZone(widget.zone!.id, name: name, description: description);
|
|
} else {
|
|
await apiService.createZone(name: name, description: description);
|
|
}
|
|
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: Form(
|
|
key: _formKey,
|
|
child: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
TextFormField(
|
|
controller: _nameController,
|
|
decoration: InputDecoration(labelText: 'Название', errorText: _fieldErrors['name']?.first),
|
|
validator: (value) => (value == null || value.trim().isEmpty) ? 'Введите название' : null,
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextFormField(
|
|
controller: _descriptionController,
|
|
decoration: InputDecoration(labelText: 'Описание', errorText: _fieldErrors['description']?.first),
|
|
maxLines: 3,
|
|
),
|
|
const SizedBox(height: 24),
|
|
FilledButton(
|
|
onPressed: _isSubmitting ? null : _submit,
|
|
child: _isSubmitting
|
|
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2))
|
|
: Text(widget.isEditing ? 'Сохранить' : 'Создать'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|