Skip to content

Commit 6706055

Browse files
authored
Merge pull request #12 from MyElectricalData/dashboard-pdl-type
Dashboard pdl type
2 parents 03384a7 + d0ace94 commit 6706055

File tree

10 files changed

+581
-5
lines changed

10 files changed

+581
-5
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""
2+
Migration: Add pricing_option column to pdls table
3+
4+
This migration adds a pricing_option VARCHAR(50) column to the pdls table
5+
to store the user's tariff type (BASE, HC_HP, TEMPO, EJP, HC_WEEKEND).
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 pricing_option column to pdls table"""
17+
async with async_session_maker() as session:
18+
async with session.begin():
19+
# Add pricing_option column
20+
await session.execute(text('''
21+
ALTER TABLE pdls
22+
ADD COLUMN IF NOT EXISTS pricing_option VARCHAR(50) DEFAULT NULL
23+
'''))
24+
25+
print("✅ Added pricing_option column to pdls table")
26+
27+
28+
async def rollback():
29+
"""Remove pricing_option column from pdls table"""
30+
async with async_session_maker() as session:
31+
async with session.begin():
32+
await session.execute(text('''
33+
ALTER TABLE pdls
34+
DROP COLUMN IF EXISTS pricing_option
35+
'''))
36+
37+
print("✅ Removed pricing_option column from pdls table")
38+
39+
40+
if __name__ == "__main__":
41+
print("Running migration: add_pricing_option_to_pdls")
42+
asyncio.run(migrate())
43+
print("Migration completed successfully!")

apps/api/src/models/pdl.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class PDL(Base, TimestampMixin):
1717
# Contract information
1818
subscribed_power: Mapped[int | None] = mapped_column(Integer, nullable=True) # kVA
1919
offpeak_hours: Mapped[dict | None] = mapped_column(JSON, nullable=True) # HC schedules by day
20+
pricing_option: Mapped[str | None] = mapped_column(String(50), nullable=True) # BASE, HC_HP, TEMPO, EJP, HC_WEEKEND
2021
has_consumption: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) # PDL has consumption data
2122
has_production: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) # PDL has production data
2223
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) # PDL is active/enabled

apps/api/src/routers/pdl.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ class PDLLinkProduction(BaseModel):
5252
linked_production_pdl_id: str | None = None # None to unlink
5353

5454

55+
class PDLUpdatePricingOption(BaseModel):
56+
pricing_option: str | None = None # BASE, HC_HP, TEMPO, EJP, HC_WEEKEND
57+
58+
5559
class PDLOrderItem(BaseModel):
5660
id: str
5761
order: int
@@ -88,6 +92,7 @@ async def list_pdls(
8892
display_order=pdl.display_order,
8993
subscribed_power=pdl.subscribed_power,
9094
offpeak_hours=pdl.offpeak_hours,
95+
pricing_option=pdl.pricing_option,
9196
has_consumption=pdl.has_consumption,
9297
has_production=pdl.has_production,
9398
is_active=pdl.is_active,
@@ -297,6 +302,7 @@ async def create_pdl(
297302
created_at=pdl.created_at,
298303
subscribed_power=pdl.subscribed_power,
299304
offpeak_hours=pdl.offpeak_hours,
305+
pricing_option=pdl.pricing_option,
300306
has_consumption=pdl.has_consumption,
301307
has_production=pdl.has_production,
302308
is_active=pdl.is_active,
@@ -335,6 +341,7 @@ async def get_pdl(
335341
display_order=pdl.display_order,
336342
subscribed_power=pdl.subscribed_power,
337343
offpeak_hours=pdl.offpeak_hours,
344+
pricing_option=pdl.pricing_option,
338345
has_consumption=pdl.has_consumption,
339346
has_production=pdl.has_production,
340347
is_active=pdl.is_active,
@@ -461,6 +468,62 @@ async def toggle_pdl_active(
461468
)
462469

463470

471+
@router.patch("/{pdl_id}/pricing-option", response_model=APIResponse)
472+
async def update_pdl_pricing_option(
473+
pdl_id: str = Path(..., description="PDL ID (UUID)", openapi_examples={"example_uuid": {"summary": "Example UUID", "value": "550e8400-e29b-41d4-a716-446655440000"}}),
474+
pricing_data: PDLUpdatePricingOption = Body(..., openapi_examples={
475+
"base": {"summary": "Tarif Base", "value": {"pricing_option": "BASE"}},
476+
"hc_hp": {"summary": "Heures Creuses / Heures Pleines", "value": {"pricing_option": "HC_HP"}},
477+
"tempo": {"summary": "Tarif Tempo", "value": {"pricing_option": "TEMPO"}},
478+
"ejp": {"summary": "Effacement Jour de Pointe", "value": {"pricing_option": "EJP"}},
479+
"hc_weekend": {"summary": "HC Nuit & Week-end", "value": {"pricing_option": "HC_WEEKEND"}},
480+
"clear": {"summary": "Remove pricing option", "value": {"pricing_option": None}}
481+
}),
482+
current_user: User = Depends(get_current_user),
483+
db: AsyncSession = Depends(get_db),
484+
) -> APIResponse:
485+
"""
486+
Update PDL pricing option (tariff type).
487+
488+
Available options:
489+
- **BASE**: Single price 24/7
490+
- **HC_HP**: Off-peak hours (Heures Creuses) / Peak hours (Heures Pleines)
491+
- **TEMPO**: 6-tier pricing based on day color (blue/white/red) and period (HC/HP)
492+
- **EJP**: Peak Day Curtailment (22 expensive days per year)
493+
- **HC_WEEKEND**: Off-peak hours extended to weekends
494+
"""
495+
result = await db.execute(select(PDL).where(PDL.id == pdl_id, PDL.user_id == current_user.id))
496+
pdl = result.scalar_one_or_none()
497+
498+
if not pdl:
499+
return APIResponse(success=False, error=ErrorDetail(code="PDL_NOT_FOUND", message="PDL not found"))
500+
501+
# Validate pricing option if provided
502+
valid_options = ["BASE", "HC_HP", "TEMPO", "EJP", "HC_WEEKEND"]
503+
if pricing_data.pricing_option is not None and pricing_data.pricing_option not in valid_options:
504+
return APIResponse(
505+
success=False,
506+
error=ErrorDetail(
507+
code="INVALID_PRICING_OPTION",
508+
message=f"Invalid pricing option. Must be one of: {', '.join(valid_options)}"
509+
)
510+
)
511+
512+
pdl.pricing_option = pricing_data.pricing_option
513+
514+
await db.commit()
515+
await db.refresh(pdl)
516+
517+
return APIResponse(
518+
success=True,
519+
data={
520+
"id": pdl.id,
521+
"usage_point_id": pdl.usage_point_id,
522+
"pricing_option": pdl.pricing_option,
523+
},
524+
)
525+
526+
464527
@router.patch("/{pdl_id}/link-production", response_model=APIResponse)
465528
async def link_production_pdl(
466529
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"}}),
@@ -701,6 +764,7 @@ async def admin_add_pdl(
701764
created_at=pdl.created_at,
702765
subscribed_power=pdl.subscribed_power,
703766
offpeak_hours=pdl.offpeak_hours,
767+
pricing_option=pdl.pricing_option,
704768
has_consumption=pdl.has_consumption,
705769
has_production=pdl.has_production,
706770
is_active=pdl.is_active,

apps/api/src/schemas/responses.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class PDLResponse(BaseModel):
5050
display_order: Optional[int] = None
5151
subscribed_power: Optional[int] = None
5252
offpeak_hours: Optional[Union[list[str], dict]] = None # Array format or legacy object format
53+
pricing_option: Optional[str] = None # BASE, HC_HP, TEMPO, EJP, HC_WEEKEND
5354
has_consumption: bool = True
5455
has_production: bool = False
5556
is_active: bool = True

apps/docs/sidebars.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ const sidebars: SidebarsConfig = {
196196
{
197197
type: "category",
198198
label: "Architecture",
199-
items: ["architecture/summary"],
199+
items: ["architecture/summary", "architecture/encryption"],
200200
},
201201
],
202202
};

apps/web/src/api/pdl.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { apiClient } from './client'
2-
import type { PDL, PDLCreate } from '@/types/api'
2+
import type { PDL, PDLCreate, PricingOption } from '@/types/api'
33

44
export const pdlApi = {
55
list: async () => {
@@ -34,6 +34,14 @@ export const pdlApi = {
3434
return apiClient.patch<PDL>(`pdl/${id}/active`, { is_active })
3535
},
3636

37+
updatePricingOption: async (id: string, pricing_option: PricingOption | null) => {
38+
return apiClient.patch<{
39+
id: string
40+
usage_point_id: string
41+
pricing_option: PricingOption | null
42+
}>(`pdl/${id}/pricing-option`, { pricing_option })
43+
},
44+
3745
linkProduction: async (consumptionPdlId: string, productionPdlId: string | null) => {
3846
return apiClient.patch<{
3947
id: string

apps/web/src/components/PDLCard.tsx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import React, { useState, useEffect, useRef } from 'react'
22
import { createPortal } from 'react-dom'
33
import { useMutation, useQueryClient } from '@tanstack/react-query'
4-
import { Info, Trash2, RefreshCw, Edit2, Save, X, Zap, Clock, Factory, Plus, Minus, Eye, EyeOff, Calendar, MoreVertical } from 'lucide-react'
4+
import { Info, Trash2, RefreshCw, Edit2, Save, X, Zap, Clock, Factory, Plus, Minus, Eye, EyeOff, Calendar, MoreVertical, Receipt } from 'lucide-react'
55
import { pdlApi } from '@/api/pdl'
66
import { oauthApi } from '@/api/oauth'
7-
import type { PDL } from '@/types/api'
7+
import type { PDL, PricingOption } from '@/types/api'
88

99
interface PDLCardProps {
1010
pdl: PDL
@@ -293,6 +293,18 @@ export default function PDLCard({ pdl, onViewDetails, onDelete, isDemo = false,
293293
},
294294
})
295295

296+
const updatePricingOptionMutation = useMutation({
297+
mutationFn: (pricing_option: PricingOption | null) => {
298+
if (isDemo) {
299+
return Promise.reject(new Error('Modifications désactivées en mode démo'))
300+
}
301+
return pdlApi.updatePricingOption(pdl.id, pricing_option)
302+
},
303+
onSuccess: () => {
304+
queryClient.invalidateQueries({ queryKey: ['pdls'] })
305+
},
306+
})
307+
296308
// Unused helper function - kept for potential future use
297309
// const saveContract = () => {
298310
// const data: { subscribed_power?: number; offpeak_hours?: string[] } = {}
@@ -815,6 +827,31 @@ export default function PDLCard({ pdl, onViewDetails, onDelete, isDemo = false,
815827
</select>
816828
</div>
817829

830+
{/* Pricing Option (Tariff Type) */}
831+
<div className="flex items-center justify-between" data-tour="pdl-pricing-option">
832+
<div className="flex items-center gap-2 text-gray-600 dark:text-gray-400">
833+
<Receipt size={16} />
834+
<span>Option tarifaire :</span>
835+
</div>
836+
<select
837+
value={pdl.pricing_option || ''}
838+
onChange={(e) => {
839+
const value = e.target.value as PricingOption | ''
840+
updatePricingOptionMutation.mutate(value === '' ? null : value as PricingOption)
841+
}}
842+
disabled={updatePricingOptionMutation.isPending}
843+
className="w-44 px-3 py-1.5 text-sm font-medium bg-white dark:bg-gray-800 border-2 border-blue-300 dark:border-blue-700 rounded-lg text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 cursor-pointer transition-all shadow-sm hover:shadow disabled:opacity-50 disabled:cursor-not-allowed"
844+
>
845+
<option value="">Sélectionner</option>
846+
<option value="BASE">Base</option>
847+
<option value="HC_HP">Heures Creuses</option>
848+
<option value="TEMPO">Tempo</option>
849+
<option value="WEEKEND">Nuit & Week-end</option>
850+
<option value="SEASONAL">Saisonnier</option>
851+
<option value="EJP">EJP (ancien)</option>
852+
</select>
853+
</div>
854+
818855
{/* Offpeak Hours */}
819856
<div className="space-y-3" data-tour="pdl-offpeak">
820857
<div className="flex items-center justify-between">
@@ -1095,6 +1132,11 @@ export default function PDLCard({ pdl, onViewDetails, onDelete, isDemo = false,
10951132
Erreur lors de la mise à jour du type de PDL
10961133
</div>
10971134
)}
1135+
{updatePricingOptionMutation.isError && (
1136+
<div className="mt-2 text-xs text-red-600 dark:text-red-400">
1137+
Erreur lors de la mise à jour de l'option tarifaire
1138+
</div>
1139+
)}
10981140

10991141
{/* Sync Warning Modal - using portal to escape transform context */}
11001142
{showSyncWarning && createPortal(

apps/web/src/types/api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ export interface TokenResponse {
6363
token_type: string
6464
}
6565

66+
// Pricing option types for electricity tariffs
67+
export type PricingOption = 'BASE' | 'HC_HP' | 'TEMPO' | 'EJP' | 'WEEKEND' | 'SEASONAL'
68+
6669
export interface PDL {
6770
id: string
6871
usage_point_id: string
@@ -71,6 +74,7 @@ export interface PDL {
7174
display_order?: number
7275
subscribed_power?: number
7376
offpeak_hours?: string[] | Record<string, string> // Array format or legacy object format
77+
pricing_option?: PricingOption // Tariff type: BASE, HC_HP, TEMPO, EJP, HC_WEEKEND
7478
has_consumption?: boolean
7579
has_production?: boolean
7680
is_active?: boolean

0 commit comments

Comments
 (0)