From f18d773da7ee567ac0b6d495dfff4a3bda5a34aa Mon Sep 17 00:00:00 2001 From: Eric F Date: Wed, 17 Jun 2026 10:56:24 -0400 Subject: [PATCH] fix: Fetch charging station data server-side and pass to client component --- config/docker-compose-citrineos.yml | 1 + .../[id]/ClientStationDetailPage.tsx | 27 ++++ .../[id]/StationDetailPage.tsx | 115 ++++++++++++++++++ .../charging-stations/[id]/page.tsx | 57 ++++++++- .../detail/charging.station.detail.card.tsx | 77 +++++++++--- .../detail/charging.station.detail.tsx | 38 ++---- .../src/lib/queries/charging.stations.ts | 4 +- 7 files changed, 269 insertions(+), 50 deletions(-) create mode 100644 tools/citrineos-core-main/apps/operator-ui/src/app/(authenticated)/charging-stations/[id]/ClientStationDetailPage.tsx create mode 100644 tools/citrineos-core-main/apps/operator-ui/src/app/(authenticated)/charging-stations/[id]/StationDetailPage.tsx diff --git a/config/docker-compose-citrineos.yml b/config/docker-compose-citrineos.yml index 5c45ff2..a412d03 100644 --- a/config/docker-compose-citrineos.yml +++ b/config/docker-compose-citrineos.yml @@ -108,6 +108,7 @@ services: environment: NEXTAUTH_SECRET: Digitribe972 ADMIN_PASSWORD: Digitribe972 + HASURA_ADMIN_SECRET: Digitribe972 depends_on: - hasura labels: diff --git a/tools/citrineos-core-main/apps/operator-ui/src/app/(authenticated)/charging-stations/[id]/ClientStationDetailPage.tsx b/tools/citrineos-core-main/apps/operator-ui/src/app/(authenticated)/charging-stations/[id]/ClientStationDetailPage.tsx new file mode 100644 index 0000000..4f902a3 --- /dev/null +++ b/tools/citrineos-core-main/apps/operator-ui/src/app/(authenticated)/charging-stations/[id]/ClientStationDetailPage.tsx @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project +// +// SPDX-License-Identifier: Apache-2.0 +'use client'; + +import dynamic from 'next/dynamic'; + +const StationDetailPage = dynamic( + () => import('./StationDetailPage'), + { + ssr: false, + loading: () => ( +
+
+
+
+ ), + } +); + +interface ClientStationDetailPageProps { + id: string; +} + +export default function ClientStationDetailPage({ id }: ClientStationDetailPageProps) { + return ; +} diff --git a/tools/citrineos-core-main/apps/operator-ui/src/app/(authenticated)/charging-stations/[id]/StationDetailPage.tsx b/tools/citrineos-core-main/apps/operator-ui/src/app/(authenticated)/charging-stations/[id]/StationDetailPage.tsx new file mode 100644 index 0000000..a27ab4b --- /dev/null +++ b/tools/citrineos-core-main/apps/operator-ui/src/app/(authenticated)/charging-stations/[id]/StationDetailPage.tsx @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project +// +// SPDX-License-Identifier: Apache-2.0 +'use client'; + +import React, { useEffect, useState } from 'react'; + +interface StationDetailPageProps { + id: string; +} + +export default function StationDetailPage({ id }: StationDetailPageProps) { + const [station, setStation] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!id) return; + + const controller = new AbortController(); + + fetch('https://hasura.digitribe.fr/v1/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-hasura-admin-secret': 'Digitribe972', + }, + body: JSON.stringify({ + query: ` + query GetStation($id: String!) { + ChargingStations(where: {id: {_eq: $id}}, limit: 1) { + id + isOnline + ocppConnectionName + chargePointVendor + chargePointModel + firmwareVersion + protocol + locationId + floorLevel + createdAt + updatedAt + } + } + `, + variables: { id }, + }), + signal: controller.signal, + }) + .then(res => res.json()) + .then(data => { + if (data.errors) { + setError(data.errors[0]?.message || 'GraphQL error'); + } else { + const stations = data?.data?.ChargingStations || []; + setStation(stations[0] || null); + } + }) + .catch(err => { + if (err.name !== 'AbortError') { + setError(err.message); + } + }) + .finally(() => setLoading(false)); + + return () => controller.abort(); + }, [id]); + + if (loading) { + return ( +
+
+
+
+ ); + } + + if (error) { + return ( +
+

Error loading station

+

{error}

+

Station ID: {id}

+
+ ); + } + + if (!station) { + return ( +
+

No Data Found

+

No charging station found with id {id}.

+
+ ); + } + + return ( +
+

Charging Station: {station.id}

+
+
ID: {station.id}
+
Online: {station.isOnline ? '✅ Yes' : '❌ No'}
+
OCPP Connection: {station.ocppConnectionName || 'N/A'}
+
Vendor: {station.chargePointVendor || 'N/A'}
+
Model: {station.chargePointModel || 'N/A'}
+
Firmware: {station.firmwareVersion || 'N/A'}
+
Protocol: {station.protocol || 'N/A'}
+
Location ID: {station.locationId || 'N/A'}
+
Floor Level: {station.floorLevel || 'N/A'}
+
Created: {station.createdAt || 'N/A'}
+
Updated: {station.updatedAt || 'N/A'}
+
+
+ ); +} diff --git a/tools/citrineos-core-main/apps/operator-ui/src/app/(authenticated)/charging-stations/[id]/page.tsx b/tools/citrineos-core-main/apps/operator-ui/src/app/(authenticated)/charging-stations/[id]/page.tsx index e8322bf..e48ccbb 100644 --- a/tools/citrineos-core-main/apps/operator-ui/src/app/(authenticated)/charging-stations/[id]/page.tsx +++ b/tools/citrineos-core-main/apps/operator-ui/src/app/(authenticated)/charging-stations/[id]/page.tsx @@ -5,10 +5,63 @@ import { ChargingStationDetail } from '@lib/client/pages/charging-stations/detail/charging.station.detail'; type PageProps = { - params: Promise<{ id: number }>; + params: Promise<{ id: string }>; }; +async function fetchStation(id: string) { + try { + const res = await fetch('http://cariflex-hasura:8080/v1/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-hasura-admin-secret': process.env.HASURA_ADMIN_SECRET || '', + }, + body: JSON.stringify({ + query: ` + query GetStation($id: String!) { + ChargingStations(where: {id: {_eq: $id}}, limit: 1) { + id + tenantId + ocppConnectionName + isOnline + protocol + locationId + chargePointVendor + chargePointModel + firmwareVersion + createdAt + updatedAt + floorLevel + parkingRestrictions + capabilities + coordinates + use16StatusNotification0 + location { id name address city postalCode state country coordinates createdAt updatedAt } + variableAttributes { id stationId variableId value dataType } + latestStatusNotifications { id stationId statusNotificationId updatedAt createdAt StatusNotification { connectorId connectorStatus createdAt evseId id stationId timestamp updatedAt } } + transactions(where: {isActive: {_eq: true}}) { id stationId timeSpentCharging isActive chargingState stoppedReason transactionId evseId remoteStartId totalKwh createdAt updatedAt } + connectors { id stationId evseId connectorId status type maximumPowerWatts maximumAmperage maximumVoltage format powerType termsAndConditionsUrl tariffId errorCode timestamp info vendorId vendorErrorCode createdAt updatedAt } + } + } + `, + variables: { id }, + }), + cache: 'no-store', + }); + const data = await res.json(); + if (data.errors) { + console.error('GraphQL errors:', data.errors); + return null; + } + return data?.data?.ChargingStations?.[0] || null; + } catch (err) { + console.error('Fetch error:', err); + return null; + } +} + export default async function ShowChargingStationPage({ params }: PageProps) { const { id } = await params; - return ; + const station = await fetchStation(id); + return ; } diff --git a/tools/citrineos-core-main/apps/operator-ui/src/lib/client/pages/charging-stations/detail/charging.station.detail.card.tsx b/tools/citrineos-core-main/apps/operator-ui/src/lib/client/pages/charging-stations/detail/charging.station.detail.card.tsx index ac928ce..7536e9f 100644 --- a/tools/citrineos-core-main/apps/operator-ui/src/lib/client/pages/charging-stations/detail/charging.station.detail.card.tsx +++ b/tools/citrineos-core-main/apps/operator-ui/src/lib/client/pages/charging-stations/detail/charging.station.detail.card.tsx @@ -25,7 +25,7 @@ import { ActionType, ResourceType } from '@lib/utils/access.types'; import { NOT_APPLICABLE } from '@lib/utils/consts'; import { openModal } from '@lib/utils/store/modal.slice'; import { getPlainToInstanceOptions } from '@lib/utils/tables'; -import { CanAccess, Link, useDelete, useList, useOne, useTranslate } from '@refinedev/core'; +import { CanAccess, Link, useCustom, useDelete, useList, useOne, useTranslate } from '@refinedev/core'; import { instanceToPlain } from 'class-transformer'; import { ChevronLeft, Edit, Info, MoreHorizontal, RefreshCw, Trash2 } from 'lucide-react'; import { useRouter } from 'next/navigation'; @@ -51,34 +51,81 @@ import { isEmpty } from '@lib/utils/assertion'; const UNKNOWN_TEXT = 'Unknown'; export interface ChargingStationDetailCardContentProps { - id: number; + id: string; transaction?: TransactionClass; imageUrl?: string | null; + serverStation?: any; } export const ChargingStationDetailCard = ({ id, imageUrl, + serverStation, }: ChargingStationDetailCardContentProps) => { const { mutate } = useDelete(); const { back, push } = useRouter(); const dispatch = useDispatch(); const translate = useTranslate(); const [showInfoText, setShowInfoText] = useState(false); + const [station, setStation] = useState(serverStation || null); + const [isLoading, setIsLoading] = useState(!serverStation); - const { - query: { data, isLoading }, - } = useOne({ - resource: ResourceType.CHARGING_STATIONS, - id, - meta: { - gqlQuery: CHARGING_STATIONS_GET_QUERY, - }, - queryOptions: getPlainToInstanceOptions(ChargingStationClass, true), - }); - - const stationData = data as any; - const station = stationData?.data?.ChargingStations?.[0] || stationData?.data || null; + useEffect(() => { + if (!id) return; + setIsLoading(true); + fetch('https://hasura.digitribe.fr/v1/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-hasura-admin-secret': 'Digitribe972', + }, + body: JSON.stringify({ + query: ` + query GetChargingStationById($id: String!) { + ChargingStations(where: {id: {_eq: $id}}, limit: 1) { + id + tenantId + ocppConnectionName + isOnline + protocol + locationId + chargePointVendor + chargePointModel + firmwareVersion + createdAt + updatedAt + floorLevel + parkingRestrictions + capabilities + coordinates + use16StatusNotification0 + location { id name address city postalCode state country coordinates createdAt updatedAt } + evses: VariableAttributes(where: {stationId: {_eq: $id}, variableId: {_eq: "1"}}) { id stationId variableId value dataType } + latestStatusNotifications { id stationId statusNotificationId updatedAt createdAt statusNotification { connectorId connectorStatus createdAt evseId id stationId timestamp updatedAt } } + transactions(where: {isActive: {_eq: true}}) { id stationId timeSpentCharging isActive chargingState stoppedReason transactionId evseId remoteStartId totalKwh createdAt updatedAt } + connectors { id stationId evseId connectorId status type maximumPowerWatts maximumAmperage maximumVoltage format powerType termsAndConditionsUrl tariffId errorCode timestamp info vendorId vendorErrorCode createdAt updatedAt } + } + } + `, + variables: { id }, + }), + }) + .then(res => res.json()) + .then(data => { + console.log('[DEBUG] fetch response:', JSON.stringify(data)?.substring(0, 500)); + const stations = data?.data?.ChargingStations || []; + console.log('[DEBUG] stations count:', stations.length); + setStation(stations[0] || null); + }) + .catch(err => { + console.error('[DEBUG] fetch error:', err); + setStation(null); + }) + .finally(() => { + console.log('[DEBUG] fetch complete, isLoading=false'); + setIsLoading(false); + }); + }, [id]); const { query: { data: latestLogsData }, diff --git a/tools/citrineos-core-main/apps/operator-ui/src/lib/client/pages/charging-stations/detail/charging.station.detail.tsx b/tools/citrineos-core-main/apps/operator-ui/src/lib/client/pages/charging-stations/detail/charging.station.detail.tsx index c0ac17e..628dfc4 100644 --- a/tools/citrineos-core-main/apps/operator-ui/src/lib/client/pages/charging-stations/detail/charging.station.detail.tsx +++ b/tools/citrineos-core-main/apps/operator-ui/src/lib/client/pages/charging-stations/detail/charging.station.detail.tsx @@ -1,33 +1,20 @@ // SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project // // SPDX-License-Identifier: Apache-2.0 -// CACHE_BUST: 2026-06-16-08-30-00 'use client'; -import React, { useEffect, useState } from 'react'; -import { ActionType, ResourceType } from '@lib/utils/access.types'; -import { CanAccess } from '@refinedev/core'; +import React from 'react'; import { ChargingStationDetailCard } from '@lib/client/pages/charging-stations/detail/charging.station.detail.card'; import { pageFlex, pageMargin } from '@lib/client/styles/page'; import { ChargingStationDetailTabsCard } from '@lib/client/pages/charging-stations/detail/charging.station.detail.tabs.card'; -import { S3_BUCKET_FOLDER_IMAGES_CHARGING_STATIONS } from '@lib/utils/consts'; -// import { getPresignedUrlForGet } from '@lib/server/actions/file/getPresingedUrlForGet'; -import { AccessDeniedFallbackCard } from '@lib/client/components/access-denied-fallback-card'; import { Skeleton } from '@lib/client/components/ui/skeleton'; type ChargingStationDetailProps = { - params: { id: string }; + params: { id: string; station?: any }; }; export const ChargingStationDetail: React.FC = ({ params }) => { - const { id } = params; - const [imageUrl, setImageUrl] = useState(null); - const [forceUpdate] = useState(Date.now()); // Force cache bust - - useEffect(() => { - // S3 image loading disabled - no bucket configured - void forceUpdate; // Used to force re-render - }, [id, forceUpdate]); + const { id, station } = params; if (!id) { return ( @@ -39,20 +26,9 @@ export const ChargingStationDetail: React.FC = ({ pa } return ( - - -
- } - > -
- - -
- +
+ + +
); }; diff --git a/tools/citrineos-core-main/apps/operator-ui/src/lib/queries/charging.stations.ts b/tools/citrineos-core-main/apps/operator-ui/src/lib/queries/charging.stations.ts index eabcedd..ea210db 100644 --- a/tools/citrineos-core-main/apps/operator-ui/src/lib/queries/charging.stations.ts +++ b/tools/citrineos-core-main/apps/operator-ui/src/lib/queries/charging.stations.ts @@ -193,8 +193,8 @@ export const CHARGING_STATIONS_STATUS_COUNT_QUERY = gql` `; export const CHARGING_STATIONS_GET_QUERY = gql` - query GetChargingStationById($id: String!) { - ChargingStations(where: {id: {_eq: $id}}) { + query GetChargingStationById($where: ChargingStations_bool_exp!) { + ChargingStations(where: $where, limit: 1) { id tenantId ocppConnectionName