import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../main.dart' show DrawerMenuButton; import '../services/nymea_service.dart'; import '../theme/app_theme.dart'; import '../widgets/energy_flow_widget.dart'; import '../widgets/production_card.dart'; import '../widgets/ev_charging_card.dart'; import '../widgets/gains_card.dart'; class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); @override State createState() => _DashboardScreenState(); } class _DashboardScreenState extends State { @override void initState() { super.initState(); // Start simulation on first load WidgetsBinding.instance.addPostFrameCallback((_) { final service = context.read(); if (!service.connected) { service.startSimulation(); } }); } @override Widget build(BuildContext context) { return Consumer( builder: (context, service, _) { final data = service.energyData; return Scaffold( backgroundColor: AppTheme.backgroundGray, body: RefreshIndicator( onRefresh: () async { service.startSimulation(); }, color: AppTheme.primaryGreen, child: CustomScrollView( slivers: [ // App bar SliverAppBar( floating: true, backgroundColor: AppTheme.backgroundGray, elevation: 0, leading: const DrawerMenuButton(), leadingWidth: 56, title: const Text( 'ETM PowerSync', style: TextStyle( fontWeight: FontWeight.bold, color: AppTheme.textDark, fontSize: 18, ), ), actions: [ // Connection status Padding( padding: const EdgeInsets.only(right: 8), child: IconButton( icon: Icon( service.connected ? Icons.wifi_rounded : Icons.wifi_off_rounded, color: service.connected ? AppTheme.primaryGreen : Colors.red, ), onPressed: () => _showConnectionDialog(context, service), ), ), Padding( padding: const EdgeInsets.only(right: 12), child: IconButton( icon: const Icon(Icons.notifications_outlined, color: AppTheme.textDark), onPressed: () {}, ), ), ], ), SliverPadding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), sliver: SliverList( delegate: SliverChildListDelegate([ // Energy flow EnergyFlowWidget(data: data), const SizedBox(height: 12), // Self-consumption rates _RatesRow(data: data), const SizedBox(height: 12), // Production card ProductionCard(data: data), const SizedBox(height: 12), // Gains GainsCard(data: data), const SizedBox(height: 12), // EV Charging EVChargingCard(data: data, service: service), const SizedBox(height: 16), ]), ), ), ], ), ), ); }, ); } void _showConnectionDialog(BuildContext context, NymeaService service) { showDialog( context: context, builder: (ctx) => _ConnectionDialog(service: service), ); } } class _RatesRow extends StatelessWidget { final dynamic data; const _RatesRow({required this.data}); @override Widget build(BuildContext context) { return Row( children: [ Expanded( child: _RateCard( label: 'Autoconsommation', value: data.selfConsumptionRate, icon: Icons.wb_sunny_rounded, color: AppTheme.solarYellow, ), ), const SizedBox(width: 12), Expanded( child: _RateCard( label: 'Autonomie', value: data.autonomyRate, icon: Icons.home_rounded, color: AppTheme.homeBlue, ), ), ], ); } } class _RateCard extends StatelessWidget { final String label; final double value; final IconData icon; final Color color; const _RateCard({ required this.label, required this.value, required this.icon, required this.color, }); @override Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(icon, color: color, size: 18), const SizedBox(width: 6), Expanded( child: Text( label, style: const TextStyle( fontSize: 12, color: AppTheme.textLight), overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 8), Text( '${value.toStringAsFixed(1)}%', style: TextStyle( fontSize: 26, fontWeight: FontWeight.bold, color: color, ), ), const SizedBox(height: 6), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: (value / 100).clamp(0, 1), backgroundColor: color.withValues(alpha:0.15), valueColor: AlwaysStoppedAnimation(color), minHeight: 6, ), ), ], ), ), ); } } // ═══════════════════════════════════════════════════════════════════════════ // Dialog de connexion avec sélection du protocole TCP / WebSocket // ═══════════════════════════════════════════════════════════════════════════ class _ConnectionDialog extends StatefulWidget { final NymeaService service; const _ConnectionDialog({required this.service}); @override State<_ConnectionDialog> createState() => _ConnectionDialogState(); } class _ConnectionDialogState extends State<_ConnectionDialog> { late TextEditingController _hostCtrl; late TextEditingController _portCtrl; late TextEditingController _userCtrl; late TextEditingController _passCtrl; late NymeaProtocol _protocol; bool _connecting = false; bool _verbose = false; // Ports par défaut selon protocole static const _defaultPorts = { NymeaProtocol.tcpRaw: 2222, NymeaProtocol.webSocket: 4444, }; @override void initState() { super.initState(); _protocol = widget.service.protocol; _hostCtrl = TextEditingController(text: widget.service.host); _portCtrl = TextEditingController(text: widget.service.port.toString()); _userCtrl = TextEditingController(text: widget.service.username); _passCtrl = TextEditingController(text: widget.service.password); _verbose = widget.service.verboseLog; } @override void dispose() { _hostCtrl.dispose(); _portCtrl.dispose(); _userCtrl.dispose(); _passCtrl.dispose(); super.dispose(); } void _onProtocolChanged(NymeaProtocol p) { setState(() { _protocol = p; // Mettre à jour le port par défaut si l'utilisateur n'a pas changé final currentPort = int.tryParse(_portCtrl.text) ?? 0; final otherDefault = _defaultPorts[_protocol == NymeaProtocol.tcpRaw ? NymeaProtocol.webSocket : NymeaProtocol.tcpRaw]!; if (currentPort == otherDefault || currentPort == 0) { _portCtrl.text = _defaultPorts[p]!.toString(); } }); } @override Widget build(BuildContext context) { return AlertDialog( title: Row( children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: AppTheme.primaryGreen.withValues(alpha:0.12), borderRadius: BorderRadius.circular(10), ), child: const Icon(Icons.router_rounded, color: AppTheme.primaryGreen, size: 20), ), const SizedBox(width: 10), const Text('Connexion Nymea'), ], ), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── Sélecteur de protocole ───────────────────────────────────── const Text('Protocole', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: AppTheme.textLight)), const SizedBox(height: 8), Container( decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10), ), child: Row( children: [ _ProtoTab( label: 'TCP brut', subtitle: 'port 2222', icon: Icons.cable_rounded, selected: _protocol == NymeaProtocol.tcpRaw, onTap: () => _onProtocolChanged(NymeaProtocol.tcpRaw), ), _ProtoTab( label: 'WebSocket', subtitle: 'port 4444', icon: Icons.wifi_rounded, selected: _protocol == NymeaProtocol.webSocket, onTap: () => _onProtocolChanged(NymeaProtocol.webSocket), ), ], ), ), const SizedBox(height: 6), // Info protocole Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppTheme.primaryGreen.withValues(alpha:0.07), borderRadius: BorderRadius.circular(8), ), child: Text( _protocol == NymeaProtocol.tcpRaw ? '💡 TCP brut : nymea envoie un message de bienvenue dès la connexion. Utilisé par l\'app nymea officielle.' : '💡 WebSocket : le client envoie JSONRPC.Hello en premier. Activez-le dans nymea → Paramètres → Serveur WebSocket.', style: const TextStyle(fontSize: 11, color: AppTheme.textLight), ), ), const SizedBox(height: 14), // ── Hôte ────────────────────────────────────────────────────── TextField( controller: _hostCtrl, decoration: const InputDecoration( labelText: 'Adresse IP / Hôte', prefixIcon: Icon(Icons.dns_outlined), border: OutlineInputBorder(), isDense: true, ), ), const SizedBox(height: 10), // ── Port ────────────────────────────────────────────────────── TextField( controller: _portCtrl, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Port', prefixIcon: Icon(Icons.settings_ethernet), border: OutlineInputBorder(), isDense: true, ), ), const SizedBox(height: 10), // ── Identifiants ────────────────────────────────────────── TextField( controller: _userCtrl, decoration: const InputDecoration( labelText: 'Utilisateur', prefixIcon: Icon(Icons.person_outline), border: OutlineInputBorder(), isDense: true, ), ), const SizedBox(height: 10), TextField( controller: _passCtrl, obscureText: true, decoration: const InputDecoration( labelText: 'Mot de passe', prefixIcon: Icon(Icons.lock_outline), border: OutlineInputBorder(), isDense: true, ), ), // ── Debug verbose ───────────────────────────────────────── const SizedBox(height: 8), Row( children: [ const Icon(Icons.terminal_rounded, size: 16, color: AppTheme.textLight), const SizedBox(width: 8), const Expanded( child: Text('Logs JSON verbose', style: TextStyle(fontSize: 13, color: AppTheme.textDark)), ), Switch( value: _verbose, activeThumbColor: AppTheme.primaryGreen, onChanged: (v) { setState(() => _verbose = v); widget.service.verboseLog = v; }, ), ], ), // ── Erreur ──────────────────────────────────────────────────── if (widget.service.connectionError != null) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ const Icon(Icons.error_outline, color: Colors.red, size: 16), const SizedBox(width: 6), Expanded( child: Text( widget.service.connectionError!, style: const TextStyle(color: Colors.red, fontSize: 11), ), ), ], ), ), ], ], ), actions: [ // Mode démo TextButton.icon( icon: const Icon(Icons.science_outlined, size: 16), label: const Text('Mode démo'), onPressed: () { widget.service.startSimulation(); Navigator.pop(context); }, ), // Connecter ElevatedButton.icon( icon: _connecting ? const SizedBox( width: 14, height: 14, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white)) : const Icon(Icons.link_rounded, size: 16), label: const Text('Connecter'), onPressed: _connecting ? null : () async { setState(() => _connecting = true); final ok = await widget.service.connect( _hostCtrl.text.trim(), int.tryParse(_portCtrl.text.trim()) ?? _defaultPorts[_protocol]!, protocol: _protocol, username: _userCtrl.text.trim(), password: _passCtrl.text, ); if (ok && context.mounted) Navigator.pop(context); if (mounted) setState(() => _connecting = false); }, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primaryGreen, foregroundColor: Colors.white, ), ), ], ); } } class _ProtoTab extends StatelessWidget { final String label; final String subtitle; final IconData icon; final bool selected; final VoidCallback onTap; const _ProtoTab({ required this.label, required this.subtitle, required this.icon, required this.selected, required this.onTap, }); @override Widget build(BuildContext context) { return Expanded( child: GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), margin: const EdgeInsets.all(4), padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: selected ? Colors.white : Colors.transparent, borderRadius: BorderRadius.circular(8), boxShadow: selected ? [BoxShadow( color: Colors.black.withValues(alpha:0.08), blurRadius: 4, offset: const Offset(0, 1))] : null, ), child: Column( children: [ Icon(icon, size: 20, color: selected ? AppTheme.primaryGreen : AppTheme.textLight), const SizedBox(height: 4), Text(label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: selected ? AppTheme.primaryGreen : AppTheme.textLight)), Text(subtitle, style: const TextStyle( fontSize: 10, color: AppTheme.textLight)), ], ), ), ), ); } }