Merge branch '20260819_1245_commcoorWidget' into '20260721_0845_Venessa_addingWidgets'
add comm coor report w/ download and copy abilities See merge request vkuchenik/website-framework!108
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.",
|
||||
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 },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user