Skip to content

Commit 92023f5

Browse files
m4dm4rtig4nClément VALENTINclaude
authored
fix(web): resolve TypeScript build errors (#37)
* feat(consumption-euro): Add cost analysis page with year comparison (#33) * feat(consumption-euro): Add cost analysis page with year comparison and detailed breakdown Implement comprehensive electricity cost analysis on /consumption_euro page: - Year stats cards with rolling 365-day period comparison - Multi-year bar/area charts with HC/HP breakdown option - Monthly breakdown table sorted newest to oldest - Offer pricing card displaying all tariff types - Expandable info block with cache, calculation, and data source details - Year-over-year comparison badges showing cost and kWh differences - Support for all offer types: Base, HC/HP, Tempo, EJP with dynamic pricing - Dark mode support throughout Added design documentation in docs/design/components/16-euro-stats.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(deps): regenerate package-lock.json with tree-sitter dependencies Fixes npm ci failure by properly resolving tree-sitter peer dependencies: - tree-sitter@0.21.1 for @swagger-api/apidom-parser-adapter-json - tree-sitter@0.22.4 for @swagger-api/apidom-parser-adapter-yaml-1-2 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Clément VALENTIN <clement.valentin@blacktiger.tech> Co-authored-by: Claude <noreply@anthropic.com> * feat(balance): energy balance page with year filters (#35) * feat(web): add Balance page for production vs consumption recap - Add new /balance route accessible from main sidebar menu - Create Balance page with 4 summary cards (consumption, production, net balance, self-consumption rate) - Add monthly comparison bar chart (production vs consumption) - Add net balance daily curve with green/red gradient - Add yearly summary table with export functionality - Support linked production PDLs - Calculate self-consumption rate from cached data (30min for precise, daily for estimate) - Data is read from React Query cache (no new API calls) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(web): improve Balance page design compliance - Fix H1 structure to use text-3xl with icon inside flex container (6 occurrences) - Add transition-colors duration-200 to all card components - Change H3 to H2 for section titles per design guidelines - Add consistent subtitle "Production vs Consommation" Design compliance score: 72% -> 95% 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(balance): use PageHeader pattern like ConsumptionKwh - Add /balance route to PDL_SELECTOR_PAGES in PageHeader - Add Balance page config to PAGE_CONFIG (Scale icon, title, subtitle) - Remove all inline H1 headers from Balance page - Remove inline PDL selectors (now handled by PageHeader) - Use empty state pattern matching ConsumptionKwh - Fix AnimatedSection isVisible prop - Fix unused Zap import in BalanceSummaryCards 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(balance): use same linked PDL banner style as Production page - Replace simple info text with gradient banner matching Production page - Add lightning bolt SVG icon - Use green gradient background with rounded corners - Display both production and consumption PDL numbers in monospace font 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(balance): add top padding (pt-6) to all page states Add consistent pt-6 padding to all return states (empty, error, loading, main content) to create proper spacing between the PageHeader and page content. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat(balance): improve year filter buttons and add InfoBlock - Display year filter buttons in a single line with multi-select support - Order years from most recent to oldest - Add semi-transparent background colors with colored borders - Include selection indicator dot in top-right corner - Add collapsible InfoBlock with cache warning and explanatory sections - Remove Total row from yearly table - Reduce spacing between chart bars for better visibility - Document Year Filter Buttons pattern in design guidelines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat(balance): display production value in year filter buttons Add production kWh information below each year in the year filter buttons to provide context when selecting years for comparison. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Clément VALENTIN <clement.valentin@blacktiger.tech> Co-authored-by: Claude <noreply@anthropic.com> * debug: Add console logs to Contribute page for provider data (#34) Add detailed logging to understand why providers list appears empty on the Contribute page. Logs show the API response and the current state of providersData, isLoading, and error. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Clément VALENTIN <clement.valentin@blacktiger.tech> Co-authored-by: Claude <noreply@anthropic.com> * fix(web): resolve TypeScript build errors - Add missing 'Info' import from lucide-react in Balance/index.tsx - Fix ejp_pointe -> ejp_peak property name in OfferPricingCard.tsx - Prefix unused hcHpCalculationTrigger parameter with underscore 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(web): regenerate package-lock.json and fix recharts type errors - Regenerate package-lock.json to resolve tree-sitter dependency sync issue - Fix recharts activeLabel type errors by converting to String() in 6 chart components: - Consumption/AnnualCurve.tsx - Consumption/MonthlyHcHp.tsx - Consumption/PowerPeaks.tsx - ConsumptionKwh/AnnualCurve.tsx - ConsumptionKwh/MonthlyHcHp.tsx - ConsumptionKwh/PowerPeaks.tsx - Production/AnnualProductionCurve.tsx This fixes the CI build failure where npm ci failed due to out-of-sync lock file. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Clément VALENTIN <clement.valentin@blacktiger.tech> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 67cbb4e commit 92023f5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+5120
-1234
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""
2+
Migration: Add selected_offer_id column to pdls table
3+
4+
This migration adds a selected_offer_id column to the pdls table
5+
to allow users to select an energy offer for their PDL.
6+
"""
7+
import asyncio
8+
import sys
9+
sys.path.insert(0, '/app')
10+
11+
from sqlalchemy import text
12+
from src.models.database import async_session_maker
13+
14+
15+
async def migrate():
16+
"""Add selected_offer_id column to pdls table"""
17+
async with async_session_maker() as session:
18+
async with session.begin():
19+
# Add selected_offer_id column
20+
await session.execute(text('''
21+
ALTER TABLE pdls
22+
ADD COLUMN IF NOT EXISTS selected_offer_id VARCHAR(36)
23+
'''))
24+
25+
# Add foreign key constraint to energy_offers table
26+
await session.execute(text('''
27+
DO $$
28+
BEGIN
29+
IF NOT EXISTS (
30+
SELECT 1
31+
FROM pg_constraint
32+
WHERE conname = 'fk_pdls_selected_offer_id'
33+
) THEN
34+
ALTER TABLE pdls
35+
ADD CONSTRAINT fk_pdls_selected_offer_id
36+
FOREIGN KEY (selected_offer_id) REFERENCES energy_offers(id)
37+
ON DELETE SET NULL;
38+
END IF;
39+
END $$;
40+
'''))
41+
42+
print("Added selected_offer_id column to pdls table")
43+
44+
45+
async def rollback():
46+
"""Remove selected_offer_id column from pdls table"""
47+
async with async_session_maker() as session:
48+
async with session.begin():
49+
# Drop foreign key constraint first
50+
await session.execute(text('''
51+
ALTER TABLE pdls
52+
DROP CONSTRAINT IF EXISTS fk_pdls_selected_offer_id
53+
'''))
54+
55+
# Drop the column
56+
await session.execute(text('''
57+
ALTER TABLE pdls
58+
DROP COLUMN IF EXISTS selected_offer_id
59+
'''))
60+
61+
print("Removed selected_offer_id column from pdls table")
62+
63+
64+
if __name__ == "__main__":
65+
print("Running migration: add_selected_offer_id")
66+
asyncio.run(migrate())
67+
print("Migration completed successfully!")

apps/api/src/models/pdl.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ class PDL(Base, TimestampMixin):
2424
oldest_available_data_date: Mapped[date | None] = mapped_column(Date, nullable=True) # Oldest date where Enedis has data (meter activation date)
2525
activation_date: Mapped[date | None] = mapped_column(Date, nullable=True) # Contract activation date (from Enedis)
2626
linked_production_pdl_id: Mapped[str | None] = mapped_column(String(36), ForeignKey("pdls.id"), nullable=True) # Link to production PDL for combined graphs
27+
selected_offer_id: Mapped[str | None] = mapped_column(String(36), ForeignKey("energy_offers.id", ondelete="SET NULL"), nullable=True) # Selected energy offer
2728

2829
# Relations
2930
user: Mapped["User"] = relationship("User", back_populates="pdls")
3031
linked_production_pdl: Mapped["PDL | None"] = relationship("PDL", remote_side=[id], foreign_keys=[linked_production_pdl_id], uselist=False)
32+
selected_offer: Mapped["EnergyOffer | None"] = relationship("EnergyOffer", foreign_keys=[selected_offer_id])
3133

3234
def __repr__(self) -> str:
3335
return f"<PDL(id={self.id}, usage_point_id={self.usage_point_id})>"

apps/api/src/routers/pdl.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from sqlalchemy.ext.asyncio import AsyncSession
44
from pydantic import BaseModel
55
from ..models import User, PDL
6+
from ..models.energy_provider import EnergyOffer
67
from ..models.database import get_db
78
from ..schemas import PDLCreate, PDLResponse, APIResponse, ErrorDetail
89
from ..schemas.requests import AdminPDLCreate
@@ -56,6 +57,10 @@ class PDLUpdatePricingOption(BaseModel):
5657
pricing_option: str | None = None # BASE, HC_HP, TEMPO, EJP, HC_WEEKEND
5758

5859

60+
class PDLUpdateSelectedOffer(BaseModel):
61+
selected_offer_id: str | None = None # Energy offer ID or None to unselect
62+
63+
5964
class PDLOrderItem(BaseModel):
6065
id: str
6166
order: int
@@ -99,6 +104,7 @@ async def list_pdls(
99104
oldest_available_data_date=pdl.oldest_available_data_date,
100105
activation_date=pdl.activation_date,
101106
linked_production_pdl_id=pdl.linked_production_pdl_id,
107+
selected_offer_id=pdl.selected_offer_id,
102108
)
103109
for pdl in pdls
104110
]
@@ -309,6 +315,7 @@ async def create_pdl(
309315
oldest_available_data_date=pdl.oldest_available_data_date,
310316
activation_date=pdl.activation_date,
311317
linked_production_pdl_id=pdl.linked_production_pdl_id,
318+
selected_offer_id=pdl.selected_offer_id,
312319
)
313320

314321
return APIResponse(success=True, data=pdl_response.model_dump())
@@ -348,6 +355,7 @@ async def get_pdl(
348355
oldest_available_data_date=pdl.oldest_available_data_date,
349356
activation_date=pdl.activation_date,
350357
linked_production_pdl_id=pdl.linked_production_pdl_id,
358+
selected_offer_id=pdl.selected_offer_id,
351359
)
352360

353361
return APIResponse(success=True, data=pdl_response.model_dump())
@@ -524,6 +532,75 @@ async def update_pdl_pricing_option(
524532
)
525533

526534

535+
@router.patch("/{pdl_id}/selected-offer", response_model=APIResponse)
536+
async def update_pdl_selected_offer(
537+
pdl_id: str = Path(..., description="PDL ID (UUID)", openapi_examples={"example_uuid": {"summary": "Example UUID", "value": "550e8400-e29b-41d4-a716-446655440000"}}),
538+
offer_data: PDLUpdateSelectedOffer = Body(..., openapi_examples={
539+
"select_offer": {"summary": "Select an energy offer", "value": {"selected_offer_id": "550e8400-e29b-41d4-a716-446655440001"}},
540+
"clear": {"summary": "Remove selected offer", "value": {"selected_offer_id": None}}
541+
}),
542+
current_user: User = Depends(get_current_user),
543+
db: AsyncSession = Depends(get_db),
544+
) -> APIResponse:
545+
"""
546+
Update PDL selected energy offer.
547+
548+
This endpoint allows you to select an energy offer for a PDL.
549+
When an offer is selected, the PDL's pricing_option will be automatically
550+
updated to match the offer's offer_type.
551+
552+
Set `selected_offer_id` to `null` to remove the selection.
553+
"""
554+
result = await db.execute(select(PDL).where(PDL.id == pdl_id, PDL.user_id == current_user.id))
555+
pdl = result.scalar_one_or_none()
556+
557+
if not pdl:
558+
return APIResponse(success=False, error=ErrorDetail(code="PDL_NOT_FOUND", message="PDL not found"))
559+
560+
# If clearing the selection
561+
if offer_data.selected_offer_id is None:
562+
pdl.selected_offer_id = None
563+
await db.commit()
564+
await db.refresh(pdl)
565+
566+
return APIResponse(
567+
success=True,
568+
data={
569+
"id": pdl.id,
570+
"usage_point_id": pdl.usage_point_id,
571+
"selected_offer_id": None,
572+
"pricing_option": pdl.pricing_option,
573+
},
574+
)
575+
576+
# Validate the offer exists and is active
577+
result = await db.execute(select(EnergyOffer).where(EnergyOffer.id == offer_data.selected_offer_id, EnergyOffer.is_active == True))
578+
offer = result.scalar_one_or_none()
579+
580+
if not offer:
581+
return APIResponse(
582+
success=False,
583+
error=ErrorDetail(code="OFFER_NOT_FOUND", message="Energy offer not found or not active")
584+
)
585+
586+
# Update PDL with selected offer and sync pricing_option
587+
pdl.selected_offer_id = offer.id
588+
pdl.pricing_option = offer.offer_type
589+
590+
await db.commit()
591+
await db.refresh(pdl)
592+
593+
return APIResponse(
594+
success=True,
595+
data={
596+
"id": pdl.id,
597+
"usage_point_id": pdl.usage_point_id,
598+
"selected_offer_id": pdl.selected_offer_id,
599+
"pricing_option": pdl.pricing_option,
600+
},
601+
)
602+
603+
527604
@router.patch("/{pdl_id}/link-production", response_model=APIResponse)
528605
async def link_production_pdl(
529606
pdl_id: str = Path(..., description="PDL ID (UUID) of the consumption PDL", openapi_examples={"example_uuid": {"summary": "Example UUID", "value": "550e8400-e29b-41d4-a716-446655440000"}}),
@@ -771,6 +848,7 @@ async def admin_add_pdl(
771848
oldest_available_data_date=pdl.oldest_available_data_date,
772849
activation_date=pdl.activation_date,
773850
linked_production_pdl_id=pdl.linked_production_pdl_id,
851+
selected_offer_id=pdl.selected_offer_id,
774852
)
775853

776854
return APIResponse(success=True, data=pdl_response.model_dump())

apps/api/src/schemas/responses.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class PDLResponse(BaseModel):
5757
oldest_available_data_date: Optional[date] = None # Oldest date where Enedis has data (meter activation)
5858
activation_date: Optional[date] = None # Contract activation date (from Enedis)
5959
linked_production_pdl_id: Optional[str] = None # Link to production PDL for combined graphs
60+
selected_offer_id: Optional[str] = None # Selected energy offer ID
6061

6162

6263
class CacheDeleteResponse(BaseModel):

0 commit comments

Comments
 (0)