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 ad3a9fa..f4f7843 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 @@ -41,6 +41,7 @@ public class ControllerPathConfig { //OrderController public static final String ORDER_BASE = "/order"; public static final String ORDER_GET_ALL = ORDER_BASE + "/all"; + public static final String ORDER_GET_ALL_ADMIN = ORDER_BASE + "/all/all"; //ReviewController public static final String REVIEW_BASE = "/review"; diff --git a/00-backend/src/main/java/de/htwsaar/webshop/controller/OrderController.java b/00-backend/src/main/java/de/htwsaar/webshop/controller/OrderController.java index 4015458..a9cd3b3 100644 --- a/00-backend/src/main/java/de/htwsaar/webshop/controller/OrderController.java +++ b/00-backend/src/main/java/de/htwsaar/webshop/controller/OrderController.java @@ -16,8 +16,7 @@ import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.UUID; -import static de.htwsaar.webshop.config.ControllerPathConfig.ORDER_BASE; -import static de.htwsaar.webshop.config.ControllerPathConfig.ORDER_GET_ALL; +import static de.htwsaar.webshop.config.ControllerPathConfig.*; import static de.htwsaar.webshop.config.ParameterConfig.*; import static de.htwsaar.webshop.util.LoggerUtil.logRequest; @@ -47,7 +46,7 @@ public class OrderController { return ResponseEntity.ok(orders.stream().map(Order::toModel).toList()); } - @RequestMapping(path = ORDER_GET_ALL, method = RequestMethod.TRACE, produces = "application/json") + @RequestMapping(path = ORDER_GET_ALL_ADMIN, method = RequestMethod.GET, produces = "application/json") public ResponseEntity> getAll(HttpServletRequest request, @RequestParam(value = PARAM_EMAIL) String email, @RequestParam(value = PARAM_SESSION) UUID token) { @@ -92,10 +91,16 @@ public class OrderController { @RequestParam(value = PARAM_STATUS) OrderStatus status) { logRequest(request); if (orderId == null) { + log.info("[{}] failed to update, empty orderID", request.getRequestURI()); + return ResponseEntity.badRequest().build(); + } + if (status == null) { + log.info("[{}] failed to update, empty status, orderID {}", request.getRequestURI(), orderId); return ResponseEntity.badRequest().build(); } Order order = orderService.getOrderById(orderId); if (order == null) { + log.info("[{}] failed to update orderID {}", request.getRequestURI(), orderId); return ResponseEntity.notFound().build(); } order.setStatus(status); diff --git a/01-frontend/package-lock.json b/01-frontend/package-lock.json index 3bf7f22..5c92418 100644 --- a/01-frontend/package-lock.json +++ b/01-frontend/package-lock.json @@ -26,6 +26,8 @@ "react": "^19.0.0", "react-chartjs-2": "^5.3.0", "react-cookie": "^8.0.1", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^19.1.0", "react-i18next": "^15.5.1", "react-router-dom": "^7.5.3" @@ -1693,6 +1695,24 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==", + "license": "MIT" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==", + "license": "MIT" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==", + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.11", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz", @@ -2933,6 +2953,17 @@ "robust-predicates": "^3.0.2" } }, + "node_modules/dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "license": "MIT", + "dependencies": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -3204,7 +3235,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -4110,6 +4140,45 @@ "react": ">= 16.3.0" } }, + "node_modules/react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "license": "MIT", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "license": "MIT", + "dependencies": { + "dnd-core": "^16.0.1" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", @@ -4218,6 +4287,15 @@ "react-dom": ">=16.6.0" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", diff --git a/01-frontend/package.json b/01-frontend/package.json index e93c645..d2f9dde 100644 --- a/01-frontend/package.json +++ b/01-frontend/package.json @@ -30,6 +30,8 @@ "react": "^19.0.0", "react-chartjs-2": "^5.3.0", "react-cookie": "^8.0.1", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^19.1.0", "react-i18next": "^15.5.1", "react-router-dom": "^7.5.3" diff --git a/01-frontend/public/locales/de/translation.json b/01-frontend/public/locales/de/translation.json index 60157d4..1a1b564 100644 --- a/01-frontend/public/locales/de/translation.json +++ b/01-frontend/public/locales/de/translation.json @@ -135,5 +135,10 @@ "itemVolumeDistribution": "Verteilung der Artikelmenge", "itemRevenueDistribution": "Verteilung des Artikelumsatzes", "stockFulfillment": "Bestandserfüllung", - "orderStatus": "Bestellstatus" + "orderStatus": "Bestellstatus", + "ORDERED": "Bestellt", + "IN_PROGRESS": "In Versand", + "ISSUES": "Probleme", + "DELIVERED": "Zugesendet", + "CANCELLED": "Storniert" } \ No newline at end of file diff --git a/01-frontend/public/locales/en/translation.json b/01-frontend/public/locales/en/translation.json index a9505f0..127eee2 100644 --- a/01-frontend/public/locales/en/translation.json +++ b/01-frontend/public/locales/en/translation.json @@ -135,5 +135,10 @@ "itemVolumeDistribution": "Item Volume Distribution", "itemRevenueDistribution": "Item Revenue Distribution", "stockFulfillment": "Stock fulfillment", - "orderStatus": "Order Status" + "orderStatus": "Order Status", + "ORDERED": "Ordered", + "IN_PROGRESS": "In Progress", + "ISSUES": "Issues", + "DELIVERED": "Delivered", + "CANCELLED": "Cancelled" } \ No newline at end of file diff --git a/01-frontend/src/components/Order.tsx b/01-frontend/src/components/Order.tsx index b7e78cf..6bedf13 100644 --- a/01-frontend/src/components/Order.tsx +++ b/01-frontend/src/components/Order.tsx @@ -1,9 +1,9 @@ export enum OrderStatusEnum { - CANCELLED = 'CANCELLED', - ISSUES = 'ISSUES', - DELIVERED = 'DELIVERED', ORDERED = 'ORDERED', IN_PROGRESS = 'IN_PROGRESS', + ISSUES = 'ISSUES', + DELIVERED = 'DELIVERED', + CANCELLED = 'CANCELLED', }; type OrderType = { diff --git a/01-frontend/src/helper/adminpanel/DroppableContainer.tsx b/01-frontend/src/helper/adminpanel/DroppableContainer.tsx deleted file mode 100644 index 6600335..0000000 --- a/01-frontend/src/helper/adminpanel/DroppableContainer.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useDroppable } from "@dnd-kit/core"; -import { Box, useTheme } from "@mui/material"; -import { ReactNode } from "react"; -import { OrderStatusEnum } from "../../components/Order"; - -type DroppableContainerProps = { - id: OrderStatusEnum; - children: ReactNode; -}; - -export function DroppableContainer({ id, children }: DroppableContainerProps) { - const { setNodeRef } = useDroppable({ id }); - const theme = useTheme(); - - return ( - - {children} - - ); -} diff --git a/01-frontend/src/helper/adminpanel/OrdersInfo.tsx b/01-frontend/src/helper/adminpanel/OrdersInfo.tsx index 43cd7c3..7ea79da 100644 --- a/01-frontend/src/helper/adminpanel/OrdersInfo.tsx +++ b/01-frontend/src/helper/adminpanel/OrdersInfo.tsx @@ -1,65 +1,27 @@ -import { closestCenter, DndContext } from "@dnd-kit/core"; -import { restrictToParentElement } from "@dnd-kit/modifiers"; import { Box, Button, Dialog, DialogContent, DialogTitle, + DialogActions, + List, ListItemText, Typography, - useTheme + useTheme, + Card, CardContent, + Stack, + Divider, + Snackbar } from "@mui/material"; -import { useState } from "react"; +import React, { useState, useEffect, PropsWithChildren } from "react"; import { useTranslation } from "react-i18next"; -import { DroppableContainer } from "./DroppableContainer"; -import SortableItem from "./SortableItem"; import OrderType, { OrderStatusEnum } from "../../components/Order"; - -const mockOrders: OrderType[] = [ - { - id: "1001", - date: "2025-05-20", - status: OrderStatusEnum.CANCELLED, - items: [ - { name: "Tomatensamen", quantity: 2, price: 3.99 }, - { name: "Blumenerde", quantity: 1, price: 7.49 } - ], - total: 15.47, - address: "Musterstraße 1, 12345 Musterstadt" - }, - { - id: "1000", - date: "2025-05-10", - status: OrderStatusEnum.ISSUES, - items: [{ name: "Gießkanne", quantity: 1, price: 12.99 }], - total: 12.99, - address: "Musterstraße 1, 12345 Musterstadt" - }, - { - id: "1002", - date: "2025-05-15", - status: OrderStatusEnum.DELIVERED, - items: [{ name: "Pflanzendünger", quantity: 1, price: 8.99 }], - total: 8.99, - address: "Musterstraße 1, 12345 Musterstadt" - }, - { - id: "1003", - date: "2025-05-18", - status: OrderStatusEnum.ORDERED, - items: [{ name: "Blumentopf", quantity: 2, price: 5.99 }], - total: 11.98, - address: "Musterstraße 1, 12345 Musterstadt" - }, - { - id: "1004", - date: "2025-05-18", - status: OrderStatusEnum.IN_PROGRESS, - items: [{ name: "TimWall", quantity: 2, price: 5.99 }], - total: 12.99, - address: "Musterstraße 1, 12345 Musterstadt" - } -]; +import { useDrag, useDrop, DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { useQuery } from "@tanstack/react-query"; +import { fetchOrdersAdmin, orderPatch } from "../query/Queries"; +import { useAccount } from "../AccountProvider"; +import { useQueryClient } from "@tanstack/react-query"; // The order in which the statuses are displayed const statusOrder: OrderStatusEnum[] = [ @@ -70,130 +32,175 @@ const statusOrder: OrderStatusEnum[] = [ OrderStatusEnum.DELIVERED ]; -// Main component for managing orders -export default function OrdersInfo() { + +const OrderCard: React.FC<{ order: OrderType; onClick: () => void }> = ({ order, onClick }) => { + const { t } = useTranslation(); const theme = useTheme(); - const [orders, setOrders] = useState(mockOrders); - const [selectedOrder, setSelectedOrder] = useState(null); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const handleDragEnd = (event: any) => { - const { active, over } = event; - if (!over || active.id === over.id) return; - - const newStatus = over.id; - setOrders((prev) => - prev.map((order) => - order.id === active.id ? { ...order, status: newStatus } : order - ) - ); - }; - - const handleNextStatus = (order: OrderType) => { - const currentIndex = statusOrder.indexOf(order.status); - if (currentIndex < statusOrder.length - 1) { - setOrders((prev) => - prev.map((o) => - o.id === order.id - ? { ...o, status: statusOrder[currentIndex + 1] as OrderType["status"] } - : o - ) - ); - } - }; - - const renderOrders = (status: OrderStatusEnum) => { - const filtered = orders.filter((o) => o.status === status); - - return ( - - - {t(status.toString())} - - {filtered.map((order) => ( - setSelectedOrder(order)} - /> - ))} - - ); - }; + + const [{ isDragging }, drag] = useDrag(() => ({ + type: 'order', + item: { id: order.id }, + collect: (monitor) => ({ + isDragging: !!monitor.isDragging(), + }), + })); return ( - - - {t("Orders Management")} {/* Bestellverwaltung */} - - - - - {statusOrder.map((status) => ( - - {renderOrders(status)} - - ))} - - - - setSelectedOrder(null)}> - {t("orderDetails")} {/* Bestelldetails */} - - {selectedOrder && ( - - - {t("orderId")}: {selectedOrder.id} - - - {t("date")}: {selectedOrder.date} - - - {t("address")}: {selectedOrder.address} - - - {t("items")}: - - {selectedOrder.items.map((item, idx) => ( - - {item.quantity}x {item.name} – {item.price.toFixed(2)} € - - ))} - - {t("total")}: {selectedOrder.total.toFixed(2)} € - - - - )} - - - - +
+ + + + Order: {order.id} + + + {t('date') + ": " + new Date(order.time).toUTCString()} + + + {t('total') + ": " + order.total} + + + +
); +}; + +const Column: React.FC void }>> = ({ status, onDrop, children }) => { + + const { t } = useTranslation(); + const theme = useTheme(); + + const [{ isOver }, drop] = useDrop(() => ({ + accept: 'order', + drop: (item: { id: number }) => onDrop(item.id, status), + collect: (monitor) => ({ + isOver: !!monitor.isOver(), + }), + })); + + return ( +
+ + + {t(status)} + {children} + + +
+ ); +}; + +const EditOrder: React.FC<{ open: boolean; order: OrderType | null; onClose: () => void}> = ({ open, order, onClose }) => { + + const { t } = useTranslation(); + const theme = useTheme(); + if(order === null) + return ""; + return ( + + + {`${t(order.status)} #${order?.id}`} + + + {order && ( + + {`${t('orderDate')}: ${new Date(order.time).toDateString()}`} + + {t('orderedItems')}: + + {order.orderItems.map((item, idx) => ( + + ))} + + + {`${t('sum')}: ${(order.total/100).toFixed(2)} €`} + + )} + + + + + + ); +}; + +// Main component for managing orders +export default function OrdersInfo() { + const [orders, setOrders] = useState([]); + const [editOrder, setEditOrder] = useState(null); + const [openSnackbar, setOpenSnackbar] = useState(false); + + const {user: loginData} = useAccount(); + const queryClient = useQueryClient() + + const { data, refetch } = useQuery({ + queryKey: ["fetchOrdersAdmin", loginData], + queryFn: () => fetchOrdersAdmin(loginData? loginData : {email: "", password: "", session: "", customerId: -1, isAdmin: false}), + retry: 3, + retryDelay: 1000, + }); + + useEffect(() => { + console.log("data", data); + if (data) { + setOrders(data); + } + }, [data]); + + + const handleDrop = async (id: number, status: OrderStatusEnum) => { + if(orders.length < 1) { + const resp = await refetch(); + setOrders(resp.data); // i dont know + } + const obj = orders.find((o) => { + return o.id === id + }) + if(!obj) { + setOpenSnackbar(true); + return; + } + obj.status = status + const resp = await queryClient.fetchQuery({ + queryKey: ["orderPatch", {id: obj.id, status: obj.status}], + queryFn: () => orderPatch({id: obj.id, status: obj.status}), + retry: 0, + retryDelay: 1000, + }) + refetch(); + setOrders(orders.map((o) => (o.id === id ? obj : o ))); + }; + + const handleEdit = (order: OrderType) => setEditOrder(order); + + return ( + +
+ {statusOrder.map((status) => ( + + {orders + .filter((o) => o.status === status) + .map((o) => ( + handleEdit(o)} /> + ))} + + ))} +
+ setEditOrder(null)} + /> + setOpenSnackbar(false)} + message="Failed changing Orderstatus" + /> +
+ ); + } diff --git a/01-frontend/src/helper/adminpanel/SortableItem.tsx b/01-frontend/src/helper/adminpanel/SortableItem.tsx deleted file mode 100644 index 46cb292..0000000 --- a/01-frontend/src/helper/adminpanel/SortableItem.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; -import { Paper, Typography, useTheme } from "@mui/material"; -import { useTranslation } from "react-i18next"; - -type SortableItemProps = { - id: string; - order: { - id: string; - total: number; - }; - onClick: () => void; -}; - -export default function SortableItem({ id, order, onClick }: SortableItemProps) { - - const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); - const theme = useTheme(); - const { t } = useTranslation(); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - cursor: "grab", - }; - - return ( - - - {t("orderId")}: {order.id} - - - {t("total")}: {order.total.toFixed(2)} € - - - ); -} diff --git a/01-frontend/src/helper/adminpanel/StatisticsInfo.tsx b/01-frontend/src/helper/adminpanel/StatisticsInfo.tsx index 653e506..9576fb7 100644 --- a/01-frontend/src/helper/adminpanel/StatisticsInfo.tsx +++ b/01-frontend/src/helper/adminpanel/StatisticsInfo.tsx @@ -136,7 +136,6 @@ export default function StatisticsInfo() { useEffect(() => { if(dataStockPercent) { - console.log(dataStockPercent); const stockPercent = [] let i = 0 for(let x = 0; x < 10; x++) { diff --git a/01-frontend/src/helper/query/Queries.tsx b/01-frontend/src/helper/query/Queries.tsx index 597653d..3059732 100644 --- a/01-frontend/src/helper/query/Queries.tsx +++ b/01-frontend/src/helper/query/Queries.tsx @@ -232,3 +232,12 @@ export const fetchStockPercent = async (loginData: User) => { } return response.json(); }; + + +export const fetchOrdersAdmin = async (loginData: User) => { + const response = await fetch("http://localhost:8085/order/all/all?email=" + loginData.email + "&session=" + loginData.session); + if (!response.ok) { + throw new Error("fetching admin orders failed"); + } + return response.json(); +}; diff --git a/01-frontend/src/pages/Orders.tsx b/01-frontend/src/pages/Orders.tsx index b9a8d55..faf09d4 100644 --- a/01-frontend/src/pages/Orders.tsx +++ b/01-frontend/src/pages/Orders.tsx @@ -83,7 +83,7 @@ export default function Orders() { setSelectedOrder(order)}> ))}