Fix to and from dates for statistics

This commit is contained in:
Miroslav Vasilev 2024-01-03 23:51:12 +02:00
parent 51465ad44d
commit a95fe9dcf4
5 changed files with 42 additions and 20 deletions

View file

@ -7,6 +7,7 @@ import dev.mvvasilev.finances.dtos.SpendingOverTimeByCategoryDTO;
import dev.mvvasilev.finances.enums.TimePeriod; import dev.mvvasilev.finances.enums.TimePeriod;
import dev.mvvasilev.finances.services.StatisticsService; import dev.mvvasilev.finances.services.StatisticsService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -38,8 +39,10 @@ public class StatisticsController extends AbstractRestController {
@PreAuthorize("@authService.isOwner(#categoryId, T(dev.mvvasilev.finances.entity.TransactionCategory))") @PreAuthorize("@authService.isOwner(#categoryId, T(dev.mvvasilev.finances.entity.TransactionCategory))")
public ResponseEntity<APIResponseDTO<SpendingByCategoriesDTO>> fetchSpendingByCategory( public ResponseEntity<APIResponseDTO<SpendingByCategoriesDTO>> fetchSpendingByCategory(
Long[] categoryId, Long[] categoryId,
@RequestParam(defaultValue = "1970-01-01T00:00:00") LocalDateTime from, @RequestParam(defaultValue = "1970-01-01T00:00:00Z")
@RequestParam(defaultValue = "2099-01-01T00:00:00") LocalDateTime to, @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime from,
@RequestParam(defaultValue = "2099-01-01T00:00:00Z")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime to,
@RequestParam(defaultValue = "false") Boolean includeUncategorized @RequestParam(defaultValue = "false") Boolean includeUncategorized
) { ) {
return ok(statisticsService.spendingByCategory(categoryId, from, to)); return ok(statisticsService.spendingByCategory(categoryId, from, to));
@ -50,8 +53,10 @@ public class StatisticsController extends AbstractRestController {
public ResponseEntity<APIResponseDTO<SpendingOverTimeByCategoryDTO>> fetchSpendingOverTimeByCategory( public ResponseEntity<APIResponseDTO<SpendingOverTimeByCategoryDTO>> fetchSpendingOverTimeByCategory(
Long[] categoryId, Long[] categoryId,
@RequestParam(defaultValue = "DAILY") TimePeriod period, @RequestParam(defaultValue = "DAILY") TimePeriod period,
@RequestParam(defaultValue = "1970-01-01T00:00:00") LocalDateTime from, @RequestParam(defaultValue = "1970-01-01T00:00:00Z")
@RequestParam(defaultValue = "2099-01-01T00:00:00") LocalDateTime to, @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime from,
@RequestParam(defaultValue = "2099-01-01T00:00:00Z")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime to,
@RequestParam(defaultValue = "false") Boolean includeUncategorized @RequestParam(defaultValue = "false") Boolean includeUncategorized
) { ) {
return ok(statisticsService.spendingByCategoryOverTime(categoryId, period, from, to)); return ok(statisticsService.spendingByCategoryOverTime(categoryId, period, from, to));
@ -61,8 +66,10 @@ public class StatisticsController extends AbstractRestController {
@PreAuthorize("@authService.isOwner(#categoryId, T(dev.mvvasilev.finances.entity.TransactionCategory))") @PreAuthorize("@authService.isOwner(#categoryId, T(dev.mvvasilev.finances.entity.TransactionCategory))")
public ResponseEntity<APIResponseDTO<Double>> sum( public ResponseEntity<APIResponseDTO<Double>> sum(
Long[] categoryId, Long[] categoryId,
@RequestParam(defaultValue = "1970-01-01T00:00:00") LocalDateTime from, @RequestParam(defaultValue = "1970-01-01T00:00:00Z")
@RequestParam(defaultValue = "2099-01-01T00:00:00") LocalDateTime to, @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime from,
@RequestParam(defaultValue = "2099-01-01T00:00:00Z")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime to,
@RequestParam(defaultValue = "false") Boolean includeUncategorized @RequestParam(defaultValue = "false") Boolean includeUncategorized
) { ) {
return ok(statisticsService.sumByCategory(categoryId, from, to, includeUncategorized)); return ok(statisticsService.sumByCategory(categoryId, from, to, includeUncategorized));

View file

@ -74,10 +74,14 @@ public class StatisticsRepository {
public Double sumByCategory(Long[] categoryId, LocalDateTime from, LocalDateTime to, Boolean includeUncategorized) { public Double sumByCategory(Long[] categoryId, LocalDateTime from, LocalDateTime to, Boolean includeUncategorized) {
Query nativeQuery = entityManager.createNativeQuery( Query nativeQuery = entityManager.createNativeQuery(
""" """
SELECT SUM(pt.amount) AS result WITH transactions AS (
FROM transactions.processed_transaction AS pt SELECT DISTINCT pt.*
LEFT OUTER JOIN categories.processed_transaction_category AS ptc ON pt.id = ptc.processed_transaction_id FROM transactions.processed_transaction AS pt
WHERE (ptc.category_id = any(?1) OR (?4 AND ptc.category_id IS NULL)) AND (pt.timestamp BETWEEN ?2 AND ?3) LEFT OUTER JOIN categories.processed_transaction_category AS ptc ON pt.id = ptc.processed_transaction_id
WHERE (pt.timestamp BETWEEN ?2 AND ?3) AND (ptc.category_id = any(?1) OR (?4 AND ptc.category_id IS NULL))
)
SELECT COALESCE(SUM(pt.amount), 0) AS result
FROM transactions AS pt
""", """,
Tuple.class Tuple.class
); );

View file

@ -6,7 +6,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import Grid from "@mui/material/Unstable_Grid2"; import Grid from "@mui/material/Unstable_Grid2";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import {Addchart, Save} from "@mui/icons-material"; import {Addchart, Save, Warning} from "@mui/icons-material";
import WidgetContainer from "@/components/widgets/WidgetContainer.jsx"; import WidgetContainer from "@/components/widgets/WidgetContainer.jsx";
import 'react-grid-layout/css/styles.css'; import 'react-grid-layout/css/styles.css';
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
@ -25,6 +25,7 @@ export default function StatisticsPage() {
const [isWidgetModalOpen, openWidgetModal] = useState(false); const [isWidgetModalOpen, openWidgetModal] = useState(false);
const [isRemoveWidgetDialogShown, showRemoveWidgetDialog] = useState(false); const [isRemoveWidgetDialogShown, showRemoveWidgetDialog] = useState(false);
const [removingWidgetId, setRemovingWidgetId] = useState(null); const [removingWidgetId, setRemovingWidgetId] = useState(null);
const [isUnsavedLayoutWarningShown, showUnsavedLayoutWarning] = useState(false);
useEffect(() => { useEffect(() => {
fetchWidgets(); fetchWidgets();
@ -48,7 +49,10 @@ export default function StatisticsPage() {
parameters: w.parameters parameters: w.parameters
} }
}) ?? [])) }) ?? []))
.then(r => utils.hideSpinner()); .then(r => {
utils.hideSpinner();
showUnsavedLayoutWarning(false);
});
} }
function openWidgetCreationModal() { function openWidgetCreationModal() {
@ -119,8 +123,11 @@ export default function StatisticsPage() {
toast.promise( toast.promise(
Promise.resolve(promises) Promise.resolve(promises)
.then(r => fetchWidgets()) //.then(r => fetchWidgets())
.then(r => utils.hideSpinner()), .then(r => {
utils.hideSpinner();
showUnsavedLayoutWarning(false);
}),
{ {
loading: "Saving...", loading: "Saving...",
success: "Saved", success: "Saved",
@ -202,7 +209,7 @@ export default function StatisticsPage() {
sx={{ sx={{
width: "100%" width: "100%"
}} }}
variant="contained" variant={isUnsavedLayoutWarningShown ? "contained" : "outlined"}
onClick={saveWidgetsLayout} onClick={saveWidgetsLayout}
startIcon={<Save/>} startIcon={<Save/>}
> >
@ -225,6 +232,7 @@ export default function StatisticsPage() {
}) })
setWidgets([...newWidgets]) setWidgets([...newWidgets])
console.log("layout change")
}} }}
draggableCancel=".grid-drag-cancel" draggableCancel=".grid-drag-cancel"
cols={{lg: 12, md: 10, sm: 6, xs: 4, xxs: 2}} cols={{lg: 12, md: 10, sm: 6, xs: 4, xxs: 2}}

View file

@ -41,8 +41,8 @@ export default function WidgetContainer({widget, sx, onEdit, onRemove}) {
var includeUncategorized = widget.parameters?.find(p => p.name === PARAMS.INCLUDE_UNCATEGORIZED)?.booleanValue ?? false; var includeUncategorized = widget.parameters?.find(p => p.name === PARAMS.INCLUDE_UNCATEGORIZED)?.booleanValue ?? false;
queryString += `&fromDate=${fromDate}`; queryString += `&from=${fromDate.toISOString()}`;
queryString += `&toDate=${toDate}`; queryString += `&to=${toDate.toISOString()}`;
queryString += `&includeUncategorized=${includeUncategorized}`; queryString += `&includeUncategorized=${includeUncategorized}`;
switch (widget.type) { switch (widget.type) {
@ -118,7 +118,7 @@ export default function WidgetContainer({widget, sx, onEdit, onRemove}) {
<Grid xs={12} lg={12}> <Grid xs={12} lg={12}>
<div className={"grid-drag-cancel"} style={{ position: "relative", height: "100%", width: "100%" }}> <div className={"grid-drag-cancel"} style={{ position: "relative", height: "100%", width: "100%" }}>
{ {
data && widget.type === "TOTAL_SPENDING_PER_CATEGORY" && !utils.isNullOrUndefined(data) && widget.type === "TOTAL_SPENDING_PER_CATEGORY" &&
<Pie <Pie
options={{ options={{
responsive: true, responsive: true,
@ -137,11 +137,11 @@ export default function WidgetContainer({widget, sx, onEdit, onRemove}) {
/> />
} }
{ {
data && widget.type === "SPENDING_OVER_TIME_PER_CATEGORY" && !utils.isNullOrUndefined(data) && widget.type === "SPENDING_OVER_TIME_PER_CATEGORY" &&
<Line /> <Line />
} }
{ {
data && widget.type === "SUM_PER_CATEGORY" && !utils.isNullOrUndefined(data) && widget.type === "SUM_PER_CATEGORY" &&
<Typography sx={{ <Typography sx={{
fontSize: "2.3em" fontSize: "2.3em"
}}> }}>

View file

@ -41,6 +41,9 @@ let utils = {
}, },
formatCurrency(number) { formatCurrency(number) {
return LEV_FORMAT.format(number); return LEV_FORMAT.format(number);
},
isNullOrUndefined(obj) {
return obj === null || obj === undefined;
} }
} }