All checks were successful
		
		
	
	Build and Deploy / build-and-deploy (push) Successful in 2m56s
				
			
		
			
				
	
	
		
			224 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			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>
 | |
|   );
 | |
| }
 | |
| 
 |