Merge remote-tracking branch 'origin/main'
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_REVENUE = STATISTICS_BASE + "/revenue";
|
||||
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.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<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 java.util.Map;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface StatisticsService {
|
||||
CatMonthModel<Integer> getSalesVolume();
|
||||
@@ -12,4 +14,6 @@ public interface StatisticsService {
|
||||
|
||||
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.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<unix milli timestamp, Map<Category, T>>
|
||||
@@ -68,4 +73,21 @@ public class StatisticsServiceImpl implements StatisticsService {
|
||||
});
|
||||
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 {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);
|
||||
|
||||
@@ -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 (
|
||||
<Box className="" sx={{ color: theme.palette.text.primary }}>
|
||||
@@ -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() {
|
||||
</Typography>
|
||||
<PieChart
|
||||
series={[{
|
||||
data: totalRevenue,
|
||||
data: stockPercent,
|
||||
innerRadius: 10,
|
||||
outerRadius: 90,
|
||||
cornerRadius: 3
|
||||
}]}
|
||||
width={200}
|
||||
height={200}
|
||||
hideLegend
|
||||
/>
|
||||
</Box>
|
||||
<Box className="vw20" sx={{ m: 2 }}>
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
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