Stock Statistic
This commit is contained in:
@@ -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";
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
};
|
||||||
|
|||||||
46
01-frontend/src/util/ColorUtil.tsx
Normal file
46
01-frontend/src/util/ColorUtil.tsx
Normal 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}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user