Compare commits

..

4 commits

25 changed files with 261 additions and 136 deletions

View file

@ -25,3 +25,4 @@ class ChargePoint(Base):
connectors = relationship("Connector", cascade="delete, delete-orphan") connectors = relationship("Connector", cascade="delete, delete-orphan")
transactions = relationship("Transaction", cascade="delete, delete-orphan") transactions = relationship("Transaction", cascade="delete, delete-orphan")
variables = relationship("ChargepointVariable", cascade="delete, delete-orphan") variables = relationship("ChargepointVariable", cascade="delete, delete-orphan")
firmware_updates = relationship("FirmwareUpdate", cascade="delete, delete-orphan")

View file

@ -1,5 +1,6 @@
import uuid import uuid
from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String, Text, Uuid from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String, Text, Uuid
from sqlalchemy.orm import relationship
from app.database import Base from app.database import Base
from app.schemas.firmware_update import FirmwareUpdateStatus from app.schemas.firmware_update import FirmwareUpdateStatus
@ -20,3 +21,4 @@ class FirmwareUpdate(Base):
signature = Column(String, nullable=True) signature = Column(String, nullable=True)
chargepoint_id = Column(Uuid, ForeignKey("chargepoints.id"), index=True) chargepoint_id = Column(Uuid, ForeignKey("chargepoints.id"), index=True)
chargepoint = relationship("ChargePoint", back_populates="firmware_updates")

View file

@ -1,5 +1,6 @@
import uuid import uuid
from sqlalchemy import Uuid, Column, DateTime, Enum, Float, ForeignKey, String from sqlalchemy import Uuid, Column, DateTime, Enum, Float, ForeignKey, String
from sqlalchemy.orm import relationship
from app.database import Base from app.database import Base
from app.schemas.meter_value import Measurand, PhaseType from app.schemas.meter_value import Measurand, PhaseType
@ -14,4 +15,5 @@ class MeterValue(Base):
unit = Column(String, nullable=True) unit = Column(String, nullable=True)
value = Column(Float) value = Column(Float)
transaction_id = Column(String, ForeignKey("transactions.id"), index=True) transaction_id = Column(String, ForeignKey("transactions.id"), index=True)
transaction = relationship("Transaction", back_populates="meter_values")

View file

@ -1,5 +1,6 @@
import uuid import uuid
from sqlalchemy import Column, DateTime, ForeignKey, String, Uuid from sqlalchemy import Column, DateTime, ForeignKey, String, Uuid
from sqlalchemy.orm import relationship
from app.database import Base from app.database import Base
@ -11,4 +12,5 @@ class Session(Base):
refresh_token = Column(String, nullable=False, unique=True, index=True) refresh_token = Column(String, nullable=False, unique=True, index=True)
last_used = Column(DateTime(timezone=True)) last_used = Column(DateTime(timezone=True))
user_id = Column(Uuid, ForeignKey("users.id"), nullable=False, index=True) user_id = Column(Uuid, ForeignKey("users.id"), nullable=False, index=True)
user = relationship("User", back_populates="sessions")

View file

@ -1,4 +1,5 @@
from sqlalchemy import String, Uuid, Column, DateTime, Enum, Numeric, ForeignKey from sqlalchemy import String, Uuid, Column, DateTime, Enum, Numeric, ForeignKey
from sqlalchemy.orm import relationship
from app.schemas.transaction import TransactionEventTriggerReason, TransactionStatus from app.schemas.transaction import TransactionEventTriggerReason, TransactionStatus
from app.database import Base from app.database import Base
@ -11,9 +12,14 @@ class Transaction(Base):
started_at = Column(DateTime, index=True) started_at = Column(DateTime, index=True)
ended_at = Column(DateTime, nullable=True, index=True) ended_at = Column(DateTime, nullable=True, index=True)
meter_start = Column(Numeric(10,2)) meter_start = Column(Numeric(10,2))
meter_end = Column(Numeric(10,2), nullable=True) meter_end = Column(Numeric(10,2))
end_reason = Column(Enum(TransactionEventTriggerReason), nullable=True) end_reason = Column(Enum(TransactionEventTriggerReason), nullable=True)
price = Column(Numeric(10,2)) price = Column(Numeric(10,2))
user_id = Column(Uuid, ForeignKey("users.id"), nullable=True, index=True) user_id = Column(Uuid, ForeignKey("users.id"), nullable=True, index=True)
user = relationship("User", back_populates="transactions")
chargepoint_id = Column(Uuid, ForeignKey("chargepoints.id"), index=True) chargepoint_id = Column(Uuid, ForeignKey("chargepoints.id"), index=True)
chargepoint = relationship("ChargePoint", back_populates="transactions")
meter_values = relationship("MeterValue", cascade="delete, delete-orphan")

View file

@ -17,3 +17,4 @@ class User(Base):
id_tokens = relationship("IdToken", back_populates="owner", cascade="delete, delete-orphan") id_tokens = relationship("IdToken", back_populates="owner", cascade="delete, delete-orphan")
transactions = relationship("Transaction", cascade="delete, delete-orphan") transactions = relationship("Transaction", cascade="delete, delete-orphan")
sessions = relationship("Session", cascade="delete, delete-orphan")

View file

@ -3,6 +3,7 @@ from datetime import datetime
from pydantic import BaseModel from pydantic import BaseModel
from app.schemas.user import Role from app.schemas.user import Role
from app.util.encoders import force_utc_datetime
@dataclass @dataclass
@ -19,3 +20,6 @@ class TokenResponse(BaseModel):
access_token: str access_token: str
refresh_token: str refresh_token: str
not_after: datetime not_after: datetime
class Config:
json_encoders = {datetime: force_utc_datetime}

View file

@ -8,7 +8,7 @@ from app.schemas.connector import Connector
from ocpp.v201.enums import ResetEnumType, ResetStatusEnumType from ocpp.v201.enums import ResetEnumType, ResetStatusEnumType
from app.util.encoders import decimal_encoder from app.util.encoders import decimal_encoder, force_utc_datetime
class ChargePointBase(BaseModel): class ChargePointBase(BaseModel):
identity: str identity: str
@ -32,6 +32,15 @@ class ChargePoint(ChargePointBase):
firmware_version: str | None firmware_version: str | None
connectors: list[Connector] = [] connectors: list[Connector] = []
class Config:
from_attributes = True
json_encoders = {Decimal: decimal_encoder, datetime: force_utc_datetime}
class ChargePointThumb(BaseModel):
id: UUID
identity: str
price: Decimal
class Config: class Config:
from_attributes = True from_attributes = True
json_encoders = {Decimal: decimal_encoder} json_encoders = {Decimal: decimal_encoder}

View file

@ -5,7 +5,7 @@ from typing import Optional
from uuid import UUID from uuid import UUID
from pydantic import BaseModel from pydantic import BaseModel
from app.util.encoders import decimal_encoder from app.util.encoders import decimal_encoder, force_utc_datetime
class PhaseType(enum.Enum): class PhaseType(enum.Enum):
L1 = "L1" L1 = "L1"
@ -57,4 +57,4 @@ class MeterValue(BaseModel):
class Config: class Config:
from_attributes = True from_attributes = True
json_encoders = {Decimal: decimal_encoder} json_encoders = {Decimal: decimal_encoder, datetime: force_utc_datetime}

View file

@ -2,6 +2,8 @@ from datetime import datetime
from uuid import UUID from uuid import UUID
from pydantic import BaseModel from pydantic import BaseModel
from app.util.encoders import force_utc_datetime
class Session(BaseModel): class Session(BaseModel):
id: UUID id: UUID
@ -9,4 +11,5 @@ class Session(BaseModel):
last_used: datetime last_used: datetime
class Config: class Config:
from_attributes = True from_attributes = True
json_encoders = {datetime: force_utc_datetime}

View file

@ -1,11 +1,12 @@
import enum
from datetime import datetime from datetime import datetime
from decimal import Decimal from decimal import Decimal
import enum
from typing import Optional from typing import Optional
from uuid import UUID
from pydantic import BaseModel from pydantic import BaseModel
from app.util.encoders import decimal_encoder from app.schemas.chargepoint import ChargePointThumb
from app.schemas.user import UserThumb
from app.util.encoders import decimal_encoder, force_utc_datetime
class TransactionStatus(enum.Enum): class TransactionStatus(enum.Enum):
ONGOING = "ongoing" ONGOING = "ongoing"
@ -44,15 +45,15 @@ class Transaction(BaseModel):
started_at: datetime started_at: datetime
ended_at: Optional[datetime] = None ended_at: Optional[datetime] = None
meter_start: Decimal meter_start: Decimal
meter_end: Optional[Decimal] = None meter_end: Decimal
end_reason: Optional[TransactionEventTriggerReason] = None end_reason: Optional[TransactionEventTriggerReason] = None
price: Decimal price: Decimal
user_id: Optional[UUID] = None user: UserThumb
chargepoint_id: UUID chargepoint: ChargePointThumb
class Config: class Config:
from_attributes = True from_attributes = True
json_encoders = {Decimal: decimal_encoder} json_encoders = {Decimal: decimal_encoder, datetime: force_utc_datetime}
class RemoteTransactionStartStopResponse(BaseModel): class RemoteTransactionStartStopResponse(BaseModel):
status: RemoteTransactionStartStopStatus status: RemoteTransactionStartStopStatus

View file

@ -30,6 +30,13 @@ class User(UserBase):
class Config: class Config:
from_attributes = True from_attributes = True
class UserThumb(BaseModel):
id: UUID
friendly_name: str
class Config:
from_attributes = True
class PasswordUpdate(BaseModel): class PasswordUpdate(BaseModel):
old_password: str = Field(max_length=100) old_password: str = Field(max_length=100)
new_password: str = Field(max_length=100) new_password: str = Field(max_length=100)

View file

@ -21,19 +21,23 @@ async def create_transaction(
with SessionLocal() as db: with SessionLocal() as db:
chargepoint = db.query(ChargePoint).filter(ChargePoint.identity == chargepoint_identity).first() chargepoint = db.query(ChargePoint).filter(ChargePoint.identity == chargepoint_identity).first()
meter_start=0 meter_start=0
meter_end=0
if "meter_value" in transaction_data.keys(): if "meter_value" in transaction_data.keys():
for meter_value_entry in transaction_data['meter_value']: for meter_value_entry in transaction_data['meter_value']:
for sampled_value in meter_value_entry['sampled_value']: for sampled_value in meter_value_entry['sampled_value']:
if "measurand" in sampled_value.keys(): if "measurand" in sampled_value.keys():
if sampled_value['measurand'] == str(Measurand.ENERGY_ACTIVE_IMPORT_REGISTER): if sampled_value['measurand'] == str(Measurand.ENERGY_ACTIVE_IMPORT_REGISTER):
meter_start = sampled_value['value'] meter_start = sampled_value['value']
meter_end = sampled_value['value']
else: else:
meter_start = sampled_value['value'] meter_start = sampled_value['value']
meter_end = sampled_value['value']
transaction = Transaction( transaction = Transaction(
id=transaction_info["transaction_id"], id=transaction_info["transaction_id"],
status=TransactionStatus.ONGOING, status=TransactionStatus.ONGOING,
started_at=timestamp, started_at=timestamp,
meter_start=meter_start, meter_start=meter_start,
meter_end=meter_end,
price=chargepoint.price, price=chargepoint.price,
chargepoint_id=chargepoint.id, chargepoint_id=chargepoint.id,
user_id=user_id user_id=user_id
@ -55,6 +59,12 @@ async def update_transaction(
transaction_id=transaction.id, transaction_id=transaction.id,
meter_value_data=meter_value_entry meter_value_data=meter_value_entry
) )
# Update current meter_end value
for sampled_value in meter_value_entry['sampled_value']:
if "measurand" in sampled_value.keys():
if sampled_value['measurand'] == str(Measurand.ENERGY_ACTIVE_IMPORT_REGISTER):
transaction.meter_end = sampled_value['value']
db.commit()
async def end_transaction( async def end_transaction(
transaction_id: str, transaction_id: str,

View file

@ -1,3 +1,4 @@
from datetime import datetime, timezone
from decimal import Decimal from decimal import Decimal
from typing import Union from typing import Union
@ -19,4 +20,8 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
if isinstance(exponent, int) and exponent >= 0: if isinstance(exponent, int) and exponent >= 0:
return int(dec_value) return int(dec_value)
else: else:
return float(dec_value) return float(dec_value)
def force_utc_datetime(datetime_value: datetime) -> datetime:
"""Force a datetime to be in the UTC timzone"""
return datetime_value.replace(tzinfo=timezone.utc)

View file

@ -25,18 +25,20 @@
<p class="text-2xl font-semibold"> <p class="text-2xl font-semibold">
{props.value}{#if props.unit}{' ' + props.unit}{/if} {props.value}{#if props.unit}{' ' + props.unit}{/if}
</p> </p>
{#if diff > 0} {#if !Number.isNaN(diff)}
<div class="badge badge-soft badge-success badge-sm gap-0.5 px-1 font-medium"> {#if diff > 0}
<i class="bi bi-arrow-up-right"></i>{diff}% <div class="badge badge-outline badge-success badge-sm gap-0.5 px-1 font-medium">
</div> <i class="bi bi-arrow-up-right"></i>{diff}%
{:else if diff < 0} </div>
<div class="badge badge-soft badge-error badge-sm gap-0.5 px-1 font-medium"> {:else if diff < 0}
<i class="bi bi-arrow-down-right"></i>{diff}% <div class="badge badge-outline badge-error badge-sm gap-0.5 px-1 font-medium">
</div> <i class="bi bi-arrow-down-right"></i>{diff}%
{:else} </div>
<div class="badge badge-soft badge-warning badge-sm gap-0.5 px-1 font-medium"> {:else}
<i class="bi bi-arrow-right"></i>{diff}% <div class="badge badge-outline badge-warning badge-sm gap-0.5 px-1 font-medium">
</div> <i class="bi bi-arrow-right"></i>{diff}%
</div>
{/if}
{/if} {/if}
</div> </div>
</div> </div>

View file

@ -1,18 +1,9 @@
<script lang="ts"> <script lang="ts">
import i18n from '$lib/i18n' import i18n from '$lib/i18n'
import type { Transaction } from '$lib/types/transaction'
let props: { let props: {
transactions: { transactions: Transaction[]
id: string
begin: Date
end: Date
chargepoint: {
name: string
id: string
}
energyAmmount: number
cost: number
}[]
} = $props() } = $props()
</script> </script>
@ -36,7 +27,7 @@
<div> <div>
<p> <p>
{$i18n.t('common:transactionTable.startTime', { {$i18n.t('common:transactionTable.startTime', {
time: transaction.begin, time: new Date(transaction.started_at),
formatParams: { formatParams: {
time: { time: {
year: 'numeric', year: 'numeric',
@ -51,7 +42,7 @@
</p> </p>
<p> <p>
{$i18n.t('common:transactionTable.endTime', { {$i18n.t('common:transactionTable.endTime', {
time: transaction.end, time: new Date(transaction.ended_at!),
formatParams: { formatParams: {
time: { time: {
year: 'numeric', year: 'numeric',
@ -70,11 +61,15 @@
<td> <td>
<a href="/chargepoint/{transaction.chargepoint.id}" class="btn btn-sm btn-primary"> <a href="/chargepoint/{transaction.chargepoint.id}" class="btn btn-sm btn-primary">
<i class="bi bi-plug-fill text-lg"></i> <i class="bi bi-plug-fill text-lg"></i>
{transaction.chargepoint.name} {transaction.chargepoint.identity}
</a> </a>
</td> </td>
<td class="font-bold">{transaction.energyAmmount} kWh</td> <td class="font-bold"
<td class="font-bold">{transaction.cost}</td> >{(transaction.meter_end - transaction.meter_start).toFixed(2)} kWh</td
>
<td class="font-bold"
>{((transaction.meter_end - transaction.meter_start) * transaction.price).toFixed(2)}</td
>
<th> <th>
<a href="/transaction/{transaction.id}" class="btn btn-sm btn-primary"> <a href="/transaction/{transaction.id}" class="btn btn-sm btn-primary">
{$i18n.t('common:transactionTable.detailButton')} {$i18n.t('common:transactionTable.detailButton')}

View file

@ -16,4 +16,10 @@ export type ChargePoint = {
status: 'Available' | 'Occupied' | 'Reserved' | 'Unavailable' | 'Faulted' status: 'Available' | 'Occupied' | 'Reserved' | 'Unavailable' | 'Faulted'
} }
] ]
}
export type ChargePointThumb = {
id: string;
identity: string;
price: number;
} }

View file

@ -0,0 +1,14 @@
import type { Transaction } from "./transaction";
export type Dashboard = {
stats: {
transaction_count: number;
transaction_count_previous: number;
transaction_energy_total: number;
transaction_energy_total_previous: number;
transaction_cost_total: number;
transaction_cost_total_previous: number;
};
current_transaction: Transaction | undefined;
recent_transactions: Transaction[];
}

View file

@ -0,0 +1,17 @@
import type { ChargePointThumb } from "./chargepoint";
import type { UserThumb } from "./user";
export type TransactionEventTriggerReason = 'Authorized' | 'CablePluggedIn' | 'ChargingRateChanged' | 'ChargingStateChanged' | 'Deauthorized' | 'EnergyLimitReached' | 'EVCommunicationLost' | 'EVConnectTimeout' | 'MeterValueClock' | 'MeterValuePeriodic' | 'TimeLimitReached' | 'Trigger' | 'UnlockCommand' | 'StopAuthorized' | 'EVDeparted' | 'EVDetected' | 'RemoteStop' | 'RemoteStart' | 'AbnormalCondition' | 'SignedDataReceived' | 'ResetCommand';
export type Transaction = {
id: string;
status: 'ongoing' | 'ended';
started_at: string;
ended_at: string | undefined;
meter_start: number;
meter_end: number;
end_reason: TransactionEventTriggerReason;
price: number;
user: UserThumb;
chargepoint: ChargePointThumb;
}

View file

@ -0,0 +1,4 @@
export type UserThumb = {
id: string;
friendly_name: string;
}

View file

@ -7,4 +7,23 @@ export const currentDaytime = function(): string {
return 'day' return 'day'
} }
return 'evening' return 'evening'
}
export function relativeTimeFromDate(date: Date): {val: number, range: Intl.RelativeTimeFormatUnit} | undefined {
const units: {unit: Intl.RelativeTimeFormatUnit; ms: number}[] = [
{unit: "year", ms: 31536000000},
{unit: "month", ms: 2628000000},
{unit: "day", ms: 86400000},
{unit: "hour", ms: 3600000},
{unit: "minute", ms: 60000},
{unit: "second", ms: 1000},
];
const elapsed = date.getTime() - new Date().getTime();
for (const {unit, ms} of units) {
if (Math.abs(elapsed) >= ms || unit === "second") {
return {val: Math.round(elapsed / ms), range: unit};
}
}
return undefined;
} }

View file

@ -4,10 +4,39 @@
import i18n from '$lib/i18n' import i18n from '$lib/i18n'
import DashboardCard from '$lib/component/DashboardCard.svelte' import DashboardCard from '$lib/component/DashboardCard.svelte'
import TransactionTable from '$lib/component/TransactionTable.svelte' import TransactionTable from '$lib/component/TransactionTable.svelte'
import type { Dashboard } from '$lib/types/dashboard'
import axios from '$lib/axios.svelte'
import { onMount } from 'svelte'
$i18n.loadNamespaces('dashboard') $i18n.loadNamespaces('dashboard')
let hasActiveTransaction = $state(true) let dashboardData: Dashboard = $state({
stats: {
transaction_count: 0,
transaction_count_previous: 0,
transaction_energy_total: 0,
transaction_energy_total_previous: 0,
transaction_cost_total: 0,
transaction_cost_total_previous: 0,
},
current_transaction: undefined,
recent_transactions: [],
})
function refreshDashboard() {
axios.get('/me/dashboard').then((res) => {
dashboardData = res.data
})
}
onMount(() => {
refreshDashboard()
const interval = setInterval(() => {
refreshDashboard()
}, 15000)
return () => clearInterval(interval)
})
</script> </script>
<div class="w-full h-full mt-10 flex flex-col"> <div class="w-full h-full mt-10 flex flex-col">
@ -23,18 +52,21 @@
{$i18n.t('dashboard:cards.currentTransaction')} {$i18n.t('dashboard:cards.currentTransaction')}
</p> </p>
<div class="mt-3 flex items-center gap-2"> <div class="mt-3 flex items-center gap-2">
{#if hasActiveTransaction} {#if dashboardData.current_transaction}
<div class="inline-grid *:[grid-area:1/1]"> <div class="inline-grid *:[grid-area:1/1]">
<div class="status status-success animate-ping"></div> <div class="status status-success animate-ping"></div>
<div class="status status-success"></div> <div class="status status-success"></div>
</div> </div>
{/if} {/if}
<p class="text-2xl font-semibold"> <p class="text-2xl font-semibold">
{#if hasActiveTransaction}0,25 kWh{:else}-{/if} {#if dashboardData.current_transaction}{dashboardData.current_transaction
.meter_end - dashboardData.current_transaction.meter_start} kWh{:else}-{/if}
</p> </p>
{#if hasActiveTransaction} {#if dashboardData.current_transaction}
<div class="badge badge-soft badge-success badge-sm gap-0.5 px-1 font-medium"> <div class="badge badge-soft badge-success badge-sm gap-0.5 px-1 font-medium">
2,30 € {(dashboardData.current_transaction.meter_end -
dashboardData.current_transaction.meter_start) *
dashboardData.current_transaction.price} €
</div> </div>
{/if} {/if}
</div> </div>
@ -43,13 +75,15 @@
</div> </div>
<div class="w-full flex flex-row items-center"> <div class="w-full flex flex-row items-center">
<p class="text-base-content/60 text-sm"> <p class="text-base-content/60 text-sm">
{#if hasActiveTransaction} {#if dashboardData.current_transaction}
{$i18n.t('dashboard:cards.chargepoint', { name: 'DE-EXMPL-0001' })} {$i18n.t('dashboard:cards.chargepoint', {
name: dashboardData.current_transaction.chargepoint.identity,
})}
{:else} {:else}
{$i18n.t('dashboard:cards.noCurrentTransaction')} {$i18n.t('dashboard:cards.noCurrentTransaction')}
{/if} {/if}
</p> </p>
{#if hasActiveTransaction} {#if dashboardData.current_transaction}
<button class="btn btn-xs btn-primary"> <button class="btn btn-xs btn-primary">
{$i18n.t('dashboard:cards.toCurrentTransactionButton')} {$i18n.t('dashboard:cards.toCurrentTransactionButton')}
<i class="bi bi-arrow-right"></i> <i class="bi bi-arrow-right"></i>
@ -61,61 +95,26 @@
<DashboardCard <DashboardCard
title={$i18n.t('dashboard:cards.transactionCount')} title={$i18n.t('dashboard:cards.transactionCount')}
icon={'bi-battery-charging'} icon={'bi-battery-charging'}
value={3} value={dashboardData.stats.transaction_count}
previousValue={1} previousValue={dashboardData.stats.transaction_count_previous}
/> />
<DashboardCard <DashboardCard
title={$i18n.t('dashboard:cards.transactionEnergyTotal')} title={$i18n.t('dashboard:cards.transactionEnergyTotal')}
icon={'bi-lightning-charge-fill'} icon={'bi-lightning-charge-fill'}
value={180} value={+dashboardData.stats.transaction_energy_total.toFixed(2)}
previousValue={50} previousValue={+dashboardData.stats.transaction_energy_total_previous.toFixed(2)}
unit={'kWh'} unit={'kWh'}
/> />
<DashboardCard <DashboardCard
title={$i18n.t('dashboard:cards.transactionCostTotal')} title={$i18n.t('dashboard:cards.transactionCostTotal')}
icon={'bi-currency-exchange'} icon={'bi-currency-exchange'}
value={30.56} value={+dashboardData.stats.transaction_cost_total.toFixed(2)}
previousValue={30.56} previousValue={+dashboardData.stats.transaction_cost_total_previous.toFixed(2)}
unit={'€'} unit={'€'}
/> />
</div> </div>
<div class="w-full flex flex-col mt-10"> <div class="w-full flex flex-col mt-10">
<p class="text-xl font-bold">{$i18n.t('dashboard:table.title')}</p> <p class="text-xl font-bold">{$i18n.t('dashboard:table.title')}</p>
<TransactionTable <TransactionTable transactions={dashboardData.recent_transactions} />
transactions={[
{
id: '1',
begin: new Date(Date.UTC(2025, 3, 15, 14, 12, 23)),
end: new Date(Date.UTC(2025, 3, 15, 16, 18, 46)),
chargepoint: { name: 'DE-EXMPL-0001', id: '1' },
energyAmmount: 58.65,
cost: 36.45,
},
{
id: '2',
begin: new Date(Date.UTC(2025, 3, 15, 14, 12, 23)),
end: new Date(Date.UTC(2025, 3, 15, 16, 18, 46)),
chargepoint: { name: 'DE-EXMPL-0001', id: '1' },
energyAmmount: 58.65,
cost: 36.45,
},
{
id: '3',
begin: new Date(Date.UTC(2025, 3, 15, 14, 12, 23)),
end: new Date(Date.UTC(2025, 3, 15, 16, 18, 46)),
chargepoint: { name: 'DE-EXMPL-0001', id: '1' },
energyAmmount: 58.65,
cost: 36.45,
},
{
id: '4',
begin: new Date(Date.UTC(2025, 3, 15, 14, 12, 23)),
end: new Date(Date.UTC(2025, 3, 15, 16, 18, 46)),
chargepoint: { name: 'DE-EXMPL-0001', id: '1' },
energyAmmount: 58.65,
cost: 36.45,
},
]}
/>
</div> </div>
</div> </div>

View file

@ -1,13 +1,42 @@
<script lang="ts"> <script lang="ts">
import axios from '$lib/axios.svelte'
import i18n from '$lib/i18n' import i18n from '$lib/i18n'
import type { ChargePoint } from '$lib/types/chargepoint' import type { ChargePoint } from '$lib/types/chargepoint'
import { relativeTimeFromDate } from '$lib/util'
import { onMount } from 'svelte'
$i18n.loadNamespaces('chargepoint') $i18n.loadNamespaces('chargepoint')
let chargepoints: ChargePoint[] = $state([]) let chargepoints: ChargePoint[] = $state([])
const pageSize = 25
let page: number = $state(1) let page: number = $state(1)
const urlParams = new URLSearchParams(window.location.search)
if (urlParams.has('page')) {
page = Number.parseInt(urlParams.get('page')!)
}
function load() {
axios
.get('/chargepoints', { params: { skip: (page - 1) * pageSize, limit: pageSize } })
.then((res) => {
chargepoints = res.data
})
}
function nextPage() {
++page
load()
}
function previousPage() {
--page
load()
}
onMount(() => {
load()
})
</script> </script>
<div class="w-full h-full mt-4 flex flex-col"> <div class="w-full h-full mt-4 flex flex-col">
@ -30,49 +59,22 @@
{#each chargepoints as chargepoint} {#each chargepoints as chargepoint}
<tr> <tr>
<td> <td>
<div class="flex items-center gap-3"> {chargepoint.identity}
<div>
<p>
{$i18n.t('common:transactionTable.startTime', {
time: transaction.begin,
formatParams: {
time: {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
},
},
})}
</p>
<p>
{$i18n.t('common:transactionTable.endTime', {
time: transaction.end,
formatParams: {
time: {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
},
},
})}
</p>
</div>
</div>
</td> </td>
<td> <td>
<a href="/chargepoint/{transaction.chargepoint.id}" class="btn btn-sm btn-primary"> {#if chargepoint.is_active}
<i class="bi bi-plug-fill text-lg"></i> {$i18n.t('chargepoint:state.enabled')}
{transaction.chargepoint.name} {:else}
</a> {$i18n.t('chargepoint:state.disabled')}
{/if}
</td> </td>
<td class="font-bold">{transaction.energyAmmount} kWh</td> <td>
<td class="font-bold">{transaction.cost}</td> {$i18n.t(
'chargepoint:tableFormatting.lastSeen',
relativeTimeFromDate(new Date(chargepoint.last_seen))!
)}
</td>
<td>{chargepoint.price}</td>
<th> <th>
<a href="/chargepoint/{chargepoint.id}" class="btn btn-sm btn-primary"> <a href="/chargepoint/{chargepoint.id}" class="btn btn-sm btn-primary">
{$i18n.t('common:transactionTable.detailButton')} {$i18n.t('common:transactionTable.detailButton')}
@ -91,8 +93,14 @@
{/if} {/if}
</div> </div>
<div class="join mt-4"> <div class="join mt-4">
<button class="join-item btn bg-base-100">«</button> <button onclick={previousPage} disabled={page === 1 || null} class="join-item btn bg-base-100"
</button
>
<button class="join-item btn bg-base-100">Page {page}</button> <button class="join-item btn bg-base-100">Page {page}</button>
<button class="join-item btn bg-base-100">»</button> <button
onclick={nextPage}
disabled={chargepoints.length < pageSize || null}
class="join-item btn bg-base-100">»</button
>
</div> </div>
</div> </div>

View file

@ -9,5 +9,9 @@
"tableFormatting": { "tableFormatting": {
"lastSeen": "{{val, relativetime}}" "lastSeen": "{{val, relativetime}}"
}, },
"state": {
"enabled": "Aktiv",
"disabled": "Deaktiviert"
},
"noChargepoints": "Noch keine Ladepunkte vorhanden." "noChargepoints": "Noch keine Ladepunkte vorhanden."
} }

View file

@ -9,5 +9,9 @@
"tableFormatting": { "tableFormatting": {
"lastSeen": "{{val, relativetime}}" "lastSeen": "{{val, relativetime}}"
}, },
"state": {
"enabled": "Active",
"disabled": "Disabled"
},
"noChargepoints": "There are no chargepoints yet." "noChargepoints": "There are no chargepoints yet."
} }