From 189d6e2d1d6ed580202318e443d149c46b25da44 Mon Sep 17 00:00:00 2001 From: vkuchenik Date: Thu, 5 Nov 2026 10:43:05 -0800 Subject: [PATCH] add: new filtering options were added to the alert changes widget. options are lvl inc, lvl dec, elevated lvls. additionally, updated how the UI looks for the al changes widget settings menu --- .../src/components/Widgets/AlertChanges.jsx | 477 +++++++++++++----- .../Widgets/settings/AlertChangeSettings.jsx | 145 +++++- client/src/constants/widgetRegistry.jsx | 5 + client/src/theme.js | 2 +- 4 files changed, 472 insertions(+), 157 deletions(-) diff --git a/client/src/components/Widgets/AlertChanges.jsx b/client/src/components/Widgets/AlertChanges.jsx index d448b46..497e008 100644 --- a/client/src/components/Widgets/AlertChanges.jsx +++ b/client/src/components/Widgets/AlertChanges.jsx @@ -1,28 +1,51 @@ -import {useDashboardStore} from "@/store/useDashboardStore.jsx"; +import { useDashboardStore } from "@/store/useDashboardStore.jsx"; import VdapApi from "../../hooks/useVdapApi.js"; import useBuildUrl from "../../hooks/useBuildUrl.js"; -import { Footprints, Plane, Sailboat, TrendingUp, TrendingDown } from 'lucide-react'; -import {Box, Collapsible, Flex, Spinner, Text, Timeline, Center} from "@chakra-ui/react"; +import { + Footprints, + Plane, + Sailboat, + TrendingUp, + TrendingDown, +} from "lucide-react"; +import { + Box, + Collapsible, + Flex, + Spinner, + Text, + Timeline, + Center, +} from "@chakra-ui/react"; import { useMemo, useCallback } from "react"; +export default function AlertChanges({ settings, widgetID }) { + // 0) CREATE SELF AWARENESS, by finding this specific widgets object in the state-list of active/displayed widgets + const thisWidget = useDashboardStore( + useCallback( + (state) => state.widgets.find((widget) => widget.id === widgetID), + [widgetID], + ), + ); -export default function AlertChanges({ settings, widgetID}) { - // Find this specific widgets object in the state list of active & displayed widgets - const thisWidget = useDashboardStore(useCallback((state) => - state.widgets.find((widget) => widget.id === widgetID), [widgetID] - )); + // 1) BUILD URL, default is 7 days past + const url = useBuildUrl( + "/api/v2/alert_changes", + settings?.startDate, + settings?.endDate, + ); - // build url, default is 7 days past - const url = useBuildUrl('/api/v2/alert_changes', settings?.startDate, settings?.endDate); + // 2) CALL API hook to retrieve data + const { data: apiData, isLoading, error } = VdapApi(url); - // call api hook to retrieve data - const {data: apiData, isLoading, error} = VdapApi(url); - - const data = useMemo(() => { + // 3) SORT the data by grouping into volcanoes and sorting form newest to oldest + // doing this 1st bc the filters might change w/ user input but the sorts (currently) wont and thus will + // only run when the api retrieves new data + const sortedData = useMemo(() => { if (apiData.length === 0) return []; // .reduce() is a built-in array method used to transform an entire array into a single value (like an object) - // here it returns a single massive object w/ volcano names as keys + // here we sort the data by volcano, and return it as a single massive object w/ volcano names as keys const grouped = apiData.reduce((accumulator, item) => { if (!accumulator[item.volcano]) accumulator[item.volcano] = []; accumulator[item.volcano].push(item); @@ -33,33 +56,97 @@ export default function AlertChanges({ settings, widgetID}) { const group_arr = Object.entries(grouped); // sort data newest to oldest within each volcano - const sorted = group_arr.map((item) => - [ - item[0], // volcano name - [...(item[1])].sort((a, b) => - new Date(b.alert_start_date_local) - new Date(a.alert_start_date_local)) - ] + const sorted = group_arr.map((item) => [ + item[0], // volcano name + [...item[1]].sort( // item[1] is the array of al changes for a volcano + (a, b) => + new Date(b.alert_start_date_local) - + new Date(a.alert_start_date_local), + ), + ]); + + // the volcanoes with the most recent issued alert level change appear at the top of the list (newest -> oldest) + const sorted_entries = [...sorted].sort( + (a, b) => + new Date(b[1][0].alert_start_date_local) - + new Date(a[1][0].alert_start_date_local), ); - // volcano with most recent alert level change appears at top of list - const sorted_entries = [...sorted].sort((a, b) => new Date(b[1][0].alert_start_date_local) - new Date(a[1][0].alert_start_date_local)); - + // additionally, seperates out data into domains within a volcano for sm widgets, + // to list all most recent al changes w/in a volcano w/in a domain if (thisWidget?.size === "small") { - return (sorted_entries.map((volcano) => { + return sorted_entries.map((volcano) => { const domain = volcano[1].reduce((acc, curr) => { - if (!acc[curr.alert_scheme_domain]) acc[curr.alert_scheme_domain] = []; + if (!acc[curr.alert_scheme_domain]) + acc[curr.alert_scheme_domain] = []; acc[curr.alert_scheme_domain].push(curr); return acc; }, {}); const domain_arr = Object.entries(domain); + console.log("sm data: ", [volcano[0], domain_arr]); return [volcano[0], domain_arr]; - })); + }); } + console.log("lg data: ", sorted_entries); return sorted_entries; }, [apiData, thisWidget]); + // 4) FILTER according to what is set in the widgets settings + const filteredData = useMemo(() => { + if (sortedData.length === 0) return []; + let filtered_arr = []; + + // must show at least one inc or dec alert levels, otherwise no data is displayed + if ( + !settings?.filters?.levelIncreases && + !settings?.filters?.levelDecreases + ) + return []; + + // return all data + if ( + settings?.filters?.levelIncreases && + settings?.filters?.levelDecreases && + !settings?.filters.elevatedLevels + ) + return sortedData; + + // only elevated alert levels + filtered_arr = settings?.filters.elevatedLevels + ? sortedData.map((volcano) => [ + volcano[0], + volcano[1].filter( + (item) => + item.alert_scheme_highest - Number(item.alert_level_rank) <= 1, + ), + ]) + .filter((volcano) => volcano[1].length > 0) + : sortedData; + + // only decreases (rm inc) + filtered_arr = !settings?.filters.levelIncreases + ? filtered_arr.map((volcano) => [ + volcano[0], + volcano[1].filter((item) => !(item.change_type === "increase")), + ]) + .filter((volcano) => volcano[1].length > 0) + : filtered_arr; + + // only increases (rm dec) + filtered_arr = !settings?.filters?.levelDecreases + ? filtered_arr.map((volcano) => [ + volcano[0], + volcano[1].filter((item) => !(item.change_type === "decrease")), + ]) + .filter((volcano) => volcano[1].length > 0) + : filtered_arr; + + console.log("filtered_arr: ", filtered_arr); + return filtered_arr; + }, [sortedData, settings]); + // Alert colors are now light/dark mode responsive const alertColors = { Red: "alertRed", @@ -68,149 +155,283 @@ export default function AlertChanges({ settings, widgetID}) { Green: "alertGreen", }; - if (isLoading) return ( - - - - ); + if (isLoading) + return ( + + + + ); - if (error) return ( - - {error} - - ); + if (error) + return ( + + {error} + + ); - if (data.length === 0) return ( - - No flight level data available for the selected dates. - - ); + if (filteredData.length === 0) + return ( + + + No alert level data available for the selected dates. + + + ); return ( - // bg="transparent" to let the glass panel show through - - {data.map((volcano, index) => { - if (thisWidget?.size === "large") return ( - - - {(collapsible) => ( - <> - - + {filteredData.map((volcano, index) => { + if (thisWidget?.size === "large") + return ( + + + {(collapsible) => ( + <> + + + - - {volcano[0]} - {volcano[1][0]?.alert_start_date_local} + minW={0} + textAlign="left" + > + + {volcano[0]} + + + {volcano[1][0]?.alert_start_date_local} + + + + + {volcano[1][0]?.change_type === "increase" ? ( + + ) : ( + + )} + + + {volcano[1][0]?.alert_level_name} + + - - - {volcano[1][0]?.alert_level_name} - - - - + - - - {volcano[1]?.map((object, index) => ( - - - - {object?.alert_scheme_domain === "terrestrial" && } - {object?.alert_scheme_domain === "aviation" && } - {object?.alert_scheme_domain === "maritime" && } - - - {object?.change_type === "increase" ? : } - - - - - - {object?.alert_start_date_local} - - {object?.alert_level_name} - - - Alert Scale: - {(Array.from({ length: Number(object?.alert_scheme_highest) - Number(object?.alert_scheme_lowest) + 1}, (v, i) => Number(object?.alert_scheme_lowest) + i)) - .map((num) => ( + + + {volcano[1]?.map((object, index) => ( + + + + {object?.alert_scheme_domain === + "terrestrial" && } + {object?.alert_scheme_domain === "aviation" && ( + + )} + {object?.alert_scheme_domain === "maritime" && ( + + )} + + + {object?.change_type === "increase" ? ( + + ) : ( + + )} + + + + + + + {object?.alert_start_date_local} + + + {object?.alert_level_name} + + + + Alert Scale:{" "} + + {Array.from( + { + length: + Number(object?.alert_scheme_highest) - + Number(object?.alert_scheme_lowest) + + 1, + }, + (v, i) => + Number(object?.alert_scheme_lowest) + i, + ).map((num) => (
{num}
- )) - } + ))} +
-
-
-
-
- ))} -
-
- - )} -
-
- ) - else if (thisWidget?.size === "small") return ( - volcano[1]?.map((domain, index) => ( + + + + ))} + + + + )} + + + ); + else if (thisWidget?.size === "small") + return volcano[1]?.map((domain, index) => ( - + - {domain[0] === "aviation" && } - {domain[0] === "terrestrial" && } - {domain[0] === "maritime" && } + {domain[0] === "aviation" && } + {domain[0] === "terrestrial" && } + {domain[0] === "maritime" && } - {volcano[0] || ""} + + {volcano[0] || ""} + - {domain[1]?.[0]?.alert_start_date_local || ""} + + {domain[1]?.[0]?.alert_start_date_local || ""} + - + - {domain[1]?.[0]?.change_type === "increase" ? : } + {domain[1]?.[0]?.change_type === "increase" ? ( + + ) : ( + + )} {domain[1]?.[0]?.alert_level_name} - )) - ); + )); })}
- ) -} \ No newline at end of file + ); +} diff --git a/client/src/components/Widgets/settings/AlertChangeSettings.jsx b/client/src/components/Widgets/settings/AlertChangeSettings.jsx index b4b041f..9b4d4cc 100644 --- a/client/src/components/Widgets/settings/AlertChangeSettings.jsx +++ b/client/src/components/Widgets/settings/AlertChangeSettings.jsx @@ -1,38 +1,127 @@ -import {Flex, Slider, Switch} from "@chakra-ui/react"; +import {Flex, Slider, Switch, Text, Field, Fieldset, Input, CheckboxCard, CheckboxGroup} from "@chakra-ui/react"; import {useDashboardStore} from "@/store/useDashboardStore.jsx"; +import { useState } from "react"; export default function IvansAlertsSettings({ widgetId, currentSettings }) { const updateSettings = useDashboardStore(state => state.updateWidgetSettings); + const [startError, setStartError] = useState(null); + const [endError, setEndError] = useState(null); console.log("currentSettings:", currentSettings); return ( - - {/* SLIDER DATE RANGE SELECTION */} - { - console.log("change end", e); - const day = new Date(Date.now() - (e.value[0])*86400000).toLocaleDateString('en-CA'); - updateSettings(widgetId, {...currentSettings, startDate: day}); - }} - > - {currentSettings?.startDate} to {currentSettings?.endDate} - - - - - - - today - 1 week ago - 2 weeks ago - - - - + + + + {/* SELECTING DATE RANGE */} + + + Date Range Selection +
{/*line seperator*/} +
+ + {/* start date */} + + + 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 */} + + + 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}} + + + +
+ + {/* FILTER ON CHANGE TYPE */} + + + + Filters +
{/*line seperator*/} +
+ { + updateSettings(widgetId, {...currentSettings, filters: {...currentSettings.filters, levelIncreases: e.checked}}) + }} + > + {/* when removed can't check/uncheck box */} + + + Level Increases + + + + + + { + updateSettings(widgetId, {...currentSettings, filters: {...currentSettings.filters, levelDecreases: e.checked}}) + }} + > + + + + Level Decreases + + + + + + { + updateSettings(widgetId, {...currentSettings, filters: {...currentSettings.filters, elevatedLevels: e.checked}}) + }} + > + + + + Only Elevated Levels + + + + +
+
+
+
+
) } \ No newline at end of file diff --git a/client/src/constants/widgetRegistry.jsx b/client/src/constants/widgetRegistry.jsx index 93919fa..79a9c19 100644 --- a/client/src/constants/widgetRegistry.jsx +++ b/client/src/constants/widgetRegistry.jsx @@ -99,6 +99,11 @@ export const WIDGET_REGISTRY = { defaultSettings: { startDate: getStartDate(), endDate: getEndDate(), + filters: { + levelIncreases: true, + levelDecreases: true, + elevatedLevels: false, // will filter to show only volcanoes in their 2nd highest to highest al + }, }, allowedSizes: { small: { w: 3, h: 6 }, diff --git a/client/src/theme.js b/client/src/theme.js index 2c50ce3..f1ff888 100644 --- a/client/src/theme.js +++ b/client/src/theme.js @@ -353,7 +353,7 @@ const glassPopoverRecipe = defineSlotRecipe({ borderRadius: "xl", p: 3, boxShadow: "lg", - bg: {base: "rgba(255, 255, 255, 0.85)", _dark: "rgba(20, 22, 25, 0.85)"}, + bg: {base: "rgba(255, 255, 255, 0.95)", _dark: "rgba(20, 22, 25, 0.95)"}, backdropFilter: "blur(15px)", WebkitBackdropFilter: "blur(15px)", },