From 5f7daa9b9c9bef84fda755c2f2ee06c133fa135b Mon Sep 17 00:00:00 2001 From: Adeolu01 Date: Sun, 26 Jul 2026 13:11:38 +0100 Subject: [PATCH] Add IoT integration dashboard with live fleet telemetry charts Implements the Live Fleet Stream product showcase (issue #310): cold chain integrity and shock monitoring highlight cards, a four-metric telemetry grid (temperature, humidity, GPS signal, battery), and Recharts area/line and bar charts for temperature-humidity trends and shock events. Follows the repo's service/hook/component layering (iotDashboardService -> useIoTDashboard -> IoTDashboard), polling every 10s to simulate a live stream, with a skeleton loading state and accessible chart summaries for screen readers. Exposed at /product/iot-dashboard. --- app/product/iot-dashboard/page.tsx | 9 + components/product/IoTDashboard.tsx | 298 ++++++++++++++++++++++++++++ hooks/useIoTDashboard.ts | 33 +++ services/iotDashboardService.ts | 52 +++++ 4 files changed, 392 insertions(+) create mode 100644 app/product/iot-dashboard/page.tsx create mode 100644 components/product/IoTDashboard.tsx create mode 100644 hooks/useIoTDashboard.ts create mode 100644 services/iotDashboardService.ts diff --git a/app/product/iot-dashboard/page.tsx b/app/product/iot-dashboard/page.tsx new file mode 100644 index 0000000..6f0999a --- /dev/null +++ b/app/product/iot-dashboard/page.tsx @@ -0,0 +1,9 @@ +import { IoTDashboard } from '@/components/product/IoTDashboard'; + +export default function IoTDashboardPage() { + return ( +
+ +
+ ); +} diff --git a/components/product/IoTDashboard.tsx b/components/product/IoTDashboard.tsx new file mode 100644 index 0000000..5428854 --- /dev/null +++ b/components/product/IoTDashboard.tsx @@ -0,0 +1,298 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { + Area, + AreaChart, + Bar, + BarChart, + CartesianGrid, + Legend, + Line, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import { Battery, Droplets, Radio, Snowflake, Thermometer, Zap } from 'lucide-react'; +import { useIoTDashboard } from '@/hooks/useIoTDashboard'; + +const SERIES_TEMP = '#2a78d6'; +const SERIES_HUMIDITY = '#eb6834'; +const SERIES_SHOCK = '#e34948'; + +function formatTime(timestamp: string): string { + return new Date(timestamp).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }); +} + +function LiveStreamBanner() { + return ( +
+
+

Live Fleet Stream

+

+ Real-time IoT telemetry from connected shipment sensors. +

+
+
+ + + + + Live +
+
+ ); +} + +interface HighlightCardProps { + icon: ReactNode; + title: string; + primaryLabel: string; + primaryValue: string; + secondaryLabel: string; + secondaryValue: string; +} + +function HighlightCard({ + icon, + title, + primaryLabel, + primaryValue, + secondaryLabel, + secondaryValue, +}: HighlightCardProps) { + return ( +
+
+ {icon} +

{title}

+
+
+
+

{primaryLabel}

+

{primaryValue}

+
+
+

{secondaryLabel}

+

+ {secondaryValue} +

+
+
+
+ ); +} + +interface MetricCardProps { + icon: ReactNode; + label: string; + value: string; +} + +function MetricCard({ icon, label, value }: MetricCardProps) { + return ( +
+
+ {icon} +
+
+

{label}

+

{value}

+
+
+ ); +} + +function DashboardSkeleton() { + return ( +
+
+
+
+
+
+
+ {[0, 1, 2, 3].map((i) => ( +
+ ))} +
+
+
+
+
+
+ ); +} + +export function IoTDashboard() { + const { data, isLoading, isError, error } = useIoTDashboard(); + + if (isLoading) { + return ; + } + + if (isError) { + return ( +
+ Failed to load IoT dashboard data{error ? `: ${error.message}` : '.'} +
+ ); + } + + if (!data) { + return null; + } + + const { coldChain, shockMonitoring, telemetry, summary } = data; + + const latestTemp = telemetry.length + ? telemetry[telemetry.length - 1].temperatureC + : coldChain.currentTempC; + const firstTemp = telemetry.length ? telemetry[0].temperatureC : latestTemp; + const tempTrend = latestTemp >= firstTemp ? 'rising' : 'falling'; + + const shockTrendSummary = shockMonitoring.events.length + ? `${shockMonitoring.events.length} impact events recorded, peak force ${Math.max( + ...shockMonitoring.events.map((event) => event.forceG), + ).toFixed(2)}g.` + : 'No impact events recorded in this window.'; + + return ( +
+ + +
+
+ +
+
+ +
+
+

+ Temperature & Humidity Over Time +

+

+ {`Temperature is currently ${tempTrend} and reads ${latestTemp.toFixed( + 1, + )}°C. Humidity is at ${summary.humidityPct.toFixed(0)} percent.`} +

+
+ + + + + + formatTime(String(value))} /> + + + + + +
+
+ +
+

+ Shock Events (G-Force) +

+

{shockTrendSummary}

+
+ + + + + + formatTime(String(value))} /> + + + +
+
+
+
+ ); +} diff --git a/hooks/useIoTDashboard.ts b/hooks/useIoTDashboard.ts new file mode 100644 index 0000000..e492f75 --- /dev/null +++ b/hooks/useIoTDashboard.ts @@ -0,0 +1,33 @@ +import { useQuery } from '@tanstack/react-query'; +import { + iotDashboardService, + type IoTDashboardData, +} from '@/services/iotDashboardService'; + +export const IOT_DASHBOARD_QUERY_KEY = ['iot-dashboard'] as const; + +export interface UseIoTDashboardReturn { + data: IoTDashboardData | null; + isLoading: boolean; + isError: boolean; + error: Error | null; +} + +/** + * useIoTDashboard — fetches live IoT fleet telemetry for the product + * showcase dashboard. Polls on an interval to simulate a live stream. + */ +export function useIoTDashboard(): UseIoTDashboardReturn { + const { data, isLoading, isError, error } = useQuery({ + queryKey: IOT_DASHBOARD_QUERY_KEY, + queryFn: () => iotDashboardService.getIoTDashboardData(), + refetchInterval: 10000, + }); + + return { + data: data ?? null, + isLoading, + isError, + error: error ?? null, + }; +} diff --git a/services/iotDashboardService.ts b/services/iotDashboardService.ts new file mode 100644 index 0000000..945a095 --- /dev/null +++ b/services/iotDashboardService.ts @@ -0,0 +1,52 @@ +import axios from 'axios'; + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? ''; + +export interface ColdChainMetric { + currentTempC: number; + minSafeTempC: number; + maxSafeTempC: number; + complianceRate: number; +} + +export interface ShockEvent { + timestamp: string; + forceG: number; +} + +export interface ShockMonitoring { + currentForceG: number; + impactThresholdG: number; + events: ShockEvent[]; +} + +export interface FleetTelemetryPoint { + timestamp: string; + temperatureC: number; + humidityPct: number; + gpsSignalPct: number; + batteryPct: number; +} + +export interface TelemetrySummary { + temperatureC: number; + humidityPct: number; + gpsSignalPct: number; + batteryPct: number; +} + +export interface IoTDashboardData { + coldChain: ColdChainMetric; + shockMonitoring: ShockMonitoring; + telemetry: FleetTelemetryPoint[]; + summary: TelemetrySummary; +} + +export const iotDashboardService = { + async getIoTDashboardData(): Promise { + const { data } = await axios.get( + `${API_BASE_URL}/api/iot/dashboard`, + ); + return data; + }, +};