mirror of
https://github.com/mvvasilev/personal-finances.git
synced 2025-04-19 14:19:52 +03:00
Added Sum Widget
This commit is contained in:
parent
3ec459da6f
commit
51465ad44d
16 changed files with 447 additions and 280 deletions
0
APIGateway/Dockerfile
Normal file
0
APIGateway/Dockerfile
Normal file
0
PersonalFinancesService/Dockerfile
Normal file
0
PersonalFinancesService/Dockerfile
Normal file
|
@ -1,9 +0,0 @@
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: 'postgres:latest'
|
|
||||||
environment:
|
|
||||||
- 'POSTGRES_DB=mydatabase'
|
|
||||||
- 'POSTGRES_PASSWORD=secret'
|
|
||||||
- 'POSTGRES_USER=myuser'
|
|
||||||
ports:
|
|
||||||
- '5432'
|
|
|
@ -39,7 +39,8 @@ public class StatisticsController extends AbstractRestController {
|
||||||
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: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));
|
return ok(statisticsService.spendingByCategory(categoryId, from, to));
|
||||||
}
|
}
|
||||||
|
@ -50,9 +51,21 @@ public class StatisticsController extends AbstractRestController {
|
||||||
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: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));
|
return ok(statisticsService.spendingByCategoryOverTime(categoryId, period, from, to));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sumByCategory")
|
||||||
|
@PreAuthorize("@authService.isOwner(#categoryId, T(dev.mvvasilev.finances.entity.TransactionCategory))")
|
||||||
|
public ResponseEntity<APIResponseDTO<Double>> 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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,8 @@ import dev.mvvasilev.common.data.PersistableEnum;
|
||||||
|
|
||||||
public enum WidgetType implements PersistableEnum<String> {
|
public enum WidgetType implements PersistableEnum<String> {
|
||||||
TOTAL_SPENDING_PER_CATEGORY,
|
TOTAL_SPENDING_PER_CATEGORY,
|
||||||
SPENDING_OVER_TIME_PER_CATEGORY;
|
SPENDING_OVER_TIME_PER_CATEGORY,
|
||||||
|
SUM_PER_CATEGORY;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String value() {
|
public String value() {
|
||||||
|
|
|
@ -70,4 +70,23 @@ public class StatisticsRepository {
|
||||||
((Tuple) r).get("period_beginning_timestamp", Timestamp.class).toLocalDateTime()
|
((Tuple) r).get("period_beginning_timestamp", Timestamp.class).toLocalDateTime()
|
||||||
)).toList();
|
)).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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package dev.mvvasilev.finances.persistence;
|
||||||
|
|
||||||
import dev.mvvasilev.finances.entity.WidgetParameter;
|
import dev.mvvasilev.finances.entity.WidgetParameter;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@ -12,4 +13,7 @@ public interface WidgetParameterRepository extends JpaRepository<WidgetParameter
|
||||||
|
|
||||||
Collection<WidgetParameter> findAllByWidgetIdIn(Collection<Long> widgetIds);
|
Collection<WidgetParameter> findAllByWidgetIdIn(Collection<Long> widgetIds);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
void deleteAllByWidgetId(Long widgetId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,4 +75,8 @@ public class StatisticsService {
|
||||||
case YEARLY -> ChronoUnit.YEARS.between(from, to) <= 30;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,15 @@ public class WidgetService {
|
||||||
|
|
||||||
widgetRepository.saveAndFlush(mapWidget(widget.get(), widget.get().getUserId(), dto));
|
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
|
return 1; // TODO: fetch rows affected from database
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
0
docker-compose.yml
Normal file
0
docker-compose.yml
Normal file
|
@ -11,6 +11,7 @@ import Grid from "@mui/material/Unstable_Grid2";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import {
|
import {
|
||||||
|
Backdrop,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
|
@ -26,6 +27,8 @@ import CategoriesBox from "@/components/categories/CategoriesBox.jsx";
|
||||||
import {PARAMS} from "@/components/widgets/WidgetParameters.js";
|
import {PARAMS} from "@/components/widgets/WidgetParameters.js";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import VisuallyHiddenInput from "@/components/VisuallyHiddenInput.jsx";
|
import VisuallyHiddenInput from "@/components/VisuallyHiddenInput.jsx";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
export default function CategoriesPage() {
|
export default function CategoriesPage() {
|
||||||
|
|
||||||
|
@ -220,6 +223,7 @@ export default function CategoriesPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
|
|
||||||
<Grid container xs={12} lg={12}>
|
<Grid container xs={12} lg={12}>
|
||||||
|
@ -279,55 +283,6 @@ export default function CategoriesPage() {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Modal
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "50%",
|
|
||||||
left: "50%",
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
width: 400,
|
|
||||||
height: "fit-content",
|
|
||||||
p: 4
|
|
||||||
}}
|
|
||||||
open={isCategoryModalOpen}
|
|
||||||
>
|
|
||||||
<Box>
|
|
||||||
<h3>Create New Category</h3>
|
|
||||||
<Divider></Divider>
|
|
||||||
<Grid container spacing={1}>
|
|
||||||
<Grid xs={12} lg={12}>
|
|
||||||
<TextField
|
|
||||||
id="category-name"
|
|
||||||
label="Category Name"
|
|
||||||
variant="outlined"
|
|
||||||
onChange={(e) => setNewCategoryName(e.target.value)}
|
|
||||||
autoFocus
|
|
||||||
sx={{width: "100%"}}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid xs={6} lg={6}>
|
|
||||||
<Button
|
|
||||||
sx={{width: "100%"}}
|
|
||||||
variant="contained"
|
|
||||||
onClick={createNewCategory}
|
|
||||||
startIcon={<SaveIcon />}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
<Grid xs={6} lg={6}>
|
|
||||||
<Button
|
|
||||||
sx={{width: "100%"}}
|
|
||||||
onClick={() => openCategoryModal(false)}
|
|
||||||
startIcon={<CloseIcon />}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
</Modal>
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={showConfirmDeleteCategoryModal}
|
open={showConfirmDeleteCategoryModal}
|
||||||
>
|
>
|
||||||
|
@ -424,5 +379,55 @@ export default function CategoriesPage() {
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Modal
|
||||||
|
open={isCategoryModalOpen}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: 400,
|
||||||
|
height: "fit-content",
|
||||||
|
p: 4
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ pb: 1, fontSize: "1.25em"}}>Create New Category</Typography>
|
||||||
|
<Divider></Divider>
|
||||||
|
<Grid container spacing={1}>
|
||||||
|
<Grid xs={12} lg={12}>
|
||||||
|
<TextField
|
||||||
|
id="category-name"
|
||||||
|
label="Category Name"
|
||||||
|
variant="outlined"
|
||||||
|
onChange={(e) => setNewCategoryName(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
sx={{width: "100%"}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid xs={6} lg={6}>
|
||||||
|
<Button
|
||||||
|
sx={{width: "100%"}}
|
||||||
|
variant="contained"
|
||||||
|
onClick={createNewCategory}
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid xs={6} lg={6}>
|
||||||
|
<Button
|
||||||
|
sx={{width: "100%"}}
|
||||||
|
onClick={() => openCategoryModal(false)}
|
||||||
|
startIcon={<CloseIcon />}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Card>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -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() {
|
function removeWidget() {
|
||||||
|
|
|
@ -10,27 +10,20 @@ import utils from "@/utils.js";
|
||||||
import { PARAMS } from "@/components/widgets/WidgetParameters.js";
|
import { PARAMS } from "@/components/widgets/WidgetParameters.js";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import 'chart.js/auto';
|
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}) {
|
export default function WidgetContainer({widget, sx, onEdit, onRemove}) {
|
||||||
|
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
switch (widget.type) {
|
|
||||||
case "TOTAL_SPENDING_PER_CATEGORY": {
|
|
||||||
var queryString = "";
|
var queryString = "";
|
||||||
|
|
||||||
console.log(widget);
|
|
||||||
|
|
||||||
queryString += widget.parameters?.filter(p => p.name.includes(PARAMS.CATEGORY_PREFIX)).map(c => `categoryId=${c.numericValue}`)?.join("&");
|
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 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;
|
let isFromStatic = widget.parameters?.find(p => p.name === PARAMS.IS_FROM_DATE_STATIC)?.booleanValue ?? false;
|
||||||
|
|
||||||
console.log(isToNow);
|
|
||||||
console.log(isFromStatic);
|
|
||||||
|
|
||||||
var fromDate;
|
var fromDate;
|
||||||
var toDate;
|
var toDate;
|
||||||
|
|
||||||
|
@ -46,9 +39,14 @@ export default function WidgetContainer({widget, sx, onEdit, onRemove}) {
|
||||||
fromDate = dayjs(widget.parameters?.find(p => p.name === PARAMS.FROM_DATE)?.timestampValue);
|
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 += `&fromDate=${fromDate}`;
|
||||||
queryString += `&toDate=${toDate}`;
|
queryString += `&toDate=${toDate}`;
|
||||||
|
queryString += `&includeUncategorized=${includeUncategorized}`;
|
||||||
|
|
||||||
|
switch (widget.type) {
|
||||||
|
case "TOTAL_SPENDING_PER_CATEGORY": {
|
||||||
utils.performRequest(`/api/statistics/totalSpendingByCategory?${queryString}`)
|
utils.performRequest(`/api/statistics/totalSpendingByCategory?${queryString}`)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.then(resp => setData(resp.result));
|
.then(resp => setData(resp.result));
|
||||||
|
@ -56,7 +54,16 @@ export default function WidgetContainer({widget, sx, onEdit, onRemove}) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "SPENDING_OVER_TIME_PER_CATEGORY": {
|
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 => resp.json())
|
||||||
.then(resp => setData(resp.result));
|
.then(resp => setData(resp.result));
|
||||||
|
|
||||||
|
@ -109,7 +116,7 @@ export default function WidgetContainer({widget, sx, onEdit, onRemove}) {
|
||||||
<Divider></Divider>
|
<Divider></Divider>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid xs={12} lg={12}>
|
<Grid xs={12} lg={12}>
|
||||||
<div 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" &&
|
data && widget.type === "TOTAL_SPENDING_PER_CATEGORY" &&
|
||||||
<Pie
|
<Pie
|
||||||
|
@ -129,6 +136,18 @@ export default function WidgetContainer({widget, sx, onEdit, onRemove}) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
data && widget.type === "SPENDING_OVER_TIME_PER_CATEGORY" &&
|
||||||
|
<Line />
|
||||||
|
}
|
||||||
|
{
|
||||||
|
data && widget.type === "SUM_PER_CATEGORY" &&
|
||||||
|
<Typography sx={{
|
||||||
|
fontSize: "2.3em"
|
||||||
|
}}>
|
||||||
|
{ utils.formatCurrency(data) }
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import Grid from "@mui/material/Unstable_Grid2";
|
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 Typography from "@mui/material/Typography";
|
||||||
import utils from "@/utils.js";
|
import utils from "@/utils.js";
|
||||||
import {DatePicker} from "@mui/x-date-pickers";
|
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 * as React from "react";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import { PARAMS } from "@/components/widgets/WidgetParameters.js";
|
import { PARAMS } from "@/components/widgets/WidgetParameters.js";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
|
||||||
export default function WidgetEditModal(
|
export default function WidgetEditModal(
|
||||||
{
|
{
|
||||||
|
@ -30,7 +41,10 @@ export default function WidgetEditModal(
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setWidget({
|
setWidget({
|
||||||
...initialWidget,
|
...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]);
|
}, [initialWidget]);
|
||||||
|
|
||||||
|
@ -48,56 +62,96 @@ export default function WidgetEditModal(
|
||||||
.then(resp => setTimePeriods(resp.result));
|
.then(resp => setTimePeriods(resp.result));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(widget);
|
||||||
|
}, [widget]);
|
||||||
|
|
||||||
function widgetParams(name, defaultValue) {
|
function widgetParams(name, defaultValue) {
|
||||||
if (!widget.parameters) {
|
if (!widget.parameters) {
|
||||||
widget.parameters = [];
|
widget.parameters = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
let val = widget.parameters?.find(p => p.name === name);
|
let val = widget.parameters[name];
|
||||||
|
|
||||||
if (val) {
|
if (val) {
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
let newVal = {
|
let newVal = {};
|
||||||
name: name
|
|
||||||
};
|
|
||||||
|
|
||||||
defaultValue(newVal);
|
defaultValue(newVal);
|
||||||
|
|
||||||
widget.parameters.push(newVal);
|
widget.parameters[name] = newVal;
|
||||||
|
|
||||||
setWidget({...widget});
|
setWidget({...widget});
|
||||||
|
|
||||||
return newVal;
|
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) {
|
function setWidgetParam(name, setParam) {
|
||||||
var param = widget.parameters?.find(p => p.name === name);
|
var param = widget.parameters[name];
|
||||||
|
|
||||||
if (!param) {
|
if (!param) {
|
||||||
widget.parameters = [...widget.parameters];
|
widget.parameters = {...widget.parameters};
|
||||||
widget.parameters.push({
|
widget.parameters[name] = {};
|
||||||
name: name
|
param = widget.parameters[name];
|
||||||
});
|
|
||||||
param = widget.parameters?.find(p => p.name === name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setParam(param);
|
setParam(param);
|
||||||
setWidget({...widget});
|
setWidget({...widget});
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapWidget() {
|
function setWidgetParamsMultiselect(prefix, selected, setParam) {
|
||||||
widget.parameters = widget.parameters.concat(widget.selectedCategories?.map((c, i) => {
|
// First, clear the current parameters of all elements with the prefix
|
||||||
return {
|
// Then, re-insert the selected elements
|
||||||
name: `${PARAMS.CATEGORY_PREFIX}-${i}`,
|
|
||||||
numericValue: c
|
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});
|
||||||
}
|
}
|
||||||
}));
|
|
||||||
|
|
||||||
widget.selectedCategories = undefined;
|
function mapWidget() {
|
||||||
|
// widget.parameters = widget.parameters.concat(widget.selectedCategories?.map((c, i) => {
|
||||||
|
// return {
|
||||||
|
// name: `${PARAMS.CATEGORY_PREFIX}-${i}`,
|
||||||
|
// numericValue: c
|
||||||
|
// }
|
||||||
|
// }));
|
||||||
|
|
||||||
return {...widget}
|
// widget.selectedCategories = undefined;
|
||||||
|
|
||||||
|
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() {
|
function onEditWidget() {
|
||||||
|
@ -111,6 +165,9 @@ export default function WidgetEditModal(
|
||||||
return (
|
return (
|
||||||
widget &&
|
widget &&
|
||||||
<Modal
|
<Modal
|
||||||
|
open={open}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: "50%",
|
top: "50%",
|
||||||
|
@ -120,14 +177,12 @@ export default function WidgetEditModal(
|
||||||
height: "fit-content",
|
height: "fit-content",
|
||||||
p: 4
|
p: 4
|
||||||
}}
|
}}
|
||||||
open={open}
|
|
||||||
>
|
>
|
||||||
<Box>
|
|
||||||
{
|
{
|
||||||
widget.dbId ? (
|
widget.dbId ? (
|
||||||
<h3>Editing Widget</h3>
|
<Typography sx={{ pb: 1, fontSize: "1.25em"}}>Editing Widget</Typography>
|
||||||
) : (
|
) : (
|
||||||
<h3>Create New Widget</h3>
|
<Typography sx={{ pb: 1, fontSize: "1.25em"}}>Create New Widget</Typography>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<Divider></Divider>
|
<Divider></Divider>
|
||||||
|
@ -249,7 +304,9 @@ export default function WidgetEditModal(
|
||||||
<Grid xs={12} lg={12}>
|
<Grid xs={12} lg={12}>
|
||||||
<Select
|
<Select
|
||||||
id={"time-period-type"}
|
id={"time-period-type"}
|
||||||
sx={{ width: "100%" }}
|
sx={{
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
value={widgetParams(PARAMS.TIME_PERIOD, val => val.stringValue = "placeholder")?.stringValue}
|
value={widgetParams(PARAMS.TIME_PERIOD, val => val.stringValue = "placeholder")?.stringValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setWidgetParam(PARAMS.TIME_PERIOD, p => p.stringValue = e.target.value);
|
setWidgetParam(PARAMS.TIME_PERIOD, p => p.stringValue = e.target.value);
|
||||||
|
@ -271,21 +328,35 @@ export default function WidgetEditModal(
|
||||||
</Select>
|
</Select>
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
|
<Grid xs={12} lg={12}>
|
||||||
|
<FormControlLabel
|
||||||
|
sx={{ width: "100%", height: "100%" }}
|
||||||
|
value="end"
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={widgetParams(PARAMS.INCLUDE_UNCATEGORIZED, val => val.booleanValue = false)?.booleanValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
setWidgetParam(PARAMS.INCLUDE_UNCATEGORIZED, p => p.booleanValue = e.target.checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Include Uncategorized"
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
<Grid xs={12} lg={12}>
|
<Grid xs={12} lg={12}>
|
||||||
<Select
|
<Select
|
||||||
sx={{ width: "100%", height: "100%" }}
|
sx={{ width: "100%", height: "100%" }}
|
||||||
input={<OutlinedInput label="Categories" />}
|
input={<OutlinedInput label="Categories" />}
|
||||||
multiple
|
multiple
|
||||||
value={widget.selectedCategories ?? []}
|
value={widgetParamsMultiselect(PARAMS.CATEGORY_PREFIX, (v) => v.numericValue) ?? []}
|
||||||
renderValue={(selected) => {
|
renderValue={(selected) => {
|
||||||
console.log(selected)
|
|
||||||
return (<Typography>
|
return (<Typography>
|
||||||
{ selected.map(s => categories.find(c => c.id === s)?.name)?.join(", ")}
|
{ selected.map(s => categories.find(c => c.id === s)?.name)?.join(", ")}
|
||||||
</Typography>)
|
</Typography>)
|
||||||
}}
|
}}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
widget.selectedCategories = e.target.value;
|
setWidgetParamsMultiselect(PARAMS.CATEGORY_PREFIX, e.target.value, (param, value) => param.numericValue = value);
|
||||||
setWidget({...widget});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem value="placeholder" disabled>
|
<MenuItem value="placeholder" disabled>
|
||||||
|
@ -294,7 +365,7 @@ export default function WidgetEditModal(
|
||||||
{
|
{
|
||||||
categories.map(c => (
|
categories.map(c => (
|
||||||
<MenuItem key={c.id} value={c.id}>
|
<MenuItem key={c.id} value={c.id}>
|
||||||
<Checkbox checked={widget.selectedCategories?.findIndex(cat => cat === c.id) > -1} />
|
<Checkbox checked={widgetParamsMultiselect(PARAMS.CATEGORY_PREFIX, (v) => v.numericValue)?.findIndex(cat => cat === c.id) > -1} />
|
||||||
<Typography>{ c.name }</Typography>
|
<Typography>{ c.name }</Typography>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))
|
))
|
||||||
|
@ -335,7 +406,7 @@ export default function WidgetEditModal(
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Card>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -5,5 +5,6 @@ export const PARAMS = {
|
||||||
IS_FROM_DATE_STATIC: "isFromDateStatic",
|
IS_FROM_DATE_STATIC: "isFromDateStatic",
|
||||||
IS_TO_NOW: "isToDateToNow",
|
IS_TO_NOW: "isToDateToNow",
|
||||||
TIME_PERIOD: "timePeriod",
|
TIME_PERIOD: "timePeriod",
|
||||||
CATEGORY_PREFIX: "category"
|
CATEGORY_PREFIX: "category",
|
||||||
|
INCLUDE_UNCATEGORIZED: "includeUncategorized"
|
||||||
}
|
}
|
|
@ -1,5 +1,10 @@
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
let LEV_FORMAT = new Intl.NumberFormat('bg-BG', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'BGN',
|
||||||
|
});
|
||||||
|
|
||||||
let utils = {
|
let utils = {
|
||||||
performRequest: async (url, options) => {
|
performRequest: async (url, options) => {
|
||||||
return await fetch(url, options).then(resp => {
|
return await fetch(url, options).then(resp => {
|
||||||
|
@ -33,6 +38,9 @@ let utils = {
|
||||||
generateUUID: () => v4(),
|
generateUUID: () => v4(),
|
||||||
isNumeric: (value) => {
|
isNumeric: (value) => {
|
||||||
return /^-?\d+(\.\d+)?$/.test(value);
|
return /^-?\d+(\.\d+)?$/.test(value);
|
||||||
|
},
|
||||||
|
formatCurrency(number) {
|
||||||
|
return LEV_FORMAT.format(number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue