Skip to content

Commit 2ff2a09

Browse files
authored
Merge pull request #17 from MyElectricalData/gitbutler/workspace
Add consumption submenu with kWh and Euro tabs
2 parents 0d5285e + e739737 commit 2ff2a09

22 files changed

+4731
-44
lines changed

apps/web/src/App.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import AdminRoles from './pages/AdminRoles'
2525
import AdminLogs from './pages/AdminLogs'
2626
import Tempo from './pages/Tempo'
2727
import EcoWatt from './pages/EcoWatt'
28-
import Consumption from './pages/Consumption'
28+
import ConsumptionKwh from './pages/ConsumptionKwh'
29+
import ConsumptionEuro from './pages/ConsumptionEuro'
2930
import Production from './pages/Production'
3031
import FAQ from './pages/FAQ'
3132
import ApiDocs from './pages/ApiDocs'
@@ -191,12 +192,27 @@ function App() {
191192
</ProtectedRoute>
192193
}
193194
/>
195+
{/* Consumption routes with submenu */}
194196
<Route
195197
path="/consumption"
198+
element={<Navigate to="/consumption_kwh" replace />}
199+
/>
200+
<Route
201+
path="/consumption_kwh"
202+
element={
203+
<ProtectedRoute>
204+
<Layout>
205+
<ConsumptionKwh />
206+
</Layout>
207+
</ProtectedRoute>
208+
}
209+
/>
210+
<Route
211+
path="/consumption_euro"
196212
element={
197213
<ProtectedRoute>
198214
<Layout>
199-
<Consumption />
215+
<ConsumptionEuro />
200216
</Layout>
201217
</ProtectedRoute>
202218
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Link, useLocation } from 'react-router-dom'
2+
import { Zap, Euro } from 'lucide-react'
3+
4+
interface Tab {
5+
name: string
6+
path: string
7+
icon: typeof Zap
8+
}
9+
10+
const tabs: Tab[] = [
11+
{ name: 'kWh', path: '/consumption_kwh', icon: Zap },
12+
{ name: 'Euro', path: '/consumption_euro', icon: Euro },
13+
]
14+
15+
export default function ConsumptionTabs() {
16+
const location = useLocation()
17+
18+
return (
19+
<div className="w-full border-b border-gray-200 dark:border-gray-700 overflow-x-auto bg-white dark:bg-gray-800">
20+
<nav className="flex justify-center gap-1 min-w-max px-3 sm:px-4 lg:px-6" aria-label="Tabs">
21+
{tabs.map((tab) => {
22+
const isActive = location.pathname === tab.path
23+
const Icon = tab.icon
24+
return (
25+
<Link
26+
key={tab.path}
27+
to={tab.path}
28+
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
29+
isActive
30+
? 'border-primary-500 text-primary-600 dark:text-primary-400'
31+
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
32+
}`}
33+
>
34+
<Icon size={16} />
35+
{tab.name}
36+
</Link>
37+
)
38+
})}
39+
</nav>
40+
</div>
41+
)
42+
}

apps/web/src/components/Layout.tsx

Lines changed: 54 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useState, useEffect } from 'react'
77
import toast, { Toaster } from 'react-hot-toast'
88
import AdminTabs from './AdminTabs'
99
import ApiDocsTabs from './ApiDocsTabs'
10+
import ConsumptionTabs from './ConsumptionTabs'
1011
import PageHeader from './PageHeader'
1112
import { PageTransition } from './PageTransition'
1213
import { useQueryClient } from '@tanstack/react-query'
@@ -34,14 +35,17 @@ export default function Layout({ children }: { children: React.ReactNode }) {
3435
// Menu items
3536
const menuItems = [
3637
{ to: '/dashboard', icon: Home, label: 'Tableau de bord' },
37-
{ to: '/consumption', icon: TrendingUp, label: 'Consommation' },
38+
{ to: '/consumption_kwh', icon: TrendingUp, label: 'Consommation' },
3839
{ to: '/production', icon: Sun, label: 'Production' },
3940
{ to: '/simulator', icon: Calculator, label: 'Simulateur' },
4041
{ to: '/contribute', icon: Users, label: 'Contribuer' },
4142
{ to: '/tempo', icon: Calendar, label: 'Tempo' },
4243
{ to: '/ecowatt', icon: Zap, label: 'EcoWatt' },
4344
]
4445

46+
// Check if we're on a consumption page (for active state and tabs)
47+
const isConsumptionPage = location.pathname.startsWith('/consumption')
48+
4549
// Clear cache function (admin only)
4650
const handleClearCache = async () => {
4751
if (!user?.is_admin) {
@@ -154,26 +158,32 @@ export default function Layout({ children }: { children: React.ReactNode }) {
154158
{/* Navigation */}
155159
<nav className="flex-1 overflow-y-auto py-4">
156160
<div className="space-y-1 px-2">
157-
{menuItems.map((item) => (
158-
<Link
159-
key={item.to}
160-
to={item.to}
161-
className={`flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors ${
162-
location.pathname === item.to
163-
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400'
164-
: 'hover:bg-gray-100 dark:hover:bg-gray-700'
165-
}`}
166-
title={sidebarCollapsed ? item.label : ''}
167-
data-tour={item.to === '/consumption' ? 'nav-consumption' :
168-
item.to === '/simulator' ? 'nav-simulator' :
169-
item.to === '/contribute' ? 'nav-contribute' : undefined}
170-
>
171-
<item.icon size={20} className="flex-shrink-0" />
172-
{!sidebarCollapsed && (
173-
<span className="font-medium">{item.label}</span>
174-
)}
175-
</Link>
176-
))}
161+
{menuItems.map((item) => {
162+
// Special handling for consumption - active if any consumption page
163+
const isActive = item.to === '/consumption_kwh'
164+
? isConsumptionPage
165+
: location.pathname === item.to
166+
return (
167+
<Link
168+
key={item.to}
169+
to={item.to}
170+
className={`flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors ${
171+
isActive
172+
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400'
173+
: 'hover:bg-gray-100 dark:hover:bg-gray-700'
174+
}`}
175+
title={sidebarCollapsed ? item.label : ''}
176+
data-tour={item.to === '/consumption_kwh' ? 'nav-consumption' :
177+
item.to === '/simulator' ? 'nav-simulator' :
178+
item.to === '/contribute' ? 'nav-contribute' : undefined}
179+
>
180+
<item.icon size={20} className="flex-shrink-0" />
181+
{!sidebarCollapsed && (
182+
<span className="font-medium">{item.label}</span>
183+
)}
184+
</Link>
185+
)
186+
})}
177187

178188
{/* Admin Link */}
179189
{canAccessAdmin() && (
@@ -326,21 +336,27 @@ export default function Layout({ children }: { children: React.ReactNode }) {
326336
{/* Navigation */}
327337
<nav className="flex-1 overflow-y-auto py-4">
328338
<div className="space-y-1 px-2">
329-
{menuItems.map((item) => (
330-
<Link
331-
key={item.to}
332-
to={item.to}
333-
onClick={() => setMobileMenuOpen(false)}
334-
className={`flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors ${
335-
location.pathname === item.to
336-
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400'
337-
: 'hover:bg-gray-100 dark:hover:bg-gray-700'
338-
}`}
339-
>
340-
<item.icon size={20} />
341-
<span className="font-medium">{item.label}</span>
342-
</Link>
343-
))}
339+
{menuItems.map((item) => {
340+
// Special handling for consumption - active if any consumption page
341+
const isActive = item.to === '/consumption_kwh'
342+
? isConsumptionPage
343+
: location.pathname === item.to
344+
return (
345+
<Link
346+
key={item.to}
347+
to={item.to}
348+
onClick={() => setMobileMenuOpen(false)}
349+
className={`flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors ${
350+
isActive
351+
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400'
352+
: 'hover:bg-gray-100 dark:hover:bg-gray-700'
353+
}`}
354+
>
355+
<item.icon size={20} />
356+
<span className="font-medium">{item.label}</span>
357+
</Link>
358+
)
359+
})}
344360

345361
{canAccessAdmin() && (
346362
<>
@@ -459,11 +475,12 @@ export default function Layout({ children }: { children: React.ReactNode }) {
459475
<PageHeader />
460476
{location.pathname.startsWith('/admin') && <AdminTabs />}
461477
{location.pathname.startsWith('/api-docs') && <ApiDocsTabs />}
478+
{isConsumptionPage && <ConsumptionTabs />}
462479
</div>
463480

464481
{/* Main Content */}
465482
<main className={`flex-1 bg-gray-50 dark:bg-gray-900 ${isAdminLogsPage ? 'overflow-hidden' : 'overflow-y-auto'}`}>
466-
<div className={`container mx-auto px-3 sm:px-4 lg:px-6 max-w-[1920px] ${isAdminLogsPage ? 'h-full pb-0' : 'pb-6'} ${(location.pathname.startsWith('/admin') || location.pathname.startsWith('/api-docs')) ? 'pt-4' : ''}`}>
483+
<div className={`container mx-auto px-3 sm:px-4 lg:px-6 max-w-[1920px] ${isAdminLogsPage ? 'h-full pb-0' : 'pb-6'} ${(location.pathname.startsWith('/admin') || location.pathname.startsWith('/api-docs') || isConsumptionPage) ? 'pt-4' : ''}`}>
467484
<PageTransition key={location.pathname}>
468485
{children}
469486
</PageTransition>

apps/web/src/components/PageHeader.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect } from 'react'
22
import { useLocation } from 'react-router-dom'
3-
import { TrendingUp, Sun, Calculator, Download, Lock, LayoutDashboard, Calendar, Zap, Users, AlertCircle, BookOpen, Settings as SettingsIcon, Key, Shield, FileText, Activity } from 'lucide-react'
3+
import { TrendingUp, Sun, Calculator, Download, Lock, LayoutDashboard, Calendar, Zap, Users, AlertCircle, BookOpen, Settings as SettingsIcon, Key, Shield, FileText, Activity, Euro } from 'lucide-react'
44
import { useQuery } from '@tanstack/react-query'
55
import { pdlApi } from '@/api/pdl'
66
import { usePdlStore } from '@/stores/pdlStore'
@@ -12,15 +12,16 @@ import type { PDL } from '@/types/api'
1212

1313
// Pages qui affichent le sélecteur de PDL avec bouton "Récupérer"
1414
const PDL_SELECTOR_PAGES = [
15-
'/consumption', '/production', '/simulator', '/dashboard', '/tempo', '/ecowatt', '/contribute',
15+
'/consumption_kwh', '/consumption_euro', '/production', '/simulator', '/dashboard', '/tempo', '/ecowatt', '/contribute',
1616
'/faq', '/api-docs', '/api-docs/auth', '/settings',
1717
'/admin', '/admin/users', '/admin/tempo', '/admin/ecowatt', '/admin/contributions', '/admin/offers', '/admin/roles', '/admin/logs', '/admin/add-pdl'
1818
]
1919

2020
// Configuration des titres et icônes par page
2121
const PAGE_CONFIG: Record<string, { title: string; icon: typeof TrendingUp; subtitle?: string }> = {
2222
'/dashboard': { title: 'Tableau de bord', icon: LayoutDashboard, subtitle: 'Gérez vos points de livraison' },
23-
'/consumption': { title: 'Consommation', icon: TrendingUp, subtitle: 'Visualisez et analysez votre consommation électrique' },
23+
'/consumption_kwh': { title: 'Consommation', icon: TrendingUp, subtitle: 'Visualisez et analysez votre consommation électrique en kWh' },
24+
'/consumption_euro': { title: 'Consommation', icon: Euro, subtitle: 'Visualisez et analysez le coût de votre consommation en euros' },
2425
'/production': { title: 'Production', icon: Sun, subtitle: 'Visualisez et analysez votre production d\'énergie solaire' },
2526
'/simulator': { title: 'Comparateur des abonnements', icon: Calculator, subtitle: 'Comparez automatiquement le coût de toutes les offres disponibles' },
2627
'/tempo': { title: 'Calendrier Tempo', icon: Calendar, subtitle: 'Historique des jours Tempo bleus, blancs et rouges fourni par RTE' },
@@ -107,6 +108,9 @@ export default function PageHeader() {
107108
const Icon = config.icon
108109
const activePdls = pdls.filter((p: PDL) => p.is_active)
109110

111+
// Check if on a consumption page
112+
const isConsumptionPage = location.pathname.startsWith('/consumption')
113+
110114
// Filter PDLs based on page
111115
const displayedPdls = location.pathname === '/production'
112116
? (() => {
@@ -130,7 +134,7 @@ export default function PageHeader() {
130134

131135
return [...consumptionWithProduction, ...standaloneProduction]
132136
})()
133-
: location.pathname === '/consumption'
137+
: isConsumptionPage
134138
? activePdls.filter((pdl: PDL) => pdl.has_consumption)
135139
: activePdls
136140

@@ -159,7 +163,7 @@ export default function PageHeader() {
159163
<div className="text-sm text-gray-600 dark:text-gray-400 italic">
160164
{location.pathname === '/production'
161165
? 'Aucun PDL de production non lié trouvé'
162-
: location.pathname === '/consumption'
166+
: isConsumptionPage
163167
? 'Aucun PDL avec l\'option consommation activée'
164168
: 'Aucun point de livraison actif trouvé'}
165169
</div>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useState, useEffect } from 'react'
2+
import { Euro, Database, ArrowRight, AlertCircle } from 'lucide-react'
3+
import { usePdlStore } from '@/stores/pdlStore'
4+
5+
export default function ConsumptionEuro() {
6+
const { selectedPdl: selectedPDL } = usePdlStore()
7+
const [isDarkMode, setIsDarkMode] = useState(false)
8+
9+
// Detect dark mode
10+
useEffect(() => {
11+
const checkDarkMode = () => {
12+
setIsDarkMode(document.documentElement.classList.contains('dark'))
13+
}
14+
checkDarkMode()
15+
const observer = new MutationObserver(checkDarkMode)
16+
observer.observe(document.documentElement, {
17+
attributes: true,
18+
attributeFilter: ['class']
19+
})
20+
return () => observer.disconnect()
21+
}, [])
22+
23+
return (
24+
<div className="w-full">
25+
{/* Coming Soon / Work in Progress State */}
26+
<div className="rounded-xl shadow-md border bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 transition-colors duration-200">
27+
<div className="flex flex-col items-center justify-center py-16 px-6 text-center">
28+
<div className="w-20 h-20 rounded-full bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center mb-6">
29+
<Euro className="w-10 h-10 text-amber-600 dark:text-amber-400" />
30+
</div>
31+
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
32+
Consommation en Euros
33+
</h3>
34+
<p className="text-gray-600 dark:text-gray-400 max-w-md mb-6">
35+
Cette fonctionnalité est en cours de développement. Elle vous permettra de visualiser
36+
votre consommation électrique convertie en euros selon les tarifs de votre fournisseur.
37+
</p>
38+
39+
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 max-w-lg">
40+
<div className="flex items-start gap-3">
41+
<AlertCircle className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
42+
<div className="text-left">
43+
<p className="text-sm text-blue-800 dark:text-blue-200 font-medium mb-1">
44+
Fonctionnalités à venir :
45+
</p>
46+
<ul className="text-sm text-blue-700 dark:text-blue-300 list-disc list-inside space-y-1">
47+
<li>Conversion de la consommation kWh en euros</li>
48+
<li>Application des tarifs HC/HP selon votre abonnement</li>
49+
<li>Comparaison des coûts par période</li>
50+
<li>Estimation de la facture mensuelle</li>
51+
</ul>
52+
</div>
53+
</div>
54+
</div>
55+
56+
<div className="mt-6 flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
57+
<span>Récupérez vos données kWh</span>
58+
<ArrowRight className="w-4 h-4" />
59+
<span>Sélectionnez une offre tarifaire</span>
60+
<ArrowRight className="w-4 h-4" />
61+
<span>Visualisez vos coûts</span>
62+
</div>
63+
</div>
64+
</div>
65+
</div>
66+
)
67+
}

0 commit comments

Comments
 (0)