UserDashboard/components/dashboards/LoggingControl.tsx
Syasya fce26a2bc4
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m56s
amend api endpoints
2025-08-26 15:10:14 +08:00

224 lines
7.0 KiB
TypeScript

'use client';
import React, { useEffect, useMemo, useState } from 'react';
import axios from 'axios';
type FnType = 'grid' | 'solar';
interface LoggingControlCardProps {
siteId: string;
projectLabel?: string; // nice display (e.g., CRM project_name)
className?: string;
}
const API_URL = process.env.NEXT_PUBLIC_FASTAPI_URL;
type FnState = {
serial: string;
isLogging: boolean;
isBusy: boolean; // to block double clicks while calling API
error?: string | null;
};
const emptyFnState: FnState = { serial: '', isLogging: false, isBusy: false, error: null };
const storageKey = (siteId: string) => `logging_control_${siteId}`;
export default function LoggingControlCard({
siteId,
projectLabel,
className = '',
}: LoggingControlCardProps) {
const [grid, setGrid] = useState<FnState>(emptyFnState);
const [solar, setSolar] = useState<FnState>(emptyFnState);
// Load persisted state (if any)
useEffect(() => {
try {
const raw = localStorage.getItem(storageKey(siteId));
if (raw) {
const parsed = JSON.parse(raw);
setGrid({ ...emptyFnState, ...(parsed.grid ?? {}) });
setSolar({ ...emptyFnState, ...(parsed.solar ?? {}) });
} else {
setGrid(emptyFnState);
setSolar(emptyFnState);
}
} catch {
setGrid(emptyFnState);
setSolar(emptyFnState);
}
}, [siteId]);
// Persist on any change
useEffect(() => {
const data = { grid, solar };
try {
localStorage.setItem(storageKey(siteId), JSON.stringify(data));
} catch {
// ignore storage errors
}
}, [siteId, grid, solar]);
const title = useMemo(
() => `Logging Control${projectLabel ? `${projectLabel}` : ''}`,
[projectLabel]
);
const topicsFor = (fn: FnType, serial: string) => {
return [`ADW300/${siteId}/${serial}/${fn}`];
};
const start = async (fn: FnType) => {
const state = fn === 'grid' ? grid : solar;
const setState = fn === 'grid' ? setGrid : setSolar;
if (!state.serial.trim()) {
setState((s) => ({ ...s, error: 'Please enter a meter serial number.' }));
return;
}
setState((s) => ({ ...s, isBusy: true, error: null }));
try {
const topics = topicsFor(fn, state.serial.trim());
const res = await axios.post(`${API_URL}/start-logging`, { topics });
console.log('Start logging:', res.data);
setState((s) => ({ ...s, isLogging: true, isBusy: false }));
} catch (e: any) {
console.error('Failed to start logging', e);
setState((s) => ({
...s,
isBusy: false,
error: e?.response?.data?.detail || e?.message || 'Failed to start logging',
}));
}
};
const stop = async (fn: FnType) => {
const state = fn === 'grid' ? grid : solar;
const setState = fn === 'grid' ? setGrid : setSolar;
if (!state.isLogging) return;
const confirmed = window.confirm(
`Stop logging for ${fn.toUpperCase()} meter "${state.serial}" at site ${siteId}?`
);
if (!confirmed) return;
setState((s) => ({ ...s, isBusy: true, error: null }));
try {
const topics = topicsFor(fn, state.serial.trim());
const res = await axios.post(`${API_URL}/stop-logging`, { topics });
console.log('Stop logging:', res.data);
setState((s) => ({ ...s, isLogging: false, isBusy: false }));
} catch (e: any) {
console.error('Failed to stop logging', e);
setState((s) => ({
...s,
isBusy: false,
error: e?.response?.data?.detail || e?.message || 'Failed to stop logging',
}));
}
};
// Responsive utility classes
const field =
'w-full px-3 py-2 sm:py-2.5 border rounded-md text-sm sm:text-base placeholder:text-gray-400 dark:border-rtgray-700 dark:bg-rtgray-700 dark:text-white';
const label =
'text-gray-600 dark:text-white/85 font-medium text-sm sm:text-base mb-1 flex items-center justify-between mr-2.5';
const section = (
fn: FnType,
labelText: string,
state: FnState,
setState: React.Dispatch<React.SetStateAction<FnState>>
) => (
<div className="space-y-2">
<div className={label}>
<span>{labelText}</span>
{state.isLogging && (
<span
className="inline-flex items-center rounded-full px-2 py-0.5 text-[11px] sm:text-xs font-semibold
bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-300"
aria-live="polite"
>
Logging
</span>
)}
</div>
{/* Input + Button: stack on mobile, row on ≥sm */}
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
<input
type="text"
autoComplete="off"
inputMode="text"
placeholder="Meter serial number"
className={`${field} flex-1`}
value={state.serial}
onChange={(e) => setState((s) => ({ ...s, serial: e.target.value }))}
disabled={state.isLogging || state.isBusy}
aria-label={`${labelText} serial number`}
/>
{!state.isLogging ? (
<button
onClick={() => start(fn)}
disabled={state.isBusy || !state.serial.trim()}
className={`h-10 sm:h-11 rounded-full font-medium transition
w-full sm:w-auto sm:min-w-28 mt-2 lg:mt-0
${state.isBusy || !state.serial.trim()
? 'bg-gray-400 cursor-not-allowed text-black/70'
: 'bg-rtyellow-200 hover:bg-rtyellow-300 text-black'}`}
aria-disabled={state.isBusy || !state.serial.trim()}
>
{state.isBusy ? 'Starting…' : 'Start'}
</button>
) : (
<button
onClick={() => stop(fn)}
disabled={state.isBusy}
className={`h-10 sm:h-11 rounded-full font-medium transition
w-full sm:w-auto sm:min-w-28 mt-2 lg:mt-0
${state.isBusy ? 'bg-gray-400 cursor-not-allowed' : 'bg-red-500 hover:bg-red-600'} text-white`}
aria-disabled={state.isBusy}
>
{state.isBusy ? 'Stopping…' : 'Stop'}
</button>
)}
</div>
{!!state.error && <div className="text-sm sm:text-[15px] text-red-600">{state.error}</div>}
</div>
);
return (
<div
className={`bg-white p-4 sm:p-5 md:p-6 rounded-xl md:rounded-2xl shadow-md space-y-4 md:space-y-5
dark:bg-rtgray-800 dark:text-white-light w-full ${className}`}
>
<h2 className="text-lg sm:text-xl md:text-2xl font-semibold truncate" title={title}>
{title}
</h2>
{section('grid', 'Grid Meter', grid, setGrid)}
<div className="border-t dark:border-rtgray-700" />
{section('solar', 'Solar Meter', solar, setSolar)}
<div className="text-[11px] sm:text-xs text-gray-500 dark:text-gray-400 pt-2 leading-relaxed break-words">
Inputs lock while logging is active. Stop to edit the serial.
<br />
Topics follow{' '}
<code className="break-all">
ADW300/{'{'}siteId{'}'}/{'{'}serial{'}'}/(grid|solar)
</code>
.
</div>
</div>
);
}