import 'package:flutter/material.dart'; import '../models/energy_data.dart'; import '../services/nymea_service.dart'; import '../theme/etm_tokens.dart'; class EVChargingCard extends StatefulWidget { final EnergyData data; final NymeaService service; const EVChargingCard({ super.key, required this.data, required this.service, }); @override State createState() => _EVChargingCardState(); } class _EVChargingCardState extends State { // Deadline option state — local UI, not persisted bool _deadlineEnabled = false; int _targetSoc = 80; DateTime _endTime = DateTime.now().add(const Duration(hours: 4)); void _selectMode(ChargingMode mode) { // Selecting Boost auto-disables deadline if (mode == ChargingMode.boost && _deadlineEnabled) { setState(() => _deadlineEnabled = false); } widget.service.setChargingInfo( mode: mode, deadline: mode != ChargingMode.boost && _deadlineEnabled, targetSoc: _targetSoc, endTime: _endTime, ); } void _toggleDeadline(bool enabled) { setState(() => _deadlineEnabled = enabled); widget.service.setChargingInfo( mode: widget.data.chargingMode, deadline: enabled, targetSoc: _targetSoc, endTime: _endTime, ); } void _applyDeadline() { widget.service.setChargingInfo( mode: widget.data.chargingMode, deadline: _deadlineEnabled, targetSoc: _targetSoc, endTime: _endTime, ); } String _statusLabel(ChargingMode mode) { switch (mode) { case ChargingMode.pv: return _deadlineEnabled ? 'Surplus PV → Boost auto avant deadline' : 'Surplus PV disponible'; case ChargingMode.minPv: return _deadlineEnabled ? 'Min garanti + PV → Boost si deadline' : 'Minimum + surplus PV'; case ChargingMode.boost: return 'Charge rapide (réseau)'; } } Future _pickEndTime() async { final initial = TimeOfDay.fromDateTime(_endTime); final picked = await showTimePicker(context: context, initialTime: initial); if (picked != null) { setState(() { final now = DateTime.now(); _endTime = DateTime(now.year, now.month, now.day, picked.hour, picked.minute); // If picked time is in the past, roll to tomorrow if (_endTime.isBefore(now)) { _endTime = _endTime.add(const Duration(days: 1)); } }); _applyDeadline(); } } @override Widget build(BuildContext context) { final mode = widget.data.chargingMode; final data = widget.data; final showDeadlineOption = mode != ChargingMode.boost; return Container( decoration: BoxDecoration( color: EtmTokens.card, borderRadius: BorderRadius.circular(EtmTokens.radiusLg), boxShadow: EtmTokens.cardShadow, ), padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Row( children: [ Container( width: 34, height: 34, decoration: BoxDecoration( color: EtmTokens.navy.withValues(alpha: 0.07), borderRadius: BorderRadius.circular(10), ), child: const Icon(Icons.ev_station_rounded, color: EtmTokens.navy, size: 20), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Borne de recharge (EVSE)', style: EtmTokens.sans(size: 15, weight: FontWeight.w600)), Text(_statusLabel(mode), style: EtmTokens.sans(size: 12, color: EtmTokens.muted)), ], ), ), GestureDetector( onTap: () => _showSettings(context), child: const Icon(Icons.tune, color: EtmTokens.faint, size: 20), ), ], ), const SizedBox(height: 14), // Status badge + power Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: EtmTokens.greenSoft, borderRadius: BorderRadius.circular(99), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container(width: 6, height: 6, decoration: const BoxDecoration(color: EtmTokens.green, shape: BoxShape.circle)), const SizedBox(width: 5), Text('En charge', style: EtmTokens.sans(size: 12, weight: FontWeight.w600, color: EtmTokens.greenDark)), ], ), ), const SizedBox(height: 8), Text( '${(data.chargingPower * 1000).toStringAsFixed(0)}', style: EtmTokens.mono(size: 38, weight: FontWeight.w700), ), Text('W Puissance actuelle', style: EtmTokens.sans(size: 12, color: EtmTokens.muted)), ], ), const Spacer(), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('${data.solarSourcePercent.toStringAsFixed(0)}%', style: EtmTokens.mono(size: 22, color: EtmTokens.green)), Text('solaire', style: EtmTokens.sans(size: 11, color: EtmTokens.muted)), ], ), ], ), const SizedBox(height: 16), // Mode buttons — 3 boutons : PV / Min+PV / Boost Row( children: [ _ModeButton( label: 'PV', icon: Icons.wb_sunny_rounded, color: EtmTokens.amber, isSelected: mode == ChargingMode.pv, onTap: () => _selectMode(ChargingMode.pv), ), const SizedBox(width: 8), _ModeButton( label: 'Min+PV', icon: Icons.bolt, color: EtmTokens.blue, isSelected: mode == ChargingMode.minPv, onTap: () => _selectMode(ChargingMode.minPv), ), const SizedBox(width: 8), _ModeButton( label: 'Boost', icon: Icons.rocket_launch_rounded, color: EtmTokens.green, isSelected: mode == ChargingMode.boost, onTap: () => _selectMode(ChargingMode.boost), ), ], ), // Deadline option — visible pour PV et Min+PV if (showDeadlineOption) ...[ const SizedBox(height: 8), _DeadlineToggleRow( enabled: _deadlineEnabled, onChanged: _toggleDeadline, ), if (_deadlineEnabled) ...[ const SizedBox(height: 10), _DeadlineParamsRow( targetSoc: _targetSoc, endTime: _endTime, onSocChanged: (v) { setState(() => _targetSoc = v); _applyDeadline(); }, onPickTime: _pickEndTime, ), ], ], const SizedBox(height: 14), // SOC progress _SocProgress(targetSoc: _targetSoc, currentSoc: 62), ], ), ); } void _showSettings(BuildContext context) { showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (_) => const _ChargingSettingsSheet(), ); } } class _DeadlineToggleRow extends StatelessWidget { final bool enabled; final ValueChanged onChanged; const _DeadlineToggleRow({required this.enabled, required this.onChanged}); @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon(Icons.flag_outlined, size: 16, color: enabled ? EtmTokens.blue : EtmTokens.faint), const SizedBox(width: 6), Text('Cible deadline', style: EtmTokens.sans(size: 13, color: enabled ? EtmTokens.blue : EtmTokens.muted, weight: enabled ? FontWeight.w600 : FontWeight.w400)), ], ), Switch( value: enabled, onChanged: onChanged, activeThumbColor: EtmTokens.blue, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ], ); } } class _DeadlineParamsRow extends StatelessWidget { final int targetSoc; final DateTime endTime; final ValueChanged onSocChanged; final VoidCallback onPickTime; const _DeadlineParamsRow({ required this.targetSoc, required this.endTime, required this.onSocChanged, required this.onPickTime, }); @override Widget build(BuildContext context) { final timeLabel = '${endTime.hour.toString().padLeft(2, '0')}:${endTime.minute.toString().padLeft(2, '0')}'; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: EtmTokens.blueSoft, borderRadius: BorderRadius.circular(12), ), child: Column( children: [ Row( children: [ const Icon(Icons.battery_charging_full, size: 16, color: EtmTokens.blue), const SizedBox(width: 6), Text('SOC cible : $targetSoc %', style: EtmTokens.sans(size: 12, color: EtmTokens.blue)), Expanded( child: Slider( value: targetSoc.toDouble(), min: 0, max: 100, divisions: 20, label: '$targetSoc %', activeColor: EtmTokens.blue, onChanged: (v) => onSocChanged(v.round()), ), ), ], ), GestureDetector( onTap: onPickTime, child: Row( children: [ const Icon(Icons.access_time, size: 16, color: EtmTokens.blue), const SizedBox(width: 6), Text('Heure d\'arrivée :', style: EtmTokens.sans(size: 12, color: EtmTokens.blue)), const SizedBox(width: 8), Text(timeLabel, style: EtmTokens.mono(size: 13, color: EtmTokens.blue)), const SizedBox(width: 4), const Icon(Icons.edit, size: 12, color: EtmTokens.blue), ], ), ), ], ), ); } } class _ModeButton extends StatelessWidget { final String label; final IconData icon; final Color color; final bool isSelected; final VoidCallback onTap; const _ModeButton({ required this.label, required this.icon, required this.color, required this.isSelected, required this.onTap, }); @override Widget build(BuildContext context) { return Expanded( child: GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(vertical: 11), decoration: BoxDecoration( color: isSelected ? color : color.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(12), border: Border.all(color: isSelected ? color : color.withValues(alpha: 0.3)), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: isSelected ? Colors.white : color, size: 15), const SizedBox(width: 4), Text( label, style: EtmTokens.sans( size: 13, weight: FontWeight.w600, color: isSelected ? Colors.white : color, ), ), ], ), ), ), ); } } /// Barre de progression SOC de la voiture avec target et valeur courante. class _SocProgress extends StatelessWidget { final int targetSoc; final int currentSoc; const _SocProgress({required this.targetSoc, required this.currentSoc}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: EtmTokens.blueSoft, borderRadius: BorderRadius.circular(13), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Charge prévue jusqu\'à $targetSoc%', style: EtmTokens.sans(size: 13, color: EtmTokens.navy)), Text('$currentSoc%', style: EtmTokens.mono(size: 13, color: EtmTokens.blue)), ], ), const SizedBox(height: 2), Text('Aujourd\'hui à 07:30', style: EtmTokens.sans(size: 11, color: EtmTokens.muted)), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(99), child: SizedBox( height: 7, child: LinearProgressIndicator( value: currentSoc / 100, backgroundColor: const Color(0xFFD6E6F2), valueColor: const AlwaysStoppedAnimation(EtmTokens.blue), ), ), ), ], ), ); } } class _ChargingSettingsSheet extends StatefulWidget { const _ChargingSettingsSheet(); @override State<_ChargingSettingsSheet> createState() => _ChargingSettingsSheetState(); } class _ChargingSettingsSheetState extends State<_ChargingSettingsSheet> { double _minPower = 6; double _maxPower = 16; bool _scheduleEnabled = false; TimeOfDay _startTime = const TimeOfDay(hour: 22, minute: 0); TimeOfDay _endTime = const TimeOfDay(hour: 6, minute: 0); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Paramètres de charge', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 20), Text('Courant minimum (A)', style: EtmTokens.sans(size: 14, weight: FontWeight.w500)), Slider( value: _minPower, min: 6, max: 16, divisions: 10, label: '${_minPower.toStringAsFixed(0)} A', onChanged: (v) => setState(() => _minPower = v), activeColor: EtmTokens.green, ), Text('Courant maximum (A)', style: EtmTokens.sans(size: 14, weight: FontWeight.w500)), Slider( value: _maxPower, min: 6, max: 32, divisions: 26, label: '${_maxPower.toStringAsFixed(0)} A', onChanged: (v) => setState(() => _maxPower = v), activeColor: EtmTokens.green, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Programmation horaire', style: EtmTokens.sans(size: 14, weight: FontWeight.w500)), Switch( value: _scheduleEnabled, onChanged: (v) => setState(() => _scheduleEnabled = v), activeColor: EtmTokens.green, ), ], ), if (_scheduleEnabled) ...[ Row( children: [ Expanded( child: _TimePicker( label: 'Début', time: _startTime, onChanged: (t) => setState(() => _startTime = t), ), ), const SizedBox(width: 16), Expanded( child: _TimePicker( label: 'Fin', time: _endTime, onChanged: (t) => setState(() => _endTime = t), ), ), ], ), ], const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom( backgroundColor: EtmTokens.green, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), child: const Text('Enregistrer'), ), ), ], ), ); } } class _TimePicker extends StatelessWidget { final String label; final TimeOfDay time; final ValueChanged onChanged; const _TimePicker({ required this.label, required this.time, required this.onChanged, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: () async { final picked = await showTimePicker(context: context, initialTime: time); if (picked != null) onChanged(picked); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ const Icon(Icons.access_time, size: 16, color: EtmTokens.faint), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: EtmTokens.sans(size: 11, color: EtmTokens.muted)), Text( '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}', style: EtmTokens.mono(size: 13), ), ], ), ], ), ), ); } }