Stock Statistic

This commit is contained in:
Tim
2025-06-16 15:03:58 +02:00
parent d6e7498c1d
commit ccf5dd5c39
8 changed files with 139 additions and 46 deletions

View File

@@ -50,5 +50,6 @@ public class ControllerPathConfig {
public static final String STATISTICS_VOLUME = STATISTICS_BASE + "/volume"; public static final String STATISTICS_VOLUME = STATISTICS_BASE + "/volume";
public static final String STATISTICS_REVENUE = STATISTICS_BASE + "/revenue"; public static final String STATISTICS_REVENUE = STATISTICS_BASE + "/revenue";
public static final String STATISTICS_ORDERSTATUS = STATISTICS_BASE + "/orderstatus"; public static final String STATISTICS_ORDERSTATUS = STATISTICS_BASE + "/orderstatus";
public static final String STATISTICS_STOCKPERCENT = STATISTICS_BASE + "/stockpercent";
} }

View File

@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@@ -68,4 +69,16 @@ public class StatisticsController {
return ResponseEntity.ok(statisticsService.getOrderStatus()); return ResponseEntity.ok(statisticsService.getOrderStatus());
} }
@RequestMapping(value = STATISTICS_STOCKPERCENT, method = RequestMethod.GET, produces = "application/json")
public ResponseEntity<Map<String, Map<Integer, Integer>>> 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());
}
} }

View File

@@ -4,6 +4,8 @@ import de.htwsaar.webshop.model.CatMonthModel;
import de.htwsaar.webshop.model.OrderStatus; import de.htwsaar.webshop.model.OrderStatus;
import java.util.Map; import java.util.Map;
import java.util.List;
import java.util.UUID;
public interface StatisticsService { public interface StatisticsService {
CatMonthModel<Integer> getSalesVolume(); CatMonthModel<Integer> getSalesVolume();
@@ -12,4 +14,6 @@ public interface StatisticsService {
Map<OrderStatus, Integer> getOrderStatus(); Map<OrderStatus, Integer> getOrderStatus();
Map<String, Map<Integer, Integer>> getStockPercent();
} }

View File

@@ -4,6 +4,7 @@ import de.htwsaar.webshop.model.ArticleCategory;
import de.htwsaar.webshop.model.CatMonthModel; import de.htwsaar.webshop.model.CatMonthModel;
import de.htwsaar.webshop.model.OrderStatus; import de.htwsaar.webshop.model.OrderStatus;
import de.htwsaar.webshop.repository.entities.OrderItem; import de.htwsaar.webshop.repository.entities.OrderItem;
import de.htwsaar.webshop.service.ArticleService;
import de.htwsaar.webshop.service.OrderService; import de.htwsaar.webshop.service.OrderService;
import de.htwsaar.webshop.service.StatisticsService; import de.htwsaar.webshop.service.StatisticsService;
import de.htwsaar.webshop.util.TimeUtil; 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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.UUID;
import java.util.function.BinaryOperator; import java.util.function.BinaryOperator;
import java.util.function.Function; import java.util.function.Function;
@@ -20,10 +23,12 @@ import java.util.function.Function;
@Slf4j @Slf4j
public class StatisticsServiceImpl implements StatisticsService { public class StatisticsServiceImpl implements StatisticsService {
private final OrderService orderService; private final OrderService orderService;
private final ArticleService articleService;
@Autowired @Autowired
public StatisticsServiceImpl(OrderService orderService) { public StatisticsServiceImpl(OrderService orderService, ArticleService articleService) {
this.orderService = orderService; this.orderService = orderService;
this.articleService = articleService;
} }
//returns Map<unix milli timestamp, Map<Category, T>> //returns Map<unix milli timestamp, Map<Category, T>>
@@ -68,4 +73,21 @@ public class StatisticsServiceImpl implements StatisticsService {
}); });
return map; return map;
} }
@Override
public Map<String, Map<Integer, Integer>> getStockPercent() {
Map<String, Map<Integer, Integer>> 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;
}
} }

View File

@@ -9,50 +9,12 @@ import {Gauge, gaugeClasses} from "@mui/x-charts";
import {useAccount} from "../AccountProvider.tsx"; import {useAccount} from "../AccountProvider.tsx";
import {useQuery} from "@tanstack/react-query"; import {useQuery} from "@tanstack/react-query";
import {fetchItems} from "../query/Queries.tsx"; import {fetchItems} from "../query/Queries.tsx";
import { mapValueToColor } from "../../util/ColorUtil.tsx";
export default function ItemsInfo() { export default function ItemsInfo() {
const theme = useTheme(); const theme = useTheme();
const {t} = useTranslation(); 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) { function handleIconEdit(item: Item) {
//TODO: implement //TODO: implement
console.log("IconEdit", item); console.log("IconEdit", item);

View File

@@ -5,9 +5,9 @@ import { BarSeriesType } from '@mui/x-charts'
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query"; 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 { useAccount } from "../AccountProvider.tsx";
import { data } from "react-router-dom"; import { getColorFromPercent } from "../../util/ColorUtil.tsx";
export default function StatisticsInfo() { export default function StatisticsInfo() {
const theme = useTheme(); const theme = useTheme();
@@ -23,6 +23,9 @@ export default function StatisticsInfo() {
const [orderStatus, setOrderStatus] = useState([]); const [orderStatus, setOrderStatus] = useState([]);
const [stockPercent, setStockPercent] = useState([]);
const { user: loginData } = useAccount(); const { user: loginData } = useAccount();
@@ -47,6 +50,12 @@ export default function StatisticsInfo() {
retryDelay: 0, 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(() => { useEffect(() => {
@@ -123,7 +132,30 @@ export default function StatisticsInfo() {
} }
setOrderStatus(orderStatus) 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 ( return (
<Box className="" sx={{ color: theme.palette.text.primary }}> <Box className="" sx={{ color: theme.palette.text.primary }}>
@@ -175,6 +207,7 @@ export default function StatisticsInfo() {
series={[{ series={[{
data: totalRevenue, data: totalRevenue,
highlightScope: { fade: 'global', highlight: 'item' }, highlightScope: { fade: 'global', highlight: 'item' },
valueFormatter: (v) => (v ? `${v.value}` : '-'),
}]} }]}
width={200} width={200}
height={200} height={200}
@@ -186,10 +219,14 @@ export default function StatisticsInfo() {
</Typography> </Typography>
<PieChart <PieChart
series={[{ series={[{
data: totalRevenue, data: stockPercent,
innerRadius: 10,
outerRadius: 90,
cornerRadius: 3
}]} }]}
width={200} width={200}
height={200} height={200}
hideLegend
/> />
</Box> </Box>
<Box className="vw20" sx={{ m: 2 }}> <Box className="vw20" sx={{ m: 2 }}>

View File

@@ -211,7 +211,15 @@ export const orderPatch = async (order: OrderPatch) => {
export const fetchOrderStatus = async (loginData: User) => { export const fetchOrderStatus = async (loginData: User) => {
const response = await fetch("http://localhost:8085/statistics/orderstatus?email=" + loginData.email + "&session=" + loginData.session); const response = await fetch("http://localhost:8085/statistics/orderstatus?email=" + loginData.email + "&session=" + loginData.session);
if (!response.ok) { if (!response.ok) {
throw new Error("fetching satistics Revenue failed"); throw new Error("fetching order status failed");
} }
return response.json(); return response.json();
}; };
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();
};

View File

@@ -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}`;
}