crm integration 2
This commit is contained in:
parent
37abbde5a1
commit
401a89dd7a
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user