etm-powersync-app/lib/widgets/timeline_slot_card.dart
pakutz79 c19c9d1a98 feat: navigation drawer, EMS setup, scheduler, tarifs, paramètres app
- Drawer custom (overlay Stack) avec mode installateur PIN SHA-256
- GoRouter + ShellRoute : navigation préservée entre onglets
- 6 providers : NavigationProvider, InstallerModeProvider, AppSettingsProvider,
  EnergySetupProvider, SchedulerProvider, TariffProvider
- Écrans Energy Manager : RoleConfigFlow (3 étapes), Scheduler, Tarifs, Timeline
- Écrans Paramètres : Apparence, Écrans actifs, AppSettingsScreen
- DrawerMenuButton présent dans les 5 AppBars principaux
- Simulation : _thingClasses générées avec interfaces EMS pour filtrage des rôles
- Compteur solaire : ajout smartmeter aux interfaces compatibles
- Thème ETM (etm_theme.dart), ProLockBadge, widgets PowerBar/RoleCard/TimelineSlotCard
- Dépendances : go_router, shared_preferences, crypto, url_launcher

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 14:52:32 +01:00

310 lines
10 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
import '../theme/etm_theme.dart';
import 'power_bar.dart';
/// Données d'un créneau horaire dans la timeline EMS.
class TimelineSlot {
final DateTime start;
final DateTime end;
final double solarW;
final double homeW;
final double evW; // > 0 = actif
final double batteryW; // > 0 = charge, < 0 = décharge
final double gridW; // > 0 = import, < 0 = export
final String reasoning; // Explication de la décision
final double savings; // €
final double selfSufficiency; // %
final bool isNow;
final bool hasOverride;
const TimelineSlot({
required this.start,
required this.end,
required this.solarW,
required this.homeW,
this.evW = 0,
this.batteryW = 0,
this.gridW = 0,
this.reasoning = '',
this.savings = 0,
this.selfSufficiency = 0,
this.isNow = false,
this.hasOverride = false,
});
}
/// Carte d'un créneau horaire dans la timeline EMS.
class TimelineSlotCard extends StatelessWidget {
final TimelineSlot slot;
final VoidCallback? onOverride;
const TimelineSlotCard({
super.key,
required this.slot,
this.onOverride,
});
@override
Widget build(BuildContext context) {
final maxW = [
slot.solarW, slot.homeW, slot.evW.abs(),
slot.batteryW.abs(), slot.gridW.abs(),
].fold<double>(0, (a, b) => a > b ? a : b).clamp(100.0, double.infinity);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
color: slot.isNow
? ETMTheme.accentColor.withValues(alpha: 0.08)
: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: slot.isNow
? ETMTheme.accentColor.withValues(alpha: 0.4)
: Colors.grey.withValues(alpha: 0.12),
),
),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── En-tête ──────────────────────────────────────────────────────
Row(
children: [
Text(
'${_fmt(slot.start)} ${_fmt(slot.end)}',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
color: slot.isNow
? ETMTheme.accentColor
: AppTheme.textDark,
),
),
if (slot.isNow) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: ETMTheme.accentColor,
borderRadius: BorderRadius.circular(6),
),
child: const Text(
'MAINTENANT',
style: TextStyle(
color: Colors.white,
fontSize: 9,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
),
],
if (slot.hasOverride) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: ETMTheme.warningColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'Override',
style: TextStyle(
color: ETMTheme.warningColor,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
const SizedBox(height: 10),
// ── Barres de puissance ──────────────────────────────────────────
if (slot.solarW > 0)
_PowerRow(
emoji: '☀️', label: 'Solaire',
value: slot.solarW, maxValue: maxW,
color: AppTheme.solarYellow,
sign: '+',
),
_PowerRow(
emoji: '🏠', label: 'Maison',
value: slot.homeW, maxValue: maxW,
color: AppTheme.homeBlue,
sign: '-',
),
if (slot.evW != 0)
_PowerRow(
emoji: '🚗', label: 'VE',
value: slot.evW.abs(), maxValue: maxW,
color: AppTheme.accentTeal,
sign: slot.evW > 0 ? '-' : '+',
badge: slot.evW > 0 ? 'ON' : null,
),
if (slot.batteryW != 0)
_PowerRow(
emoji: '🔋', label: 'Batterie',
value: slot.batteryW.abs(), maxValue: maxW,
color: AppTheme.batteryGreen,
sign: slot.batteryW > 0 ? '-' : '+',
badge: slot.batteryW > 0 ? '' : '',
),
if (slot.gridW != 0)
_PowerRow(
emoji: '', label: 'Réseau',
value: slot.gridW.abs(), maxValue: maxW,
color: slot.gridW > 0
? AppTheme.powerAcquisitionColor
: AppTheme.powerReturnColor,
sign: slot.gridW > 0 ? '-' : '+',
badge: slot.gridW > 0 ? '' : '',
),
if (slot.reasoning.isNotEmpty) ...[
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('💬 ', style: TextStyle(fontSize: 13)),
Expanded(
child: Text(
'"${slot.reasoning}"',
style: const TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
color: AppTheme.textLight,
),
),
),
],
),
),
],
const SizedBox(height: 8),
// ── Métriques + Override ─────────────────────────────────────────
Row(
children: [
if (slot.savings != 0)
Text(
'💰 ${slot.savings >= 0 ? '-' : '+'}${slot.savings.abs().toStringAsFixed(3)}',
style: TextStyle(
fontSize: 11,
color: slot.savings > 0
? ETMTheme.successColor
: ETMTheme.errorColor,
),
),
if (slot.savings != 0 && slot.selfSufficiency > 0)
const SizedBox(width: 12),
if (slot.selfSufficiency > 0)
Text(
'Autosuff.: ${slot.selfSufficiency.toStringAsFixed(0)}%',
style: const TextStyle(
fontSize: 11, color: AppTheme.textLight),
),
const Spacer(),
if (onOverride != null)
TextButton(
style: TextButton.styleFrom(
foregroundColor: ETMTheme.accentColor,
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4),
),
onPressed: onOverride,
child: const Text('Override manuel',
style: TextStyle(fontSize: 11)),
),
],
),
],
),
),
);
}
String _fmt(DateTime dt) =>
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
}
class _PowerRow extends StatelessWidget {
final String emoji;
final String label;
final double value;
final double maxValue;
final Color color;
final String sign;
final String? badge;
const _PowerRow({
required this.emoji,
required this.label,
required this.value,
required this.maxValue,
required this.color,
required this.sign,
this.badge,
});
@override
Widget build(BuildContext context) {
final displayW = value >= 1000
? '${(value / 1000).toStringAsFixed(1)} kW'
: '${value.toStringAsFixed(0)} W';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
children: [
Text(emoji, style: const TextStyle(fontSize: 14)),
const SizedBox(width: 6),
SizedBox(
width: 70,
child: Text(
label,
style: const TextStyle(fontSize: 12, color: AppTheme.textLight),
),
),
Text(
'$sign$displayW',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppTheme.textDark,
),
),
const SizedBox(width: 8),
Expanded(
child: PowerBar(
value: value, maxValue: maxValue, color: color, height: 6,
),
),
if (badge != null) ...[
const SizedBox(width: 6),
Text(badge!,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: color)),
],
],
),
);
}
}