crm integration 2

This commit is contained in:
Syasya 2025-08-13 12:31:23 +08:00
parent 37abbde5a1
commit 401a89dd7a
2 changed files with 214 additions and 193 deletions

View File

@ -1,26 +1,51 @@
'use client';
import type { SiteName } from '@/components/dashboards/SiteStatus'; type Option = { label: string; value: string };
type SiteSelectorProps = { type SiteSelectorProps = {
selectedSite: SiteName; options: Option[]; // e.g. [{label: 'Timo… (Installation)', value: 'PROJ-0008'}, …]
setSelectedSite: (site: SiteName) => void; selectedValue: string | null; // the selected project "name" (siteId) or null
onChange: (value: string) => void; // called with the selected value
label?: string;
disabled?: boolean;
}; };
const SiteSelector = ({ selectedSite, setSelectedSite }: SiteSelectorProps) => {
const SiteSelector = ({
options,
selectedValue,
onChange,
label = 'Select Site:',
disabled = false,
}: SiteSelectorProps) => {
const isEmpty = !options || options.length === 0;
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="site" className="font-semibold text-lg dark:text-white">Select Site:</label> <label htmlFor="site" className="font-semibold text-lg dark:text-white">
{label}
</label>
<select <select
id="site" id="site"
className="border p-2 rounded dark:text-white dark:bg-rtgray-800 dark:border-rtgray-700" className="border p-2 rounded dark:text-white dark:bg-rtgray-800 dark:border-rtgray-700"
value={selectedSite} value={selectedValue ?? ''} // keep controlled even when null
onChange={(e) => setSelectedSite(e.target.value as SiteName)} onChange={(e) => onChange(e.target.value)}
disabled={disabled || isEmpty}
> >
<option>Site A</option> {/* Placeholder when nothing selected */}
<option>Site B</option> <option value="" disabled>
<option>Site C</option> {isEmpty ? 'No sites available' : 'Choose a site…'}
</option>
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select> </select>
</div> </div>
); );
}; };
export default SiteSelector; export default SiteSelector;

View File

@ -1,82 +1,80 @@
import axios from "axios"; 'use client';
import React, { useState, useEffect } from "react";
export type SiteName = 'Site A' | 'Site B' | 'Site C'; import axios from "axios";
import React, { useState, useEffect, useMemo } from "react";
export type SiteName = string;
interface SiteStatusProps { interface SiteStatusProps {
selectedSite: SiteName; selectedSite: string; // display label (e.g., CRM project_name)
siteId: string; // canonical id (e.g., CRM Project.name like PROJ-0008)
status?: string; // CRM status (Open/Completed/On Hold/…)
location: string; location: string;
inverterProvider: string; inverterProvider: string;
emergencyContact: string; emergencyContact: string;
lastSyncTimestamp: string; lastSyncTimestamp: string;
} }
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000";
const WS_URL = process.env.NEXT_PUBLIC_WS_URL ?? "ws://localhost:8000/ws";
const SiteStatus = ({ const SiteStatus = ({
selectedSite, selectedSite,
siteId,
status,
location, location,
inverterProvider, inverterProvider,
emergencyContact, emergencyContact,
lastSyncTimestamp, lastSyncTimestamp,
}: SiteStatusProps) => { }: SiteStatusProps) => {
// --- WebSocket to receive MQTT-forwarded messages ---
useEffect(() => { useEffect(() => {
const ws = new WebSocket("ws://localhost:8000/ws"); const ws = new WebSocket(WS_URL);
ws.onmessage = (event) => {
const data = event.data;
alert(`MQTT: ${data}`);
};
ws.onopen = () => console.log("WebSocket connected"); ws.onopen = () => console.log("WebSocket connected");
ws.onclose = () => console.log("WebSocket disconnected"); ws.onclose = () => console.log("WebSocket disconnected");
ws.onerror = (e) => console.error("WebSocket error:", e);
ws.onmessage = (event) => {
// Tip: avoid alert storms; log or toast instead
try {
const data = JSON.parse(event.data);
console.log("WS:", data);
} catch {
console.log("WS raw:", event.data);
}
};
return () => ws.close(); return () => ws.close();
}, []); }, []);
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [deviceId, setDeviceId] = useState(""); const [deviceId, setDeviceId] = useState("");
const [functionType, setFunctionType] = useState("Grid"); const [functionType, setFunctionType] = useState<"Grid" | "Solar">("Grid");
// Map site names to site IDs // Track devices connected per siteId (dynamic)
const siteIdMap: Record<SiteName, string> = { const [loggedDevices, setLoggedDevices] = useState<Record<string, string[]>>({});
"Site A": "site_01", const devicesAtSite = loggedDevices[siteId] ?? [];
"Site B": "site_02",
"Site C": "site_03",
};
// Track devices connected per site const handleStartLogging = () => setShowModal(true);
const [loggedDevices, setLoggedDevices] = useState<Record<string, string[]>>({
site_01: [],
site_02: [],
site_03: [],
});
const siteId = siteIdMap[selectedSite];
const devicesAtSite = loggedDevices[siteId] || [];
const handleStartLogging = () => {
setShowModal(true);
};
const handleConfirm = async () => { const handleConfirm = async () => {
const siteId = siteIdMap[selectedSite]; const id = deviceId.trim();
const topic = `ADW300/${siteId}/${deviceId}/${functionType.toLowerCase()}`; if (!id) return;
const topic = `ADW300/${siteId}/${id}/${functionType.toLowerCase()}`;
try { try {
const response = await axios.post("http://localhost:8000/start-logging", { const response = await axios.post(`${API_URL}/start-logging`, { topics: [topic] });
topics: [topic],
});
console.log("Started logging:", response.data); console.log("Started logging:", response.data);
// Add device to list setLoggedDevices(prev => ({
setLoggedDevices((prev) => ({
...prev, ...prev,
[siteId]: [...(prev[siteId] || []), deviceId], [siteId]: [...(prev[siteId] ?? []), id],
})); }));
setShowModal(false); setShowModal(false);
setDeviceId("");
} catch (error) { } catch (error) {
console.error("Failed to start logging:", error); console.error("Failed to start logging:", error);
} }
@ -84,40 +82,37 @@ const SiteStatus = ({
const handleStopLogging = async () => { const handleStopLogging = async () => {
try { try {
await axios.post("http://localhost:8000/stop-logging"); // Stop only this site's topics (both function types for each device)
const topics = (loggedDevices[siteId] ?? []).flatMap(did => [
// Clear all devices for the site (or modify to remove only specific one) `ADW300/${siteId}/${did}/grid`,
setLoggedDevices((prev) => ({ `ADW300/${siteId}/${did}/solar`,
...prev, ]);
[siteId]: [], await axios.post(`${API_URL}/stop-logging`, topics.length ? { topics } : {});
}));
setLoggedDevices(prev => ({ ...prev, [siteId]: [] }));
console.log("Stopped logging for", siteId); console.log("Stopped logging for", siteId);
} catch (error) { } catch (error) {
console.error("Failed to stop logging:", error); console.error("Failed to stop logging:", error);
} }
}; };
const statusMap: Record<SiteName, string> = { const statusClass = useMemo(() => {
'Site A': 'Active', const s = (status ?? "").toLowerCase();
'Site B': 'Inactive', if (s === "open" || s === "active") return "text-green-500";
'Site C': 'Faulty', if (s === "completed" || s === "closed") return "text-blue-500";
}; if (s === "inactive" || s === "on hold") return "text-orange-500";
if (s === "faulty" || s === "cancelled") return "text-red-500";
return "text-gray-500";
}, [status]);
return ( return (
<div className="bg-white p-4 rounded-lg shadow-md space-y-2 dark:bg-rtgray-800 dark:text-white-light"> <div className="bg-white p-4 rounded-lg shadow-md space-y-2 dark:bg-rtgray-800 dark:text-white-light">
<h2 className="text-xl font-semibold mb-3">Site Details</h2> <h2 className="text-xl font-semibold mb-3">Site Details</h2>
{/* Status */} {/* Status (from CRM) */}
<div className="flex justify-between items-center text-base"> <div className="flex justify-between items-center text-base">
<p className="text-gray-600 dark:text-white/85 font-medium">Status:</p> <p className="text-gray-600 dark:text-white/85 font-medium">Status:</p>
<p className={`font-semibold ${ <p className={`font-semibold ${statusClass}`}>{status ?? "—"}</p>
statusMap[selectedSite] === 'Active' ? 'text-green-500' :
statusMap[selectedSite] === 'Inactive' ? 'text-orange-500' :
'text-red-500'
}`}>
{statusMap[selectedSite]}
</p>
</div> </div>
{/* Site ID */} {/* Site ID */}
@ -150,7 +145,7 @@ const SiteStatus = ({
<p className="font-medium">{lastSyncTimestamp}</p> <p className="font-medium">{lastSyncTimestamp}</p>
</div> </div>
{/* Start Logging Button */} {/* Start/Stop */}
<div className="flex justify-between items-center text-base space-x-2"> <div className="flex justify-between items-center text-base space-x-2">
{devicesAtSite.length > 0 ? ( {devicesAtSite.length > 0 ? (
<button <button
@ -169,25 +164,24 @@ const SiteStatus = ({
)} )}
</div> </div>
{/* Modal */} {/* Modal */}
{showModal && ( {showModal && (
<div className="fixed inset-0 z-50 bg-black bg-opacity-50 flex items-center justify-center"> <div className="fixed inset-0 z-50 bg-black bg-opacity-50 flex items-center justify-center">
<div className="bg-white rounded-lg p-6 w-[90%] max-w-md shadow-lg"> <div className="bg-white dark:bg-rtgray-800 rounded-lg p-6 w-[90%] max-w-md shadow-lg">
<h2 className="text-lg font-semibold mb-4">Enter Device Info</h2> <h2 className="text-lg font-semibold mb-4">Enter Device Info</h2>
<input <input
type="text" type="text"
placeholder="Device ID (e.g. device_01)" placeholder="Device ID (e.g. device_01)"
className="w-full p-2 mb-4 border rounded" className="w-full p-2 mb-4 border rounded dark:border-rtgray-800 dark:bg-rtgray-700 dark:text-white"
value={deviceId} value={deviceId}
onChange={(e) => setDeviceId(e.target.value)} onChange={(e) => setDeviceId(e.target.value)}
/> />
<select <select
className="w-full p-2 mb-4 border rounded" className="w-full p-2 mb-4 border rounded dark:border-rtgray-800 dark:bg-rtgray-700 dark:text-white"
value={functionType} value={functionType}
onChange={(e) => setFunctionType(e.target.value)} onChange={(e) => setFunctionType(e.target.value as "Grid" | "Solar")}
> >
<option value="Grid">Grid</option> <option value="Grid">Grid</option>
<option value="Solar">Solar</option> <option value="Solar">Solar</option>
@ -203,6 +197,7 @@ const SiteStatus = ({
<button <button
onClick={handleConfirm} onClick={handleConfirm}
className="btn-primary px-4 py-2" className="btn-primary px-4 py-2"
disabled={!deviceId.trim()}
> >
Confirm Confirm
</button> </button>
@ -215,3 +210,4 @@ const SiteStatus = ({
}; };
export default SiteStatus; export default SiteStatus;