diff --git a/client/src/components/Widgets/CommCoorReport.jsx b/client/src/components/Widgets/CommCoorReport.jsx new file mode 100644 index 0000000..f156d2f --- /dev/null +++ b/client/src/components/Widgets/CommCoorReport.jsx @@ -0,0 +1,140 @@ +import {Flex, Button, Spinner, Text} from "@chakra-ui/react"; +import useBuildUrl from "../../hooks/useBuildUrl.js"; +import VdapApi from '../../hooks/useVdapApi.js'; +import { useRef, useEffect } from 'react'; +import {useDashboardStore} from "../../store/useDashboardStore.jsx"; + +export default function CommCoorReport({ settings, widgetID }) { + const reportRef = useRef(null); + + const updateSettings = useDashboardStore(state => state.updateWidgetSettings); + + // build url, defaults to 7 days past + const url = useBuildUrl('/api/v2/comm_coor_report', settings?.startDate, settings?.endDate); + console.log(url); + + // VdapApi hook only calls api if the url changes and apiData remains stable since vdapApi uses state to track the data + const {data: apiData, isLoading, error} = VdapApi(url); + console.log(apiData); + + const handleDownloadReport = () => { + if (apiData.length === 0) return; + const element = document.createElement('a'); + element.style.display = 'none'; + // Appends HTML right inside the element, after its last child + element.insertAdjacentHTML('afterbegin', apiData?.report_html); + + // 1. Create a blob object with text content and text/plain mime type + const blob = new Blob([element.textContent || ""], { type: 'text/plain' }); + + // 2. Generate a temporary URL pointing to that blob + const fileUrl = URL.createObjectURL(blob); + + // 3. Create a hidden anchor element + const linkElement = document.createElement('a'); + linkElement.href = fileUrl; + linkElement.download = `comm_coor_report_${apiData?.start_date}_${apiData?.end_date}`; // Sets the target file name + linkElement.style.display = 'none'; + + // 4. Append to body, trigger click, and remove it immediately + document.body.appendChild(linkElement); + linkElement.click(); + document.body.removeChild(linkElement); + document.body.removeChild(element); + + // 5. Free up memory by revoking the blob URL + URL.revokeObjectURL(fileUrl); + } + + const handleCopyReport = () => { + if (apiData.length === 0) return; + const element = document.createElement('a'); + element.style.display = 'none'; + // Appends HTML right inside the element, after its last child + element.insertAdjacentHTML('afterbegin', apiData?.report_html); + + navigator.clipboard.writeText(element.textContent || "") + .then(() => { + alert('Copied to clipboard!'); + }) + .catch((err => { + console.error('Failed to copy: ', err); + })); + + document.body.removeChild(element); + } + + useEffect(() => { + if (reportRef.current) { + reportRef.current.innerHTML = apiData?.report_html; + const p = reportRef.current.querySelectorAll('p'); + const b = reportRef.current.querySelectorAll('b'); + p[0].remove(); + b[0].remove(); + } + }, [apiData]); + + // copy data to clipboard in formatted way when user clicks on copy btn in widget settings + useEffect(() => { + if (settings?.copyToClipboard === true && reportRef.current) { + navigator.clipboard.writeText(reportRef.current.innerText) + .then(() => { + alert('Copied to clipboard!'); + }) + .catch((err => { + console.error('Failed to copy: ', err); + })); + updateSettings(widgetID, {copyToClipboard: false}); + } + }, [settings, widgetID, updateSettings]); + + // download report + useEffect(() => { + if (settings?.downloadReport === true && reportRef.current) { + // 1. Create a blob object with text content and text/plain mime type + const blob = new Blob([reportRef.current.innerText || ""], { type: 'text/plain' }); + + // 2. Generate a temporary URL pointing to that blob + const fileUrl = URL.createObjectURL(blob); + + // 3. Create a hidden anchor element + const linkElement = document.createElement('a'); + linkElement.href = fileUrl; + linkElement.download = `comm_coor_report_${apiData?.start_date}_${apiData?.end_date}`; // Sets the target file name + linkElement.style.display = 'none'; + + // 4. Append to body, trigger click, and remove it immediately + document.body.appendChild(linkElement); + linkElement.click(); + document.body.removeChild(linkElement); + + // 5. Free up memory by revoking the blob URL + URL.revokeObjectURL(fileUrl); + updateSettings(widgetID, {downloadReport: false}); + } + }, [settings, widgetID, updateSettings, apiData]); + + if (isLoading) return ( + + + + ); + + if (error) return ( + + {error} + + ); + + if (!apiData) return ( + + No data + + ); + + return ( + + + + ) +} \ No newline at end of file diff --git a/client/src/components/Widgets/settings/CommCoorReportSettings.jsx b/client/src/components/Widgets/settings/CommCoorReportSettings.jsx new file mode 100644 index 0000000..3edf232 --- /dev/null +++ b/client/src/components/Widgets/settings/CommCoorReportSettings.jsx @@ -0,0 +1,83 @@ +import {Button, Field, Flex, Input, Switch, Text} from "@chakra-ui/react"; +import {useDashboardStore} from "@/store/useDashboardStore.jsx"; +import {useState} from "react"; + +export default function CommCoorReportSettings({ widgetId, currentSettings }) { + const updateSettings = useDashboardStore(state => state.updateWidgetSettings); + const [startError, setStartError] = useState(null); + const [endError, setEndError] = useState(null); + + const handleCopyToClipboard = () => { + updateSettings(widgetId, {copyToClipboard: true}); + } + + const handleDownloadReport = () => { + updateSettings(widgetId, {downloadReport: true}); + } + + return ( + + {/* COPY TO CLIPBOARD */} + + + {/* DOWNLOAD REPORT */} + + + {/* SELECTING DATE RANGE */} + + + + + Start Date + + + { + const value = e.target.value; + updateSettings(widgetId, {...currentSettings, startDate: value}); + + const dateSelected = new Date(value); + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if (value && (dateSelected > today)) setStartError("Can't Choose a Future Date"); + else setStartError(""); + }} + /> + {startError && {startError}} + + + + + + + End Date + + + { + const value = e.target.value; + updateSettings(widgetId, {...currentSettings, endDate: value}); + + const dateSelected = new Date(value); + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if (value && (dateSelected > today)) setEndError("Can't Choose a Future Date"); + else setEndError(""); + }} + /> + {endError && {endError}} + + + + + + ) +} \ No newline at end of file diff --git a/client/src/constants/widgetRegistry.jsx b/client/src/constants/widgetRegistry.jsx index dd6fbfe..2118963 100644 --- a/client/src/constants/widgetRegistry.jsx +++ b/client/src/constants/widgetRegistry.jsx @@ -159,7 +159,24 @@ export const WIDGET_REGISTRY = { description: "Percentage of TP, TN, FP, and FN changes in viable alert levels.", component: lazy(() => import('../components/Widgets/LlmAlChangesQuadPlot.jsx')), allowedSizes: { - small: { w: 3, h: 12 } + medium: { w: 3, h: 12 } + } + }, + + "comm_coor_report": { + name: "Communication Coordinator Report", + category: "Daily Volcanic Activity", + description: "Generated Report", + component: lazy(() => import('../components/Widgets/CommCoorReport.jsx')), + settingsComponent: lazy(() => import('../components/Widgets/settings/CommCoorReportSettings.jsx')), + defaultSettings: { + copyToClipboard: false, + downloadReport: false, + startDate: getStartDate(), + endDate: getEndDate(), + }, + allowedSizes: { + medium: { w: 3, h: 12 }, } } diff --git a/client/src/hooks/useVdapApi.js b/client/src/hooks/useVdapApi.js index ed369db..0f780b1 100644 --- a/client/src/hooks/useVdapApi.js +++ b/client/src/hooks/useVdapApi.js @@ -26,7 +26,7 @@ export default function useWidgetApi(endpoint) { } const json = await response.json(); - const responseData = Array.isArray(json) ? json : json.data; + const responseData = Array.isArray(json) ? json : (json?.data ?? json); setData(responseData || []); } catch (err) {