etm-powersync-app/lib/screens/energy/tariff_screen.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

538 lines
16 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../providers/tariff_provider.dart';
import '../../theme/app_theme.dart';
import '../../theme/etm_theme.dart';
import '../../widgets/pro_lock_badge.dart';
/// Écran "Tarifs & providers".
class TariffScreen extends StatefulWidget {
const TariffScreen({super.key});
@override
State<TariffScreen> createState() => _TariffScreenState();
}
class _TariffScreenState extends State<TariffScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<TariffProvider>().load();
});
}
@override
Widget build(BuildContext context) {
final tariff = context.watch<TariffProvider>();
return Scaffold(
backgroundColor: const Color(0xFFF0F2F5),
appBar: AppBar(
title: const Text('Tarifs & providers'),
backgroundColor: Colors.white,
foregroundColor: const Color(0xFF1A1A2E),
elevation: 0,
),
body: tariff.loading
? const Center(child: CircularProgressIndicator())
: ListView(
padding: const EdgeInsets.all(16),
children: [
_ProviderCard(tariff),
// Carte tarif manuel uniquement si provider = manual
if (tariff.activeProvider == TariffProviderType.manual) ...[
const SizedBox(height: 12),
_ManualTariffCard(tariff),
] else ...[
const SizedBox(height: 12),
_ModeCard(tariff),
if (tariff.mode == TariffMode.hchp) ...[
const SizedBox(height: 12),
_HcHpCard(tariff),
],
const SizedBox(height: 12),
_PricesCard(tariff),
],
],
),
);
}
}
// ── Carte provider ────────────────────────────────────────────────────────────
class _ProviderCard extends StatelessWidget {
final TariffProvider tariff;
const _ProviderCard(this.tariff);
@override
Widget build(BuildContext context) {
return _SectionCard(
title: 'Provider actif',
child: Column(
children: TariffProviderType.values.map((p) {
final selected = tariff.activeProvider == p;
return _ProviderTile(
provider: p,
selected: selected,
onTap: p.isPro
? null
: () => context.read<TariffProvider>().setProvider(p),
);
}).toList(),
),
);
}
}
class _ProviderTile extends StatelessWidget {
final TariffProviderType provider;
final bool selected;
final VoidCallback? onTap;
const _ProviderTile({
required this.provider,
required this.selected,
this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Opacity(
opacity: provider.isPro ? 0.6 : 1.0,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
Icon(
selected
? Icons.check_circle_rounded
: Icons.radio_button_unchecked_rounded,
color: selected ? AppTheme.primaryGreen : Colors.grey,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text(provider.label,
style: const TextStyle(fontSize: 13)),
),
if (provider.isPro)
ProLockBadge(featureName: provider.label),
],
),
),
),
);
}
}
// ── Carte mode tarifaire ──────────────────────────────────────────────────────
class _ModeCard extends StatelessWidget {
final TariffProvider tariff;
const _ModeCard(this.tariff);
@override
Widget build(BuildContext context) {
return _SectionCard(
title: 'Mode tarifaire',
child: DropdownButtonFormField<TariffMode>(
value: tariff.mode,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 12),
),
items: TariffMode.values
.map((m) =>
DropdownMenuItem(value: m, child: Text(m.label)))
.toList(),
onChanged: (v) {
if (v != null) context.read<TariffProvider>().setMode(v);
},
),
);
}
}
// ── Carte HC/HP ───────────────────────────────────────────────────────────────
class _HcHpCard extends StatelessWidget {
final TariffProvider tariff;
const _HcHpCard(this.tariff);
@override
Widget build(BuildContext context) {
final hchp = tariff.hchp;
return _SectionCard(
title: 'Tarif HC/HP',
child: Column(
children: [
_PriceRow(
label: 'Prix HC',
value: hchp.offPeakPrice,
unit: '€/kWh',
onChanged: (v) => context
.read<TariffProvider>()
.updateHcHp(hchp.copyWith(offPeakPrice: v)),
),
_PriceRow(
label: 'Prix HP',
value: hchp.peakPrice,
unit: '€/kWh',
onChanged: (v) => context
.read<TariffProvider>()
.updateHcHp(hchp.copyWith(peakPrice: v)),
),
_TimeRow(
label: 'Début HC',
time: hchp.offPeakStart,
onChanged: (t) => context
.read<TariffProvider>()
.updateHcHp(hchp.copyWith(offPeakStart: t)),
),
_TimeRow(
label: 'Fin HC',
time: hchp.offPeakEnd,
onChanged: (t) => context
.read<TariffProvider>()
.updateHcHp(hchp.copyWith(offPeakEnd: t)),
),
],
),
);
}
}
class _PriceRow extends StatefulWidget {
final String label;
final double value;
final String unit;
final ValueChanged<double> onChanged;
const _PriceRow({
required this.label,
required this.value,
required this.unit,
required this.onChanged,
});
@override
State<_PriceRow> createState() => _PriceRowState();
}
class _PriceRowState extends State<_PriceRow> {
late TextEditingController _ctrl;
@override
void initState() {
super.initState();
_ctrl = TextEditingController(
text: widget.value.toStringAsFixed(3));
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
Expanded(
child: Text(widget.label,
style: const TextStyle(fontSize: 13)),
),
SizedBox(
width: 90,
child: TextFormField(
controller: _ctrl,
textAlign: TextAlign.center,
keyboardType: const TextInputType.numberWithOptions(
decimal: true),
decoration: InputDecoration(
isDense: true,
filled: true,
fillColor: Colors.grey.shade50,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 8),
),
onFieldSubmitted: (v) {
final n = double.tryParse(v);
if (n != null) widget.onChanged(n);
},
),
),
const SizedBox(width: 6),
Text(widget.unit,
style: const TextStyle(
fontSize: 12, color: AppTheme.textLight)),
],
),
);
}
}
class _TimeRow extends StatelessWidget {
final String label;
final TimeOfDay time;
final ValueChanged<TimeOfDay> onChanged;
const _TimeRow({
required this.label,
required this.time,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
final formatted =
'${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
Expanded(
child: Text(label,
style: const TextStyle(fontSize: 13))),
GestureDetector(
onTap: () async {
final picked = await showTimePicker(
context: context,
initialTime: time,
);
if (picked != null) onChanged(picked);
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
color: Colors.grey.shade50,
),
child: Text(formatted,
style: const TextStyle(
fontSize: 13, fontWeight: FontWeight.w500)),
),
),
],
),
);
}
}
// ── Carte tarif manuel ────────────────────────────────────────────────────────
class _ManualTariffCard extends StatefulWidget {
final TariffProvider tariff;
const _ManualTariffCard(this.tariff);
@override
State<_ManualTariffCard> createState() => _ManualTariffCardState();
}
class _ManualTariffCardState extends State<_ManualTariffCard> {
late TextEditingController _ctrl;
@override
void initState() {
super.initState();
_ctrl = TextEditingController(
text: widget.tariff.manualPrice.toStringAsFixed(4));
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _SectionCard(
title: 'Tarif manuel',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Prix unique appliqué à toutes les décisions du scheduler.',
style: TextStyle(fontSize: 12, color: AppTheme.textLight),
),
const SizedBox(height: 16),
Row(
children: [
const Expanded(
child: Text('Prix de l\'électricité',
style: TextStyle(fontSize: 13))),
SizedBox(
width: 100,
child: TextFormField(
controller: _ctrl,
textAlign: TextAlign.center,
keyboardType: const TextInputType.numberWithOptions(
decimal: true),
decoration: InputDecoration(
isDense: true,
filled: true,
fillColor: Colors.grey.shade50,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 10),
),
onFieldSubmitted: (v) {
final n = double.tryParse(v);
if (n != null) {
context.read<TariffProvider>().setManualPrice(n);
}
},
),
),
const SizedBox(width: 8),
const Text('€/kWh',
style: TextStyle(fontSize: 12, color: AppTheme.textLight)),
],
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: ETMTheme.accentColor.withValues(alpha: 0.07),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
const Icon(Icons.info_outline_rounded,
size: 15, color: ETMTheme.accentColor),
const SizedBox(width: 8),
Expanded(
child: Text(
'Prix actuel : ${widget.tariff.manualPrice.toStringAsFixed(4)} €/kWh',
style: const TextStyle(
fontSize: 12, color: ETMTheme.accentColor),
),
),
],
),
),
],
),
);
}
}
// ── Carte prix actuels ────────────────────────────────────────────────────────
class _PricesCard extends StatelessWidget {
final TariffProvider tariff;
const _PricesCard(this.tariff);
@override
Widget build(BuildContext context) {
final p = tariff.prices;
String _fmtTime(DateTime? dt) {
if (dt == null) return '';
return ' (${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')})';
}
return _SectionCard(
title: 'Prix actuels (live)',
child: Column(
children: [
_PriceItem(
label: 'Maintenant',
value: '${p.now.toStringAsFixed(3)} €/kWh',
),
_PriceItem(
label: 'Dans 1h',
value: '${p.inOneHour.toStringAsFixed(3)} €/kWh',
),
_PriceItem(
label: 'Minimum 24h',
value:
'${p.min24h.toStringAsFixed(3)} €/kWh${_fmtTime(p.minTime)}',
color: AppTheme.primaryGreen,
),
_PriceItem(
label: 'Maximum 24h',
value:
'${p.max24h.toStringAsFixed(3)} €/kWh${_fmtTime(p.maxTime)}',
color: AppTheme.boostRed,
),
],
),
);
}
}
class _PriceItem extends StatelessWidget {
final String label;
final String value;
final Color? color;
const _PriceItem({
required this.label,
required this.value,
this.color,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
Expanded(
child: Text(label,
style: const TextStyle(fontSize: 13))),
Text(
value,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: color ?? AppTheme.textDark,
),
),
],
),
);
}
}
// ── Card générique ────────────────────────────────────────────────────────────
class _SectionCard extends StatelessWidget {
final String title;
final Widget child;
const _SectionCard({required this.title, required this.child});
@override
Widget build(BuildContext context) {
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 14)),
const SizedBox(height: 14),
child,
],
),
),
);
}
}