From ccf5dd5c397a5f26ad85b4d534d03e5636a0be4c Mon Sep 17 00:00:00 2001 From: Tim <47184194+imgde@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:03:58 +0200 Subject: [PATCH] Stock Statistic --- .../webshop/config/ControllerPathConfig.java | 1 + .../controller/StatisticsController.java | 13 ++++++ .../webshop/service/StatisticsService.java | 4 ++ .../service/impl/StatisticsServiceImpl.java | 24 +++++++++- .../src/helper/adminpanel/ItemsInfo.tsx | 40 +--------------- .../src/helper/adminpanel/StatisticsInfo.tsx | 45 ++++++++++++++++-- 01-frontend/src/helper/query/Queries.tsx | 12 ++++- 01-frontend/src/util/ColorUtil.tsx | 46 +++++++++++++++++++ 8 files changed, 139 insertions(+), 46 deletions(-) create mode 100644 01-frontend/src/util/ColorUtil.tsx diff --git a/00-backend/src/main/java/de/htwsaar/webshop/config/ControllerPathConfig.java b/00-backend/src/main/java/de/htwsaar/webshop/config/ControllerPathConfig.java index cdc1f56..86705bf 100644 --- a/00-backend/src/main/java/de/htwsaar/webshop/config/ControllerPathConfig.java +++ b/00-backend/src/main/java/de/htwsaar/webshop/config/ControllerPathConfig.java @@ -50,5 +50,6 @@ public class ControllerPathConfig { public static final String STATISTICS_VOLUME = STATISTICS_BASE + "/volume"; public static final String STATISTICS_REVENUE = STATISTICS_BASE + "/revenue"; public static final String STATISTICS_ORDERSTATUS = STATISTICS_BASE + "/orderstatus"; + public static final String STATISTICS_STOCKPERCENT = STATISTICS_BASE + "/stockpercent"; } \ No newline at end of file diff --git a/00-backend/src/main/java/de/htwsaar/webshop/controller/StatisticsController.java b/00-backend/src/main/java/de/htwsaar/webshop/controller/StatisticsController.java index 3c1e3e6..4aa6334 100644 --- a/00-backend/src/main/java/de/htwsaar/webshop/controller/StatisticsController.java +++ b/00-backend/src/main/java/de/htwsaar/webshop/controller/StatisticsController.java @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -68,4 +69,16 @@ public class StatisticsController { return ResponseEntity.ok(statisticsService.getOrderStatus()); } + @RequestMapping(value = STATISTICS_STOCKPERCENT, method = RequestMethod.GET, produces = "application/json") + public ResponseEntity>> getStockPercent(HttpServletRequest request, + @RequestParam(value = PARAM_SESSION) UUID token, + @RequestParam(value = PARAM_EMAIL) String email) { + logRequest(request); + if (!sessionService.isAdmin(token, email)) { + log.warn("Invalid session requesting Admin {}", token); + return ResponseEntity.status(403).build(); + } + return ResponseEntity.ok(statisticsService.getStockPercent()); + } + } diff --git a/00-backend/src/main/java/de/htwsaar/webshop/service/StatisticsService.java b/00-backend/src/main/java/de/htwsaar/webshop/service/StatisticsService.java index 8c1480b..b7ef384 100644 --- a/00-backend/src/main/java/de/htwsaar/webshop/service/StatisticsService.java +++ b/00-backend/src/main/java/de/htwsaar/webshop/service/StatisticsService.java @@ -4,6 +4,8 @@ import de.htwsaar.webshop.model.CatMonthModel; import de.htwsaar.webshop.model.OrderStatus; import java.util.Map; +import java.util.List; +import java.util.UUID; public interface StatisticsService { CatMonthModel getSalesVolume(); @@ -12,4 +14,6 @@ public interface StatisticsService { Map getOrderStatus(); + Map> getStockPercent(); + } diff --git a/00-backend/src/main/java/de/htwsaar/webshop/service/impl/StatisticsServiceImpl.java b/00-backend/src/main/java/de/htwsaar/webshop/service/impl/StatisticsServiceImpl.java index 9cb7bc4..e943bed 100644 --- a/00-backend/src/main/java/de/htwsaar/webshop/service/impl/StatisticsServiceImpl.java +++ b/00-backend/src/main/java/de/htwsaar/webshop/service/impl/StatisticsServiceImpl.java @@ -4,6 +4,7 @@ import de.htwsaar.webshop.model.ArticleCategory; import de.htwsaar.webshop.model.CatMonthModel; import de.htwsaar.webshop.model.OrderStatus; import de.htwsaar.webshop.repository.entities.OrderItem; +import de.htwsaar.webshop.service.ArticleService; import de.htwsaar.webshop.service.OrderService; import de.htwsaar.webshop.service.StatisticsService; import de.htwsaar.webshop.util.TimeUtil; @@ -11,8 +12,10 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.List; import java.util.Map; import java.util.TreeMap; +import java.util.UUID; import java.util.function.BinaryOperator; import java.util.function.Function; @@ -20,10 +23,12 @@ import java.util.function.Function; @Slf4j public class StatisticsServiceImpl implements StatisticsService { private final OrderService orderService; + private final ArticleService articleService; @Autowired - public StatisticsServiceImpl(OrderService orderService) { + public StatisticsServiceImpl(OrderService orderService, ArticleService articleService) { this.orderService = orderService; + this.articleService = articleService; } //returns Map> @@ -68,4 +73,21 @@ public class StatisticsServiceImpl implements StatisticsService { }); return map; } + + @Override + public Map> getStockPercent() { + Map> map = new TreeMap<>(); + for (ArticleCategory value : ArticleCategory.values()) { + map.putIfAbsent(value.loc, new TreeMap<>()); + log.info("Value {}", value.loc); + } + articleService.findAll().forEach(article -> { + int percent = (int) Math.floor(((1.0d * article.getStock() / article.getStockExpected()) * 100) / 10) * 10; + log.info("Stock percent: {} {}", article.getUuid(), percent); + log.info("Cat: {}", article.getCategory()); + map.get(article.getCategory()).putIfAbsent(percent, 0); + map.get(article.getCategory()).computeIfPresent(percent, (k,v) -> ++v); + }); + return map; + } } diff --git a/01-frontend/src/helper/adminpanel/ItemsInfo.tsx b/01-frontend/src/helper/adminpanel/ItemsInfo.tsx index a3571cd..b67b91e 100644 --- a/01-frontend/src/helper/adminpanel/ItemsInfo.tsx +++ b/01-frontend/src/helper/adminpanel/ItemsInfo.tsx @@ -9,50 +9,12 @@ import {Gauge, gaugeClasses} from "@mui/x-charts"; import {useAccount} from "../AccountProvider.tsx"; import {useQuery} from "@tanstack/react-query"; import {fetchItems} from "../query/Queries.tsx"; +import { mapValueToColor } from "../../util/ColorUtil.tsx"; export default function ItemsInfo() { const theme = useTheme(); const {t} = useTranslation(); - function mapValueToColor(minVal: number, maxVal: number, actualVal: number): string { - const clamped = Math.min(Math.max(actualVal, minVal), maxVal); - - // Calculate interpolation ratio (0-1) - const ratio = maxVal !== minVal - ? (clamped - minVal) / (maxVal - minVal) - : 0; - return hsvDegToHex(120 * ratio);//120° is green, 0° is red - } - - function hsvDegToHex(h: number): string { - h = Math.max(0, Math.min(360, h)); - - const c = 1; - const x = (1 - Math.abs(((h / 60) % 2) - 1)); - - let r, g, b; - if (h < 60) { - r = c; g = x; b = 0; - } else if (h < 120) { - r = x; g = c; b = 0; - } else if (h < 180) { - r = 0; g = c; b = x; - } else if (h < 240) { - r = 0; g = x; b = c; - } else if (h < 300) { - r = x; g = 0; b = c; - } else { - r = c; g = 0; b = x; - } - - // scale to 0-255 - const rHex = Math.round(r * 255).toString(16).padStart(2, '0'); - const gHex = Math.round(g * 255).toString(16).padStart(2, '0'); - const bHex = Math.round(b * 255).toString(16).padStart(2, '0'); - - return `#${rHex}${gHex}${bHex}`; - } - function handleIconEdit(item: Item) { //TODO: implement console.log("IconEdit", item); diff --git a/01-frontend/src/helper/adminpanel/StatisticsInfo.tsx b/01-frontend/src/helper/adminpanel/StatisticsInfo.tsx index 36516dc..b9c46ad 100644 --- a/01-frontend/src/helper/adminpanel/StatisticsInfo.tsx +++ b/01-frontend/src/helper/adminpanel/StatisticsInfo.tsx @@ -5,9 +5,9 @@ import { BarSeriesType } from '@mui/x-charts' import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useQuery } from "@tanstack/react-query"; -import { fetchStatisticsVolume, fetchStatisticsRevenue, fetchOrderStatus } from "../query/Queries.tsx"; +import { fetchStatisticsVolume, fetchStatisticsRevenue, fetchOrderStatus, fetchStockPercent } from "../query/Queries.tsx"; import { useAccount } from "../AccountProvider.tsx"; -import { data } from "react-router-dom"; +import { getColorFromPercent } from "../../util/ColorUtil.tsx"; export default function StatisticsInfo() { const theme = useTheme(); @@ -23,6 +23,9 @@ export default function StatisticsInfo() { const [orderStatus, setOrderStatus] = useState([]); + const [stockPercent, setStockPercent] = useState([]); + + const { user: loginData } = useAccount(); @@ -47,6 +50,12 @@ export default function StatisticsInfo() { retryDelay: 0, }); + const { data: dataStockPercent } = useQuery({ + queryKey: ["fetchStockPercent", loginData], + queryFn: () => fetchStockPercent(loginData ? loginData : { email: "", password: "", session: "", customerId: -1, isAdmin: false }), + retry: 0, + retryDelay: 0, + }); useEffect(() => { @@ -123,7 +132,30 @@ export default function StatisticsInfo() { } setOrderStatus(orderStatus) } - }, [dataOrderStatus]) + }, [dataOrderStatus]) ; + + useEffect(() => { + if(dataStockPercent) { + console.log(dataStockPercent); + const stockPercent = [] + let i = 0 + for(let x = 0; x < 10; x++) { + stockPercent.push({value: 0, label: String(x*10), color: getColorFromPercent(String(x*10))}); + } + for(var cat in dataStockPercent) { + for(var percent in dataStockPercent[cat]) { + let index = stockPercent.findIndex( (entry) => entry.label == percent) + const datapoint = dataStockPercent[cat][percent] + if(index === -1) { + index = stockPercent.push({value: 0, label: percent, color: getColorFromPercent(percent)}) -1 + } + stockPercent[index].value += datapoint + } + i++ + } + setStockPercent(stockPercent) + } + }, [dataStockPercent]) return ( @@ -175,6 +207,7 @@ export default function StatisticsInfo() { series={[{ data: totalRevenue, highlightScope: { fade: 'global', highlight: 'item' }, + valueFormatter: (v) => (v ? `${v.value}€` : '-'), }]} width={200} height={200} @@ -186,10 +219,14 @@ export default function StatisticsInfo() { diff --git a/01-frontend/src/helper/query/Queries.tsx b/01-frontend/src/helper/query/Queries.tsx index f38d2c4..bf2a953 100644 --- a/01-frontend/src/helper/query/Queries.tsx +++ b/01-frontend/src/helper/query/Queries.tsx @@ -211,7 +211,15 @@ export const orderPatch = async (order: OrderPatch) => { export const fetchOrderStatus = async (loginData: User) => { const response = await fetch("http://localhost:8085/statistics/orderstatus?email=" + loginData.email + "&session=" + loginData.session); if (!response.ok) { - throw new Error("fetching satistics Revenue failed"); + throw new Error("fetching order status failed"); } return response.json(); -}; \ No newline at end of file +}; + +export const fetchStockPercent = async (loginData: User) => { + const response = await fetch("http://localhost:8085/statistics/stockpercent?email=" + loginData.email + "&session=" + loginData.session); + if (!response.ok) { + throw new Error("fetching stock% failed"); + } + return response.json(); +}; diff --git a/01-frontend/src/util/ColorUtil.tsx b/01-frontend/src/util/ColorUtil.tsx new file mode 100644 index 0000000..bdebd4b --- /dev/null +++ b/01-frontend/src/util/ColorUtil.tsx @@ -0,0 +1,46 @@ +export function mapValueToColor(minVal: number, maxVal: number, actualVal: number): string { + const clamped = Math.min(Math.max(actualVal, minVal), maxVal); + + // Calculate interpolation ratio (0-1) + const ratio = maxVal !== minVal + ? (clamped - minVal) / (maxVal - minVal) + : 0; + return hsvDegToHex(120 * ratio);//120° is green, 0° is red +} + +export function getColorFromPercent(percent: string): string { + let perc = Number(percent) / 100 + if(perc > 1){ + perc = 2.5; + } + return hsvDegToHex(120 * perc); +} + +export function hsvDegToHex(h: number): string { + h = Math.max(0, Math.min(360, h)); + + const c = 1; + const x = (1 - Math.abs(((h / 60) % 2) - 1)); + + let r, g, b; + if (h < 60) { + r = c; g = x; b = 0; + } else if (h < 120) { + r = x; g = c; b = 0; + } else if (h < 180) { + r = 0; g = c; b = x; + } else if (h < 240) { + r = 0; g = x; b = c; + } else if (h < 300) { + r = x; g = 0; b = c; + } else { + r = c; g = 0; b = x; + } + + // scale to 0-255 + const rHex = Math.round(r * 255).toString(16).padStart(2, '0'); + const gHex = Math.round(g * 255).toString(16).padStart(2, '0'); + const bHex = Math.round(b * 255).toString(16).padStart(2, '0'); + + return `#${rHex}${gHex}${bHex}`; +}