2026-08-06 14:58:12 +00:00
|
|
|
import {Button, Flex, Spinner, Text, Box, Collapsible} from "@chakra-ui/react";
|
2026-08-19 09:52:08 -07:00
|
|
|
import {useEffect, useMemo, useCallback } from 'react';
|
2026-07-30 09:18:47 -07:00
|
|
|
import {useDashboardStore} from "@/store/useDashboardStore.jsx";
|
2026-08-06 14:58:12 +00:00
|
|
|
import VdapApi from '../../hooks/useVdapApi.js';
|
2026-08-18 21:20:49 +00:00
|
|
|
import useBuildUrl from "@/hooks/useBuildUrl.js";
|
2026-07-22 16:29:17 -07:00
|
|
|
|
2026-07-30 09:18:47 -07:00
|
|
|
export default function IvansAlerts({ settings, widgetID }) {
|
2026-08-19 09:52:08 -07:00
|
|
|
console.log("Ivans settings: ", settings, "Ivans widgetID: ", widgetID);
|
2026-07-30 09:18:47 -07:00
|
|
|
const updateSettings = useDashboardStore(state => state.updateWidgetSettings);
|
2026-08-19 09:52:08 -07:00
|
|
|
|
|
|
|
|
// find this specific widgets object in the state list of active/displayed widgets and extract its uuid
|
|
|
|
|
const thisWidget = useDashboardStore(useCallback((state) =>
|
|
|
|
|
state.widgets.find((widget) => widget.id === widgetID), [widgetID]
|
|
|
|
|
));
|
2026-07-22 16:29:17 -07:00
|
|
|
|
2026-08-18 21:20:49 +00:00
|
|
|
// build url, default is 7 days past
|
|
|
|
|
const url = useBuildUrl('/api/v2/comm_coor_ivans', settings?.startDate, settings?.endDate);
|
2026-07-30 09:18:47 -07:00
|
|
|
|
2026-08-05 17:16:45 +00:00
|
|
|
// VdapApi hook only calls api if the url changes and apiData remains stable since vdapApi uses state to track the data
|
2026-08-27 12:36:25 -07:00
|
|
|
const {data, isLoading, error} = VdapApi(url);
|
2026-07-30 09:18:47 -07:00
|
|
|
|
2026-08-19 09:52:08 -07:00
|
|
|
// sort the apiData
|
2026-08-27 12:36:25 -07:00
|
|
|
const some_data = useMemo(() => {
|
|
|
|
|
if (data?.length === 0) return [];
|
2026-08-19 09:52:08 -07:00
|
|
|
const sorts = settings?.activeSorts;
|
2026-07-30 09:18:47 -07:00
|
|
|
|
2026-08-27 12:36:25 -07:00
|
|
|
if (sorts?.volcanoSort && sorts?.timeSort === "asc") return [...data].sort(ascTimeVolcanoSort);
|
|
|
|
|
else if (sorts?.volcanoSort && sorts?.timeSort === "desc") return [...data].sort(descTimeVolcanoSort);
|
|
|
|
|
else if (sorts?.timeSort === "asc") return [...data].sort(ascTimeSort);
|
|
|
|
|
else return [...data].sort(descTimeSort);
|
2026-07-30 09:18:47 -07:00
|
|
|
|
2026-08-27 12:36:25 -07:00
|
|
|
}, [data, settings]);
|
2026-07-30 09:18:47 -07:00
|
|
|
|
2026-08-19 09:52:08 -07:00
|
|
|
// copy data to clipboard in formatted way when user clicks on copy btn in widget settings
|
2026-07-30 09:18:47 -07:00
|
|
|
useEffect(() => {
|
2026-08-19 09:52:08 -07:00
|
|
|
if (settings?.copyToClipboard === true) {
|
2026-08-27 12:36:25 -07:00
|
|
|
const dataText = some_data.map((item) => {
|
2026-08-11 09:20:07 -07:00
|
|
|
return `${item.volcano_name}\n` +
|
|
|
|
|
`Report Number: ${item.report_num}\n` +
|
|
|
|
|
`Send Time: ${item.send_time}\n` +
|
|
|
|
|
`Preparer: ${item.preparer}\n` +
|
|
|
|
|
`Summary:\n${item.summary}\n`;
|
|
|
|
|
}).join('\n\n');
|
2026-08-05 17:16:45 +00:00
|
|
|
navigator.clipboard.writeText(dataText)
|
2026-07-30 09:18:47 -07:00
|
|
|
.then(() => {
|
2026-10-20 16:19:45 -07:00
|
|
|
alert('Copied to clipboard!');
|
2026-07-30 09:18:47 -07:00
|
|
|
})
|
|
|
|
|
.catch((err => {
|
2026-10-20 16:19:45 -07:00
|
|
|
console.error('Failed to copy: ', err);
|
2026-07-30 09:18:47 -07:00
|
|
|
}));
|
|
|
|
|
updateSettings(widgetID, {copyToClipboard: false});
|
|
|
|
|
}
|
2026-08-27 12:36:25 -07:00
|
|
|
}, [settings, some_data, widgetID, updateSettings]);
|
2026-07-30 09:18:47 -07:00
|
|
|
|
2026-10-20 16:19:45 -07:00
|
|
|
// Cleaned hardcoded grays for transparent so glass shell shows through
|
2026-07-30 09:18:47 -07:00
|
|
|
if (isLoading) return (
|
2026-10-20 16:19:45 -07:00
|
|
|
<Flex w="100%" h="100%" align="center" justify="center" borderRadius="lg" bg="transparent">
|
2026-07-30 09:18:47 -07:00
|
|
|
<Spinner size="xl" color="blue.400" thickness="4px"/>
|
|
|
|
|
</Flex>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (error) return (
|
2026-10-20 16:19:45 -07:00
|
|
|
<Flex w="100%" h="100%" align="center" justify="center" borderRadius="lg" bg="transparent">
|
2026-07-30 09:18:47 -07:00
|
|
|
<Text color="red.400">{error}</Text>
|
|
|
|
|
</Flex>
|
|
|
|
|
);
|
|
|
|
|
|
2026-08-27 12:36:25 -07:00
|
|
|
if (some_data.length === 0) return (
|
2026-10-20 16:19:45 -07:00
|
|
|
<Flex w="100%" h="100%" align="center" justify="center" borderRadius="lg" bg="transparent">
|
|
|
|
|
<Text textAlign="center" color="widgetTextMuted">No flight level data available for the selected dates.</Text>
|
2026-07-30 09:18:47 -07:00
|
|
|
</Flex>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
2026-08-06 14:58:12 +00:00
|
|
|
<Flex direction="column"
|
|
|
|
|
w="100%" h="100%"
|
|
|
|
|
pos="relative"
|
|
|
|
|
justify="top" align="center"
|
|
|
|
|
p={2} gap={2}
|
2026-08-10 07:53:26 -07:00
|
|
|
overflow="auto"
|
2026-08-06 14:58:12 +00:00
|
|
|
borderRadius="xl"
|
2026-10-20 16:19:45 -07:00
|
|
|
bg="transparent" // Let the global widgetRecipe glass shell show through
|
2026-08-06 14:58:12 +00:00
|
|
|
>
|
2026-08-27 12:36:25 -07:00
|
|
|
{some_data.map((item, index) => {
|
2026-08-19 09:52:08 -07:00
|
|
|
if (thisWidget?.size === "large") return (
|
2026-08-06 14:58:12 +00:00
|
|
|
<Collapsible.Root key={index} w="100%">
|
2026-08-10 07:53:26 -07:00
|
|
|
{/* Using context to dynamically change the border radii of the triggers */}
|
2026-08-06 14:58:12 +00:00
|
|
|
<Collapsible.Context>
|
|
|
|
|
{(collapsible) => (
|
|
|
|
|
<>
|
|
|
|
|
<Collapsible.Trigger w="100%">
|
2026-08-19 09:52:08 -07:00
|
|
|
<Flex
|
|
|
|
|
direction="row"
|
|
|
|
|
w="100%" h="100%"
|
|
|
|
|
justify="space-between" align="center"
|
|
|
|
|
p={1} pl={3} pr={3}
|
|
|
|
|
gap={2}
|
|
|
|
|
borderTopRadius="lg"
|
|
|
|
|
borderBottomRadius={collapsible.open ? "0" : "lg"}
|
2026-10-20 16:19:45 -07:00
|
|
|
bg="widgetItemBg"
|
2026-08-06 14:58:12 +00:00
|
|
|
>
|
2026-10-20 16:19:45 -07:00
|
|
|
<Flex direction="column" w="100%" minW={0} textAlign="left" color="text">
|
2026-08-11 08:37:08 -07:00
|
|
|
<Text truncate>{item.volcano_name}</Text>
|
2026-10-20 16:19:45 -07:00
|
|
|
<Text truncate fontSize="xs" color="widgetTextMuted">{item.send_time}</Text>
|
2026-08-06 14:58:12 +00:00
|
|
|
</Flex>
|
2026-08-11 13:11:11 -07:00
|
|
|
<Flex w="100%" minW={0} justify="right">
|
2026-10-20 16:19:45 -07:00
|
|
|
<Text truncate pl={2} pr={2} borderRadius="lg" bg="widgetBadgeBg" color="text" fontWeight="semibold">{item.report_num}</Text>
|
2026-08-06 14:58:12 +00:00
|
|
|
</Flex>
|
|
|
|
|
</Flex>
|
|
|
|
|
</Collapsible.Trigger>
|
2026-08-10 07:53:26 -07:00
|
|
|
<Collapsible.Content w="100%" h="auto">
|
|
|
|
|
{/* This box needs to be at h="100%" for the pdf toolbar to appear */}
|
2026-10-20 16:19:45 -07:00
|
|
|
<Box w="100%" h="100%" p={1} borderBottomRadius="lg" bg="widgetItemExpandedBg">
|
|
|
|
|
<Flex direction="column" gap={2} bg="widgetItemBg" borderRadius="lg" p={2} align="end">
|
|
|
|
|
<Text w="100%" h="100%" fontSize="sm" color="text" textAlign="left" whiteSpace="pre-line">{item?.summary}</Text>
|
|
|
|
|
<Text w="fit" fontSize="sm" color="text" pl={1} pr={1} bg="widgetBadgeBg" borderRadius="md">Preparer: {item?.preparer}</Text>
|
2026-08-10 16:22:34 -07:00
|
|
|
</Flex>
|
2026-08-06 14:58:12 +00:00
|
|
|
{item?.files?.map((file, index) => {
|
|
|
|
|
return (
|
2026-08-10 07:53:26 -07:00
|
|
|
<Box w="100%" h="100%" key={index} p={1}>
|
2026-08-10 16:22:34 -07:00
|
|
|
<br />
|
2026-08-18 14:50:37 -07:00
|
|
|
<Flex mb={2} justify="space-between">
|
2026-10-20 16:19:45 -07:00
|
|
|
<Text pl={1} pr={1} bg="widgetBadgeBg" borderRadius="md" fontSize="xs" color="text">{file?.name}</Text>
|
|
|
|
|
<Box borderRadius="lg" bg="widgetBadgeBg" color="text" pl={1} pr={1} fontSize={13} asChild>
|
2026-08-18 14:50:37 -07:00
|
|
|
<a href={`${file?.url}#view=FitV&toolbar=1`} target="_blank" rel="noopener noreferrer">
|
|
|
|
|
Open
|
|
|
|
|
</a>
|
|
|
|
|
</Box>
|
2026-08-10 16:22:34 -07:00
|
|
|
</Flex>
|
2026-08-10 07:53:26 -07:00
|
|
|
<object
|
|
|
|
|
data={`${file?.url}#view=FitH&toolbar=1`}
|
|
|
|
|
type="application/pdf"
|
|
|
|
|
style={{ width: '100%', height: '100%', display: 'block' }}
|
2026-08-06 14:58:12 +00:00
|
|
|
>
|
2026-10-20 16:19:45 -07:00
|
|
|
<Flex w="100%" h="100%" p={4} color="widgetTextMuted">
|
|
|
|
|
<Text mb={2}>
|
2026-08-06 14:58:12 +00:00
|
|
|
Your browser cannot display this PDF directly.
|
|
|
|
|
</Text>
|
|
|
|
|
<a href={file?.url} target="_blank">
|
|
|
|
|
Download PDF
|
|
|
|
|
</a>
|
|
|
|
|
</Flex>
|
|
|
|
|
</object>
|
2026-08-10 07:53:26 -07:00
|
|
|
</Box>
|
2026-08-06 14:58:12 +00:00
|
|
|
)})}
|
|
|
|
|
</Box>
|
|
|
|
|
</Collapsible.Content>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Collapsible.Context>
|
|
|
|
|
</Collapsible.Root>
|
2026-07-30 09:18:47 -07:00
|
|
|
)
|
2026-08-19 09:52:08 -07:00
|
|
|
else if (thisWidget?.size === "small") return (
|
|
|
|
|
<Flex direction="row"
|
2026-10-20 16:19:45 -07:00
|
|
|
key={index} // Added missing map key
|
2026-08-19 09:52:08 -07:00
|
|
|
w="100%"
|
|
|
|
|
justify="space-between" align="center"
|
|
|
|
|
p={1} pl={3} pr={3}
|
|
|
|
|
gap={2}
|
|
|
|
|
borderRadius="lg"
|
2026-10-20 16:19:45 -07:00
|
|
|
bg="widgetItemBg"
|
2026-08-19 09:52:08 -07:00
|
|
|
>
|
2026-10-20 16:19:45 -07:00
|
|
|
<Flex direction="column" w="100%" minW={0} textAlign="left" color="text">
|
2026-08-19 09:52:08 -07:00
|
|
|
<Text truncate>{item.volcano_name}</Text>
|
2026-10-20 16:19:45 -07:00
|
|
|
<Text truncate fontSize="xs" color="widgetTextMuted">{item.send_time}</Text>
|
2026-08-19 09:52:08 -07:00
|
|
|
</Flex>
|
|
|
|
|
<Flex w="100%" minW={0} justify="right">
|
2026-10-20 16:19:45 -07:00
|
|
|
<Text truncate pl={2} pr={2} borderRadius="lg" bg="widgetBadgeBg" color="text" fontWeight="semibold">{item.report_num}</Text>
|
2026-08-19 09:52:08 -07:00
|
|
|
</Flex>
|
|
|
|
|
</Flex>
|
|
|
|
|
)
|
2026-07-30 09:18:47 -07:00
|
|
|
})}
|
|
|
|
|
</Flex>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function ascTimeVolcanoSort(a, b) {
|
|
|
|
|
if (a.volcano_name < b.volcano_name) return -1
|
|
|
|
|
else if (a.volcano_name > b.volcano_name) return 1
|
|
|
|
|
else return ascTimeSort(a, b);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function descTimeVolcanoSort(a, b) {
|
|
|
|
|
if (a.volcano_name < b.volcano_name) return -1
|
|
|
|
|
else if (a.volcano_name > b.volcano_name) return 1
|
|
|
|
|
else return descTimeSort(a, b);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ascTimeSort(a, b) {
|
|
|
|
|
const aTime = new Date(a.send_time);
|
|
|
|
|
const bTime = new Date(b.send_time);
|
|
|
|
|
|
|
|
|
|
if (aTime > bTime) return 1;
|
|
|
|
|
if (aTime < bTime) return -1;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function descTimeSort(a, b) {
|
|
|
|
|
const aTime = new Date(a.send_time);
|
|
|
|
|
const bTime = new Date(b.send_time);
|
|
|
|
|
|
|
|
|
|
if (aTime > bTime) return -1;
|
|
|
|
|
if (aTime < bTime) return 1;
|
|
|
|
|
return 0;
|
2026-07-22 16:29:17 -07:00
|
|
|
}
|