resolved merge conflict

This commit is contained in:
Laura Dolibois
2025-06-15 20:29:57 +02:00
7 changed files with 265 additions and 161 deletions

View File

@@ -31,7 +31,7 @@ public class Order {
@Column(name = "status", nullable = false)
private OrderStatus status;
@OneToMany(mappedBy = "order", orphanRemoval = true, fetch = FetchType.EAGER, cascade = CascadeType.ALL, targetEntity = OrderItem.class)
@OneToMany(mappedBy = "order", orphanRemoval = true, fetch = FetchType.LAZY, targetEntity = OrderItem.class)
private List<OrderItem> orderItems;
public OrderModel toModel() {

View File

@@ -62,8 +62,8 @@ public class OrderServiceImpl implements OrderService {
ArrayList::addAll
)
);
log.info("Created Order from Model: {}", order);
orderItemRepository.saveAll(order.getOrderItems());
log.info("Saved OrderItems");
return order;
}
@@ -94,9 +94,10 @@ public class OrderServiceImpl implements OrderService {
long lower = fromMilli;
long upper;
while (months > 0) {
upper = fromMilli + TimeUtil.monthLength(fromMilli);
log.info("Getting Orders from {} to {}", lower, upper);
orders.put(fromMilli, orderRepository.getOrdersByTimeBetween(lower, upper));
upper = lower + TimeUtil.monthLength(lower);
List<Order> a = orderRepository.getOrdersByTimeBetween(lower, upper);
log.info("Getting Orders from {} to {}: size {}", lower, upper, a.size());
orders.put(lower, a);
lower = upper;
months--;
}

View File

@@ -1,7 +1,6 @@
package de.htwsaar.webshop.service.impl;
import de.htwsaar.webshop.model.MonthlyCatModel;
import de.htwsaar.webshop.repository.entities.Order;
import de.htwsaar.webshop.repository.entities.OrderItem;
import de.htwsaar.webshop.service.OrderService;
import de.htwsaar.webshop.service.StatisticsService;
@@ -10,11 +9,10 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@Slf4j
@@ -26,34 +24,33 @@ public class StatisticsServiceImpl implements StatisticsService {
this.orderService = orderService;
}
//don't ask pls
//returns Map<unix milli timestamp, Map<Category, T>>
private <T extends Number> MonthlyCatModel<T> getMonthCategoryMap(Function<OrderItem, T> mappingFunction,
BinaryOperator<T> reduceFunction) {
return new MonthlyCatModel<>(
orderService.getTimeSortedOrders(TimeUtil.nowMonthsAgo(12), 12).entrySet().stream()
.map(entry -> Map.entry(entry.getKey(),
entry.getValue().stream()
.map(Order::getOrderItems)
.reduce(new ArrayList<>(), (l, r) -> {
l.addAll(r);
return l;
BinaryOperator<T> reduceFunction,
T defaultValue) {
Map<Long, Map<String, T>> map = new HashMap<>();
orderService.getTimeSortedOrders(TimeUtil.nowMonthsAgo(12), 12).forEach( (k,v) -> {
log.info("Month {} has {}", k, v.size());
map.putIfAbsent(k, new HashMap<>());
v.forEach( (order) ->
order.getOrderItems().forEach((item) -> {
//log.info(" OrderItem {} has cat {}", item, item.getArticle().getCategory());
map.get(k).putIfAbsent(item.getArticle().getCategory(), defaultValue);
map.get(k).computeIfPresent(item.getArticle().getCategory(), (cat,left ) -> reduceFunction.apply(left, mappingFunction.apply(item)));
})
.stream()
.map(orderItem -> Map.entry(orderItem.getArticle().getCategory(), mappingFunction.apply(orderItem)))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, reduceFunction))
))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
);
log.info("Month has {} cats", map.get(k).size());
});
return new MonthlyCatModel<>(map);
}
@Override
public MonthlyCatModel<Integer> getSalesVolume() {
return getMonthCategoryMap(OrderItem::getAmount, Integer::sum);
return getMonthCategoryMap(OrderItem::getAmount, Integer::sum, 0);
}
@Override
public MonthlyCatModel<Integer> getSalesRevenue() {
return getMonthCategoryMap(item -> item.getArticle().getPrice100(), Integer::sum);
return getMonthCategoryMap(item -> item.getArticle().getPrice100(), Integer::sum, 0);
}
}

View File

@@ -108,5 +108,8 @@
"deleteProduct": "Produkt löschen",
"description": "Beschreibung",
"images": "Bilder",
"loggedInAs": "Angemeldet als"
"loggedInAs": "Angemeldet als",
"confirmDeleteAccount": "Bist du sicher, dass du dein Konto löschen möchtest? Dies kann nicht rückgängig gemacht werden.",
"enterPasswordToConfirmDeletion": "Bitte gib dein Passwort ein, um die Löschung zu bestätigen.",
"deleteAccountFailed": "Konto konnte nicht gelöscht werden. Bitte versuche es später erneut."
}

View File

@@ -108,5 +108,8 @@
"deleteProduct": "Delete Product",
"description": "Description",
"images": "Images",
"loggedInAs": "Logged in as"
"loggedInAs": "Logged in as",
"confirmDeleteAccount": "Are you sure you want to delete your account? This action cannot be undone.",
"enterPasswordToConfirmDeletion": "Please enter your password to confirm the deletion of your account.",
"deleteAccountFailed": "Failed to delete account. Please try again later."
}

View File

@@ -132,8 +132,8 @@ export const fetchCustomer = async (userId: number) => {
return data;
};
export const deleteAccount = async (userId: number) => {
const response = await fetch('http://localhost:8085/account?id=' + userId, {
export const deleteAccount = async (user: User) => {
const response = await fetch('http://localhost:8085/account?email=' + user.email + '&password=' + user.password, {
method: 'DELETE',
});
if (!response.ok) {
@@ -166,3 +166,17 @@ export const fetchItems = async (loginData: User) => {
}
return response.json();
};
export const editAccount = async (customer: CustomerType) => {
const response = await fetch('http://localhost:8085/customer?id=' + customer.id, {
method: 'PUT',
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(customer),
});
if (!response.ok) {
throw new Error('Fehler beim Löschen des Accounts');
}
return await response.json();
};

View File

@@ -1,35 +1,52 @@
import { Box, Button, Divider, Paper, Stack, TextField, Typography } from "@mui/material";
import {
Box,
Button,
Divider,
Paper,
Stack,
TextField,
Typography,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { CustomerType } from "../components/Account";
import { CustomerType, User } from "../components/Account";
import { useAccount } from "../helper/AccountProvider";
import { deleteAccount, fetchCustomer } from "../helper/query/Queries";
import { deleteAccount, editAccount, fetchCustomer } from "../helper/query/Queries";
import "./pages.css";
export default function Account() {
const { t } = useTranslation();
const navigate = useNavigate();
const { user: userData, logout } = useAccount();
// Beispielhafte Userdaten (könnten aus Context/Backend kommen)
const [user, setUser] = useState<CustomerType>({
name: "",
surname: "",
address: "",
country: "",
zip: "",
id: userData?.customerId || 0, // Initialwert
id: userData?.customerId || 0,
});
const [userDataState, setUserDataState] = useState<User>(userData || {
password: "",
email: "",
customerId: 0,
session: "",
isAdmin: false,
});
// Aktualisiere den `user`-State, wenn sich `userData` ändert
useEffect(() => {
if (userData?.customerId) {
setUser((prev) => ({
...prev,
id: userData.customerId, // Aktualisiere die ID
id: userData.customerId,
}));
}
}, [userData]);
@@ -37,23 +54,33 @@ export default function Account() {
const [edit, setEdit] = useState(false);
const [form, setForm] = useState(user);
// Neu: Passwort-Dialog-Status und Passwort-Input
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
const [passwordInput, setPasswordInput] = useState("");
const { data } = useQuery<CustomerType>({
queryKey: ['fetchCustomer', userData?.customerId],
queryFn: () => fetchCustomer(userData?.customerId || 0), // Funktion zum Abrufen der Kundendaten
retry: 3, // Versucht es 3-mal erneut
retryDelay: 1000, // Wartezeit zwischen den Versuchen (in ms)
queryKey: ["fetchCustomer", userData?.customerId],
queryFn: () => fetchCustomer(userData?.customerId || 0),
retry: 1,
retryDelay: 1000,
});
const { refetch: deleteRefetch } = useQuery({
queryKey: ['deleteAccount', userData?.customerId],
queryFn: () => deleteAccount(userData?.customerId || 0), // Funktion zum Löschen des Accounts
enabled: false, // Diese Abfrage wird nicht automatisch ausgeführt
queryKey: ["deleteAccount", userDataState],
queryFn: () => deleteAccount(userDataState!),
enabled: false,
});
const { refetch: editRefetch } = useQuery({
queryKey: ["editAccount", form],
queryFn: () => editAccount(form),
enabled: false,
});
useEffect(() => {
if (data) {
setUser(data); // Aktualisiere den user-State mit den abgerufenen Daten
setForm(data); // Optional: Aktualisiere auch den form-State
setUser(data);
setForm(data);
}
}, [data]);
@@ -65,26 +92,60 @@ export default function Account() {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
const handleSave = () => {
const handleSave = async () => {
setUser(form);
setEdit(false);
await editRefetch();
};
const handleDelete = () => {
deleteRefetch();
// Neu: Passwort-Dialog öffnen
const handleDeleteClick = () => {
setPasswordInput("");
setPasswordDialogOpen(true);
};
// Neu: Passwort-Dialog schließen
const handlePasswordDialogClose = () => {
setPasswordDialogOpen(false);
};
// Neu: Passwort-Eingabe bestätigen
const handlePasswordConfirm = async () => {
if (!passwordInput) {
alert(t("pleaseEnterPassword"));
return;
}
// Passwort in Form aktualisieren (hier z.B. als field "password", anpassen falls anders)
setUserDataState({ ...userDataState, password: passwordInput });
// Erst User-Daten mit Passwort aktualisieren
try {
await editRefetch(); // Achtung: editRefetch verwendet immer noch alten form, daher call direkt mit updatedForm:
// Danach Account löschen
await deleteRefetch();
logout();
navigate("/");
logout(); // Logout nach dem Löschen
} catch (error) {
console.error("Fehler beim Löschen des Accounts:", error);
alert(t("deleteAccountFailed"));
} finally {
setPasswordDialogOpen(false);
}
};
return (
<Box className="page-background page-background-center" sx={{ minHeight: "100vh", justifyContent: "flex-start", pt: 4 }}>
<Box
className="page-background page-background-center"
sx={{ minHeight: "100vh", justifyContent: "flex-start", pt: 4 }}
>
<Paper elevation={3} sx={{ p: 4, maxWidth: 500, width: "100%", mx: "auto" }}>
<Typography variant="h4" gutterBottom>
{t('myAccount')}
{t("myAccount")}
</Typography>
<Divider sx={{ mb: 3 }} />
<Stack spacing={2}>
<TextField
label={t('name')}
label={t("name")}
name="name"
value={edit ? form.name : user.name}
onChange={handleChange}
@@ -92,7 +153,7 @@ export default function Account() {
fullWidth
/>
<TextField
label={t('surname')}
label={t("surname")}
name="surname"
value={edit ? form.surname : user.surname}
onChange={handleChange}
@@ -100,7 +161,7 @@ export default function Account() {
fullWidth
/>
<TextField
label={t('address')}
label={t("address")}
name="address"
value={edit ? form.address : user.address}
onChange={handleChange}
@@ -108,7 +169,7 @@ export default function Account() {
fullWidth
/>
<TextField
label={t('country')}
label={t("country")}
name="country"
value={edit ? form.country : user.country}
onChange={handleChange}
@@ -116,7 +177,7 @@ export default function Account() {
fullWidth
/>
<TextField
label={t('zip')}
label={t("zip")}
name="zip"
value={edit ? form.zip : user.zip}
onChange={handleChange}
@@ -128,27 +189,52 @@ export default function Account() {
{edit ? (
<>
<Button variant="contained" color="primary" onClick={handleSave}>
{t('save')}
{t("save")}
</Button>
<Button variant="outlined" color="secondary" onClick={handleCancel}>
{t('cancel')}
{t("cancel")}
</Button>
</>
) : (
<Button variant="contained" color="primary" onClick={handleEdit}>
{t('edit')}
{t("edit")}
</Button>
)}
<Button
variant="outlined"
color="error"
onClick={handleDelete}
onClick={handleDeleteClick} // Neu: Passwort-Dialog öffnen
sx={{ marginLeft: "auto" }}
>
{t('deleteAccount')}
{t("deleteAccount")}
</Button>
</Box>
</Paper>
{/* Passwort-Dialog */}
<Dialog open={passwordDialogOpen} onClose={handlePasswordDialogClose}>
<DialogTitle>{t("confirmDeleteAccount")}</DialogTitle>
<DialogContent>
<Typography>{t("enterPasswordToConfirmDeletion")}</Typography>
<TextField
autoFocus
margin="dense"
label={t("password")}
type="password"
fullWidth
variant="standard"
value={passwordInput}
onChange={(e) => setPasswordInput(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handlePasswordDialogClose}>{t("cancel")}</Button>
<Button color="error" onClick={handlePasswordConfirm}>
{t("delete")}
</Button>
</DialogActions>
</Dialog>
</Box>
);
}