diff --git a/lib/features/sensors/sensor_detail_screen.dart b/lib/features/sensors/sensor_detail_screen.dart index a3fe195..80088b0 100644 --- a/lib/features/sensors/sensor_detail_screen.dart +++ b/lib/features/sensors/sensor_detail_screen.dart @@ -2,58 +2,178 @@ 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'; -class SensorDetailScreen extends ConsumerWidget { +class SensorDetailScreen extends ConsumerStatefulWidget { const SensorDetailScreen({super.key, required this.sensorId}); final String sensorId; @override - Widget build(BuildContext context, WidgetRef ref) { - // TODO: fetch sensor via sensorRepositoryProvider.getSensor(sensorId). + ConsumerState createState() => _SensorDetailScreenState(); +} + +class _SensorDetailScreenState extends ConsumerState { + late final int _id = int.parse(widget.sensorId); + + @override + Widget build(BuildContext context) { + final sensor = ref.watch(sensorProvider(_id)); return Scaffold( - appBar: AppBar(title: Text('Sensor $sensorId')), - body: Padding( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // TODO: display sensor fields (name, status, position, last seen). - const Placeholder(fallbackHeight: 200), - const SizedBox(height: 24), - OutlinedButton.icon( - icon: const Icon(Icons.map_outlined), - label: const Text('Locate on floor plan'), - onPressed: () { - ref.read(selectedSensorIdProvider.notifier).state = sensorId; - context.go('/floorplan'); - }, - ), - const SizedBox(height: 12), - // TODO: re-provision button → show BleProvisionSheet pre-filled. - OutlinedButton.icon( - icon: const Icon(Icons.bluetooth), - label: const Text('Re-provision WiFi'), - onPressed: () {}, // TODO - ), - const SizedBox(height: 12), - OutlinedButton.icon( - icon: const Icon(Icons.edit_outlined), - label: const Text('Rename'), - onPressed: () {}, // TODO - ), - const Spacer(), - TextButton( - style: TextButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.error, - ), - onPressed: () {}, // TODO: confirm dialog then delete - child: const Text('Delete sensor'), - ), - ], - ), + appBar: AppBar( + title: Text(sensor.valueOrNull?.displayName ?? 'Sensor'), + ), + body: sensor.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, _) => Center(child: Text(e.toString())), + data: (s) => _Body(sensor: s, id: _id, sensorId: widget.sensorId), + ), + ); + } +} + +class _Body extends ConsumerWidget { + const _Body({required this.sensor, required this.id, required this.sensorId}); + + final Sensor sensor; + final int id; + final String sensorId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _InfoRow(label: 'Device ID', value: sensor.sensorId), + if (sensor.rssiRef != null) + _InfoRow( + label: 'RSSI reference', + value: '${sensor.rssiRef!.toStringAsFixed(1)} dBm', + ), + _InfoRow( + label: 'Floor position', + value: sensor.isPlaced + ? '(${sensor.x!.toStringAsFixed(2)}, ${sensor.y!.toStringAsFixed(2)})' + : 'Not placed', + ), + const SizedBox(height: 24), + OutlinedButton.icon( + icon: const Icon(Icons.map_outlined), + label: const Text('Locate on floor plan'), + onPressed: () { + ref.read(selectedSensorIdProvider.notifier).state = sensorId; + context.go('/floorplan'); + }, + ), + const SizedBox(height: 12), + OutlinedButton.icon( + icon: const Icon(Icons.edit_outlined), + label: const Text('Rename'), + onPressed: () => _rename(context, ref), + ), + const SizedBox(height: 12), + OutlinedButton.icon( + icon: const Icon(Icons.bluetooth), + label: const Text('Re-provision WiFi'), + onPressed: () {}, // TODO: show BleProvisionSheet pre-filled + ), + const Spacer(), + TextButton( + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.error, + ), + onPressed: () => _delete(context, ref), + child: const Text('Delete sensor'), + ), + ], + ), + ); + } + + Future _rename(BuildContext context, WidgetRef ref) async { + final controller = TextEditingController(text: sensor.name); + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Rename sensor'), + content: TextField( + controller: controller, + decoration: const InputDecoration(labelText: 'Name'), + autofocus: true, + onSubmitted: (_) => Navigator.of(ctx).pop(true), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.of(ctx).pop(true), + child: const Text('Save'), + ), + ], + ), + ); + final name = controller.text.trim(); + controller.dispose(); + if (confirmed == true && name.isNotEmpty && context.mounted) { + await ref.read(sensorRepositoryProvider).updateSensor(id, name: name); + ref.invalidate(sensorProvider(id)); + ref.invalidate(sensorsProvider); + } + } + + Future _delete(BuildContext context, WidgetRef ref) async { + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Delete sensor?'), + content: const Text('This will unenrol the sensor from the system.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), + child: const Text('Cancel'), + ), + FilledButton( + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + ), + onPressed: () => Navigator.of(ctx).pop(true), + child: const Text('Delete'), + ), + ], + ), + ); + if (confirmed == true && context.mounted) { + await ref.read(sensorRepositoryProvider).deleteSensor(id); + ref.invalidate(sensorsProvider); + if (context.mounted) context.pop(); + } + } +} + +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: 8), + child: Row( + children: [ + Expanded( + child: Text(label, + style: Theme.of(context).textTheme.bodySmall), + ), + Text(value, style: Theme.of(context).textTheme.bodyMedium), + ], ), ); }