Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 0 additions & 41 deletions apps/web/src/components/OfferSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,47 +165,6 @@ export default function OfferSelector({
return `${numPrice.toFixed(2)} €/mois`
}

// Get main price to display (short version for dropdown)
const getMainPrice = (offer: EnergyOffer): string => {
// Base offers
if (offer.offer_type === 'BASE' && offer.base_price !== undefined && offer.base_price !== null) {
return formatPrice(offer.base_price)
}
// HP/HC offers
if (offer.offer_type === 'HC_HP' && offer.hp_price !== undefined && offer.hp_price !== null) {
return `HP: ${formatPrice(offer.hp_price)}`
}
// Tempo offers - show blue HC as reference (most common)
if (offer.offer_type === 'TEMPO' && offer.tempo_blue_hc !== undefined && offer.tempo_blue_hc !== null) {
return `Bleu HC: ${formatPrice(offer.tempo_blue_hc)}`
}
// EJP offers
if (offer.offer_type === 'EJP' && offer.ejp_normal !== undefined && offer.ejp_normal !== null) {
return `Normal: ${formatPrice(offer.ejp_normal)}`
}
// Weekend offers
if (offer.offer_type === 'WEEKEND') {
if (offer.base_price !== undefined && offer.base_price !== null) {
return formatPrice(offer.base_price)
}
if (offer.hp_price !== undefined && offer.hp_price !== null) {
return `Semaine: ${formatPrice(offer.hp_price)}`
}
}
// Seasonal offers
if (offer.offer_type === 'SEASONAL' && offer.hp_price_winter !== undefined && offer.hp_price_winter !== null) {
return `Hiver HP: ${formatPrice(offer.hp_price_winter)}`
}
// Fallback to base_price or hp_price
if (offer.base_price !== undefined && offer.base_price !== null) {
return formatPrice(offer.base_price)
}
if (offer.hp_price !== undefined && offer.hp_price !== null) {
return `HP: ${formatPrice(offer.hp_price)}`
}
return '-'
}

// Get detailed prices for selected offer summary
const getDetailedPrices = (offer: EnergyOffer): React.ReactNode => {
const priceRows: React.ReactNode[] = []
Expand Down
29 changes: 25 additions & 4 deletions apps/web/src/components/PageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
import { TrendingUp, Sun, Calculator, Download, Lock, LayoutDashboard, Calendar, Zap, Users, AlertCircle, BookOpen, Settings as SettingsIcon, Key, Shield, FileText, Activity, Euro, Scale } from 'lucide-react'
import { useQuery } from '@tanstack/react-query'
import toast from 'react-hot-toast'
import { pdlApi } from '@/api/pdl'
import { usePdlStore } from '@/stores/pdlStore'
import { useDataFetchStore } from '@/stores/dataFetchStore'
Expand Down Expand Up @@ -141,6 +142,27 @@ export default function PageHeader() {
? activePdls.filter((pdl: PDL) => pdl.has_consumption)
: activePdls.filter((pdl: PDL) => !linkedProductionIds.has(pdl.id)) // Hide linked production PDLs globally

// Auto-select first PDL if none selected OR if current PDL is not in the displayed list
useEffect(() => {
if (displayedPdls.length > 0) {
// Check if current PDL is in the displayed list for this page
const currentPdlInList = displayedPdls.some(p => p.usage_point_id === selectedPdl)

if (!selectedPdl || !currentPdlInList) {
const newPdl = displayedPdls[0]
// Show toast only if we're switching from an incompatible PDL (not on first load)
if (selectedPdl && !currentPdlInList) {
toast(`PDL changé automatiquement vers "${newPdl.name || newPdl.usage_point_id}"`, {
icon: '🔄',
duration: 4000,
})
Comment on lines +155 to +158
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The toast message uses template literals but doesn't handle potential undefined values gracefully. If newPdl.name is undefined, it will display "undefined" in the message. Consider using: toast(\PDL changé automatiquement vers "${newPdl.name || newPdl.usage_point_id}"`, ...)` for consistency with how it's displayed in the select option.

Copilot uses AI. Check for mistakes.
}
// Select the first available PDL for this page
setSelectedPdl(newPdl.usage_point_id)
}
}
}, [displayedPdls, selectedPdl, setSelectedPdl])

return (
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div className="container mx-auto px-3 sm:px-4 lg:px-6 max-w-[1920px]">
Expand Down Expand Up @@ -186,7 +208,6 @@ export default function PageHeader() {
onChange={(e) => setSelectedPdl(e.target.value)}
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"
>
<option value="">Sélectionner un PDL</option>
{displayedPdls.map((pdl: PDL) => (
<option key={pdl.usage_point_id} value={pdl.usage_point_id}>
{pdl.name || pdl.usage_point_id}
Expand All @@ -197,7 +218,7 @@ export default function PageHeader() {
{/* Bouton de récupération - Affiché seulement quand pas en chargement */}
<button
onClick={async () => {
if (!isDemo && selectedPdl) {
if (!isDemo && selectedPdl && selectedPDLDetails) {
setIsLoading(true)
try {
await fetchAllData()
Expand All @@ -206,9 +227,9 @@ export default function PageHeader() {
}
}
}}
disabled={!selectedPdl || isDemo}
disabled={!selectedPdl || !selectedPDLDetails || isDemo}
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"
title={isDemo ? 'Récupération bloquée en mode démo' : 'Récupérer toutes les données depuis Enedis'}
title={isDemo ? 'Récupération bloquée en mode démo' : !selectedPDLDetails ? 'Chargement du PDL...' : 'Récupérer toutes les données depuis Enedis'}
>
{isDemo ? (
<>
Expand Down
52 changes: 51 additions & 1 deletion apps/web/src/pages/Balance/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { useState, useMemo, useEffect } from 'react'
import { useState, useMemo, useEffect, useRef } from 'react'
import { Database, ArrowRight, AlertTriangle, Info } from 'lucide-react'
import { useThemeStore } from '@/stores/themeStore'
import { usePdlStore } from '@/stores/pdlStore'
import { useDataFetchStore } from '@/stores/dataFetchStore'
import { useBalanceData } from './hooks/useBalanceData'
import { useBalanceCalcs } from './hooks/useBalanceCalcs'
import { useIsDemo } from '@/hooks/useIsDemo'
import { useUnifiedDataFetch } from '@/hooks/useUnifiedDataFetch'
import { useQuery } from '@tanstack/react-query'
import { pdlApi } from '@/api/pdl'
import type { PDL } from '@/types/api'
import { logger } from '@/utils/logger'
import { BalanceSummaryCards } from './components/BalanceSummaryCards'
import { MonthlyComparison } from './components/MonthlyComparison'
import { NetBalanceCurve } from './components/NetBalanceCurve'
Expand All @@ -17,6 +24,9 @@ import type { DateRange } from './types/balance.types'
export default function Balance() {
const { isDark } = useThemeStore()
const { selectedPdl } = usePdlStore()
const { setIsLoading } = useDataFetchStore()
const isDemo = useIsDemo()
const demoAutoFetchDone = useRef(false)

// Default date range: 3 years back
const defaultDateRange = useMemo((): DateRange => {
Expand Down Expand Up @@ -59,6 +69,30 @@ export default function Balance() {
productionDetailData
)

// Get PDL list for unified fetch
const { data: pdlsResponse } = useQuery({
queryKey: ['pdls'],
queryFn: async () => {
const response = await pdlApi.list()
if (response.success && Array.isArray(response.data)) {
return response.data as PDL[]
}
return []
},
})
const allPDLs: PDL[] = Array.isArray(pdlsResponse) ? pdlsResponse : []

// Hook for demo auto-fetch
const { fetchAllData } = useUnifiedDataFetch({
selectedPDL: selectedPdl,
selectedPDLDetails,
allPDLs,
pageContext: 'all',
})

// Check if data is in cache
const hasDataInCache = hasConsumptionData && hasProductionData

// Reset loading states when PDL changes
useEffect(() => {
setIsInitialLoadingFromCache(false)
Expand All @@ -74,6 +108,22 @@ export default function Balance() {
return () => clearTimeout(timer)
}, [selectedPdl])

// Auto-fetch data for demo account
useEffect(() => {
if (isDemo && selectedPdl && selectedPDLDetails && !demoAutoFetchDone.current && !hasDataInCache && !isInitializing) {
logger.log('[DEMO] Auto-fetching data for demo account on Balance')
demoAutoFetchDone.current = true
setTimeout(async () => {
setIsLoading(true)
try {
await fetchAllData()
} finally {
setIsLoading(false)
}
}, 300)
}
}, [isDemo, selectedPdl, selectedPDLDetails, hasDataInCache, isInitializing, setIsLoading, fetchAllData])

// Initialize selected years when chartData becomes available
useEffect(() => {
if (chartData?.years?.length && selectedYears.length === 0) {
Expand Down
71 changes: 39 additions & 32 deletions apps/web/src/pages/ConsumptionEuro/components/EuroCostCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,81 +58,88 @@ export function EuroCostCards({ yearlyCosts, selectedOffer, isLoading }: EuroCos
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Total Cost Card */}
<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">
<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">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-green-700 dark:text-green-300">Coût Total Annuel</span>
<Euro className="text-green-600 dark:text-green-400" size={18} />
<span className="text-sm font-medium text-gray-600 dark:text-gray-300">Coût Total Annuel</span>
<Euro className="text-emerald-500" size={18} />
</div>
<div className="text-2xl font-bold text-green-900 dark:text-green-100">
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{formatCurrency(currentYear.totalCost)}
</div>
<div className="text-xs text-green-600 dark:text-green-400 mt-1">
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{currentYear.periodLabel}
</div>
{yoyChange !== null && (
<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'}`}>
{yoyChange > 0 ? <TrendingUp size={14} /> : <TrendingDown size={14} />}
<span>{yoyChange > 0 ? '+' : ''}{yoyChange.toFixed(1)}% vs année précédente</span>
<div className={`flex items-center gap-1.5 mt-2 text-sm`}>
{yoyChange > 0 ? (
<TrendingUp size={14} className="text-red-500" />
) : (
<TrendingDown size={14} className="text-emerald-500" />
)}
<span className={`font-medium ${yoyChange > 0 ? 'text-red-600 dark:text-red-400' : 'text-emerald-600 dark:text-emerald-400'}`}>
{yoyChange > 0 ? '+' : ''}{yoyChange.toFixed(1)}%
</span>
<span className="text-gray-500 dark:text-gray-400">vs année précédente</span>
</div>
)}
</div>

{/* Average Monthly Cost */}
<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">
<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">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">Coût Moyen Mensuel</span>
<Calendar className="text-blue-600 dark:text-blue-400" size={18} />
<span className="text-sm font-medium text-gray-600 dark:text-gray-300">Coût Moyen Mensuel</span>
<Calendar className="text-blue-500" size={18} />
</div>
<div className="text-2xl font-bold text-blue-900 dark:text-blue-100">
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{formatCurrency(currentYear.avgMonthlyCost)}
</div>
<div className="text-xs text-blue-600 dark:text-blue-400 mt-1">
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Sur {currentYear.months.length} mois
</div>
<div className="text-xs text-blue-500 dark:text-blue-500 mt-2">
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
≈ {formatCurrency(currentYear.avgMonthlyCost / 30)}/jour
</div>
</div>

{/* Consumption Cost Breakdown */}
<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">
<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">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-amber-700 dark:text-amber-300">Consommation</span>
<Zap className="text-amber-600 dark:text-amber-400" size={18} />
<span className="text-sm font-medium text-gray-600 dark:text-gray-300">Consommation</span>
<Zap className="text-amber-500" size={18} />
</div>
<div className="text-2xl font-bold text-amber-900 dark:text-amber-100">
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{formatCurrency(currentYear.consumptionCost)}
</div>
<div className="text-xs text-amber-600 dark:text-amber-400 mt-1">
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{formatKwh(currentYear.totalKwh)} consommés
</div>
<div className="text-xs text-amber-500 dark:text-amber-500 mt-2">
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
Abonnement: {formatCurrency(currentYear.subscriptionCost)}
</div>
</div>

{/* HC/HP Split */}
<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">
<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">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-purple-700 dark:text-purple-300">Répartition HC/HP</span>
<span className="text-sm font-medium text-gray-600 dark:text-gray-300">Répartition HC/HP</span>
<div className="flex gap-1">
<Moon className="text-purple-600 dark:text-purple-400" size={14} />
<Sun className="text-purple-600 dark:text-purple-400" size={14} />
<Moon className="text-purple-500" size={14} />
<Sun className="text-purple-500" size={14} />
</div>
</div>
<div className="text-2xl font-bold text-purple-900 dark:text-purple-100">
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{hcPercent.toFixed(0)}% HC
</div>
<div className="flex gap-2 mt-2">
<div className="flex-1">
<div className="text-xs text-purple-600 dark:text-purple-400">Heures Creuses</div>
<div className="text-sm font-semibold text-purple-800 dark:text-purple-200">
<div className="text-xs text-gray-500 dark:text-gray-400">Heures Creuses</div>
<div className="text-sm font-semibold text-gray-700 dark:text-gray-200">
{formatCurrency(currentYear.hcCost)}
</div>
</div>
<div className="flex-1">
<div className="text-xs text-purple-600 dark:text-purple-400">Heures Pleines</div>
<div className="text-sm font-semibold text-purple-800 dark:text-purple-200">
<div className="text-xs text-gray-500 dark:text-gray-400">Heures Pleines</div>
<div className="text-sm font-semibold text-gray-700 dark:text-gray-200">
{formatCurrency(currentYear.hpCost)}
</div>
</div>
Expand All @@ -143,7 +150,7 @@ export function EuroCostCards({ yearlyCosts, selectedOffer, isLoading }: EuroCos
{/* Previous year comparison */}
{previousYear && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
<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">
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">{previousYear.periodLabel}</div>
<div className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{formatCurrency(previousYear.totalCost)}
Expand All @@ -152,7 +159,7 @@ export function EuroCostCards({ yearlyCosts, selectedOffer, isLoading }: EuroCos
{formatKwh(previousYear.totalKwh)}
</div>
</div>
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
<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">
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Économie/Surcoût</div>
<div className={`text-lg font-semibold ${currentYear.totalCost > previousYear.totalCost ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}`}>
{currentYear.totalCost > previousYear.totalCost ? '+' : ''}{formatCurrency(currentYear.totalCost - previousYear.totalCost)}
Expand All @@ -161,7 +168,7 @@ export function EuroCostCards({ yearlyCosts, selectedOffer, isLoading }: EuroCos
Par rapport à l'année précédente
</div>
</div>
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
<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">
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Variation Consommation</div>
<div className={`text-lg font-semibold ${currentYear.totalKwh > previousYear.totalKwh ? 'text-amber-600 dark:text-amber-400' : 'text-green-600 dark:text-green-400'}`}>
{currentYear.totalKwh > previousYear.totalKwh ? '+' : ''}{((currentYear.totalKwh - previousYear.totalKwh) / previousYear.totalKwh * 100).toFixed(1)}%
Expand Down
Loading