From 51465ad44dd68d937f6ede19be8900856970bce4 Mon Sep 17 00:00:00 2001 From: mvvasilev Date: Wed, 3 Jan 2024 22:46:16 +0200 Subject: [PATCH] Added Sum Widget --- APIGateway/Dockerfile | 0 PersonalFinancesService/Dockerfile | 0 PersonalFinancesService/compose.yaml | 9 - .../controllers/StatisticsController.java | 17 +- .../mvvasilev/finances/enums/WidgetType.java | 3 +- .../persistence/StatisticsRepository.java | 41 ++- .../WidgetParameterRepository.java | 4 + .../finances/services/StatisticsService.java | 4 + .../finances/services/WidgetService.java | 9 + docker-compose.yml | 0 frontend/src/app/pages/CategoriesPage.jsx | 329 +++++++++--------- frontend/src/app/pages/StatisticsPage.jsx | 24 +- .../components/widgets/WidgetContainer.jsx | 123 ++++--- .../components/widgets/WidgetEditModal.jsx | 153 +++++--- .../components/widgets/WidgetParameters.js | 3 +- frontend/src/utils.js | 8 + 16 files changed, 447 insertions(+), 280 deletions(-) create mode 100644 APIGateway/Dockerfile create mode 100644 PersonalFinancesService/Dockerfile delete mode 100644 PersonalFinancesService/compose.yaml create mode 100644 docker-compose.yml diff --git a/APIGateway/Dockerfile b/APIGateway/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/PersonalFinancesService/Dockerfile b/PersonalFinancesService/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/PersonalFinancesService/compose.yaml b/PersonalFinancesService/compose.yaml deleted file mode 100644 index 7c8044f..0000000 --- a/PersonalFinancesService/compose.yaml +++ /dev/null @@ -1,9 +0,0 @@ -services: - postgres: - image: 'postgres:latest' - environment: - - 'POSTGRES_DB=mydatabase' - - 'POSTGRES_PASSWORD=secret' - - 'POSTGRES_USER=myuser' - ports: - - '5432' diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/controllers/StatisticsController.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/controllers/StatisticsController.java index 2d0019b..c033206 100644 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/controllers/StatisticsController.java +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/controllers/StatisticsController.java @@ -39,7 +39,8 @@ public class StatisticsController extends AbstractRestController { public ResponseEntity> fetchSpendingByCategory( Long[] categoryId, @RequestParam(defaultValue = "1970-01-01T00:00:00") LocalDateTime from, - @RequestParam(defaultValue = "2099-01-01T00:00:00") LocalDateTime to + @RequestParam(defaultValue = "2099-01-01T00:00:00") LocalDateTime to, + @RequestParam(defaultValue = "false") Boolean includeUncategorized ) { return ok(statisticsService.spendingByCategory(categoryId, from, to)); } @@ -50,9 +51,21 @@ public class StatisticsController extends AbstractRestController { Long[] categoryId, @RequestParam(defaultValue = "DAILY") TimePeriod period, @RequestParam(defaultValue = "1970-01-01T00:00:00") LocalDateTime from, - @RequestParam(defaultValue = "2099-01-01T00:00:00") LocalDateTime to + @RequestParam(defaultValue = "2099-01-01T00:00:00") LocalDateTime to, + @RequestParam(defaultValue = "false") Boolean includeUncategorized ) { return ok(statisticsService.spendingByCategoryOverTime(categoryId, period, from, to)); } + @GetMapping("/sumByCategory") + @PreAuthorize("@authService.isOwner(#categoryId, T(dev.mvvasilev.finances.entity.TransactionCategory))") + public ResponseEntity> sum( + Long[] categoryId, + @RequestParam(defaultValue = "1970-01-01T00:00:00") LocalDateTime from, + @RequestParam(defaultValue = "2099-01-01T00:00:00") LocalDateTime to, + @RequestParam(defaultValue = "false") Boolean includeUncategorized + ) { + return ok(statisticsService.sumByCategory(categoryId, from, to, includeUncategorized)); + } + } diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/WidgetType.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/WidgetType.java index 2f79d5e..af48e97 100644 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/WidgetType.java +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/WidgetType.java @@ -5,7 +5,8 @@ import dev.mvvasilev.common.data.PersistableEnum; public enum WidgetType implements PersistableEnum { TOTAL_SPENDING_PER_CATEGORY, - SPENDING_OVER_TIME_PER_CATEGORY; + SPENDING_OVER_TIME_PER_CATEGORY, + SUM_PER_CATEGORY; @Override public String value() { diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/StatisticsRepository.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/StatisticsRepository.java index 3107943..d5f099f 100644 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/StatisticsRepository.java +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/StatisticsRepository.java @@ -29,17 +29,17 @@ public class StatisticsRepository { public Map fetchSpendingByCategory(Long[] categoryId, LocalDateTime from, LocalDateTime to) { Query nativeQuery = entityManager.createNativeQuery( - """ - SELECT ptc.category_id AS category_id, ROUND(CAST(SUM(pt.amount) AS NUMERIC), 2) AS total_spending - FROM transactions.processed_transaction AS pt - JOIN categories.processed_transaction_category AS ptc ON ptc.processed_transaction_id = pt.id - WHERE - pt.is_inflow = FALSE - AND ptc.category_id = any(?1) - AND (pt.timestamp BETWEEN ?2 AND ?3) - GROUP BY ptc.category_id - ORDER BY total_spending DESC; - """, + """ + SELECT ptc.category_id AS category_id, ROUND(CAST(SUM(pt.amount) AS NUMERIC), 2) AS total_spending + FROM transactions.processed_transaction AS pt + JOIN categories.processed_transaction_category AS ptc ON ptc.processed_transaction_id = pt.id + WHERE + pt.is_inflow = FALSE + AND ptc.category_id = any(?1) + AND (pt.timestamp BETWEEN ?2 AND ?3) + GROUP BY ptc.category_id + ORDER BY total_spending DESC; + """, Tuple.class ); @@ -70,4 +70,23 @@ public class StatisticsRepository { ((Tuple) r).get("period_beginning_timestamp", Timestamp.class).toLocalDateTime() )).toList(); } + + public Double sumByCategory(Long[] categoryId, LocalDateTime from, LocalDateTime to, Boolean includeUncategorized) { + Query nativeQuery = entityManager.createNativeQuery( + """ + SELECT SUM(pt.amount) AS result + FROM transactions.processed_transaction AS pt + LEFT OUTER JOIN categories.processed_transaction_category AS ptc ON pt.id = ptc.processed_transaction_id + WHERE (ptc.category_id = any(?1) OR (?4 AND ptc.category_id IS NULL)) AND (pt.timestamp BETWEEN ?2 AND ?3) + """, + Tuple.class + ); + + nativeQuery.setParameter(1, categoryId); + nativeQuery.setParameter(2, from); + nativeQuery.setParameter(3, to); + nativeQuery.setParameter(4, includeUncategorized); + + return ((Tuple) nativeQuery.getSingleResult()).get("result", Double.class); + } } diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/WidgetParameterRepository.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/WidgetParameterRepository.java index 73b6442..2d59721 100644 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/WidgetParameterRepository.java +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/WidgetParameterRepository.java @@ -2,6 +2,7 @@ package dev.mvvasilev.finances.persistence; import dev.mvvasilev.finances.entity.WidgetParameter; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @@ -12,4 +13,7 @@ public interface WidgetParameterRepository extends JpaRepository findAllByWidgetIdIn(Collection widgetIds); + @Modifying + void deleteAllByWidgetId(Long widgetId); + } diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/StatisticsService.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/StatisticsService.java index dd37009..9b42630 100644 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/StatisticsService.java +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/StatisticsService.java @@ -75,4 +75,8 @@ public class StatisticsService { case YEARLY -> ChronoUnit.YEARS.between(from, to) <= 30; }; } + + public Double sumByCategory(Long[] categoryId, LocalDateTime from, LocalDateTime to, Boolean includeUncategorized) { + return statisticsRepository.sumByCategory(categoryId, from, to, includeUncategorized); + } } diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/WidgetService.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/WidgetService.java index e8f0897..1967257 100644 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/WidgetService.java +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/WidgetService.java @@ -51,6 +51,15 @@ public class WidgetService { widgetRepository.saveAndFlush(mapWidget(widget.get(), widget.get().getUserId(), dto)); + widgetParameterRepository.deleteAllByWidgetId(id); + + final var params = dto.parameters() + .stream() + .map(p -> mapWidgetParameter(new WidgetParameter(), id, p)) + .toList(); + + widgetParameterRepository.saveAllAndFlush(params); + return 1; // TODO: fetch rows affected from database } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/pages/CategoriesPage.jsx b/frontend/src/app/pages/CategoriesPage.jsx index 742751f..2427d40 100644 --- a/frontend/src/app/pages/CategoriesPage.jsx +++ b/frontend/src/app/pages/CategoriesPage.jsx @@ -11,6 +11,7 @@ import Grid from "@mui/material/Unstable_Grid2"; import Button from "@mui/material/Button"; import Divider from "@mui/material/Divider"; import { + Backdrop, Checkbox, Dialog, DialogActions, @@ -26,6 +27,8 @@ import CategoriesBox from "@/components/categories/CategoriesBox.jsx"; import {PARAMS} from "@/components/widgets/WidgetParameters.js"; import * as React from "react"; import VisuallyHiddenInput from "@/components/VisuallyHiddenInput.jsx"; +import Card from "@mui/material/Card"; +import Typography from "@mui/material/Typography"; export default function CategoriesPage() { @@ -220,80 +223,177 @@ export default function CategoriesPage() { } return ( - +
+ - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - setSelectedCategory({...c})} - onCategoryDelete={(e, c) => { - setSelectedCategory(c); - openConfirmDeleteCategoryModal(true); - }} - showDelete - /> - - - - - - - - { - selectedCategory && - { - selectedCategory.ruleBehavior = value; - setSelectedCategory({...selectedCategory}); + + setSelectedCategory({...c})} + onCategoryDelete={(e, c) => { + setSelectedCategory(c); + openConfirmDeleteCategoryModal(true); }} - onSave={() => saveCategory(selectedCategory)} + showDelete /> - } - + + + + + + + { + selectedCategory && + { + selectedCategory.ruleBehavior = value; + setSelectedCategory({...selectedCategory}); + }} + onSave={() => saveCategory(selectedCategory)} + /> + } + + + + {`Delete Category "${selectedCategory?.name}"?`} + + + + Deleting a category will also clear it from all transactions it is currently applied to + + + + + + + + + + {"Apply all categorization rules?"} + + + + Applying all categorization rules to your current transactions will wipe all categories + assigned to them, and re-assign them based on the rules as currently defined. + + + + + + + + + + {"Replace Existing Categories?"} + + + + Would you like to replace your existing categories completely with the ones in your import? + ( Note that this will remove all current categories from your transactions ) + + { + setReplaceExistingOnUpload(e.target.checked); + }} + /> + } + label="Replace Existing Categories" + labelPlacement="end" + /> + + + + + + + - -

Create New Category

+ + Create New Category @@ -326,103 +426,8 @@ export default function CategoriesPage() { -
+
- - - {`Delete Category "${selectedCategory?.name}"?`} - - - - Deleting a category will also clear it from all transactions it is currently applied to - - - - - - - - - - {"Apply all categorization rules?"} - - - - Applying all categorization rules to your current transactions will wipe all categories - assigned to them, and re-assign them based on the rules as currently defined. - - - - - - - - - - {"Replace Existing Categories?"} - - - - Would you like to replace your existing categories completely with the ones in your import? - ( Note that this will remove all current categories from your transactions ) - - { - setReplaceExistingOnUpload(e.target.checked); - }} - /> - } - label="Replace Existing Categories" - labelPlacement="end" - /> - - - - - - -
+
); } \ No newline at end of file diff --git a/frontend/src/app/pages/StatisticsPage.jsx b/frontend/src/app/pages/StatisticsPage.jsx index 2d7eb78..329a643 100644 --- a/frontend/src/app/pages/StatisticsPage.jsx +++ b/frontend/src/app/pages/StatisticsPage.jsx @@ -129,8 +129,30 @@ export default function StatisticsPage() { ); } - function updateExistingWidget() { + async function updateExistingWidget(widget) { + utils.showSpinner(); + openWidgetModal(false); + await utils.performRequest(`/api/widgets/${widget.dbId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + positionX: widget.x, + positionY: widget.y, + sizeX: widget.w, + sizeY: widget.h, + name: widget.name, + type: widget.type, + parameters: widget.parameters + }) + }) + .then(resp => resp.json()) + .then(r => fetchWidgets()) + .then(resp => { + utils.hideSpinner(); + }); } function removeWidget() { diff --git a/frontend/src/components/widgets/WidgetContainer.jsx b/frontend/src/components/widgets/WidgetContainer.jsx index 62f24de..5a4712a 100644 --- a/frontend/src/components/widgets/WidgetContainer.jsx +++ b/frontend/src/components/widgets/WidgetContainer.jsx @@ -10,45 +10,43 @@ import utils from "@/utils.js"; import { PARAMS } from "@/components/widgets/WidgetParameters.js"; import dayjs from "dayjs"; import 'chart.js/auto'; -import { Pie } from 'react-chartjs-2'; +import {Line, Pie} from 'react-chartjs-2'; export default function WidgetContainer({widget, sx, onEdit, onRemove}) { const [data, setData] = useState(null); useEffect(() => { + var queryString = ""; + + queryString += widget.parameters?.filter(p => p.name.includes(PARAMS.CATEGORY_PREFIX)).map(c => `categoryId=${c.numericValue}`)?.join("&"); + + let isToNow = widget.parameters?.find(p => p.name === PARAMS.IS_TO_NOW)?.booleanValue ?? false; + let isFromStatic = widget.parameters?.find(p => p.name === PARAMS.IS_FROM_DATE_STATIC)?.booleanValue ?? false; + + var fromDate; + var toDate; + + if (isToNow) { + toDate = dayjs(); + } else { + toDate = dayjs(widget.parameters?.find(p => p.name === PARAMS.TO_DATE)?.timestampValue); + } + + if (!isFromStatic) { + fromDate = dayjs().subtract(widget.parameters?.find(p => p.name === PARAMS.RELATIVE_FROM_PERIOD)?.numericValue ?? 30, 'days'); + } else { + fromDate = dayjs(widget.parameters?.find(p => p.name === PARAMS.FROM_DATE)?.timestampValue); + } + + var includeUncategorized = widget.parameters?.find(p => p.name === PARAMS.INCLUDE_UNCATEGORIZED)?.booleanValue ?? false; + + queryString += `&fromDate=${fromDate}`; + queryString += `&toDate=${toDate}`; + queryString += `&includeUncategorized=${includeUncategorized}`; + switch (widget.type) { case "TOTAL_SPENDING_PER_CATEGORY": { - var queryString = ""; - - console.log(widget); - - queryString += widget.parameters?.filter(p => p.name.includes(PARAMS.CATEGORY_PREFIX)).map(c => `categoryId=${c.numericValue}`)?.join("&"); - - let isToNow = widget.parameters?.find(p => p.name === PARAMS.IS_TO_NOW)?.booleanValue ?? false; - let isFromStatic = widget.parameters?.find(p => p.name === PARAMS.IS_FROM_DATE_STATIC)?.booleanValue ?? false; - - console.log(isToNow); - console.log(isFromStatic); - - var fromDate; - var toDate; - - if (isToNow) { - toDate = dayjs(); - } else { - toDate = dayjs(widget.parameters?.find(p => p.name === PARAMS.TO_DATE)?.timestampValue); - } - - if (!isFromStatic) { - fromDate = dayjs().subtract(widget.parameters?.find(p => p.name === PARAMS.RELATIVE_FROM_PERIOD)?.numericValue ?? 30, 'days'); - } else { - fromDate = dayjs(widget.parameters?.find(p => p.name === PARAMS.FROM_DATE)?.timestampValue); - } - - queryString += `&fromDate=${fromDate}`; - queryString += `&toDate=${toDate}`; - utils.performRequest(`/api/statistics/totalSpendingByCategory?${queryString}`) .then(resp => resp.json()) .then(resp => setData(resp.result)); @@ -56,7 +54,16 @@ export default function WidgetContainer({widget, sx, onEdit, onRemove}) { break; } case "SPENDING_OVER_TIME_PER_CATEGORY": { - utils.performRequest("/api/statistics/spendingOverTimeByCategory") + queryString += widget.parameters?.find(p => p.name === PARAMS.TIME_PERIOD)?.stringValue ?? "DAILY"; + + utils.performRequest(`/api/statistics/spendingOverTimeByCategory?${queryString}`) + .then(resp => resp.json()) + .then(resp => setData(resp.result)); + + break; + } + case "SUM_PER_CATEGORY": { + utils.performRequest(`/api/statistics/sumByCategory?${queryString}`) .then(resp => resp.json()) .then(resp => setData(resp.result)); @@ -109,26 +116,38 @@ export default function WidgetContainer({widget, sx, onEdit, onRemove}) {
-
- { - data && widget.type === "TOTAL_SPENDING_PER_CATEGORY" && - c.name), - datasets: [ - { - label: "Amount", - data: data.categories.map(c => data.spendingByCategory[c.id]) - } - ] - }} - /> - } +
+ { + data && widget.type === "TOTAL_SPENDING_PER_CATEGORY" && + c.name), + datasets: [ + { + label: "Amount", + data: data.categories.map(c => data.spendingByCategory[c.id]) + } + ] + }} + /> + } + { + data && widget.type === "SPENDING_OVER_TIME_PER_CATEGORY" && + + } + { + data && widget.type === "SUM_PER_CATEGORY" && + + { utils.formatCurrency(data) } + + }
diff --git a/frontend/src/components/widgets/WidgetEditModal.jsx b/frontend/src/components/widgets/WidgetEditModal.jsx index 01328ca..b3a297b 100644 --- a/frontend/src/components/widgets/WidgetEditModal.jsx +++ b/frontend/src/components/widgets/WidgetEditModal.jsx @@ -1,7 +1,17 @@ import Box from "@mui/material/Box"; import Divider from "@mui/material/Divider"; import Grid from "@mui/material/Unstable_Grid2"; -import {Checkbox, FormControlLabel, MenuItem, Modal, OutlinedInput, Select, Slider, TextField} from "@mui/material"; +import { + Checkbox, + FormControlLabel, + Menu, + MenuItem, + Modal, + OutlinedInput, + Select, + Slider, + TextField +} from "@mui/material"; import Typography from "@mui/material/Typography"; import utils from "@/utils.js"; import {DatePicker} from "@mui/x-date-pickers"; @@ -11,6 +21,7 @@ import {Close as CloseIcon, Save as SaveIcon} from "@mui/icons-material"; import * as React from "react"; import {useEffect, useState} from "react"; import { PARAMS } from "@/components/widgets/WidgetParameters.js"; +import Card from "@mui/material/Card"; export default function WidgetEditModal( { @@ -30,7 +41,10 @@ export default function WidgetEditModal( useEffect(() => { setWidget({ ...initialWidget, - selectedCategories: initialWidget.parameters?.filter(p => p.name.includes("category")).map(p => p.numericValue) ?? [] + parameters: initialWidget.parameters?.reduce((acc, item) => { + acc[item.name] = { ...item, name: undefined }; + return acc; + }, {}) ?? {} }); }, [initialWidget]); @@ -48,56 +62,96 @@ export default function WidgetEditModal( .then(resp => setTimePeriods(resp.result)); }, []); + useEffect(() => { + console.log(widget); + }, [widget]); + function widgetParams(name, defaultValue) { if (!widget.parameters) { - widget.parameters = []; + widget.parameters = {}; } - let val = widget.parameters?.find(p => p.name === name); + let val = widget.parameters[name]; if (val) { return val; } - let newVal = { - name: name - }; + let newVal = {}; defaultValue(newVal); - widget.parameters.push(newVal); + widget.parameters[name] = newVal; setWidget({...widget}); return newVal; } + function widgetParamsMultiselect(prefix, fetchValue) { + return Object.entries(widget.parameters) + .filter(([name, value]) => name.startsWith(prefix)) + .map(([name, value]) => fetchValue(value)); + } + function setWidgetParam(name, setParam) { - var param = widget.parameters?.find(p => p.name === name); + var param = widget.parameters[name]; if (!param) { - widget.parameters = [...widget.parameters]; - widget.parameters.push({ - name: name - }); - param = widget.parameters?.find(p => p.name === name); + widget.parameters = {...widget.parameters}; + widget.parameters[name] = {}; + param = widget.parameters[name]; } setParam(param); setWidget({...widget}); } + function setWidgetParamsMultiselect(prefix, selected, setParam) { + // First, clear the current parameters of all elements with the prefix + // Then, re-insert the selected elements + + widget.parameters = Object.entries(widget.parameters) + .filter(([name, value]) => !name.startsWith(prefix)) + .reduce((acc, item) => { + acc[item.name] = { ...item, name: undefined }; + return acc; + }); + + selected.forEach((value, i) => { + widget.parameters[`${prefix}-${i}`] = {}; + setParam(widget.parameters[`${prefix}-${i}`], value); + }) + + setWidget({...widget}); + } + function mapWidget() { - widget.parameters = widget.parameters.concat(widget.selectedCategories?.map((c, i) => { - return { - name: `${PARAMS.CATEGORY_PREFIX}-${i}`, - numericValue: c - } - })); + // widget.parameters = widget.parameters.concat(widget.selectedCategories?.map((c, i) => { + // return { + // name: `${PARAMS.CATEGORY_PREFIX}-${i}`, + // numericValue: c + // } + // })); - widget.selectedCategories = undefined; + // widget.selectedCategories = undefined; - return {...widget} + return { + ...widget, + selectedCategories: undefined, + parameters: Object.entries(widget.parameters).map(([name, value]) => { + return { + ...value, + name: name + } + }) + // .concat(widget.selectedCategories?.map((c, i) => { + // return { + // name: `${PARAMS.CATEGORY_PREFIX}-${i}`, + // numericValue: c + // } + // })) + } } function onEditWidget() { @@ -111,23 +165,24 @@ export default function WidgetEditModal( return ( widget && - + { widget.dbId ? ( -

Editing Widget

+ Editing Widget ) : ( -

Create New Widget

+ Create New Widget ) } @@ -249,7 +304,9 @@ export default function WidgetEditModal( } + + val.booleanValue = false)?.booleanValue} + onChange={(e) => { + setWidgetParam(PARAMS.INCLUDE_UNCATEGORIZED, p => p.booleanValue = e.target.checked); + }} + /> + } + label="Include Uncategorized" + labelPlacement="end" + /> +