Skip to content

Commit 1dbbfb1

Browse files
m4dm4rtig4nClément VALENTINclaude
authored
Rework consumption kwh (#82)
* fix(OfferSelector): remove unused getMainPrice function Removes dead code that was causing TypeScript error TS6133 in CI build. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat: Enhance production and consumption components with improved styling and functionality - Updated AnnualProductionCurve component to use consistent graph colors and improved button styles. - Enhanced YearlyProduction component with updated background gradients and tooltip colors. - Refined YearlyProductionCards to include year-over-year change indicators and improved card styling. - Improved Production index page with new demo auto-fetch logic and cache handling. - Updated Simulator component to support demo auto-fetch and improved cache detection. - Enhanced documentation for consumption and dashboard features, including visual design guidelines and API details. * fix: Update PDL fetching logic to include short staleTime for consistent provider updates --------- Co-authored-by: Clément VALENTIN <[email protected]> Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent 5f3e33f commit 1dbbfb1

23 files changed

+1160
-302
lines changed

apps/web/src/components/OfferSelector.tsx

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -165,47 +165,6 @@ export default function OfferSelector({
165165
return `${numPrice.toFixed(2)} €/mois`
166166
}
167167

168-
// Get main price to display (short version for dropdown)
169-
const getMainPrice = (offer: EnergyOffer): string => {
170-
// Base offers
171-
if (offer.offer_type === 'BASE' && offer.base_price !== undefined && offer.base_price !== null) {
172-
return formatPrice(offer.base_price)
173-
}
174-
// HP/HC offers
175-
if (offer.offer_type === 'HC_HP' && offer.hp_price !== undefined && offer.hp_price !== null) {
176-
return `HP: ${formatPrice(offer.hp_price)}`
177-
}
178-
// Tempo offers - show blue HC as reference (most common)
179-
if (offer.offer_type === 'TEMPO' && offer.tempo_blue_hc !== undefined && offer.tempo_blue_hc !== null) {
180-
return `Bleu HC: ${formatPrice(offer.tempo_blue_hc)}`
181-
}
182-
// EJP offers
183-
if (offer.offer_type === 'EJP' && offer.ejp_normal !== undefined && offer.ejp_normal !== null) {
184-
return `Normal: ${formatPrice(offer.ejp_normal)}`
185-
}
186-
// Weekend offers
187-
if (offer.offer_type === 'WEEKEND') {
188-
if (offer.base_price !== undefined && offer.base_price !== null) {
189-
return formatPrice(offer.base_price)
190-
}
191-
if (offer.hp_price !== undefined && offer.hp_price !== null) {
192-
return `Semaine: ${formatPrice(offer.hp_price)}`
193-
}
194-
}
195-
// Seasonal offers
196-
if (offer.offer_type === 'SEASONAL' && offer.hp_price_winter !== undefined && offer.hp_price_winter !== null) {
197-
return `Hiver HP: ${formatPrice(offer.hp_price_winter)}`
198-
}
199-
// Fallback to base_price or hp_price
200-
if (offer.base_price !== undefined && offer.base_price !== null) {
201-
return formatPrice(offer.base_price)
202-
}
203-
if (offer.hp_price !== undefined && offer.hp_price !== null) {
204-
return `HP: ${formatPrice(offer.hp_price)}`
205-
}
206-
return '-'
207-
}
208-
209168
// Get detailed prices for selected offer summary
210169
const getDetailedPrices = (offer: EnergyOffer): React.ReactNode => {
211170
const priceRows: React.ReactNode[] = []

apps/web/src/components/PageHeader.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useEffect } from 'react'
22
import { useLocation } from 'react-router-dom'
33
import { TrendingUp, Sun, Calculator, Download, Lock, LayoutDashboard, Calendar, Zap, Users, AlertCircle, BookOpen, Settings as SettingsIcon, Key, Shield, FileText, Activity, Euro, Scale } from 'lucide-react'
44
import { useQuery } from '@tanstack/react-query'
5+
import toast from 'react-hot-toast'
56
import { pdlApi } from '@/api/pdl'
67
import { usePdlStore } from '@/stores/pdlStore'
78
import { useDataFetchStore } from '@/stores/dataFetchStore'
@@ -141,6 +142,27 @@ export default function PageHeader() {
141142
? activePdls.filter((pdl: PDL) => pdl.has_consumption)
142143
: activePdls.filter((pdl: PDL) => !linkedProductionIds.has(pdl.id)) // Hide linked production PDLs globally
143144

145+
// Auto-select first PDL if none selected OR if current PDL is not in the displayed list
146+
useEffect(() => {
147+
if (displayedPdls.length > 0) {
148+
// Check if current PDL is in the displayed list for this page
149+
const currentPdlInList = displayedPdls.some(p => p.usage_point_id === selectedPdl)
150+
151+
if (!selectedPdl || !currentPdlInList) {
152+
const newPdl = displayedPdls[0]
153+
// Show toast only if we're switching from an incompatible PDL (not on first load)
154+
if (selectedPdl && !currentPdlInList) {
155+
toast(`PDL changé automatiquement vers "${newPdl.name || newPdl.usage_point_id}"`, {
156+
icon: '🔄',
157+
duration: 4000,
158+
})
159+
}
160+
// Select the first available PDL for this page
161+
setSelectedPdl(newPdl.usage_point_id)
162+
}
163+
}
164+
}, [displayedPdls, selectedPdl, setSelectedPdl])
165+
144166
return (
145167
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
146168
<div className="container mx-auto px-3 sm:px-4 lg:px-6 max-w-[1920px]">
@@ -186,7 +208,6 @@ export default function PageHeader() {
186208
onChange={(e) => setSelectedPdl(e.target.value)}
187209
className="px-3 sm:px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none transition-colors text-sm w-full sm:w-auto"
188210
>
189-
<option value="">Sélectionner un PDL</option>
190211
{displayedPdls.map((pdl: PDL) => (
191212
<option key={pdl.usage_point_id} value={pdl.usage_point_id}>
192213
{pdl.name || pdl.usage_point_id}
@@ -197,7 +218,7 @@ export default function PageHeader() {
197218
{/* Bouton de récupération - Affiché seulement quand pas en chargement */}
198219
<button
199220
onClick={async () => {
200-
if (!isDemo && selectedPdl) {
221+
if (!isDemo && selectedPdl && selectedPDLDetails) {
201222
setIsLoading(true)
202223
try {
203224
await fetchAllData()
@@ -206,9 +227,9 @@ export default function PageHeader() {
206227
}
207228
}
208229
}}
209-
disabled={!selectedPdl || isDemo}
230+
disabled={!selectedPdl || !selectedPDLDetails || isDemo}
210231
className="flex items-center justify-center gap-2 px-3 sm:px-4 py-2 bg-primary-600 hover:bg-primary-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors disabled:cursor-not-allowed whitespace-nowrap text-sm w-full sm:w-auto"
211-
title={isDemo ? 'Récupération bloquée en mode démo' : 'Récupérer toutes les données depuis Enedis'}
232+
title={isDemo ? 'Récupération bloquée en mode démo' : !selectedPDLDetails ? 'Chargement du PDL...' : 'Récupérer toutes les données depuis Enedis'}
212233
>
213234
{isDemo ? (
214235
<>

apps/web/src/pages/Balance/index.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
import { useState, useMemo, useEffect } from 'react'
1+
import { useState, useMemo, useEffect, useRef } from 'react'
22
import { Database, ArrowRight, AlertTriangle, Info } from 'lucide-react'
33
import { useThemeStore } from '@/stores/themeStore'
44
import { usePdlStore } from '@/stores/pdlStore'
5+
import { useDataFetchStore } from '@/stores/dataFetchStore'
56
import { useBalanceData } from './hooks/useBalanceData'
67
import { useBalanceCalcs } from './hooks/useBalanceCalcs'
8+
import { useIsDemo } from '@/hooks/useIsDemo'
9+
import { useUnifiedDataFetch } from '@/hooks/useUnifiedDataFetch'
10+
import { useQuery } from '@tanstack/react-query'
11+
import { pdlApi } from '@/api/pdl'
12+
import type { PDL } from '@/types/api'
13+
import { logger } from '@/utils/logger'
714
import { BalanceSummaryCards } from './components/BalanceSummaryCards'
815
import { MonthlyComparison } from './components/MonthlyComparison'
916
import { NetBalanceCurve } from './components/NetBalanceCurve'
@@ -17,6 +24,9 @@ import type { DateRange } from './types/balance.types'
1724
export default function Balance() {
1825
const { isDark } = useThemeStore()
1926
const { selectedPdl } = usePdlStore()
27+
const { setIsLoading } = useDataFetchStore()
28+
const isDemo = useIsDemo()
29+
const demoAutoFetchDone = useRef(false)
2030

2131
// Default date range: 3 years back
2232
const defaultDateRange = useMemo((): DateRange => {
@@ -59,6 +69,30 @@ export default function Balance() {
5969
productionDetailData
6070
)
6171

72+
// Get PDL list for unified fetch
73+
const { data: pdlsResponse } = useQuery({
74+
queryKey: ['pdls'],
75+
queryFn: async () => {
76+
const response = await pdlApi.list()
77+
if (response.success && Array.isArray(response.data)) {
78+
return response.data as PDL[]
79+
}
80+
return []
81+
},
82+
})
83+
const allPDLs: PDL[] = Array.isArray(pdlsResponse) ? pdlsResponse : []
84+
85+
// Hook for demo auto-fetch
86+
const { fetchAllData } = useUnifiedDataFetch({
87+
selectedPDL: selectedPdl,
88+
selectedPDLDetails,
89+
allPDLs,
90+
pageContext: 'all',
91+
})
92+
93+
// Check if data is in cache
94+
const hasDataInCache = hasConsumptionData && hasProductionData
95+
6296
// Reset loading states when PDL changes
6397
useEffect(() => {
6498
setIsInitialLoadingFromCache(false)
@@ -74,6 +108,22 @@ export default function Balance() {
74108
return () => clearTimeout(timer)
75109
}, [selectedPdl])
76110

111+
// Auto-fetch data for demo account
112+
useEffect(() => {
113+
if (isDemo && selectedPdl && selectedPDLDetails && !demoAutoFetchDone.current && !hasDataInCache && !isInitializing) {
114+
logger.log('[DEMO] Auto-fetching data for demo account on Balance')
115+
demoAutoFetchDone.current = true
116+
setTimeout(async () => {
117+
setIsLoading(true)
118+
try {
119+
await fetchAllData()
120+
} finally {
121+
setIsLoading(false)
122+
}
123+
}, 300)
124+
}
125+
}, [isDemo, selectedPdl, selectedPDLDetails, hasDataInCache, isInitializing, setIsLoading, fetchAllData])
126+
77127
// Initialize selected years when chartData becomes available
78128
useEffect(() => {
79129
if (chartData?.years?.length && selectedYears.length === 0) {

apps/web/src/pages/ConsumptionEuro/components/EuroCostCards.tsx

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -58,81 +58,88 @@ export function EuroCostCards({ yearlyCosts, selectedOffer, isLoading }: EuroCos
5858
{/* Stats Cards */}
5959
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
6060
{/* Total Cost Card */}
61-
<div className="bg-gradient-to-br from-green-50 to-emerald-100 dark:from-green-900/30 dark:to-emerald-900/30 rounded-xl p-4 border border-green-200 dark:border-green-800">
61+
<div className="bg-gray-50 dark:bg-gray-800/80 rounded-xl p-4 border-2 border-emerald-500 shadow-sm hover:shadow-md transition-all duration-200">
6262
<div className="flex items-center justify-between mb-2">
63-
<span className="text-sm font-medium text-green-700 dark:text-green-300">Coût Total Annuel</span>
64-
<Euro className="text-green-600 dark:text-green-400" size={18} />
63+
<span className="text-sm font-medium text-gray-600 dark:text-gray-300">Coût Total Annuel</span>
64+
<Euro className="text-emerald-500" size={18} />
6565
</div>
66-
<div className="text-2xl font-bold text-green-900 dark:text-green-100">
66+
<div className="text-2xl font-bold text-gray-900 dark:text-white">
6767
{formatCurrency(currentYear.totalCost)}
6868
</div>
69-
<div className="text-xs text-green-600 dark:text-green-400 mt-1">
69+
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
7070
{currentYear.periodLabel}
7171
</div>
7272
{yoyChange !== null && (
73-
<div className={`flex items-center gap-1 mt-2 text-sm ${yoyChange > 0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}`}>
74-
{yoyChange > 0 ? <TrendingUp size={14} /> : <TrendingDown size={14} />}
75-
<span>{yoyChange > 0 ? '+' : ''}{yoyChange.toFixed(1)}% vs année précédente</span>
73+
<div className={`flex items-center gap-1.5 mt-2 text-sm`}>
74+
{yoyChange > 0 ? (
75+
<TrendingUp size={14} className="text-red-500" />
76+
) : (
77+
<TrendingDown size={14} className="text-emerald-500" />
78+
)}
79+
<span className={`font-medium ${yoyChange > 0 ? 'text-red-600 dark:text-red-400' : 'text-emerald-600 dark:text-emerald-400'}`}>
80+
{yoyChange > 0 ? '+' : ''}{yoyChange.toFixed(1)}%
81+
</span>
82+
<span className="text-gray-500 dark:text-gray-400">vs année précédente</span>
7683
</div>
7784
)}
7885
</div>
7986

8087
{/* Average Monthly Cost */}
81-
<div className="bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-blue-900/30 dark:to-indigo-900/30 rounded-xl p-4 border border-blue-200 dark:border-blue-800">
88+
<div className="bg-gray-50 dark:bg-gray-800/80 rounded-xl p-4 border-2 border-blue-500 shadow-sm hover:shadow-md transition-all duration-200">
8289
<div className="flex items-center justify-between mb-2">
83-
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">Coût Moyen Mensuel</span>
84-
<Calendar className="text-blue-600 dark:text-blue-400" size={18} />
90+
<span className="text-sm font-medium text-gray-600 dark:text-gray-300">Coût Moyen Mensuel</span>
91+
<Calendar className="text-blue-500" size={18} />
8592
</div>
86-
<div className="text-2xl font-bold text-blue-900 dark:text-blue-100">
93+
<div className="text-2xl font-bold text-gray-900 dark:text-white">
8794
{formatCurrency(currentYear.avgMonthlyCost)}
8895
</div>
89-
<div className="text-xs text-blue-600 dark:text-blue-400 mt-1">
96+
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
9097
Sur {currentYear.months.length} mois
9198
</div>
92-
<div className="text-xs text-blue-500 dark:text-blue-500 mt-2">
99+
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
93100
{formatCurrency(currentYear.avgMonthlyCost / 30)}/jour
94101
</div>
95102
</div>
96103

97104
{/* Consumption Cost Breakdown */}
98-
<div className="bg-gradient-to-br from-amber-50 to-orange-100 dark:from-amber-900/30 dark:to-orange-900/30 rounded-xl p-4 border border-amber-200 dark:border-amber-800">
105+
<div className="bg-gray-50 dark:bg-gray-800/80 rounded-xl p-4 border-2 border-amber-500 shadow-sm hover:shadow-md transition-all duration-200">
99106
<div className="flex items-center justify-between mb-2">
100-
<span className="text-sm font-medium text-amber-700 dark:text-amber-300">Consommation</span>
101-
<Zap className="text-amber-600 dark:text-amber-400" size={18} />
107+
<span className="text-sm font-medium text-gray-600 dark:text-gray-300">Consommation</span>
108+
<Zap className="text-amber-500" size={18} />
102109
</div>
103-
<div className="text-2xl font-bold text-amber-900 dark:text-amber-100">
110+
<div className="text-2xl font-bold text-gray-900 dark:text-white">
104111
{formatCurrency(currentYear.consumptionCost)}
105112
</div>
106-
<div className="text-xs text-amber-600 dark:text-amber-400 mt-1">
113+
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
107114
{formatKwh(currentYear.totalKwh)} consommés
108115
</div>
109-
<div className="text-xs text-amber-500 dark:text-amber-500 mt-2">
116+
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
110117
Abonnement: {formatCurrency(currentYear.subscriptionCost)}
111118
</div>
112119
</div>
113120

114121
{/* HC/HP Split */}
115-
<div className="bg-gradient-to-br from-purple-50 to-violet-100 dark:from-purple-900/30 dark:to-violet-900/30 rounded-xl p-4 border border-purple-200 dark:border-purple-800">
122+
<div className="bg-gray-50 dark:bg-gray-800/80 rounded-xl p-4 border-2 border-purple-500 shadow-sm hover:shadow-md transition-all duration-200">
116123
<div className="flex items-center justify-between mb-2">
117-
<span className="text-sm font-medium text-purple-700 dark:text-purple-300">Répartition HC/HP</span>
124+
<span className="text-sm font-medium text-gray-600 dark:text-gray-300">Répartition HC/HP</span>
118125
<div className="flex gap-1">
119-
<Moon className="text-purple-600 dark:text-purple-400" size={14} />
120-
<Sun className="text-purple-600 dark:text-purple-400" size={14} />
126+
<Moon className="text-purple-500" size={14} />
127+
<Sun className="text-purple-500" size={14} />
121128
</div>
122129
</div>
123-
<div className="text-2xl font-bold text-purple-900 dark:text-purple-100">
130+
<div className="text-2xl font-bold text-gray-900 dark:text-white">
124131
{hcPercent.toFixed(0)}% HC
125132
</div>
126133
<div className="flex gap-2 mt-2">
127134
<div className="flex-1">
128-
<div className="text-xs text-purple-600 dark:text-purple-400">Heures Creuses</div>
129-
<div className="text-sm font-semibold text-purple-800 dark:text-purple-200">
135+
<div className="text-xs text-gray-500 dark:text-gray-400">Heures Creuses</div>
136+
<div className="text-sm font-semibold text-gray-700 dark:text-gray-200">
130137
{formatCurrency(currentYear.hcCost)}
131138
</div>
132139
</div>
133140
<div className="flex-1">
134-
<div className="text-xs text-purple-600 dark:text-purple-400">Heures Pleines</div>
135-
<div className="text-sm font-semibold text-purple-800 dark:text-purple-200">
141+
<div className="text-xs text-gray-500 dark:text-gray-400">Heures Pleines</div>
142+
<div className="text-sm font-semibold text-gray-700 dark:text-gray-200">
136143
{formatCurrency(currentYear.hpCost)}
137144
</div>
138145
</div>
@@ -143,7 +150,7 @@ export function EuroCostCards({ yearlyCosts, selectedOffer, isLoading }: EuroCos
143150
{/* Previous year comparison */}
144151
{previousYear && (
145152
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
146-
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
153+
<div className="bg-gray-50 dark:bg-gray-800/80 rounded-xl p-3 border-2 border-gray-300 dark:border-gray-600 shadow-sm">
147154
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">{previousYear.periodLabel}</div>
148155
<div className="text-lg font-semibold text-gray-900 dark:text-gray-100">
149156
{formatCurrency(previousYear.totalCost)}
@@ -152,7 +159,7 @@ export function EuroCostCards({ yearlyCosts, selectedOffer, isLoading }: EuroCos
152159
{formatKwh(previousYear.totalKwh)}
153160
</div>
154161
</div>
155-
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
162+
<div className="bg-gray-50 dark:bg-gray-800/80 rounded-xl p-3 border-2 border-gray-300 dark:border-gray-600 shadow-sm">
156163
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Économie/Surcoût</div>
157164
<div className={`text-lg font-semibold ${currentYear.totalCost > previousYear.totalCost ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}`}>
158165
{currentYear.totalCost > previousYear.totalCost ? '+' : ''}{formatCurrency(currentYear.totalCost - previousYear.totalCost)}
@@ -161,7 +168,7 @@ export function EuroCostCards({ yearlyCosts, selectedOffer, isLoading }: EuroCos
161168
Par rapport à l'année précédente
162169
</div>
163170
</div>
164-
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
171+
<div className="bg-gray-50 dark:bg-gray-800/80 rounded-xl p-3 border-2 border-gray-300 dark:border-gray-600 shadow-sm">
165172
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Variation Consommation</div>
166173
<div className={`text-lg font-semibold ${currentYear.totalKwh > previousYear.totalKwh ? 'text-amber-600 dark:text-amber-400' : 'text-green-600 dark:text-green-400'}`}>
167174
{currentYear.totalKwh > previousYear.totalKwh ? '+' : ''}{((currentYear.totalKwh - previousYear.totalKwh) / previousYear.totalKwh * 100).toFixed(1)}%

0 commit comments

Comments
 (0)