diff --git a/lib/screens/energy_screen.dart b/lib/screens/energy_screen.dart index 1aca96f..f009b76 100644 --- a/lib/screens/energy_screen.dart +++ b/lib/screens/energy_screen.dart @@ -19,6 +19,79 @@ import '../main.dart' show DrawerMenuButton; // 4 KPIs · sélecteur période · line chart double axe (kW / SOC%) · bar chart // ───────────────────────────────────────────────────────────────────────────── +/// **La barre de navigation de période** — deux flèches, un libellé qui ouvre le calendrier. +/// +/// Lire une consommation, c'est presque toujours la comparer à la période précédente. Le +/// geste le plus fréquent doit donc coûter un appui, pas l'ouverture d'un calendrier et le +/// calcul mental de la date d'avant. +class _NavPeriode extends StatelessWidget { + final String libelle; + final bool peutAvancer; + final bool surAujourdhui; + final VoidCallback onReculer; + final VoidCallback onAvancer; + final VoidCallback onChoisir; + final VoidCallback onAujourdhui; + + const _NavPeriode({ + required this.libelle, + required this.peutAvancer, + required this.surAujourdhui, + required this.onReculer, + required this.onAvancer, + required this.onChoisir, + required this.onAujourdhui, + }); + + @override + Widget build(BuildContext context) { + final muted = EtmTokens.mutedOf(context); + return Container( + decoration: BoxDecoration( + color: EtmTokens.surfaceOf(context), + borderRadius: BorderRadius.circular(EtmTokens.radiusCard), + border: Border.all(color: EtmTokens.lineOf(context)), + ), + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + child: Row(children: [ + IconButton( + icon: const Icon(Icons.chevron_left_rounded, size: 22), + color: EtmTokens.inkOf(context), + onPressed: onReculer, + tooltip: 'Période précédente', + ), + Expanded( + child: TextButton( + onPressed: onChoisir, + child: Text(libelle, + textAlign: TextAlign.center, + style: EtmTokens.sans( + size: 13.5, + weight: FontWeight.w600, + color: EtmTokens.inkOf(context))), + ), + ), + // ⚠️ La flèche « suivant » est DÉSACTIVÉE, pas masquée : une commande qui + // disparaît laisse chercher où elle est passée, là où une commande grisée dit + // « il n'y a rien après ». + IconButton( + icon: const Icon(Icons.chevron_right_rounded, size: 22), + color: peutAvancer ? EtmTokens.inkOf(context) : muted.withValues(alpha: 0.35), + onPressed: peutAvancer ? onAvancer : null, + tooltip: 'Période suivante', + ), + if (!surAujourdhui) + TextButton( + onPressed: onAujourdhui, + style: TextButton.styleFrom(minimumSize: const Size(0, 40)), + child: Text('Aujourd\'hui', + style: EtmTokens.sans(size: 12, color: EtmTokens.ecoOf(context))), + ), + ]), + ); + } +} + /// Une ligne d'explication sous un graphe — elle ne sort que quand elle DIT quelque chose. class _NoteEcran extends StatelessWidget { final IconData icon; @@ -332,6 +405,49 @@ class _EnergyScreenState extends State { ? DateTime(_selectedDate!.year, _selectedDate!.month, _selectedDate!.day, 23, 59, 59) : DateTime.now(); + /// Le nom de la période affichée, à la granularité de l'onglet. + /// + /// « 9 sept. » n'est pas « 3 – 9 sept. » : dire « jour » quand on montre une semaine est + /// exactement l'erreur que les onglets faisaient avant d'être corrigés. + String _libellePeriode() { + const mois = [ + 'janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin', + 'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.' + ]; + final tab = _tabs[_tabIdx]; + final fin = _anchorDate(); + final debut = fin.subtract(tab.range - const Duration(days: 1)); + String jour(DateTime d) => '${d.day} ${mois[d.month - 1]}'; + return switch (tab.label) { + 'Année' => '${fin.year}', + 'Mois' => '${mois[fin.month - 1]} ${fin.year}', + 'Jour' || 'Heures' => '${jour(fin)} ${fin.year}', + _ => '${jour(debut)} → ${jour(fin)}', + }; + } + + /// Avancer n'a de sens que si la période suivante a commencé. + /// + /// Sans cette borne, on part dans un futur où la box n'a rien : un graphe vide qui ne + /// dit pas qu'il est vide **parce qu'on est allé trop loin**. + bool _peutAvancer() => + _selectedDate != null && + _anchorDate().add(_tabs[_tabIdx].range).isBefore(DateTime.now()); + + void _decalerPeriode(int sens) { + final tab = _tabs[_tabIdx]; + final base = _selectedDate ?? DateTime.now(); + final cible = base.add(tab.range * sens); + setState(() { + // Revenir sur aujourd'hui remet le mode « suivi », où la fenêtre se recale à chaque + // lecture. Rester sur une date figée l'aurait gelée sans le dire. + final maintenant = DateTime.now(); + _selectedDate = cible.isAfter(maintenant) ? null : cible; + _initialSocFetched = false; + }); + _fetch(); + } + Future _pickDate() async { final picked = await showDatePicker( context: context, @@ -411,17 +527,25 @@ class _EnergyScreenState extends State { }, ), - // Chip date sélectionnée - if (_selectedDate != null) ...[ - const SizedBox(height: 10), - _DateChip( - date: _selectedDate!, - onClear: () { - setState(() => _selectedDate = null); - _fetch(); - }, - ), - ], + // **Naviguer de période en période**, pas seulement choisir une date. + // + // Le sélecteur de la barre du haut permettait de sauter à un jour, pas + // de reculer d'une semaine puis d'une autre. Or lire une consommation, + // c'est presque toujours la comparer à la période précédente : le geste + // le plus fréquent doit être une flèche, pas un calendrier. + const SizedBox(height: 10), + _NavPeriode( + libelle: _libellePeriode(), + peutAvancer: _peutAvancer(), + onReculer: () => _decalerPeriode(-1), + onAvancer: () => _decalerPeriode(1), + onChoisir: _pickDate, + surAujourdhui: _selectedDate == null, + onAujourdhui: () { + setState(() => _selectedDate = null); + _fetch(); + }, + ), const SizedBox(height: 16), // ── Vues d'ÉNERGIE : une seule carte, en barres ────────────── @@ -433,8 +557,7 @@ class _EnergyScreenState extends State { if (_tabs[_tabIdx].rendu == _Rendu.barres) ...[ _EtmChartCard( title: 'Bilan énergétique', - subtitle: 'kWh par période — le bloc orange est partagé : ' - 'ce qui a été produit ET gardé', + subtitle: 'kWh par période', legend: [ _LegendDot(EtmTokens.selfUse, 'Autoconsommé'), _LegendDot(EtmTokens.amber, 'Exporté'), @@ -945,7 +1068,11 @@ class _EnergyScreenState extends State { final prod = delta(a.totalProductionKwh, b.totalProductionKwh); final conso = delta(a.totalConsumptionKwh, b.totalConsumptionKwh); final exp = delta(a.totalReturnKwh, b.totalReturnKwh); - final imp = delta(a.totalAcquisitionKwh, b.totalAcquisitionKwh); + // Le soutiré n'est pas relu du compteur d'import : c'est `conso - autoconsommé`, + // par construction de la pile. Prendre `totalAcquisition` ferait une pile dont les + // segments ne rejoignent pas leur sommet dès qu'un compteur dérive — un trou dans la + // barre que personne ne saurait lire. L'identité des compteurs se vérifie ailleurs, + // sur le bilan du jour, où elle a une phrase pour se dire. // Autoconsommé = ce qui a été produit et n'est pas parti. Borné à la production : // un export supérieur à la production est une incohérence de compteurs, pas une // autoconsommation négative. @@ -1711,45 +1838,6 @@ class _PeriodSelector extends StatelessWidget { // ─────────────────────────── Chip date ───────────────────────────────────────── -class _DateChip extends StatelessWidget { - final DateTime date; - final VoidCallback onClear; - const _DateChip({required this.date, required this.onClear}); - - @override - Widget build(BuildContext context) { - return Align( - alignment: Alignment.centerLeft, - child: GestureDetector( - onTap: onClear, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: EtmTokens.blueSoft, - borderRadius: BorderRadius.circular(99), - border: Border.all(color: EtmTokens.blue.withValues(alpha: 0.4)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.calendar_today_rounded, size: 12, color: EtmTokens.blue), - const SizedBox(width: 6), - Text( - '${date.day.toString().padLeft(2, '0')}/' - '${date.month.toString().padLeft(2, '0')}/' - '${date.year}', - style: EtmTokens.sans(size: 12, weight: FontWeight.w600, color: EtmTokens.blue), - ), - const SizedBox(width: 6), - const Icon(Icons.close_rounded, size: 12, color: EtmTokens.blue), - ], - ), - ), - ), - ); - } -} - // ─────────────────────────── Carte graphe ────────────────────────────────────── class _EtmChartCard extends StatelessWidget {