import 'package:flutter/material.dart'; import '../models/energy_data.dart' show ChargingMode; import '../models/ev_charger_live.dart'; import '../theme/etm_tokens.dart'; /// Les trois modes de recharge — **le seul endroit de l'app qui écrit `SetChargingInfo`**. /// /// ## Un seul lieu d'écriture, délibérément /// /// Ces boutons ont vécu aux favoris, sur la tuile de la borne. Ils sont ici depuis le /// 2026-08-28 : « Configurer » est l'écran de **mise en service**, celui où un installateur /// découvre qu'une borne neuve est hors arbitrage — et deux écrans qui écrivent la même /// chose sont deux comportements qui divergent. La tuile des favoris garde la LECTURE et /// la note « hors arbitrage » : le client doit voir que sa borne n'est pas pilotée, même /// là où il ne peut pas la régler. /// /// ## Ce qui est affiché comme sélectionné /// /// Le mode que la **box** porte, jamais celui que l'app vient de demander. Un mode non lu /// n'allume rien : trois boutons éteints valent mieux qu'un allumé au hasard. Après une /// écriture acceptée, `NymeaService` relit `GetChargingInfos` — la sélection ne bouge que /// quand la box l'a dit. /// /// ## Un refus se voit /// /// `setChargingInfo` rend un code d'erreur ; il était avalé. Une commande muette est pire /// qu'une lecture seule — l'utilisateur croit avoir réglé la borne. class EvModeButtons extends StatefulWidget { final EvChargerLive live; /// Écrit le mode et rend `null` si la box a accepté, sinon le code de refus. final Future Function(ChargingMode) onSetMode; /// Disposition : en colonne sur une tuile étroite, en ligne sur un écran. final Axis axis; const EvModeButtons({ super.key, required this.live, required this.onSetMode, this.axis = Axis.horizontal, }); @override State createState() => _EvModeButtonsState(); } class _EvModeButtonsState extends State { /// Mode en cours d'écriture — jamais affiché comme sélectionné : tant que la box n'a pas /// répondu, l'app ne sait pas ce que porte la borne. ChargingMode? _pending; Future _set(ChargingMode m) async { setState(() => _pending = m); final err = await widget.onSetMode(m); if (!mounted) return; setState(() => _pending = null); if (err == null) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('La box a refusé le mode de recharge ($err). ' 'Détail dans son journal.'), )); } @override Widget build(BuildContext context) { final boutons = [for (final m in ChargingMode.values) _bouton(m)]; return widget.axis == Axis.horizontal ? Row(children: [ for (final b in boutons) ...[Expanded(child: b), const SizedBox(width: 6)] ]..removeLast()) : Column(mainAxisSize: MainAxisSize.min, children: boutons); } /// 44 px de cible tactile — les pastilles d'origine faisaient 7 à 10 px, et écrivaient /// pour de vrai. Widget _bouton(ChargingMode m) { final (label, couleur) = switch (m) { ChargingMode.pv => ('PV', EtmTokens.amber), ChargingMode.minPv => ('Min+PV', EtmTokens.blue), ChargingMode.boost => ('Boost', EtmTokens.green), }; final selected = widget.live.uiMode == m; final busy = _pending != null; return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: SizedBox( height: 44, width: widget.axis == Axis.vertical ? 76 : null, child: OutlinedButton( onPressed: busy ? null : () => _set(m), style: OutlinedButton.styleFrom( padding: EdgeInsets.zero, backgroundColor: selected ? couleur.withValues(alpha: 0.14) : Colors.transparent, side: BorderSide( color: selected ? couleur : EtmTokens.lineOf(context).withValues(alpha: 0.6)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), child: _pending == m ? const SizedBox( width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2)) : Text(label, style: EtmTokens.sans( size: 12, weight: selected ? FontWeight.w700 : FontWeight.w400, color: selected ? couleur : EtmTokens.mutedOf(context))), ), ), ); } }