fix: Fetch charging station data server-side and pass to client component
This commit is contained in:
@@ -108,6 +108,7 @@ services:
|
||||
environment:
|
||||
NEXTAUTH_SECRET: Digitribe972
|
||||
ADMIN_PASSWORD: Digitribe972
|
||||
HASURA_ADMIN_SECRET: Digitribe972
|
||||
depends_on:
|
||||
- hasura
|
||||
labels:
|
||||
|
||||
@@ -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: () => (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<div style={{ background: '#f0f0f0', height: '100px', marginBottom: '10px', borderRadius: '4px' }} />
|
||||
<div style={{ background: '#f0f0f0', height: '200px', borderRadius: '4px' }} />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
interface ClientStationDetailPageProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default function ClientStationDetailPage({ id }: ClientStationDetailPageProps) {
|
||||
return <StationDetailPage id={id} />;
|
||||
}
|
||||
@@ -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<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<div style={{ background: '#f0f0f0', height: '100px', marginBottom: '10px', borderRadius: '4px' }} />
|
||||
<div style={{ background: '#f0f0f0', height: '200px', borderRadius: '4px' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ padding: '20px', color: 'red' }}>
|
||||
<h2>Error loading station</h2>
|
||||
<p>{error}</p>
|
||||
<p>Station ID: {id}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!station) {
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h2>No Data Found</h2>
|
||||
<p>No charging station found with id {id}.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h1>Charging Station: {station.id}</h1>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '10px', marginTop: '20px' }}>
|
||||
<div><strong>ID:</strong> {station.id}</div>
|
||||
<div><strong>Online:</strong> {station.isOnline ? '✅ Yes' : '❌ No'}</div>
|
||||
<div><strong>OCPP Connection:</strong> {station.ocppConnectionName || 'N/A'}</div>
|
||||
<div><strong>Vendor:</strong> {station.chargePointVendor || 'N/A'}</div>
|
||||
<div><strong>Model:</strong> {station.chargePointModel || 'N/A'}</div>
|
||||
<div><strong>Firmware:</strong> {station.firmwareVersion || 'N/A'}</div>
|
||||
<div><strong>Protocol:</strong> {station.protocol || 'N/A'}</div>
|
||||
<div><strong>Location ID:</strong> {station.locationId || 'N/A'}</div>
|
||||
<div><strong>Floor Level:</strong> {station.floorLevel || 'N/A'}</div>
|
||||
<div><strong>Created:</strong> {station.createdAt || 'N/A'}</div>
|
||||
<div><strong>Updated:</strong> {station.updatedAt || 'N/A'}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 <ChargingStationDetail params={{ id: String(id) }} />;
|
||||
const station = await fetchStation(id);
|
||||
return <ChargingStationDetail params={{ id, station }} />;
|
||||
}
|
||||
|
||||
@@ -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<any>(serverStation || null);
|
||||
const [isLoading, setIsLoading] = useState(!serverStation);
|
||||
|
||||
const {
|
||||
query: { data, isLoading },
|
||||
} = useOne<ChargingStationDetailsDto>({
|
||||
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 },
|
||||
|
||||
@@ -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<ChargingStationDetailProps> = ({ params }) => {
|
||||
const { id } = params;
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(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<ChargingStationDetailProps> = ({ pa
|
||||
}
|
||||
|
||||
return (
|
||||
<CanAccess
|
||||
resource={ResourceType.CHARGING_STATIONS}
|
||||
action={ActionType.SHOW}
|
||||
params={{ id }}
|
||||
fallback={
|
||||
<div className={`${pageMargin} ${pageFlex}`}>
|
||||
<AccessDeniedFallbackCard />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={`${pageMargin} ${pageFlex}`}>
|
||||
<ChargingStationDetailCard id={id} imageUrl={imageUrl} />
|
||||
<ChargingStationDetailTabsCard id={id} />
|
||||
</div>
|
||||
</CanAccess>
|
||||
<div className={`${pageMargin} {pageFlex}`}>
|
||||
<ChargingStationDetailCard id={id} imageUrl={null} serverStation={station} />
|
||||
<ChargingStationDetailTabsCard id={id} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user