fix: Fetch charging station data server-side and pass to client component
This commit is contained in:
@@ -108,6 +108,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
NEXTAUTH_SECRET: Digitribe972
|
NEXTAUTH_SECRET: Digitribe972
|
||||||
ADMIN_PASSWORD: Digitribe972
|
ADMIN_PASSWORD: Digitribe972
|
||||||
|
HASURA_ADMIN_SECRET: Digitribe972
|
||||||
depends_on:
|
depends_on:
|
||||||
- hasura
|
- hasura
|
||||||
labels:
|
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';
|
import { ChargingStationDetail } from '@lib/client/pages/charging-stations/detail/charging.station.detail';
|
||||||
|
|
||||||
type PageProps = {
|
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) {
|
export default async function ShowChargingStationPage({ params }: PageProps) {
|
||||||
const { id } = await params;
|
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 { NOT_APPLICABLE } from '@lib/utils/consts';
|
||||||
import { openModal } from '@lib/utils/store/modal.slice';
|
import { openModal } from '@lib/utils/store/modal.slice';
|
||||||
import { getPlainToInstanceOptions } from '@lib/utils/tables';
|
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 { instanceToPlain } from 'class-transformer';
|
||||||
import { ChevronLeft, Edit, Info, MoreHorizontal, RefreshCw, Trash2 } from 'lucide-react';
|
import { ChevronLeft, Edit, Info, MoreHorizontal, RefreshCw, Trash2 } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -51,34 +51,81 @@ import { isEmpty } from '@lib/utils/assertion';
|
|||||||
const UNKNOWN_TEXT = 'Unknown';
|
const UNKNOWN_TEXT = 'Unknown';
|
||||||
|
|
||||||
export interface ChargingStationDetailCardContentProps {
|
export interface ChargingStationDetailCardContentProps {
|
||||||
id: number;
|
id: string;
|
||||||
transaction?: TransactionClass;
|
transaction?: TransactionClass;
|
||||||
imageUrl?: string | null;
|
imageUrl?: string | null;
|
||||||
|
serverStation?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChargingStationDetailCard = ({
|
export const ChargingStationDetailCard = ({
|
||||||
id,
|
id,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
|
serverStation,
|
||||||
}: ChargingStationDetailCardContentProps) => {
|
}: ChargingStationDetailCardContentProps) => {
|
||||||
const { mutate } = useDelete();
|
const { mutate } = useDelete();
|
||||||
const { back, push } = useRouter();
|
const { back, push } = useRouter();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const [showInfoText, setShowInfoText] = useState(false);
|
const [showInfoText, setShowInfoText] = useState(false);
|
||||||
|
const [station, setStation] = useState<any>(serverStation || null);
|
||||||
|
const [isLoading, setIsLoading] = useState(!serverStation);
|
||||||
|
|
||||||
const {
|
useEffect(() => {
|
||||||
query: { data, isLoading },
|
if (!id) return;
|
||||||
} = useOne<ChargingStationDetailsDto>({
|
setIsLoading(true);
|
||||||
resource: ResourceType.CHARGING_STATIONS,
|
fetch('https://hasura.digitribe.fr/v1/graphql', {
|
||||||
id,
|
method: 'POST',
|
||||||
meta: {
|
headers: {
|
||||||
gqlQuery: CHARGING_STATIONS_GET_QUERY,
|
'Content-Type': 'application/json',
|
||||||
|
'x-hasura-admin-secret': 'Digitribe972',
|
||||||
},
|
},
|
||||||
queryOptions: getPlainToInstanceOptions(ChargingStationClass, true),
|
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 stationData = data as any;
|
|
||||||
const station = stationData?.data?.ChargingStations?.[0] || stationData?.data || null;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
query: { data: latestLogsData },
|
query: { data: latestLogsData },
|
||||||
|
|||||||
@@ -1,33 +1,20 @@
|
|||||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
// CACHE_BUST: 2026-06-16-08-30-00
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import { ActionType, ResourceType } from '@lib/utils/access.types';
|
|
||||||
import { CanAccess } from '@refinedev/core';
|
|
||||||
import { ChargingStationDetailCard } from '@lib/client/pages/charging-stations/detail/charging.station.detail.card';
|
import { ChargingStationDetailCard } from '@lib/client/pages/charging-stations/detail/charging.station.detail.card';
|
||||||
import { pageFlex, pageMargin } from '@lib/client/styles/page';
|
import { pageFlex, pageMargin } from '@lib/client/styles/page';
|
||||||
import { ChargingStationDetailTabsCard } from '@lib/client/pages/charging-stations/detail/charging.station.detail.tabs.card';
|
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';
|
import { Skeleton } from '@lib/client/components/ui/skeleton';
|
||||||
|
|
||||||
type ChargingStationDetailProps = {
|
type ChargingStationDetailProps = {
|
||||||
params: { id: string };
|
params: { id: string; station?: any };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChargingStationDetail: React.FC<ChargingStationDetailProps> = ({ params }) => {
|
export const ChargingStationDetail: React.FC<ChargingStationDetailProps> = ({ params }) => {
|
||||||
const { id } = params;
|
const { id, station } = 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]);
|
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return (
|
return (
|
||||||
@@ -39,20 +26,9 @@ export const ChargingStationDetail: React.FC<ChargingStationDetailProps> = ({ pa
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CanAccess
|
<div className={`${pageMargin} {pageFlex}`}>
|
||||||
resource={ResourceType.CHARGING_STATIONS}
|
<ChargingStationDetailCard id={id} imageUrl={null} serverStation={station} />
|
||||||
action={ActionType.SHOW}
|
|
||||||
params={{ id }}
|
|
||||||
fallback={
|
|
||||||
<div className={`${pageMargin} ${pageFlex}`}>
|
|
||||||
<AccessDeniedFallbackCard />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className={`${pageMargin} ${pageFlex}`}>
|
|
||||||
<ChargingStationDetailCard id={id} imageUrl={imageUrl} />
|
|
||||||
<ChargingStationDetailTabsCard id={id} />
|
<ChargingStationDetailTabsCard id={id} />
|
||||||
</div>
|
</div>
|
||||||
</CanAccess>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -193,8 +193,8 @@ export const CHARGING_STATIONS_STATUS_COUNT_QUERY = gql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const CHARGING_STATIONS_GET_QUERY = gql`
|
export const CHARGING_STATIONS_GET_QUERY = gql`
|
||||||
query GetChargingStationById($id: String!) {
|
query GetChargingStationById($where: ChargingStations_bool_exp!) {
|
||||||
ChargingStations(where: {id: {_eq: $id}}) {
|
ChargingStations(where: $where, limit: 1) {
|
||||||
id
|
id
|
||||||
tenantId
|
tenantId
|
||||||
ocppConnectionName
|
ocppConnectionName
|
||||||
|
|||||||
Reference in New Issue
Block a user