add comm coor report w/ download and copy abilities
This commit is contained in:
@@ -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 (
|
||||||
|
<Flex w="100%" h="100%" align="center" justify="center" borderRadius="lg" bg="gray.800">
|
||||||
|
<Spinner size="xl" color="blue.400" thickness="4px"/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) return (
|
||||||
|
<Flex w="100%" h="100%" align="center" justify="center" textAlign="center" borderRadius="lg" bg="gray.800">
|
||||||
|
<Text color="red.400">{error}</Text>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!apiData) return (
|
||||||
|
<Flex w="100%" h="100%" align="center" justify="center" borderRadius="lg" bg="gray.800">
|
||||||
|
<Text textAlign="center" color="gray.400">No data</Text>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="row" w="100%" h="100%" p={1} overflowY="auto">
|
||||||
|
<Text ref={reportRef}></Text>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<Flex flexDirection="column" justifyContent="space-between" gap={2} overflowY="auto">
|
||||||
|
{/* COPY TO CLIPBOARD */}
|
||||||
|
<Button w="fit" size="xs" onClick={handleCopyToClipboard}>Copy Report</Button>
|
||||||
|
|
||||||
|
{/* DOWNLOAD REPORT */}
|
||||||
|
<Button w="fit" size="xs" onClick={handleDownloadReport}>Download Report</Button>
|
||||||
|
|
||||||
|
{/* SELECTING DATE RANGE */}
|
||||||
|
<Flex direction="column" w="100%" h="100%" align="center" justify="center" gap="2">
|
||||||
|
<Field.Root invalid={!!startError}>
|
||||||
|
<Flex direction="row" w="100%" h="100%" align="center" justify="center">
|
||||||
|
<Text w="50%" align="center" justify="center" mt={2} color="gray.700">
|
||||||
|
Start Date
|
||||||
|
</Text>
|
||||||
|
<Flex direction="column" w="100%">
|
||||||
|
<Input
|
||||||
|
w="100%"
|
||||||
|
type="date"
|
||||||
|
value={currentSettings?.startDate || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
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 && <Field.ErrorText>{startError}</Field.ErrorText>}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Field.Root>
|
||||||
|
<Field.Root invalid={!!endError}>
|
||||||
|
<Flex direction="row" w="100%" h="100%" align="center" justify="center">
|
||||||
|
<Text w="50%" align="center" justify="center" mt={2} color="gray.700">
|
||||||
|
End Date
|
||||||
|
</Text>
|
||||||
|
<Flex direction="column" w="100%">
|
||||||
|
<Input
|
||||||
|
w="100%"
|
||||||
|
type="date"
|
||||||
|
value={currentSettings?.endDate || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
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 && <Field.ErrorText>{endError}</Field.ErrorText>}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Field.Root>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -159,7 +159,24 @@ export const WIDGET_REGISTRY = {
|
|||||||
description: "Percentage of TP, TN, FP, and FN changes in viable alert levels.",
|
description: "Percentage of TP, TN, FP, and FN changes in viable alert levels.",
|
||||||
component: lazy(() => import('../components/Widgets/LlmAlChangesQuadPlot.jsx')),
|
component: lazy(() => import('../components/Widgets/LlmAlChangesQuadPlot.jsx')),
|
||||||
allowedSizes: {
|
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 },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function useWidgetApi(endpoint) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
const responseData = Array.isArray(json) ? json : json.data;
|
const responseData = Array.isArray(json) ? json : (json?.data ?? json);
|
||||||
setData(responseData || []);
|
setData(responseData || []);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user