import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../domain/models/sensor.dart'; import '../../providers.dart'; void showSensorDetailSheet(BuildContext context, int sensorId) { showModalBottomSheet( context: context, useRootNavigator: true, isScrollControlled: true, builder: (_) => SensorDetailSheet(sensorId: sensorId), ); } class SensorDetailSheet extends ConsumerStatefulWidget { const SensorDetailSheet({super.key, required this.sensorId}); final int sensorId; @override ConsumerState createState() => _SensorDetailSheetState(); } class _SensorDetailSheetState extends ConsumerState { bool _editing = false; final _nameCtrl = TextEditingController(); @override void dispose() { _nameCtrl.dispose(); super.dispose(); } Future _saveName(Sensor sensor) async { final name = _nameCtrl.text.trim(); setState(() => _editing = false); final unchanged = name == (sensor.name ?? '') || (name.isEmpty && sensor.name == null); if (unchanged) return; await ref .read(sensorRepositoryProvider) .updateSensor(sensor.id, name: name.isEmpty ? null : name); ref.invalidate(sensorProvider(sensor.id)); ref.invalidate(sensorsProvider); } Future _delete(BuildContext context, Sensor sensor) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Delete sensor?'), content: const Text('This will unenroll the sensor from the system.'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Cancel'), ), FilledButton( style: FilledButton.styleFrom( backgroundColor: Theme.of(ctx).colorScheme.error, ), onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Delete'), ), ], ), ); if (confirmed == true && context.mounted) { await ref.read(sensorRepositoryProvider).deleteSensor(sensor.id); ref.invalidate(sensorsProvider); if (context.mounted) Navigator.of(context, rootNavigator: true).pop(); } } void _placeOnFloorPlan(BuildContext context, Sensor sensor) { final router = GoRouter.of(context); final fromSensors = router.routeInformationProvider.value.uri.path == '/sensors'; ref.read(sensorPlacementProvider.notifier).state = sensor; ref.read(sensorPlacementOriginSensorsProvider.notifier).state = fromSensors; Navigator.of(context, rootNavigator: true).pop(); router.go('/floorplan'); } @override Widget build(BuildContext context) { final sensorAsync = ref.watch(sensorProvider(widget.sensorId)); final roomsAsync = ref.watch(roomsProvider); final versionAsync = ref.watch(sensorVersionProvider(widget.sensorId)); return sensorAsync.when( loading: () => const SizedBox( height: 200, child: Center(child: CircularProgressIndicator()), ), error: (e, _) => SizedBox( height: 200, child: Center(child: Text(e.toString())), ), data: (sensor) { // Keep controller in sync when not actively editing. if (!_editing) _nameCtrl.text = sensor.name ?? ''; final roomName = roomsAsync.whenOrNull( data: (rooms) => rooms .where((r) => r.id == sensor.roomId) .firstOrNull ?.name, ); final version = versionAsync.whenOrNull(data: (v) => v); return _SheetBody( sensor: sensor, roomName: roomName, version: version, editing: _editing, nameCtrl: _nameCtrl, onEditToggle: () => setState(() { _editing = true; _nameCtrl.text = sensor.name ?? ''; }), onSaveName: () => _saveName(sensor), onPlace: () => _placeOnFloorPlan(context, sensor), onDelete: () => _delete(context, sensor), ); }, ); } } class _SheetBody extends StatelessWidget { const _SheetBody({ required this.sensor, required this.roomName, required this.version, required this.editing, required this.nameCtrl, required this.onEditToggle, required this.onSaveName, required this.onPlace, required this.onDelete, }); final Sensor sensor; final String? roomName; final String? version; final bool editing; final TextEditingController nameCtrl; final VoidCallback onEditToggle; final VoidCallback onSaveName; final VoidCallback onPlace; final VoidCallback onDelete; @override Widget build(BuildContext context) { final theme = Theme.of(context); return SafeArea( child: Padding( padding: EdgeInsets.fromLTRB( 24, 12, 24, MediaQuery.of(context).viewInsets.bottom + 24, ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ Center( child: Container( width: 32, height: 4, decoration: BoxDecoration( color: theme.colorScheme.outlineVariant, borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(height: 16), Row( children: [ Expanded( child: editing ? TextField( controller: nameCtrl, autofocus: true, style: theme.textTheme.titleLarge, decoration: const InputDecoration( isDense: true, border: InputBorder.none, ), onSubmitted: (_) => onSaveName(), ) : Text( sensor.displayName, style: theme.textTheme.titleLarge, ), ), IconButton( icon: Icon(editing ? Icons.check : Icons.edit_outlined), tooltip: editing ? 'Save name' : 'Rename', onPressed: editing ? onSaveName : onEditToggle, ), ], ), const SizedBox(height: 16), _InfoRow(label: 'Device ID', value: sensor.sensorId), _InfoRow(label: 'Room', value: roomName ?? '—'), _InfoRow( label: 'Position', value: sensor.isPlaced ? '(${sensor.x!.toStringAsFixed(2)}, ${sensor.y!.toStringAsFixed(2)})' : 'Not placed', ), _InfoRow(label: 'Firmware', value: version ?? '—'), const SizedBox(height: 24), FilledButton.icon( icon: Icon(Icons.my_location), label: Text(sensor.isPlaced ? 'Reposition on floor plan' : 'Place on floor plan'), onPressed: onPlace, ), const SizedBox(height: 12), OutlinedButton.icon( icon: const Icon(Icons.bluetooth), label: const Text('Reprovision'), onPressed: () {}, ), const SizedBox(height: 24), TextButton( style: TextButton.styleFrom( foregroundColor: theme.colorScheme.error, ), onPressed: onDelete, child: const Text('Delete sensor'), ), ], ), ), ); } } class _InfoRow extends StatelessWidget { const _InfoRow({required this.label, required this.value}); final String label; final String value; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ Expanded( child: Text(label, style: Theme.of(context).textTheme.bodySmall), ), Text(value, style: Theme.of(context).textTheme.bodyMedium), ], ), ); } }