etm-powersync-app/lib/widgets/production_card.dart
pakutz79 8862dc2a72 feat: historique énergie, navigation Things, actions nymea
Énergie :
- Écran Énergie reécrit : line chart (production/conso/autoconso/batterie)
  et bar chart (bilan Wh par période) avec onglets 15 min / 1 h / 1 j / 1 sem
- Datepicker pour sélectionner une période historique (chip dismissible)
- Timelines des deux graphiques alignées (même x=i → data[i].timestamp)
- PowerBalanceEntry + fetchPowerBalanceLogs() + simulation sinusoïdale
- Overflow fixes : energy_flow_widget (Expanded sur titre), production_card

Things :
- Navigation 3 niveaux : ThingsScreen → CategoryOverviewScreen → ThingDetailScreen
- Catégorie Cars ajoutée, carrousel corrigé (clamp RangeError)
- ThingDetailScreen : executeAction, setStateValue, activeThumbColor fix
- NymeaTile widget, state_history_chart widget (générique Logging.GetLogEntries)

Modèles / service :
- HistoryEntry, PowerBalanceEntry ajoutés
- fetchHistory(), fetchPowerBalanceLogs() dans NymeaService
- interfaceToCategoryMap étendu (Cars, etc.)
- AppTheme : nouvelles couleurs (accentTeal, boostRed, pvGreen, minPvBlue…)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 07:15:48 +01:00

181 lines
5.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:percent_indicator/percent_indicator.dart';
import '../models/energy_data.dart';
import '../theme/app_theme.dart';
class ProductionCard extends StatelessWidget {
final EnergyData data;
const ProductionCard({super.key, required this.data});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Production du jour',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppTheme.textDark,
),
),
Text(
'${_formatEnergy(data.dayProductionWh)} Wh',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: AppTheme.textDark,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 8),
CircularPercentIndicator(
radius: 35,
lineWidth: 6,
percent: (data.selfConsumptionRate / 100).clamp(0, 1),
center: Text(
'${data.selfConsumptionRate.toStringAsFixed(0)}%',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
progressColor: AppTheme.primaryGreen,
backgroundColor: Colors.grey.shade200,
),
],
),
const Divider(height: 24),
// Self-consumption
_StatRow(
icon: Icons.home_rounded,
iconColor: AppTheme.solarYellow,
label: 'Usage personnel',
value: '${_formatEnergy(data.daySelfConsumptionWh)} Wh',
subLabel: 'Autoconsommation',
percent: data.selfConsumptionRate,
color: AppTheme.solarYellow,
),
const SizedBox(height: 12),
// Grid injection
_StatRow(
icon: Icons.electrical_services_rounded,
iconColor: AppTheme.gridGray,
label: 'Vers réseau',
value: '${_formatEnergy(data.dayGridInjectionWh)} Wh',
subLabel: 'Autonomie',
percent: data.autonomyRate,
color: AppTheme.homeBlue,
),
],
),
),
);
}
String _formatEnergy(double wh) {
if (wh >= 1000) return '${(wh / 1000).toStringAsFixed(2)} k';
return wh.toStringAsFixed(0);
}
}
class _StatRow extends StatelessWidget {
final IconData icon;
final Color iconColor;
final String label;
final String value;
final String subLabel;
final double percent;
final Color color;
const _StatRow({
required this.icon,
required this.iconColor,
required this.label,
required this.value,
required this.subLabel,
required this.percent,
required this.color,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: iconColor.withValues(alpha:0.15),
shape: BoxShape.circle,
),
child: Icon(icon, color: iconColor, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label,
style: const TextStyle(
fontSize: 14, color: AppTheme.textLight)),
Text(value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppTheme.textDark)),
],
),
const SizedBox(height: 4),
Row(
children: [
Text(
'$subLabel : ${percent.toStringAsFixed(0)}%',
style: TextStyle(fontSize: 11, color: color),
),
const SizedBox(width: 8),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: (percent / 100).clamp(0, 1),
backgroundColor: Colors.grey.shade200,
valueColor: AlwaysStoppedAnimation(color),
minHeight: 5,
),
),
),
],
),
],
),
),
],
),
],
);
}
}