import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../services/nymea_service.dart'; import '../../theme/app_theme.dart'; import '../../theme/etm_theme.dart'; import '../../widgets/timeline_slot_card.dart'; /// Écran "Timeline & décisions" — vue horaire des décisions EMS. class TimelineScreen extends StatefulWidget { const TimelineScreen({super.key}); @override State createState() => _TimelineScreenState(); } class _TimelineScreenState extends State { String _horizon = '24h'; bool _loading = true; List _slots = []; Timer? _autoRefresh; static const _horizons = ['12h', '24h', '48h']; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => _load()); // Auto-refresh toutes les minutes _autoRefresh = Timer.periodic(const Duration(minutes: 1), (_) { if (mounted) _load(silent: true); }); } @override void dispose() { _autoRefresh?.cancel(); super.dispose(); } Future _load({bool silent = false}) async { if (!mounted) return; if (!silent) setState(() => _loading = true); // Génère des créneaux de démonstration (à brancher sur GetEnergyTimeline) await Future.delayed(const Duration(milliseconds: 400)); if (!mounted) return; setState(() { _slots = _generateDemoSlots(); _loading = false; }); } List _generateDemoSlots() { final now = DateTime.now(); final start = now.subtract(Duration(hours: now.hour)).copyWith( minute: 0, second: 0, microsecond: 0); final hours = _horizonHours(); final slots = []; for (int i = 0; i < hours; i++) { final slotStart = start.add(Duration(hours: i)); final isNow = now.isAfter(slotStart) && now.isBefore(slotStart.add(const Duration(hours: 1))); // Simulation d'une production PV en cloche final h = slotStart.hour.toDouble(); final solar = (h >= 7 && h <= 19) ? (2800 * _bell(h, 13, 4)).clamp(0.0, 5000.0) : 0.0; final home = 800 + (i % 3) * 200.0; final evW = (h >= 10 && h <= 15) ? 1200.0 : 0.0; final battery = (solar > home + evW) ? (solar - home - evW).clamp(0.0, 3000.0) : 0.0; final grid = (solar - home - evW - battery); slots.add(TimelineSlot( start: slotStart, end: slotStart.add(const Duration(hours: 1)), solarW: solar, homeW: home, evW: evW, batteryW: battery, gridW: grid, reasoning: _reasoning(solar, home, evW, battery, grid), savings: grid < 0 ? grid.abs() / 1000 * 0.12 : 0, selfSufficiency: solar > 0 ? (solar / (home + evW.abs())).clamp(0.0, 1.5) * 100 : 0, isNow: isNow, )); } return slots; } double _bell(double x, double mu, double sigma) { final d = (x - mu) / sigma; return 1.0 / (sigma * 2.5066) * (1 / (1 + d * d)); } String _reasoning(double solar, double home, double ev, double battery, double grid) { if (solar > home + ev + 500) { return 'Surplus solaire ${_kw(solar - home - ev)} — ' '${battery > 0 ? 'charge batterie + ' : ''}' '${ev > 0 ? 'recharge VE' : 'export réseau'}'; } else if (solar > 0 && solar >= home * 0.8) { return 'Solaire ≈ consommation de base — ' '${ev == 0 ? 'VE en pause' : 'recharge VE en cours'}'; } else { return 'Faible production solaire — import réseau ${_kw(grid.abs())}'; } } String _kw(double w) { return w >= 1000 ? '+${(w / 1000).toStringAsFixed(1)} kW' : '+${w.toStringAsFixed(0)} W'; } int _horizonHours() => switch (_horizon) { '12h' => 12, '48h' => 48, _ => 24, }; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF0F2F5), appBar: AppBar( title: const Text('Timeline & décisions'), backgroundColor: Colors.white, foregroundColor: const Color(0xFF1A1A2E), elevation: 0, actions: [ // Sélecteur d'horizon Padding( padding: const EdgeInsets.only(right: 4), child: DropdownButton( value: _horizon, underline: const SizedBox.shrink(), style: const TextStyle( fontSize: 13, color: Color(0xFF1A1A2E), fontWeight: FontWeight.w500), items: _horizons .map((h) => DropdownMenuItem(value: h, child: Text(h))) .toList(), onChanged: (v) { if (v != null && v != _horizon) { setState(() => _horizon = v); _load(); } }, ), ), // Bouton actualiser IconButton( icon: const Icon(Icons.refresh_rounded), tooltip: 'Actualiser', onPressed: _load, ), ], ), body: _loading ? const Center(child: CircularProgressIndicator()) : _slots.isEmpty ? _EmptyState(onRetry: _load) : ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: _slots.length, itemBuilder: (context, i) { final slot = _slots[i]; return TimelineSlotCard( slot: slot, onOverride: () => _showOverrideSheet(context, slot), ); }, ), ); } void _showOverrideSheet(BuildContext context, TimelineSlot slot) { showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (_) => _OverrideSheet(slot: slot), ); } } // ── Feuille d'override ──────────────────────────────────────────────────────── class _OverrideSheet extends StatefulWidget { final TimelineSlot slot; const _OverrideSheet({required this.slot}); @override State<_OverrideSheet> createState() => _OverrideSheetState(); } class _OverrideSheetState extends State<_OverrideSheet> { double _evW = 0; bool _dhwOn = false; double _batW = 0; // >0 charge, <0 décharge String _reason = ''; @override void initState() { super.initState(); _evW = widget.slot.evW; _batW = widget.slot.batteryW; } @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom), child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const Center( child: _DragHandle(), ), const SizedBox(height: 16), const Text('Modifier ce créneau', style: TextStyle( fontSize: 17, fontWeight: FontWeight.bold)), const SizedBox(height: 4), Text( '${_fmt(widget.slot.start)} – ${_fmt(widget.slot.end)}', style: const TextStyle( fontSize: 13, color: AppTheme.textLight), ), const SizedBox(height: 20), // VE Charger if (widget.slot.evW >= 0) ...[ const Text('🚗 VE Charger', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 13)), const SizedBox(height: 6), Row( children: [ const Text('0 W', style: TextStyle(fontSize: 11)), Expanded( child: Slider( value: _evW, min: 0, max: 7400, divisions: 37, label: '${_evW.toStringAsFixed(0)} W', activeColor: AppTheme.accentTeal, onChanged: (v) => setState(() => _evW = v), ), ), const Text('7.4 kW', style: TextStyle(fontSize: 11)), ], ), const SizedBox(height: 12), ], // Chauffe-eau Row( children: [ const Text('🌡️ Chauffe-eau', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 13)), const Spacer(), Switch.adaptive( value: _dhwOn, activeColor: AppTheme.primaryGreen, onChanged: (v) => setState(() => _dhwOn = v), ), ], ), const SizedBox(height: 12), // Batterie const Text('🔋 Batterie', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 13)), const SizedBox(height: 6), Row( children: [ const Text('Décharge', style: TextStyle(fontSize: 11)), Expanded( child: Slider( value: _batW, min: -3000, max: 3000, divisions: 30, label: _batW == 0 ? '0 (pause)' : '${_batW > 0 ? '+' : ''}${_batW.toStringAsFixed(0)} W', activeColor: AppTheme.batteryGreen, onChanged: (v) => setState(() => _batW = v), ), ), const Text('Charge', style: TextStyle(fontSize: 11)), ], ), const SizedBox(height: 16), // Raison TextFormField( initialValue: _reason, decoration: InputDecoration( labelText: 'Raison (optionnel)', hintText: 'Ex. Départ annulé ce soir', border: OutlineInputBorder( borderRadius: BorderRadius.circular(12)), contentPadding: const EdgeInsets.symmetric( horizontal: 14, vertical: 12), ), onChanged: (v) => _reason = v, ), const SizedBox(height: 20), // Boutons Row( children: [ Expanded( child: OutlinedButton( onPressed: () => Navigator.pop(context), style: OutlinedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(vertical: 14), ), child: const Text('Annuler'), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: ETMTheme.accentColor, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(vertical: 14), ), onPressed: () { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Override appliqué'), behavior: SnackBarBehavior.floating, ), ); }, child: const Text('Appliquer'), ), ), ], ), const SizedBox(height: 8), ], ), ), ), ); } String _fmt(DateTime dt) => '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } class _DragHandle extends StatelessWidget { const _DragHandle(); @override Widget build(BuildContext context) { return Container( width: 36, height: 4, decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(2), ), ); } } class _EmptyState extends StatelessWidget { final VoidCallback onRetry; const _EmptyState({required this.onRetry}); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.schedule_rounded, size: 48, color: Colors.grey.shade400), const SizedBox(height: 16), const Text('Aucune donnée de timeline', style: TextStyle(fontSize: 15)), const SizedBox(height: 8), const Text( 'Le scheduler n\'a pas encore généré de planification.', textAlign: TextAlign.center, style: TextStyle(color: AppTheme.textLight), ), const SizedBox(height: 16), ElevatedButton.icon( icon: const Icon(Icons.refresh_rounded, size: 18), label: const Text('Réessayer'), style: ElevatedButton.styleFrom( backgroundColor: ETMTheme.accentColor, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), ), onPressed: onRetry, ), ], ), ); } }