From bdd53a0accfff60116884b411249fe4bfd8aa40b Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Mon, 15 Jun 2026 16:05:57 +0200 Subject: [PATCH 01/30] Move CreatedBoxes component into CreatedBoxesFilterContainer --- .../CreatedBoxesFilterContainer.tsx | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesFilterContainer.tsx b/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesFilterContainer.tsx index 405e9af0a..d1681cf1a 100644 --- a/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesFilterContainer.tsx +++ b/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesFilterContainer.tsx @@ -1,7 +1,8 @@ +import { Wrap, WrapItem, Box } from "@chakra-ui/react"; import { useEffect, useMemo } from "react"; import { TidyFn, distinct, filter, tidy } from "@tidyjs/tidy"; import { useReactiveVar } from "@apollo/client"; -import CreatedBoxesCharts from "./CreatedBoxesCharts"; +import CreatedBoxes from "./CreatedBoxes"; import { filterListByInterval } from "../../../../utils/helpers"; import useTimerange from "../../../hooks/useTimerange"; import useValueFilter from "../../../hooks/useValueFilter"; @@ -29,10 +30,10 @@ import { categoryFilterValuesVar, } from "../../../state/filter"; import { filterByTags } from "../../../utils/filterByTags"; -import { CreatedBoxes, CreatedBoxesResult } from "../../../../../graphql/types"; +import { CreatedBoxes as CreatedBoxesType, CreatedBoxesResult } from "../../../../../graphql/types"; interface ICreatedBoxesFilterContainerProps { - createdBoxes: CreatedBoxes; + createdBoxes: CreatedBoxesType; } export default function CreatedBoxesFilterContainer({ @@ -167,5 +168,18 @@ export default function CreatedBoxesFilterContainer({ dimensions: createdBoxes?.dimensions, }; - return ; + return ( + + + + + + + + ); } From 975217434013726c94d722df3206151126645f84 Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Mon, 15 Jun 2026 16:06:37 +0200 Subject: [PATCH 02/30] Remove StockOverview from Dashboard --- shared-components/statviz/dashboard/Dashboard.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/shared-components/statviz/dashboard/Dashboard.tsx b/shared-components/statviz/dashboard/Dashboard.tsx index 4fe30df76..eba4bb285 100644 --- a/shared-components/statviz/dashboard/Dashboard.tsx +++ b/shared-components/statviz/dashboard/Dashboard.tsx @@ -3,7 +3,6 @@ import TimeRangeSelect from "../components/filter/TimeRangeSelect"; import Demographics from "./Demographics"; import MovedBoxes from "./MovedBoxes"; import ItemsAndBoxes from "./ItemsAndBoxes"; -import StockOverview from "./StockOverview"; import BoxesOrItemsSelect, { boxesOrItemsFilterValues, } from "../components/filter/BoxesOrItemsSelect"; @@ -83,7 +82,6 @@ export default function Dashboard() { - ); From e59def1612705f0c943cd2d92327c4a206dc7547 Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Mon, 15 Jun 2026 16:06:58 +0200 Subject: [PATCH 03/30] Update section titles --- shared-components/statviz/dashboard/Demographics.tsx | 2 +- shared-components/statviz/dashboard/ItemsAndBoxes.tsx | 2 +- shared-components/statviz/dashboard/MovedBoxes.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shared-components/statviz/dashboard/Demographics.tsx b/shared-components/statviz/dashboard/Demographics.tsx index ff0cdafd3..f5c10e635 100644 --- a/shared-components/statviz/dashboard/Demographics.tsx +++ b/shared-components/statviz/dashboard/Demographics.tsx @@ -15,7 +15,7 @@ export default function Demographics() { - Demographics + Beneficiary Overview diff --git a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx index efe35b67f..1f158c500 100644 --- a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx +++ b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx @@ -15,7 +15,7 @@ export default function ItemsAndBoxes() { - Items and Boxes + Stock Overview diff --git a/shared-components/statviz/dashboard/MovedBoxes.tsx b/shared-components/statviz/dashboard/MovedBoxes.tsx index 3fadbbdc1..c82271a8b 100644 --- a/shared-components/statviz/dashboard/MovedBoxes.tsx +++ b/shared-components/statviz/dashboard/MovedBoxes.tsx @@ -13,7 +13,7 @@ export default function MovedBoxes() { - Shipments + Movement History From dfd81d664c564c97b07747859770e0509275c5f6 Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Mon, 15 Jun 2026 16:09:43 +0200 Subject: [PATCH 04/30] Remove unneeded components --- .../createdBoxes/CreatedBoxesCharts.tsx | 35 --------- .../createdBoxes/TopCreatedProducts.tsx | 71 ------------------- .../statviz/utils/analytics/constants.ts | 1 - 3 files changed, 107 deletions(-) delete mode 100644 shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesCharts.tsx delete mode 100644 shared-components/statviz/components/visualizations/createdBoxes/TopCreatedProducts.tsx diff --git a/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesCharts.tsx b/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesCharts.tsx deleted file mode 100644 index 58f21d058..000000000 --- a/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesCharts.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Wrap, WrapItem, Box } from "@chakra-ui/react"; -import CreatedBoxes from "./CreatedBoxes"; -import TopCreatedProducts from "./TopCreatedProducts"; -import { BoxesOrItems } from "../../filter/BoxesOrItemsSelect"; -import { CreatedBoxes as CreatedBoxesType } from "../../../../../graphql/types"; - -export default function CreatedBoxesCharts(props: { - data: Partial; - boxesOrItems: BoxesOrItems; -}) { - return ( - - - - - - - - - - - - - ); -} diff --git a/shared-components/statviz/components/visualizations/createdBoxes/TopCreatedProducts.tsx b/shared-components/statviz/components/visualizations/createdBoxes/TopCreatedProducts.tsx deleted file mode 100644 index e8a376fcd..000000000 --- a/shared-components/statviz/components/visualizations/createdBoxes/TopCreatedProducts.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Card, CardBody } from "@chakra-ui/react"; -import { useMemo } from "react"; -import { arrange, desc, groupBy, innerJoin, map, sum, summarize, tidy } from "@tidyjs/tidy"; -import BarChart from "../../nivo/BarChart"; -import VisHeader from "../../VisHeader"; -import getOnExport from "../../../utils/chartExport"; -import NoDataCard from "../../NoDataCard"; -import { CreatedBoxes, CreatedBoxesResult, Product } from "../../../../../graphql/types"; - -export default function TopCreatedProducts(props: { - width: string; - height: string; - boxesOrItems: string; - data: Partial; -}) { - const onExport = getOnExport(BarChart); - const { boxesOrItems, data } = { ...props }; - - const getChartData = () => - tidy( - data?.facts as CreatedBoxesResult[], - map((row) => ({ ...row, productId: row.productId })), - groupBy( - ["productId", "gender"], - [ - summarize({ - itemsCount: sum("itemsCount"), - boxesCount: sum("boxesCount"), - }), - ], - ), - innerJoin(data?.dimensions?.product as Product[], { by: { id: "productId" } as any }), - map((row) => ({ - id: `${row.name} (${row.gender})`, - value: row[boxesOrItems], - label: `${row[boxesOrItems]}`, - })), - arrange([desc("value")]), - ).splice(0, 5); - - const chartData = useMemo(getChartData, [data, boxesOrItems]); - - const chartProps = { - data: chartData, - width: props.width, - height: props.height, - }; - - const topProductsHeading = boxesOrItems === "boxesCount" ? "Boxes" : "Products"; - const heading = `Top Created ${topProductsHeading}`; - - if (chartData.length === 0) { - return ; - } - - return ( - - - - - - - ); -} diff --git a/shared-components/statviz/utils/analytics/constants.ts b/shared-components/statviz/utils/analytics/constants.ts index f15943548..01cb8e7f5 100644 --- a/shared-components/statviz/utils/analytics/constants.ts +++ b/shared-components/statviz/utils/analytics/constants.ts @@ -1,6 +1,5 @@ const GRAPH_TYPES = { CREATED_BOXES: "CreatedBoxes", - TOP_CREATED_PRODUCTS: "TopCreatedProducts", OUTGOING_BOXES: "OutGoingBoxes", DEMOGRAPHICS_BENEFICIARIES_REGISTERED: "BeneficiariesRegistered", STOCK_OVERVIEW: "StockOverview", From 227d28c8e920dac7b56ab7441c20d733013dceca Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Mon, 22 Jun 2026 09:08:25 +0200 Subject: [PATCH 05/30] Remove StockOverview --- .../statviz/dashboard/StockOverview.tsx | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 shared-components/statviz/dashboard/StockOverview.tsx diff --git a/shared-components/statviz/dashboard/StockOverview.tsx b/shared-components/statviz/dashboard/StockOverview.tsx deleted file mode 100644 index fcb21ebf6..000000000 --- a/shared-components/statviz/dashboard/StockOverview.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { - AccordionItem, - AccordionButton, - Heading, - AccordionIcon, - AccordionPanel, - Box, -} from "@chakra-ui/react"; -import StockDataContainer from "../components/visualizations/stock/StockDataContainer"; - -export default function StockOverview() { - return ( - - - - Stock Overview - - - - - - - - ); -} From 86fb76e945873d9a5c76c36c68151c7181b13c0e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:17:00 +0000 Subject: [PATCH 06/30] Rework statviz filtering: per-section local state, staged Apply, URL sync Co-authored-by: pylipp <10617122+pylipp@users.noreply.github.com> --- .../filter/DemographicsFilterPanel.tsx | 155 ++++++++ .../components/filter/MovementFilterPanel.tsx | 231 +++++++++++ .../components/filter/StockFilterPanel.tsx | 227 +++++++++++ .../CreatedBoxesDataContainer.tsx | 20 +- .../CreatedBoxesFilterContainer.tsx | 157 ++------ .../demographic/DemographicDataContainer.tsx | 16 +- .../DemographicFilterContainer.tsx | 80 ++-- .../movedBoxes/MovedBoxes.test.tsx | 17 +- .../movedBoxes/MovedBoxesDataContainer.tsx | 20 +- .../movedBoxes/MovedBoxesFilterContainer.tsx | 130 ++---- .../statviz/dashboard/Dashboard.tsx | 162 ++++---- .../statviz/dashboard/Demographics.tsx | 43 +- .../statviz/dashboard/ItemsAndBoxes.tsx | 76 +++- .../statviz/dashboard/MovedBoxes.tsx | 71 +++- .../statviz/utils/dashboardFilters.ts | 375 ++++++++++++++++++ .../statviz/utils/filterByTags.ts | 6 +- 16 files changed, 1419 insertions(+), 367 deletions(-) create mode 100644 shared-components/statviz/components/filter/DemographicsFilterPanel.tsx create mode 100644 shared-components/statviz/components/filter/MovementFilterPanel.tsx create mode 100644 shared-components/statviz/components/filter/StockFilterPanel.tsx create mode 100644 shared-components/statviz/utils/dashboardFilters.ts diff --git a/shared-components/statviz/components/filter/DemographicsFilterPanel.tsx b/shared-components/statviz/components/filter/DemographicsFilterPanel.tsx new file mode 100644 index 000000000..df3d0a4d8 --- /dev/null +++ b/shared-components/statviz/components/filter/DemographicsFilterPanel.tsx @@ -0,0 +1,155 @@ +import { useCallback, useEffect, useState } from "react"; +import { + VStack, + Button, + Drawer, + DrawerBody, + DrawerCloseButton, + DrawerContent, + DrawerHeader, + DrawerOverlay, + SimpleGrid, + Box, + IconButton, + CheckboxGroup, + Checkbox, + HStack, + FormLabel, +} from "@chakra-ui/react"; +import { SettingsIcon } from "@chakra-ui/icons"; +import TabbedTagDropdown from "./TabbedTagDropdown"; +import type { ITagOption, DemographicsAppliedFilters } from "../../utils/dashboardFilters"; +import { AGE_RANGES } from "../../utils/dashboardFilters"; + +const HUMAN_GENDERS = ["Male", "Female", "Diverse"] as const; + +interface DemographicsFilterPanelProps { + appliedFilters: DemographicsAppliedFilters; + tags: ITagOption[]; + onApply: (filters: DemographicsAppliedFilters) => void; +} + +export default function DemographicsFilterPanel({ + appliedFilters, + tags, + onApply, +}: DemographicsFilterPanelProps) { + const [isOpen, setIsOpen] = useState(false); + const [staged, setStaged] = useState(appliedFilters); + + // Re-initialise staged state when the drawer opens + useEffect(() => { + if (isOpen) { + setStaged(appliedFilters); + } + }, [isOpen, appliedFilters]); + + const handleApply = useCallback(() => { + onApply(staged); + setIsOpen(false); + }, [staged, onApply]); + + const handleClear = useCallback(() => { + setStaged({ ageRanges: [], genders: [], includedTags: [], excludedTags: [] }); + }, []); + + return ( + <> + } + size="sm" + variant="outline" + onClick={(e) => { + e.stopPropagation(); + setIsOpen(true); + }} + /> + setIsOpen(false)} placement="right" size="md"> + + + Demographics Filters + + + + + + Age + + setStaged((prev) => ({ ...prev, ageRanges: values as string[] })) + } + > + + {AGE_RANGES.map((range) => ( + + {range.label} + + ))} + + + + + Gender + + setStaged((prev) => ({ ...prev, genders: values as string[] })) + } + > + + {HUMAN_GENDERS.map((gender) => ( + + {gender} + + ))} + + + + + Tags + + setStaged((prev) => ({ ...prev, includedTags: newTags })) + } + onExcludedChange={(newTags) => + setStaged((prev) => ({ ...prev, excludedTags: newTags })) + } + onClearAll={() => + setStaged((prev) => ({ ...prev, includedTags: [], excludedTags: [] })) + } + placeholder="Select tags" + /> + + + + + + + + + + + + + + ); +} diff --git a/shared-components/statviz/components/filter/MovementFilterPanel.tsx b/shared-components/statviz/components/filter/MovementFilterPanel.tsx new file mode 100644 index 000000000..729545bb2 --- /dev/null +++ b/shared-components/statviz/components/filter/MovementFilterPanel.tsx @@ -0,0 +1,231 @@ +import { useCallback, useEffect, useState } from "react"; +import { + VStack, + Button, + Drawer, + DrawerBody, + DrawerCloseButton, + DrawerContent, + DrawerHeader, + DrawerOverlay, + SimpleGrid, + Box, + IconButton, + Input, + FormLabel, + HStack, +} from "@chakra-ui/react"; +import { SettingsIcon } from "@chakra-ui/icons"; +import MultiSelectFilter from "./MultiSelectFilter"; +import TabbedTagDropdown from "./TabbedTagDropdown"; +import type { + IProductOption, + ICategoryOption, + ITagOption, + MovementAppliedFilters, +} from "../../utils/dashboardFilters"; +import { genders } from "./GenderProductFilter"; +import type { IFilterValue } from "./ValueFilter"; + +interface MovementFilterPanelProps { + appliedFilters: MovementAppliedFilters; + products: IProductOption[]; + categories: ICategoryOption[]; + tags: ITagOption[]; + onApply: (filters: MovementAppliedFilters) => void; +} + +function toProductFilterValues(products: IProductOption[]): IFilterValue[] { + return products.map((p) => ({ + value: String(p.id), + label: p.gender ? `${p.name} (${p.gender})` : p.name, + urlId: String(p.id), + })); +} + +function toFilterValues(items: { id: number; name: string }[]): IFilterValue[] { + return items.map((item) => ({ + value: String(item.id), + label: item.name, + urlId: String(item.id), + })); +} + +export default function MovementFilterPanel({ + appliedFilters, + products, + categories, + tags, + onApply, +}: MovementFilterPanelProps) { + const [isOpen, setIsOpen] = useState(false); + const [staged, setStaged] = useState(appliedFilters); + + // Re-initialise staged state when the drawer opens + useEffect(() => { + if (isOpen) { + setStaged(appliedFilters); + } + }, [isOpen, appliedFilters]); + + const productOptions = toProductFilterValues(products); + const categoryOptions = toFilterValues(categories); + + const handleApply = useCallback(() => { + onApply(staged); + setIsOpen(false); + }, [staged, onApply]); + + const handleClear = useCallback(() => { + setStaged((prev) => ({ + ...prev, + products: [], + genders: [], + categories: [], + includedTags: [], + excludedTags: [], + })); + }, []); + + const selectedProductValues = productOptions.filter((o) => + staged.products.some((p) => String(p.id) === o.value), + ); + const selectedCategoryValues = categoryOptions.filter((o) => + staged.categories.some((c) => String(c.id) === o.value), + ); + const selectedGenderValues = genders.filter((g) => staged.genders.includes(g.value)); + + return ( + <> + } + size="sm" + variant="outline" + onClick={(e) => { + e.stopPropagation(); + setIsOpen(true); + }} + /> + setIsOpen(false)} placement="right" size="md"> + + + Movement Filters + + + + + + Move date + + + From + + setStaged((prev) => ({ ...prev, dateFrom: e.target.value })) + } + /> + + + To + setStaged((prev) => ({ ...prev, dateTo: e.target.value }))} + /> + + + + + setStaged((prev) => ({ + ...prev, + genders: selected.map((s) => s.value), + })) + } + placeholder="All" + /> + { + const selectedIds = selected.map((s) => Number(s.value)); + setStaged((prev) => ({ + ...prev, + products: products.filter((p) => selectedIds.includes(p.id)), + })); + }} + placeholder="All" + /> + { + const selectedIds = selected.map((s) => Number(s.value)); + setStaged((prev) => ({ + ...prev, + categories: categories.filter((c) => selectedIds.includes(c.id)), + })); + }} + placeholder="All" + /> + + Tags + + setStaged((prev) => ({ ...prev, includedTags: newTags })) + } + onExcludedChange={(newTags) => + setStaged((prev) => ({ ...prev, excludedTags: newTags })) + } + onClearAll={() => + setStaged((prev) => ({ ...prev, includedTags: [], excludedTags: [] })) + } + placeholder="Select tags" + /> + + + + + + + + + + + + + + ); +} diff --git a/shared-components/statviz/components/filter/StockFilterPanel.tsx b/shared-components/statviz/components/filter/StockFilterPanel.tsx new file mode 100644 index 000000000..84fcbd301 --- /dev/null +++ b/shared-components/statviz/components/filter/StockFilterPanel.tsx @@ -0,0 +1,227 @@ +import { useCallback, useEffect, useState } from "react"; +import { + VStack, + Button, + Drawer, + DrawerBody, + DrawerCloseButton, + DrawerContent, + DrawerHeader, + DrawerOverlay, + SimpleGrid, + Box, + IconButton, + FormLabel, +} from "@chakra-ui/react"; +import { SettingsIcon } from "@chakra-ui/icons"; +import MultiSelectFilter from "./MultiSelectFilter"; +import TabbedTagDropdown from "./TabbedTagDropdown"; +import type { + IProductOption, + ICategoryOption, + ILocationOption, + ITagOption, + StockAppliedFilters, +} from "../../utils/dashboardFilters"; +import { genders } from "./GenderProductFilter"; +import type { IFilterValue } from "./ValueFilter"; + +interface StockFilterPanelProps { + appliedFilters: StockAppliedFilters; + products: IProductOption[]; + categories: ICategoryOption[]; + locations: ILocationOption[]; + tags: ITagOption[]; + onApply: (filters: StockAppliedFilters) => void; +} + +function toFilterValues( + items: { id: number; name: string }[], + genderSuffix?: string, +): IFilterValue[] { + return items.map((item) => ({ + value: String(item.id), + label: genderSuffix ? `${item.name} (${genderSuffix})` : item.name, + urlId: String(item.id), + })); +} + +function toProductFilterValues(products: IProductOption[]): IFilterValue[] { + return products.map((p) => ({ + value: String(p.id), + label: p.gender ? `${p.name} (${p.gender})` : p.name, + urlId: String(p.id), + })); +} + +export default function StockFilterPanel({ + appliedFilters, + products, + categories, + locations, + tags, + onApply, +}: StockFilterPanelProps) { + const [isOpen, setIsOpen] = useState(false); + const [staged, setStaged] = useState(appliedFilters); + + // Re-initialise staged state when the drawer opens + useEffect(() => { + if (isOpen) { + setStaged(appliedFilters); + } + }, [isOpen, appliedFilters]); + + const productOptions = toProductFilterValues(products); + const categoryOptions = toFilterValues(categories); + const locationOptions = toFilterValues(locations); + + const handleApply = useCallback(() => { + onApply(staged); + setIsOpen(false); + }, [staged, onApply]); + + const handleClear = useCallback(() => { + setStaged({ + products: [], + genders: [], + categories: [], + locations: [], + includedTags: [], + excludedTags: [], + }); + }, []); + + // Helpers to convert between IFilterValue selection and option objects + const selectedProductValues = productOptions.filter((o) => + staged.products.some((p) => String(p.id) === o.value), + ); + const selectedCategoryValues = categoryOptions.filter((o) => + staged.categories.some((c) => String(c.id) === o.value), + ); + const selectedLocationValues = locationOptions.filter((o) => + staged.locations.some((l) => String(l.id) === o.value), + ); + const selectedGenderValues = genders.filter((g) => staged.genders.includes(g.value)); + + return ( + <> + } + size="sm" + variant="outline" + onClick={(e) => { + e.stopPropagation(); + setIsOpen(true); + }} + /> + setIsOpen(false)} placement="right" size="md"> + + + Stock Filters + + + + + + setStaged((prev) => ({ + ...prev, + genders: selected.map((s) => s.value), + })) + } + placeholder="All" + /> + { + const selectedIds = selected.map((s) => Number(s.value)); + setStaged((prev) => ({ + ...prev, + products: products.filter((p) => selectedIds.includes(p.id)), + })); + }} + placeholder="All" + /> + { + const selectedIds = selected.map((s) => Number(s.value)); + setStaged((prev) => ({ + ...prev, + categories: categories.filter((c) => selectedIds.includes(c.id)), + })); + }} + placeholder="All" + /> + { + const selectedIds = selected.map((s) => Number(s.value)); + setStaged((prev) => ({ + ...prev, + locations: locations.filter((l) => selectedIds.includes(l.id)), + })); + }} + placeholder="All" + /> + + Tags + + setStaged((prev) => ({ ...prev, includedTags: newTags })) + } + onExcludedChange={(newTags) => + setStaged((prev) => ({ ...prev, excludedTags: newTags })) + } + onClearAll={() => + setStaged((prev) => ({ ...prev, includedTags: [], excludedTags: [] })) + } + placeholder="Select tags" + /> + + + + + + + + + + + + + + ); +} diff --git a/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesDataContainer.tsx b/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesDataContainer.tsx index a3dbf7c7d..ab62c9d81 100644 --- a/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesDataContainer.tsx +++ b/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesDataContainer.tsx @@ -4,6 +4,8 @@ import { useParams } from "react-router-dom"; import ErrorCard, { predefinedErrors } from "../../ErrorCard"; import CreatedBoxesFilterContainer from "./CreatedBoxesFilterContainer"; import { graphql } from "../../../../../graphql/graphql"; +import type { BoxesOrItems } from "../../filter/BoxesOrItemsSelect"; +import type { StockAppliedFilters } from "../../../utils/dashboardFilters"; export const CREATED_BOXES_QUERY = graphql(` query createdBoxes($baseId: Int!) { @@ -37,7 +39,15 @@ export const CREATED_BOXES_QUERY = graphql(` } `); -export default function CreatedBoxesDataContainer() { +interface CreatedBoxesDataContainerProps { + appliedFilters: StockAppliedFilters; + boxesOrItems: BoxesOrItems; +} + +export default function CreatedBoxesDataContainer({ + appliedFilters, + boxesOrItems, +}: CreatedBoxesDataContainerProps) { const { baseId } = useParams(); const { data, loading, error } = useQuery(CREATED_BOXES_QUERY, { variables: { baseId: parseInt(baseId!, 10) }, @@ -52,5 +62,11 @@ export default function CreatedBoxesDataContainer() { if (data === undefined) { return ; } - return ; + return ( + + ); } diff --git a/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesFilterContainer.tsx b/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesFilterContainer.tsx index d1681cf1a..46a9e7984 100644 --- a/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesFilterContainer.tsx +++ b/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesFilterContainer.tsx @@ -1,116 +1,39 @@ import { Wrap, WrapItem, Box } from "@chakra-ui/react"; -import { useEffect, useMemo } from "react"; -import { TidyFn, distinct, filter, tidy } from "@tidyjs/tidy"; -import { useReactiveVar } from "@apollo/client"; +import { useMemo } from "react"; +import { TidyFn, filter, tidy } from "@tidyjs/tidy"; import CreatedBoxes from "./CreatedBoxes"; import { filterListByInterval } from "../../../../utils/helpers"; -import useTimerange from "../../../hooks/useTimerange"; -import useValueFilter from "../../../hooks/useValueFilter"; -import { - IBoxesOrItemsFilter, - boxesOrItemsFilterValues, - boxesOrItemsUrlId, - defaultBoxesOrItems, -} from "../../filter/BoxesOrItemsSelect"; -import { - genderFilterId, - genders, - productFilterId, - categoryFilterId, - productToFilterValue, - categoryToFilterValue, -} from "../../filter/GenderProductFilter"; -import useMultiSelectFilter from "../../../hooks/useMultiSelectFilter"; -import { tagToFilterValue } from "../../filter/TagFilter"; -import { tagFilterIncludedId, tagFilterExcludedId } from "../../filter/TabbedTagFilter"; -import { - productFilterValuesVar, - tagFilterIncludedValuesVar, - tagFilterExcludedValuesVar, - categoryFilterValuesVar, -} from "../../../state/filter"; +import type { BoxesOrItems } from "../../filter/BoxesOrItemsSelect"; +import type { StockAppliedFilters } from "../../../utils/dashboardFilters"; import { filterByTags } from "../../../utils/filterByTags"; import { CreatedBoxes as CreatedBoxesType, CreatedBoxesResult } from "../../../../../graphql/types"; interface ICreatedBoxesFilterContainerProps { createdBoxes: CreatedBoxesType; + appliedFilters: StockAppliedFilters; + boxesOrItems: BoxesOrItems; + /** Optional time interval for filtering by creation date */ + interval?: { start: Date; end: Date }; } export default function CreatedBoxesFilterContainer({ createdBoxes, + appliedFilters, + boxesOrItems, + interval, }: ICreatedBoxesFilterContainerProps) { - const { interval } = useTimerange(); - - const { filterValue } = useValueFilter( - boxesOrItemsFilterValues, - defaultBoxesOrItems, - boxesOrItemsUrlId, - ); - const productFilterValues = useReactiveVar(productFilterValuesVar); - const categoryFilterValues = useReactiveVar(categoryFilterValuesVar); - - const { filterValue: filterProductGenders } = useMultiSelectFilter(genders, genderFilterId); - const { filterValue: filterProducts } = useMultiSelectFilter( - productFilterValues, - productFilterId, - ); - const { filterValue: filterCategories } = useMultiSelectFilter( - categoryFilterValues, - categoryFilterId, - ); - - const includedTagFilterValues = useReactiveVar(tagFilterIncludedValuesVar); - const excludedTagFilterValues = useReactiveVar(tagFilterExcludedValuesVar); - const { includedFilterValue: includedTags, excludedFilterValue: excludedTags } = - useMultiSelectFilter( - includedTagFilterValues, - tagFilterIncludedId, - excludedTagFilterValues, - tagFilterExcludedId, - ); - - // use products from the createdBoxes query to feed the global products and Tags for Boxes filter - // Beneficiary and All Tags are merged inside the DemographicFilterContainer - // and filter the product filter by filtered product genders - useEffect(() => { - const p = createdBoxes?.dimensions!.product!.map((e) => productToFilterValue(e!)); - if (filterProductGenders.length > 0 && p?.length) { - productFilterValuesVar([ - ...filterProducts, - ...p.filter( - (product) => filterProductGenders.findIndex((fPG) => fPG.value === product.gender) !== -1, - ), - ]); - } else { - productFilterValuesVar(p); - } - - const c = createdBoxes?.dimensions!.category!.map((e) => categoryToFilterValue(e!)); - categoryFilterValuesVar(c); - - const boxTags = createdBoxes?.dimensions!.tag!.map((e) => tagToFilterValue(e!)); - if (boxTags?.length) { - const distinctTagFilterValues = tidy( - [...includedTagFilterValues, ...boxTags], - distinct(["id"]), - ); - - // Populate the tag filter values for both included and excluded - tagFilterIncludedValuesVar(distinctTagFilterValues); - tagFilterExcludedValuesVar(distinctTagFilterValues); - } - // we only need to update products if the product gender selection is updated - // including filterProducts would cause unnecessary rerenders - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [createdBoxes?.dimensions, filterProductGenders]); + const { products, genders, categories, includedTags, excludedTags } = appliedFilters; const createdBoxesFacts = useMemo(() => { try { - return filterListByInterval( - (createdBoxes?.facts as CreatedBoxesResult[]) ?? [], - "createdOn", - interval, - ); + if (interval) { + return filterListByInterval( + (createdBoxes?.facts as CreatedBoxesResult[]) ?? [], + "createdOn", + interval, + ); + } + return (createdBoxes?.facts as CreatedBoxesResult[]) ?? []; } catch { // TODO useError } @@ -119,29 +42,16 @@ export default function CreatedBoxesFilterContainer({ const filteredFacts = useMemo(() => { const filters: TidyFn[] = []; - if (filterProductGenders.length > 0) { - filters.push( - filter( - (fact: CreatedBoxesResult) => - filterProductGenders.find((fPG) => fPG.value === fact.gender!) !== undefined, - ), - ); + if (genders.length > 0) { + filters.push(filter((fact: CreatedBoxesResult) => genders.includes(fact.gender ?? ""))); } - if (filterProducts.length > 0) { - filters.push( - filter( - (fact: CreatedBoxesResult) => - filterProducts.find((fBP) => fBP?.id === fact.productId!) !== undefined, - ), - ); + if (products.length > 0) { + const productIds = new Set(products.map((p) => p.id)); + filters.push(filter((fact: CreatedBoxesResult) => productIds.has(fact.productId!))); } - if (filterCategories.length > 0) { - filters.push( - filter( - (fact: CreatedBoxesResult) => - filterCategories.find((fC) => fC?.id === fact.categoryId!) !== undefined, - ), - ); + if (categories.length > 0) { + const categoryIds = new Set(categories.map((c) => c.id)); + filters.push(filter((fact: CreatedBoxesResult) => categoryIds.has(fact.categoryId!))); } let filtered = createdBoxesFacts; @@ -154,14 +64,7 @@ export default function CreatedBoxesFilterContainer({ filtered = filterByTags(filtered, includedTags, excludedTags); return filtered; - }, [ - createdBoxesFacts, - filterProductGenders, - filterProducts, - filterCategories, - includedTags, - excludedTags, - ]); + }, [createdBoxesFacts, genders, products, categories, includedTags, excludedTags]); const filteredCreatedBoxesCube = { facts: filteredFacts, @@ -175,7 +78,7 @@ export default function CreatedBoxesFilterContainer({ diff --git a/shared-components/statviz/components/visualizations/demographic/DemographicDataContainer.tsx b/shared-components/statviz/components/visualizations/demographic/DemographicDataContainer.tsx index 9e1a53c78..3c15a2a82 100644 --- a/shared-components/statviz/components/visualizations/demographic/DemographicDataContainer.tsx +++ b/shared-components/statviz/components/visualizations/demographic/DemographicDataContainer.tsx @@ -6,6 +6,7 @@ import { graphql } from "../../../../../graphql/graphql"; import DemographicFilterContainer from "./DemographicFilterContainer"; import ErrorCard, { predefinedErrors } from "../../ErrorCard"; import NoDataCard from "../../NoDataCard"; +import type { DemographicsAppliedFilters } from "../../../utils/dashboardFilters"; export const DEMOGRAPHIC_QUERY = graphql(` query BeneficiaryDemographics($baseId: Int!) { @@ -28,7 +29,13 @@ export const DEMOGRAPHIC_QUERY = graphql(` } `); -export default function DemographicDataContainer() { +interface DemographicDataContainerProps { + appliedFilters: DemographicsAppliedFilters; +} + +export default function DemographicDataContainer({ + appliedFilters, +}: DemographicDataContainerProps) { const { baseId } = useParams(); const { data, loading, error } = useQuery(DEMOGRAPHIC_QUERY, { variables: { baseId: parseInt(baseId!, 10) }, @@ -51,5 +58,10 @@ export default function DemographicDataContainer() { /> ); } - return ; + return ( + + ); } diff --git a/shared-components/statviz/components/visualizations/demographic/DemographicFilterContainer.tsx b/shared-components/statviz/components/visualizations/demographic/DemographicFilterContainer.tsx index 023ec4416..fcf5bb800 100644 --- a/shared-components/statviz/components/visualizations/demographic/DemographicFilterContainer.tsx +++ b/shared-components/statviz/components/visualizations/demographic/DemographicFilterContainer.tsx @@ -1,14 +1,8 @@ -import { useReactiveVar } from "@apollo/client"; -import { useEffect, useMemo } from "react"; -import { distinct, tidy } from "@tidyjs/tidy"; +import { useMemo } from "react"; import DemographicCharts from "./DemographicCharts"; -import { tagToFilterValue } from "../../filter/TagFilter"; -import { tagFilterIncludedId, tagFilterExcludedId } from "../../filter/TabbedTagFilter"; -import useTimerange from "../../../hooks/useTimerange"; -import { filterListByInterval } from "../../../../utils/helpers"; -import { tagFilterIncludedValuesVar, tagFilterExcludedValuesVar } from "../../../state/filter"; -import useMultiSelectFilter from "../../../hooks/useMultiSelectFilter"; import { filterByTags } from "../../../utils/filterByTags"; +import type { DemographicsAppliedFilters } from "../../../utils/dashboardFilters"; +import { AGE_RANGES } from "../../../utils/dashboardFilters"; import { BeneficiaryDemographics, BeneficiaryDemographicsResult, @@ -16,63 +10,45 @@ import { interface IDemographicFilterContainerProps { demographics: BeneficiaryDemographics; + appliedFilters: DemographicsAppliedFilters; } export default function DemographicFilterContainer({ demographics, + appliedFilters, }: IDemographicFilterContainerProps) { - const { interval } = useTimerange(); + const { ageRanges, genders, includedTags, excludedTags } = appliedFilters; - const includedTagFilterValues = useReactiveVar(tagFilterIncludedValuesVar); - const excludedTagFilterValues = useReactiveVar(tagFilterExcludedValuesVar); - const { includedFilterValue: includedTags, excludedFilterValue: excludedTags } = - useMultiSelectFilter( - includedTagFilterValues, - tagFilterIncludedId, - excludedTagFilterValues, - tagFilterExcludedId, - ); + const demographicFacts = useMemo( + () => (demographics?.facts as BeneficiaryDemographicsResult[]) ?? [], + [demographics?.facts], + ); - // merge Beneficiary tags to Box and All tags - useEffect(() => { - const beneficiaryTagFilterValues = demographics?.dimensions?.tag?.map((e) => - tagToFilterValue(e!), - ); - - if ((beneficiaryTagFilterValues?.length ?? 0) > 0) { - const distinctTagFilterValues = tidy( - [...includedTagFilterValues, ...beneficiaryTagFilterValues!], - distinct(["id"]), - ); - - // Populate the tag filter values for both included and excluded - tagFilterIncludedValuesVar(distinctTagFilterValues); - tagFilterExcludedValuesVar(distinctTagFilterValues); + const filteredFacts = useMemo(() => { + let filtered = demographicFacts; + + // Filter by age ranges + if (ageRanges.length > 0) { + filtered = filtered.filter((fact) => { + if (fact.age === null || fact.age === undefined) return false; + return ageRanges.some((rangeLabel) => { + const range = AGE_RANGES.find((r) => r.label === rangeLabel); + if (!range) return false; + return fact.age! >= range.min && fact.age! <= range.max; + }); + }); } - // including tagFilterOptions in the dependencies can lead to infinite update loops - // between CreatedBoxes updating the TagFilter and DemographicFilter updating the TagFilter - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [demographics?.dimensions]); - const demographicFacts = useMemo(() => { - try { - return filterListByInterval( - (demographics?.facts as BeneficiaryDemographicsResult[]) ?? [], - "createdOn", - interval, - ) as BeneficiaryDemographicsResult[]; - } catch { - // TODO useError + // Filter by human gender + if (genders.length > 0) { + filtered = filtered.filter((fact) => genders.includes(fact.gender ?? "")); } - return []; - }, [demographics?.facts, interval]); - const filteredFacts = useMemo(() => { // Apply tag filter (included/excluded) - const filtered = filterByTags(demographicFacts, includedTags, excludedTags); + filtered = filterByTags(filtered, includedTags, excludedTags); return filtered; - }, [demographicFacts, includedTags, excludedTags]); + }, [demographicFacts, ageRanges, genders, includedTags, excludedTags]); const demographicCube = { ...demographics, diff --git a/shared-components/statviz/components/visualizations/movedBoxes/MovedBoxes.test.tsx b/shared-components/statviz/components/visualizations/movedBoxes/MovedBoxes.test.tsx index fc8e70f80..5cef37e16 100644 --- a/shared-components/statviz/components/visualizations/movedBoxes/MovedBoxes.test.tsx +++ b/shared-components/statviz/components/visualizations/movedBoxes/MovedBoxes.test.tsx @@ -2,6 +2,7 @@ import { it, expect } from "vitest"; import MovedBoxesDataContainer, { MOVED_BOXES_QUERY } from "./MovedBoxesDataContainer"; import { render, screen } from "../../../../tests/testUtils"; import { GraphQLError } from "graphql"; +import { defaultMovementFilters } from "../../../utils/dashboardFilters"; export class FakeGraphQLError extends GraphQLError { constructor(errorCode?: string, errorDescription?: string) { @@ -42,11 +43,17 @@ const movedBoxesDataTests = [ movedBoxesDataTests.forEach(({ name, mocks, alert }) => { it(name, async () => { - render(, { - routePath: "/bases/:baseId/", - initialUrl: "/bases/1/", - mocks, - }); + render( + , + { + routePath: "/bases/:baseId/", + initialUrl: "/bases/1/", + mocks, + }, + ); expect(await screen.findByText(alert)).toBeInTheDocument(); }); diff --git a/shared-components/statviz/components/visualizations/movedBoxes/MovedBoxesDataContainer.tsx b/shared-components/statviz/components/visualizations/movedBoxes/MovedBoxesDataContainer.tsx index a7aa59f01..a7ec8050d 100644 --- a/shared-components/statviz/components/visualizations/movedBoxes/MovedBoxesDataContainer.tsx +++ b/shared-components/statviz/components/visualizations/movedBoxes/MovedBoxesDataContainer.tsx @@ -4,6 +4,8 @@ import { Box, Spinner } from "@chakra-ui/react"; import MovedBoxesFilterContainer from "./MovedBoxesFilterContainer"; import ErrorCard, { predefinedErrors } from "../../ErrorCard"; import { graphql } from "../../../../../graphql/graphql"; +import type { BoxesOrItems } from "../../filter/BoxesOrItemsSelect"; +import type { MovementAppliedFilters } from "../../../utils/dashboardFilters"; export const MOVED_BOXES_QUERY = graphql(` query movedBoxes($baseId: Int!) { @@ -34,10 +36,18 @@ export const MOVED_BOXES_QUERY = graphql(` } `); +interface MovedBoxesDataContainerProps { + appliedFilters: MovementAppliedFilters; + boxesOrItems: BoxesOrItems; +} + // The data wrapper collects data and passes it to the filter-wrapper // which applys filters to the data // the filter wrapper passes it to the Chart which maps the Datacube to a VisX or Nivo Chart -export default function MovedBoxesDataContainer() { +export default function MovedBoxesDataContainer({ + appliedFilters, + boxesOrItems, +}: MovedBoxesDataContainerProps) { const { baseId } = useParams(); const { data, loading, error } = useQuery(MOVED_BOXES_QUERY, { variables: { baseId: parseInt(baseId!, 10) }, @@ -52,5 +62,11 @@ export default function MovedBoxesDataContainer() { if (data === undefined) { return ; } - return ; + return ( + + ); } diff --git a/shared-components/statviz/components/visualizations/movedBoxes/MovedBoxesFilterContainer.tsx b/shared-components/statviz/components/visualizations/movedBoxes/MovedBoxesFilterContainer.tsx index 908b75bbb..8afdebd5e 100644 --- a/shared-components/statviz/components/visualizations/movedBoxes/MovedBoxesFilterContainer.tsx +++ b/shared-components/statviz/components/visualizations/movedBoxes/MovedBoxesFilterContainer.tsx @@ -1,79 +1,34 @@ -import { useEffect, useMemo } from "react"; -import { useReactiveVar } from "@apollo/client"; +import { useMemo } from "react"; import { TidyFn, filter, tidy } from "@tidyjs/tidy"; -import useTimerange from "../../../hooks/useTimerange"; import { filterListByInterval } from "../../../../utils/helpers"; import MovedBoxesCharts from "./MovedBoxesCharts"; -import useValueFilter from "../../../hooks/useValueFilter"; -import { - boxesOrItemsFilterValues, - boxesOrItemsUrlId, - defaultBoxesOrItems, -} from "../../filter/BoxesOrItemsSelect"; -import useMultiSelectFilter from "../../../hooks/useMultiSelectFilter"; -import { - genderFilterId, - genders, - productFilterId, - categoryFilterId, -} from "../../filter/GenderProductFilter"; -import { - targetFilterValuesVar, - productFilterValuesVar, - tagFilterIncludedValuesVar, - tagFilterExcludedValuesVar, - categoryFilterValuesVar, -} from "../../../state/filter"; -import { tagFilterIncludedId, tagFilterExcludedId } from "../../filter/TabbedTagFilter"; +import type { BoxesOrItems } from "../../filter/BoxesOrItemsSelect"; +import type { MovementAppliedFilters } from "../../../utils/dashboardFilters"; import { filterByTags } from "../../../utils/filterByTags"; -import { targetFilterId, targetToFilterValue } from "../../filter/LocationFilter"; import { MovedBoxes, MovedBoxesResult } from "../../../../../graphql/types"; interface IMovedBoxesFilterContainerProps { movedBoxes: MovedBoxes; + appliedFilters: MovementAppliedFilters; + boxesOrItems: BoxesOrItems; } -export default function MovedBoxesFilterContainer({ movedBoxes }: IMovedBoxesFilterContainerProps) { - const { interval } = useTimerange(); +export default function MovedBoxesFilterContainer({ + movedBoxes, + appliedFilters, + boxesOrItems, +}: IMovedBoxesFilterContainerProps) { + const { products, genders, categories, includedTags, excludedTags, dateFrom, dateTo } = + appliedFilters; - const { filterValue } = useValueFilter( - boxesOrItemsFilterValues, - defaultBoxesOrItems, - boxesOrItemsUrlId, + const interval = useMemo( + () => ({ + start: new Date(dateFrom), + end: new Date(dateTo), + }), + [dateFrom, dateTo], ); - const productsFilterValues = useReactiveVar(productFilterValuesVar); - const targetFilterValues = useReactiveVar(targetFilterValuesVar); - const categoryFilterValues = useReactiveVar(categoryFilterValuesVar); - - const { filterValue: productsFilter } = useMultiSelectFilter( - productsFilterValues, - productFilterId, - ); - - const { filterValue: genderFilter } = useMultiSelectFilter(genders, genderFilterId); - const { filterValue: excludedTargets } = useMultiSelectFilter(targetFilterValues, targetFilterId); - const { filterValue: filterCategories } = useMultiSelectFilter( - categoryFilterValues, - categoryFilterId, - ); - - const includedTagFilterValues = useReactiveVar(tagFilterIncludedValuesVar); - const excludedTagFilterValues = useReactiveVar(tagFilterExcludedValuesVar); - const { includedFilterValue: includedTags, excludedFilterValue: excludedTags } = - useMultiSelectFilter( - includedTagFilterValues, - tagFilterIncludedId, - excludedTagFilterValues, - tagFilterExcludedId, - ); - - // fill target filter with data - useEffect(() => { - const targets = movedBoxes?.dimensions?.target?.map((t) => targetToFilterValue(t!)) ?? []; - targetFilterValuesVar(targets); - }, [movedBoxes?.dimensions]); - const movedBoxesFacts = useMemo(() => { try { return filterListByInterval(movedBoxes?.facts! as MovedBoxesResult[], "movedOn", interval); @@ -85,40 +40,23 @@ export default function MovedBoxesFilterContainer({ movedBoxes }: IMovedBoxesFil const filteredFacts = useMemo(() => { const filters: TidyFn[] = []; - if (genderFilter.length > 0) { + if (genders.length > 0) { filters.push( - filter( - (fact: MovedBoxesResult) => - genderFilter.find((fPG) => fPG.value === fact.gender?.valueOf()) !== undefined, - ), + filter((fact: MovedBoxesResult) => genders.includes(fact.gender?.valueOf() ?? "")), ); } - if (productsFilter.length > 0) { + if (products.length > 0) { filters.push( - filter( - (fact: MovedBoxesResult) => - productsFilter.find( - (fBP) => fBP?.name.toLowerCase() === fact.productName! && fBP.gender === fact.gender, - ) !== undefined, + filter((fact: MovedBoxesResult) => + products.some( + (p) => p.name.toLowerCase() === fact.productName! && p.gender === fact.gender, + ), ), ); } - if (filterCategories.length > 0) { - filters.push( - filter( - (fact: MovedBoxesResult) => - filterCategories.find((fC) => fC?.id === fact.categoryId!) !== undefined, - ), - ); - } - if (excludedTargets.length > 0) { - filters.push( - filter( - (fact: MovedBoxesResult) => - excludedTargets.find((filteredTarget) => filteredTarget.id! === fact.targetId!) === - undefined, - ), - ); + if (categories.length > 0) { + const categoryIds = new Set(categories.map((c) => c.id)); + filters.push(filter((fact: MovedBoxesResult) => categoryIds.has(fact.categoryId!))); } let filtered = movedBoxesFacts; @@ -131,19 +69,11 @@ export default function MovedBoxesFilterContainer({ movedBoxes }: IMovedBoxesFil filtered = filterByTags(filtered, includedTags, excludedTags); return filtered; - }, [ - excludedTargets, - genderFilter, - movedBoxesFacts, - productsFilter, - filterCategories, - includedTags, - excludedTags, - ]); + }, [movedBoxesFacts, genders, products, categories, includedTags, excludedTags]); const filteredMovedBoxesCube = { facts: filteredFacts, dimensions: movedBoxes?.dimensions, }; - return ; + return ; } diff --git a/shared-components/statviz/dashboard/Dashboard.tsx b/shared-components/statviz/dashboard/Dashboard.tsx index eba4bb285..e75bb210f 100644 --- a/shared-components/statviz/dashboard/Dashboard.tsx +++ b/shared-components/statviz/dashboard/Dashboard.tsx @@ -1,87 +1,115 @@ -import { Accordion, Center, Heading, Wrap, WrapItem } from "@chakra-ui/react"; -import TimeRangeSelect from "../components/filter/TimeRangeSelect"; +import { Accordion, Heading, Spinner } from "@chakra-ui/react"; +import { useQuery } from "@apollo/client"; +import { useParams } from "react-router-dom"; +import { useMemo } from "react"; import Demographics from "./Demographics"; import MovedBoxes from "./MovedBoxes"; import ItemsAndBoxes from "./ItemsAndBoxes"; -import BoxesOrItemsSelect, { - boxesOrItemsFilterValues, -} from "../components/filter/BoxesOrItemsSelect"; -import GenderProductFilter from "../components/filter/GenderProductFilter"; -import TabbedTagFilter from "../components/filter/TabbedTagFilter"; -import { useSearchParams } from "react-router-dom"; -import { useEffect } from "react"; -import { date2String } from "../../utils/helpers"; -import { subMonths } from "date-fns"; import InfoText from "./InfoText"; +import { graphql } from "../../../graphql/graphql"; +import ErrorCard from "../components/ErrorCard"; +import type { + IProductOption, + ICategoryOption, + ILocationOption, + ITagOption, +} from "../utils/dashboardFilters"; + +export const DASHBOARD_FILTER_DATA_QUERY = graphql(` + query DashboardFilterData($baseId: ID!) { + base(id: $baseId) { + products { + id + name + gender + category { + id + name + } + } + locations { + id + name + } + tags { + id + name + color + } + } + } +`); export default function Dashboard() { - const [searchParams, setSearchParams] = useSearchParams(); + const { baseId } = useParams(); + const { data, loading, error } = useQuery(DASHBOARD_FILTER_DATA_QUERY, { + variables: { baseId: baseId! }, + }); - // set default filter states - // TODO: this is a quick fix and we should revisit the state handling of filters since it seems to be all over the place. - useEffect(() => { - const currentQuery = searchParams.toString(); - const newSearchParams = searchParams; + const products = useMemo( + () => + (data?.base?.products ?? []).map((p) => ({ + id: Number(p.id), + name: p.name, + gender: p.gender ?? null, + })), + [data], + ); - // TimeRangeFilter - if (!searchParams.get("from")) { - newSearchParams.append("from", date2String(subMonths(new Date(), 3))); - } - if (!searchParams.get("to")) { - newSearchParams.append("to", date2String(new Date())); - } - if (!searchParams.get("boi")) { - newSearchParams.append("boi", boxesOrItemsFilterValues[0].urlId); + const categories = useMemo(() => { + const seen = new Set(); + const result: ICategoryOption[] = []; + for (const product of data?.base?.products ?? []) { + const catId = Number(product.category.id); + if (!seen.has(catId)) { + seen.add(catId); + result.push({ id: catId, name: product.category.name }); + } } + return result; + }, [data]); - if (newSearchParams.toString() !== currentQuery) { - setSearchParams(newSearchParams); - } - }, [searchParams, setSearchParams]); + const locations = useMemo( + () => + (data?.base?.locations ?? []).map((l) => ({ + id: Number(l.id), + name: l.name ?? "", + })), + [data], + ); + + const tags = useMemo( + () => + (data?.base?.tags ?? []).map((t) => ({ + id: Number(t.id), + name: t.name, + color: t.color ?? "#999", + value: String(t.id), + label: t.name, + urlId: String(t.id), + })), + [data], + ); + + if (error) { + return ; + } return (
Dashboard - - - -
- -
-
- -
- -
-
- -
- -
-
- -
- -
-
-
+ {loading && } - - - + + +
); diff --git a/shared-components/statviz/dashboard/Demographics.tsx b/shared-components/statviz/dashboard/Demographics.tsx index f5c10e635..86d3f4395 100644 --- a/shared-components/statviz/dashboard/Demographics.tsx +++ b/shared-components/statviz/dashboard/Demographics.tsx @@ -7,22 +7,61 @@ import { Wrap, WrapItem, Box, + HStack, } from "@chakra-ui/react"; +import { useCallback, useMemo } from "react"; +import { useSearchParams } from "react-router-dom"; import DemographicDataContainer from "../components/visualizations/demographic/DemographicDataContainer"; +import DemographicsFilterPanel from "../components/filter/DemographicsFilterPanel"; +import { + readDemographicsFiltersFromUrl, + writeDemographicsFiltersToUrl, + type DemographicsAppliedFilters, + type ITagOption, +} from "../utils/dashboardFilters"; + +interface DemographicsProps { + tags: ITagOption[]; +} + +export default function Demographics({ tags }: DemographicsProps) { + const [searchParams, setSearchParams] = useSearchParams(); + + const appliedFilters = useMemo( + () => readDemographicsFiltersFromUrl(searchParams, tags), + // We intentionally only re-derive when URL params change, not when option arrays change + // eslint-disable-next-line react-hooks/exhaustive-deps + [searchParams], + ); + + const handleApplyFilters = useCallback( + (filters: DemographicsAppliedFilters) => { + const newParams = new URLSearchParams(searchParams); + writeDemographicsFiltersToUrl(filters, newParams); + setSearchParams(newParams); + }, + [searchParams, setSearchParams], + ); -export default function Demographics() { return ( Beneficiary Overview + e.stopPropagation()} mr={2}> + + - + diff --git a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx index 1f158c500..a83ee54ce 100644 --- a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx +++ b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx @@ -5,22 +5,94 @@ import { Heading, AccordionIcon, AccordionPanel, + HStack, + Select, } from "@chakra-ui/react"; +import { useCallback, useMemo } from "react"; +import { useSearchParams } from "react-router-dom"; import CreatedBoxesDataContainer from "../components/visualizations/createdBoxes/CreatedBoxesDataContainer"; +import StockFilterPanel from "../components/filter/StockFilterPanel"; +import { + STOCK_URL_PARAMS, + readStockFiltersFromUrl, + writeStockFiltersToUrl, + type StockAppliedFilters, + type IProductOption, + type ICategoryOption, + type ILocationOption, + type ITagOption, +} from "../utils/dashboardFilters"; +import type { BoxesOrItems } from "../components/filter/BoxesOrItemsSelect"; export type BoxesOrItemsCount = "boxesCount" | "itemsCount"; -export default function ItemsAndBoxes() { +interface ItemsAndBoxesProps { + products: IProductOption[]; + categories: ICategoryOption[]; + locations: ILocationOption[]; + tags: ITagOption[]; +} + +export default function ItemsAndBoxes({ + products, + categories, + locations, + tags, +}: ItemsAndBoxesProps) { + const [searchParams, setSearchParams] = useSearchParams(); + + const appliedFilters = useMemo( + () => readStockFiltersFromUrl(searchParams, products, categories, locations, tags), + // We intentionally only re-derive when URL params change, not when option arrays change + // eslint-disable-next-line react-hooks/exhaustive-deps + [searchParams], + ); + + const boxesOrItems: BoxesOrItems = + searchParams.get(STOCK_URL_PARAMS.boxesOrItems) === "ic" ? "itemsCount" : "boxesCount"; + + const handleApplyFilters = useCallback( + (filters: StockAppliedFilters) => { + const newParams = new URLSearchParams(searchParams); + writeStockFiltersToUrl(filters, newParams); + setSearchParams(newParams); + }, + [searchParams, setSearchParams], + ); + + const handleBoxesOrItemsChange = useCallback( + (e: React.ChangeEvent) => { + const newParams = new URLSearchParams(searchParams); + newParams.set(STOCK_URL_PARAMS.boxesOrItems, e.target.value === "itemsCount" ? "ic" : "bc"); + setSearchParams(newParams); + }, + [searchParams, setSearchParams], + ); + return ( Stock Overview + e.stopPropagation()} mr={2}> + + + - + ); diff --git a/shared-components/statviz/dashboard/MovedBoxes.tsx b/shared-components/statviz/dashboard/MovedBoxes.tsx index c82271a8b..1aaa13ed5 100644 --- a/shared-components/statviz/dashboard/MovedBoxes.tsx +++ b/shared-components/statviz/dashboard/MovedBoxes.tsx @@ -5,20 +5,87 @@ import { AccordionIcon, AccordionPanel, Box, + HStack, + Select, } from "@chakra-ui/react"; +import { useCallback, useMemo } from "react"; +import { useSearchParams } from "react-router-dom"; import MovedBoxesDataContainer from "../components/visualizations/movedBoxes/MovedBoxesDataContainer"; +import MovementFilterPanel from "../components/filter/MovementFilterPanel"; +import { + MOVEMENT_URL_PARAMS, + readMovementFiltersFromUrl, + writeMovementFiltersToUrl, + type MovementAppliedFilters, + type IProductOption, + type ICategoryOption, + type ITagOption, +} from "../utils/dashboardFilters"; +import type { BoxesOrItems } from "../components/filter/BoxesOrItemsSelect"; + +interface MovedBoxesProps { + products: IProductOption[]; + categories: ICategoryOption[]; + tags: ITagOption[]; +} + +export default function MovedBoxes({ products, categories, tags }: MovedBoxesProps) { + const [searchParams, setSearchParams] = useSearchParams(); + + const appliedFilters = useMemo( + () => readMovementFiltersFromUrl(searchParams, products, categories, tags), + // We intentionally only re-derive when URL params change, not when option arrays change + // eslint-disable-next-line react-hooks/exhaustive-deps + [searchParams], + ); + + const boxesOrItems: BoxesOrItems = + searchParams.get(MOVEMENT_URL_PARAMS.boxesOrItems) === "ic" ? "itemsCount" : "boxesCount"; + + const handleApplyFilters = useCallback( + (filters: MovementAppliedFilters) => { + const newParams = new URLSearchParams(searchParams); + writeMovementFiltersToUrl(filters, newParams); + setSearchParams(newParams); + }, + [searchParams, setSearchParams], + ); + + const handleBoxesOrItemsChange = useCallback( + (e: React.ChangeEvent) => { + const newParams = new URLSearchParams(searchParams); + newParams.set( + MOVEMENT_URL_PARAMS.boxesOrItems, + e.target.value === "itemsCount" ? "ic" : "bc", + ); + setSearchParams(newParams); + }, + [searchParams, setSearchParams], + ); -export default function MovedBoxes() { return ( Movement History + e.stopPropagation()} mr={2}> + + + - + ); diff --git a/shared-components/statviz/utils/dashboardFilters.ts b/shared-components/statviz/utils/dashboardFilters.ts new file mode 100644 index 000000000..9f3b1e34f --- /dev/null +++ b/shared-components/statviz/utils/dashboardFilters.ts @@ -0,0 +1,375 @@ +import { date2String } from "../../utils/helpers"; +import { subMonths } from "date-fns"; +import { ProductGender } from "../../../graphql/types"; + +// --------------------------------------------------------------------------- +// Option types for filter dropdowns +// --------------------------------------------------------------------------- + +export interface IProductOption { + id: number; + name: string; + gender: ProductGender | null; +} + +export interface ICategoryOption { + id: number; + name: string; +} + +export interface ILocationOption { + id: number; + name: string; +} + +/** + * Tag option for dashboard filters. + * Structurally compatible with ITagFilterValue so it can be used directly + * with TabbedTagDropdown. + */ +export interface ITagOption { + id: number; + /** Same as label; populated when created from backend data. */ + name?: string; + color: string; + /** = String(id) — required by chakra-react-select */ + value: string; + /** = name — required by react-select for display */ + label: string; + /** = String(id) — used for URL serialisation */ + urlId: string; +} + +// --------------------------------------------------------------------------- +// Applied filter state types (one per dashboard section) +// --------------------------------------------------------------------------- + +export interface StockAppliedFilters { + products: IProductOption[]; + genders: string[]; + categories: ICategoryOption[]; + locations: ILocationOption[]; + includedTags: ITagOption[]; + excludedTags: ITagOption[]; +} + +export interface MovementAppliedFilters { + dateFrom: string; + dateTo: string; + products: IProductOption[]; + genders: string[]; + categories: ICategoryOption[]; + includedTags: ITagOption[]; + excludedTags: ITagOption[]; +} + +export interface DemographicsAppliedFilters { + /** e.g. ["0-7", "8-15"] */ + ageRanges: string[]; + /** HumanGender values: "Male", "Female", "Diverse" */ + genders: string[]; + includedTags: ITagOption[]; + excludedTags: ITagOption[]; +} + +// --------------------------------------------------------------------------- +// Age range definitions for demographics filter +// --------------------------------------------------------------------------- + +export interface IAgeRange { + label: string; + min: number; + max: number; +} + +export const AGE_RANGES: IAgeRange[] = [ + { label: "0-7", min: 0, max: 7 }, + { label: "8-15", min: 8, max: 15 }, + { label: "16-25", min: 16, max: 25 }, + { label: "26-40", min: 26, max: 40 }, + { label: "41-65", min: 41, max: 65 }, + { label: "66+", min: 66, max: Infinity }, +]; + +// --------------------------------------------------------------------------- +// URL parameter names per section +// --------------------------------------------------------------------------- + +export const STOCK_URL_PARAMS = { + products: "sp", + genders: "sg", + categories: "sc", + locations: "sl", + includedTags: "st", + excludedTags: "snt", + boxesOrItems: "sboi", +} as const; + +export const MOVEMENT_URL_PARAMS = { + dateFrom: "md1", + dateTo: "md2", + products: "mp", + genders: "mg", + categories: "mc", + includedTags: "mt", + excludedTags: "mnt", + boxesOrItems: "mboi", +} as const; + +export const DEMOGRAPHICS_URL_PARAMS = { + ageRanges: "ba", + genders: "bg", + includedTags: "bt", + excludedTags: "bnt", +} as const; + +// --------------------------------------------------------------------------- +// Default filters +// --------------------------------------------------------------------------- + +export const DEFAULT_STOCK_FILTERS: StockAppliedFilters = { + products: [], + genders: [], + categories: [], + locations: [], + includedTags: [], + excludedTags: [], +}; + +export const DEFAULT_DEMOGRAPHICS_FILTERS: DemographicsAppliedFilters = { + ageRanges: [], + genders: [], + includedTags: [], + excludedTags: [], +}; + +export function defaultMovementFilters(): MovementAppliedFilters { + return { + dateFrom: date2String(subMonths(new Date(), 3)), + dateTo: date2String(new Date()), + products: [], + genders: [], + categories: [], + includedTags: [], + excludedTags: [], + }; +} + +// --------------------------------------------------------------------------- +// URL serialization / deserialization helpers +// --------------------------------------------------------------------------- + +/** Parse a comma-separated list of positive integers. */ +export function parseIdsParam(value: string | null): number[] { + if (!value) return []; + return value + .split(",") + .map(Number) + .filter((n) => !isNaN(n) && n > 0); +} + +/** Parse a comma-separated list of non-empty strings. */ +export function parseValuesParam(value: string | null): string[] { + if (!value) return []; + return value.split(",").filter(Boolean); +} + +/** Serialize array of numbers to a URL param value (undefined if empty). */ +export function serializeIds(ids: number[]): string | undefined { + return ids.length === 0 ? undefined : ids.join(","); +} + +/** Serialize array of strings to a URL param value (undefined if empty). */ +export function serializeValues(values: string[]): string | undefined { + return values.length === 0 ? undefined : values.join(","); +} + +// --------------------------------------------------------------------------- +// Resolver helpers — look up objects from option arrays by ID or value +// --------------------------------------------------------------------------- + +export function resolveProductIds(ids: number[], allProducts: IProductOption[]): IProductOption[] { + return allProducts.filter((p) => ids.includes(p.id)); +} + +export function resolveCategoryIds( + ids: number[], + allCategories: ICategoryOption[], +): ICategoryOption[] { + return allCategories.filter((c) => ids.includes(c.id)); +} + +export function resolveLocationIds( + ids: number[], + allLocations: ILocationOption[], +): ILocationOption[] { + return allLocations.filter((l) => ids.includes(l.id)); +} + +export function resolveTagIds(ids: number[], allTags: ITagOption[]): ITagOption[] { + return allTags.filter((t) => ids.includes(t.id)); +} + +// --------------------------------------------------------------------------- +// Read applied filters from URL search params +// --------------------------------------------------------------------------- + +export function readStockFiltersFromUrl( + searchParams: URLSearchParams, + allProducts: IProductOption[], + allCategories: ICategoryOption[], + allLocations: ILocationOption[], + allTags: ITagOption[], +): StockAppliedFilters { + return { + products: resolveProductIds( + parseIdsParam(searchParams.get(STOCK_URL_PARAMS.products)), + allProducts, + ), + genders: parseValuesParam(searchParams.get(STOCK_URL_PARAMS.genders)), + categories: resolveCategoryIds( + parseIdsParam(searchParams.get(STOCK_URL_PARAMS.categories)), + allCategories, + ), + locations: resolveLocationIds( + parseIdsParam(searchParams.get(STOCK_URL_PARAMS.locations)), + allLocations, + ), + includedTags: resolveTagIds( + parseIdsParam(searchParams.get(STOCK_URL_PARAMS.includedTags)), + allTags, + ), + excludedTags: resolveTagIds( + parseIdsParam(searchParams.get(STOCK_URL_PARAMS.excludedTags)), + allTags, + ), + }; +} + +export function readMovementFiltersFromUrl( + searchParams: URLSearchParams, + allProducts: IProductOption[], + allCategories: ICategoryOption[], + allTags: ITagOption[], +): MovementAppliedFilters { + const defaults = defaultMovementFilters(); + return { + dateFrom: searchParams.get(MOVEMENT_URL_PARAMS.dateFrom) ?? defaults.dateFrom, + dateTo: searchParams.get(MOVEMENT_URL_PARAMS.dateTo) ?? defaults.dateTo, + products: resolveProductIds( + parseIdsParam(searchParams.get(MOVEMENT_URL_PARAMS.products)), + allProducts, + ), + genders: parseValuesParam(searchParams.get(MOVEMENT_URL_PARAMS.genders)), + categories: resolveCategoryIds( + parseIdsParam(searchParams.get(MOVEMENT_URL_PARAMS.categories)), + allCategories, + ), + includedTags: resolveTagIds( + parseIdsParam(searchParams.get(MOVEMENT_URL_PARAMS.includedTags)), + allTags, + ), + excludedTags: resolveTagIds( + parseIdsParam(searchParams.get(MOVEMENT_URL_PARAMS.excludedTags)), + allTags, + ), + }; +} + +export function readDemographicsFiltersFromUrl( + searchParams: URLSearchParams, + allTags: ITagOption[], +): DemographicsAppliedFilters { + return { + ageRanges: parseValuesParam(searchParams.get(DEMOGRAPHICS_URL_PARAMS.ageRanges)), + genders: parseValuesParam(searchParams.get(DEMOGRAPHICS_URL_PARAMS.genders)), + includedTags: resolveTagIds( + parseIdsParam(searchParams.get(DEMOGRAPHICS_URL_PARAMS.includedTags)), + allTags, + ), + excludedTags: resolveTagIds( + parseIdsParam(searchParams.get(DEMOGRAPHICS_URL_PARAMS.excludedTags)), + allTags, + ), + }; +} + +// --------------------------------------------------------------------------- +// Write applied filters to URL search params (mutates the passed URLSearchParams) +// --------------------------------------------------------------------------- + +function setOrDelete(params: URLSearchParams, key: string, value: string | undefined) { + params.delete(key); + if (value !== undefined) params.set(key, value); +} + +export function writeStockFiltersToUrl( + filters: StockAppliedFilters, + params: URLSearchParams, +): void { + setOrDelete(params, STOCK_URL_PARAMS.products, serializeIds(filters.products.map((p) => p.id))); + setOrDelete(params, STOCK_URL_PARAMS.genders, serializeValues(filters.genders)); + setOrDelete( + params, + STOCK_URL_PARAMS.categories, + serializeIds(filters.categories.map((c) => c.id)), + ); + setOrDelete(params, STOCK_URL_PARAMS.locations, serializeIds(filters.locations.map((l) => l.id))); + setOrDelete( + params, + STOCK_URL_PARAMS.includedTags, + serializeIds(filters.includedTags.map((t) => t.id)), + ); + setOrDelete( + params, + STOCK_URL_PARAMS.excludedTags, + serializeIds(filters.excludedTags.map((t) => t.id)), + ); +} + +export function writeMovementFiltersToUrl( + filters: MovementAppliedFilters, + params: URLSearchParams, +): void { + setOrDelete(params, MOVEMENT_URL_PARAMS.dateFrom, filters.dateFrom || undefined); + setOrDelete(params, MOVEMENT_URL_PARAMS.dateTo, filters.dateTo || undefined); + setOrDelete( + params, + MOVEMENT_URL_PARAMS.products, + serializeIds(filters.products.map((p) => p.id)), + ); + setOrDelete(params, MOVEMENT_URL_PARAMS.genders, serializeValues(filters.genders)); + setOrDelete( + params, + MOVEMENT_URL_PARAMS.categories, + serializeIds(filters.categories.map((c) => c.id)), + ); + setOrDelete( + params, + MOVEMENT_URL_PARAMS.includedTags, + serializeIds(filters.includedTags.map((t) => t.id)), + ); + setOrDelete( + params, + MOVEMENT_URL_PARAMS.excludedTags, + serializeIds(filters.excludedTags.map((t) => t.id)), + ); +} + +export function writeDemographicsFiltersToUrl( + filters: DemographicsAppliedFilters, + params: URLSearchParams, +): void { + setOrDelete(params, DEMOGRAPHICS_URL_PARAMS.ageRanges, serializeValues(filters.ageRanges)); + setOrDelete(params, DEMOGRAPHICS_URL_PARAMS.genders, serializeValues(filters.genders)); + setOrDelete( + params, + DEMOGRAPHICS_URL_PARAMS.includedTags, + serializeIds(filters.includedTags.map((t) => t.id)), + ); + setOrDelete( + params, + DEMOGRAPHICS_URL_PARAMS.excludedTags, + serializeIds(filters.excludedTags.map((t) => t.id)), + ); +} diff --git a/shared-components/statviz/utils/filterByTags.ts b/shared-components/statviz/utils/filterByTags.ts index fe73f9137..3dc7161f3 100644 --- a/shared-components/statviz/utils/filterByTags.ts +++ b/shared-components/statviz/utils/filterByTags.ts @@ -1,5 +1,3 @@ -import { ITagFilterValue } from "../state/filter"; - /** * Utility function to filter data by included and excluded tags. * @@ -15,8 +13,8 @@ import { ITagFilterValue } from "../state/filter"; */ export function filterByTags( data: T[], - includedTags: ITagFilterValue[], - excludedTags: ITagFilterValue[], + includedTags: { id: number }[], + excludedTags: { id: number }[], ): T[] { return data.filter((item) => { const itemTagIds = item.tagIds || []; From 1488a2f41b40115f78cfe148a40fefcc9ef04d59 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:09:38 +0000 Subject: [PATCH 07/30] refactor: use FilterPanel + *Filters pattern in statviz, move controls to AccordionPanel Co-authored-by: pylipp <10617122+pylipp@users.noreply.github.com> --- .../filter/DemographicsFilterPanel.tsx | 155 ++---------- .../components/filter/DemographicsFilters.tsx | 124 ++++++++++ .../statviz/components/filter/FilterPanel.tsx | 54 +++++ .../components/filter/MovementFilterPanel.tsx | 224 ++---------------- .../components/filter/MovementFilters.tsx | 194 +++++++++++++++ .../components/filter/StockFilterPanel.tsx | 218 ++--------------- .../components/filter/StockFilters.tsx | 190 +++++++++++++++ .../statviz/dashboard/Demographics.tsx | 22 +- .../statviz/dashboard/ItemsAndBoxes.tsx | 38 +-- .../statviz/dashboard/MovedBoxes.tsx | 36 +-- 10 files changed, 677 insertions(+), 578 deletions(-) create mode 100644 shared-components/statviz/components/filter/DemographicsFilters.tsx create mode 100644 shared-components/statviz/components/filter/FilterPanel.tsx create mode 100644 shared-components/statviz/components/filter/MovementFilters.tsx create mode 100644 shared-components/statviz/components/filter/StockFilters.tsx diff --git a/shared-components/statviz/components/filter/DemographicsFilterPanel.tsx b/shared-components/statviz/components/filter/DemographicsFilterPanel.tsx index df3d0a4d8..bc77a43c6 100644 --- a/shared-components/statviz/components/filter/DemographicsFilterPanel.tsx +++ b/shared-components/statviz/components/filter/DemographicsFilterPanel.tsx @@ -1,27 +1,7 @@ -import { useCallback, useEffect, useState } from "react"; -import { - VStack, - Button, - Drawer, - DrawerBody, - DrawerCloseButton, - DrawerContent, - DrawerHeader, - DrawerOverlay, - SimpleGrid, - Box, - IconButton, - CheckboxGroup, - Checkbox, - HStack, - FormLabel, -} from "@chakra-ui/react"; -import { SettingsIcon } from "@chakra-ui/icons"; -import TabbedTagDropdown from "./TabbedTagDropdown"; +import { useDisclosure } from "@chakra-ui/react"; import type { ITagOption, DemographicsAppliedFilters } from "../../utils/dashboardFilters"; -import { AGE_RANGES } from "../../utils/dashboardFilters"; - -const HUMAN_GENDERS = ["Male", "Female", "Diverse"] as const; +import { FilterPanel } from "./FilterPanel"; +import { DemographicsFilters } from "./DemographicsFilters"; interface DemographicsFilterPanelProps { appliedFilters: DemographicsAppliedFilters; @@ -34,122 +14,23 @@ export default function DemographicsFilterPanel({ tags, onApply, }: DemographicsFilterPanelProps) { - const [isOpen, setIsOpen] = useState(false); - const [staged, setStaged] = useState(appliedFilters); - - // Re-initialise staged state when the drawer opens - useEffect(() => { - if (isOpen) { - setStaged(appliedFilters); - } - }, [isOpen, appliedFilters]); - - const handleApply = useCallback(() => { - onApply(staged); - setIsOpen(false); - }, [staged, onApply]); - - const handleClear = useCallback(() => { - setStaged({ ageRanges: [], genders: [], includedTags: [], excludedTags: [] }); - }, []); + const { isOpen, onOpen, onClose } = useDisclosure(); return ( - <> - } - size="sm" - variant="outline" - onClick={(e) => { - e.stopPropagation(); - setIsOpen(true); - }} + + - setIsOpen(false)} placement="right" size="md"> - - - Demographics Filters - - - - - - Age - - setStaged((prev) => ({ ...prev, ageRanges: values as string[] })) - } - > - - {AGE_RANGES.map((range) => ( - - {range.label} - - ))} - - - - - Gender - - setStaged((prev) => ({ ...prev, genders: values as string[] })) - } - > - - {HUMAN_GENDERS.map((gender) => ( - - {gender} - - ))} - - - - - Tags - - setStaged((prev) => ({ ...prev, includedTags: newTags })) - } - onExcludedChange={(newTags) => - setStaged((prev) => ({ ...prev, excludedTags: newTags })) - } - onClearAll={() => - setStaged((prev) => ({ ...prev, includedTags: [], excludedTags: [] })) - } - placeholder="Select tags" - /> - - - - - - - - - - - - - + ); } diff --git a/shared-components/statviz/components/filter/DemographicsFilters.tsx b/shared-components/statviz/components/filter/DemographicsFilters.tsx new file mode 100644 index 000000000..16aedc84f --- /dev/null +++ b/shared-components/statviz/components/filter/DemographicsFilters.tsx @@ -0,0 +1,124 @@ +import { useCallback, useEffect, useState } from "react"; +import { + VStack, + Button, + SimpleGrid, + Box, + CheckboxGroup, + Checkbox, + HStack, + FormLabel, +} from "@chakra-ui/react"; +import TabbedTagDropdown from "./TabbedTagDropdown"; +import type { ITagOption, DemographicsAppliedFilters } from "../../utils/dashboardFilters"; +import { AGE_RANGES } from "../../utils/dashboardFilters"; + +const HUMAN_GENDERS = ["Male", "Female", "Diverse"] as const; + +interface DemographicsFiltersProps { + isOpen: boolean; + onClose: () => void; + appliedFilters: DemographicsAppliedFilters; + tags: ITagOption[]; + onApply: (filters: DemographicsAppliedFilters) => void; +} + +export function DemographicsFilters({ + isOpen, + onClose, + appliedFilters, + tags, + onApply, +}: DemographicsFiltersProps) { + const [staged, setStaged] = useState(appliedFilters); + + useEffect(() => { + if (isOpen) { + setStaged(appliedFilters); + } + }, [isOpen, appliedFilters]); + + const handleApply = useCallback(() => { + onApply(staged); + onClose(); + }, [staged, onApply, onClose]); + + const handleClear = useCallback(() => { + setStaged({ ageRanges: [], genders: [], includedTags: [], excludedTags: [] }); + }, []); + + return ( + + + + Age + setStaged((prev) => ({ ...prev, ageRanges: values as string[] }))} + > + + {AGE_RANGES.map((range) => ( + + {range.label} + + ))} + + + + + Gender + setStaged((prev) => ({ ...prev, genders: values as string[] }))} + > + + {HUMAN_GENDERS.map((gender) => ( + + {gender} + + ))} + + + + + Tags + + setStaged((prev) => ({ ...prev, includedTags: newTags })) + } + onExcludedChange={(newTags) => + setStaged((prev) => ({ ...prev, excludedTags: newTags })) + } + onClearAll={() => + setStaged((prev) => ({ ...prev, includedTags: [], excludedTags: [] })) + } + placeholder="Select tags" + /> + + + + + + + + + + ); +} diff --git a/shared-components/statviz/components/filter/FilterPanel.tsx b/shared-components/statviz/components/filter/FilterPanel.tsx new file mode 100644 index 000000000..26f9d1d80 --- /dev/null +++ b/shared-components/statviz/components/filter/FilterPanel.tsx @@ -0,0 +1,54 @@ +import { ReactNode } from "react"; +import { + Drawer, + DrawerBody, + DrawerCloseButton, + DrawerContent, + DrawerHeader, + DrawerOverlay, + IconButton, + useBreakpointValue, +} from "@chakra-ui/react"; +import { SettingsIcon } from "@chakra-ui/icons"; + +interface FilterPanelProps { + label: string; + ariaLabel: string; + isOpen: boolean; + onOpen: () => void; + onClose: () => void; + children: ReactNode; +} + +export function FilterPanel({ + label, + ariaLabel, + isOpen, + onOpen, + onClose, + children, +}: FilterPanelProps) { + const placement = useBreakpointValue({ base: "left" as const, md: "right" as const }) ?? "right"; + const size = useBreakpointValue({ base: undefined, md: "md" }); + const maxW = useBreakpointValue({ base: "90vw", md: undefined }); + + return ( + <> + } + size="sm" + variant="outline" + onClick={onOpen} + /> + + + + {label} + + {children} + + + + ); +} diff --git a/shared-components/statviz/components/filter/MovementFilterPanel.tsx b/shared-components/statviz/components/filter/MovementFilterPanel.tsx index 729545bb2..0dc19f9d5 100644 --- a/shared-components/statviz/components/filter/MovementFilterPanel.tsx +++ b/shared-components/statviz/components/filter/MovementFilterPanel.tsx @@ -1,31 +1,12 @@ -import { useCallback, useEffect, useState } from "react"; -import { - VStack, - Button, - Drawer, - DrawerBody, - DrawerCloseButton, - DrawerContent, - DrawerHeader, - DrawerOverlay, - SimpleGrid, - Box, - IconButton, - Input, - FormLabel, - HStack, -} from "@chakra-ui/react"; -import { SettingsIcon } from "@chakra-ui/icons"; -import MultiSelectFilter from "./MultiSelectFilter"; -import TabbedTagDropdown from "./TabbedTagDropdown"; +import { useDisclosure } from "@chakra-ui/react"; import type { IProductOption, ICategoryOption, ITagOption, MovementAppliedFilters, } from "../../utils/dashboardFilters"; -import { genders } from "./GenderProductFilter"; -import type { IFilterValue } from "./ValueFilter"; +import { FilterPanel } from "./FilterPanel"; +import { MovementFilters } from "./MovementFilters"; interface MovementFilterPanelProps { appliedFilters: MovementAppliedFilters; @@ -35,22 +16,6 @@ interface MovementFilterPanelProps { onApply: (filters: MovementAppliedFilters) => void; } -function toProductFilterValues(products: IProductOption[]): IFilterValue[] { - return products.map((p) => ({ - value: String(p.id), - label: p.gender ? `${p.name} (${p.gender})` : p.name, - urlId: String(p.id), - })); -} - -function toFilterValues(items: { id: number; name: string }[]): IFilterValue[] { - return items.map((item) => ({ - value: String(item.id), - label: item.name, - urlId: String(item.id), - })); -} - export default function MovementFilterPanel({ appliedFilters, products, @@ -58,174 +23,25 @@ export default function MovementFilterPanel({ tags, onApply, }: MovementFilterPanelProps) { - const [isOpen, setIsOpen] = useState(false); - const [staged, setStaged] = useState(appliedFilters); - - // Re-initialise staged state when the drawer opens - useEffect(() => { - if (isOpen) { - setStaged(appliedFilters); - } - }, [isOpen, appliedFilters]); - - const productOptions = toProductFilterValues(products); - const categoryOptions = toFilterValues(categories); - - const handleApply = useCallback(() => { - onApply(staged); - setIsOpen(false); - }, [staged, onApply]); - - const handleClear = useCallback(() => { - setStaged((prev) => ({ - ...prev, - products: [], - genders: [], - categories: [], - includedTags: [], - excludedTags: [], - })); - }, []); - - const selectedProductValues = productOptions.filter((o) => - staged.products.some((p) => String(p.id) === o.value), - ); - const selectedCategoryValues = categoryOptions.filter((o) => - staged.categories.some((c) => String(c.id) === o.value), - ); - const selectedGenderValues = genders.filter((g) => staged.genders.includes(g.value)); + const { isOpen, onOpen, onClose } = useDisclosure(); return ( - <> - } - size="sm" - variant="outline" - onClick={(e) => { - e.stopPropagation(); - setIsOpen(true); - }} + + - setIsOpen(false)} placement="right" size="md"> - - - Movement Filters - - - - - - Move date - - - From - - setStaged((prev) => ({ ...prev, dateFrom: e.target.value })) - } - /> - - - To - setStaged((prev) => ({ ...prev, dateTo: e.target.value }))} - /> - - - - - setStaged((prev) => ({ - ...prev, - genders: selected.map((s) => s.value), - })) - } - placeholder="All" - /> - { - const selectedIds = selected.map((s) => Number(s.value)); - setStaged((prev) => ({ - ...prev, - products: products.filter((p) => selectedIds.includes(p.id)), - })); - }} - placeholder="All" - /> - { - const selectedIds = selected.map((s) => Number(s.value)); - setStaged((prev) => ({ - ...prev, - categories: categories.filter((c) => selectedIds.includes(c.id)), - })); - }} - placeholder="All" - /> - - Tags - - setStaged((prev) => ({ ...prev, includedTags: newTags })) - } - onExcludedChange={(newTags) => - setStaged((prev) => ({ ...prev, excludedTags: newTags })) - } - onClearAll={() => - setStaged((prev) => ({ ...prev, includedTags: [], excludedTags: [] })) - } - placeholder="Select tags" - /> - - - - - - - - - - - - - + ); } diff --git a/shared-components/statviz/components/filter/MovementFilters.tsx b/shared-components/statviz/components/filter/MovementFilters.tsx new file mode 100644 index 000000000..d19cf6ec7 --- /dev/null +++ b/shared-components/statviz/components/filter/MovementFilters.tsx @@ -0,0 +1,194 @@ +import { useCallback, useEffect, useState } from "react"; +import { VStack, Button, SimpleGrid, Box, Input, FormLabel, HStack } from "@chakra-ui/react"; +import MultiSelectFilter from "./MultiSelectFilter"; +import TabbedTagDropdown from "./TabbedTagDropdown"; +import type { + IProductOption, + ICategoryOption, + ITagOption, + MovementAppliedFilters, +} from "../../utils/dashboardFilters"; +import { genders } from "./GenderProductFilter"; +import type { IFilterValue } from "./ValueFilter"; + +function toProductFilterValues(products: IProductOption[]): IFilterValue[] { + return products.map((p) => ({ + value: String(p.id), + label: p.gender ? `${p.name} (${p.gender})` : p.name, + urlId: String(p.id), + })); +} + +function toFilterValues(items: { id: number; name: string }[]): IFilterValue[] { + return items.map((item) => ({ + value: String(item.id), + label: item.name, + urlId: String(item.id), + })); +} + +interface MovementFiltersProps { + isOpen: boolean; + onClose: () => void; + appliedFilters: MovementAppliedFilters; + products: IProductOption[]; + categories: ICategoryOption[]; + tags: ITagOption[]; + onApply: (filters: MovementAppliedFilters) => void; +} + +export function MovementFilters({ + isOpen, + onClose, + appliedFilters, + products, + categories, + tags, + onApply, +}: MovementFiltersProps) { + const [staged, setStaged] = useState(appliedFilters); + + useEffect(() => { + if (isOpen) { + setStaged(appliedFilters); + } + }, [isOpen, appliedFilters]); + + const productOptions = toProductFilterValues(products); + const categoryOptions = toFilterValues(categories); + + const handleApply = useCallback(() => { + onApply(staged); + onClose(); + }, [staged, onApply, onClose]); + + const handleClear = useCallback(() => { + setStaged((prev) => ({ + ...prev, + products: [], + genders: [], + categories: [], + includedTags: [], + excludedTags: [], + })); + }, []); + + const selectedProductValues = productOptions.filter((o) => + staged.products.some((p) => String(p.id) === o.value), + ); + const selectedCategoryValues = categoryOptions.filter((o) => + staged.categories.some((c) => String(c.id) === o.value), + ); + const selectedGenderValues = genders.filter((g) => staged.genders.includes(g.value)); + + return ( + + + + Move date + + + From + setStaged((prev) => ({ ...prev, dateFrom: e.target.value }))} + /> + + + To + setStaged((prev) => ({ ...prev, dateTo: e.target.value }))} + /> + + + + + setStaged((prev) => ({ + ...prev, + genders: selected.map((s) => s.value), + })) + } + placeholder="All" + /> + { + const selectedIds = selected.map((s) => Number(s.value)); + setStaged((prev) => ({ + ...prev, + products: products.filter((p) => selectedIds.includes(p.id)), + })); + }} + placeholder="All" + /> + { + const selectedIds = selected.map((s) => Number(s.value)); + setStaged((prev) => ({ + ...prev, + categories: categories.filter((c) => selectedIds.includes(c.id)), + })); + }} + placeholder="All" + /> + + Tags + + setStaged((prev) => ({ ...prev, includedTags: newTags })) + } + onExcludedChange={(newTags) => + setStaged((prev) => ({ ...prev, excludedTags: newTags })) + } + onClearAll={() => + setStaged((prev) => ({ ...prev, includedTags: [], excludedTags: [] })) + } + placeholder="Select tags" + /> + + + + + + + + + + ); +} diff --git a/shared-components/statviz/components/filter/StockFilterPanel.tsx b/shared-components/statviz/components/filter/StockFilterPanel.tsx index 84fcbd301..15fff4c77 100644 --- a/shared-components/statviz/components/filter/StockFilterPanel.tsx +++ b/shared-components/statviz/components/filter/StockFilterPanel.tsx @@ -1,21 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; -import { - VStack, - Button, - Drawer, - DrawerBody, - DrawerCloseButton, - DrawerContent, - DrawerHeader, - DrawerOverlay, - SimpleGrid, - Box, - IconButton, - FormLabel, -} from "@chakra-ui/react"; -import { SettingsIcon } from "@chakra-ui/icons"; -import MultiSelectFilter from "./MultiSelectFilter"; -import TabbedTagDropdown from "./TabbedTagDropdown"; +import { useDisclosure } from "@chakra-ui/react"; import type { IProductOption, ICategoryOption, @@ -23,8 +6,8 @@ import type { ITagOption, StockAppliedFilters, } from "../../utils/dashboardFilters"; -import { genders } from "./GenderProductFilter"; -import type { IFilterValue } from "./ValueFilter"; +import { FilterPanel } from "./FilterPanel"; +import { StockFilters } from "./StockFilters"; interface StockFilterPanelProps { appliedFilters: StockAppliedFilters; @@ -35,25 +18,6 @@ interface StockFilterPanelProps { onApply: (filters: StockAppliedFilters) => void; } -function toFilterValues( - items: { id: number; name: string }[], - genderSuffix?: string, -): IFilterValue[] { - return items.map((item) => ({ - value: String(item.id), - label: genderSuffix ? `${item.name} (${genderSuffix})` : item.name, - urlId: String(item.id), - })); -} - -function toProductFilterValues(products: IProductOption[]): IFilterValue[] { - return products.map((p) => ({ - value: String(p.id), - label: p.gender ? `${p.name} (${p.gender})` : p.name, - urlId: String(p.id), - })); -} - export default function StockFilterPanel({ appliedFilters, products, @@ -62,166 +26,26 @@ export default function StockFilterPanel({ tags, onApply, }: StockFilterPanelProps) { - const [isOpen, setIsOpen] = useState(false); - const [staged, setStaged] = useState(appliedFilters); - - // Re-initialise staged state when the drawer opens - useEffect(() => { - if (isOpen) { - setStaged(appliedFilters); - } - }, [isOpen, appliedFilters]); - - const productOptions = toProductFilterValues(products); - const categoryOptions = toFilterValues(categories); - const locationOptions = toFilterValues(locations); - - const handleApply = useCallback(() => { - onApply(staged); - setIsOpen(false); - }, [staged, onApply]); - - const handleClear = useCallback(() => { - setStaged({ - products: [], - genders: [], - categories: [], - locations: [], - includedTags: [], - excludedTags: [], - }); - }, []); - - // Helpers to convert between IFilterValue selection and option objects - const selectedProductValues = productOptions.filter((o) => - staged.products.some((p) => String(p.id) === o.value), - ); - const selectedCategoryValues = categoryOptions.filter((o) => - staged.categories.some((c) => String(c.id) === o.value), - ); - const selectedLocationValues = locationOptions.filter((o) => - staged.locations.some((l) => String(l.id) === o.value), - ); - const selectedGenderValues = genders.filter((g) => staged.genders.includes(g.value)); + const { isOpen, onOpen, onClose } = useDisclosure(); return ( - <> - } - size="sm" - variant="outline" - onClick={(e) => { - e.stopPropagation(); - setIsOpen(true); - }} + + - setIsOpen(false)} placement="right" size="md"> - - - Stock Filters - - - - - - setStaged((prev) => ({ - ...prev, - genders: selected.map((s) => s.value), - })) - } - placeholder="All" - /> - { - const selectedIds = selected.map((s) => Number(s.value)); - setStaged((prev) => ({ - ...prev, - products: products.filter((p) => selectedIds.includes(p.id)), - })); - }} - placeholder="All" - /> - { - const selectedIds = selected.map((s) => Number(s.value)); - setStaged((prev) => ({ - ...prev, - categories: categories.filter((c) => selectedIds.includes(c.id)), - })); - }} - placeholder="All" - /> - { - const selectedIds = selected.map((s) => Number(s.value)); - setStaged((prev) => ({ - ...prev, - locations: locations.filter((l) => selectedIds.includes(l.id)), - })); - }} - placeholder="All" - /> - - Tags - - setStaged((prev) => ({ ...prev, includedTags: newTags })) - } - onExcludedChange={(newTags) => - setStaged((prev) => ({ ...prev, excludedTags: newTags })) - } - onClearAll={() => - setStaged((prev) => ({ ...prev, includedTags: [], excludedTags: [] })) - } - placeholder="Select tags" - /> - - - - - - - - - - - - - + ); } diff --git a/shared-components/statviz/components/filter/StockFilters.tsx b/shared-components/statviz/components/filter/StockFilters.tsx new file mode 100644 index 000000000..85490217b --- /dev/null +++ b/shared-components/statviz/components/filter/StockFilters.tsx @@ -0,0 +1,190 @@ +import { useCallback, useEffect, useState } from "react"; +import { VStack, Button, SimpleGrid, Box, FormLabel } from "@chakra-ui/react"; +import MultiSelectFilter from "./MultiSelectFilter"; +import TabbedTagDropdown from "./TabbedTagDropdown"; +import type { + IProductOption, + ICategoryOption, + ILocationOption, + ITagOption, + StockAppliedFilters, +} from "../../utils/dashboardFilters"; +import { genders } from "./GenderProductFilter"; +import type { IFilterValue } from "./ValueFilter"; + +function toFilterValues(items: { id: number; name: string }[]): IFilterValue[] { + return items.map((item) => ({ + value: String(item.id), + label: item.name, + urlId: String(item.id), + })); +} + +function toProductFilterValues(products: IProductOption[]): IFilterValue[] { + return products.map((p) => ({ + value: String(p.id), + label: p.gender ? `${p.name} (${p.gender})` : p.name, + urlId: String(p.id), + })); +} + +interface StockFiltersProps { + isOpen: boolean; + onClose: () => void; + appliedFilters: StockAppliedFilters; + products: IProductOption[]; + categories: ICategoryOption[]; + locations: ILocationOption[]; + tags: ITagOption[]; + onApply: (filters: StockAppliedFilters) => void; +} + +export function StockFilters({ + isOpen, + onClose, + appliedFilters, + products, + categories, + locations, + tags, + onApply, +}: StockFiltersProps) { + const [staged, setStaged] = useState(appliedFilters); + + useEffect(() => { + if (isOpen) { + setStaged(appliedFilters); + } + }, [isOpen, appliedFilters]); + + const productOptions = toProductFilterValues(products); + const categoryOptions = toFilterValues(categories); + const locationOptions = toFilterValues(locations); + + const handleApply = useCallback(() => { + onApply(staged); + onClose(); + }, [staged, onApply, onClose]); + + const handleClear = useCallback(() => { + setStaged({ + products: [], + genders: [], + categories: [], + locations: [], + includedTags: [], + excludedTags: [], + }); + }, []); + + const selectedProductValues = productOptions.filter((o) => + staged.products.some((p) => String(p.id) === o.value), + ); + const selectedCategoryValues = categoryOptions.filter((o) => + staged.categories.some((c) => String(c.id) === o.value), + ); + const selectedLocationValues = locationOptions.filter((o) => + staged.locations.some((l) => String(l.id) === o.value), + ); + const selectedGenderValues = genders.filter((g) => staged.genders.includes(g.value)); + + return ( + + + + setStaged((prev) => ({ + ...prev, + genders: selected.map((s) => s.value), + })) + } + placeholder="All" + /> + { + const selectedIds = selected.map((s) => Number(s.value)); + setStaged((prev) => ({ + ...prev, + products: products.filter((p) => selectedIds.includes(p.id)), + })); + }} + placeholder="All" + /> + { + const selectedIds = selected.map((s) => Number(s.value)); + setStaged((prev) => ({ + ...prev, + categories: categories.filter((c) => selectedIds.includes(c.id)), + })); + }} + placeholder="All" + /> + { + const selectedIds = selected.map((s) => Number(s.value)); + setStaged((prev) => ({ + ...prev, + locations: locations.filter((l) => selectedIds.includes(l.id)), + })); + }} + placeholder="All" + /> + + Tags + + setStaged((prev) => ({ ...prev, includedTags: newTags })) + } + onExcludedChange={(newTags) => + setStaged((prev) => ({ ...prev, excludedTags: newTags })) + } + onClearAll={() => + setStaged((prev) => ({ ...prev, includedTags: [], excludedTags: [] })) + } + placeholder="Select tags" + /> + + + + + + + + + + ); +} diff --git a/shared-components/statviz/dashboard/Demographics.tsx b/shared-components/statviz/dashboard/Demographics.tsx index 86d3f4395..e43c503fa 100644 --- a/shared-components/statviz/dashboard/Demographics.tsx +++ b/shared-components/statviz/dashboard/Demographics.tsx @@ -7,7 +7,7 @@ import { Wrap, WrapItem, Box, - HStack, + VStack, } from "@chakra-ui/react"; import { useCallback, useMemo } from "react"; import { useSearchParams } from "react-router-dom"; @@ -49,21 +49,21 @@ export default function Demographics({ tags }: DemographicsProps) { Beneficiary Overview - e.stopPropagation()} mr={2}> + + + + - - - - - - - - - + + + + + + ); diff --git a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx index a83ee54ce..dabb6b354 100644 --- a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx +++ b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx @@ -7,6 +7,7 @@ import { AccordionPanel, HStack, Select, + VStack, } from "@chakra-ui/react"; import { useCallback, useMemo } from "react"; import { useSearchParams } from "react-router-dom"; @@ -75,24 +76,31 @@ export default function ItemsAndBoxes({ Stock Overview - e.stopPropagation()} mr={2}> - - - - + + + + + + +
); diff --git a/shared-components/statviz/dashboard/MovedBoxes.tsx b/shared-components/statviz/dashboard/MovedBoxes.tsx index 1aaa13ed5..40a296e21 100644 --- a/shared-components/statviz/dashboard/MovedBoxes.tsx +++ b/shared-components/statviz/dashboard/MovedBoxes.tsx @@ -7,6 +7,7 @@ import { Box, HStack, Select, + VStack, } from "@chakra-ui/react"; import { useCallback, useMemo } from "react"; import { useSearchParams } from "react-router-dom"; @@ -69,23 +70,30 @@ export default function MovedBoxes({ products, categories, tags }: MovedBoxesPro Movement History - e.stopPropagation()} mr={2}> - - - - + + + + + + +
); From ceef657bca7b31d8287c8e47c88c42d2fb5ff9c2 Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Mon, 22 Jun 2026 17:08:41 +0200 Subject: [PATCH 08/30] Avoid extra code by using FilterPanel directly --- .../filter/DemographicsFilterPanel.tsx | 36 ------------- .../statviz/components/filter/FilterPanel.tsx | 16 +++--- .../components/filter/MovementFilterPanel.tsx | 47 ----------------- .../components/filter/StockFilterPanel.tsx | 51 ------------------- .../statviz/dashboard/Demographics.tsx | 25 ++++++--- .../statviz/dashboard/ItemsAndBoxes.tsx | 27 ++++++---- .../statviz/dashboard/MovedBoxes.tsx | 26 ++++++---- 7 files changed, 60 insertions(+), 168 deletions(-) delete mode 100644 shared-components/statviz/components/filter/DemographicsFilterPanel.tsx delete mode 100644 shared-components/statviz/components/filter/MovementFilterPanel.tsx delete mode 100644 shared-components/statviz/components/filter/StockFilterPanel.tsx diff --git a/shared-components/statviz/components/filter/DemographicsFilterPanel.tsx b/shared-components/statviz/components/filter/DemographicsFilterPanel.tsx deleted file mode 100644 index bc77a43c6..000000000 --- a/shared-components/statviz/components/filter/DemographicsFilterPanel.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useDisclosure } from "@chakra-ui/react"; -import type { ITagOption, DemographicsAppliedFilters } from "../../utils/dashboardFilters"; -import { FilterPanel } from "./FilterPanel"; -import { DemographicsFilters } from "./DemographicsFilters"; - -interface DemographicsFilterPanelProps { - appliedFilters: DemographicsAppliedFilters; - tags: ITagOption[]; - onApply: (filters: DemographicsAppliedFilters) => void; -} - -export default function DemographicsFilterPanel({ - appliedFilters, - tags, - onApply, -}: DemographicsFilterPanelProps) { - const { isOpen, onOpen, onClose } = useDisclosure(); - - return ( - - - - ); -} diff --git a/shared-components/statviz/components/filter/FilterPanel.tsx b/shared-components/statviz/components/filter/FilterPanel.tsx index 26f9d1d80..c1a68abac 100644 --- a/shared-components/statviz/components/filter/FilterPanel.tsx +++ b/shared-components/statviz/components/filter/FilterPanel.tsx @@ -9,11 +9,10 @@ import { IconButton, useBreakpointValue, } from "@chakra-ui/react"; -import { SettingsIcon } from "@chakra-ui/icons"; +import { MdFilterList } from "react-icons/md"; interface FilterPanelProps { - label: string; - ariaLabel: string; + label?: string; isOpen: boolean; onOpen: () => void; onClose: () => void; @@ -21,8 +20,7 @@ interface FilterPanelProps { } export function FilterPanel({ - label, - ariaLabel, + label = "Filters", isOpen, onOpen, onClose, @@ -35,10 +33,10 @@ export function FilterPanel({ return ( <> } - size="sm" - variant="outline" + icon={} + aria-label="Open ${label}" + size="md" + data-testid="${label.replaceAll(' ', '').lower()}-drawer-button" onClick={onOpen} /> diff --git a/shared-components/statviz/components/filter/MovementFilterPanel.tsx b/shared-components/statviz/components/filter/MovementFilterPanel.tsx deleted file mode 100644 index 0dc19f9d5..000000000 --- a/shared-components/statviz/components/filter/MovementFilterPanel.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { useDisclosure } from "@chakra-ui/react"; -import type { - IProductOption, - ICategoryOption, - ITagOption, - MovementAppliedFilters, -} from "../../utils/dashboardFilters"; -import { FilterPanel } from "./FilterPanel"; -import { MovementFilters } from "./MovementFilters"; - -interface MovementFilterPanelProps { - appliedFilters: MovementAppliedFilters; - products: IProductOption[]; - categories: ICategoryOption[]; - tags: ITagOption[]; - onApply: (filters: MovementAppliedFilters) => void; -} - -export default function MovementFilterPanel({ - appliedFilters, - products, - categories, - tags, - onApply, -}: MovementFilterPanelProps) { - const { isOpen, onOpen, onClose } = useDisclosure(); - - return ( - - - - ); -} diff --git a/shared-components/statviz/components/filter/StockFilterPanel.tsx b/shared-components/statviz/components/filter/StockFilterPanel.tsx deleted file mode 100644 index 15fff4c77..000000000 --- a/shared-components/statviz/components/filter/StockFilterPanel.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useDisclosure } from "@chakra-ui/react"; -import type { - IProductOption, - ICategoryOption, - ILocationOption, - ITagOption, - StockAppliedFilters, -} from "../../utils/dashboardFilters"; -import { FilterPanel } from "./FilterPanel"; -import { StockFilters } from "./StockFilters"; - -interface StockFilterPanelProps { - appliedFilters: StockAppliedFilters; - products: IProductOption[]; - categories: ICategoryOption[]; - locations: ILocationOption[]; - tags: ITagOption[]; - onApply: (filters: StockAppliedFilters) => void; -} - -export default function StockFilterPanel({ - appliedFilters, - products, - categories, - locations, - tags, - onApply, -}: StockFilterPanelProps) { - const { isOpen, onOpen, onClose } = useDisclosure(); - - return ( - - - - ); -} diff --git a/shared-components/statviz/dashboard/Demographics.tsx b/shared-components/statviz/dashboard/Demographics.tsx index e43c503fa..39b8ba685 100644 --- a/shared-components/statviz/dashboard/Demographics.tsx +++ b/shared-components/statviz/dashboard/Demographics.tsx @@ -1,4 +1,5 @@ import { + useDisclosure, AccordionItem, AccordionButton, Heading, @@ -12,13 +13,14 @@ import { import { useCallback, useMemo } from "react"; import { useSearchParams } from "react-router-dom"; import DemographicDataContainer from "../components/visualizations/demographic/DemographicDataContainer"; -import DemographicsFilterPanel from "../components/filter/DemographicsFilterPanel"; import { readDemographicsFiltersFromUrl, writeDemographicsFiltersToUrl, type DemographicsAppliedFilters, type ITagOption, } from "../utils/dashboardFilters"; +import { FilterPanel } from "./../components/filter/FilterPanel"; +import { DemographicsFilters } from "./../components/filter/DemographicsFilters"; interface DemographicsProps { tags: ITagOption[]; @@ -43,6 +45,8 @@ export default function Demographics({ tags }: DemographicsProps) { [searchParams, setSearchParams], ); + const { isOpen, onOpen, onClose } = useDisclosure(); + return ( @@ -53,11 +57,20 @@ export default function Demographics({ tags }: DemographicsProps) { - + + + diff --git a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx index dabb6b354..1a42e6eec 100644 --- a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx +++ b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx @@ -1,4 +1,5 @@ import { + useDisclosure, Box, AccordionItem, AccordionButton, @@ -12,7 +13,6 @@ import { import { useCallback, useMemo } from "react"; import { useSearchParams } from "react-router-dom"; import CreatedBoxesDataContainer from "../components/visualizations/createdBoxes/CreatedBoxesDataContainer"; -import StockFilterPanel from "../components/filter/StockFilterPanel"; import { STOCK_URL_PARAMS, readStockFiltersFromUrl, @@ -24,6 +24,8 @@ import { type ITagOption, } from "../utils/dashboardFilters"; import type { BoxesOrItems } from "../components/filter/BoxesOrItemsSelect"; +import { FilterPanel } from "./../components/filter/FilterPanel"; +import { StockFilters } from "./../components/filter/StockFilters"; export type BoxesOrItemsCount = "boxesCount" | "itemsCount"; @@ -70,6 +72,7 @@ export default function ItemsAndBoxes({ [searchParams, setSearchParams], ); + const { isOpen, onOpen, onClose } = useDisclosure(); return ( @@ -82,7 +85,7 @@ export default function ItemsAndBoxes({ - + + + diff --git a/shared-components/statviz/dashboard/MovedBoxes.tsx b/shared-components/statviz/dashboard/MovedBoxes.tsx index 40a296e21..35ae2dd89 100644 --- a/shared-components/statviz/dashboard/MovedBoxes.tsx +++ b/shared-components/statviz/dashboard/MovedBoxes.tsx @@ -1,4 +1,5 @@ import { + useDisclosure, AccordionItem, AccordionButton, Heading, @@ -12,7 +13,8 @@ import { import { useCallback, useMemo } from "react"; import { useSearchParams } from "react-router-dom"; import MovedBoxesDataContainer from "../components/visualizations/movedBoxes/MovedBoxesDataContainer"; -import MovementFilterPanel from "../components/filter/MovementFilterPanel"; +import { FilterPanel } from "./../components/filter/FilterPanel"; +import { MovementFilters } from "./../components/filter/MovementFilters"; import { MOVEMENT_URL_PARAMS, readMovementFiltersFromUrl, @@ -64,6 +66,8 @@ export default function MovedBoxes({ products, categories, tags }: MovedBoxesPro [searchParams, setSearchParams], ); + const { isOpen, onOpen, onClose } = useDisclosure(); + return ( @@ -76,7 +80,7 @@ export default function MovedBoxes({ products, categories, tags }: MovedBoxesPro - + + + From f47e86e20230b70aab45d6f77c84ca83dfc87dd6 Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Mon, 22 Jun 2026 17:13:44 +0200 Subject: [PATCH 09/30] align --- .../statviz/dashboard/Demographics.tsx | 27 ++++++++++--------- .../statviz/dashboard/ItemsAndBoxes.tsx | 2 +- .../statviz/dashboard/MovedBoxes.tsx | 2 +- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/shared-components/statviz/dashboard/Demographics.tsx b/shared-components/statviz/dashboard/Demographics.tsx index 39b8ba685..5646cd87d 100644 --- a/shared-components/statviz/dashboard/Demographics.tsx +++ b/shared-components/statviz/dashboard/Demographics.tsx @@ -3,6 +3,7 @@ import { AccordionItem, AccordionButton, Heading, + HStack, AccordionIcon, AccordionPanel, Wrap, @@ -57,20 +58,22 @@ export default function Demographics({ tags }: DemographicsProps) { - - + - + > + + + diff --git a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx index 1a42e6eec..21ffe33c6 100644 --- a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx +++ b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx @@ -83,7 +83,7 @@ export default function ItemsAndBoxes({ - + Date: Mon, 22 Jun 2026 15:58:00 +0000 Subject: [PATCH 10/30] feat: add StockOverviewRing pie chart and fix react-icons dependency --- front/package.json | 1 - package.json | 1 + pnpm-lock.yaml | 6 +- .../stock/StockOverviewRing.tsx | 76 +++++++++++++++++++ .../stock/StockOverviewRingDataContainer.tsx | 40 ++++++++++ .../StockOverviewRingFilterContainer.tsx | 59 ++++++++++++++ .../statviz/dashboard/ItemsAndBoxes.tsx | 5 ++ 7 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 shared-components/statviz/components/visualizations/stock/StockOverviewRing.tsx create mode 100644 shared-components/statviz/components/visualizations/stock/StockOverviewRingDataContainer.tsx create mode 100644 shared-components/statviz/components/visualizations/stock/StockOverviewRingFilterContainer.tsx diff --git a/front/package.json b/front/package.json index 11ee793f7..dbc0c86cf 100644 --- a/front/package.json +++ b/front/package.json @@ -11,7 +11,6 @@ "jotai": "^2.20.0", "react-big-calendar": "^1.20.0", "react-csv": "^2.2.2", - "react-icons": "^5.6.0", "react-joyride": "^3.1.0", "react-table": "^7.8.0", "regenerator-runtime": "^0.14.1", diff --git a/package.json b/package.json index a8c77884d..ac547387b 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "react": "^18.3.1", "react-dom": "^18.2.0", "react-hook-form": "^7.62.0", + "react-icons": "^5.6.0", "react-router-dom": "^6.30.4", "zod": "^4.4.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6dba892c3..8e7477d14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,6 +108,9 @@ importers: react-hook-form: specifier: ^7.62.0 version: 7.62.0(react@18.3.1) + react-icons: + specifier: ^5.6.0 + version: 5.6.0(react@18.3.1) react-router-dom: specifier: ^6.30.4 version: 6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -226,9 +229,6 @@ importers: react-csv: specifier: ^2.2.2 version: 2.2.2 - react-icons: - specifier: ^5.6.0 - version: 5.6.0(react@18.3.1) react-joyride: specifier: ^3.1.0 version: 3.1.0(@types/react@19.2.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) diff --git a/shared-components/statviz/components/visualizations/stock/StockOverviewRing.tsx b/shared-components/statviz/components/visualizations/stock/StockOverviewRing.tsx new file mode 100644 index 000000000..63069e36a --- /dev/null +++ b/shared-components/statviz/components/visualizations/stock/StockOverviewRing.tsx @@ -0,0 +1,76 @@ +import { Card, CardBody } from "@chakra-ui/react"; +import { useMemo } from "react"; +import { groupBy, innerJoin, map, sum, summarize, tidy } from "@tidyjs/tidy"; +import { StockOverview, StockOverviewResult } from "../../../../../graphql/types"; +import { BoxesOrItemsCount } from "../../../dashboard/ItemsAndBoxes"; +import PieChart from "../../nivo/PieChart"; +import VisHeader from "../../VisHeader"; +import getOnExport from "../../../utils/chartExport"; + +type PieData = { id: string; value: number }; + +interface StockOverviewRingProps { + data: StockOverview; + boxesOrItems: BoxesOrItemsCount; + width: string; + height: string; +} + +export default function StockOverviewRing({ + data, + boxesOrItems, + width, + height, +}: StockOverviewRingProps) { + const onExport = getOnExport(PieChart); + + const chartData = useMemo(() => { + const categoryDim = (data?.dimensions?.category ?? []).map((c) => ({ + categoryId: c.id!, + categoryName: c.name!, + })); + + return tidy( + (data?.facts ?? []) as StockOverviewResult[], + innerJoin(categoryDim, { by: "categoryId" }), + groupBy("categoryName", summarize({ value: sum(boxesOrItems) })), + map((row) => ({ + id: row.categoryName as string, + value: row.value as number, + })), + ) as PieData[]; + }, [data, boxesOrItems]); + + const total = useMemo(() => chartData.reduce((acc, d) => acc + d.value, 0), [chartData]); + + const heading = + boxesOrItems === "boxesCount" ? "Instock Boxes by Category" : "Instock Items by Category"; + + const centerData = { + level: total, + grouping: boxesOrItems === "boxesCount" ? "boxes" : "items", + }; + + const chartProps = { + data: chartData, + width, + height, + }; + + return ( + + + + + + + ); +} diff --git a/shared-components/statviz/components/visualizations/stock/StockOverviewRingDataContainer.tsx b/shared-components/statviz/components/visualizations/stock/StockOverviewRingDataContainer.tsx new file mode 100644 index 000000000..96a601b97 --- /dev/null +++ b/shared-components/statviz/components/visualizations/stock/StockOverviewRingDataContainer.tsx @@ -0,0 +1,40 @@ +import { Spinner } from "@chakra-ui/react"; +import { useQuery } from "@apollo/client"; +import { useParams } from "react-router-dom"; +import ErrorCard, { predefinedErrors } from "../../ErrorCard"; +import StockOverviewRingFilterContainer from "./StockOverviewRingFilterContainer"; +import { STOCK_QUERY } from "./StockDataContainer"; +import type { StockAppliedFilters } from "../../../utils/dashboardFilters"; +import type { BoxesOrItems } from "../../filter/BoxesOrItemsSelect"; + +interface StockOverviewRingDataContainerProps { + appliedFilters: StockAppliedFilters; + boxesOrItems: BoxesOrItems; +} + +export default function StockOverviewRingDataContainer({ + appliedFilters, + boxesOrItems, +}: StockOverviewRingDataContainerProps) { + const { baseId } = useParams(); + const { data, loading, error } = useQuery(STOCK_QUERY, { + variables: { baseId: parseInt(baseId!, 10) }, + }); + + if (error) { + return ; + } + if (loading) { + return ; + } + if (data === undefined) { + return ; + } + return ( + + ); +} diff --git a/shared-components/statviz/components/visualizations/stock/StockOverviewRingFilterContainer.tsx b/shared-components/statviz/components/visualizations/stock/StockOverviewRingFilterContainer.tsx new file mode 100644 index 000000000..94c52c2a6 --- /dev/null +++ b/shared-components/statviz/components/visualizations/stock/StockOverviewRingFilterContainer.tsx @@ -0,0 +1,59 @@ +import { useMemo } from "react"; +import { StockOverview, StockOverviewResult } from "../../../../../graphql/types"; +import { filterByTags } from "../../../utils/filterByTags"; +import type { StockAppliedFilters } from "../../../utils/dashboardFilters"; +import type { BoxesOrItems } from "../../filter/BoxesOrItemsSelect"; +import StockOverviewRing from "./StockOverviewRing"; + +interface StockOverviewRingFilterContainerProps { + stockOverview: StockOverview; + appliedFilters: StockAppliedFilters; + boxesOrItems: BoxesOrItems; +} + +export default function StockOverviewRingFilterContainer({ + stockOverview, + appliedFilters, + boxesOrItems, +}: StockOverviewRingFilterContainerProps) { + const { products, genders, categories, locations, includedTags, excludedTags } = appliedFilters; + + const filteredStockOverview = useMemo(() => { + let facts = (stockOverview?.facts ?? []) as StockOverviewResult[]; + + // Only show InStock boxes + facts = facts.filter((f) => f.boxState === "InStock"); + + if (genders.length > 0) { + facts = facts.filter((f) => genders.includes(f.gender ?? "")); + } + + if (categories.length > 0) { + const categoryIds = new Set(categories.map((c) => c.id)); + facts = facts.filter((f) => categoryIds.has(f.categoryId!)); + } + + if (locations.length > 0) { + const locationIds = new Set(locations.map((l) => l.id)); + facts = facts.filter((f) => locationIds.has(f.locationId!)); + } + + if (products.length > 0) { + const productNames = new Set(products.map((p) => p.name)); + facts = facts.filter((f) => productNames.has(f.productName ?? "")); + } + + facts = filterByTags(facts, includedTags, excludedTags); + + return { ...stockOverview, facts } as StockOverview; + }, [stockOverview, genders, categories, locations, products, includedTags, excludedTags]); + + return ( + + ); +} diff --git a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx index 21ffe33c6..48653beb4 100644 --- a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx +++ b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx @@ -13,6 +13,7 @@ import { import { useCallback, useMemo } from "react"; import { useSearchParams } from "react-router-dom"; import CreatedBoxesDataContainer from "../components/visualizations/createdBoxes/CreatedBoxesDataContainer"; +import StockOverviewRingDataContainer from "../components/visualizations/stock/StockOverviewRingDataContainer"; import { STOCK_URL_PARAMS, readStockFiltersFromUrl, @@ -107,6 +108,10 @@ export default function ItemsAndBoxes({ + From 409ca8b64d91b3a54101fe322635cdb729ae1081 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 06:14:44 +0000 Subject: [PATCH 11/30] feat: add user-controlled grouping dimension to StockOverviewRing Co-authored-by: pylipp <10617122+pylipp@users.noreply.github.com> --- .../stock/StockOverviewRing.tsx | 94 ++++++++++++++++--- 1 file changed, 83 insertions(+), 11 deletions(-) diff --git a/shared-components/statviz/components/visualizations/stock/StockOverviewRing.tsx b/shared-components/statviz/components/visualizations/stock/StockOverviewRing.tsx index 63069e36a..e7bb59508 100644 --- a/shared-components/statviz/components/visualizations/stock/StockOverviewRing.tsx +++ b/shared-components/statviz/components/visualizations/stock/StockOverviewRing.tsx @@ -1,4 +1,4 @@ -import { Card, CardBody } from "@chakra-ui/react"; +import { Card, CardBody, Wrap, WrapItem } from "@chakra-ui/react"; import { useMemo } from "react"; import { groupBy, innerJoin, map, sum, summarize, tidy } from "@tidyjs/tidy"; import { StockOverview, StockOverviewResult } from "../../../../../graphql/types"; @@ -6,9 +6,23 @@ import { BoxesOrItemsCount } from "../../../dashboard/ItemsAndBoxes"; import PieChart from "../../nivo/PieChart"; import VisHeader from "../../VisHeader"; import getOnExport from "../../../utils/chartExport"; +import ValueFilter, { IFilterValue } from "../../filter/ValueFilter"; +import useValueFilter from "../../../hooks/useValueFilter"; type PieData = { id: string; value: number }; +// srg = stock ring grouping +const ringGroupingUrlId = "srg"; + +const ringGroupingOptions: IFilterValue[] = [ + { value: "categoryName", label: "Category", urlId: "cn" }, + { value: "gender", label: "Gender", urlId: "g" }, + { value: "sizeName", label: "Size", urlId: "s" }, + { value: "locationName", label: "Location", urlId: "l" }, +]; + +const defaultRingGrouping = ringGroupingOptions[0]; + interface StockOverviewRingProps { data: StockOverview; boxesOrItems: BoxesOrItemsCount; @@ -24,27 +38,74 @@ export default function StockOverviewRing({ }: StockOverviewRingProps) { const onExport = getOnExport(PieChart); + const { onFilterChange, filterValue: groupingOption } = useValueFilter( + ringGroupingOptions, + defaultRingGrouping, + ringGroupingUrlId, + ); + + const groupKey = groupingOption.value; + const chartData = useMemo(() => { - const categoryDim = (data?.dimensions?.category ?? []).map((c) => ({ - categoryId: c.id!, - categoryName: c.name!, - })); + const facts = (data?.facts ?? []) as StockOverviewResult[]; + + if (groupKey === "categoryName") { + const categoryDim = (data?.dimensions?.category ?? []).map((c) => ({ + categoryId: c.id!, + categoryName: c.name!, + })); + return tidy( + facts, + innerJoin(categoryDim, { by: "categoryId" }), + groupBy("categoryName", summarize({ value: sum(boxesOrItems) })), + map((row) => ({ id: row.categoryName as string, value: row.value as number })), + ) as PieData[]; + } + + if (groupKey === "sizeName") { + const sizeDim = (data?.dimensions?.size ?? []).map((s) => ({ + sizeId: s.id!, + sizeName: s.name!, + })); + return tidy( + facts, + innerJoin(sizeDim, { by: "sizeId" }), + groupBy("sizeName", summarize({ value: sum(boxesOrItems) })), + map((row) => ({ id: row.sizeName as string, value: row.value as number })), + ) as PieData[]; + } + + if (groupKey === "locationName") { + const locationDim = (data?.dimensions?.location ?? []).map((l) => ({ + locationId: l.id!, + locationName: l.name!, + })); + return tidy( + facts, + innerJoin(locationDim, { by: "locationId" }), + groupBy("locationName", summarize({ value: sum(boxesOrItems) })), + map((row) => ({ id: row.locationName as string, value: row.value as number })), + ) as PieData[]; + } + // groupKey === "gender" return tidy( - (data?.facts ?? []) as StockOverviewResult[], - innerJoin(categoryDim, { by: "categoryId" }), - groupBy("categoryName", summarize({ value: sum(boxesOrItems) })), + facts, + groupBy("gender", summarize({ value: sum(boxesOrItems) })), map((row) => ({ - id: row.categoryName as string, + id: (row.gender as string | null) ?? "Unknown", value: row.value as number, })), ) as PieData[]; - }, [data, boxesOrItems]); + }, [data, boxesOrItems, groupKey]); const total = useMemo(() => chartData.reduce((acc, d) => acc + d.value, 0), [chartData]); + const groupLabel = groupingOption.label; const heading = - boxesOrItems === "boxesCount" ? "Instock Boxes by Category" : "Instock Items by Category"; + boxesOrItems === "boxesCount" + ? `Instock Boxes by ${groupLabel}` + : `Instock Items by ${groupLabel}`; const centerData = { level: total, @@ -69,6 +130,17 @@ export default function StockOverviewRing({ customIncludes={[{ prop: { centerData }, value: "include center data" }]} /> + + + + + From 4e57ff05c3500a93e42bd0610e22b8a0df080eaa Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Mon, 22 Jun 2026 17:25:08 +0200 Subject: [PATCH 12/30] time-select-size --- .../statviz/components/filter/MovementFilters.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/shared-components/statviz/components/filter/MovementFilters.tsx b/shared-components/statviz/components/filter/MovementFilters.tsx index d19cf6ec7..26b4b9a8b 100644 --- a/shared-components/statviz/components/filter/MovementFilters.tsx +++ b/shared-components/statviz/components/filter/MovementFilters.tsx @@ -91,20 +91,30 @@ export function MovementFilters({ From setStaged((prev) => ({ ...prev, dateFrom: e.target.value }))} + border="2px" + borderRadius="0" + borderColor="gray.300" + _hover={{ borderColor: "gray.300" }} + _focus={{ borderColor: "gray.300", boxShadow: "none" }} /> To setStaged((prev) => ({ ...prev, dateTo: e.target.value }))} + border="2px" + borderRadius="0" + borderColor="gray.300" + _hover={{ borderColor: "gray.300" }} + _focus={{ borderColor: "gray.300", boxShadow: "none" }} /> From a55591cbe915f9e282c85a9094d9d20574668437 Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Tue, 23 Jun 2026 08:09:18 +0200 Subject: [PATCH 13/30] pie-visuals --- shared-components/statviz/components/nivo/PieChart.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/shared-components/statviz/components/nivo/PieChart.tsx b/shared-components/statviz/components/nivo/PieChart.tsx index 90bd61624..aba5690b3 100644 --- a/shared-components/statviz/components/nivo/PieChart.tsx +++ b/shared-components/statviz/components/nivo/PieChart.tsx @@ -71,7 +71,7 @@ export default function PieChart(chart: IPieChart) { layers.push(() => {chart.timestamp}); } if (chart.centerData) { - const y = (height - margin.top - margin.bottom) / 2; + const y = (height - margin.top - margin.bottom) / 1.8; const x = (width - margin.right - margin.left) / 2; const textMaxWidth = baseFontSize * 7; // same as 7em const fontSizeGroupingText = baseFontSize * 0.8; // 0.8em @@ -101,13 +101,14 @@ export default function PieChart(chart: IPieChart) { margin={margin} borderWidth={0} arcLabel="value" - innerRadius={0.4} + innerRadius={0.6} layers={layers} padAngle={0.7} cornerRadius={3} animate={chart.animate === true || chart.animate === null} onClick={chart.onClick} theme={theme} + // colors={{ scheme: 'tableau10' }} isInteractive activeOuterRadiusOffset={2} arcLinkLabelsSkipAngle={10} @@ -116,7 +117,7 @@ export default function PieChart(chart: IPieChart) { arcLinkLabelsColor={{ from: "color" }} arcLabelsSkipAngle={10} arcLinkLabel={arcLabel} - arcLinkLabelsDiagonalLength={10} + arcLinkLabelsDiagonalLength={20} arcLinkLabelsStraightLength={16} /> From 6f1b93835c6fd3b51199bec79f19d6c5c66d022b Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Tue, 23 Jun 2026 15:50:05 +0200 Subject: [PATCH 14/30] Align multiple graphs --- .../CreatedBoxesFilterContainer.tsx | 22 ++++++++----------- .../stock/StockOverviewRing.tsx | 1 + .../StockOverviewRingFilterContainer.tsx | 15 ++++++++----- .../statviz/dashboard/ItemsAndBoxes.tsx | 16 +++++++++----- 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesFilterContainer.tsx b/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesFilterContainer.tsx index 46a9e7984..db8be604b 100644 --- a/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesFilterContainer.tsx +++ b/shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesFilterContainer.tsx @@ -1,4 +1,4 @@ -import { Wrap, WrapItem, Box } from "@chakra-ui/react"; +import { Box } from "@chakra-ui/react"; import { useMemo } from "react"; import { TidyFn, filter, tidy } from "@tidyjs/tidy"; import CreatedBoxes from "./CreatedBoxes"; @@ -72,17 +72,13 @@ export default function CreatedBoxesFilterContainer({ }; return ( - - - - - - - + + + ); } diff --git a/shared-components/statviz/components/visualizations/stock/StockOverviewRing.tsx b/shared-components/statviz/components/visualizations/stock/StockOverviewRing.tsx index e7bb59508..f2a3a5302 100644 --- a/shared-components/statviz/components/visualizations/stock/StockOverviewRing.tsx +++ b/shared-components/statviz/components/visualizations/stock/StockOverviewRing.tsx @@ -138,6 +138,7 @@ export default function StockOverviewRing({ onFilterChange={onFilterChange} filterId={ringGroupingUrlId} fieldLabel="Group by" + inlineLabel={true} /> diff --git a/shared-components/statviz/components/visualizations/stock/StockOverviewRingFilterContainer.tsx b/shared-components/statviz/components/visualizations/stock/StockOverviewRingFilterContainer.tsx index 94c52c2a6..de6dc0cdc 100644 --- a/shared-components/statviz/components/visualizations/stock/StockOverviewRingFilterContainer.tsx +++ b/shared-components/statviz/components/visualizations/stock/StockOverviewRingFilterContainer.tsx @@ -1,3 +1,4 @@ +import { Box } from "@chakra-ui/react"; import { useMemo } from "react"; import { StockOverview, StockOverviewResult } from "../../../../../graphql/types"; import { filterByTags } from "../../../utils/filterByTags"; @@ -49,11 +50,13 @@ export default function StockOverviewRingFilterContainer({ }, [stockOverview, genders, categories, locations, products, includedTags, excludedTags]); return ( - + + + ); } diff --git a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx index 48653beb4..595d95e2f 100644 --- a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx +++ b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx @@ -1,4 +1,5 @@ import { + SimpleGrid, useDisclosure, Box, AccordionItem, @@ -107,11 +108,16 @@ export default function ItemsAndBoxes({ /> - - + + + + From f6df2ac23c06a9abdd2abd6b3a0380eb7f5320d5 Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Tue, 23 Jun 2026 17:16:27 +0200 Subject: [PATCH 15/30] show-share-link-action --- .../components/visualizations/stock/StockOverviewRing.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/shared-components/statviz/components/visualizations/stock/StockOverviewRing.tsx b/shared-components/statviz/components/visualizations/stock/StockOverviewRing.tsx index f2a3a5302..603d4568e 100644 --- a/shared-components/statviz/components/visualizations/stock/StockOverviewRing.tsx +++ b/shared-components/statviz/components/visualizations/stock/StockOverviewRing.tsx @@ -128,6 +128,7 @@ export default function StockOverviewRing({ chartProps={chartProps} maxWidthPx={600} customIncludes={[{ prop: { centerData }, value: "include center data" }]} + view="StockOverview" /> From 00a6ca8cd4fd08a6d7c9c52809c56c64f60ae34f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:54:42 +0000 Subject: [PATCH 16/30] Replace StockDataFilter with StockOverviewRingFilterContainer in shared-front/App.tsx Co-authored-by: pylipp <10617122+pylipp@users.noreply.github.com> --- shared-front/src/App.tsx | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/shared-front/src/App.tsx b/shared-front/src/App.tsx index d1a67482f..5b80de5bc 100644 --- a/shared-front/src/App.tsx +++ b/shared-front/src/App.tsx @@ -1,20 +1,18 @@ -import { ReactNode, useEffect } from "react"; +import { ReactNode } from "react"; import { gql, useQuery } from "@apollo/client"; +import { useSearchParams } from "react-router-dom"; import { Alert, AlertIcon, Flex, Heading, Skeleton, Center, WrapItem } from "@chakra-ui/react"; import BoxtributeLogo from "./BoxtributeLogo"; -import StockDataFilter from "@boxtribute/shared-components/statviz/components/visualizations/stock/StockDataFilter"; +import StockOverviewRingFilterContainer from "@boxtribute/shared-components/statviz/components/visualizations/stock/StockOverviewRingFilterContainer"; import ErrorCard, { predefinedErrors, } from "@boxtribute/shared-components/statviz/components/ErrorCard"; -import { - tagFilterIncludedValuesVar, - tagFilterExcludedValuesVar, -} from "@boxtribute/shared-components/statviz/state/filter"; -import { tagToFilterValue } from "@boxtribute/shared-components/statviz/components/filter/TagFilter"; import BoxesOrItemsSelect, { boxesOrItemsFilterValues, + type BoxesOrItems, } from "@boxtribute/shared-components/statviz/components/filter/BoxesOrItemsSelect"; +import { DEFAULT_STOCK_FILTERS } from "@boxtribute/shared-components/statviz/utils/dashboardFilters"; const RESOLVE_LINK = gql(` query resolveLink($code: String!) { @@ -95,16 +93,13 @@ function App() { const code = searchParams.get("code"); const view = searchParams.get("view"); - const { data, loading, error } = useQuery(RESOLVE_LINK, { variables: { code } }); + const [routerSearchParams] = useSearchParams(); + const boiUrlId = routerSearchParams.get("boi"); + const boxesOrItems: BoxesOrItems = ( + boxesOrItemsFilterValues.find((f) => f.urlId === boiUrlId) ?? boxesOrItemsFilterValues[0] + ).value; - // Get tag filters. - useEffect(() => { - const tags = data?.resolveLink?.data[0].dimensions?.tag?.map((t) => tagToFilterValue(t!)); - if (tags?.length) { - tagFilterIncludedValuesVar(tags); - tagFilterExcludedValuesVar(tags); - } - }, [data?.resolveLink?.data]); + const { data, loading, error } = useQuery(RESOLVE_LINK, { variables: { code } }); if (error) { return {matchErrorMessage(error.message)}; @@ -168,7 +163,11 @@ function App() { {/* TODO: Match view with view returned from data once other views are implemented. */} - + ); } From 586d493fdaaa10ef84aedf7bcac6e218c40d4985 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:23:28 +0000 Subject: [PATCH 17/30] Infer appliedFilters from URL params in shared-front/App.tsx Co-authored-by: pylipp <10617122+pylipp@users.noreply.github.com> --- shared-front/src/App.tsx | 47 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/shared-front/src/App.tsx b/shared-front/src/App.tsx index 5b80de5bc..1067d1a2c 100644 --- a/shared-front/src/App.tsx +++ b/shared-front/src/App.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from "react"; +import { ReactNode, useMemo } from "react"; import { gql, useQuery } from "@apollo/client"; import { useSearchParams } from "react-router-dom"; import { Alert, AlertIcon, Flex, Heading, Skeleton, Center, WrapItem } from "@chakra-ui/react"; @@ -12,7 +12,12 @@ import BoxesOrItemsSelect, { boxesOrItemsFilterValues, type BoxesOrItems, } from "@boxtribute/shared-components/statviz/components/filter/BoxesOrItemsSelect"; -import { DEFAULT_STOCK_FILTERS } from "@boxtribute/shared-components/statviz/utils/dashboardFilters"; +import { + readStockFiltersFromUrl, + type ICategoryOption, + type ILocationOption, + type ITagOption, +} from "@boxtribute/shared-components/statviz/utils/dashboardFilters"; const RESOLVE_LINK = gql(` query resolveLink($code: String!) { @@ -101,6 +106,42 @@ function App() { const { data, loading, error } = useQuery(RESOLVE_LINK, { variables: { code } }); + const allCategories = useMemo( + () => + (data?.resolveLink?.data[0]?.dimensions?.category ?? []).map((c) => ({ + id: Number(c.id), + name: c.name ?? "", + })), + [data], + ); + + const allLocations = useMemo( + () => + (data?.resolveLink?.data[0]?.dimensions?.location ?? []).map((l) => ({ + id: Number(l.id), + name: l.name ?? "", + })), + [data], + ); + + const allTags = useMemo( + () => + (data?.resolveLink?.data[0]?.dimensions?.tag ?? []).map((t) => ({ + id: Number(t.id), + name: t.name ?? "", + color: t.color ?? "#999", + value: String(t.id), + label: t.name ?? "", + urlId: String(t.id), + })), + [data], + ); + + const appliedFilters = useMemo( + () => readStockFiltersFromUrl(routerSearchParams, [], allCategories, allLocations, allTags), + [routerSearchParams, allCategories, allLocations, allTags], + ); + if (error) { return {matchErrorMessage(error.message)}; } @@ -165,7 +206,7 @@ function App() { {/* TODO: Match view with view returned from data once other views are implemented. */} From 48355567c0c0507cc6f4d44fa96cd2f846317214 Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Tue, 23 Jun 2026 17:47:51 +0200 Subject: [PATCH 18/30] no-spinner --- shared-components/statviz/dashboard/Dashboard.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/shared-components/statviz/dashboard/Dashboard.tsx b/shared-components/statviz/dashboard/Dashboard.tsx index e75bb210f..f3c0c9c6f 100644 --- a/shared-components/statviz/dashboard/Dashboard.tsx +++ b/shared-components/statviz/dashboard/Dashboard.tsx @@ -1,4 +1,4 @@ -import { Accordion, Heading, Spinner } from "@chakra-ui/react"; +import { Accordion, Heading } from "@chakra-ui/react"; import { useQuery } from "@apollo/client"; import { useParams } from "react-router-dom"; import { useMemo } from "react"; @@ -42,7 +42,7 @@ export const DASHBOARD_FILTER_DATA_QUERY = graphql(` export default function Dashboard() { const { baseId } = useParams(); - const { data, loading, error } = useQuery(DASHBOARD_FILTER_DATA_QUERY, { + const { data, error } = useQuery(DASHBOARD_FILTER_DATA_QUERY, { variables: { baseId: baseId! }, }); @@ -99,7 +99,6 @@ export default function Dashboard() {
Dashboard - {loading && } Date: Thu, 25 Jun 2026 13:33:14 +0200 Subject: [PATCH 19/30] Remove unused code --- .../filter/GenderProductFilter.test.tsx | 68 --------- .../components/filter/GenderProductFilter.tsx | 138 ------------------ .../components/filter/LocationFilter.tsx | 35 ----- .../components/filter/MovementFilters.tsx | 2 +- .../components/filter/StockFilters.tsx | 2 +- .../filter/TabbedTagFilter.test.tsx | 69 --------- .../components/filter/TabbedTagFilter.tsx | 46 ------ .../statviz/components/filter/TagFilter.tsx | 2 + .../statviz/components/filter/constants.ts | 55 +++++++ .../movedBoxes/BoxFlowSankey.tsx | 6 - .../stock/StockDataContainer.tsx | 23 --- .../stock/StockDataFilter.test.tsx | 133 ----------------- .../visualizations/stock/StockDataFilter.tsx | 58 -------- .../statviz/hooks/useShareableLink.ts | 2 +- shared-components/statviz/state/filter.tsx | 20 --- 15 files changed, 60 insertions(+), 599 deletions(-) delete mode 100644 shared-components/statviz/components/filter/GenderProductFilter.test.tsx delete mode 100644 shared-components/statviz/components/filter/GenderProductFilter.tsx delete mode 100644 shared-components/statviz/components/filter/LocationFilter.tsx delete mode 100644 shared-components/statviz/components/filter/TabbedTagFilter.test.tsx delete mode 100644 shared-components/statviz/components/filter/TabbedTagFilter.tsx create mode 100644 shared-components/statviz/components/filter/constants.ts delete mode 100644 shared-components/statviz/components/visualizations/stock/StockDataFilter.test.tsx delete mode 100644 shared-components/statviz/components/visualizations/stock/StockDataFilter.tsx diff --git a/shared-components/statviz/components/filter/GenderProductFilter.test.tsx b/shared-components/statviz/components/filter/GenderProductFilter.test.tsx deleted file mode 100644 index 15af5425b..000000000 --- a/shared-components/statviz/components/filter/GenderProductFilter.test.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { BrowserRouter } from "react-router-dom"; -import { ChakraProvider } from "@chakra-ui/react"; -import { MockedProvider } from "@apollo/client/testing"; -import GenderProductFilter, { - categoryFilterId, - categoryToFilterValue, -} from "./GenderProductFilter"; - -const TestWrapper = ({ children }: { children: React.ReactNode }) => ( - - - {children} - - -); - -describe("GenderProductFilter", () => { - it("renders the category filter dropdown", () => { - render( - - - , - ); - - // Check that the category filter is rendered - expect(screen.getByLabelText("product category")).toBeInTheDocument(); - }); - - it("has correct categoryFilterId", () => { - expect(categoryFilterId).toBe("cf"); - }); - - it("categoryToFilterValue converts category correctly", () => { - const mockCategory = { - id: 1, - name: "Clothing", - }; - - const result = categoryToFilterValue(mockCategory); - - expect(result).toEqual({ - id: 1, - value: "1", - name: "Clothing", - label: "Clothing", - urlId: "1", - }); - }); - - it("categoryToFilterValue handles non-null fields", () => { - const mockCategory = { - id: 2, - name: "Footwear", - }; - - const result = categoryToFilterValue(mockCategory); - - expect(result).toEqual({ - id: 2, - value: "2", - name: "Footwear", - label: "Footwear", - urlId: "2", - }); - }); -}); diff --git a/shared-components/statviz/components/filter/GenderProductFilter.tsx b/shared-components/statviz/components/filter/GenderProductFilter.tsx deleted file mode 100644 index 464b0f0c0..000000000 --- a/shared-components/statviz/components/filter/GenderProductFilter.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { useReactiveVar } from "@apollo/client"; -import { Wrap, WrapItem } from "@chakra-ui/react"; -import MultiSelectFilter from "./MultiSelectFilter"; -import { IFilterValue } from "./ValueFilter"; -import useMultiSelectFilter from "../../hooks/useMultiSelectFilter"; -import { - IProductFilterValue, - productFilterValuesVar, - ICategoryFilterValue, - categoryFilterValuesVar, -} from "../../state/filter"; -import { Product } from "../../../../graphql/types"; - -export const genders: IFilterValue[] = [ - { - value: "Boy", - label: "Boy", - urlId: "boy", - }, - { - value: "Girl", - label: "Girl", - urlId: "girl", - }, - { - value: "Men", - label: "Men", - urlId: "men", - }, - { - value: "None", - label: "None", - urlId: "none", - }, - { - value: "Teen Boy", - label: "Teen Boy", - urlId: "tb", - }, - { - value: "Teen Girl", - label: "Teen Girl", - urlId: "tg", - }, - { - value: "UnisexAdult", - label: "UnisexAdult", - urlId: "ua", - }, - { - value: "Unisex Baby", - label: "Unisex Baby", - urlId: "ub", - }, - { - value: "Unisex Kid", - label: "Unisex Kid", - urlId: "uk", - }, - { - value: "Women", - label: "Women", - urlId: "women", - }, -]; - -export const genderFilterId = "gf"; -export const productFilterId = "pf"; -export const categoryFilterId = "cf"; - -export const productToFilterValue = (product: Product): IProductFilterValue => ({ - id: product.id!, - value: product.id!.toString(), - name: product.name!, - label: `${product.name!} (${product.gender!})`, - urlId: product.id!.toString(), - gender: product.gender!, -}); - -export const categoryToFilterValue = (category: { - id: number | null; - name: string | null; -}): ICategoryFilterValue => ({ - id: category.id!, - value: category.id!.toString(), - name: category.name!, - label: category.name!, - urlId: category.id!.toString(), -}); - -export default function GenderProductFilter() { - const productFilterValues = useReactiveVar(productFilterValuesVar); - const categoryFilterValues = useReactiveVar(categoryFilterValuesVar); - - const { onFilterChange: onProductFilterChange, filterValue: productFilterValue } = - useMultiSelectFilter(productFilterValues, productFilterId); - - const { onFilterChange: onGenderFilterChange, filterValue: genderFilterValue } = - useMultiSelectFilter(genders, genderFilterId); - - const { onFilterChange: onCategoryFilterChange, filterValue: categoryFilterValue } = - useMultiSelectFilter(categoryFilterValues, categoryFilterId); - - return ( - - - - - - - - - - - - ); -} diff --git a/shared-components/statviz/components/filter/LocationFilter.tsx b/shared-components/statviz/components/filter/LocationFilter.tsx deleted file mode 100644 index 4ae423405..000000000 --- a/shared-components/statviz/components/filter/LocationFilter.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useReactiveVar } from "@apollo/client"; -import { ResultOf } from "gql.tada"; -import useMultiSelectFilter from "../../hooks/useMultiSelectFilter"; -import { ITargetFilterValue, targetFilterValuesVar } from "../../state/filter"; -import MultiSelectFilter from "./MultiSelectFilter"; -import { TARGET_DIMENSION_INFO_FRAGMENT } from "../../queries/fragments"; - -export const targetFilterId = "loc"; - -export const targetToFilterValue = ( - target: ResultOf, -): ITargetFilterValue => ({ - id: target.id!, - value: target.name!, - label: target.name!, - urlId: target.id!, - type: target.type!, -}); - -export default function Targetfilter() { - const targetFilterValues = useReactiveVar(targetFilterValuesVar); - - const { onFilterChange, filterValue } = useMultiSelectFilter(targetFilterValues, targetFilterId); - - return ( - - ); -} diff --git a/shared-components/statviz/components/filter/MovementFilters.tsx b/shared-components/statviz/components/filter/MovementFilters.tsx index 26b4b9a8b..4b6406dcd 100644 --- a/shared-components/statviz/components/filter/MovementFilters.tsx +++ b/shared-components/statviz/components/filter/MovementFilters.tsx @@ -8,7 +8,7 @@ import type { ITagOption, MovementAppliedFilters, } from "../../utils/dashboardFilters"; -import { genders } from "./GenderProductFilter"; +import { genders } from "./constants"; import type { IFilterValue } from "./ValueFilter"; function toProductFilterValues(products: IProductOption[]): IFilterValue[] { diff --git a/shared-components/statviz/components/filter/StockFilters.tsx b/shared-components/statviz/components/filter/StockFilters.tsx index 85490217b..c35f6d79e 100644 --- a/shared-components/statviz/components/filter/StockFilters.tsx +++ b/shared-components/statviz/components/filter/StockFilters.tsx @@ -9,7 +9,7 @@ import type { ITagOption, StockAppliedFilters, } from "../../utils/dashboardFilters"; -import { genders } from "./GenderProductFilter"; +import { genders } from "./constants"; import type { IFilterValue } from "./ValueFilter"; function toFilterValues(items: { id: number; name: string }[]): IFilterValue[] { diff --git a/shared-components/statviz/components/filter/TabbedTagFilter.test.tsx b/shared-components/statviz/components/filter/TabbedTagFilter.test.tsx deleted file mode 100644 index e2d1bfd35..000000000 --- a/shared-components/statviz/components/filter/TabbedTagFilter.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { BrowserRouter } from "react-router-dom"; -import { ChakraProvider } from "@chakra-ui/react"; -import { MockedProvider } from "@apollo/client/testing"; -import TabbedTagFilter, { tagFilterIncludedId, tagFilterExcludedId } from "./TabbedTagFilter"; -import { tagToFilterValue } from "./TagFilter"; - -const TestWrapper = ({ children }: { children: React.ReactNode }) => ( - - - {children} - - -); - -describe("TabbedTagFilter", () => { - it("renders the tag filter component", () => { - render( - - - , - ); - - // Check that the tags label is rendered - expect(screen.getByText("tags")).toBeInTheDocument(); - }); - - it("has correct filter IDs", () => { - expect(tagFilterIncludedId).toBe("tags"); - expect(tagFilterExcludedId).toBe("notags"); - }); - - it("tagToFilterValue converts tag correctly", () => { - const mockTag = { - id: 1, - name: "Urgent", - color: "#ff0000", - }; - - const result = tagToFilterValue(mockTag); - - expect(result).toEqual({ - id: 1, - value: "1", - label: "Urgent", - color: "#ff0000", - urlId: "1", - }); - }); - - it("tagToFilterValue handles different tag data", () => { - const mockTag = { - id: 42, - name: "Priority", - color: "#00ff00", - }; - - const result = tagToFilterValue(mockTag); - - expect(result).toEqual({ - id: 42, - value: "42", - label: "Priority", - color: "#00ff00", - urlId: "42", - }); - }); -}); diff --git a/shared-components/statviz/components/filter/TabbedTagFilter.tsx b/shared-components/statviz/components/filter/TabbedTagFilter.tsx deleted file mode 100644 index da38753e9..000000000 --- a/shared-components/statviz/components/filter/TabbedTagFilter.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Box, FormLabel } from "@chakra-ui/react"; -import { useReactiveVar } from "@apollo/client"; -import { - tagFilterIncludedValuesVar, - tagFilterExcludedValuesVar, - ITagFilterValue, -} from "../../state/filter"; -import useMultiSelectFilter from "../../hooks/useMultiSelectFilter"; -import TabbedTagDropdown from "./TabbedTagDropdown"; -import { tagFilterId } from "./TagFilter"; - -export const tagFilterIncludedId = tagFilterId; -export const tagFilterExcludedId = "notags"; - -export default function TabbedTagFilter() { - const includedTagFilterValues = useReactiveVar(tagFilterIncludedValuesVar); - const excludedTagFilterValues = useReactiveVar(tagFilterExcludedValuesVar); - - const { - includedFilterValue: includedTags, - excludedFilterValue: excludedTags, - onIncludedFilterChange, - onExcludedFilterChange, - onClearAll, - } = useMultiSelectFilter( - includedTagFilterValues, - tagFilterIncludedId, - excludedTagFilterValues, - tagFilterExcludedId, - ); - - return ( - - tags - - - ); -} diff --git a/shared-components/statviz/components/filter/TagFilter.tsx b/shared-components/statviz/components/filter/TagFilter.tsx index d3785c108..bec48e5bd 100644 --- a/shared-components/statviz/components/filter/TagFilter.tsx +++ b/shared-components/statviz/components/filter/TagFilter.tsx @@ -7,6 +7,8 @@ import { ITagFilterValue, tagFilterIncludedValuesVar } from "../../state/filter" import { TAG_DIMENSION_INFO_FRAGMENT } from "../../queries/fragments"; export const tagFilterId = "tags"; +export const tagFilterIncludedId = tagFilterId; +export const tagFilterExcludedId = "notags"; export const tagToFilterValue = ( tag: ResultOf, diff --git a/shared-components/statviz/components/filter/constants.ts b/shared-components/statviz/components/filter/constants.ts new file mode 100644 index 000000000..ec5e8677e --- /dev/null +++ b/shared-components/statviz/components/filter/constants.ts @@ -0,0 +1,55 @@ +import { IFilterValue } from "./ValueFilter"; + +export const genders: IFilterValue[] = [ + { + value: "Boy", + label: "Boy", + urlId: "boy", + }, + { + value: "Girl", + label: "Girl", + urlId: "girl", + }, + { + value: "Men", + label: "Men", + urlId: "men", + }, + { + value: "None", + label: "None", + urlId: "none", + }, + { + value: "Teen Boy", + label: "Teen Boy", + urlId: "tb", + }, + { + value: "Teen Girl", + label: "Teen Girl", + urlId: "tg", + }, + { + value: "UnisexAdult", + label: "UnisexAdult", + urlId: "ua", + }, + { + value: "Unisex Baby", + label: "Unisex Baby", + urlId: "ub", + }, + { + value: "Unisex Kid", + label: "Unisex Kid", + urlId: "uk", + }, + { + value: "Women", + label: "Women", + urlId: "women", + }, +]; + diff --git a/shared-components/statviz/components/visualizations/movedBoxes/BoxFlowSankey.tsx b/shared-components/statviz/components/visualizations/movedBoxes/BoxFlowSankey.tsx index 9d18f3cf1..1bba3cdb7 100644 --- a/shared-components/statviz/components/visualizations/movedBoxes/BoxFlowSankey.tsx +++ b/shared-components/statviz/components/visualizations/movedBoxes/BoxFlowSankey.tsx @@ -7,7 +7,6 @@ import SankeyChart, { ISankeyData } from "../../nivo/SankeyChart"; import getOnExport from "../../../utils/chartExport"; import { BoxesOrItemsCount } from "../../../dashboard/ItemsAndBoxes"; import NoDataCard from "../../NoDataCard"; -import Targetfilter from "../../filter/LocationFilter"; import { MovedBoxes, MovedBoxesResult } from "../../../../../graphql/types"; import { TARGET_DIMENSION_INFO_FRAGMENT } from "../../../queries/fragments"; @@ -185,11 +184,6 @@ export default function BoxFlowSankey({ width, height, data, boxesOrItems }: IBo maxWidthPx={1000} /> - - - - - diff --git a/shared-components/statviz/components/visualizations/stock/StockDataContainer.tsx b/shared-components/statviz/components/visualizations/stock/StockDataContainer.tsx index f1c544072..b0275a6c7 100644 --- a/shared-components/statviz/components/visualizations/stock/StockDataContainer.tsx +++ b/shared-components/statviz/components/visualizations/stock/StockDataContainer.tsx @@ -1,8 +1,3 @@ -import { useQuery } from "@apollo/client"; -import { Box, Spinner } from "@chakra-ui/react"; -import { useParams } from "react-router-dom"; -import StockDataFilter from "./StockDataFilter"; -import ErrorCard, { predefinedErrors } from "../../ErrorCard"; import { graphql } from "../../../../../graphql/graphql"; import { TAG_FRAGMENT } from "../../../queries/fragments"; @@ -43,21 +38,3 @@ export const STOCK_QUERY = graphql( `, [TAG_FRAGMENT], ); - -export default function StockDataContainer() { - const { baseId } = useParams(); - const { data, loading, error } = useQuery(STOCK_QUERY, { - variables: { baseId: parseInt(baseId!, 10) }, - }); - - if (error) { - return An unexpected error happened {error.message}; - } - if (loading) { - return ; - } - if (data === undefined) { - return ; - } - return ; -} diff --git a/shared-components/statviz/components/visualizations/stock/StockDataFilter.test.tsx b/shared-components/statviz/components/visualizations/stock/StockDataFilter.test.tsx deleted file mode 100644 index 787ec8937..000000000 --- a/shared-components/statviz/components/visualizations/stock/StockDataFilter.test.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { it, expect } from "vitest"; -import { filter, tidy } from "@tidyjs/tidy"; - -import { userEvent } from "@testing-library/user-event"; -import { render, screen } from "../../../../tests/testUtils"; - -import StockDataFilter from "./StockDataFilter"; -import { StockOverviewResult } from "../../../../../graphql/types"; - -it("x.x.x.x - User clicks on 'Gender' filter in drilldown chart", async () => { - render( - , - { - routePath: "/bases/:baseId/", - initialUrl: "/bases/1/", - }, - ); - - const filterDropdown = screen.getByRole("combobox"); - await userEvent.click(filterDropdown); - - const dropdownOption = screen.getByText(/gender/i); - expect(dropdownOption).toBeInTheDocument(); - - await userEvent.click(dropdownOption); - - expect(await screen.findByText(/Drilldown Chart of Instock Boxes/)).toBeInTheDocument(); -}); - -it("should filter out only items with boxState === 'InStock'", () => { - // TODO: Make the data be returned in the mocks - const data: Partial[] = [ - { - __typename: "StockOverviewResult", - boxState: "InStock", - boxesCount: 1, - categoryId: 1, - gender: "UnisexAdult", - itemsCount: 5, - locationId: 100000036, - productName: "underwear", - sizeId: 42, - tagIds: [45], - }, - { - __typename: "StockOverviewResult", - boxState: "Donated", - boxesCount: 20, - categoryId: 2, - gender: "UnisexAdult", - itemsCount: 8, - locationId: 100000036, - productName: "underwear", - sizeId: 38, - tagIds: [3], - }, - { - __typename: "StockOverviewResult", - boxState: "InStock", - boxesCount: 15, - categoryId: 1, - gender: "UnisexAdult", - itemsCount: 6, - locationId: 100000036, - productName: "underwear", - sizeId: 40, - tagIds: [1, 4], - }, - ]; - - const inStockFilter = filter((fact: StockOverviewResult) => fact.boxState === "InStock"); - const filteredData = tidy(data, inStockFilter) as StockOverviewResult[]; - - expect(filteredData.length).toBe(2); - expect(filteredData.every((fact) => fact.boxState === "InStock")).toBe(true); -}); diff --git a/shared-components/statviz/components/visualizations/stock/StockDataFilter.tsx b/shared-components/statviz/components/visualizations/stock/StockDataFilter.tsx deleted file mode 100644 index 6178c33bb..000000000 --- a/shared-components/statviz/components/visualizations/stock/StockDataFilter.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useReactiveVar } from "@apollo/client"; -import { useMemo } from "react"; -import StockCharts from "./StockCharts"; -import { - boxesOrItemsFilterValues, - boxesOrItemsUrlId, - defaultBoxesOrItems, -} from "../../filter/BoxesOrItemsSelect"; -import useValueFilter from "../../../hooks/useValueFilter"; -import { tagFilterIncludedValuesVar, tagFilterExcludedValuesVar } from "../../../state/filter"; -import { tagFilterIncludedId, tagFilterExcludedId } from "../../filter/TabbedTagFilter"; -import useMultiSelectFilter from "../../../hooks/useMultiSelectFilter"; -import { filterByTags } from "../../../utils/filterByTags"; -import { StockOverview, StockOverviewResult } from "../../../../../graphql/types"; - -interface IStockDataFilterProps { - stockOverview: StockOverview; -} - -export default function StockDataFilter({ stockOverview }: IStockDataFilterProps) { - // currently not affected by the selected timerange - - const includedTagFilterValues = useReactiveVar(tagFilterIncludedValuesVar); - const excludedTagFilterValues = useReactiveVar(tagFilterExcludedValuesVar); - - const { filterValue } = useValueFilter( - boxesOrItemsFilterValues, - defaultBoxesOrItems, - boxesOrItemsUrlId, - ); - - const { includedFilterValue: includedTags, excludedFilterValue: excludedTags } = - useMultiSelectFilter( - includedTagFilterValues, - tagFilterIncludedId, - excludedTagFilterValues, - tagFilterExcludedId, - ); - - const filteredStockOverview = useMemo(() => { - // Filter by included and excluded tags - const tagFilteredFacts = filterByTags( - (stockOverview?.facts ?? []) as StockOverviewResult[], - includedTags, - excludedTags, - ); - - // Filter by box state - const inStockFacts = tagFilteredFacts.filter((fact) => fact.boxState === "InStock"); - - return { - ...stockOverview, - facts: inStockFacts, - } as StockOverview; - }, [includedTags, excludedTags, stockOverview]); - - return ; -} diff --git a/shared-components/statviz/hooks/useShareableLink.ts b/shared-components/statviz/hooks/useShareableLink.ts index b1075a9c7..456203f82 100644 --- a/shared-components/statviz/hooks/useShareableLink.ts +++ b/shared-components/statviz/hooks/useShareableLink.ts @@ -12,7 +12,7 @@ import { defaultBoxesOrItems, IBoxesOrItemsFilter, } from "../components/filter/BoxesOrItemsSelect"; -import { tagFilterIncludedId, tagFilterExcludedId } from "../components/filter/TabbedTagFilter"; +import { tagFilterIncludedId, tagFilterExcludedId } from "../components/filter/TagFilter"; import { tagFilterIncludedValuesVar, tagFilterExcludedValuesVar } from "../state/filter"; import useMultiSelectFilter from "./useMultiSelectFilter"; diff --git a/shared-components/statviz/state/filter.tsx b/shared-components/statviz/state/filter.tsx index 204744035..92a0c0ac4 100644 --- a/shared-components/statviz/state/filter.tsx +++ b/shared-components/statviz/state/filter.tsx @@ -1,13 +1,5 @@ import { makeVar } from "@apollo/client"; import { IFilterValue } from "../components/filter/ValueFilter"; -import { ProductGender, TargetType } from "../../../graphql/types"; - -export interface IProductFilterValue extends IFilterValue { - id: number; - name: string; - gender: ProductGender; -} -export const productFilterValuesVar = makeVar([]); export interface ITagFilterValue extends IFilterValue { color: string; @@ -18,15 +10,3 @@ export const tagFilterIncludedValuesVar = makeVar([]); // Reactive variable for excluded tags (tags to filter data OUT) export const tagFilterExcludedValuesVar = makeVar([]); - -export interface ITargetFilterValue extends IFilterValue { - id: string; - type: TargetType; -} -export const targetFilterValuesVar = makeVar([]); - -export interface ICategoryFilterValue extends IFilterValue { - id: number; - name: string; -} -export const categoryFilterValuesVar = makeVar([]); From ed04a195ae1c7034423000a67275cbc35de75c29 Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Thu, 25 Jun 2026 15:33:16 +0200 Subject: [PATCH 20/30] format --- shared-components/statviz/components/filter/constants.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/shared-components/statviz/components/filter/constants.ts b/shared-components/statviz/components/filter/constants.ts index ec5e8677e..c5c3235c3 100644 --- a/shared-components/statviz/components/filter/constants.ts +++ b/shared-components/statviz/components/filter/constants.ts @@ -52,4 +52,3 @@ export const genders: IFilterValue[] = [ urlId: "women", }, ]; - From 538559e026e3862cc2b9f9514ccf9bfdbfda93b4 Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Thu, 25 Jun 2026 15:42:20 +0200 Subject: [PATCH 21/30] import --- .../components/AddItemsToPackingList/AddItemsToPackingList.tsx | 1 + shared-components/statviz/dashboard/ItemsAndBoxes.tsx | 1 + shared-components/statviz/dashboard/MovedBoxes.tsx | 1 + 3 files changed, 3 insertions(+) diff --git a/front/src/views/Distributions/components/AddItemsToPackingList/AddItemsToPackingList.tsx b/front/src/views/Distributions/components/AddItemsToPackingList/AddItemsToPackingList.tsx index 1ae013370..55892e3e4 100644 --- a/front/src/views/Distributions/components/AddItemsToPackingList/AddItemsToPackingList.tsx +++ b/front/src/views/Distributions/components/AddItemsToPackingList/AddItemsToPackingList.tsx @@ -15,6 +15,7 @@ import { } from "@chakra-ui/react"; import _ from "lodash"; import { useContext, useState } from "react"; +import type React from "react"; import { DistroEventDetailsForPlanningStateContext } from "views/Distributions/DistroEventView/components/State1Planning/DistroEventDetailsForPlanningStateContainer"; import { IPackingListEntry } from "views/Distributions/types"; import { ProductGender } from "../../../../../../graphql/types"; diff --git a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx index 595d95e2f..3972c169b 100644 --- a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx +++ b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx @@ -12,6 +12,7 @@ import { VStack, } from "@chakra-ui/react"; import { useCallback, useMemo } from "react"; +import type React from "react"; import { useSearchParams } from "react-router-dom"; import CreatedBoxesDataContainer from "../components/visualizations/createdBoxes/CreatedBoxesDataContainer"; import StockOverviewRingDataContainer from "../components/visualizations/stock/StockOverviewRingDataContainer"; diff --git a/shared-components/statviz/dashboard/MovedBoxes.tsx b/shared-components/statviz/dashboard/MovedBoxes.tsx index 5da8c3225..9ff2b250b 100644 --- a/shared-components/statviz/dashboard/MovedBoxes.tsx +++ b/shared-components/statviz/dashboard/MovedBoxes.tsx @@ -11,6 +11,7 @@ import { VStack, } from "@chakra-ui/react"; import { useCallback, useMemo } from "react"; +import type React from "react"; import { useSearchParams } from "react-router-dom"; import MovedBoxesDataContainer from "../components/visualizations/movedBoxes/MovedBoxesDataContainer"; import { FilterPanel } from "./../components/filter/FilterPanel"; From 60f1a7eb5ff35824e549d5c375bc54664b1b44ff Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Thu, 25 Jun 2026 15:42:29 +0200 Subject: [PATCH 22/30] filterpanel --- shared-components/statviz/components/filter/FilterPanel.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shared-components/statviz/components/filter/FilterPanel.tsx b/shared-components/statviz/components/filter/FilterPanel.tsx index c1a68abac..31b174a83 100644 --- a/shared-components/statviz/components/filter/FilterPanel.tsx +++ b/shared-components/statviz/components/filter/FilterPanel.tsx @@ -33,10 +33,10 @@ export function FilterPanel({ return ( <> } - aria-label="Open ${label}" + icon={} + aria-label={`Open ${label}`} size="md" - data-testid="${label.replaceAll(' ', '').lower()}-drawer-button" + data-testid={`${label.replaceAll(" ", "").toLowerCase()}-drawer-button`} onClick={onOpen} /> From db3429ef69bb2f08bbbfe1633f0b69678806f18f Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Thu, 25 Jun 2026 15:42:45 +0200 Subject: [PATCH 23/30] appliedFilters-deps --- shared-components/statviz/dashboard/Demographics.tsx | 4 +--- shared-components/statviz/dashboard/ItemsAndBoxes.tsx | 4 +--- shared-components/statviz/dashboard/MovedBoxes.tsx | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/shared-components/statviz/dashboard/Demographics.tsx b/shared-components/statviz/dashboard/Demographics.tsx index 5646cd87d..0d256a1d0 100644 --- a/shared-components/statviz/dashboard/Demographics.tsx +++ b/shared-components/statviz/dashboard/Demographics.tsx @@ -32,9 +32,7 @@ export default function Demographics({ tags }: DemographicsProps) { const appliedFilters = useMemo( () => readDemographicsFiltersFromUrl(searchParams, tags), - // We intentionally only re-derive when URL params change, not when option arrays change - // eslint-disable-next-line react-hooks/exhaustive-deps - [searchParams], + [searchParams, tags], ); const handleApplyFilters = useCallback( diff --git a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx index 3972c169b..a0bd9746b 100644 --- a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx +++ b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx @@ -49,9 +49,7 @@ export default function ItemsAndBoxes({ const appliedFilters = useMemo( () => readStockFiltersFromUrl(searchParams, products, categories, locations, tags), - // We intentionally only re-derive when URL params change, not when option arrays change - // eslint-disable-next-line react-hooks/exhaustive-deps - [searchParams], + [searchParams, products, categories, locations, tags], ); const boxesOrItems: BoxesOrItems = diff --git a/shared-components/statviz/dashboard/MovedBoxes.tsx b/shared-components/statviz/dashboard/MovedBoxes.tsx index 9ff2b250b..3092b2079 100644 --- a/shared-components/statviz/dashboard/MovedBoxes.tsx +++ b/shared-components/statviz/dashboard/MovedBoxes.tsx @@ -38,9 +38,7 @@ export default function MovedBoxes({ products, categories, tags }: MovedBoxesPro const appliedFilters = useMemo( () => readMovementFiltersFromUrl(searchParams, products, categories, tags), - // We intentionally only re-derive when URL params change, not when option arrays change - // eslint-disable-next-line react-hooks/exhaustive-deps - [searchParams], + [searchParams, products, categories, tags], ); const boxesOrItems: BoxesOrItems = From 84d7fbc43d5f1813653afc38a7078c33329bdfdf Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Thu, 25 Jun 2026 16:29:43 +0200 Subject: [PATCH 24/30] remove-more --- .../visualizations/stock/StockCharts.tsx | 24 -- .../visualizations/stock/StockOverviewPie.tsx | 326 ------------------ 2 files changed, 350 deletions(-) delete mode 100644 shared-components/statviz/components/visualizations/stock/StockCharts.tsx delete mode 100644 shared-components/statviz/components/visualizations/stock/StockOverviewPie.tsx diff --git a/shared-components/statviz/components/visualizations/stock/StockCharts.tsx b/shared-components/statviz/components/visualizations/stock/StockCharts.tsx deleted file mode 100644 index 37cc0eda5..000000000 --- a/shared-components/statviz/components/visualizations/stock/StockCharts.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Wrap, WrapItem } from "@chakra-ui/react"; -import StockOverviewPie from "./StockOverviewPie"; -import { BoxesOrItemsCount } from "../../../dashboard/ItemsAndBoxes"; -import { StockOverview } from "../../../../../graphql/types"; - -interface IStockChartProps { - stockOverview: StockOverview; - boxesOrItems: BoxesOrItemsCount; -} - -export default function StockCharts({ stockOverview, boxesOrItems }: IStockChartProps) { - return ( - - - - - - ); -} diff --git a/shared-components/statviz/components/visualizations/stock/StockOverviewPie.tsx b/shared-components/statviz/components/visualizations/stock/StockOverviewPie.tsx deleted file mode 100644 index 19abec188..000000000 --- a/shared-components/statviz/components/visualizations/stock/StockOverviewPie.tsx +++ /dev/null @@ -1,326 +0,0 @@ -import { - Box, - Button, - Card, - CardBody, - FormLabel, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalHeader, - ModalOverlay, - Wrap, - WrapItem, - useDisclosure, -} from "@chakra-ui/react"; -import { filter, groupBy, innerJoin, map, sum, summarize, tidy } from "@tidyjs/tidy"; -import { useEffect, useMemo, useState } from "react"; -import { ArrowForwardIcon, ArrowLeftIcon } from "@chakra-ui/icons"; -import PieChart from "../../nivo/PieChart"; -import VisHeader from "../../VisHeader"; -import getOnExport from "../../../utils/chartExport"; -import { BoxesOrItemsCount } from "../../../dashboard/ItemsAndBoxes"; -import useValueFilter from "../../../hooks/useValueFilter"; -import ValueFilter from "../../filter/ValueFilter"; -import { StockOverview, StockOverviewResult } from "../../../../../graphql/types"; - -interface ISizeDim { - sizeId: number; - sizeName: string | undefined; -} - -interface ICategoryDim { - categoryId: number; - categoryName: string | undefined; -} - -type PreparedStock = StockOverviewResult & ICategoryDim & ISizeDim; -type PreparedStockAttributes = keyof PreparedStock; - -const mappingFunctions = { - categoryName: map((category: PreparedStock) => ({ - id: category.categoryName, - value: category.boxesCount, - })), - gender: map((gender: PreparedStock) => ({ - id: gender.gender, - value: gender.boxesCount, - })), - sizeName: map((size: PreparedStock) => ({ id: size.sizeName, value: size.boxesCount })), - productName: map((product: PreparedStock) => ({ - id: product.productName, - value: product.boxesCount, - })), -}; - -const groupOptions = [ - { - value: "categoryName", - label: "Category", - urlId: "cn", - }, - { - value: "productName", - label: "Product", - urlId: "pn", - }, - { - value: "gender", - label: "Gender", - urlId: "g", - }, - { - value: "sizeName", - label: "Size", - urlId: "s", - }, -]; - -// stg = stock group -const filterId = "stg"; - -const groupOptionValues = groupOptions.map((e) => e.value); - -interface IStockOverviewPieProps { - width: string; - height: string; - data: StockOverview; - boxesOrItems: BoxesOrItemsCount; -} - -export default function StockOverviewPie({ - width, - height, - data, - boxesOrItems, -}: IStockOverviewPieProps) { - const [chartData, setChartData] = useState([]); - const [drilldownPath, setDrilldownPath] = useState(["categoryName"]); - const [drilldownValues, setDrilldownValues] = useState([]); - const [selectedDrilldownValue, setSelectedDrilldownValue] = useState(""); - - const heading = - boxesOrItems === "boxesCount" - ? "Drilldown Chart of Instock Boxes" - : "Drilldown Chart of Instock Items"; - - const { onFilterChange, filterValue } = useValueFilter(groupOptions, groupOptions[0], filterId); - - const onExport = getOnExport(PieChart); - - const { - isOpen: showGroupOptions, - onOpen: openGroupOptions, - onClose: closeGroupOptions, - } = useDisclosure(); - - const onGroupSelect = (node) => { - setSelectedDrilldownValue(node.id); - if (drilldownPath.length <= 3) { - openGroupOptions(); - } - }; - - const drilldownFilters = drilldownValues.map((drilldownValue, index) => - filter((stockData) => stockData[drilldownPath[index]] === drilldownValue), - ); - - const availableGroupOptions = groupOptionValues.filter( - (option: PreparedStockAttributes) => drilldownPath.indexOf(option) === -1, - ); - - const setNewDrilldownPath = (base: PreparedStockAttributes, newDrilldownValues: string[]) => { - setDrilldownPath([base]); - setDrilldownValues(newDrilldownValues); - }; - - useEffect(() => { - setNewDrilldownPath(filterValue.value as PreparedStockAttributes, []); - }, [filterValue]); - - const onNextDrilldownChoice = (event) => { - setDrilldownPath([...drilldownPath, event.target.value]); - setDrilldownValues([...drilldownValues, selectedDrilldownValue]); - closeGroupOptions(); - }; - - useMemo(() => { - const sizeDim = data?.dimensions.size.map((size) => ({ - sizeId: size.id!, - sizeName: size.name!, - })); - - const categoryDim = data?.dimensions.category.map((category) => ({ - categoryId: category.id!, - categoryName: category.name!, - })); - - const preparedStockData = tidy( - data?.facts as Partial, - innerJoin(categoryDim!, { by: "categoryId" }), - innerJoin(sizeDim!, { - by: "sizeId", - }), - // @ts-expect-error spread of tidy filter functions not typed - ...drilldownFilters, - ) as PreparedStock[]; - - const grouped = tidy( - preparedStockData, - groupBy(drilldownPath, summarize({ boxesCount: sum(boxesOrItems) })), - mappingFunctions[drilldownPath[drilldownPath.length - 1]], - ); - - setChartData(grouped); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [drilldownPath, drilldownValues, data?.facts, boxesOrItems]); - - const getGroupOption = (levelsBack: number = 0) => - groupOptions.find( - (groupOption) => groupOption.value === drilldownPath[drilldownPath.length - levelsBack - 1], - ); - - const getSummarization = () => { - const level = drilldownPath.length; - const currentValueText = - drilldownValues.length > 0 ? `${drilldownValues[drilldownValues.length - 1]}` : ""; - - if (level === 1) { - return { - level, - grouping: `All Items by ${getGroupOption()?.label}`, - }; - } - - const previousGroupOption = getGroupOption(1); - if (level === 2 && previousGroupOption?.value === "gender") { - return { - level, - grouping: `${getGroupOption()?.label} for ${currentValueText}`, - }; - } - if (previousGroupOption?.value === "gender") { - return { - level: drilldownPath.length, - grouping: `${drilldownValues[drilldownValues.length - 2] ?? ""} ${currentValueText} by ${getGroupOption()?.label}`, - }; - } - if (previousGroupOption?.value === "sizeName") { - return { - level: drilldownPath.length, - grouping: `Size ${currentValueText} by ${getGroupOption()?.label}`, - }; - } - return { - level: drilldownPath.length, - grouping: `${currentValueText} by ${getGroupOption()?.label}`, - }; - }; - - const centerDataProp = getSummarization(); - - const chartProps = { - onClick: onGroupSelect, - data: chartData, - width, - height, - }; - return ( - - - - - - Select the next grouping - - {availableGroupOptions.map((groupOption) => ( - - ))} - - - - - - - - - - - - - - - - - - - - - - - {drilldownPath.map((value, index) => { - if (index === drilldownPath.length - 1) { - return ( - - {" "} - {groupOptions.find((option) => value === option.value)?.label} - - ); - } - return ( - - {" "} - {groupOptions.find((option) => value === option.value)?.label}: " - {drilldownValues[index]}" - - ); - })} - - - - - ); -} From 2d272dd91651a00cff67fd667aaefb31919f394f Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Thu, 25 Jun 2026 17:48:30 +0200 Subject: [PATCH 25/30] reactive-vars --- .../statviz/components/LinkSharingSection.tsx | 4 ---- .../statviz/components/ShareableLinkAlert.tsx | 19 +------------------ .../statviz/components/filter/TagFilter.tsx | 7 +++++-- .../statviz/hooks/useShareableLink.ts | 16 +--------------- shared-components/statviz/state/filter.tsx | 6 ------ 5 files changed, 7 insertions(+), 45 deletions(-) diff --git a/shared-components/statviz/components/LinkSharingSection.tsx b/shared-components/statviz/components/LinkSharingSection.tsx index 932f741e3..7505a492a 100644 --- a/shared-components/statviz/components/LinkSharingSection.tsx +++ b/shared-components/statviz/components/LinkSharingSection.tsx @@ -22,8 +22,6 @@ export default function LinkSharingSection({ view }: { view?: "StockOverview" }) isLinkSharingEnabled, copyLinkToClipboard, handleShareLinkClick, - includedTags, - excludedTags, boi, expirationDate, } = useShareableLink({ view }); @@ -48,8 +46,6 @@ export default function LinkSharingSection({ view }: { view?: "StockOverview" }) diff --git a/shared-components/statviz/components/ShareableLinkAlert.tsx b/shared-components/statviz/components/ShareableLinkAlert.tsx index eb691561f..ff52751c7 100644 --- a/shared-components/statviz/components/ShareableLinkAlert.tsx +++ b/shared-components/statviz/components/ShareableLinkAlert.tsx @@ -1,39 +1,23 @@ import React from "react"; import { Alert, AlertIcon, Box } from "@chakra-ui/react"; import { IBoxesOrItemsFilter } from "./filter/BoxesOrItemsSelect"; -import { ITagFilterValue } from "../state/filter"; import { IFilterValue } from "./filter/ValueFilter"; interface ShareableLinkAlertProps { alertType?: "info" | "warning"; boi?: IFilterValue & IBoxesOrItemsFilter; - includedTags?: (IFilterValue & ITagFilterValue)[]; - excludedTags?: (IFilterValue & ITagFilterValue)[]; expirationDate?: string; } export const ShareableLinkAlert: React.FC = ({ alertType, boi, - includedTags = [], - excludedTags = [], expirationDate, }) => { if (!alertType) return ; const boiText = boi?.label; - const tagText = (() => { - const parts: string[] = []; - if (includedTags.length > 0) { - parts.push(`including: ${includedTags.map(({ label }) => label).join(", ")}`); - } - if (excludedTags.length > 0) { - parts.push(`excluding: ${excludedTags.map(({ label }) => label).join(", ")}`); - } - return parts.length > 0 ? `, filtered by tags (${parts.join("; ")})` : ""; - })(); - const expirationText = expirationDate ? `Link will expire on ${expirationDate}.` : ""; return ( @@ -43,8 +27,7 @@ export const ShareableLinkAlert: React.FC = ({

Shareable Link Created
- This link will show your inventory in {boiText} - {tagText}. + This link will show your inventory in {boiText}.
{expirationText}

diff --git a/shared-components/statviz/components/filter/TagFilter.tsx b/shared-components/statviz/components/filter/TagFilter.tsx index bec48e5bd..874d998d2 100644 --- a/shared-components/statviz/components/filter/TagFilter.tsx +++ b/shared-components/statviz/components/filter/TagFilter.tsx @@ -1,11 +1,14 @@ import { Box } from "@chakra-ui/react"; -import { useReactiveVar } from "@apollo/client"; +import { makeVar, useReactiveVar } from "@apollo/client"; import { ResultOf } from "gql.tada"; import MultiSelectFilter from "./MultiSelectFilter"; import useMultiSelectFilter from "../../hooks/useMultiSelectFilter"; -import { ITagFilterValue, tagFilterIncludedValuesVar } from "../../state/filter"; +import { ITagFilterValue } from "../../state/filter"; import { TAG_DIMENSION_INFO_FRAGMENT } from "../../queries/fragments"; +export const tagFilterIncludedValuesVar = makeVar([]); +export const tagFilterExcludedValuesVar = makeVar([]); + export const tagFilterId = "tags"; export const tagFilterIncludedId = tagFilterId; export const tagFilterExcludedId = "notags"; diff --git a/shared-components/statviz/hooks/useShareableLink.ts b/shared-components/statviz/hooks/useShareableLink.ts index 456203f82..3d1920136 100644 --- a/shared-components/statviz/hooks/useShareableLink.ts +++ b/shared-components/statviz/hooks/useShareableLink.ts @@ -4,7 +4,7 @@ import { useParams, useSearchParams } from "react-router-dom"; // TODO: Move common utils to shared-components, use alias for imports. import { graphql } from "../../../graphql/graphql"; import { useNotification } from "../../../front/src/hooks/useNotification"; -import { useMutation, useReactiveVar } from "@apollo/client"; +import { useMutation } from "@apollo/client"; import useValueFilter from "./useValueFilter"; import { boxesOrItemsFilterValues, @@ -12,9 +12,6 @@ import { defaultBoxesOrItems, IBoxesOrItemsFilter, } from "../components/filter/BoxesOrItemsSelect"; -import { tagFilterIncludedId, tagFilterExcludedId } from "../components/filter/TagFilter"; -import { tagFilterIncludedValuesVar, tagFilterExcludedValuesVar } from "../state/filter"; -import useMultiSelectFilter from "./useMultiSelectFilter"; const BASE_PUBLIC_LINK_SHARING_URL = import.meta.env.FRONT_PUBLIC_URL; @@ -55,15 +52,6 @@ export default function useShareableLink({ defaultBoxesOrItems, boxesOrItemsUrlId, ); - const includedTagFilterValues = useReactiveVar(tagFilterIncludedValuesVar); - const excludedTagFilterValues = useReactiveVar(tagFilterExcludedValuesVar); - const { includedFilterValue: includedTags, excludedFilterValue: excludedTags } = - useMultiSelectFilter( - includedTagFilterValues, - tagFilterIncludedId, - excludedTagFilterValues, - tagFilterExcludedId, - ); const [expirationDate, setExpirationDate] = useState(); // Remove the JSX from the hook @@ -140,8 +128,6 @@ export default function useShareableLink({ isLinkSharingEnabled, copyLinkToClipboard, handleShareLinkClick, - includedTags, - excludedTags, boi, expirationDate, }; diff --git a/shared-components/statviz/state/filter.tsx b/shared-components/statviz/state/filter.tsx index 92a0c0ac4..b898195cb 100644 --- a/shared-components/statviz/state/filter.tsx +++ b/shared-components/statviz/state/filter.tsx @@ -1,12 +1,6 @@ -import { makeVar } from "@apollo/client"; import { IFilterValue } from "../components/filter/ValueFilter"; export interface ITagFilterValue extends IFilterValue { color: string; id: number; } -// Reactive variable for included tags (tags to filter data IN) -export const tagFilterIncludedValuesVar = makeVar([]); - -// Reactive variable for excluded tags (tags to filter data OUT) -export const tagFilterExcludedValuesVar = makeVar([]); From 688c938ede18648703631b60e026dd736b25f40a Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Thu, 25 Jun 2026 18:01:18 +0200 Subject: [PATCH 26/30] Remove FilterPanel --- .../statviz/components/filter/FilterPanel.tsx | 52 ------------------- .../statviz/dashboard/Demographics.tsx | 2 +- .../statviz/dashboard/ItemsAndBoxes.tsx | 2 +- .../statviz/dashboard/MovedBoxes.tsx | 2 +- 4 files changed, 3 insertions(+), 55 deletions(-) delete mode 100644 shared-components/statviz/components/filter/FilterPanel.tsx diff --git a/shared-components/statviz/components/filter/FilterPanel.tsx b/shared-components/statviz/components/filter/FilterPanel.tsx deleted file mode 100644 index 31b174a83..000000000 --- a/shared-components/statviz/components/filter/FilterPanel.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { ReactNode } from "react"; -import { - Drawer, - DrawerBody, - DrawerCloseButton, - DrawerContent, - DrawerHeader, - DrawerOverlay, - IconButton, - useBreakpointValue, -} from "@chakra-ui/react"; -import { MdFilterList } from "react-icons/md"; - -interface FilterPanelProps { - label?: string; - isOpen: boolean; - onOpen: () => void; - onClose: () => void; - children: ReactNode; -} - -export function FilterPanel({ - label = "Filters", - isOpen, - onOpen, - onClose, - children, -}: FilterPanelProps) { - const placement = useBreakpointValue({ base: "left" as const, md: "right" as const }) ?? "right"; - const size = useBreakpointValue({ base: undefined, md: "md" }); - const maxW = useBreakpointValue({ base: "90vw", md: undefined }); - - return ( - <> - } - aria-label={`Open ${label}`} - size="md" - data-testid={`${label.replaceAll(" ", "").toLowerCase()}-drawer-button`} - onClick={onOpen} - /> - - - - {label} - - {children} - - - - ); -} diff --git a/shared-components/statviz/dashboard/Demographics.tsx b/shared-components/statviz/dashboard/Demographics.tsx index 0d256a1d0..3bc6a2885 100644 --- a/shared-components/statviz/dashboard/Demographics.tsx +++ b/shared-components/statviz/dashboard/Demographics.tsx @@ -20,7 +20,7 @@ import { type DemographicsAppliedFilters, type ITagOption, } from "../utils/dashboardFilters"; -import { FilterPanel } from "./../components/filter/FilterPanel"; +import { FilterPanel } from "../../filter/FilterPanel"; import { DemographicsFilters } from "./../components/filter/DemographicsFilters"; interface DemographicsProps { diff --git a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx index a0bd9746b..b0f16f700 100644 --- a/shared-components/statviz/dashboard/ItemsAndBoxes.tsx +++ b/shared-components/statviz/dashboard/ItemsAndBoxes.tsx @@ -27,7 +27,7 @@ import { type ITagOption, } from "../utils/dashboardFilters"; import type { BoxesOrItems } from "../components/filter/BoxesOrItemsSelect"; -import { FilterPanel } from "./../components/filter/FilterPanel"; +import { FilterPanel } from "../../filter/FilterPanel"; import { StockFilters } from "./../components/filter/StockFilters"; export type BoxesOrItemsCount = "boxesCount" | "itemsCount"; diff --git a/shared-components/statviz/dashboard/MovedBoxes.tsx b/shared-components/statviz/dashboard/MovedBoxes.tsx index 3092b2079..33960c19f 100644 --- a/shared-components/statviz/dashboard/MovedBoxes.tsx +++ b/shared-components/statviz/dashboard/MovedBoxes.tsx @@ -14,7 +14,7 @@ import { useCallback, useMemo } from "react"; import type React from "react"; import { useSearchParams } from "react-router-dom"; import MovedBoxesDataContainer from "../components/visualizations/movedBoxes/MovedBoxesDataContainer"; -import { FilterPanel } from "./../components/filter/FilterPanel"; +import { FilterPanel } from "../../filter/FilterPanel"; import { MovementFilters } from "./../components/filter/MovementFilters"; import { MOVEMENT_URL_PARAMS, From 656c7478fab65d85b5ad71b1a33445690afeb1bf Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Thu, 25 Jun 2026 18:08:27 +0200 Subject: [PATCH 27/30] deduplicate --- .../components/filter/MovementFilters.tsx | 18 +----------------- .../statviz/components/filter/StockFilters.tsx | 18 +----------------- .../statviz/utils/dashboardFilters.ts | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 34 deletions(-) diff --git a/shared-components/statviz/components/filter/MovementFilters.tsx b/shared-components/statviz/components/filter/MovementFilters.tsx index 4b6406dcd..9a53088a1 100644 --- a/shared-components/statviz/components/filter/MovementFilters.tsx +++ b/shared-components/statviz/components/filter/MovementFilters.tsx @@ -9,23 +9,7 @@ import type { MovementAppliedFilters, } from "../../utils/dashboardFilters"; import { genders } from "./constants"; -import type { IFilterValue } from "./ValueFilter"; - -function toProductFilterValues(products: IProductOption[]): IFilterValue[] { - return products.map((p) => ({ - value: String(p.id), - label: p.gender ? `${p.name} (${p.gender})` : p.name, - urlId: String(p.id), - })); -} - -function toFilterValues(items: { id: number; name: string }[]): IFilterValue[] { - return items.map((item) => ({ - value: String(item.id), - label: item.name, - urlId: String(item.id), - })); -} +import { toFilterValues, toProductFilterValues } from "../../utils/dashboardFilters"; interface MovementFiltersProps { isOpen: boolean; diff --git a/shared-components/statviz/components/filter/StockFilters.tsx b/shared-components/statviz/components/filter/StockFilters.tsx index c35f6d79e..063392a17 100644 --- a/shared-components/statviz/components/filter/StockFilters.tsx +++ b/shared-components/statviz/components/filter/StockFilters.tsx @@ -10,23 +10,7 @@ import type { StockAppliedFilters, } from "../../utils/dashboardFilters"; import { genders } from "./constants"; -import type { IFilterValue } from "./ValueFilter"; - -function toFilterValues(items: { id: number; name: string }[]): IFilterValue[] { - return items.map((item) => ({ - value: String(item.id), - label: item.name, - urlId: String(item.id), - })); -} - -function toProductFilterValues(products: IProductOption[]): IFilterValue[] { - return products.map((p) => ({ - value: String(p.id), - label: p.gender ? `${p.name} (${p.gender})` : p.name, - urlId: String(p.id), - })); -} +import { toFilterValues, toProductFilterValues } from "../../utils/dashboardFilters"; interface StockFiltersProps { isOpen: boolean; diff --git a/shared-components/statviz/utils/dashboardFilters.ts b/shared-components/statviz/utils/dashboardFilters.ts index 9f3b1e34f..51172de75 100644 --- a/shared-components/statviz/utils/dashboardFilters.ts +++ b/shared-components/statviz/utils/dashboardFilters.ts @@ -1,6 +1,7 @@ import { date2String } from "../../utils/helpers"; import { subMonths } from "date-fns"; import { ProductGender } from "../../../graphql/types"; +import type { IFilterValue } from "../components/filter/ValueFilter"; // --------------------------------------------------------------------------- // Option types for filter dropdowns @@ -373,3 +374,19 @@ export function writeDemographicsFiltersToUrl( serializeIds(filters.excludedTags.map((t) => t.id)), ); } + +export function toFilterValues(items: { id: number; name: string }[]): IFilterValue[] { + return items.map((item) => ({ + value: String(item.id), + label: item.name, + urlId: String(item.id), + })); +} + +export function toProductFilterValues(products: IProductOption[]): IFilterValue[] { + return products.map((p) => ({ + value: String(p.id), + label: p.gender ? `${p.name} (${p.gender})` : p.name, + urlId: String(p.id), + })); +} From e00689cb93caf4d33f7efd4650306bb4a996c04a Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Thu, 25 Jun 2026 18:14:42 +0200 Subject: [PATCH 28/30] move-stock-query --- graphql/types.ts | 2 +- .../visualizations/stock/StockOverviewRingDataContainer.tsx | 2 +- .../stock/StockDataContainer.tsx => queries/queries.ts} | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) rename shared-components/statviz/{components/visualizations/stock/StockDataContainer.tsx => queries/queries.ts} (84%) diff --git a/graphql/types.ts b/graphql/types.ts index 3fd0e57d1..c09a28824 100644 --- a/graphql/types.ts +++ b/graphql/types.ts @@ -11,7 +11,7 @@ import { } from "./fragments"; import { CREATED_BOXES_QUERY } from "../shared-components/statviz/components/visualizations/createdBoxes/CreatedBoxesDataContainer"; import { MOVED_BOXES_QUERY } from "../shared-components/statviz/components/visualizations/movedBoxes/MovedBoxesDataContainer"; -import { STOCK_QUERY } from "../shared-components/statviz/components/visualizations/stock/StockDataContainer"; +import { STOCK_QUERY } from "../shared-components/statviz/queries/queries"; import { DEMOGRAPHIC_QUERY } from "../shared-components/statviz/components/visualizations/demographic/DemographicDataContainer"; /** @todo Make a fragment to infer this type. */ diff --git a/shared-components/statviz/components/visualizations/stock/StockOverviewRingDataContainer.tsx b/shared-components/statviz/components/visualizations/stock/StockOverviewRingDataContainer.tsx index 96a601b97..c15b769b6 100644 --- a/shared-components/statviz/components/visualizations/stock/StockOverviewRingDataContainer.tsx +++ b/shared-components/statviz/components/visualizations/stock/StockOverviewRingDataContainer.tsx @@ -3,7 +3,7 @@ import { useQuery } from "@apollo/client"; import { useParams } from "react-router-dom"; import ErrorCard, { predefinedErrors } from "../../ErrorCard"; import StockOverviewRingFilterContainer from "./StockOverviewRingFilterContainer"; -import { STOCK_QUERY } from "./StockDataContainer"; +import { STOCK_QUERY } from "../../../queries/queries"; import type { StockAppliedFilters } from "../../../utils/dashboardFilters"; import type { BoxesOrItems } from "../../filter/BoxesOrItemsSelect"; diff --git a/shared-components/statviz/components/visualizations/stock/StockDataContainer.tsx b/shared-components/statviz/queries/queries.ts similarity index 84% rename from shared-components/statviz/components/visualizations/stock/StockDataContainer.tsx rename to shared-components/statviz/queries/queries.ts index b0275a6c7..a6b635551 100644 --- a/shared-components/statviz/components/visualizations/stock/StockDataContainer.tsx +++ b/shared-components/statviz/queries/queries.ts @@ -1,5 +1,5 @@ -import { graphql } from "../../../../../graphql/graphql"; -import { TAG_FRAGMENT } from "../../../queries/fragments"; +import { graphql } from "../../../graphql/graphql"; +import { TAG_FRAGMENT } from "./fragments"; export const STOCK_QUERY = graphql( ` @@ -38,3 +38,4 @@ export const STOCK_QUERY = graphql( `, [TAG_FRAGMENT], ); + From 8016331d45dc695a28ed387d7d15f00506c281cd Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Thu, 25 Jun 2026 18:30:22 +0200 Subject: [PATCH 29/30] format --- shared-components/statviz/queries/queries.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/shared-components/statviz/queries/queries.ts b/shared-components/statviz/queries/queries.ts index a6b635551..6510e133b 100644 --- a/shared-components/statviz/queries/queries.ts +++ b/shared-components/statviz/queries/queries.ts @@ -38,4 +38,3 @@ export const STOCK_QUERY = graphql( `, [TAG_FRAGMENT], ); - From bbeb221be151a4c6e82bb0e8a449f34aa32da4c2 Mon Sep 17 00:00:00 2001 From: Philipp Metzner Date: Thu, 25 Jun 2026 18:41:51 +0200 Subject: [PATCH 30/30] correct-url-param --- shared-front/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-front/src/App.tsx b/shared-front/src/App.tsx index 1067d1a2c..4983fd3cd 100644 --- a/shared-front/src/App.tsx +++ b/shared-front/src/App.tsx @@ -99,7 +99,7 @@ function App() { const view = searchParams.get("view"); const [routerSearchParams] = useSearchParams(); - const boiUrlId = routerSearchParams.get("boi"); + const boiUrlId = routerSearchParams.get("sboi"); const boxesOrItems: BoxesOrItems = ( boxesOrItemsFilterValues.find((f) => f.urlId === boiUrlId) ?? boxesOrItemsFilterValues[0] ).value;