Compare commits
10 Commits
bf4b95dce9
...
8fb20f70c7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fb20f70c7 | ||
|
|
cd2aca8826 | ||
|
|
06720d400f | ||
|
|
fd9f805c44 | ||
|
|
83b207f4cb | ||
|
|
7c3ab76c79 | ||
|
|
483cb4b043 | ||
|
|
e3004bbc72 | ||
|
|
8b51809085 | ||
|
|
d3becdef72 |
Binary file not shown.
@@ -25,8 +25,8 @@ public class ControllerPathConfig {
|
|||||||
|
|
||||||
//ArticleController
|
//ArticleController
|
||||||
public static final String ARTICLE_BASE = "/article";
|
public static final String ARTICLE_BASE = "/article";
|
||||||
public static final String ARTICLE_GET_ALL = ARTICLE_BASE + "/all";
|
public static final String ARTICLE_GET_ALL = "/all";
|
||||||
public static final String ARTICLE_GET_ALL_WITH_IMAGE = ARTICLE_BASE + "/all/image";
|
public static final String ARTICLE_GET_ALL_WITH_IMAGE = "/all/image";
|
||||||
|
|
||||||
//CustomerController
|
//CustomerController
|
||||||
public static final String CUSTOMER_BASE = "/customer";
|
public static final String CUSTOMER_BASE = "/customer";
|
||||||
@@ -43,12 +43,12 @@ public class ControllerPathConfig {
|
|||||||
|
|
||||||
//OrderController
|
//OrderController
|
||||||
public static final String ORDER_BASE = "/order";
|
public static final String ORDER_BASE = "/order";
|
||||||
public static final String ORDER_GET_ALL = ORDER_BASE + "/all";
|
public static final String ORDER_GET_ALL = "/all";
|
||||||
public static final String ORDER_GET_ALL_ADMIN = ORDER_BASE + "/all/all";
|
public static final String ORDER_GET_ALL_ADMIN = "/all/all";
|
||||||
|
|
||||||
//ReviewController
|
//ReviewController
|
||||||
public static final String REVIEW_BASE = "/review";
|
public static final String REVIEW_BASE = "/review";
|
||||||
public static final String REVIEW_GET_ALL = REVIEW_BASE + "/all";
|
public static final String REVIEW_GET_ALL = "/all";
|
||||||
|
|
||||||
//StatisticsController
|
//StatisticsController
|
||||||
private static final String STATISTICS_BASE = "/statistics";
|
private static final String STATISTICS_BASE = "/statistics";
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import static de.htwsaar.webshop.util.LoggerUtil.logRequest;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@RequestMapping(path = ARTICLE_BASE, produces = "application/json")
|
||||||
public class ArticleController {
|
public class ArticleController {
|
||||||
private final ArticleService articleService;
|
private final ArticleService articleService;
|
||||||
|
|
||||||
@@ -27,26 +28,26 @@ public class ArticleController {
|
|||||||
this.articleService = articleService;
|
this.articleService = articleService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = ARTICLE_GET_ALL, method = RequestMethod.GET, produces = "application/json")
|
@GetMapping(path = ARTICLE_GET_ALL)
|
||||||
public ResponseEntity<List<ArticleModel>> getAll(HttpServletRequest request) {
|
public ResponseEntity<List<ArticleModel>> getAll(HttpServletRequest request) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
return ResponseEntity.ok(articleService.from(articleService.findAll()));
|
return ResponseEntity.ok(articleService.from(articleService.findAll()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = ARTICLE_GET_ALL_WITH_IMAGE, method = RequestMethod.GET, produces = "application/json")
|
@GetMapping(path = ARTICLE_GET_ALL_WITH_IMAGE)
|
||||||
public ResponseEntity<List<ArticleWithImageModel>> getAllWithImageData(HttpServletRequest request) {
|
public ResponseEntity<List<ArticleWithImageModel>> getAllWithImageData(HttpServletRequest request) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
return ResponseEntity.ok(articleService.fromWithImage(articleService.findAll()));
|
return ResponseEntity.ok(articleService.fromWithImage(articleService.findAll()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = ARTICLE_BASE, method = RequestMethod.GET, produces = "application/json")
|
@GetMapping
|
||||||
public ResponseEntity<ArticleModel> getByUUID(HttpServletRequest request,
|
public ResponseEntity<ArticleModel> getByUUID(HttpServletRequest request,
|
||||||
@RequestParam(value = PARAM_UUID) UUID uuid) {
|
@RequestParam(value = PARAM_UUID) UUID uuid) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
return ResponseEntity.ok(articleService.from(articleService.findByUUID(uuid)));
|
return ResponseEntity.ok(articleService.from(articleService.findByUUID(uuid)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = ARTICLE_BASE, method = RequestMethod.POST, produces = "application/json")
|
@PostMapping
|
||||||
public ResponseEntity<Boolean> add(HttpServletRequest request,
|
public ResponseEntity<Boolean> add(HttpServletRequest request,
|
||||||
@RequestBody Article article) {
|
@RequestBody Article article) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
@@ -60,7 +61,7 @@ public class ArticleController {
|
|||||||
return ResponseEntity.ok(a != null);
|
return ResponseEntity.ok(a != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = ARTICLE_BASE, method = RequestMethod.PUT, produces = "application/json")
|
@PutMapping
|
||||||
public ResponseEntity<Boolean> update(HttpServletRequest request,
|
public ResponseEntity<Boolean> update(HttpServletRequest request,
|
||||||
@RequestParam(value = PARAM_UUID) UUID uuid,
|
@RequestParam(value = PARAM_UUID) UUID uuid,
|
||||||
@RequestBody Article article) {
|
@RequestBody Article article) {
|
||||||
@@ -80,7 +81,7 @@ public class ArticleController {
|
|||||||
return ResponseEntity.ok(true);
|
return ResponseEntity.ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = ARTICLE_BASE, method = RequestMethod.DELETE, produces = "application/json")
|
@DeleteMapping
|
||||||
public ResponseEntity<Boolean> delete(HttpServletRequest request,
|
public ResponseEntity<Boolean> delete(HttpServletRequest request,
|
||||||
@RequestParam(value = PARAM_UUID) UUID uuid) {
|
@RequestParam(value = PARAM_UUID) UUID uuid) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import static de.htwsaar.webshop.util.LoggerUtil.logRequest;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@RequestMapping(path = ORDER_BASE, produces = "application/json")
|
||||||
public class OrderController {
|
public class OrderController {
|
||||||
|
|
||||||
private final OrderService orderService;
|
private final OrderService orderService;
|
||||||
@@ -35,7 +36,7 @@ public class OrderController {
|
|||||||
this.sessionService = sessionService;
|
this.sessionService = sessionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = ORDER_GET_ALL, method = RequestMethod.GET, produces = "application/json")
|
@GetMapping(path = ORDER_GET_ALL)
|
||||||
public ResponseEntity<List<OrderModel>> getAll(HttpServletRequest request,
|
public ResponseEntity<List<OrderModel>> getAll(HttpServletRequest request,
|
||||||
@RequestParam(value = PARAM_CUSTOMER_ID) Long customerId) {
|
@RequestParam(value = PARAM_CUSTOMER_ID) Long customerId) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
@@ -46,7 +47,7 @@ public class OrderController {
|
|||||||
return ResponseEntity.ok(orders.stream().map(Order::toModel).toList());
|
return ResponseEntity.ok(orders.stream().map(Order::toModel).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = ORDER_GET_ALL_ADMIN, method = RequestMethod.GET, produces = "application/json")
|
@GetMapping(path = ORDER_GET_ALL_ADMIN)
|
||||||
public ResponseEntity<List<OrderModel>> getAll(HttpServletRequest request,
|
public ResponseEntity<List<OrderModel>> getAll(HttpServletRequest request,
|
||||||
@RequestParam(value = PARAM_EMAIL) String email,
|
@RequestParam(value = PARAM_EMAIL) String email,
|
||||||
@RequestParam(value = PARAM_SESSION) UUID token) {
|
@RequestParam(value = PARAM_SESSION) UUID token) {
|
||||||
@@ -59,7 +60,7 @@ public class OrderController {
|
|||||||
return ResponseEntity.ok(orderService.findAll().stream().map(Order::toModel).toList());
|
return ResponseEntity.ok(orderService.findAll().stream().map(Order::toModel).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = ORDER_BASE, method = RequestMethod.GET, produces = "application/json")
|
@GetMapping
|
||||||
public ResponseEntity<OrderModel> get(HttpServletRequest request,
|
public ResponseEntity<OrderModel> get(HttpServletRequest request,
|
||||||
@RequestParam(value = PARAM_ID) Long orderId) {
|
@RequestParam(value = PARAM_ID) Long orderId) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
@@ -70,7 +71,7 @@ public class OrderController {
|
|||||||
return ResponseEntity.ok(order.toModel());
|
return ResponseEntity.ok(order.toModel());
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = ORDER_BASE, method = RequestMethod.POST, produces = "application/json")
|
@PostMapping
|
||||||
public ResponseEntity<Boolean> add(HttpServletRequest request,
|
public ResponseEntity<Boolean> add(HttpServletRequest request,
|
||||||
@RequestBody OrderModel order) {
|
@RequestBody OrderModel order) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
@@ -85,8 +86,8 @@ public class OrderController {
|
|||||||
return ResponseEntity.ok(saved != null);
|
return ResponseEntity.ok(saved != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = ORDER_BASE, method = RequestMethod.PATCH, produces = "application/json")
|
@PatchMapping
|
||||||
public ResponseEntity<Boolean> update(HttpServletRequest request,
|
public ResponseEntity<Boolean> patch(HttpServletRequest request,
|
||||||
@RequestParam(value = PARAM_ID) Long orderId,
|
@RequestParam(value = PARAM_ID) Long orderId,
|
||||||
@RequestParam(value = PARAM_STATUS) OrderStatus status) {
|
@RequestParam(value = PARAM_STATUS) OrderStatus status) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
@@ -107,7 +108,7 @@ public class OrderController {
|
|||||||
return ResponseEntity.ok(orderService.saveNew(order) != null);
|
return ResponseEntity.ok(orderService.saveNew(order) != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = ORDER_BASE, method = RequestMethod.DELETE, produces = "application/json")
|
@DeleteMapping
|
||||||
public ResponseEntity<Boolean> delete(HttpServletRequest request,
|
public ResponseEntity<Boolean> delete(HttpServletRequest request,
|
||||||
@RequestParam(value = PARAM_ID) Long orderId) {
|
@RequestParam(value = PARAM_ID) Long orderId) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
package de.htwsaar.webshop.controller;
|
package de.htwsaar.webshop.controller;
|
||||||
|
|
||||||
import de.htwsaar.webshop.model.ReviewModel;
|
import de.htwsaar.webshop.model.ReviewModel;
|
||||||
@@ -5,8 +6,13 @@ import de.htwsaar.webshop.repository.entities.Review;
|
|||||||
import de.htwsaar.webshop.service.ArticleService;
|
import de.htwsaar.webshop.service.ArticleService;
|
||||||
import de.htwsaar.webshop.service.ReviewService;
|
import de.htwsaar.webshop.service.ReviewService;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@@ -19,6 +25,7 @@ import static de.htwsaar.webshop.config.ParameterConfig.*;
|
|||||||
import static de.htwsaar.webshop.util.LoggerUtil.logRequest;
|
import static de.htwsaar.webshop.util.LoggerUtil.logRequest;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping(value = REVIEW_BASE, produces = "application/json")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ReviewController {
|
public class ReviewController {
|
||||||
|
|
||||||
@@ -31,70 +38,96 @@ public class ReviewController {
|
|||||||
this.articleService = articleService;
|
this.articleService = articleService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = REVIEW_GET_ALL, method = RequestMethod.GET, produces = "application/json")
|
@GetMapping(path = REVIEW_GET_ALL)
|
||||||
public ResponseEntity<List<ReviewModel>> getAll(HttpServletRequest request,
|
public ResponseEntity<List<ReviewModel>> getAll(HttpServletRequest request,
|
||||||
@RequestParam(value = PARAM_UUID) UUID uuid) {
|
@RequestParam(value = PARAM_UUID) UUID uuid) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
List<ReviewModel> review = reviewService.getAllByUUID(uuid).stream().map(reviewService::toModel).toList();
|
List<ReviewModel> reviews = reviewService.getAllByUUID(uuid)
|
||||||
if (review.isEmpty()) {
|
.stream()
|
||||||
|
.map(reviewService::toModel)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (reviews.isEmpty()) {
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
return ResponseEntity.ok(review);
|
return ResponseEntity.ok(reviews);
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = REVIEW_BASE, method = RequestMethod.GET, produces = "application/json")
|
@GetMapping
|
||||||
public ResponseEntity<Review> get(HttpServletRequest request,
|
public ResponseEntity<ReviewModel> get(HttpServletRequest request,
|
||||||
@RequestParam(value = PARAM_ID) Long reviewId) {
|
@RequestParam(value = PARAM_ID) Long reviewId) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
Review review = reviewService.getReviewById(reviewId);
|
Review review = reviewService.getReviewById(reviewId);
|
||||||
|
|
||||||
if (review == null) {
|
if (review == null) {
|
||||||
|
log.debug("Review with id {} not found", reviewId);
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
return ResponseEntity.ok(review);
|
|
||||||
|
return ResponseEntity.ok(reviewService.toModel(review));
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = REVIEW_BASE, method = RequestMethod.POST, produces = "application/json")
|
@PostMapping
|
||||||
public ResponseEntity<Boolean> add(HttpServletRequest request,
|
public ResponseEntity<ReviewModel> add(HttpServletRequest request,
|
||||||
@RequestParam(value = PARAM_UUID) UUID uuid,
|
@RequestParam(value = PARAM_UUID) UUID articleUuid,
|
||||||
@RequestParam(value = PARAM_RATING) int rating,
|
@RequestParam(value = PARAM_RATING)
|
||||||
@RequestBody String review) {
|
@Min(value = 0, message = "Rating must be at least 0")
|
||||||
|
@Max(value = 10, message = "Rating must be at most 10")
|
||||||
|
int rating,
|
||||||
|
@RequestBody @NotBlank(message = "Review text cannot be empty") String reviewText) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
|
|
||||||
if (uuid == null || articleService.findByUUID(uuid) == null
|
if (articleService.findByUUID(articleUuid) == null) {
|
||||||
|| rating < 0 || rating > 10) {
|
log.warn("Article with UUID {} not found for review creation", articleUuid);
|
||||||
log.warn("[{}] failed Validation, sending bad request", request.getRequestURI());
|
return ResponseEntity.badRequest().build();
|
||||||
return ResponseEntity.badRequest().body(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Review saved = reviewService.save(uuid, rating, review);
|
Review savedReview = reviewService.save(articleUuid, rating, reviewText);
|
||||||
return ResponseEntity.ok(saved != null);
|
|
||||||
|
if (savedReview == null) {
|
||||||
|
log.error("Failed to save review for article UUID {}", articleUuid);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(reviewService.toModel(savedReview));
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = REVIEW_BASE, method = RequestMethod.PUT, produces = "application/json")
|
@PutMapping
|
||||||
public ResponseEntity<Boolean> update(HttpServletRequest request,
|
public ResponseEntity<ReviewModel> update(HttpServletRequest request,
|
||||||
@RequestParam(value = PARAM_ID) Long reviewId,
|
@RequestParam(value = PARAM_ID) Long reviewId,
|
||||||
@RequestBody Review review) {
|
@RequestBody @Valid Review review) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
if (reviewId == null || reviewService.getReviewById(reviewId) == null) {
|
|
||||||
return ResponseEntity.badRequest().body(false);
|
Review existingReview = reviewService.getReviewById(reviewId);
|
||||||
|
if (existingReview == null) {
|
||||||
|
log.warn("Review with id {} not found for update", reviewId);
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
review.setId(reviewService.getReviewById(reviewId).getId());
|
|
||||||
return ResponseEntity.ok(reviewService.save(review) != null);
|
// Ensure the ID matches
|
||||||
|
review.setId(reviewId);
|
||||||
|
Review updatedReview = reviewService.save(review);
|
||||||
|
|
||||||
|
if (updatedReview == null) {
|
||||||
|
log.error("Failed to update review with id {}", reviewId);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(reviewService.toModel(updatedReview));
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = REVIEW_BASE, method = RequestMethod.DELETE, produces = "application/json")
|
@DeleteMapping
|
||||||
public ResponseEntity<Boolean> delete(HttpServletRequest request,
|
public ResponseEntity<Void> delete(HttpServletRequest request,
|
||||||
@RequestParam(value = PARAM_ID) Long reviewId) {
|
@RequestParam(value = PARAM_ID) Long reviewId) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
if (reviewId == null) {
|
|
||||||
log.warn("[{}] got invalid imageId", request.getRequestURI());
|
Review existingReview = reviewService.getReviewById(reviewId);
|
||||||
return ResponseEntity.badRequest().body(false);
|
if (existingReview == null) {
|
||||||
}
|
log.warn("Review with id {} not found for deletion", reviewId);
|
||||||
if (reviewService.getReviewById(reviewId) != null) {
|
return ResponseEntity.notFound().build();
|
||||||
log.warn("[{}] got invalid imageId", request.getRequestURI());
|
|
||||||
return ResponseEntity.badRequest().body(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reviewService.delete(reviewId);
|
reviewService.delete(reviewId);
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package de.htwsaar.webshop.controller;
|
package de.htwsaar.webshop.controller;
|
||||||
|
|
||||||
import de.htwsaar.webshop.model.CatMonthModel;
|
import de.htwsaar.webshop.model.CategoryMonthModel;
|
||||||
import de.htwsaar.webshop.model.OrderStatus;
|
import de.htwsaar.webshop.model.OrderStatus;
|
||||||
import de.htwsaar.webshop.service.SessionService;
|
import de.htwsaar.webshop.service.SessionService;
|
||||||
import de.htwsaar.webshop.service.StatisticsService;
|
import de.htwsaar.webshop.service.StatisticsService;
|
||||||
@@ -12,7 +12,6 @@ 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;
|
||||||
|
|
||||||
@@ -34,9 +33,9 @@ public class StatisticsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(value = STATISTICS_VOLUME, method = RequestMethod.GET, produces = "application/json")
|
@RequestMapping(value = STATISTICS_VOLUME, method = RequestMethod.GET, produces = "application/json")
|
||||||
public ResponseEntity<CatMonthModel<Integer>> getMonthlySalesVolume(HttpServletRequest request,
|
public ResponseEntity<CategoryMonthModel<Integer>> getMonthlySalesVolume(HttpServletRequest request,
|
||||||
@RequestParam(value = PARAM_SESSION) UUID session,
|
@RequestParam(value = PARAM_SESSION) UUID session,
|
||||||
@RequestParam(value = PARAM_EMAIL) String email) {
|
@RequestParam(value = PARAM_EMAIL) String email) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
if (!sessionService.isAdmin(session, email)) {
|
if (!sessionService.isAdmin(session, email)) {
|
||||||
log.warn("Invalid session requesting Admin {}", session);
|
log.warn("Invalid session requesting Admin {}", session);
|
||||||
@@ -46,9 +45,9 @@ public class StatisticsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(value = STATISTICS_REVENUE, method = RequestMethod.GET, produces = "application/json")
|
@RequestMapping(value = STATISTICS_REVENUE, method = RequestMethod.GET, produces = "application/json")
|
||||||
public ResponseEntity<CatMonthModel<Integer>> getMonthlyRevenue(HttpServletRequest request,
|
public ResponseEntity<CategoryMonthModel<Integer>> getMonthlyRevenue(HttpServletRequest request,
|
||||||
@RequestParam(value = PARAM_SESSION) UUID token,
|
@RequestParam(value = PARAM_SESSION) UUID token,
|
||||||
@RequestParam(value = PARAM_EMAIL) String email) {
|
@RequestParam(value = PARAM_EMAIL) String email) {
|
||||||
logRequest(request);
|
logRequest(request);
|
||||||
if (!sessionService.isAdmin(token, email)) {
|
if (!sessionService.isAdmin(token, email)) {
|
||||||
log.warn("Invalid session requesting Admin {}", token);
|
log.warn("Invalid session requesting Admin {}", token);
|
||||||
|
|||||||
@@ -16,17 +16,10 @@ public class ExpiredSessionDeleteJob {
|
|||||||
|
|
||||||
private final SessionService sessionService;
|
private final SessionService sessionService;
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor of the class
|
|
||||||
* @param sessionService used in the class
|
|
||||||
*/
|
|
||||||
public ExpiredSessionDeleteJob(SessionService sessionService) {
|
public ExpiredSessionDeleteJob(SessionService sessionService) {
|
||||||
this.sessionService = sessionService;
|
this.sessionService = sessionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method running the job calling the referring service method
|
|
||||||
*/
|
|
||||||
@Scheduled(fixedRate = MILLIS_TO_WEEK)
|
@Scheduled(fixedRate = MILLIS_TO_WEEK)
|
||||||
public void runFixedRateTask() {
|
public void runFixedRateTask() {
|
||||||
log.info("Deleting expired sessions...");
|
log.info("Deleting expired sessions...");
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ import java.util.Map;
|
|||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class CatMonthModel<T extends Number> {
|
public class CategoryMonthModel<T extends Number> {
|
||||||
Map<String, Map<Long, T>> catMonthMap;
|
Map<String, Map<Long, T>> catMonthMap;
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
package de.htwsaar.webshop.service;
|
package de.htwsaar.webshop.service;
|
||||||
|
|
||||||
import de.htwsaar.webshop.model.CatMonthModel;
|
import de.htwsaar.webshop.model.CategoryMonthModel;
|
||||||
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();
|
CategoryMonthModel<Integer> getSalesVolume();
|
||||||
|
|
||||||
CatMonthModel<Integer> getSalesRevenue();
|
CategoryMonthModel<Integer> getSalesRevenue();
|
||||||
|
|
||||||
Map<OrderStatus, Integer> getOrderStatus();
|
Map<OrderStatus, Integer> getOrderStatus();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package de.htwsaar.webshop.service.impl;
|
package de.htwsaar.webshop.service.impl;
|
||||||
|
|
||||||
import de.htwsaar.webshop.model.ArticleCategory;
|
import de.htwsaar.webshop.model.ArticleCategory;
|
||||||
import de.htwsaar.webshop.model.CatMonthModel;
|
import de.htwsaar.webshop.model.CategoryMonthModel;
|
||||||
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.ArticleService;
|
||||||
@@ -30,9 +30,9 @@ public class StatisticsServiceImpl implements StatisticsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//returns Map<unix milli timestamp, Map<Category, T>>
|
//returns Map<unix milli timestamp, Map<Category, T>>
|
||||||
private <T extends Number> CatMonthModel<T> getMonthCategoryMap(Function<OrderItem, T> mappingFunction,
|
private <T extends Number> CategoryMonthModel<T> getMonthCategoryMap(Function<OrderItem, T> mappingFunction,
|
||||||
BinaryOperator<T> reduceFunction,
|
BinaryOperator<T> reduceFunction,
|
||||||
T defaultValue) {
|
T defaultValue) {
|
||||||
Map<String, Map<Long, T>> map = new TreeMap<>();
|
Map<String, Map<Long, T>> map = new TreeMap<>();
|
||||||
for (ArticleCategory value : ArticleCategory.values()) {
|
for (ArticleCategory value : ArticleCategory.values()) {
|
||||||
map.put(value.loc, new TreeMap<>());
|
map.put(value.loc, new TreeMap<>());
|
||||||
@@ -49,16 +49,16 @@ public class StatisticsServiceImpl implements StatisticsService {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return new CatMonthModel<>(map);
|
return new CategoryMonthModel<>(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CatMonthModel<Integer> getSalesVolume() {
|
public CategoryMonthModel<Integer> getSalesVolume() {
|
||||||
return getMonthCategoryMap(OrderItem::getAmount, Integer::sum, 0);
|
return getMonthCategoryMap(OrderItem::getAmount, Integer::sum, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CatMonthModel<Integer> getSalesRevenue() {
|
public CategoryMonthModel<Integer> getSalesRevenue() {
|
||||||
return getMonthCategoryMap(item -> item.getArticle().getPrice100(), Integer::sum, 0);
|
return getMonthCategoryMap(item -> item.getArticle().getPrice100(), Integer::sum, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
01-frontend/package-lock.json
generated
15
01-frontend/package-lock.json
generated
@@ -4880,21 +4880,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
|
||||||
"version": "2.8.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
|
|
||||||
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"yaml": "bin.mjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|||||||
@@ -132,8 +132,8 @@
|
|||||||
"salesStatistics": "Sales Statistics",
|
"salesStatistics": "Sales Statistics",
|
||||||
"monthlySalesVolume": "Monthly Sales Volume",
|
"monthlySalesVolume": "Monthly Sales Volume",
|
||||||
"monthlySalesRevenue": "Monthly Sales Revenue in €",
|
"monthlySalesRevenue": "Monthly Sales Revenue in €",
|
||||||
"itemVolumeDistribution": "Item Volume Distribution",
|
"itemVolumeDistribution": "Sales Volume Distribution",
|
||||||
"itemRevenueDistribution": "Item Revenue Distribution",
|
"itemRevenueDistribution": "Sales Revenue Distribution",
|
||||||
"stockFulfillment": "Stock fulfillment",
|
"stockFulfillment": "Stock fulfillment",
|
||||||
"orderStatus": "Order Status",
|
"orderStatus": "Order Status",
|
||||||
"ORDERED": "Ordered",
|
"ORDERED": "Ordered",
|
||||||
|
|||||||
@@ -1,63 +1,64 @@
|
|||||||
/* App.css - CSS-Variablen-basierte Version */
|
/* App.css - CSS-Variablen-basierte Version */
|
||||||
:root {
|
:root {
|
||||||
--background-color: #fafafa;
|
--background-color: #fafafa;
|
||||||
--text-color: #000000;
|
--text-color: #000000;
|
||||||
--navbar-height: 5.1vh;
|
--navbar-height: 5.1vh;
|
||||||
--page-height: 94vh;
|
--page-height: 94vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: var(--background-color) !important;
|
background-color: var(--background-color) !important;
|
||||||
color: var(--text-color) !important;
|
color: var(--text-color) !important;
|
||||||
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html,
|
||||||
margin: 0;
|
body {
|
||||||
padding: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
padding: 0;
|
||||||
background-color: var(--background-color) !important;
|
overflow: hidden;
|
||||||
color: var(--text-color) !important;
|
background-color: var(--background-color) !important;
|
||||||
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
color: var(--text-color) !important;
|
||||||
|
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
height: 6em;
|
height: 6em;
|
||||||
padding: 1.5em;
|
padding: 1.5em;
|
||||||
will-change: filter;
|
will-change: filter;
|
||||||
transition: filter 300ms;
|
transition: filter 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo:hover {
|
.logo:hover {
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo.react:hover {
|
.logo.react:hover {
|
||||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes logo-spin {
|
@keyframes logo-spin {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
a:nth-of-type(2) .logo {
|
a:nth-of-type(2) .logo {
|
||||||
animation: logo-spin infinite 20s linear;
|
animation: logo-spin infinite 20s linear;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.read-the-docs {
|
.read-the-docs {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,51 @@
|
|||||||
import {StyledEngineProvider} from '@mui/material/styles';
|
import { StyledEngineProvider } from "@mui/material/styles";
|
||||||
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import {BrowserRouter, Route, Routes} from 'react-router-dom';
|
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||||
import './App.css';
|
import "./App.css";
|
||||||
import {BasketProvider} from './helper/BasketProvider';
|
import { BasketProvider } from "./helper/BasketProvider";
|
||||||
import NavBar from './helper/navbar/NavBar';
|
import NavBar from "./helper/navbar/NavBar";
|
||||||
import Account from './pages/Account';
|
import Account from "./pages/Account";
|
||||||
import Contact from './pages/Contact';
|
import Contact from "./pages/Contact";
|
||||||
import Home from './pages/Home';
|
import Home from "./pages/Home";
|
||||||
import NoPage from './pages/NoPage';
|
import NoPage from "./pages/NoPage";
|
||||||
import Orders from './pages/Orders';
|
import Orders from "./pages/Orders";
|
||||||
import Payment from './pages/Payment';
|
import Payment from "./pages/Payment";
|
||||||
import Product from './pages/Product';
|
import Product from "./pages/Product";
|
||||||
import {CustomThemeProvider} from './theme/ThemeContext';
|
import { CustomThemeProvider } from "./theme/ThemeContext";
|
||||||
import FSComponents from './pages/FSComponents.tsx';
|
import FSComponents from "./pages/FSComponents.tsx";
|
||||||
import AdminPanel from './pages/AdminPanel';
|
import AdminPanel from "./pages/AdminPanel";
|
||||||
import {AccountProvider} from './helper/AccountProvider.tsx';
|
import { AccountProvider } from "./helper/AccountProvider.tsx";
|
||||||
import {CookiesProvider} from 'react-cookie';
|
import { CookiesProvider } from "react-cookie";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
return (
|
<StyledEngineProvider injectFirst>
|
||||||
<QueryClientProvider client={queryClient}>
|
<CustomThemeProvider>
|
||||||
<StyledEngineProvider injectFirst>
|
<CookiesProvider>
|
||||||
<CustomThemeProvider>
|
<AccountProvider>
|
||||||
<CookiesProvider>
|
<BasketProvider>
|
||||||
<AccountProvider>
|
<BrowserRouter>
|
||||||
<BasketProvider>
|
<NavBar />
|
||||||
<BrowserRouter>
|
<Routes>
|
||||||
<NavBar/>
|
<Route path="/" element={<Home />} />
|
||||||
<Routes>
|
<Route path="*" element={<NoPage />} />
|
||||||
<Route path="/" element={<Home/>}/>
|
<Route path="/product/:id" element={<Product />} />
|
||||||
<Route path="*" element={<NoPage/>}/>
|
<Route path="/checkout" element={<Payment />} />
|
||||||
<Route path="/product/:id" element={<Product/>}/>
|
<Route path="/components" element={<FSComponents />} />
|
||||||
<Route path="/checkout" element={<Payment/>}/>
|
<Route path="/contact" element={<Contact />} />
|
||||||
<Route path="/components" element={<FSComponents/>}/>
|
<Route path="/account" element={<Account />} />
|
||||||
<Route path="/contact" element={<Contact/>}/>
|
<Route path="/orders" element={<Orders />} />
|
||||||
<Route path='/account' element={<Account/>}/>
|
<Route path="/admin" element={<AdminPanel />} />
|
||||||
<Route path='/orders' element={<Orders/>}/>
|
</Routes>
|
||||||
<Route path='/admin' element={<AdminPanel/>}/>
|
</BrowserRouter>
|
||||||
</Routes>
|
</BasketProvider>
|
||||||
</BrowserRouter>
|
</AccountProvider>
|
||||||
</BasketProvider>
|
</CookiesProvider>
|
||||||
</AccountProvider>
|
</CustomThemeProvider>
|
||||||
</CookiesProvider>
|
</StyledEngineProvider>
|
||||||
</CustomThemeProvider>
|
</QueryClientProvider>
|
||||||
</StyledEngineProvider>
|
);
|
||||||
</QueryClientProvider>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,57 @@
|
|||||||
type AccountType = {
|
type AccountType = {
|
||||||
|
id: number;
|
||||||
|
customer: {
|
||||||
id: number;
|
id: number;
|
||||||
customer: {
|
name: string;
|
||||||
id: number;
|
surname: string;
|
||||||
name: string;
|
address: string;
|
||||||
surname: string;
|
country: string;
|
||||||
address: string;
|
zip: string;
|
||||||
country: string;
|
};
|
||||||
zip: string;
|
password: string;
|
||||||
};
|
langI18n: string;
|
||||||
password: string;
|
admin: boolean;
|
||||||
langI18n: string;
|
email: string;
|
||||||
admin: boolean;
|
|
||||||
email: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AccountType;
|
export default AccountType;
|
||||||
|
|
||||||
export type CustomerType =
|
export type CustomerType = {
|
||||||
{
|
id: number;
|
||||||
id: number;
|
name: string;
|
||||||
name: string;
|
surname: string;
|
||||||
surname: string;
|
address: string;
|
||||||
address: string;
|
country: string;
|
||||||
country: string;
|
zip: string;
|
||||||
zip: string;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export type SubmitLogin = {
|
export type SubmitLogin = {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SubmitLoginSession = {
|
export type SubmitLoginSession = {
|
||||||
email: string;
|
email: string;
|
||||||
session: string;
|
session: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminAccountOperation = {
|
export type AdminAccountOperation = {
|
||||||
email: string;
|
email: string;
|
||||||
uuid: string;
|
uuid: string;
|
||||||
accountId: number;
|
accountId: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
password: string;
|
password: string;
|
||||||
email: string;
|
email: string;
|
||||||
customerId: number;
|
customerId: number;
|
||||||
session: string;
|
session: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
// weitere Felder nach Bedarf
|
// weitere Felder nach Bedarf
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AccountContextType = {
|
export type AccountContextType = {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
login: (userData: User) => void;
|
login: (userData: User) => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,43 +1,43 @@
|
|||||||
export type Item = {
|
export type Item = {
|
||||||
id: number;
|
id: number;
|
||||||
uuid: string;
|
uuid: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
price100: number;
|
price100: number;
|
||||||
stock: number;
|
stock: number;
|
||||||
stockExpected: number;
|
stockExpected: number;
|
||||||
category: string;
|
category: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
discount100: number;
|
discount100: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ItemWithImage = {
|
type ItemWithImage = {
|
||||||
id: number;
|
id: number;
|
||||||
uuid: string;
|
uuid: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
price100: number;
|
price100: number;
|
||||||
stock: number;
|
stock: number;
|
||||||
stockExpected: number;
|
stockExpected: number;
|
||||||
category: string;
|
category: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
discount100: number;
|
discount100: number;
|
||||||
image: string;
|
image: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ItemWithFSImage = {
|
export type ItemWithFSImage = {
|
||||||
id: number;
|
id: number;
|
||||||
uuid: string;
|
uuid: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
price100: number;
|
price100: number;
|
||||||
stock: number;
|
stock: number;
|
||||||
stockExpected: number;
|
stockExpected: number;
|
||||||
category: string;
|
category: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
discount100: number;
|
discount100: number;
|
||||||
image: string;
|
image: string;
|
||||||
farmImage: string;
|
farmImage: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ItemWithImage;
|
export default ItemWithImage;
|
||||||
|
|||||||
@@ -1,40 +1,39 @@
|
|||||||
export enum OrderStatusEnum {
|
export enum OrderStatusEnum {
|
||||||
ORDERED = 'ORDERED',
|
ORDERED = "ORDERED",
|
||||||
IN_PROGRESS = 'IN_PROGRESS',
|
IN_PROGRESS = "IN_PROGRESS",
|
||||||
ISSUES = 'ISSUES',
|
ISSUES = "ISSUES",
|
||||||
DELIVERED = 'DELIVERED',
|
DELIVERED = "DELIVERED",
|
||||||
CANCELLED = 'CANCELLED',
|
CANCELLED = "CANCELLED",
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderType = {
|
type OrderType = {
|
||||||
id: number;
|
id: number;
|
||||||
customerId: number;
|
customerId: number;
|
||||||
time: number;
|
time: number;
|
||||||
status: OrderStatusEnum;
|
status: OrderStatusEnum;
|
||||||
orderItems: { id: number; amount: number; article: string }[];
|
orderItems: { id: number; amount: number; article: string }[];
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OrderType;
|
export default OrderType;
|
||||||
|
|
||||||
|
|
||||||
export type ShippingDetails = {
|
export type ShippingDetails = {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
telefon: string;
|
telefon: string;
|
||||||
address: string;
|
address: string;
|
||||||
postalCode: string;
|
postalCode: string;
|
||||||
city: string;
|
city: string;
|
||||||
country: string;
|
country: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OrderItem = {
|
export type OrderItem = {
|
||||||
id: string;
|
id: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
article: string; // UUID of the item
|
article: string; // UUID of the item
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OrderPatch = {
|
export type OrderPatch = {
|
||||||
id: number;
|
id: number;
|
||||||
status: OrderStatusEnum;
|
status: OrderStatusEnum;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
type RatingType = {
|
type RatingType = {
|
||||||
rating: number;
|
rating: number;
|
||||||
content: string;
|
content: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RatingType;
|
export default RatingType;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
type RatingSubmitType = {
|
type RatingSubmitType = {
|
||||||
articleId: string;
|
articleId: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
content: string;
|
content: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RatingSubmitType;
|
export default RatingSubmitType;
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import {initReactI18next} from "react-i18next";
|
import { initReactI18next } from "react-i18next";
|
||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import HttpBackend from "i18next-http-backend";
|
import HttpBackend from "i18next-http-backend";
|
||||||
|
|
||||||
i18next
|
i18next
|
||||||
.use(HttpBackend)
|
.use(HttpBackend)
|
||||||
.use(LanguageDetector)
|
.use(LanguageDetector)
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
.init({
|
.init({
|
||||||
fallbackLng: "en",
|
fallbackLng: "en",
|
||||||
debug: false,
|
debug: false,
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
escapeValue: false,
|
||||||
},
|
},
|
||||||
backend: {
|
backend: {
|
||||||
loadPath: "/locales/{{lng}}/translation.json",
|
loadPath: "/locales/{{lng}}/translation.json",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,42 +1,48 @@
|
|||||||
import {createContext, ReactNode, useContext, useEffect, useState} from "react";
|
import {
|
||||||
import {AccountContextType, User} from "../components/Account";
|
createContext,
|
||||||
import {useCookies} from 'react-cookie';
|
ReactNode,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { AccountContextType, User } from "../components/Account";
|
||||||
|
import { useCookies } from "react-cookie";
|
||||||
|
|
||||||
const AccountContext = createContext<AccountContextType | undefined>(undefined);
|
const AccountContext = createContext<AccountContextType | undefined>(undefined);
|
||||||
|
|
||||||
export const AccountProvider = ({children}: { children: ReactNode }) => {
|
export const AccountProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [cookies, setCookie] = useCookies(["account"]);
|
const [cookies, setCookie] = useCookies(["account"]);
|
||||||
|
|
||||||
const initialAccount =
|
const initialAccount =
|
||||||
typeof cookies.account === "object" && !Array.isArray(cookies.account)
|
typeof cookies.account === "object" && !Array.isArray(cookies.account)
|
||||||
? cookies.account
|
? cookies.account
|
||||||
: null;
|
: null;
|
||||||
const [user, setUser] = useState<User | null>(initialAccount);
|
const [user, setUser] = useState<User | null>(initialAccount);
|
||||||
|
|
||||||
const login = (userData: User) => {
|
const login = (userData: User) => {
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCookie("account", user, {path: "/", maxAge: 3600 * 24 * 7});
|
setCookie("account", user, { path: "/", maxAge: 3600 * 24 * 7 });
|
||||||
}, [user, setCookie]);
|
}, [user, setCookie]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccountContext.Provider value={{user, login, logout}}>
|
<AccountContext.Provider value={{ user, login, logout }}>
|
||||||
{children}
|
{children}
|
||||||
</AccountContext.Provider>
|
</AccountContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export const useAccount = () => {
|
export const useAccount = () => {
|
||||||
const context = useContext(AccountContext);
|
const context = useContext(AccountContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useAccount must be used within an AccountProvider");
|
throw new Error("useAccount must be used within an AccountProvider");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,60 +1,64 @@
|
|||||||
import React, {createContext, useContext, useEffect, useState} from 'react';
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
import Item from '../components/Item';
|
import Item from "../components/Item";
|
||||||
import {useCookies} from 'react-cookie';
|
import { useCookies } from "react-cookie";
|
||||||
|
|
||||||
export interface BasketItem {
|
export interface BasketItem {
|
||||||
item: Item;
|
item: Item;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BasketContextType {
|
interface BasketContextType {
|
||||||
basket: BasketItem[];
|
basket: BasketItem[];
|
||||||
addToBasket: (item: Item, quantity: number) => void;
|
addToBasket: (item: Item, quantity: number) => void;
|
||||||
clearBasket: () => void;
|
clearBasket: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BasketContext = createContext<BasketContextType | undefined>(undefined);
|
const BasketContext = createContext<BasketContextType | undefined>(undefined);
|
||||||
|
|
||||||
export const BasketProvider: React.FC<{ children: React.ReactNode }> = ({children}) => {
|
export const BasketProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
const [cookies, setCookie] = useCookies(["basket"]);
|
children,
|
||||||
const [basket, setBasket] = useState<BasketItem[]>(cookies.basket || []);
|
}) => {
|
||||||
|
const [cookies, setCookie] = useCookies(["basket"]);
|
||||||
|
const [basket, setBasket] = useState<BasketItem[]>(cookies.basket || []);
|
||||||
|
|
||||||
const addToBasket = (item: Item, quantity: number) => {
|
const addToBasket = (item: Item, quantity: number) => {
|
||||||
setBasket((prevBasket) => {
|
setBasket((prevBasket) => {
|
||||||
const existingItem = prevBasket.find((basketItem) => basketItem.item.uuid === item.uuid);
|
const existingItem = prevBasket.find(
|
||||||
if (existingItem) {
|
(basketItem) => basketItem.item.uuid === item.uuid,
|
||||||
// Update quantity if item already exists
|
);
|
||||||
return prevBasket.map((basketItem) =>
|
if (existingItem) {
|
||||||
basketItem.item.uuid === item.uuid
|
// Update quantity if item already exists
|
||||||
? {...basketItem, quantity: basketItem.quantity + quantity}
|
return prevBasket.map((basketItem) =>
|
||||||
: basketItem
|
basketItem.item.uuid === item.uuid
|
||||||
);
|
? { ...basketItem, quantity: basketItem.quantity + quantity }
|
||||||
}
|
: basketItem,
|
||||||
// Add new item to basket
|
);
|
||||||
return [...prevBasket, {item, quantity}];
|
}
|
||||||
});
|
// Add new item to basket
|
||||||
};
|
return [...prevBasket, { item, quantity }];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const clearBasket = () => {
|
const clearBasket = () => {
|
||||||
setBasket([]);
|
setBasket([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCookie("basket", basket, {path: "/", maxAge: 3600 * 24 * 7}); // 7 Tage
|
setCookie("basket", basket, { path: "/", maxAge: 3600 * 24 * 7 }); // 7 Tage
|
||||||
}, [basket, setCookie]);
|
}, [basket, setCookie]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasketContext.Provider value={{basket, addToBasket, clearBasket}}>
|
<BasketContext.Provider value={{ basket, addToBasket, clearBasket }}>
|
||||||
{children}
|
{children}
|
||||||
</BasketContext.Provider>
|
</BasketContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export const useBasket = () => {
|
export const useBasket = () => {
|
||||||
const context = useContext(BasketContext);
|
const context = useContext(BasketContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useBasket must be used within a BasketProvider');
|
throw new Error("useBasket must be used within a BasketProvider");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,183 +1,211 @@
|
|||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import EditIcon from "@mui/icons-material/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
import {Box, Button, IconButton, Toolbar, useTheme} from "@mui/material";
|
import { Box, Button, IconButton, Toolbar, useTheme } from "@mui/material";
|
||||||
import {DataGrid, GridColDef, GridRowId, GridRowSelectionModel} from "@mui/x-data-grid";
|
import {
|
||||||
import {useMutation, useQuery} from "@tanstack/react-query";
|
DataGrid,
|
||||||
import {useEffect, useState} from "react";
|
GridColDef,
|
||||||
import {useTranslation} from "react-i18next";
|
GridRowId,
|
||||||
import AccountType, {AdminAccountOperation, CustomerType} from "../../components/Account";
|
GridRowSelectionModel,
|
||||||
import {useAccount} from "../AccountProvider";
|
} from "@mui/x-data-grid";
|
||||||
import {deleteAccountAdmin, fetchAccounts, updateAccountAdmin} from "../query/Queries";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import AccountType, {
|
||||||
|
AdminAccountOperation,
|
||||||
|
CustomerType,
|
||||||
|
} from "../../components/Account";
|
||||||
|
import { useAccount } from "../AccountProvider";
|
||||||
|
import {
|
||||||
|
deleteAccountAdmin,
|
||||||
|
fetchAccounts,
|
||||||
|
updateAccountAdmin,
|
||||||
|
} from "../query/Queries";
|
||||||
import CustomerEditDialog from "./CustomerEditDialog";
|
import CustomerEditDialog from "./CustomerEditDialog";
|
||||||
|
|
||||||
export default function AccountsInfo() {
|
export default function AccountsInfo() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [customerData, setCustomerData] = useState<CustomerType>({
|
const [customerData, setCustomerData] = useState<CustomerType>({
|
||||||
id: 0,
|
id: 0,
|
||||||
name: "",
|
name: "",
|
||||||
surname: "",
|
surname: "",
|
||||||
address: "",
|
address: "",
|
||||||
zip: "",
|
zip: "",
|
||||||
country: ""
|
country: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleCustomerEdit(account: AccountType) {
|
async function handleCustomerEdit(account: AccountType) {
|
||||||
setCustomerData(account.customer);
|
setCustomerData(account.customer);
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rows, setRows] = useState<AccountType[]>([]);
|
||||||
|
const [selectedRows, setSelectedRows] = useState<Set<GridRowId>>(new Set());
|
||||||
|
|
||||||
|
const { user: loginData } = useAccount();
|
||||||
|
|
||||||
|
const { data, refetch } = useQuery({
|
||||||
|
queryKey: ["fetchAccounts", loginData],
|
||||||
|
queryFn: () =>
|
||||||
|
fetchAccounts(
|
||||||
|
loginData
|
||||||
|
? loginData
|
||||||
|
: {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
session: "",
|
||||||
|
customerId: -1,
|
||||||
|
isAdmin: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
retry: 3,
|
||||||
|
retryDelay: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteAccount = useMutation({
|
||||||
|
mutationFn: (user: AdminAccountOperation) => deleteAccountAdmin(user),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
setRows(data);
|
||||||
}
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
const [rows, setRows] = useState<AccountType[]>([]);
|
const handleSelectionChange = (newSelection: GridRowSelectionModel) => {
|
||||||
const [selectedRows, setSelectedRows] = useState<Set<GridRowId>>(new Set());
|
setSelectedRows(newSelection.ids);
|
||||||
|
};
|
||||||
|
|
||||||
const {user: loginData} = useAccount();
|
const handleDeleteSelected = async () => {
|
||||||
|
selectedRows.forEach(async (rowId) => {
|
||||||
const {data, refetch} = useQuery({
|
let id = rows.find((row) => row.id === rowId)?.id;
|
||||||
queryKey: ["fetchAccounts", loginData],
|
if (id === undefined) id = -1;
|
||||||
queryFn: () => fetchAccounts(loginData ? loginData : {
|
await deleteAccount.mutateAsync({
|
||||||
email: "",
|
email: loginData?.email || "",
|
||||||
password: "",
|
uuid: loginData?.session || "",
|
||||||
session: "",
|
accountId: id,
|
||||||
customerId: -1,
|
});
|
||||||
isAdmin: false
|
|
||||||
}),
|
|
||||||
retry: 3,
|
|
||||||
retryDelay: 1000,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteAccount = useMutation({
|
setRows(rows.filter((row) => !selectedRows.has(row.id)));
|
||||||
mutationFn: (user: AdminAccountOperation) =>
|
};
|
||||||
deleteAccountAdmin(user),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
const updateAdmin = useMutation({
|
||||||
if (data) {
|
mutationFn: (account: AccountType) =>
|
||||||
setRows(data);
|
updateAccountAdmin(
|
||||||
}
|
account,
|
||||||
}, [data]);
|
loginData
|
||||||
|
? loginData
|
||||||
|
: {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
session: "",
|
||||||
|
customerId: -1,
|
||||||
|
isAdmin: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
const handleSelectionChange = (newSelection: GridRowSelectionModel) => {
|
const columns: GridColDef<(typeof rows)[number]>[] = [
|
||||||
setSelectedRows(newSelection.ids);
|
{ field: "id", headerName: "ID", width: 60 },
|
||||||
};
|
{
|
||||||
|
field: "admin",
|
||||||
|
headerName: t("admin"),
|
||||||
|
type: "boolean",
|
||||||
|
width: 90,
|
||||||
|
editable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "email",
|
||||||
|
headerName: t("email"),
|
||||||
|
width: 150,
|
||||||
|
editable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "langI18n",
|
||||||
|
headerName: t("language"),
|
||||||
|
width: 150,
|
||||||
|
editable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
//edit billing information button
|
||||||
|
field: "customer",
|
||||||
|
headerName: t("address"),
|
||||||
|
width: 90,
|
||||||
|
sortable: false,
|
||||||
|
disableReorder: true,
|
||||||
|
disableColumnMenu: true,
|
||||||
|
renderCell: (params) => (
|
||||||
|
<IconButton onClick={() => handleCustomerEdit(params.row)}>
|
||||||
|
{" "}
|
||||||
|
<EditIcon />{" "}
|
||||||
|
</IconButton>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const handleDeleteSelected = async () => {
|
const [open, setOpen] = useState(false);
|
||||||
selectedRows.forEach(async (rowId) => {
|
|
||||||
let id = rows.find((row) => row.id === rowId)?.id
|
|
||||||
if(id === undefined) id = -1;
|
|
||||||
await deleteAccount.mutateAsync({
|
|
||||||
email: loginData?.email || '',
|
|
||||||
uuid: loginData?.session || '',
|
|
||||||
accountId: id,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
setRows(rows.filter((row) => !selectedRows.has(row.id)));
|
const handleCustomerEditSubmit = async () => {
|
||||||
};
|
setOpen(false);
|
||||||
|
await refetch();
|
||||||
|
};
|
||||||
|
|
||||||
const updateAdmin = useMutation({
|
return (
|
||||||
mutationFn: (account: AccountType) =>
|
<>
|
||||||
updateAccountAdmin(account, loginData ? loginData : {
|
<Box
|
||||||
email: "",
|
className="page-table"
|
||||||
password: "",
|
sx={{
|
||||||
session: "",
|
backgroundColor: theme.palette.background.paper,
|
||||||
customerId: -1,
|
color: theme.palette.text.secondary,
|
||||||
isAdmin: false
|
}}
|
||||||
}),
|
>
|
||||||
|
<DataGrid
|
||||||
});
|
rows={rows}
|
||||||
|
columns={columns}
|
||||||
const columns: GridColDef<(typeof rows)[number]>[] = [
|
initialState={{}}
|
||||||
{field: 'id', headerName: 'ID', width: 60},
|
checkboxSelection
|
||||||
{
|
disableRowSelectionOnClick
|
||||||
field: 'admin',
|
onRowSelectionModelChange={handleSelectionChange}
|
||||||
headerName: t('admin'),
|
slots={{
|
||||||
type: "boolean",
|
toolbar: () => (
|
||||||
width: 90,
|
<Toolbar>
|
||||||
editable: true
|
<Button
|
||||||
},
|
variant="contained"
|
||||||
{
|
color="error"
|
||||||
field: 'email',
|
startIcon={<DeleteIcon />}
|
||||||
headerName: t('email'),
|
onClick={handleDeleteSelected}
|
||||||
width: 150,
|
disabled={selectedRows.size === 0}
|
||||||
editable: false,
|
sx={{
|
||||||
},
|
marginRight: 1,
|
||||||
{
|
}}
|
||||||
field: 'langI18n',
|
>
|
||||||
headerName: t('language'),
|
{t("deleteAccount")}
|
||||||
width: 150,
|
</Button>
|
||||||
editable: false,
|
</Toolbar>
|
||||||
},
|
),
|
||||||
{ //edit billing information button
|
}}
|
||||||
field: "customer",
|
showToolbar
|
||||||
headerName: t('address'),
|
processRowUpdate={async (updatedRow) => {
|
||||||
width: 90,
|
const originalRow = rows.find((row) => row.id === updatedRow.id);
|
||||||
sortable: false,
|
if (originalRow && originalRow.admin !== updatedRow.admin) {
|
||||||
disableReorder: true,
|
await updateAdmin.mutateAsync(updatedRow);
|
||||||
disableColumnMenu: true,
|
}
|
||||||
renderCell: params => <IconButton onClick={() => handleCustomerEdit(params.row)}> <EditIcon/> </IconButton>,
|
setRows(
|
||||||
}
|
rows.map((row) => (row.id === updatedRow.id ? updatedRow : row)),
|
||||||
];
|
);
|
||||||
|
return updatedRow;
|
||||||
const [open, setOpen] = useState(false);
|
}}
|
||||||
|
/>
|
||||||
const handleCustomerEditSubmit = async () => {
|
</Box>
|
||||||
setOpen(false);
|
<CustomerEditDialog
|
||||||
await refetch();
|
open={open}
|
||||||
};
|
onClose={() => setOpen(false)}
|
||||||
|
onSubmit={handleCustomerEditSubmit}
|
||||||
return (
|
customerData={customerData}
|
||||||
<>
|
setCustomerData={setCustomerData}
|
||||||
<Box
|
/>
|
||||||
className="page-table"
|
</>
|
||||||
sx={{
|
);
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
color: theme.palette.text.secondary
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DataGrid
|
|
||||||
rows={rows}
|
|
||||||
columns={columns}
|
|
||||||
initialState={{}}
|
|
||||||
checkboxSelection
|
|
||||||
disableRowSelectionOnClick
|
|
||||||
onRowSelectionModelChange={handleSelectionChange}
|
|
||||||
slots={{
|
|
||||||
toolbar: () => (
|
|
||||||
<Toolbar>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="error"
|
|
||||||
startIcon={<DeleteIcon/>}
|
|
||||||
onClick={handleDeleteSelected}
|
|
||||||
disabled={selectedRows.size === 0}
|
|
||||||
sx={{
|
|
||||||
marginRight: 1
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('deleteAccount')}
|
|
||||||
</Button>
|
|
||||||
</Toolbar>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
showToolbar
|
|
||||||
processRowUpdate={async (updatedRow) => {
|
|
||||||
const originalRow = rows.find(row => row.id === updatedRow.id);
|
|
||||||
if (originalRow && originalRow.admin !== updatedRow.admin) {
|
|
||||||
await updateAdmin.mutateAsync(updatedRow);
|
|
||||||
}
|
|
||||||
setRows(rows.map(row => row.id === updatedRow.id ? updatedRow : row));
|
|
||||||
return updatedRow;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<CustomerEditDialog
|
|
||||||
open={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
onSubmit={handleCustomerEditSubmit}
|
|
||||||
customerData={customerData}
|
|
||||||
setCustomerData={setCustomerData}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,108 +1,123 @@
|
|||||||
import {Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField} from "@mui/material";
|
import {
|
||||||
import {useMutation} from "@tanstack/react-query";
|
Button,
|
||||||
import React, {useEffect} from "react";
|
Dialog,
|
||||||
import {useTranslation} from "react-i18next";
|
DialogActions,
|
||||||
import {CustomerType} from "../../components/Account";
|
DialogContent,
|
||||||
import {updateCustomer} from "../query/Queries"; // Importiere die Funktion für die Registrierung
|
DialogTitle,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { CustomerType } from "../../components/Account";
|
||||||
|
import { updateCustomer } from "../query/Queries"; // Importiere die Funktion für die Registrierung
|
||||||
|
|
||||||
type CustomerDialogProps = {
|
type CustomerDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: () => void; // Funktion, die aufgerufen wird, wenn der Login erfolgreich ist
|
onSubmit: () => void; // Funktion, die aufgerufen wird, wenn der Login erfolgreich ist
|
||||||
customerData: CustomerType;
|
customerData: CustomerType;
|
||||||
setCustomerData: React.Dispatch<React.SetStateAction<CustomerType>>;
|
setCustomerData: React.Dispatch<React.SetStateAction<CustomerType>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomerEditDialog: React.FC<CustomerDialogProps> = ({
|
const CustomerEditDialog: React.FC<CustomerDialogProps> = ({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
customerData,
|
customerData,
|
||||||
setCustomerData,
|
setCustomerData,
|
||||||
onSubmit
|
onSubmit,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {t} = useTranslation();
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
useEffect(() => {
|
const active = document.activeElement as HTMLElement | null;
|
||||||
if (open) {
|
if (active && typeof active.blur === "function") {
|
||||||
const active = document.activeElement as HTMLElement | null;
|
active.blur();
|
||||||
if (active && typeof active.blur === "function") {
|
}
|
||||||
active.blur();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const changeCustomer = useMutation({
|
|
||||||
mutationFn: (customer: CustomerType) =>
|
|
||||||
updateCustomer(customer),
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
const customer: CustomerType = {
|
|
||||||
id: customerData.id,
|
|
||||||
name: customerData.name,
|
|
||||||
surname: customerData.surname,
|
|
||||||
address: customerData.address,
|
|
||||||
zip: customerData.zip,
|
|
||||||
country: customerData.country,
|
|
||||||
};
|
|
||||||
await changeCustomer.mutateAsync(customer);
|
|
||||||
onSubmit(); // Rufe die onSubmit-Funktion auf, um den Dialog zu schließen und die Änderungen zu übernehmen
|
|
||||||
onClose(); // Schließe den Dialog
|
|
||||||
}
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
return (
|
const changeCustomer = useMutation({
|
||||||
<Dialog open={open} onClose={onClose} disableEnforceFocus>
|
mutationFn: (customer: CustomerType) => updateCustomer(customer),
|
||||||
<DialogTitle>{t('changeCustomer')}</DialogTitle>
|
});
|
||||||
<DialogContent>
|
|
||||||
<TextField
|
const handleSave = async () => {
|
||||||
margin="dense"
|
const customer: CustomerType = {
|
||||||
label={t("name")}
|
id: customerData.id,
|
||||||
type="text"
|
name: customerData.name,
|
||||||
fullWidth
|
surname: customerData.surname,
|
||||||
value={customerData.name}
|
address: customerData.address,
|
||||||
onChange={e => setCustomerData(prev => ({...prev, name: e.target.value}))}
|
zip: customerData.zip,
|
||||||
/>
|
country: customerData.country,
|
||||||
<TextField
|
};
|
||||||
margin="dense"
|
await changeCustomer.mutateAsync(customer);
|
||||||
label={t("surname")}
|
onSubmit(); // Rufe die onSubmit-Funktion auf, um den Dialog zu schließen und die Änderungen zu übernehmen
|
||||||
type="text"
|
onClose(); // Schließe den Dialog
|
||||||
fullWidth
|
};
|
||||||
value={customerData.surname}
|
|
||||||
onChange={e => setCustomerData(prev => ({...prev, surname: e.target.value}))}
|
return (
|
||||||
/>
|
<Dialog open={open} onClose={onClose} disableEnforceFocus>
|
||||||
<TextField
|
<DialogTitle>{t("changeCustomer")}</DialogTitle>
|
||||||
margin="dense"
|
<DialogContent>
|
||||||
label={t("address")}
|
<TextField
|
||||||
type="text"
|
margin="dense"
|
||||||
fullWidth
|
label={t("name")}
|
||||||
value={customerData.address}
|
type="text"
|
||||||
onChange={e => setCustomerData(prev => ({...prev, address: e.target.value}))}
|
fullWidth
|
||||||
/>
|
value={customerData.name}
|
||||||
<TextField
|
onChange={(e) =>
|
||||||
margin="dense"
|
setCustomerData((prev) => ({ ...prev, name: e.target.value }))
|
||||||
label={t("zip")}
|
}
|
||||||
type="text"
|
/>
|
||||||
fullWidth
|
<TextField
|
||||||
value={customerData.zip}
|
margin="dense"
|
||||||
onChange={e => setCustomerData(prev => ({...prev, zip: e.target.value}))}
|
label={t("surname")}
|
||||||
/>
|
type="text"
|
||||||
<TextField
|
fullWidth
|
||||||
margin="dense"
|
value={customerData.surname}
|
||||||
label={t("country")}
|
onChange={(e) =>
|
||||||
type="text"
|
setCustomerData((prev) => ({ ...prev, surname: e.target.value }))
|
||||||
fullWidth
|
}
|
||||||
value={customerData.country}
|
/>
|
||||||
onChange={e => setCustomerData(prev => ({...prev, country: e.target.value}))}
|
<TextField
|
||||||
/>
|
margin="dense"
|
||||||
</DialogContent>
|
label={t("address")}
|
||||||
<DialogActions>
|
type="text"
|
||||||
<Button variant="contained" color="primary" onClick={handleSave}>
|
fullWidth
|
||||||
{t("save")}
|
value={customerData.address}
|
||||||
</Button>
|
onChange={(e) =>
|
||||||
</DialogActions>
|
setCustomerData((prev) => ({ ...prev, address: e.target.value }))
|
||||||
</Dialog>
|
}
|
||||||
);
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
label={t("zip")}
|
||||||
|
type="text"
|
||||||
|
fullWidth
|
||||||
|
value={customerData.zip}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCustomerData((prev) => ({ ...prev, zip: e.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
label={t("country")}
|
||||||
|
type="text"
|
||||||
|
fullWidth
|
||||||
|
value={customerData.country}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCustomerData((prev) => ({ ...prev, country: e.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="contained" color="primary" onClick={handleSave}>
|
||||||
|
{t("save")}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CustomerEditDialog;
|
export default CustomerEditDialog;
|
||||||
|
|||||||
@@ -1,229 +1,243 @@
|
|||||||
import React, {useState} from 'react';
|
import React, { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
IconButton,
|
IconButton,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from "@mui/material";
|
||||||
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import {useTranslation} from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import {Item} from '../../components/Item.tsx';
|
import { Item } from "../../components/Item.tsx";
|
||||||
|
|
||||||
interface ItemImageDialogProps {
|
interface ItemImageDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
item: Item;
|
item: Item;
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
isFarmStationImage: boolean;
|
isFarmStationImage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ItemImageDialog({open, onClose, item, onSuccess, isFarmStationImage}: ItemImageDialogProps) {
|
export default function ItemImageDialog({
|
||||||
const {t} = useTranslation();
|
open,
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
onClose,
|
||||||
const [preview, setPreview] = useState<string | null>(null);
|
item,
|
||||||
const [loading, setLoading] = useState(false);
|
onSuccess,
|
||||||
const [error, setError] = useState<string | null>(null);
|
isFarmStationImage,
|
||||||
const [success, setSuccess] = useState(false);
|
}: ItemImageDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
const [preview, setPreview] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
|
||||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
// Validate file type
|
// Validate file type
|
||||||
if (!file.type.startsWith('image/')) {
|
if (!file.type.startsWith("image/")) {
|
||||||
setError('Please select a valid image file');
|
setError("Please select a valid image file");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate file size (5MB limit)
|
// Validate file size (5MB limit)
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
setError('File size must be less than 5MB');
|
setError("File size must be less than 5MB");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedFile(file);
|
setSelectedFile(file);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Create preview
|
// Create preview
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
setPreview(e.target?.result as string);
|
setPreview(e.target?.result as string);
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const convertFileToBase64 = (file: File): Promise<string> => {
|
const convertFileToBase64 = (file: File): Promise<string> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
const result = reader.result as string;
|
const result = reader.result as string;
|
||||||
// Remove the data URL prefix (e.g., "data:image/jpeg;base64,")
|
// Remove the data URL prefix (e.g., "data:image/jpeg;base64,")
|
||||||
const base64 = result.split(',')[1];
|
const base64 = result.split(",")[1];
|
||||||
resolve(base64);
|
resolve(base64);
|
||||||
};
|
};
|
||||||
reader.onerror = reject;
|
reader.onerror = reject;
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
if (!selectedFile) {
|
if (!selectedFile) {
|
||||||
setError('Please select an image file');
|
setError("Please select an image file");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const base64Image = await convertFileToBase64(selectedFile);
|
const base64Image = await convertFileToBase64(selectedFile);
|
||||||
|
|
||||||
const response = await fetch((isFarmStationImage ? 'http://localhost:8085/farm' : 'http://localhost:8085/image') + '?uuid=' + item.uuid, {
|
const response = await fetch(
|
||||||
method: 'PUT',
|
(isFarmStationImage
|
||||||
headers: {
|
? "http://localhost:8085/farm"
|
||||||
'Content-Type': 'application/json',
|
: "http://localhost:8085/image") +
|
||||||
},
|
"?uuid=" +
|
||||||
body: base64Image,
|
item.uuid,
|
||||||
});
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: base64Image,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error('Failed to upload image:', await response.text());
|
console.error("Failed to upload image:", await response.text());
|
||||||
}
|
}
|
||||||
|
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
|
|
||||||
// Auto-close dialog after 1.5 seconds
|
// Auto-close dialog after 1.5 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
handleClose();
|
handleClose();
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to upload image");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
} catch (err) {
|
const handleClose = () => {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to upload image');
|
setSelectedFile(null);
|
||||||
} finally {
|
setPreview(null);
|
||||||
setLoading(false);
|
setError(null);
|
||||||
}
|
setSuccess(false);
|
||||||
};
|
setLoading(false);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
return (
|
||||||
setSelectedFile(null);
|
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||||
setPreview(null);
|
<DialogTitle
|
||||||
setError(null);
|
sx={{
|
||||||
setSuccess(false);
|
display: "flex",
|
||||||
setLoading(false);
|
justifyContent: "space-between",
|
||||||
onClose();
|
alignItems: "center",
|
||||||
};
|
}}
|
||||||
|
>
|
||||||
|
{t("uploadImage")} - {item.name}
|
||||||
|
<IconButton onClick={handleClose} size="small">
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
return (
|
<DialogContent>
|
||||||
<Dialog
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, py: 1 }}>
|
||||||
open={open}
|
{/* Item Info */}
|
||||||
onClose={handleClose}
|
<Box>
|
||||||
maxWidth="sm"
|
<Typography variant="body2" color="textSecondary">
|
||||||
fullWidth
|
{t("item")}: {item.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
UUID: {item.uuid}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="error">
|
||||||
|
{t("imageUploadNotice")}
|
||||||
|
</Typography>
|
||||||
|
{isFarmStationImage && (
|
||||||
|
<Typography variant="body2" color="error">
|
||||||
|
{t("imageUploadNoticeFs")}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* File Upload */}
|
||||||
|
<Box>
|
||||||
|
<input
|
||||||
|
accept="image/*"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
id="image-upload"
|
||||||
|
type="file"
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
/>
|
||||||
|
<label htmlFor="image-upload">
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
component="span"
|
||||||
|
startIcon={<CloudUploadIcon />}
|
||||||
|
fullWidth
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
>
|
||||||
|
{selectedFile ? selectedFile.name : t("selectImage")}
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Image Preview */}
|
||||||
|
{preview && (
|
||||||
|
<Box sx={{ textAlign: "center" }}>
|
||||||
|
<img
|
||||||
|
src={preview}
|
||||||
|
alt="Preview"
|
||||||
|
style={{
|
||||||
|
maxWidth: "100%",
|
||||||
|
maxHeight: "300px",
|
||||||
|
objectFit: "contain",
|
||||||
|
border: "1px solid #ddd",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
|
||||||
|
{selectedFile?.name} (
|
||||||
|
{(selectedFile?.size || 0 / 1024).toFixed(1)} KB)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" onClose={() => setError(null)}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Success Message */}
|
||||||
|
{success && (
|
||||||
|
<Alert severity="success">{t("imageUploadedSuccessfully")}</Alert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleClose} disabled={loading}>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleUpload}
|
||||||
|
variant="contained"
|
||||||
|
disabled={!selectedFile || loading}
|
||||||
|
startIcon={loading ? <CircularProgress size={20} /> : undefined}
|
||||||
>
|
>
|
||||||
<DialogTitle sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
{loading ? t("uploading") : t("upload")}
|
||||||
{t('uploadImage')} - {item.name}
|
</Button>
|
||||||
<IconButton onClick={handleClose} size="small">
|
</DialogActions>
|
||||||
<CloseIcon/>
|
</Dialog>
|
||||||
</IconButton>
|
);
|
||||||
</DialogTitle>
|
}
|
||||||
|
|
||||||
<DialogContent>
|
|
||||||
<Box sx={{display: 'flex', flexDirection: 'column', gap: 2, py: 1}}>
|
|
||||||
{/* Item Info */}
|
|
||||||
<Box>
|
|
||||||
<Typography variant="body2" color="textSecondary">
|
|
||||||
{t('item')}: {item.name}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="textSecondary">
|
|
||||||
UUID: {item.uuid}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="error">
|
|
||||||
{t('imageUploadNotice')}
|
|
||||||
</Typography>
|
|
||||||
{isFarmStationImage && <Typography variant="body2" color="error">
|
|
||||||
{t('imageUploadNoticeFs')}
|
|
||||||
</Typography>}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* File Upload */}
|
|
||||||
<Box>
|
|
||||||
<input
|
|
||||||
accept="image/*"
|
|
||||||
style={{display: 'none'}}
|
|
||||||
id="image-upload"
|
|
||||||
type="file"
|
|
||||||
onChange={handleFileSelect}
|
|
||||||
/>
|
|
||||||
<label htmlFor="image-upload">
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
component="span"
|
|
||||||
startIcon={<CloudUploadIcon/>}
|
|
||||||
fullWidth
|
|
||||||
sx={{mb: 2}}
|
|
||||||
>
|
|
||||||
{selectedFile ? selectedFile.name : t('selectImage')}
|
|
||||||
</Button>
|
|
||||||
</label>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Image Preview */}
|
|
||||||
{preview && (
|
|
||||||
<Box sx={{textAlign: 'center'}}>
|
|
||||||
<img
|
|
||||||
src={preview}
|
|
||||||
alt="Preview"
|
|
||||||
style={{
|
|
||||||
maxWidth: '100%',
|
|
||||||
maxHeight: '300px',
|
|
||||||
objectFit: 'contain',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Typography variant="caption" display="block" sx={{mt: 1}}>
|
|
||||||
{selectedFile?.name} ({(selectedFile?.size || 0 / 1024).toFixed(1)} KB)
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
{error && (
|
|
||||||
<Alert severity="error" onClose={() => setError(null)}>
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Success Message */}
|
|
||||||
{success && (
|
|
||||||
<Alert severity="success">
|
|
||||||
{t('imageUploadedSuccessfully')}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</DialogContent>
|
|
||||||
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={handleClose} disabled={loading}>
|
|
||||||
{t('cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleUpload}
|
|
||||||
variant="contained"
|
|
||||||
disabled={!selectedFile || loading}
|
|
||||||
startIcon={loading ? <CircularProgress size={20}/> : undefined}
|
|
||||||
>
|
|
||||||
{loading ? t('uploading') : t('upload')}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,256 +3,284 @@ import DeleteIcon from "@mui/icons-material/Delete";
|
|||||||
import EditIcon from "@mui/icons-material/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
import { Box, Button, IconButton, Toolbar, useTheme } from "@mui/material";
|
import { Box, Button, IconButton, Toolbar, useTheme } from "@mui/material";
|
||||||
import { Gauge, gaugeClasses } from "@mui/x-charts";
|
import { Gauge, gaugeClasses } from "@mui/x-charts";
|
||||||
import { DataGrid, GridColDef, GridRowId, GridRowSelectionModel } from "@mui/x-data-grid";
|
import {
|
||||||
|
DataGrid,
|
||||||
|
GridColDef,
|
||||||
|
GridRowId,
|
||||||
|
GridRowSelectionModel,
|
||||||
|
} from "@mui/x-data-grid";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Item from "../../components/Item";
|
import Item from "../../components/Item";
|
||||||
import { mapValueToColor } from "../../util/ColorUtil.tsx";
|
import { mapValueToColor } from "../../util/ColorUtil.tsx";
|
||||||
import { useAccount } from "../AccountProvider.tsx";
|
import {
|
||||||
import { deleteItemQuery, fetchItems, updateItemAdmin } from "../query/Queries.tsx";
|
deleteItemQuery,
|
||||||
|
fetchItemList,
|
||||||
|
updateItemAdmin,
|
||||||
|
} from "../query/Queries.tsx";
|
||||||
import ItemImageDialog from "./ItemImageDialog.tsx";
|
import ItemImageDialog from "./ItemImageDialog.tsx";
|
||||||
import NewItemDialog from "./NewItemDialog.tsx";
|
import NewItemDialog from "./NewItemDialog.tsx";
|
||||||
|
|
||||||
export default function ItemsInfo() {
|
export default function ItemsInfo({ itemList }: { itemList: Item[] }) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [rows, setRows] = useState<Item[]>([]);
|
const [rows, setRows] = useState<Item[]>([]);
|
||||||
const [selectedRows, setSelectedRows] = useState<Set<GridRowId>>(new Set());
|
const [selectedRows, setSelectedRows] = useState<Set<GridRowId>>(new Set());
|
||||||
|
|
||||||
const [editImageDialog, setEditImageDialog] = useState(false);
|
const [editImageDialog, setEditImageDialog] = useState(false);
|
||||||
const [newItemDialog, setNewItemDialog] = useState(false);
|
const [newItemDialog, setNewItemDialog] = useState(false);
|
||||||
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
|
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
|
||||||
const [isFarmStationImage, setIsFarmStationImage] = useState(false);
|
const [isFarmStationImage, setIsFarmStationImage] = useState(false);
|
||||||
|
|
||||||
|
function handleImageEdit(item: Item) {
|
||||||
|
setIsFarmStationImage(false);
|
||||||
|
setSelectedItem(item);
|
||||||
|
setEditImageDialog(true);
|
||||||
|
console.log("IconEdit", item);
|
||||||
|
}
|
||||||
|
|
||||||
function handleImageEdit(item: Item) {
|
function handleFarmImageEdit(item: Item) {
|
||||||
setIsFarmStationImage(false);
|
setIsFarmStationImage(true);
|
||||||
setSelectedItem(item);
|
setSelectedItem(item);
|
||||||
setEditImageDialog(true);
|
setEditImageDialog(true);
|
||||||
console.log("IconEdit", item);
|
console.log("IconEdit", item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddItem() {
|
||||||
|
setNewItemDialog(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (itemList) {
|
||||||
|
setRows(itemList);
|
||||||
}
|
}
|
||||||
|
}, [itemList]);
|
||||||
|
|
||||||
function handleFarmImageEdit(item: Item) {
|
const handleSelectionChange = (newSelection: GridRowSelectionModel) => {
|
||||||
setIsFarmStationImage(true);
|
setSelectedRows(newSelection.ids);
|
||||||
setSelectedItem(item);
|
};
|
||||||
setEditImageDialog(true);
|
|
||||||
console.log("IconEdit", item);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const deleteItem = useMutation({
|
||||||
|
mutationFn: (uuid: string) => deleteItemQuery(uuid),
|
||||||
|
});
|
||||||
|
|
||||||
function handleAddItem() {
|
const handleDeleteSelected = async () => {
|
||||||
setNewItemDialog(true);
|
await Promise.all(
|
||||||
}
|
Array.from(selectedRows).map(async (row) => {
|
||||||
|
await deleteItem.mutateAsync(
|
||||||
const { user: loginData } = useAccount();
|
rows.find((item) => item.id === row)?.uuid || "",
|
||||||
|
|
||||||
const { data } = useQuery({
|
|
||||||
queryKey: ["fetchItems", loginData],
|
|
||||||
queryFn: () => fetchItems(),
|
|
||||||
retry: 3,
|
|
||||||
retryDelay: 1000,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
setRows(data);
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const handleSelectionChange = (newSelection: GridRowSelectionModel) => {
|
|
||||||
setSelectedRows(newSelection.ids);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteItem = useMutation({
|
|
||||||
mutationFn: (uuid: string) =>
|
|
||||||
deleteItemQuery(uuid),
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleDeleteSelected = async () => {
|
|
||||||
await Promise.all(
|
|
||||||
Array.from(selectedRows).map(async (row) => {
|
|
||||||
await deleteItem.mutateAsync(rows.find(item => item.id === row)?.uuid || "");
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
}),
|
||||||
setRows(rows.filter((row) => !selectedRows.has(row.id)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateItem = useMutation({
|
|
||||||
mutationFn: (item: Item) =>
|
|
||||||
updateItemAdmin(item),
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleRowUpdate = async (updatedRow: Item) => {
|
|
||||||
setRows(rows.map(row => row.id === updatedRow.id ? updatedRow : row));
|
|
||||||
await updateItem.mutateAsync(updatedRow);
|
|
||||||
return updatedRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns: GridColDef<(typeof rows)[number]>[] = [
|
|
||||||
{ field: 'id', headerName: 'ID', width: 60 },
|
|
||||||
{
|
|
||||||
field: 'uuid',
|
|
||||||
headerName: t('uuid'),
|
|
||||||
type: "string",
|
|
||||||
width: 120,
|
|
||||||
editable: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'name',
|
|
||||||
headerName: t('name'),
|
|
||||||
width: 200,
|
|
||||||
editable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'category',
|
|
||||||
headerName: t('category'),
|
|
||||||
width: 150,
|
|
||||||
editable: true,
|
|
||||||
valueFormatter: (val) => t(val),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'description',
|
|
||||||
headerName: t('description'),
|
|
||||||
width: 150,
|
|
||||||
editable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'price100',
|
|
||||||
headerName: t('price100€'),
|
|
||||||
width: 100,
|
|
||||||
editable: true,
|
|
||||||
type: 'number',
|
|
||||||
valueFormatter: (val) => (val / 100).toFixed(2),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'discount100',
|
|
||||||
headerName: t('discount100'),
|
|
||||||
width: 120,
|
|
||||||
editable: true,
|
|
||||||
type: 'number'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'stock',
|
|
||||||
headerName: t('stock'),
|
|
||||||
width: 100,
|
|
||||||
editable: true,
|
|
||||||
type: 'number',
|
|
||||||
renderCell: params => <Gauge value={Math.min(params.row.stock, params.row.stockExpected)} valueMin={0}
|
|
||||||
valueMax={params.row.stockExpected} startAngle={-90} endAngle={90} sx={{
|
|
||||||
[`& .${gaugeClasses.valueArc}`]: {
|
|
||||||
fill: () => {
|
|
||||||
return mapValueToColor(0, params.row.stockExpected, params.row.stock)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}} text={() => `${params.row.stock}`} />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'rating',
|
|
||||||
headerName: t('rating'),
|
|
||||||
width: 100,
|
|
||||||
editable: false, //the rating is averaged from ratings
|
|
||||||
type: 'number',
|
|
||||||
renderCell: params => <Gauge value={Math.min(params.row.rating, 10)} valueMin={0} valueMax={10}
|
|
||||||
startAngle={-90} endAngle={90} sx={{
|
|
||||||
[`& .${gaugeClasses.valueArc}`]: {
|
|
||||||
fill: () => {
|
|
||||||
return mapValueToColor(0, 10, params.row.rating)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}} text={() => `${params.row.rating.toFixed(2)}`} />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "actualPrice",
|
|
||||||
headerName: t('actualPrice'),
|
|
||||||
width: 90,
|
|
||||||
editable: false,
|
|
||||||
valueGetter: (_, row) => (row.price100 / 100 * ((100 - row.discount100) / 100)).toFixed(2)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'images',
|
|
||||||
headerName: t('images'),
|
|
||||||
width: 90,
|
|
||||||
editable: false,
|
|
||||||
renderCell: params => <IconButton onClick={() => handleImageEdit(params.row)}> <EditIcon /> </IconButton>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'farmImage',
|
|
||||||
headerName: t('fsImage'),
|
|
||||||
width: 90,
|
|
||||||
editable: false,
|
|
||||||
renderCell: params => <IconButton onClick={() => handleFarmImageEdit(params.row)}> <EditIcon />
|
|
||||||
</IconButton>,
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
className="page-table"
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
color: theme.palette.text.secondary
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DataGrid
|
|
||||||
rows={rows}
|
|
||||||
columns={columns}
|
|
||||||
initialState={{}}
|
|
||||||
checkboxSelection
|
|
||||||
disableRowSelectionOnClick
|
|
||||||
onRowSelectionModelChange={handleSelectionChange}
|
|
||||||
slots={{
|
|
||||||
toolbar: () => (
|
|
||||||
<Toolbar>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="error"
|
|
||||||
startIcon={<DeleteIcon />}
|
|
||||||
onClick={handleDeleteSelected}
|
|
||||||
disabled={selectedRows.size === 0}
|
|
||||||
sx={{
|
|
||||||
marginRight: 1
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('deleteProduct')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
startIcon={<AddIcon/>}
|
|
||||||
onClick={handleAddItem}
|
|
||||||
sx={{
|
|
||||||
marginRight: 1
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('addProduct')}
|
|
||||||
</Button>
|
|
||||||
</Toolbar>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
showToolbar
|
|
||||||
processRowUpdate={handleRowUpdate}
|
|
||||||
/>
|
|
||||||
{selectedItem && (
|
|
||||||
<ItemImageDialog
|
|
||||||
open={editImageDialog}
|
|
||||||
onClose={() => setEditImageDialog(false)}
|
|
||||||
item={selectedItem}
|
|
||||||
onSuccess={() => {
|
|
||||||
// Refresh data or update UI
|
|
||||||
console.log('Image uploaded successfully');
|
|
||||||
}}
|
|
||||||
isFarmStationImage={isFarmStationImage}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<NewItemDialog
|
|
||||||
open={newItemDialog}
|
|
||||||
onClose={() => setNewItemDialog(false)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
setRows(rows.filter((row) => !selectedRows.has(row.id)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateItem = useMutation({
|
||||||
|
mutationFn: (item: Item) => updateItemAdmin(item),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRowUpdate = async (updatedRow: Item) => {
|
||||||
|
setRows(rows.map((row) => (row.id === updatedRow.id ? updatedRow : row)));
|
||||||
|
await updateItem.mutateAsync(updatedRow);
|
||||||
|
return updatedRow;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: GridColDef<(typeof rows)[number]>[] = [
|
||||||
|
{ field: "id", headerName: "ID", width: 60 },
|
||||||
|
{
|
||||||
|
field: "uuid",
|
||||||
|
headerName: t("uuid"),
|
||||||
|
type: "string",
|
||||||
|
width: 120,
|
||||||
|
editable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "name",
|
||||||
|
headerName: t("name"),
|
||||||
|
width: 200,
|
||||||
|
editable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "category",
|
||||||
|
headerName: t("category"),
|
||||||
|
width: 150,
|
||||||
|
editable: true,
|
||||||
|
valueFormatter: (val) => t(val),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "description",
|
||||||
|
headerName: t("description"),
|
||||||
|
width: 150,
|
||||||
|
editable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "price100",
|
||||||
|
headerName: t("price100€"),
|
||||||
|
width: 100,
|
||||||
|
editable: true,
|
||||||
|
type: "number",
|
||||||
|
valueFormatter: (val) => (val / 100).toFixed(2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "discount100",
|
||||||
|
headerName: t("discount100"),
|
||||||
|
width: 120,
|
||||||
|
editable: true,
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "stock",
|
||||||
|
headerName: t("stock"),
|
||||||
|
width: 100,
|
||||||
|
editable: true,
|
||||||
|
type: "number",
|
||||||
|
renderCell: (params) => (
|
||||||
|
<Gauge
|
||||||
|
value={Math.min(params.row.stock, params.row.stockExpected)}
|
||||||
|
valueMin={0}
|
||||||
|
valueMax={params.row.stockExpected}
|
||||||
|
startAngle={-90}
|
||||||
|
endAngle={90}
|
||||||
|
sx={{
|
||||||
|
[`& .${gaugeClasses.valueArc}`]: {
|
||||||
|
fill: () => {
|
||||||
|
return mapValueToColor(
|
||||||
|
0,
|
||||||
|
params.row.stockExpected,
|
||||||
|
params.row.stock,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
text={() => `${params.row.stock}`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "rating",
|
||||||
|
headerName: t("rating"),
|
||||||
|
width: 100,
|
||||||
|
editable: false, //the rating is averaged from ratings
|
||||||
|
type: "number",
|
||||||
|
renderCell: (params) => (
|
||||||
|
<Gauge
|
||||||
|
value={Math.min(params.row.rating, 10)}
|
||||||
|
valueMin={0}
|
||||||
|
valueMax={10}
|
||||||
|
startAngle={-90}
|
||||||
|
endAngle={90}
|
||||||
|
sx={{
|
||||||
|
[`& .${gaugeClasses.valueArc}`]: {
|
||||||
|
fill: () => {
|
||||||
|
return mapValueToColor(0, 10, params.row.rating);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
text={() => `${params.row.rating.toFixed(2)}`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "actualPrice",
|
||||||
|
headerName: t("actualPrice"),
|
||||||
|
width: 90,
|
||||||
|
editable: false,
|
||||||
|
valueGetter: (_, row) =>
|
||||||
|
((row.price100 / 100) * ((100 - row.discount100) / 100)).toFixed(2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "images",
|
||||||
|
headerName: t("images"),
|
||||||
|
width: 90,
|
||||||
|
editable: false,
|
||||||
|
renderCell: (params) => (
|
||||||
|
<IconButton onClick={() => handleImageEdit(params.row)}>
|
||||||
|
{" "}
|
||||||
|
<EditIcon />{" "}
|
||||||
|
</IconButton>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "farmImage",
|
||||||
|
headerName: t("fsImage"),
|
||||||
|
width: 90,
|
||||||
|
editable: false,
|
||||||
|
renderCell: (params) => (
|
||||||
|
<IconButton onClick={() => handleFarmImageEdit(params.row)}>
|
||||||
|
{" "}
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className="page-table"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DataGrid
|
||||||
|
rows={rows}
|
||||||
|
columns={columns}
|
||||||
|
initialState={{}}
|
||||||
|
checkboxSelection
|
||||||
|
disableRowSelectionOnClick
|
||||||
|
onRowSelectionModelChange={handleSelectionChange}
|
||||||
|
slots={{
|
||||||
|
toolbar: () => (
|
||||||
|
<Toolbar>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
startIcon={<DeleteIcon />}
|
||||||
|
onClick={handleDeleteSelected}
|
||||||
|
disabled={selectedRows.size === 0}
|
||||||
|
sx={{
|
||||||
|
marginRight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("deleteProduct")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
onClick={handleAddItem}
|
||||||
|
sx={{
|
||||||
|
marginRight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("addProduct")}
|
||||||
|
</Button>
|
||||||
|
</Toolbar>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
showToolbar
|
||||||
|
processRowUpdate={handleRowUpdate}
|
||||||
|
/>
|
||||||
|
{selectedItem && (
|
||||||
|
<ItemImageDialog
|
||||||
|
open={editImageDialog}
|
||||||
|
onClose={() => setEditImageDialog(false)}
|
||||||
|
item={selectedItem}
|
||||||
|
onSuccess={() => {
|
||||||
|
// Refresh data or update UI
|
||||||
|
console.log("Image uploaded successfully");
|
||||||
|
}}
|
||||||
|
isFarmStationImage={isFarmStationImage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<NewItemDialog
|
||||||
|
open={newItemDialog}
|
||||||
|
onClose={() => setNewItemDialog(false)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,166 +1,164 @@
|
|||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
IconButton,
|
IconButton,
|
||||||
TextField
|
TextField,
|
||||||
} from '@mui/material';
|
} from "@mui/material";
|
||||||
import {useMutation} from '@tanstack/react-query';
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
import {useTranslation} from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import {Item} from '../../components/Item';
|
import { Item } from "../../components/Item";
|
||||||
import {submitItem} from '../query/Queries';
|
import { submitItem } from "../query/Queries";
|
||||||
|
|
||||||
interface NewItemDialogProps {
|
interface NewItemDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NewItemDialog({open, onClose}: NewItemDialogProps) {
|
export default function NewItemDialog({ open, onClose }: NewItemDialogProps) {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [item, setItem] = useState<Item>({
|
const [item, setItem] = useState<Item>({
|
||||||
id: 0,
|
id: 0,
|
||||||
uuid: "",
|
uuid: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
price100: 0,
|
price100: 0,
|
||||||
stock: 0,
|
stock: 0,
|
||||||
stockExpected: 1,
|
stockExpected: 1,
|
||||||
category: "",
|
category: "",
|
||||||
rating: -1,
|
rating: -1,
|
||||||
discount100: 0,
|
discount100: 0,
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
setSuccess(false);
|
setSuccess(false);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setItem({...item, [e.target.name]: e.target.value});
|
setItem({ ...item, [e.target.name]: e.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveItem = useMutation({
|
const saveItem = useMutation({
|
||||||
mutationFn: (item: Item) =>
|
mutationFn: (item: Item) => submitItem(item),
|
||||||
submitItem(item),
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setSuccess(false);
|
setSuccess(false);
|
||||||
|
|
||||||
await saveItem.mutateAsync(item)
|
await saveItem.mutateAsync(item);
|
||||||
|
|
||||||
onClose(); // Close the dialog after saving
|
onClose(); // Close the dialog after saving
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||||
open={open}
|
<DialogTitle
|
||||||
onClose={handleClose}
|
sx={{
|
||||||
maxWidth="sm"
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("createNewItem")}
|
||||||
|
<IconButton onClick={handleClose} size="small">
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, py: 1 }}>
|
||||||
|
{/* Name, Kategorie, Beschreibung, Preis, Rabatt, Bestand, Bestand erwartet */}
|
||||||
|
<TextField
|
||||||
|
label={t("name")}
|
||||||
|
name="name"
|
||||||
|
value={item.name}
|
||||||
|
onChange={handleChange}
|
||||||
fullWidth
|
fullWidth
|
||||||
>
|
/>
|
||||||
<DialogTitle sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
<TextField
|
||||||
{t('createNewItem')}
|
label={t("category")}
|
||||||
<IconButton onClick={handleClose} size="small">
|
name="category"
|
||||||
<CloseIcon/>
|
value={item.category}
|
||||||
</IconButton>
|
onChange={handleChange}
|
||||||
</DialogTitle>
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label={t("description")}
|
||||||
|
name="description"
|
||||||
|
value={item.description}
|
||||||
|
onChange={handleChange}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label={t("price100")}
|
||||||
|
name="price100"
|
||||||
|
value={item.price100}
|
||||||
|
onChange={handleChange}
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label={t("discount100")}
|
||||||
|
name="discount100"
|
||||||
|
value={item.discount100}
|
||||||
|
onChange={handleChange}
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label={t("stockExpected")}
|
||||||
|
name="stockExpected"
|
||||||
|
value={item.stockExpected}
|
||||||
|
onChange={handleChange}
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label={t("stock")}
|
||||||
|
name="stock"
|
||||||
|
value={item.stock}
|
||||||
|
onChange={handleChange}
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
|
||||||
<DialogContent>
|
{/* Error Message */}
|
||||||
<Box sx={{display: 'flex', flexDirection: 'column', gap: 2, py: 1}}>
|
{error && (
|
||||||
{/* Name, Kategorie, Beschreibung, Preis, Rabatt, Bestand, Bestand erwartet */}
|
<Alert severity="error" onClose={() => setError(null)}>
|
||||||
<TextField
|
{error}
|
||||||
label={t("name")}
|
</Alert>
|
||||||
name="name"
|
)}
|
||||||
value={item.name}
|
|
||||||
onChange={handleChange}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label={t("category")}
|
|
||||||
name="category"
|
|
||||||
value={item.category}
|
|
||||||
onChange={handleChange}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label={t("description")}
|
|
||||||
name="description"
|
|
||||||
value={item.description}
|
|
||||||
onChange={handleChange}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label={t("price100")}
|
|
||||||
name="price100"
|
|
||||||
value={item.price100}
|
|
||||||
onChange={handleChange}
|
|
||||||
fullWidth
|
|
||||||
type='number'
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label={t("discount100")}
|
|
||||||
name="discount100"
|
|
||||||
value={item.discount100}
|
|
||||||
onChange={handleChange}
|
|
||||||
fullWidth
|
|
||||||
type='number'
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label={t("stockExpected")}
|
|
||||||
name="stockExpected"
|
|
||||||
value={item.stockExpected}
|
|
||||||
onChange={handleChange}
|
|
||||||
fullWidth
|
|
||||||
type='number'
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label={t("stock")}
|
|
||||||
name="stock"
|
|
||||||
value={item.stock}
|
|
||||||
onChange={handleChange}
|
|
||||||
fullWidth
|
|
||||||
type='number'
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Error Message */}
|
{/* Success Message */}
|
||||||
{error && (
|
{success && (
|
||||||
<Alert severity="error" onClose={() => setError(null)}>
|
<Alert severity="success">{t("itemCreatedSuccessfully")}</Alert>
|
||||||
{error}
|
)}
|
||||||
</Alert>
|
</Box>
|
||||||
)}
|
</DialogContent>
|
||||||
|
|
||||||
{/* Success Message */}
|
<DialogActions>
|
||||||
{success && (
|
<Button onClick={handleSave} disabled={loading}>
|
||||||
<Alert severity="success">
|
{t("save")}
|
||||||
{t('itemCreatedSuccessfully')}
|
</Button>
|
||||||
</Alert>
|
<Button onClick={handleClose} disabled={loading}>
|
||||||
)}
|
{t("cancel")}
|
||||||
</Box>
|
</Button>
|
||||||
</DialogContent>
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
<DialogActions>
|
);
|
||||||
<Button onClick={handleSave} disabled={loading}>
|
}
|
||||||
{t('save')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleClose} disabled={loading}>
|
|
||||||
{t('cancel')}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,231 +1,251 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Divider,
|
Divider,
|
||||||
List,
|
List,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
Stack,
|
Stack,
|
||||||
Typography,
|
Typography,
|
||||||
useTheme
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {useMutation, useQuery} from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import React, {PropsWithChildren, useState} from "react";
|
import React, { PropsWithChildren, useState } from "react";
|
||||||
import {DndProvider, useDrag, useDrop} from 'react-dnd';
|
import { DndProvider, useDrag, useDrop } from "react-dnd";
|
||||||
import {HTML5Backend} from 'react-dnd-html5-backend';
|
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import OrderType, {OrderPatch, OrderStatusEnum} from "../../components/Order";
|
import OrderType, { OrderPatch, OrderStatusEnum } from "../../components/Order";
|
||||||
import {useAccount} from "../AccountProvider";
|
import { useAccount } from "../AccountProvider";
|
||||||
import {fetchOrdersAdmin, orderPatch} from "../query/Queries";
|
import { fetchOrdersAdmin, orderPatch } from "../query/Queries";
|
||||||
|
import Item from "../../components/Item";
|
||||||
|
|
||||||
// The order in which the statuses are displayed
|
// The order in which the statuses are displayed
|
||||||
const statusOrder: OrderStatusEnum[] = [
|
const statusOrder: OrderStatusEnum[] = [
|
||||||
OrderStatusEnum.CANCELLED,
|
OrderStatusEnum.CANCELLED,
|
||||||
OrderStatusEnum.ISSUES,
|
OrderStatusEnum.ISSUES,
|
||||||
OrderStatusEnum.ORDERED,
|
OrderStatusEnum.ORDERED,
|
||||||
OrderStatusEnum.IN_PROGRESS,
|
OrderStatusEnum.IN_PROGRESS,
|
||||||
OrderStatusEnum.DELIVERED
|
OrderStatusEnum.DELIVERED,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const OrderCard: React.FC<{ order: OrderType; onClick: () => void }> = ({
|
||||||
|
order,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const OrderCard: React.FC<{ order: OrderType; onClick: () => void }> = ({order, onClick}) => {
|
const [{ isDragging }, drag] = useDrag(() => ({
|
||||||
|
type: "order",
|
||||||
|
item: { id: order.id },
|
||||||
|
collect: (monitor) => ({
|
||||||
|
isDragging: !!monitor.isDragging(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
const {t} = useTranslation();
|
return (
|
||||||
|
<div ref={drag} style={{ opacity: isDragging ? 0.5 : 1, marginBottom: 8 }}>
|
||||||
const [{isDragging}, drag] = useDrag(() => ({
|
<Card
|
||||||
type: 'order',
|
elevation={4}
|
||||||
item: {id: order.id},
|
sx={{
|
||||||
collect: (monitor) => ({
|
m: 1,
|
||||||
isDragging: !!monitor.isDragging(),
|
}}
|
||||||
}),
|
>
|
||||||
}));
|
<CardContent onClick={onClick}>
|
||||||
|
<Typography gutterBottom variant="h5" component="div">
|
||||||
return (
|
Order: {order.id}
|
||||||
<div ref={drag} style={{opacity: isDragging ? 0.5 : 1, marginBottom: 8}}>
|
</Typography>
|
||||||
<Card elevation={4} sx={{
|
<Typography>
|
||||||
m: 1
|
{t("date") + ": " + new Date(order.time).toUTCString()}
|
||||||
}}>
|
</Typography>
|
||||||
<CardContent onClick={onClick}>
|
<Typography>
|
||||||
<Typography gutterBottom variant="h5" component="div">
|
{t("total") + ": " + (order.total / 100).toFixed(2) + " €"}
|
||||||
Order: {order.id}
|
</Typography>
|
||||||
</Typography>
|
</CardContent>
|
||||||
<Typography>
|
</Card>
|
||||||
{t('date') + ": " + new Date(order.time).toUTCString()}
|
</div>
|
||||||
</Typography>
|
);
|
||||||
<Typography>
|
|
||||||
{t('total') + ": " + (order.total / 100).toFixed(2) + " €"}
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Column: React.FC<PropsWithChildren<{
|
const Column: React.FC<
|
||||||
|
PropsWithChildren<{
|
||||||
status: OrderStatusEnum;
|
status: OrderStatusEnum;
|
||||||
onDrop: (id: number, status: OrderStatusEnum) => void
|
onDrop: (id: number, status: OrderStatusEnum) => void;
|
||||||
}>> = ({status, onDrop, children}) => {
|
}>
|
||||||
|
> = ({ status, onDrop, children }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
const {t} = useTranslation();
|
const [{ isOver }, drop] = useDrop(() => ({
|
||||||
const theme = useTheme();
|
accept: "order",
|
||||||
|
drop: (item: { id: number }) => onDrop(item.id, status),
|
||||||
|
collect: (monitor) => ({
|
||||||
|
isOver: !!monitor.isOver(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
const [{isOver}, drop] = useDrop(() => ({
|
return (
|
||||||
accept: 'order',
|
<div
|
||||||
drop: (item: { id: number }) => onDrop(item.id, status),
|
ref={drop}
|
||||||
collect: (monitor) => ({
|
style={{
|
||||||
isOver: !!monitor.isOver(),
|
flex: 1,
|
||||||
}),
|
backgroundColor: isOver
|
||||||
}));
|
? theme.palette.background.paper
|
||||||
|
: "transparent",
|
||||||
return (
|
minWidth: 300,
|
||||||
<div ref={drop} style={{
|
maxWidth: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
minHeight: "100%",
|
||||||
|
marginTop: 2,
|
||||||
|
marginLeft: 1,
|
||||||
|
height: "80vh",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
elevation={1}
|
||||||
|
>
|
||||||
|
<CardContent
|
||||||
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: isOver ? theme.palette.background.paper : 'transparent',
|
display: "flex",
|
||||||
minWidth: 300,
|
flexDirection: "column",
|
||||||
maxWidth: 400
|
overflowY: "auto",
|
||||||
}}>
|
height: "100%",
|
||||||
<Card sx={{
|
}}
|
||||||
minHeight: '100%',
|
>
|
||||||
marginTop: 2,
|
<Typography variant="h6">{t(status)}</Typography>
|
||||||
marginLeft: 1,
|
<div style={{ flex: 1, overflowY: "auto" }}>{children}</div>
|
||||||
height: '80vh',
|
</CardContent>
|
||||||
display: 'flex',
|
</Card>
|
||||||
flexDirection: 'column'
|
</div>
|
||||||
}} elevation={1}>
|
);
|
||||||
<CardContent sx={{
|
|
||||||
flex: 1,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
overflowY: 'auto',
|
|
||||||
height: '100%',
|
|
||||||
}}>
|
|
||||||
<Typography variant="h6">{t(status)}</Typography>
|
|
||||||
<div style={{flex: 1, overflowY: 'auto'}}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditOrder: React.FC<{ open: boolean; order: OrderType | null; onClose: () => void }> = ({
|
const EditOrder: React.FC<{
|
||||||
open,
|
open: boolean;
|
||||||
order,
|
order: OrderType | null;
|
||||||
onClose
|
articleNameMapping: (uuid: string) => string;
|
||||||
}) => {
|
onClose: () => void;
|
||||||
|
}> = ({ open, order, articleNameMapping, onClose }) => {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (order === null)
|
if (order === null) return "";
|
||||||
return "";
|
return (
|
||||||
return (
|
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
<DialogTitle>{`${t(order.status)} #${order?.id}`}</DialogTitle>
|
||||||
<DialogTitle>
|
<DialogContent dividers>
|
||||||
{`${t(order.status)} #${order?.id}`}
|
{order && (
|
||||||
</DialogTitle>
|
<Stack spacing={2}>
|
||||||
<DialogContent dividers>
|
<Typography variant="subtitle1">{`${t("orderDate")}: ${new Date(order.time).toDateString()}`}</Typography>
|
||||||
{order && (
|
<Divider />
|
||||||
<Stack spacing={2}>
|
<Typography variant="subtitle2">{t("orderedItems")}:</Typography>
|
||||||
<Typography
|
<List dense>
|
||||||
variant="subtitle1">{`${t('orderDate')}: ${new Date(order.time).toDateString()}`}</Typography>
|
{order.orderItems.map((item, idx) => (
|
||||||
<Divider/>
|
<ListItemText
|
||||||
<Typography variant="subtitle2">{t('orderedItems')}:</Typography>
|
key={idx}
|
||||||
<List dense>
|
primary={`${item.amount}x ${articleNameMapping(item.article)}`}
|
||||||
{order.orderItems.map((item, idx) => (
|
/>
|
||||||
<ListItemText
|
))}
|
||||||
key={idx}
|
</List>
|
||||||
primary={`${item.article} x${item.amount}`}
|
<Divider />
|
||||||
/>
|
<Typography variant="h6">{`${t("sum")}: ${(order.total / 100).toFixed(2)} €`}</Typography>
|
||||||
))}
|
</Stack>
|
||||||
</List>
|
)}
|
||||||
<Divider/>
|
</DialogContent>
|
||||||
<Typography variant="h6">{`${t('sum')}: ${(order.total / 100).toFixed(2)} €`}</Typography>
|
<DialogActions>
|
||||||
</Stack>
|
<Button onClick={onClose}>{t("close")}</Button>
|
||||||
)}
|
</DialogActions>
|
||||||
</DialogContent>
|
</Dialog>
|
||||||
<DialogActions>
|
);
|
||||||
<Button onClick={onClose}>{t('close')}</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function OrdersInfo() {
|
export default function OrdersInfo({ itemList }: { itemList: Item[] }) {
|
||||||
const [editOrder, setEditOrder] = useState<OrderType | null>(null);
|
const [editOrder, setEditOrder] = useState<OrderType | null>(null);
|
||||||
const [openSnackbar, setOpenSnackbar] = useState(false);
|
const [openSnackbar, setOpenSnackbar] = useState(false);
|
||||||
|
|
||||||
const {user: loginData} = useAccount();
|
const { user: loginData } = useAccount();
|
||||||
|
|
||||||
const {data, refetch, isLoading} = useQuery({
|
const { data, refetch, isLoading } = useQuery<OrderType[]>({
|
||||||
queryKey: ["fetchOrdersAdmin", loginData],
|
queryKey: ["fetchOrdersAdmin", loginData],
|
||||||
queryFn: () => fetchOrdersAdmin(loginData ? loginData : {
|
queryFn: () =>
|
||||||
email: "",
|
fetchOrdersAdmin(
|
||||||
password: "",
|
loginData
|
||||||
session: "",
|
? loginData
|
||||||
customerId: -1,
|
: {
|
||||||
isAdmin: false
|
email: "",
|
||||||
}),
|
password: "",
|
||||||
retry: 3,
|
session: "",
|
||||||
retryDelay: 1000,
|
customerId: -1,
|
||||||
});
|
isAdmin: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
retry: 3,
|
||||||
|
retryDelay: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
const patchOrderMutation = useMutation({
|
const patchOrderMutation = useMutation({
|
||||||
mutationFn: (order: OrderPatch) =>
|
mutationFn: (order: OrderPatch) =>
|
||||||
orderPatch({id: order.id, status: order.status}),
|
orderPatch({ id: order.id, status: order.status }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleDrop = async (id: number, status: OrderStatusEnum) => {
|
||||||
const handleDrop = async (id: number, status: OrderStatusEnum) => {
|
const currentOrders = data ?? [];
|
||||||
|
const obj = currentOrders.find((o) => o.id === id);
|
||||||
const currentOrders = data ?? [];
|
if (!obj) {
|
||||||
const obj = currentOrders.find((o) => o.id === id);
|
setOpenSnackbar(true);
|
||||||
if (!obj) {
|
return;
|
||||||
setOpenSnackbar(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await patchOrderMutation.mutateAsync({id: obj.id, status: status});
|
|
||||||
await refetch();
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
} catch (error) {
|
|
||||||
setOpenSnackbar(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleEdit = (order: OrderType) => setEditOrder(order);
|
|
||||||
|
|
||||||
if (isLoading || !data) {
|
|
||||||
return <div>Lade Bestellungen...</div>;
|
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
await patchOrderMutation.mutateAsync({ id: obj.id, status: status });
|
||||||
|
await refetch();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
} catch (error) {
|
||||||
|
setOpenSnackbar(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleEdit = (order: OrderType) => setEditOrder(order);
|
||||||
|
|
||||||
return (
|
if (isLoading || !data) {
|
||||||
<DndProvider backend={HTML5Backend}>
|
return <div>Lade Bestellungen...</div>;
|
||||||
<div style={{display: 'flex', gap: 10, minHeight: '90%'}}>
|
}
|
||||||
{statusOrder.map((status) => (
|
|
||||||
<Column key={status} status={status} onDrop={handleDrop}>
|
if (!itemList) {
|
||||||
{data
|
return <div>itemlist empty</div>;
|
||||||
.filter((o) => o.status === status)
|
}
|
||||||
.map((o) => (
|
|
||||||
<OrderCard key={o.id} order={o} onClick={() => handleEdit(o)}/>
|
return (
|
||||||
))}
|
<DndProvider backend={HTML5Backend}>
|
||||||
</Column>
|
<div style={{ display: "flex", gap: 10, minHeight: "90%" }}>
|
||||||
))}
|
{statusOrder.map((status) => (
|
||||||
</div>
|
<Column key={status} status={status} onDrop={handleDrop}>
|
||||||
<EditOrder
|
{data
|
||||||
open={!!editOrder}
|
.filter((o) => o.status === status)
|
||||||
order={editOrder}
|
.map((o) => (
|
||||||
onClose={() => setEditOrder(null)}
|
<OrderCard key={o.id} order={o} onClick={() => handleEdit(o)} />
|
||||||
/>
|
))}
|
||||||
<Snackbar
|
</Column>
|
||||||
open={openSnackbar}
|
))}
|
||||||
autoHideDuration={4000}
|
</div>
|
||||||
onClose={() => setOpenSnackbar(false)}
|
<EditOrder
|
||||||
message="Failed changing Orderstatus"
|
open={!!editOrder}
|
||||||
/>
|
order={editOrder}
|
||||||
</DndProvider>
|
articleNameMapping={(uuid: string) => {
|
||||||
);
|
return itemList.find((item) => item.uuid == uuid)?.name || uuid;
|
||||||
|
}}
|
||||||
|
onClose={() => setEditOrder(null)}
|
||||||
|
/>
|
||||||
|
<Snackbar
|
||||||
|
open={openSnackbar}
|
||||||
|
autoHideDuration={4000}
|
||||||
|
onClose={() => setOpenSnackbar(false)}
|
||||||
|
message="Failed changing Orderstatus"
|
||||||
|
/>
|
||||||
|
</DndProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,290 +1,339 @@
|
|||||||
import {Box, Typography, useTheme} from "@mui/material";
|
import { Box, Typography, useTheme } from "@mui/material";
|
||||||
import {BarChart} from '@mui/x-charts/BarChart';
|
import { BarChart } from "@mui/x-charts/BarChart";
|
||||||
import {PieChart} from '@mui/x-charts/PieChart';
|
import { PieChart } from "@mui/x-charts/PieChart";
|
||||||
import {BarSeriesType} from '@mui/x-charts'
|
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 {fetchOrderStatus, fetchStatisticsRevenue, fetchStatisticsVolume, fetchStockPercent} from "../query/Queries.tsx";
|
import {
|
||||||
import {useAccount} from "../AccountProvider.tsx";
|
fetchOrderStatus,
|
||||||
import {getColorFromPercent} from "../../util/ColorUtil.tsx";
|
fetchStatisticsRevenue,
|
||||||
|
fetchStatisticsVolume,
|
||||||
|
fetchStockPercent,
|
||||||
|
} from "../query/Queries.tsx";
|
||||||
|
import { useAccount } from "../AccountProvider.tsx";
|
||||||
|
import { getColorFromPercent } from "../../util/ColorUtil.tsx";
|
||||||
|
|
||||||
export default function StatisticsInfo() {
|
export default function StatisticsInfo() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [monthlyVolume, setMonthlyVolume] = useState<BarSeriesType[]>([]);
|
const [monthlyVolume, setMonthlyVolume] = useState<BarSeriesType[]>([]);
|
||||||
const [monthlyVolumeXaxis, setMonthlyVolumeXaxis] = useState([{data: []}]);
|
const [monthlyVolumeXaxis, setMonthlyVolumeXaxis] = useState([{ data: [] }]);
|
||||||
const [totalVolume, setTotalVolume] = useState([]);
|
const [totalVolume, setTotalVolume] = useState([]);
|
||||||
|
|
||||||
const [monthlyRevenue, setMonthlyRevenue] = useState<BarSeriesType[]>([]);
|
const [monthlyRevenue, setMonthlyRevenue] = useState<BarSeriesType[]>([]);
|
||||||
const [monthlyRevenueXaxis, setMonthlyRevenueXaxis] = useState([{data: []}]);
|
const [monthlyRevenueXaxis, setMonthlyRevenueXaxis] = useState([
|
||||||
const [totalRevenue, setTotalRevenue] = useState([]);
|
{ data: [] },
|
||||||
|
]);
|
||||||
|
const [totalRevenue, setTotalRevenue] = useState([]);
|
||||||
|
|
||||||
const [orderStatus, setOrderStatus] = useState([]);
|
const [orderStatus, setOrderStatus] = useState([]);
|
||||||
|
|
||||||
const [stockPercent, setStockPercent] = useState([]);
|
const [stockPercent, setStockPercent] = useState([]);
|
||||||
|
|
||||||
|
const { user: loginData } = useAccount();
|
||||||
|
|
||||||
const {user: loginData} = useAccount();
|
const { data: dataVolume } = useQuery({
|
||||||
|
queryKey: ["fetchStatisticsVolume", loginData],
|
||||||
|
queryFn: () =>
|
||||||
|
fetchStatisticsVolume(
|
||||||
|
loginData
|
||||||
|
? loginData
|
||||||
|
: {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
session: "",
|
||||||
|
customerId: -1,
|
||||||
|
isAdmin: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
retry: 0,
|
||||||
|
retryDelay: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: dataRevenue } = useQuery({
|
||||||
|
queryKey: ["fetchStatisticsRevenue", loginData],
|
||||||
|
queryFn: () =>
|
||||||
|
fetchStatisticsRevenue(
|
||||||
|
loginData
|
||||||
|
? loginData
|
||||||
|
: {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
session: "",
|
||||||
|
customerId: -1,
|
||||||
|
isAdmin: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
retry: 0,
|
||||||
|
retryDelay: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const {data: dataVolume} = useQuery({
|
const { data: dataOrderStatus } = useQuery({
|
||||||
queryKey: ["fetchStatisticsVolume", loginData],
|
queryKey: ["fetchOrderStatus", loginData],
|
||||||
queryFn: () => fetchStatisticsVolume(loginData ? loginData : {
|
queryFn: () =>
|
||||||
email: "",
|
fetchOrderStatus(
|
||||||
password: "",
|
loginData
|
||||||
session: "",
|
? loginData
|
||||||
customerId: -1,
|
: {
|
||||||
isAdmin: false
|
email: "",
|
||||||
}),
|
password: "",
|
||||||
retry: 0,
|
session: "",
|
||||||
retryDelay: 0,
|
customerId: -1,
|
||||||
});
|
isAdmin: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
retry: 0,
|
||||||
|
retryDelay: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const {data: dataRevenue} = useQuery({
|
const { data: dataStockPercent } = useQuery({
|
||||||
queryKey: ["fetchStatisticsRevenue", loginData],
|
queryKey: ["fetchStockPercent", loginData],
|
||||||
queryFn: () => fetchStatisticsRevenue(loginData ? loginData : {
|
queryFn: () =>
|
||||||
email: "",
|
fetchStockPercent(
|
||||||
password: "",
|
loginData
|
||||||
session: "",
|
? loginData
|
||||||
customerId: -1,
|
: {
|
||||||
isAdmin: false
|
email: "",
|
||||||
}),
|
password: "",
|
||||||
retry: 0,
|
session: "",
|
||||||
retryDelay: 0,
|
customerId: -1,
|
||||||
});
|
isAdmin: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
retry: 0,
|
||||||
|
retryDelay: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const {data: dataOrderStatus} = useQuery({
|
useEffect(() => {
|
||||||
queryKey: ["fetchOrderStatus", loginData],
|
if (dataVolume) {
|
||||||
queryFn: () => fetchOrderStatus(loginData ? loginData : {
|
const cmm = [];
|
||||||
email: "",
|
const cmmx = monthlyVolumeXaxis;
|
||||||
password: "",
|
const tv = [];
|
||||||
session: "",
|
let i = 0;
|
||||||
customerId: -1,
|
for (const cat in dataVolume.catMonthMap) {
|
||||||
isAdmin: false
|
for (const timestamp in dataVolume.catMonthMap[cat]) {
|
||||||
}),
|
const date = new Date(parseInt(timestamp));
|
||||||
retry: 0,
|
const formattedDate =
|
||||||
retryDelay: 0,
|
date.getFullYear() +
|
||||||
});
|
"-" +
|
||||||
|
String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
if (!cmmx[0].data.includes(formattedDate)) {
|
||||||
|
cmmx[0].data.push(formattedDate);
|
||||||
|
}
|
||||||
|
const datapoint = dataVolume.catMonthMap[cat][timestamp];
|
||||||
|
if (cmm.length == i) {
|
||||||
|
cmm.push({ id: i, data: [], label: t(cat), type: "bar" });
|
||||||
|
}
|
||||||
|
cmm[i].data.push(datapoint);
|
||||||
|
|
||||||
const {data: dataStockPercent} = useQuery({
|
if (tv.length == i) {
|
||||||
queryKey: ["fetchStockPercent", loginData],
|
tv.push({ id: i, value: 0, label: t(cat) });
|
||||||
queryFn: () => fetchStockPercent(loginData ? loginData : {
|
}
|
||||||
email: "",
|
tv[i].value += datapoint;
|
||||||
password: "",
|
|
||||||
session: "",
|
|
||||||
customerId: -1,
|
|
||||||
isAdmin: false
|
|
||||||
}),
|
|
||||||
retry: 0,
|
|
||||||
retryDelay: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (dataVolume) {
|
|
||||||
const cmm = []
|
|
||||||
const cmmx = monthlyVolumeXaxis
|
|
||||||
const tv = []
|
|
||||||
let i = 0
|
|
||||||
for (const cat in dataVolume.catMonthMap) {
|
|
||||||
for (const timestamp in dataVolume.catMonthMap[cat]) {
|
|
||||||
const date = new Date(parseInt(timestamp))
|
|
||||||
const formattedDate = date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, '0')
|
|
||||||
if (!cmmx[0].data.includes(formattedDate)) {
|
|
||||||
cmmx[0].data.push(formattedDate);
|
|
||||||
}
|
|
||||||
const datapoint = dataVolume.catMonthMap[cat][timestamp]
|
|
||||||
if (cmm.length == i) {
|
|
||||||
cmm.push({id: i, data: [], label: t(cat), type: "bar"})
|
|
||||||
}
|
|
||||||
cmm[i].data.push(datapoint)
|
|
||||||
|
|
||||||
if (tv.length == i) {
|
|
||||||
tv.push({id: i, value: 0, label: t(cat)})
|
|
||||||
}
|
|
||||||
tv[i].value += datapoint
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
setMonthlyVolume(cmm)
|
|
||||||
setMonthlyVolumeXaxis(cmmx)
|
|
||||||
setTotalVolume(tv)
|
|
||||||
}
|
}
|
||||||
}, [dataVolume]);
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
setMonthlyVolume(cmm);
|
||||||
if (dataRevenue) {
|
setMonthlyVolumeXaxis(cmmx);
|
||||||
const cmm = []
|
setTotalVolume(tv);
|
||||||
const cmmx = monthlyRevenueXaxis
|
}
|
||||||
const tv = []
|
}, [dataVolume]);
|
||||||
let i = 0
|
|
||||||
for (const cat in dataRevenue.catMonthMap) {
|
|
||||||
for (const timestamp in dataRevenue.catMonthMap[cat]) {
|
|
||||||
const date = new Date(parseInt(timestamp))
|
|
||||||
const formattedDate = date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, '0')
|
|
||||||
if (!cmmx[0].data.includes(formattedDate)) {
|
|
||||||
cmmx[0].data.push(formattedDate);
|
|
||||||
}
|
|
||||||
const datapoint = dataRevenue.catMonthMap[cat][timestamp] / 100
|
|
||||||
if (cmm.length == i) {
|
|
||||||
cmm.push({id: i, data: [], label: t(cat), type: "bar"})
|
|
||||||
}
|
|
||||||
cmm[i].data.push(datapoint)
|
|
||||||
|
|
||||||
if (tv.length == i) {
|
useEffect(() => {
|
||||||
tv.push({id: i, value: 0, label: t(cat)})
|
if (dataRevenue) {
|
||||||
}
|
const cmm = [];
|
||||||
tv[i].value += datapoint
|
const cmmx = monthlyRevenueXaxis;
|
||||||
}
|
const tv = [];
|
||||||
i++;
|
let i = 0;
|
||||||
}
|
for (const cat in dataRevenue.catMonthMap) {
|
||||||
|
for (const timestamp in dataRevenue.catMonthMap[cat]) {
|
||||||
|
const date = new Date(parseInt(timestamp));
|
||||||
|
const formattedDate =
|
||||||
|
date.getFullYear() +
|
||||||
|
"-" +
|
||||||
|
String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
if (!cmmx[0].data.includes(formattedDate)) {
|
||||||
|
cmmx[0].data.push(formattedDate);
|
||||||
|
}
|
||||||
|
const datapoint = dataRevenue.catMonthMap[cat][timestamp] / 100;
|
||||||
|
if (cmm.length == i) {
|
||||||
|
cmm.push({ id: i, data: [], label: t(cat), type: "bar" });
|
||||||
|
}
|
||||||
|
cmm[i].data.push(datapoint);
|
||||||
|
|
||||||
setMonthlyRevenue(cmm)
|
if (tv.length == i) {
|
||||||
setMonthlyRevenueXaxis(cmmx)
|
tv.push({ id: i, value: 0, label: t(cat) });
|
||||||
setTotalRevenue(tv)
|
}
|
||||||
|
tv[i].value += datapoint;
|
||||||
}
|
}
|
||||||
}, [dataRevenue, monthlyRevenueXaxis, t]);
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
setMonthlyRevenue(cmm);
|
||||||
if (dataOrderStatus) {
|
setMonthlyRevenueXaxis(cmmx);
|
||||||
const orderStatus = []
|
setTotalRevenue(tv);
|
||||||
for (const status in dataOrderStatus) {
|
}
|
||||||
orderStatus.push({value: dataOrderStatus[status], label: t(status)})
|
}, [dataRevenue, monthlyRevenueXaxis, t]);
|
||||||
}
|
|
||||||
setOrderStatus(orderStatus)
|
useEffect(() => {
|
||||||
|
if (dataOrderStatus) {
|
||||||
|
const orderStatus = [];
|
||||||
|
for (const status in dataOrderStatus) {
|
||||||
|
orderStatus.push({ value: dataOrderStatus[status], label: t(status) });
|
||||||
|
}
|
||||||
|
setOrderStatus(orderStatus);
|
||||||
|
}
|
||||||
|
}, [dataOrderStatus, t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function generateName(percent: string): string {
|
||||||
|
return ">" + percent + "%";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataStockPercent) {
|
||||||
|
const stockPercent = [];
|
||||||
|
let i = 0;
|
||||||
|
for (let x = 0; x < 10; x++) {
|
||||||
|
stockPercent.push({
|
||||||
|
value: 0,
|
||||||
|
label: generateName(String(x * 10)),
|
||||||
|
color: getColorFromPercent(String(x * 10)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const cat in dataStockPercent) {
|
||||||
|
for (const percent in dataStockPercent[cat]) {
|
||||||
|
let index = stockPercent.findIndex(
|
||||||
|
(entry) => entry.label == generateName(percent),
|
||||||
|
);
|
||||||
|
const datapoint = dataStockPercent[cat][percent];
|
||||||
|
if (index === -1) {
|
||||||
|
index =
|
||||||
|
stockPercent.push({
|
||||||
|
value: 0,
|
||||||
|
label: generateName(percent),
|
||||||
|
color: getColorFromPercent(percent),
|
||||||
|
}) - 1;
|
||||||
|
}
|
||||||
|
stockPercent[index].value += datapoint;
|
||||||
}
|
}
|
||||||
}, [dataOrderStatus, t]);
|
i++;
|
||||||
|
}
|
||||||
|
setStockPercent(stockPercent);
|
||||||
|
}
|
||||||
|
}, [dataStockPercent]);
|
||||||
|
|
||||||
useEffect(() => {
|
return (
|
||||||
function generateName(percent: string): string {
|
<Box className="" sx={{ color: theme.palette.text.primary }}>
|
||||||
return ">" + percent + "%";
|
<Typography mt={4} variant="h4" align="center" gutterBottom>
|
||||||
}
|
{t("salesStatistics")}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
if (dataStockPercent) {
|
<Box sx={{ mb: 4 }}>
|
||||||
const stockPercent = []
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
let i = 0
|
{t("monthlySalesVolume")}
|
||||||
for (let x = 0; x < 10; x++) {
|
</Typography>
|
||||||
stockPercent.push({
|
<BarChart
|
||||||
value: 0,
|
series={monthlyVolume}
|
||||||
label: generateName(String(x * 10)),
|
height={290}
|
||||||
color: getColorFromPercent(String(x * 10))
|
xAxis={monthlyVolumeXaxis}
|
||||||
});
|
/>
|
||||||
}
|
</Box>
|
||||||
for (const cat in dataStockPercent) {
|
<Box sx={{ mb: 4 }}>
|
||||||
for (const percent in dataStockPercent[cat]) {
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
let index = stockPercent.findIndex((entry) => entry.label == generateName(percent))
|
{t("monthlySalesRevenue")}
|
||||||
const datapoint = dataStockPercent[cat][percent]
|
</Typography>
|
||||||
if (index === -1) {
|
<BarChart
|
||||||
index = stockPercent.push({
|
series={monthlyRevenue}
|
||||||
value: 0,
|
height={290}
|
||||||
label: generateName(percent),
|
xAxis={monthlyRevenueXaxis}
|
||||||
color: getColorFromPercent(percent)
|
/>
|
||||||
}) - 1
|
</Box>
|
||||||
}
|
<Box display={"flex"} mb={9}>
|
||||||
stockPercent[index].value += datapoint
|
<Box className="vw20" sx={{ m: 2 }}>
|
||||||
}
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
i++
|
{t("itemVolumeDistribution")}
|
||||||
}
|
</Typography>
|
||||||
setStockPercent(stockPercent)
|
<PieChart
|
||||||
}
|
series={[
|
||||||
}, [dataStockPercent])
|
{
|
||||||
|
data: totalVolume,
|
||||||
return (
|
highlightScope: { fade: "global", highlight: "item" },
|
||||||
<Box className="" sx={{color: theme.palette.text.primary}}>
|
faded: {
|
||||||
<Typography mt={4} variant="h4" align="center" gutterBottom>
|
innerRadius: 30,
|
||||||
{t("salesStatistics")}
|
additionalRadius: -30,
|
||||||
</Typography>
|
color: "gray",
|
||||||
|
},
|
||||||
<Box sx={{mb: 4}}>
|
},
|
||||||
<Typography variant="h6" align="center" gutterBottom>
|
]}
|
||||||
{t("monthlySalesVolume")}
|
width={200}
|
||||||
</Typography>
|
height={200}
|
||||||
<BarChart
|
/>
|
||||||
series={monthlyVolume}
|
|
||||||
height={290}
|
|
||||||
xAxis={monthlyVolumeXaxis}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{mb: 4}}>
|
|
||||||
<Typography variant="h6" align="center" gutterBottom>
|
|
||||||
{t("monthlySalesRevenue")}
|
|
||||||
</Typography>
|
|
||||||
<BarChart
|
|
||||||
series={monthlyRevenue}
|
|
||||||
height={290}
|
|
||||||
xAxis={monthlyRevenueXaxis}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box display={"flex"} mb={9}>
|
|
||||||
<Box className="vw20" sx={{m: 2}}>
|
|
||||||
<Typography variant="h6" align="center" gutterBottom>
|
|
||||||
{t("itemVolumeDistribution")}
|
|
||||||
</Typography>
|
|
||||||
<PieChart
|
|
||||||
series={[{
|
|
||||||
data: totalVolume,
|
|
||||||
highlightScope: {fade: 'global', highlight: 'item'},
|
|
||||||
faded: {innerRadius: 30, additionalRadius: -30, color: 'gray'},
|
|
||||||
}]}
|
|
||||||
width={200}
|
|
||||||
height={200}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box className="vw20" sx={{m: 2}}>
|
|
||||||
<Typography variant="h6" align="center" gutterBottom>
|
|
||||||
{t("itemRevenueDistribution")}
|
|
||||||
</Typography>
|
|
||||||
<PieChart
|
|
||||||
series={[{
|
|
||||||
data: totalRevenue,
|
|
||||||
highlightScope: {fade: 'global', highlight: 'item'},
|
|
||||||
valueFormatter: (v) => (v ? `${v.value}€` : '-'),
|
|
||||||
}]}
|
|
||||||
width={200}
|
|
||||||
height={200}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box className="vw20" sx={{m: 2}}>
|
|
||||||
<Typography variant="h6" align="center" gutterBottom>
|
|
||||||
{t("stockFulfillment")}
|
|
||||||
</Typography>
|
|
||||||
<PieChart
|
|
||||||
series={[{
|
|
||||||
data: stockPercent,
|
|
||||||
innerRadius: 10,
|
|
||||||
outerRadius: 85,
|
|
||||||
cornerRadius: 5,
|
|
||||||
paddingAngle: 2,
|
|
||||||
valueFormatter: (v) => (v ? `${v.value}%` : '-'),
|
|
||||||
}]}
|
|
||||||
width={200}
|
|
||||||
height={200}
|
|
||||||
hideLegend
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box className="vw20" sx={{m: 2}}>
|
|
||||||
<Typography variant="h6" align="center" gutterBottom>
|
|
||||||
{t("orderStatus")}
|
|
||||||
</Typography>
|
|
||||||
<PieChart
|
|
||||||
series={[{
|
|
||||||
data: orderStatus,
|
|
||||||
innerRadius: 20,
|
|
||||||
outerRadius: 70,
|
|
||||||
cornerRadius: 5,
|
|
||||||
paddingAngle: 1,
|
|
||||||
highlightScope: {fade: 'global', highlight: 'item'},
|
|
||||||
faded: {innerRadius: 30, additionalRadius: -10, color: 'gray'},
|
|
||||||
}]}
|
|
||||||
height={200}
|
|
||||||
width={200}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
|
||||||
|
<Box className="vw20" sx={{ m: 2 }}>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
{t("itemRevenueDistribution")}
|
||||||
|
</Typography>
|
||||||
|
<PieChart
|
||||||
|
series={[
|
||||||
|
{
|
||||||
|
data: totalRevenue,
|
||||||
|
highlightScope: { fade: "global", highlight: "item" },
|
||||||
|
valueFormatter: (v) => (v ? `${v.value}€` : "-"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box className="vw20" sx={{ m: 2 }}>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
{t("stockFulfillment")}
|
||||||
|
</Typography>
|
||||||
|
<PieChart
|
||||||
|
series={[
|
||||||
|
{
|
||||||
|
data: stockPercent,
|
||||||
|
innerRadius: 10,
|
||||||
|
outerRadius: 85,
|
||||||
|
cornerRadius: 5,
|
||||||
|
paddingAngle: 2,
|
||||||
|
valueFormatter: (v) => (v ? `${v.value}%` : "-"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
hideLegend
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box className="vw20" sx={{ m: 2 }}>
|
||||||
|
<Typography variant="h6" align="center" gutterBottom>
|
||||||
|
{t("orderStatus")}
|
||||||
|
</Typography>
|
||||||
|
<PieChart
|
||||||
|
series={[
|
||||||
|
{
|
||||||
|
data: orderStatus,
|
||||||
|
innerRadius: 20,
|
||||||
|
outerRadius: 70,
|
||||||
|
cornerRadius: 5,
|
||||||
|
paddingAngle: 1,
|
||||||
|
highlightScope: { fade: "global", highlight: "item" },
|
||||||
|
faded: {
|
||||||
|
innerRadius: 30,
|
||||||
|
additionalRadius: -10,
|
||||||
|
color: "gray",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
height={200}
|
||||||
|
width={200}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
.item-description {
|
.item-description {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between; /* Elemente an gegenüberliegenden Seiten platzieren */
|
justify-content: space-between; /* Elemente an gegenüberliegenden Seiten platzieren */
|
||||||
align-items: center; /* Vertikale Zentrierung */
|
align-items: center; /* Vertikale Zentrierung */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-card-button {
|
.item-card-button {
|
||||||
color: green;
|
color: green;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rating-card-body {
|
.rating-card-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: center; /* Vertikale Zentrierung */
|
align-items: center; /* Vertikale Zentrierung */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rating-button {
|
.rating-button {
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rating-card-box {
|
.rating-card-box {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: center; /* Vertikale Zentrierung */
|
align-items: center; /* Vertikale Zentrierung */
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rating-text-field {
|
.rating-text-field {
|
||||||
background-color: whitesmoke;
|
background-color: whitesmoke;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vw20 {
|
.vw20 {
|
||||||
width: 20vw;
|
width: 20vw;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +1,83 @@
|
|||||||
import {FormControl, FormControlLabel, FormLabel, Radio, RadioGroup, Rating, useTheme,} from "@mui/material";
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
FormLabel,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
Rating,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
type FilterItemOption = {
|
type FilterItemOption = {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FilterItemProps = {
|
type FilterItemProps = {
|
||||||
filterName: string;
|
filterName: string;
|
||||||
filterItems: FilterItemOption[];
|
filterItems: FilterItemOption[];
|
||||||
value?: string | null;
|
value?: string | null;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FilterItem({
|
export default function FilterItem({
|
||||||
filterName,
|
filterName,
|
||||||
filterItems,
|
filterItems,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: FilterItemProps) {
|
}: FilterItemProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
if (!value && filterItems.length > 0) {
|
if (!value && filterItems.length > 0) {
|
||||||
value = filterItems[0].value;
|
value = filterItems[0].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(event.target.value);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
return (
|
||||||
if (onChange) {
|
<div style={{ marginBottom: "1.5rem" }}>
|
||||||
onChange(event.target.value);
|
<FormLabel
|
||||||
}
|
component="legend"
|
||||||
};
|
sx={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filterName}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
return (
|
<FormControl>
|
||||||
<div style={{marginBottom: "1.5rem"}}>
|
<RadioGroup value={value} onChange={handleChange}>
|
||||||
<FormLabel
|
{filterItems.map((item, idx) => (
|
||||||
component="legend"
|
<FormControlLabel
|
||||||
sx={{
|
key={idx}
|
||||||
fontWeight: "bold",
|
value={item.value}
|
||||||
color: theme.palette.text.primary,
|
control={<Radio />}
|
||||||
mb: 1,
|
label={
|
||||||
}}
|
/^[1-5]$/.test(item.value) ? (
|
||||||
>
|
<Rating
|
||||||
{filterName}
|
readOnly
|
||||||
</FormLabel>
|
value={Number(item.value)}
|
||||||
|
precision={1}
|
||||||
<FormControl>
|
size="small"
|
||||||
<RadioGroup value={value} onChange={handleChange}>
|
/>
|
||||||
{filterItems.map((item, idx) => (
|
) : (
|
||||||
<FormControlLabel
|
item.label
|
||||||
key={idx}
|
)
|
||||||
value={item.value}
|
}
|
||||||
control={<Radio/>}
|
sx={{
|
||||||
label={
|
color: theme.palette.text.primary,
|
||||||
/^[1-5]$/.test(item.value) ? (
|
}}
|
||||||
<Rating
|
/>
|
||||||
readOnly
|
))}
|
||||||
value={Number(item.value)}
|
</RadioGroup>
|
||||||
precision={1}
|
</FormControl>
|
||||||
size="small"
|
</div>
|
||||||
/>
|
);
|
||||||
) : (
|
|
||||||
item.label
|
|
||||||
)
|
|
||||||
}
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,24 @@
|
|||||||
|
|
||||||
/* Container um jedes Filter-Widget */
|
/* Container um jedes Filter-Widget */
|
||||||
.filter-item {
|
.filter-item {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Überschrift (FormLabel) */
|
/* Überschrift (FormLabel) */
|
||||||
.filter-item__label {
|
.filter-item__label {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
/* nutze die CSS-Variable, die GlobalStyles füllen */
|
/* nutze die CSS-Variable, die GlobalStyles füllen */
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Das Material-UI FormControl-Element */
|
/* Das Material-UI FormControl-Element */
|
||||||
.filter-item__group {
|
.filter-item__group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Jeder Radio-Button mit Label */
|
/* Jeder Radio-Button mit Label */
|
||||||
.filter-item__option {
|
.filter-item__option {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +1,150 @@
|
|||||||
import {AddShoppingCart} from "@mui/icons-material";
|
import { AddShoppingCart } from "@mui/icons-material";
|
||||||
import {Box, Card, CardActionArea, CardContent, CardMedia, IconButton, Paper, Rating, Typography} from "@mui/material";
|
import {
|
||||||
import {useState} from "react";
|
Box,
|
||||||
import {useTranslation} from 'react-i18next';
|
Card,
|
||||||
import {useNavigate} from "react-router-dom";
|
CardActionArea,
|
||||||
|
CardContent,
|
||||||
|
CardMedia,
|
||||||
|
IconButton,
|
||||||
|
Paper,
|
||||||
|
Rating,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import ItemWithImage from "../../components/Item";
|
import ItemWithImage from "../../components/Item";
|
||||||
import {useBasket} from "../BasketProvider";
|
import { useBasket } from "../BasketProvider";
|
||||||
import "../helper.css";
|
import "../helper.css";
|
||||||
|
|
||||||
export default function ItemCard({item}: { item: ItemWithImage }) {
|
export default function ItemCard({ item }: { item: ItemWithImage }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { addToBasket } = useBasket();
|
||||||
|
|
||||||
const {t} = useTranslation();
|
const handleAddToCart = () => {
|
||||||
const navigate = useNavigate()
|
addToBasket(item, 1);
|
||||||
const {addToBasket} = useBasket();
|
console.log(`Added ${1} of ${item.name} to basket`);
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddToCart = () => {
|
const handleClick = () => {
|
||||||
addToBasket(item, 1);
|
navigate(`/product/${item.id}`, { state: { item } });
|
||||||
console.log(`Added ${1} of ${item.name} to basket`);
|
};
|
||||||
};
|
const [imageUrl, setImageUrl] = useState<string>(
|
||||||
|
item.image || "/src/assets/default.jpg",
|
||||||
|
); // Fallback-Bild
|
||||||
|
|
||||||
const handleClick = () => {
|
if (
|
||||||
navigate(`/product/${item.id}`, {state: {item}});
|
imageUrl !== "/src/assets/default.jpg" &&
|
||||||
}
|
!imageUrl.startsWith("data:image/")
|
||||||
const [imageUrl, setImageUrl] = useState<string>(item.image || "/src/assets/default.jpg"); // Fallback-Bild
|
) {
|
||||||
|
setImageUrl("data:image/jpeg;base64," + imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
if (imageUrl !== "/src/assets/default.jpg" && !imageUrl.startsWith("data:image/")) {
|
return (
|
||||||
setImageUrl("data:image/jpeg;base64," + imageUrl);
|
<Paper elevation={4}>
|
||||||
}
|
<Card
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardActionArea
|
||||||
|
onClick={handleClick}
|
||||||
|
sx={{ height: "100%", width: "100%" }}
|
||||||
|
component="div"
|
||||||
|
>
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
height="140"
|
||||||
|
src={imageUrl}
|
||||||
|
alt={item.name}
|
||||||
|
onError={(event) => {
|
||||||
|
event.currentTarget.src = "/src/assets/default.jpg"; // Standardbild setzen
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
objectFit: "contain", // Bild wird skaliert, um vollständig sichtbar zu sein
|
||||||
|
maxWidth: "100%", // Begrenze die maximale Breite auf den Container
|
||||||
|
maxHeight: "100%", // Begrenze die maximale Höhe auf den Container
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CardContent
|
||||||
|
sx={{ display: "flex", flexDirection: "column", flexGrow: 1 }}
|
||||||
|
>
|
||||||
|
<Typography gutterBottom variant="h5" component="div">
|
||||||
|
{item.name}
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: "auto",
|
||||||
|
display: "flex",
|
||||||
|
flexGrow: 1,
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Rating
|
||||||
|
name="half-rating"
|
||||||
|
readOnly
|
||||||
|
defaultValue={item.rating / 2}
|
||||||
|
precision={0.5}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "text.secondary" }}
|
||||||
|
className="item-description"
|
||||||
|
>
|
||||||
|
{(
|
||||||
|
(item.price100 / 100) *
|
||||||
|
(1 - item.discount100 / 100)
|
||||||
|
).toFixed(2)}{" "}
|
||||||
|
€
|
||||||
|
</Typography>
|
||||||
|
{item.discount100 == 0 ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "red" }}
|
||||||
|
className="item-description"
|
||||||
|
>
|
||||||
|
{-item.discount100}%
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
aria-label={t("addToCart")}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
handleAddToCart();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AddShoppingCart />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
return (
|
{item.stock > 10 ? (
|
||||||
<Paper elevation={4}>
|
<Typography variant="body2">{t("inStock")}</Typography>
|
||||||
<Card sx={{height: "100%", width: "100%"}}>
|
) : item.stock > 0 ? (
|
||||||
<CardActionArea onClick={handleClick} sx={{height: "100%"}} component="div">
|
<Typography variant="body2" sx={{ color: "orange" }}>
|
||||||
<CardMedia
|
{t("almostSoldOut")}
|
||||||
component="img"
|
</Typography>
|
||||||
height="140"
|
) : (
|
||||||
src={imageUrl}
|
<Typography variant="body2" sx={{ color: "red" }}>
|
||||||
alt={item.name}
|
{t("outOfStock")}
|
||||||
onError={(event) => {
|
</Typography>
|
||||||
event.currentTarget.src = "/src/assets/default.jpg"; // Standardbild setzen
|
)}
|
||||||
}}
|
</Box>
|
||||||
sx={{
|
</CardContent>
|
||||||
objectFit: "contain", // Bild wird skaliert, um vollständig sichtbar zu sein
|
</CardActionArea>
|
||||||
maxWidth: "100%", // Begrenze die maximale Breite auf den Container
|
</Card>
|
||||||
maxHeight: "100%", // Begrenze die maximale Höhe auf den Container
|
</Paper>
|
||||||
}}
|
);
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
<Typography gutterBottom variant="h5" component="div">
|
|
||||||
{item.name}
|
|
||||||
</Typography>
|
|
||||||
<Rating name="half-rating" readOnly defaultValue={item.rating / 2} precision={0.5}/>
|
|
||||||
<Box sx={{display: "flex", justifyContent: "space-between", alignItems: "flex-end"}}>
|
|
||||||
<Typography variant="body2" sx={{color: 'text.secondary'}} className="item-description">
|
|
||||||
{(item.price100 / 100 * (1 - item.discount100 / 100)).toFixed(2)} €
|
|
||||||
</Typography>
|
|
||||||
{item.discount100 == 0 ? <></> :
|
|
||||||
<Typography variant="body2" sx={{color: 'red'}} className="item-description">
|
|
||||||
{(- item.discount100)}%
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
<IconButton
|
|
||||||
aria-label={t('addToCart')}
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
handleAddToCart();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AddShoppingCart/>
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{item.stock > 10 ? (
|
|
||||||
<Typography variant="body2">
|
|
||||||
{t('inStock')}
|
|
||||||
</Typography>
|
|
||||||
) : item.stock > 0 ? (
|
|
||||||
<Typography variant="body2" sx={{color: 'orange'}}>
|
|
||||||
{t('almostSoldOut')}
|
|
||||||
</Typography>
|
|
||||||
) : (
|
|
||||||
<Typography variant="body2" sx={{color: 'red'}}>
|
|
||||||
{t('outOfStock')}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</CardActionArea>
|
|
||||||
</Card>
|
|
||||||
</Paper>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,81 +1,88 @@
|
|||||||
import {Box, Slider, Typography, useTheme} from "@mui/material";
|
import { Box, Slider, Typography, useTheme } from "@mui/material";
|
||||||
import {SyntheticEvent, useEffect, useState} from "react";
|
import { SyntheticEvent, useEffect, useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type PriceSliderProps = {
|
type PriceSliderProps = {
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
onChange?: (range: [number, number]) => void;
|
onChange?: (range: [number, number]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PriceSlider({min = 0, max = 10000, onChange}: PriceSliderProps) {
|
export default function PriceSlider({
|
||||||
const {t} = useTranslation();
|
min = 0,
|
||||||
const theme = useTheme();
|
max = 10000,
|
||||||
|
onChange,
|
||||||
|
}: PriceSliderProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
const [value, setValue] = useState<[number, number]>([min, max]);
|
const [value, setValue] = useState<[number, number]>([min, max]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue([min, max]);
|
setValue([min, max]);
|
||||||
onChange?.([min, max]);
|
onChange?.([min, max]);
|
||||||
}, [min, max]);
|
}, [min, max]);
|
||||||
|
|
||||||
const handleChange = (_: Event, newValue: number | number[]) => {
|
const handleChange = (_: Event, newValue: number | number[]) => {
|
||||||
if (Array.isArray(newValue)) {
|
if (Array.isArray(newValue)) {
|
||||||
setValue([newValue[0], newValue[1]]);
|
setValue([newValue[0], newValue[1]]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCommitted = (_: Event | SyntheticEvent<Element, Event>, newValue: number | number[]) => {
|
const handleCommitted = (
|
||||||
if (Array.isArray(newValue)) {
|
_: Event | SyntheticEvent<Element, Event>,
|
||||||
onChange?.([newValue[0], newValue[1]]);
|
newValue: number | number[],
|
||||||
}
|
) => {
|
||||||
};
|
if (Array.isArray(newValue)) {
|
||||||
|
onChange?.([newValue[0], newValue[1]]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const formatValueToEuro = (val: number) => `${(val / 100).toFixed(2)} €`;
|
const formatValueToEuro = (val: number) => `${(val / 100).toFixed(2)} €`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{mb: 4}}>
|
<Box sx={{ mb: 4 }}>
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
variant="h6"
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
mb: 1,
|
mb: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("price")}
|
{t("price")}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box sx={{px: 1}}>
|
<Box sx={{ px: 1 }}>
|
||||||
<Slider
|
<Slider
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onChangeCommitted={handleCommitted}
|
onChangeCommitted={handleCommitted}
|
||||||
valueLabelDisplay="auto"
|
valueLabelDisplay="auto"
|
||||||
valueLabelFormat={formatValueToEuro}
|
valueLabelFormat={formatValueToEuro}
|
||||||
min={min}
|
min={min}
|
||||||
max={max}
|
max={max}
|
||||||
step={1}
|
step={1}
|
||||||
sx={{
|
sx={{
|
||||||
color: "#0fd13f",
|
color: "#0fd13f",
|
||||||
'& .MuiSlider-valueLabel': {
|
"& .MuiSlider-valueLabel": {
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{
|
sx={{
|
||||||
mt: 1,
|
mt: 1,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
fontSize: "0.9rem",
|
fontSize: "0.9rem",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{formatValueToEuro(value[0])} – {formatValueToEuro(value[1])}
|
{formatValueToEuro(value[0])} – {formatValueToEuro(value[1])}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,257 +1,307 @@
|
|||||||
import {Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Link, TextField} from "@mui/material";
|
import {
|
||||||
import {useQuery} from "@tanstack/react-query";
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Link,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import AccountType, {User} from "../../components/Account";
|
import AccountType, { User } from "../../components/Account";
|
||||||
import {useAccount} from "../AccountProvider";
|
import { useAccount } from "../AccountProvider";
|
||||||
import {fetchAccount, submitLogin, submitRegister} from "../query/Queries"; // Importiere die Funktion für die Registrierung
|
import { fetchAccount, submitLogin, submitRegister } from "../query/Queries"; // Importiere die Funktion für die Registrierung
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type LoginDialogProps = {
|
type LoginDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: () => void; // Funktion, die aufgerufen wird, wenn der Login erfolgreich ist
|
onSubmit: () => void; // Funktion, die aufgerufen wird, wenn der Login erfolgreich ist
|
||||||
loginData: { email: string; password: string };
|
loginData: { email: string; password: string };
|
||||||
setLoginData: React.Dispatch<React.SetStateAction<{ email: string; password: string, customerId: number }>>;
|
setLoginData: React.Dispatch<
|
||||||
|
React.SetStateAction<{
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
customerId: number;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setLoginData, onSubmit}) => {
|
const LoginDialog: React.FC<LoginDialogProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
loginData,
|
||||||
|
setLoginData,
|
||||||
|
onSubmit,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { login } = useAccount();
|
||||||
|
const [showRegister, setShowRegister] = useState(false);
|
||||||
|
const [registerData, setRegisterData] = useState<AccountType>({
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
id: 0,
|
||||||
|
customer: {
|
||||||
|
id: 0,
|
||||||
|
name: "",
|
||||||
|
surname: "",
|
||||||
|
address: "",
|
||||||
|
country: "",
|
||||||
|
zip: "",
|
||||||
|
},
|
||||||
|
langI18n: i18next.language,
|
||||||
|
admin: false,
|
||||||
|
});
|
||||||
|
const [showErrorRegister, setShowErrorRegister] = useState(false); // Neuer Zustand für die Anzeige der Fehlermeldung
|
||||||
|
const [showErrorLogin, setShowErrorLogin] = useState(false); // Neuer Zustand für die Anzeige der Login-Fehlermeldung
|
||||||
|
|
||||||
const {t} = useTranslation();
|
useEffect(() => {
|
||||||
const {login} = useAccount();
|
if (open) {
|
||||||
const [showRegister, setShowRegister] = useState(false);
|
const active = document.activeElement as HTMLElement | null;
|
||||||
const [registerData, setRegisterData] = useState<AccountType>({
|
if (active && typeof active.blur === "function") {
|
||||||
email: "",
|
active.blur();
|
||||||
password: "",
|
}
|
||||||
id: 0,
|
}
|
||||||
customer: {id: 0, name: "", surname: "", address: "", country: "", zip: ""},
|
}, [open]);
|
||||||
langI18n: i18next.language,
|
|
||||||
admin: false
|
|
||||||
});
|
|
||||||
const [showErrorRegister, setShowErrorRegister] = useState(false); // Neuer Zustand für die Anzeige der Fehlermeldung
|
|
||||||
const [showErrorLogin, setShowErrorLogin] = useState(false); // Neuer Zustand für die Anzeige der Login-Fehlermeldung
|
|
||||||
|
|
||||||
useEffect(() => {
|
// useQuery für Login
|
||||||
if (open) {
|
const {
|
||||||
const active = document.activeElement as HTMLElement | null;
|
refetch: refetchLogin,
|
||||||
if (active && typeof active.blur === "function") {
|
isLoading: isLoadingLogin,
|
||||||
active.blur();
|
error: errorLogin,
|
||||||
}
|
} = useQuery({
|
||||||
}
|
queryKey: ["submitLogin", loginData],
|
||||||
}, [open]);
|
queryFn: () => submitLogin(loginData),
|
||||||
|
retry: 0,
|
||||||
|
retryDelay: 1000,
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { refetch: refetchAccount } = useQuery({
|
||||||
|
queryKey: ["fetchAccount", loginData],
|
||||||
|
queryFn: () => fetchAccount(loginData),
|
||||||
|
retry: 0,
|
||||||
|
retryDelay: 1000,
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
// useQuery für Login
|
// useQuery für Registrierung
|
||||||
const {refetch: refetchLogin, isLoading: isLoadingLogin, error: errorLogin} = useQuery({
|
const {
|
||||||
queryKey: ["submitLogin", loginData],
|
refetch: refetchRegister,
|
||||||
queryFn: () => submitLogin(loginData),
|
isLoading: isLoadingRegister,
|
||||||
retry: 0,
|
error: errorRegister,
|
||||||
retryDelay: 1000,
|
} = useQuery({
|
||||||
enabled: false,
|
queryKey: ["submitRegister", registerData],
|
||||||
});
|
queryFn: () => submitRegister(registerData),
|
||||||
|
retry: 0,
|
||||||
|
retryDelay: 1000,
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
const {refetch: refetchAccount} = useQuery({
|
const handleClose = () => {
|
||||||
queryKey: ["fetchAccount", loginData],
|
setShowErrorLogin(false); // Fehlermeldung zurücksetzen
|
||||||
queryFn: () => fetchAccount(loginData),
|
setShowErrorRegister(false); // Fehlermeldung zurücksetzen
|
||||||
retry: 0,
|
onClose();
|
||||||
retryDelay: 1000,
|
};
|
||||||
enabled: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// useQuery für Registrierung
|
const handleLogin = async () => {
|
||||||
const {refetch: refetchRegister, isLoading: isLoadingRegister, error: errorRegister} = useQuery({
|
try {
|
||||||
queryKey: ["submitRegister", registerData],
|
setShowErrorLogin(false); // Fehlermeldung zurücksetzen
|
||||||
queryFn: () => submitRegister(registerData),
|
setShowErrorRegister(false); // Fehlermeldung zurücksetzen
|
||||||
retry: 0,
|
const response = await refetchLogin(); // Anfrage auslösen
|
||||||
retryDelay: 1000,
|
if (response.status === "success") {
|
||||||
enabled: false,
|
const session = response.data.uuid; // Session-Daten aus der Antwort extrahieren
|
||||||
});
|
const customerData = (await refetchAccount()).data;
|
||||||
|
const user: User = {
|
||||||
|
email: customerData.email,
|
||||||
|
password: customerData.password,
|
||||||
|
customerId: customerData.customer.id, // Setze die customerId aus den Account-Daten
|
||||||
|
session: session, // Setze die Session aus der Login-Antwort
|
||||||
|
isAdmin: customerData.admin,
|
||||||
|
};
|
||||||
|
login(user);
|
||||||
|
setShowRegister(false); // Zurück zum Login wechseln
|
||||||
|
onSubmit(); // Dialog schließen
|
||||||
|
} else {
|
||||||
|
setShowErrorLogin(true); // Fehlermeldung anzeigen
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
} catch (error) {
|
||||||
|
setShowErrorLogin(true); // Fehlermeldung anzeigen
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleRegister = async () => {
|
||||||
setShowErrorLogin(false); // Fehlermeldung zurücksetzen
|
try {
|
||||||
setShowErrorRegister(false); // Fehlermeldung zurücksetzen
|
setShowErrorLogin(false); // Fehlermeldung zurücksetzen
|
||||||
onClose();
|
setShowErrorRegister(false); // Fehlermeldung zurücksetzen
|
||||||
};
|
await refetchRegister(); // Beispiel für den Refetch-Aufruf
|
||||||
|
// Erfolgslogik hier
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
} catch (error) {
|
||||||
|
setShowErrorRegister(true); // Fehlermeldung anzeigen
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleLogin = async () => {
|
return (
|
||||||
try {
|
<Dialog open={open} onClose={handleClose} disableEnforceFocus>
|
||||||
setShowErrorLogin(false); // Fehlermeldung zurücksetzen
|
<form
|
||||||
setShowErrorRegister(false); // Fehlermeldung zurücksetzen
|
onSubmit={(e) => {
|
||||||
const response = await refetchLogin(); // Anfrage auslösen
|
e.preventDefault();
|
||||||
if (response.status === "success") {
|
if (showRegister) {
|
||||||
const session = response.data.uuid; // Session-Daten aus der Antwort extrahieren
|
handleLogin();
|
||||||
const customerData = (await refetchAccount()).data;
|
} else {
|
||||||
const user: User = {
|
handleRegister();
|
||||||
email: customerData.email,
|
}
|
||||||
password: customerData.password,
|
}}
|
||||||
customerId: customerData.customer.id, // Setze die customerId aus den Account-Daten
|
noValidate
|
||||||
session: session, // Setze die Session aus der Login-Antwort
|
>
|
||||||
isAdmin: customerData.admin
|
<DialogTitle>{showRegister ? t("register") : t("login")}</DialogTitle>
|
||||||
};
|
<DialogContent>
|
||||||
login(user);
|
<TextField
|
||||||
setShowRegister(false); // Zurück zum Login wechseln
|
margin="dense"
|
||||||
onSubmit(); // Dialog schließen
|
label={t("email")}
|
||||||
} else {
|
type="email"
|
||||||
setShowErrorLogin(true); // Fehlermeldung anzeigen
|
fullWidth
|
||||||
}
|
value={showRegister ? registerData.email : loginData.email}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
onChange={(e) => {
|
||||||
} catch (error) {
|
setLoginData((prev) => ({ ...prev, email: e.target.value }));
|
||||||
setShowErrorLogin(true); // Fehlermeldung anzeigen
|
setRegisterData((prev) => ({ ...prev, email: e.target.value }));
|
||||||
}
|
}}
|
||||||
};
|
/>
|
||||||
|
<TextField
|
||||||
const handleRegister = async () => {
|
margin="dense"
|
||||||
try {
|
label={t("password")}
|
||||||
setShowErrorLogin(false); // Fehlermeldung zurücksetzen
|
type="password"
|
||||||
setShowErrorRegister(false); // Fehlermeldung zurücksetzen
|
fullWidth
|
||||||
await refetchRegister(); // Beispiel für den Refetch-Aufruf
|
value={showRegister ? registerData.password : loginData.password}
|
||||||
// Erfolgslogik hier
|
onChange={(e) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
setLoginData((prev) => ({ ...prev, password: e.target.value }));
|
||||||
} catch (error) {
|
setRegisterData((prev) => ({
|
||||||
setShowErrorRegister(true); // Fehlermeldung anzeigen
|
...prev,
|
||||||
}
|
password: e.target.value,
|
||||||
};
|
}));
|
||||||
|
}}
|
||||||
return (
|
/>
|
||||||
<Dialog open={open} onClose={handleClose} disableEnforceFocus>
|
{showRegister && (
|
||||||
<form onSubmit={e => {
|
<>
|
||||||
e.preventDefault();
|
<TextField
|
||||||
if (showRegister) {
|
margin="dense"
|
||||||
handleLogin();
|
label={t("firstName")}
|
||||||
} else {
|
type="text"
|
||||||
handleRegister();
|
fullWidth
|
||||||
|
value={registerData.customer.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setRegisterData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
customer: { ...prev.customer, name: e.target.value },
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}} noValidate>
|
/>
|
||||||
<DialogTitle>{showRegister ? t("register") : t("login")}</DialogTitle>
|
<TextField
|
||||||
<DialogContent>
|
margin="dense"
|
||||||
<TextField
|
label={t("lastName")}
|
||||||
margin="dense"
|
type="text"
|
||||||
label={t("email")}
|
fullWidth
|
||||||
type="email"
|
value={registerData.customer.surname}
|
||||||
fullWidth
|
onChange={(e) =>
|
||||||
value={showRegister ? registerData.email : loginData.email}
|
setRegisterData((prev) => ({
|
||||||
onChange={e => {
|
...prev,
|
||||||
setLoginData(prev => ({...prev, email: e.target.value}));
|
customer: { ...prev.customer, surname: e.target.value },
|
||||||
setRegisterData(prev => ({...prev, email: e.target.value}))
|
}))
|
||||||
}}
|
}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
label={t("password")}
|
label={t("address")}
|
||||||
type="password"
|
type="text"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={showRegister ? registerData.password : loginData.password}
|
value={registerData.customer.address}
|
||||||
onChange={e => {
|
onChange={(e) =>
|
||||||
setLoginData(prev => ({...prev, password: e.target.value}))
|
setRegisterData((prev) => ({
|
||||||
setRegisterData(prev => ({...prev, password: e.target.value}))
|
...prev,
|
||||||
}}
|
customer: { ...prev.customer, address: e.target.value },
|
||||||
/>
|
}))
|
||||||
{showRegister &&
|
}
|
||||||
<>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
label={t("firstName")}
|
label={t("country")}
|
||||||
type="text"
|
type="text"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={registerData.customer.name}
|
value={registerData.customer.country}
|
||||||
onChange={e => setRegisterData(prev => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setRegisterData((prev) => ({
|
||||||
customer: {...prev.customer, name: e.target.value},
|
...prev,
|
||||||
}))}
|
customer: { ...prev.customer, country: e.target.value },
|
||||||
/>
|
}))
|
||||||
<TextField
|
}
|
||||||
margin="dense"
|
/>
|
||||||
label={t("lastName")}
|
<TextField
|
||||||
type="text"
|
margin="dense"
|
||||||
fullWidth
|
label={t("zip")}
|
||||||
value={registerData.customer.surname}
|
type="text"
|
||||||
onChange={e => setRegisterData(prev => ({
|
fullWidth
|
||||||
...prev,
|
value={registerData.customer.zip}
|
||||||
customer: {...prev.customer, surname: e.target.value},
|
onChange={(e) =>
|
||||||
}))}
|
setRegisterData((prev) => ({
|
||||||
/>
|
...prev,
|
||||||
<TextField
|
customer: { ...prev.customer, zip: e.target.value },
|
||||||
margin="dense"
|
}))
|
||||||
label={t("address")}
|
}
|
||||||
type="text"
|
/>
|
||||||
fullWidth
|
</>
|
||||||
value={registerData.customer.address}
|
)}
|
||||||
onChange={e => setRegisterData(prev => ({
|
</DialogContent>
|
||||||
...prev,
|
<DialogActions>
|
||||||
customer: {...prev.customer, address: e.target.value},
|
<Button onClick={handleClose}>{t("cancel")}</Button>
|
||||||
}))}
|
{showRegister ? (
|
||||||
/>
|
<Button onClick={handleRegister} disabled={isLoadingRegister}>
|
||||||
<TextField
|
{isLoadingRegister ? t("loading") : t("register")}
|
||||||
margin="dense"
|
</Button>
|
||||||
label={t("country")}
|
) : (
|
||||||
type="text"
|
<Button onClick={handleLogin} disabled={isLoadingLogin}>
|
||||||
fullWidth
|
{isLoadingLogin ? t("loading") : t("login")}
|
||||||
value={registerData.customer.country}
|
</Button>
|
||||||
onChange={e => setRegisterData(prev => ({
|
)}
|
||||||
...prev,
|
</DialogActions>
|
||||||
customer: {...prev.customer, country: e.target.value},
|
{showErrorLogin && errorLogin && (
|
||||||
}))}
|
<Box color="error.main">{t("loginFailed")}</Box>
|
||||||
/>
|
)}
|
||||||
<TextField
|
{showErrorRegister && errorRegister !== null && (
|
||||||
margin="dense"
|
<Box color="error.main">{t("registerFailed")}</Box>
|
||||||
label={t("zip")}
|
)}
|
||||||
type="text"
|
{showRegister ? (
|
||||||
fullWidth
|
<Box sx={{ width: "100%", textAlign: "center", pb: 2 }}>
|
||||||
value={registerData.customer.zip}
|
<Link
|
||||||
onChange={e => setRegisterData(prev => ({
|
component="button"
|
||||||
...prev,
|
variant="body2"
|
||||||
customer: {...prev.customer, zip: e.target.value},
|
onClick={() => setShowRegister(false)}
|
||||||
}))}
|
color="primary"
|
||||||
/>
|
underline="hover"
|
||||||
</>
|
>
|
||||||
}
|
{t("backToLogin")}
|
||||||
</DialogContent>
|
</Link>
|
||||||
<DialogActions>
|
</Box>
|
||||||
<Button onClick={handleClose}>{t("cancel")}</Button>
|
) : (
|
||||||
{showRegister ? (
|
<Box sx={{ width: "100%", textAlign: "center", pb: 2 }}>
|
||||||
<Button onClick={handleRegister} disabled={isLoadingRegister}>
|
<Link
|
||||||
{isLoadingRegister ? t("loading") : t("register")}
|
component="button"
|
||||||
</Button>
|
variant="body2"
|
||||||
) : (
|
onClick={() => setShowRegister(true)}
|
||||||
<Button onClick={handleLogin} disabled={isLoadingLogin}>
|
color="primary"
|
||||||
{isLoadingLogin ? t("loading") : t("login")}
|
underline="hover"
|
||||||
</Button>
|
>
|
||||||
)}
|
{t("noAccountRegister")}
|
||||||
</DialogActions>
|
</Link>
|
||||||
{showErrorLogin && errorLogin && (
|
</Box>
|
||||||
<Box color="error.main">{t("loginFailed")}</Box>
|
)}
|
||||||
)}
|
</form>
|
||||||
{showErrorRegister && errorRegister !== null && (
|
</Dialog>
|
||||||
<Box color="error.main">{t("registerFailed")}</Box>
|
);
|
||||||
)}
|
|
||||||
{showRegister ? (
|
|
||||||
<Box sx={{width: '100%', textAlign: 'center', pb: 2}}>
|
|
||||||
<Link
|
|
||||||
component="button"
|
|
||||||
variant="body2"
|
|
||||||
onClick={() => setShowRegister(false)}
|
|
||||||
color="primary"
|
|
||||||
underline="hover"
|
|
||||||
>
|
|
||||||
{t("backToLogin")}
|
|
||||||
</Link>
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Box sx={{width: '100%', textAlign: 'center', pb: 2}}>
|
|
||||||
<Link
|
|
||||||
component="button"
|
|
||||||
variant="body2"
|
|
||||||
onClick={() => setShowRegister(true)}
|
|
||||||
color="primary"
|
|
||||||
underline="hover"
|
|
||||||
>
|
|
||||||
{t("noAccountRegister")}
|
|
||||||
</Link>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LoginDialog;
|
export default LoginDialog;
|
||||||
|
|||||||
@@ -1,80 +1,80 @@
|
|||||||
/* Navbar styles */
|
/* Navbar styles */
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
/*color in tsx*/
|
/*color in tsx*/
|
||||||
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
|
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
height: var(--navbar-height);
|
height: var(--navbar-height);
|
||||||
min-height: 64px;
|
min-height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Logo styles */
|
/* Logo styles */
|
||||||
.navbar-logo {
|
.navbar-logo {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Menu styles */
|
/* Menu styles */
|
||||||
.navbar-menu {
|
.navbar-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search styles */
|
/* Search styles */
|
||||||
.search {
|
.search {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: rgba(255, 255, 255, 0.15);
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search:hover {
|
.search:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.25);
|
background-color: rgba(255, 255, 255, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-icon-wrapper {
|
.search-icon-wrapper {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px 8px 8px 40px;
|
padding: 8px 8px 8px 40px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* User avatar styles */
|
/* User avatar styles */
|
||||||
.navbar-user {
|
.navbar-user {
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Typography styles */
|
/* Typography styles */
|
||||||
.navbar-typography {
|
.navbar-typography {
|
||||||
font-family: 'monospace';
|
font-family: "monospace";
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: .3rem;
|
letter-spacing: 0.3rem;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Button styles */
|
/* Button styles */
|
||||||
.navbar-button {
|
.navbar-button {
|
||||||
margin: 2;
|
margin: 2;
|
||||||
color: white;
|
color: white;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-offset {
|
.navbar-offset {
|
||||||
height: var(--navbar-height);
|
height: var(--navbar-height);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,331 +1,348 @@
|
|||||||
import MenuIcon from '@mui/icons-material/Menu';
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
import {Autocomplete, Badge, TextField} from '@mui/material';
|
import { Autocomplete, Badge, TextField } from "@mui/material";
|
||||||
import AppBar from '@mui/material/AppBar';
|
import AppBar from "@mui/material/AppBar";
|
||||||
import Avatar from '@mui/material/Avatar';
|
import Avatar from "@mui/material/Avatar";
|
||||||
import Box from '@mui/material/Box';
|
import Box from "@mui/material/Box";
|
||||||
import Button from '@mui/material/Button';
|
import Button from "@mui/material/Button";
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from "@mui/material/IconButton";
|
||||||
import Menu from '@mui/material/Menu';
|
import Menu from "@mui/material/Menu";
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from "@mui/material/Typography";
|
||||||
import {useQuery} from '@tanstack/react-query';
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import {useTranslation} from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import {useNavigate} from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
import Item from '../../components/Item';
|
import Item from "../../components/Item";
|
||||||
import ThemeToggle from '../../theme/ThemeToggle';
|
import ThemeToggle from "../../theme/ThemeToggle";
|
||||||
import {useAccount} from '../AccountProvider';
|
import { useAccount } from "../AccountProvider";
|
||||||
import {fetchItemList} from '../query/Queries';
|
import { fetchItemList } from "../query/Queries";
|
||||||
import LoginDialog from './LoginDialog';
|
import LoginDialog from "./LoginDialog";
|
||||||
import './NavBar.css';
|
import "./NavBar.css";
|
||||||
import {useBasket} from '../BasketProvider';
|
import { useBasket } from "../BasketProvider";
|
||||||
import logo from '../../assets/logo/Blume-logo.png';
|
import logo from "../../assets/logo/Blume-logo.png";
|
||||||
|
|
||||||
|
|
||||||
export default function NavBar() {
|
export default function NavBar() {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(null);
|
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(
|
||||||
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(null);
|
null,
|
||||||
const [avatarName, setAvatarName] = React.useState<string>(''); // Für Avatar-Tooltip
|
);
|
||||||
|
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [avatarName, setAvatarName] = React.useState<string>(""); // Für Avatar-Tooltip
|
||||||
|
|
||||||
const {user, logout} = useAccount();
|
const { user, logout } = useAccount();
|
||||||
|
|
||||||
const {basket} = useBasket();
|
const { basket } = useBasket();
|
||||||
|
|
||||||
const totalQuantity = basket?.reduce((sum, item) => sum + item.quantity, 0) ?? 0;
|
const totalQuantity =
|
||||||
|
basket?.reduce((sum, item) => sum + item.quantity, 0) ?? 0;
|
||||||
|
|
||||||
|
const [loginOpen, setLoginOpen] = React.useState(false);
|
||||||
|
const [loginData, setLoginData] = React.useState({
|
||||||
|
password: "",
|
||||||
|
email: "",
|
||||||
|
customerId: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const [loginOpen, setLoginOpen] = React.useState(false);
|
const [itemNames, setItemNames] = React.useState<string[]>([]); // Für Autocomplete
|
||||||
const [loginData, setLoginData] = React.useState({password: '', email: '', customerId: 0});
|
|
||||||
|
|
||||||
|
const pageKeys = ["components", "checkout", "contact", "admin"];
|
||||||
|
|
||||||
const [itemNames, setItemNames] = React.useState<string[]>([]); // Für Autocomplete
|
const filteredPages = pageKeys
|
||||||
|
.filter((key) => {
|
||||||
|
if (key === "admin") {
|
||||||
|
return user?.isAdmin === true; // nur Admins sehen Admin-Seite
|
||||||
|
}
|
||||||
|
return true; // alle anderen Seiten immer anzeigen
|
||||||
|
})
|
||||||
|
.map((key) => ({ key, label: t(key) }));
|
||||||
|
|
||||||
const pageKeys = ['components', 'checkout', 'contact', 'admin'];
|
const settings = user
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "email",
|
||||||
|
label: `${t("loggedInAs")}: ${user.email}`,
|
||||||
|
disabled: true, // wir nutzen dieses Flag gleich zur Erkennung
|
||||||
|
},
|
||||||
|
{ key: "account", label: t("account") },
|
||||||
|
{ key: "orders", label: t("orders") },
|
||||||
|
{ key: "logout", label: t("logout") },
|
||||||
|
]
|
||||||
|
: [{ key: "login", label: t("login") }];
|
||||||
|
|
||||||
const filteredPages = pageKeys
|
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
.filter(key => {
|
setAnchorElNav(event.currentTarget);
|
||||||
if (key === "admin") {
|
};
|
||||||
return user?.isAdmin === true; // nur Admins sehen Admin-Seite
|
|
||||||
}
|
|
||||||
return true; // alle anderen Seiten immer anzeigen
|
|
||||||
})
|
|
||||||
.map(key => ({key, label: t(key)}));
|
|
||||||
|
|
||||||
|
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorElUser(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
const settings = user
|
const handleCloseNavMenu = (link: string) => {
|
||||||
? [
|
setAnchorElNav(null);
|
||||||
{
|
navigate(`/${link.toLowerCase()}`);
|
||||||
key: 'email',
|
};
|
||||||
label: `${t('loggedInAs')}: ${user.email}`,
|
|
||||||
disabled: true // wir nutzen dieses Flag gleich zur Erkennung
|
|
||||||
},
|
|
||||||
{key: 'account', label: t('account')},
|
|
||||||
{key: 'orders', label: t('orders')},
|
|
||||||
{key: 'logout', label: t('logout')}
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{key: 'login', label: t('login')}
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
|
const handleCloseUserMenu = (link: string) => {
|
||||||
setAnchorElNav(event.currentTarget);
|
setAnchorElUser(null);
|
||||||
};
|
if (link === "login") {
|
||||||
|
setLoginOpen(true);
|
||||||
|
} else if (link === "logout") {
|
||||||
|
logout();
|
||||||
|
if (
|
||||||
|
location.pathname.startsWith("/account") ||
|
||||||
|
location.pathname.startsWith("/orders")
|
||||||
|
) {
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
navigate(`/${link.toLowerCase()}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
|
const handleLoginSubmit = () => {
|
||||||
setAnchorElUser(event.currentTarget);
|
setLoginOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseNavMenu = (link: string) => {
|
// useQuery, um die Item-Namen zu laden
|
||||||
setAnchorElNav(null);
|
const { data: items = [] } = useQuery<Item[]>({
|
||||||
navigate(`/${link.toLowerCase()}`);
|
queryKey: ["fetchItemList"],
|
||||||
};
|
queryFn: fetchItemList,
|
||||||
|
});
|
||||||
|
|
||||||
const handleCloseUserMenu = (link: string) => {
|
React.useEffect(() => {
|
||||||
setAnchorElUser(null);
|
// Extrahiere die Namen der Items für Autocomplete
|
||||||
if (link === 'login') {
|
setItemNames(items.map((item) => item.name));
|
||||||
setLoginOpen(true);
|
}, [items]);
|
||||||
} else if (link === 'logout') {
|
|
||||||
logout();
|
|
||||||
if (
|
|
||||||
location.pathname.startsWith('/account') ||
|
|
||||||
location.pathname.startsWith('/orders')
|
|
||||||
) {
|
|
||||||
navigate('/');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
navigate(`/${link.toLowerCase()}`)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoginSubmit = () => {
|
React.useEffect(() => {
|
||||||
setLoginOpen(false);
|
// Setze den Avatar-Namen, wenn der Benutzer angemeldet ist
|
||||||
};
|
if (user !== undefined && user !== null) {
|
||||||
|
setAvatarName(user.email.toUpperCase());
|
||||||
|
}
|
||||||
|
if (!user) {
|
||||||
|
setAvatarName("");
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const handleSearch = (_: React.SyntheticEvent, value: string | null) => {
|
||||||
|
if (!value) {
|
||||||
|
// Wenn der Suchwert leer ist, navigiere zur Homepage ohne Suchparameter
|
||||||
|
navigate("/");
|
||||||
|
} else {
|
||||||
|
// Navigiere zur Homepage mit dem Suchparameter
|
||||||
|
navigate(`/?search=${encodeURIComponent(value)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// useQuery, um die Item-Namen zu laden
|
return (
|
||||||
const {data: items = []} = useQuery<Item[]>({
|
<>
|
||||||
queryKey: ["fetchItemList"],
|
<AppBar className="navbar" color="primary" elevation={4}>
|
||||||
queryFn: fetchItemList,
|
<Toolbar
|
||||||
});
|
disableGutters
|
||||||
|
sx={{
|
||||||
React.useEffect(() => {
|
display: "flex",
|
||||||
// Extrahiere die Namen der Items für Autocomplete
|
justifyContent: "space-between",
|
||||||
setItemNames(items.map((item) => item.name));
|
alignItems: "center", // <--- HIER hinzugefügt
|
||||||
}, [items]);
|
px: 3,
|
||||||
|
minHeight: { xs: 56, sm: 64 }, // optional: Standardhöhe für bessere Zentrierung
|
||||||
React.useEffect(() => {
|
}}
|
||||||
// Setze den Avatar-Namen, wenn der Benutzer angemeldet ist
|
>
|
||||||
if (user !== undefined && user !== null) {
|
<Box sx={{ display: "flex", alignItems: "center", minWidth: "0px" }}>
|
||||||
setAvatarName(user.email.toUpperCase());
|
<img
|
||||||
}
|
src={logo}
|
||||||
if (!user) {
|
alt="Logo"
|
||||||
setAvatarName('');
|
className="navbar-logo"
|
||||||
}
|
style={{
|
||||||
}, [user]);
|
display: "block",
|
||||||
|
height: 40,
|
||||||
const handleSearch = (_: React.SyntheticEvent, value: string | null) => {
|
marginRight: 12,
|
||||||
|
objectFit: "contain",
|
||||||
if (!value) {
|
}} // optional: Höhe anpassen
|
||||||
// Wenn der Suchwert leer ist, navigiere zur Homepage ohne Suchparameter
|
onClick={() => navigate("/")}
|
||||||
navigate("/");
|
|
||||||
} else {
|
|
||||||
// Navigiere zur Homepage mit dem Suchparameter
|
|
||||||
navigate(`/?search=${encodeURIComponent(value)}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AppBar className="navbar" color="primary" elevation={4}>
|
|
||||||
<Toolbar
|
|
||||||
disableGutters
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center', // <--- HIER hinzugefügt
|
|
||||||
px: 3,
|
|
||||||
minHeight: { xs: 56, sm: 64 }, // optional: Standardhöhe für bessere Zentrierung
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{display: "flex", alignItems: "center", minWidth: "0px"}}>
|
|
||||||
<img
|
|
||||||
src={logo}
|
|
||||||
alt="Logo"
|
|
||||||
className="navbar-logo"
|
|
||||||
style={{ display: "block", height: 40, marginRight: 12, objectFit: "contain" }} // optional: Höhe anpassen
|
|
||||||
onClick={() => navigate('/')}
|
|
||||||
/>
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
noWrap
|
|
||||||
component="a"
|
|
||||||
onClick={() => navigate('/')}
|
|
||||||
sx={{
|
|
||||||
fontFamily: "monospace",
|
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: ".3rem",
|
|
||||||
color: "white",
|
|
||||||
textDecoration: "none",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center", // <--- HIER hinzugefügt
|
|
||||||
height: "100%", // optional
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Digitaler Produktionsshop
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{
|
|
||||||
flexGrow: 1,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center", // <--- HIER hinzugefügt
|
|
||||||
px: 3,
|
|
||||||
zIndex: 100000
|
|
||||||
}}>
|
|
||||||
<Autocomplete
|
|
||||||
sx={{
|
|
||||||
flexGrow: 1,
|
|
||||||
minWidth: "150px",
|
|
||||||
maxWidth: "600px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center" // <--- HIER hinzugefügt
|
|
||||||
}}
|
|
||||||
freeSolo
|
|
||||||
options={itemNames}
|
|
||||||
onInputChange={handleSearch}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
placeholder={t("search") + "..."}
|
|
||||||
InputProps={{
|
|
||||||
...params.InputProps,
|
|
||||||
startAdornment: (
|
|
||||||
<SearchIcon sx={{color: "white", mr: 1}}/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
'& .MuiOutlinedInput-root': {
|
|
||||||
'& fieldset': {
|
|
||||||
borderColor: 'white',
|
|
||||||
borderWidth: '1px',
|
|
||||||
},
|
|
||||||
'&:hover fieldset': {
|
|
||||||
borderColor: 'white',
|
|
||||||
},
|
|
||||||
'&.Mui-focused fieldset': {
|
|
||||||
borderColor: 'white',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
color: 'white',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{display: "flex", alignItems: "center", gap: 2, marginLeft: 'auto'}}>
|
|
||||||
<Box sx={{display: {xs: "none", md: "flex"}, gap: 2}}>
|
|
||||||
{filteredPages.map(({key, label}) => {
|
|
||||||
if (key === 'checkout') {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={key}
|
|
||||||
onClick={() => handleCloseNavMenu(key)}
|
|
||||||
sx={{color: "white", fontWeight: 500}}
|
|
||||||
>
|
|
||||||
<Badge
|
|
||||||
badgeContent={totalQuantity}
|
|
||||||
color="error"
|
|
||||||
overlap="rectangular"
|
|
||||||
showZero={false}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Badge>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={key}
|
|
||||||
onClick={() => handleCloseNavMenu(key)}
|
|
||||||
sx={{color: "white", fontWeight: 500}}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{display: {xs: "flex", md: "none"}}}>
|
|
||||||
<IconButton
|
|
||||||
size="large"
|
|
||||||
aria-label={t('menu')}
|
|
||||||
onClick={handleOpenNavMenu}
|
|
||||||
color="inherit"
|
|
||||||
>
|
|
||||||
<MenuIcon/>
|
|
||||||
</IconButton>
|
|
||||||
<Menu
|
|
||||||
anchorEl={anchorElNav}
|
|
||||||
anchorOrigin={{vertical: "top", horizontal: "right"}}
|
|
||||||
transformOrigin={{vertical: "top", horizontal: "right"}}
|
|
||||||
open={Boolean(anchorElNav)}
|
|
||||||
onClose={() => setAnchorElNav(null)}
|
|
||||||
>
|
|
||||||
{filteredPages.map(({key, label}) => (
|
|
||||||
<MenuItem key={key} onClick={() => handleCloseNavMenu(key)}>
|
|
||||||
<Typography>{label}</Typography>
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Menu>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<ThemeToggle/>
|
|
||||||
<Tooltip title={t('openSettings')} placement='bottom-end'>
|
|
||||||
<IconButton onClick={handleOpenUserMenu} sx={{p: 0}}>
|
|
||||||
<Avatar alt={avatarName} src="/static/images/avatar/2.jpg"/>
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Menu
|
|
||||||
sx={{mt: "15px"}}
|
|
||||||
id="menu-appbar-user"
|
|
||||||
anchorEl={anchorElUser}
|
|
||||||
open={Boolean(anchorElUser)}
|
|
||||||
onClose={() => setAnchorElUser(null)}
|
|
||||||
>
|
|
||||||
{settings.map(({key, label, disabled}) => (
|
|
||||||
<MenuItem
|
|
||||||
key={key}
|
|
||||||
onClick={() => {
|
|
||||||
if (!disabled) handleCloseUserMenu(key);
|
|
||||||
}}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<Typography sx={{textAlign: "center"}}>{label}</Typography>
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Menu>
|
|
||||||
</Box>
|
|
||||||
</Toolbar>
|
|
||||||
</AppBar>
|
|
||||||
<LoginDialog
|
|
||||||
open={loginOpen}
|
|
||||||
onClose={() => setLoginOpen(false)}
|
|
||||||
onSubmit={handleLoginSubmit}
|
|
||||||
loginData={loginData}
|
|
||||||
setLoginData={setLoginData}
|
|
||||||
/>
|
/>
|
||||||
<div className="navbar-offset"/>
|
<Typography
|
||||||
</>
|
variant="h6"
|
||||||
);
|
noWrap
|
||||||
|
component="a"
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
sx={{
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: ".3rem",
|
||||||
|
color: "white",
|
||||||
|
textDecoration: "none",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center", // <--- HIER hinzugefügt
|
||||||
|
height: "100%", // optional
|
||||||
|
":hover": {
|
||||||
|
color: "#fff1d8ff",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Digitaler Produktionsshop
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center", // <--- HIER hinzugefügt
|
||||||
|
px: 3,
|
||||||
|
zIndex: 100000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Autocomplete
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
minWidth: "150px",
|
||||||
|
maxWidth: "600px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center", // <--- HIER hinzugefügt
|
||||||
|
}}
|
||||||
|
freeSolo
|
||||||
|
options={itemNames}
|
||||||
|
onInputChange={handleSearch}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
placeholder={t("search") + "..."}
|
||||||
|
InputProps={{
|
||||||
|
...params.InputProps,
|
||||||
|
startAdornment: (
|
||||||
|
<SearchIcon sx={{ color: "white", mr: 1 }} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
"& fieldset": {
|
||||||
|
borderColor: "white",
|
||||||
|
borderWidth: "1px",
|
||||||
|
},
|
||||||
|
"&:hover fieldset": {
|
||||||
|
borderColor: "white",
|
||||||
|
},
|
||||||
|
"&.Mui-focused fieldset": {
|
||||||
|
borderColor: "white",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
color: "white",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 2,
|
||||||
|
marginLeft: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: { xs: "none", md: "flex" }, gap: 2 }}>
|
||||||
|
{filteredPages.map(({ key, label }) => {
|
||||||
|
if (key === "checkout") {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={key}
|
||||||
|
onClick={() => handleCloseNavMenu(key)}
|
||||||
|
sx={{ color: "white", fontWeight: 500 }}
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
badgeContent={totalQuantity}
|
||||||
|
color="error"
|
||||||
|
overlap="rectangular"
|
||||||
|
showZero={false}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Badge>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={key}
|
||||||
|
onClick={() => handleCloseNavMenu(key)}
|
||||||
|
sx={{ color: "white", fontWeight: 500 }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: { xs: "flex", md: "none" } }}>
|
||||||
|
<IconButton
|
||||||
|
size="large"
|
||||||
|
aria-label={t("menu")}
|
||||||
|
onClick={handleOpenNavMenu}
|
||||||
|
color="inherit"
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorElNav}
|
||||||
|
anchorOrigin={{ vertical: "top", horizontal: "right" }}
|
||||||
|
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||||
|
open={Boolean(anchorElNav)}
|
||||||
|
onClose={() => setAnchorElNav(null)}
|
||||||
|
>
|
||||||
|
{filteredPages.map(({ key, label }) => (
|
||||||
|
<MenuItem key={key} onClick={() => handleCloseNavMenu(key)}>
|
||||||
|
<Typography>{label}</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ThemeToggle />
|
||||||
|
<Tooltip title={t("openSettings")} placement="bottom-end">
|
||||||
|
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
|
||||||
|
<Avatar alt={avatarName} src="/static/images/avatar/2.jpg" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Menu
|
||||||
|
sx={{ mt: "15px" }}
|
||||||
|
id="menu-appbar-user"
|
||||||
|
anchorEl={anchorElUser}
|
||||||
|
open={Boolean(anchorElUser)}
|
||||||
|
onClose={() => setAnchorElUser(null)}
|
||||||
|
>
|
||||||
|
{settings.map(({ key, label, disabled }) => (
|
||||||
|
<MenuItem
|
||||||
|
key={key}
|
||||||
|
onClick={() => {
|
||||||
|
if (!disabled) handleCloseUserMenu(key);
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<Typography sx={{ textAlign: "center" }}>{label}</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
<LoginDialog
|
||||||
|
open={loginOpen}
|
||||||
|
onClose={() => setLoginOpen(false)}
|
||||||
|
onSubmit={handleLoginSubmit}
|
||||||
|
loginData={loginData}
|
||||||
|
setLoginData={setLoginData}
|
||||||
|
/>
|
||||||
|
<div className="navbar-offset" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,209 +1,216 @@
|
|||||||
import {Close, LocalShipping, ShoppingCart} from "@mui/icons-material";
|
import { Close, LocalShipping, ShoppingCart } from "@mui/icons-material";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Divider,
|
Divider,
|
||||||
Grid,
|
Grid,
|
||||||
IconButton,
|
IconButton,
|
||||||
Rating,
|
Rating,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
SnackbarCloseReason,
|
SnackbarCloseReason,
|
||||||
Stack,
|
Stack,
|
||||||
TextField,
|
TextField,
|
||||||
Typography
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Item from "../../components/Item";
|
import Item from "../../components/Item";
|
||||||
import {useBasket} from "../BasketProvider";
|
import { useBasket } from "../BasketProvider";
|
||||||
|
|
||||||
export default function ProductInfo({item}: { item: Item }) {
|
export default function ProductInfo({ item }: { item: Item }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [quantity, setQuantity] = useState<number>(1);
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
const [imageDimensions, setImageDimensions] = useState({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const {t} = useTranslation();
|
const { addToBasket } = useBasket();
|
||||||
const [quantity, setQuantity] = useState<number>(1);
|
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
|
||||||
const [imageDimensions, setImageDimensions] = useState({width: 0, height: 0});
|
|
||||||
|
|
||||||
|
const handleClose = (
|
||||||
|
_: React.SyntheticEvent | Event,
|
||||||
|
reason?: SnackbarCloseReason,
|
||||||
|
) => {
|
||||||
|
if (reason === "clickaway") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const {addToBasket} = useBasket();
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleClose = (
|
const action = (
|
||||||
_: React.SyntheticEvent | Event,
|
<React.Fragment>
|
||||||
reason?: SnackbarCloseReason,
|
<IconButton
|
||||||
) => {
|
size="small"
|
||||||
if (reason === 'clickaway') {
|
aria-label={t("close")}
|
||||||
return;
|
color="inherit"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
<Close fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddToCart = () => {
|
||||||
|
addToBasket(item, quantity);
|
||||||
|
setOpen(true);
|
||||||
|
console.log(`Added {quantity} of €{item.name} to basket`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const discountedPrice = item.price100 * (1 - item.discount100 / 100);
|
||||||
|
|
||||||
|
const handleImageLoad = (event: React.SyntheticEvent<HTMLImageElement>) => {
|
||||||
|
const { naturalWidth, naturalHeight } = event.currentTarget;
|
||||||
|
setImageDimensions({ width: naturalWidth, height: naturalHeight });
|
||||||
|
};
|
||||||
|
|
||||||
|
const [imageUrl, setImageUrl] = useState<string>("/src/assets/default.jpg"); // Fallback-Bild
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchImage = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`http://localhost:8085/image?uuid=${item.uuid}`,
|
||||||
|
);
|
||||||
|
let data = await response.text();
|
||||||
|
if (data.length == 0) {
|
||||||
|
console.error("Got emtpy picture for article ", item.uuid);
|
||||||
}
|
}
|
||||||
|
if (!data.startsWith("data:image/")) {
|
||||||
setOpen(false);
|
data = "data:image/jpeg;base64," + data;
|
||||||
|
}
|
||||||
|
setImageUrl(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fehler beim Laden des Bildes:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = (
|
fetchImage();
|
||||||
<React.Fragment>
|
}, [item.uuid]);
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
aria-label={t('close')}
|
|
||||||
color="inherit"
|
|
||||||
onClick={handleClose}
|
|
||||||
>
|
|
||||||
<Close fontSize="small"/>
|
|
||||||
</IconButton>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={4}>
|
||||||
|
{/* Left Column - Image */}
|
||||||
|
|
||||||
const handleAddToCart = () => {
|
<Card
|
||||||
addToBasket(item, quantity);
|
elevation={2}
|
||||||
setOpen(true);
|
sx={{ width: "100%", maxWidth: 400, display: "inherit" }}
|
||||||
console.log(`Added {quantity} of €{item.name} to basket`);
|
>
|
||||||
};
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={imageUrl}
|
||||||
|
alt={item.name}
|
||||||
|
onLoad={handleImageLoad} // Event-Handler zum Ermitteln der Bildgröße
|
||||||
|
onError={(event) => {
|
||||||
|
event.currentTarget.src = "/src/assets/default.jpg"; // Standardbild setzen
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
maxWidth:
|
||||||
|
imageDimensions.width > imageDimensions.height ? "100%" : "auto",
|
||||||
|
maxHeight:
|
||||||
|
imageDimensions.height >= imageDimensions.width ? 400 : "auto",
|
||||||
|
width: "auto",
|
||||||
|
height: "auto",
|
||||||
|
objectFit: "contain",
|
||||||
|
margin: "auto",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
const discountedPrice = item.price100 * (1 - item.discount100 / 100);
|
{/* Right Column - Product Details */}
|
||||||
|
|
||||||
const handleImageLoad = (event: React.SyntheticEvent<HTMLImageElement>) => {
|
<Stack spacing={3}>
|
||||||
const {naturalWidth, naturalHeight} = event.currentTarget;
|
<Typography variant="h4" component="h1">
|
||||||
setImageDimensions({width: naturalWidth, height: naturalHeight});
|
{item.name}
|
||||||
};
|
</Typography>
|
||||||
|
|
||||||
const [imageUrl, setImageUrl] = useState<string>("/src/assets/default.jpg"); // Fallback-Bild
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
|
<Rating value={item.rating / 2} precision={0.5} readOnly />
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{item.rating > 0 ? `(${item.rating / 2} / 5)` : t("noRatingsYet")}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
useEffect(() => {
|
<Stack direction="row" alignItems="center" spacing={2}>
|
||||||
const fetchImage = async () => {
|
{item.discount100 > 0 ? (
|
||||||
try {
|
<>
|
||||||
const response = await fetch(`http://localhost:8085/image?uuid=${item.uuid}`);
|
<Typography variant="h4" color="green">
|
||||||
let data = await response.text();
|
{(discountedPrice / 100).toFixed(2)} €
|
||||||
if (data.length == 0) {
|
</Typography>
|
||||||
console.error("Got emtpy picture for article ", item.uuid);
|
<Typography
|
||||||
}
|
variant="h6"
|
||||||
if (!data.startsWith("data:image/")) {
|
color="text.secondary"
|
||||||
data = "data:image/jpeg;base64," + data
|
sx={{ textDecoration: "line-through" }}
|
||||||
}
|
>
|
||||||
setImageUrl(data);
|
{(item.price100 / 100).toFixed(2)} €
|
||||||
} catch (error) {
|
</Typography>
|
||||||
console.error("Fehler beim Laden des Bildes:", error);
|
<Typography variant="h6" color="error">
|
||||||
}
|
-{item.discount100} %
|
||||||
};
|
</Typography>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Typography variant="h4" color="green">
|
||||||
|
{(item.price100 / 100).toFixed(2)} €
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
fetchImage();
|
<Divider />
|
||||||
}, [item.uuid]);
|
|
||||||
|
|
||||||
return (
|
<Box>
|
||||||
<Grid container spacing={4}>
|
{item.stock > 10 ? (
|
||||||
{/* Left Column - Image */}
|
<Alert severity="success" variant="outlined">
|
||||||
|
{t("inStock")} ({item.stock} {t("available")})
|
||||||
|
</Alert>
|
||||||
|
) : item.stock > 0 ? (
|
||||||
|
<Alert severity="warning" variant="outlined">
|
||||||
|
{t("almostSoldOut")} ({item.stock} {t("available")})
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<Alert severity="error" variant="filled">
|
||||||
|
{t("outOfStock")}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Card elevation={2} sx={{width: '100%', maxWidth: 400, display: 'inherit'}}>
|
<Stack direction="row" spacing={2} alignItems="center">
|
||||||
<Box
|
<TextField
|
||||||
component="img"
|
type="number"
|
||||||
src={imageUrl}
|
label={t("quantity")}
|
||||||
alt={item.name}
|
value={quantity}
|
||||||
onLoad={handleImageLoad} // Event-Handler zum Ermitteln der Bildgröße
|
onChange={(e) => setQuantity(Math.max(1, parseInt(e.target.value)))}
|
||||||
onError={(event) => {
|
InputProps={{ inputProps: { min: 1, max: item.stock } }}
|
||||||
event.currentTarget.src = "/src/assets/default.jpg"; // Standardbild setzen
|
sx={{ width: 100 }}
|
||||||
}}
|
/>
|
||||||
sx={{
|
<Button
|
||||||
maxWidth: imageDimensions.width > imageDimensions.height ? "100%" : "auto",
|
variant="contained"
|
||||||
maxHeight: imageDimensions.height >= imageDimensions.width ? 400 : "auto",
|
size="large"
|
||||||
width: "auto",
|
startIcon={<ShoppingCart />}
|
||||||
height: "auto",
|
onClick={handleAddToCart}
|
||||||
objectFit: "contain",
|
disabled={item.stock <= 0}
|
||||||
}}
|
fullWidth
|
||||||
/>
|
>
|
||||||
</Card>
|
{t("addToCart")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
{/* Right Column - Product Details */}
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
<LocalShipping sx={{ mr: 1, verticalAlign: "middle" }} />
|
||||||
<Stack spacing={3}>
|
{t("freeShipping")}
|
||||||
<Typography variant="h4" component="h1">
|
</Typography>
|
||||||
{item.name}
|
</Box>
|
||||||
</Typography>
|
</Stack>
|
||||||
|
<Snackbar
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
open={open}
|
||||||
<Rating value={item.rating / 2} precision={0.5} readOnly/>
|
autoHideDuration={3000}
|
||||||
<Typography variant="body2" color="text.secondary">
|
onClose={handleClose}
|
||||||
{item.rating > 0 ? `(${item.rating / 2} / 5)` : t('noRatingsYet')}
|
message={t("addedToCart")}
|
||||||
|
action={action}
|
||||||
</Typography>
|
/>
|
||||||
</Box>
|
</Grid>
|
||||||
|
);
|
||||||
<Stack direction="row" alignItems="center" spacing={2}>
|
}
|
||||||
{item.discount100 > 0 ? (
|
|
||||||
<>
|
|
||||||
<Typography variant="h4" color="green">
|
|
||||||
{(discountedPrice / 100).toFixed(2)} €
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{textDecoration: 'line-through'}}
|
|
||||||
>
|
|
||||||
{(item.price100 / 100).toFixed(2)} €
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="h6" color="error">
|
|
||||||
-{item.discount100} %
|
|
||||||
</Typography>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Typography variant="h4" color="green">
|
|
||||||
{(item.price100 / 100).toFixed(2)} €
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Divider/>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
{item.stock > 10 ? (
|
|
||||||
<Alert severity="success" variant='outlined'>
|
|
||||||
{t('inStock')} ({item.stock} {t('available')})
|
|
||||||
</Alert>
|
|
||||||
) : item.stock > 0 ? (
|
|
||||||
<Alert severity="warning"
|
|
||||||
variant='outlined'>{t('almostSoldOut')} ({item.stock} {t('available')})</Alert>
|
|
||||||
) : (
|
|
||||||
<Alert severity="error" variant='filled'>{t('outOfStock')}</Alert>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Stack direction="row" spacing={2} alignItems="center">
|
|
||||||
<TextField
|
|
||||||
type="number"
|
|
||||||
label={t('quantity')}
|
|
||||||
value={quantity}
|
|
||||||
onChange={(e) => setQuantity(Math.max(1, parseInt(e.target.value)))}
|
|
||||||
InputProps={{inputProps: {min: 1, max: item.stock}}}
|
|
||||||
sx={{width: 100}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
size="large"
|
|
||||||
startIcon={<ShoppingCart/>}
|
|
||||||
onClick={handleAddToCart}
|
|
||||||
disabled={item.stock <= 0}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
{t('addToCart')}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Box sx={{mt: 2}}>
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
<LocalShipping sx={{mr: 1, verticalAlign: 'middle'}}/>
|
|
||||||
{t('freeShipping')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
</Stack>
|
|
||||||
<Snackbar
|
|
||||||
open={open}
|
|
||||||
autoHideDuration={3000}
|
|
||||||
onClose={handleClose}
|
|
||||||
message={t('addedToCart')}
|
|
||||||
action={action}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,55 +1,63 @@
|
|||||||
import {Card, CardActionArea, CardContent, Paper, Rating, Typography, useTheme} from "@mui/material";
|
import {
|
||||||
|
Card,
|
||||||
|
CardActionArea,
|
||||||
|
CardContent,
|
||||||
|
Paper,
|
||||||
|
Rating,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
import RatingType from "../../components/Rating";
|
import RatingType from "../../components/Rating";
|
||||||
import {useTranslation} from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function RatingCard(ratingType: RatingType) {
|
export default function RatingCard(ratingType: RatingType) {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useTheme(); // Zugriff auf Light/Dark-Mode
|
const theme = useTheme(); // Zugriff auf Light/Dark-Mode
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {};
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
elevation={4}
|
elevation={4}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
mb: 3
|
mb: 3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<CardActionArea onClick={handleClick}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography
|
||||||
|
gutterBottom
|
||||||
|
variant="h6"
|
||||||
|
component="div"
|
||||||
|
sx={{ color: theme.palette.text.primary }}
|
||||||
>
|
>
|
||||||
<CardActionArea onClick={handleClick}>
|
{t("ratingFrom")}{" "}
|
||||||
<CardContent>
|
{new Date(ratingType.timestamp).toLocaleDateString("de-DE")}
|
||||||
<Typography
|
</Typography>
|
||||||
gutterBottom
|
|
||||||
variant="h6"
|
|
||||||
component="div"
|
|
||||||
sx={{color: theme.palette.text.primary}}
|
|
||||||
>
|
|
||||||
{t('ratingFrom')} {new Date(ratingType.timestamp).toLocaleDateString('de-DE')}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Rating
|
<Rating
|
||||||
name="half-rating"
|
name="half-rating"
|
||||||
readOnly
|
readOnly
|
||||||
defaultValue={ratingType.rating / 2}
|
defaultValue={ratingType.rating / 2}
|
||||||
precision={0.5}
|
precision={0.5}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{color: theme.palette.text.secondary, mt: 1}}
|
sx={{ color: theme.palette.text.secondary, mt: 1 }}
|
||||||
>
|
>
|
||||||
{ratingType.content}
|
{ratingType.content}
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</CardActionArea>
|
</CardActionArea>
|
||||||
</Card>
|
</Card>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,166 +1,170 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Divider,
|
Divider,
|
||||||
IconButton,
|
IconButton,
|
||||||
Rating,
|
Rating,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
SnackbarCloseReason,
|
SnackbarCloseReason,
|
||||||
TextField,
|
TextField,
|
||||||
Typography,
|
Typography,
|
||||||
useTheme
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {Close} from "@mui/icons-material";
|
import { Close } from "@mui/icons-material";
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import React, {useMemo, useState} from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import {useTranslation} from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import RatingType from "../../components/Rating";
|
import RatingType from "../../components/Rating";
|
||||||
import {fetchRatingList, submitRating} from "../query/Queries";
|
import { fetchRatingList, submitRating } from "../query/Queries";
|
||||||
import RatingCard from "./RatingCard";
|
import RatingCard from "./RatingCard";
|
||||||
import RatingSubmitType from "../../components/RatingSubmit";
|
import RatingSubmitType from "../../components/RatingSubmit";
|
||||||
|
|
||||||
export default function Ratings({itemId}: { itemId: string }) {
|
export default function Ratings({ itemId }: { itemId: string }) {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
const [ratingText, setRatingText] = useState<string>("");
|
const [ratingText, setRatingText] = useState<string>("");
|
||||||
const [ratingValue, setRatingValue] = useState<number | null>(2.5);
|
const [ratingValue, setRatingValue] = useState<number | null>(2.5);
|
||||||
|
|
||||||
const ratingData: RatingSubmitType = {
|
const ratingData: RatingSubmitType = {
|
||||||
rating: ratingValue || 0,
|
rating: ratingValue || 0,
|
||||||
content: ratingText || "",
|
content: ratingText || "",
|
||||||
articleId: itemId,
|
articleId: itemId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {refetch} = useQuery({
|
const { refetch } = useQuery({
|
||||||
queryKey: ["submitRating", ratingData],
|
queryKey: ["submitRating", ratingData],
|
||||||
queryFn: () => submitRating(ratingData),
|
queryFn: () => submitRating(ratingData),
|
||||||
retry: 3,
|
retry: 3,
|
||||||
retryDelay: 1000,
|
retryDelay: 1000,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleRatingSubmit = () => {
|
const handleRatingSubmit = () => {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
void refetch(); // bewusst ausgelöst, kein await notwendig
|
void refetch(); // bewusst ausgelöst, kein await notwendig
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = (
|
const handleClose = (
|
||||||
_: React.SyntheticEvent | Event,
|
_: React.SyntheticEvent | Event,
|
||||||
reason?: SnackbarCloseReason
|
reason?: SnackbarCloseReason,
|
||||||
) => {
|
) => {
|
||||||
if (reason === "clickaway") return;
|
if (reason === "clickaway") return;
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = (
|
const action = (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
aria-label={t("close")}
|
aria-label={t("close")}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
>
|
>
|
||||||
<Close fontSize="small"/>
|
<Close fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
const {data = []} = useQuery<RatingType[]>({
|
const { data = [] } = useQuery<RatingType[]>({
|
||||||
queryKey: ["fetchRatingList", itemId],
|
queryKey: ["fetchRatingList", itemId],
|
||||||
queryFn: () => fetchRatingList(itemId),
|
queryFn: () => fetchRatingList(itemId),
|
||||||
retry: 3,
|
retry: 3,
|
||||||
retryDelay: 1000,
|
retryDelay: 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ratings: RatingType[] = useMemo(() => data || [], [data]);
|
const ratings: RatingType[] = useMemo(() => data || [], [data]);
|
||||||
|
|
||||||
const getRatings = () => {
|
const getRatings = () => {
|
||||||
if (ratings.length === 0) {
|
if (ratings.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Typography variant="body1" sx={{color: theme.palette.text.secondary}}>
|
<Typography
|
||||||
{t("noRatingsYet")}
|
variant="body1"
|
||||||
</Typography>
|
sx={{ color: theme.palette.text.secondary }}
|
||||||
);
|
>
|
||||||
}
|
{t("noRatingsYet")}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return ratings.map((ratingType: RatingType) => (
|
return ratings.map((ratingType: RatingType) => (
|
||||||
<RatingCard key={ratingType.timestamp} {...ratingType} />
|
<RatingCard key={ratingType.timestamp} {...ratingType} />
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Divider sx={{backgroundColor: theme.palette.divider, my: 3}}/>
|
<Divider sx={{ backgroundColor: theme.palette.divider, my: 3 }} />
|
||||||
|
|
||||||
<Box sx={{mb: 4}}>
|
<Box sx={{ mb: 4 }}>
|
||||||
<Typography variant="h5" sx={{color: theme.palette.text.primary, mb: 2}}>
|
<Typography
|
||||||
{t("rateThisProduct")}:
|
variant="h5"
|
||||||
</Typography>
|
sx={{ color: theme.palette.text.primary, mb: 2 }}
|
||||||
|
>
|
||||||
|
{t("rateThisProduct")}:
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<Rating
|
<Rating
|
||||||
name="half-rating"
|
name="half-rating"
|
||||||
value={ratingValue}
|
value={ratingValue}
|
||||||
onChange={(_, value) => setRatingValue(value)}
|
onChange={(_, value) => setRatingValue(value)}
|
||||||
precision={0.5}
|
precision={0.5}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label={t("review")}
|
label={t("review")}
|
||||||
multiline
|
multiline
|
||||||
minRows={4}
|
minRows={4}
|
||||||
maxRows={16}
|
maxRows={16}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={{
|
sx={{
|
||||||
mt: 2,
|
mt: 2,
|
||||||
mb: 2,
|
mb: 2,
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
'& .MuiInputBase-input': {
|
"& .MuiInputBase-input": {
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
},
|
},
|
||||||
'& label': {
|
"& label": {
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
},
|
},
|
||||||
'& .MuiOutlinedInput-root': {
|
"& .MuiOutlinedInput-root": {
|
||||||
'& fieldset': {
|
"& fieldset": {
|
||||||
borderColor: theme.palette.divider,
|
borderColor: theme.palette.divider,
|
||||||
},
|
},
|
||||||
'&:hover fieldset': {
|
"&:hover fieldset": {
|
||||||
borderColor: theme.palette.text.primary,
|
borderColor: theme.palette.text.primary,
|
||||||
},
|
},
|
||||||
'&.Mui-focused fieldset': {
|
"&.Mui-focused fieldset": {
|
||||||
borderColor: theme.palette.primary.main,
|
borderColor: theme.palette.primary.main,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
value={ratingText}
|
value={ratingText}
|
||||||
onChange={(e) => setRatingText(e.target.value)}
|
onChange={(e) => setRatingText(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleRatingSubmit}
|
onClick={handleRatingSubmit}
|
||||||
>
|
>
|
||||||
{t("submit")}
|
{t("submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Divider sx={{backgroundColor: theme.palette.divider, my: 3}}/>
|
<Divider sx={{ backgroundColor: theme.palette.divider, my: 3 }} />
|
||||||
|
|
||||||
<Box>
|
<Box>{getRatings()}</Box>
|
||||||
{getRatings()}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Snackbar
|
<Snackbar
|
||||||
open={open}
|
open={open}
|
||||||
autoHideDuration={3000}
|
autoHideDuration={3000}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
message={t("thanksForRating")}
|
message={t("thanksForRating")}
|
||||||
action={action}
|
action={action}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,323 +1,406 @@
|
|||||||
// api/queries.js
|
// api/queries.js
|
||||||
|
|
||||||
import AccountType, {AdminAccountOperation, CustomerType, SubmitLogin, User} from "../../components/Account";
|
import AccountType, {
|
||||||
import OrderType, {OrderPatch} from "../../components/Order";
|
AdminAccountOperation,
|
||||||
|
CustomerType,
|
||||||
|
SubmitLogin,
|
||||||
|
User,
|
||||||
|
} from "../../components/Account";
|
||||||
|
import OrderType, { OrderPatch } from "../../components/Order";
|
||||||
import RatingSubmitType from "../../components/RatingSubmit";
|
import RatingSubmitType from "../../components/RatingSubmit";
|
||||||
import {Item, ItemWithFSImage} from "../../components/Item";
|
import { Item, ItemWithFSImage } from "../../components/Item";
|
||||||
|
|
||||||
export const fetchItemList = async () => {
|
export const fetchItemList = async () => {
|
||||||
const response = await fetch('http://localhost:8085/article/all');
|
const response = await fetch("http://localhost:8085/article/all");
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Fehler beim Laden der Items');
|
throw new Error("Fehler beim Laden der Items");
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchItemListWithImage = async () => {
|
export const fetchItemListWithImage = async () => {
|
||||||
const response = await fetch('http://localhost:8085/article/all/image');
|
const response = await fetch("http://localhost:8085/article/all/image");
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Fehler beim Laden der Items');
|
throw new Error("Fehler beim Laden der Items");
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const submitRating = async (ratingData: RatingSubmitType) => {
|
export const submitRating = async (ratingData: RatingSubmitType) => {
|
||||||
const response = await fetch('http://localhost:8085/review?uuid=' + ratingData.articleId + '&rating=' + ratingData.rating * 2, {
|
const response = await fetch(
|
||||||
method: 'POST',
|
"http://localhost:8085/review?uuid=" +
|
||||||
headers: {
|
ratingData.articleId +
|
||||||
'Content-Type': 'text/plain',
|
"&rating=" +
|
||||||
},
|
ratingData.rating * 2,
|
||||||
body: ratingData.content,
|
{
|
||||||
});
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
},
|
||||||
|
body: ratingData.content,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Fehler beim Senden der Bewertung');
|
throw new Error("Fehler beim Senden der Bewertung");
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const fetchRatingList = async (itemId: string) => {
|
export const fetchRatingList = async (itemId: string) => {
|
||||||
const response = await fetch('http://localhost:8085/review/all?uuid=' + itemId);
|
const response = await fetch(
|
||||||
if (!response.ok) {
|
"http://localhost:8085/review/all?uuid=" + itemId,
|
||||||
throw new Error('Fehler beim Laden der Items');
|
);
|
||||||
}
|
if (!response.ok) {
|
||||||
const data = await response.json();
|
throw new Error("Fehler beim Laden der Items");
|
||||||
return data;
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const submitOrder = async (data: OrderType) => {
|
export const submitOrder = async (data: OrderType) => {
|
||||||
const response = await fetch('http://localhost:8085/order', {
|
const response = await fetch("http://localhost:8085/order", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Fehler beim Senden der Bestellung');
|
throw new Error("Fehler beim Senden der Bestellung");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
}
|
};
|
||||||
|
|
||||||
export const submitAccount = async (data: AccountType) => {
|
export const submitAccount = async (data: AccountType) => {
|
||||||
const response = await fetch('http://localhost:8085/account', {
|
const response = await fetch("http://localhost:8085/account", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Fehler beim Senden des Accounts');
|
throw new Error("Fehler beim Senden des Accounts");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
}
|
};
|
||||||
|
|
||||||
export const submitCustomer = async (data: CustomerType) => {
|
export const submitCustomer = async (data: CustomerType) => {
|
||||||
const response = await fetch('http://localhost:8085/customer', {
|
const response = await fetch("http://localhost:8085/customer", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Fehler beim Senden des Accounts');
|
throw new Error("Fehler beim Senden des Accounts");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
}
|
};
|
||||||
|
|
||||||
export const submitLogin = async (loginData: SubmitLogin) => {
|
export const submitLogin = async (loginData: SubmitLogin) => {
|
||||||
const response = await fetch("http://localhost:8085/session?email=" + loginData.email + "&password=" + loginData.password, {
|
const response = await fetch(
|
||||||
method: "POST",
|
"http://localhost:8085/session?email=" +
|
||||||
headers: {
|
loginData.email +
|
||||||
"Content-Type": "application/json",
|
"&password=" +
|
||||||
},
|
loginData.password,
|
||||||
body: JSON.stringify(loginData),
|
{
|
||||||
});
|
method: "POST",
|
||||||
if (!response.ok) {
|
headers: {
|
||||||
throw new Error("Login failed");
|
"Content-Type": "application/json",
|
||||||
}
|
},
|
||||||
return response.json();
|
body: JSON.stringify(loginData),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Login failed");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchAccount = async (loginData: SubmitLogin) => {
|
export const fetchAccount = async (loginData: SubmitLogin) => {
|
||||||
const response = await fetch("http://localhost:8085/account?email=" + loginData.email + "&password=" + loginData.password);
|
const response = await fetch(
|
||||||
if (!response.ok) {
|
"http://localhost:8085/account?email=" +
|
||||||
throw new Error("Login failed");
|
loginData.email +
|
||||||
}
|
"&password=" +
|
||||||
return response.json();
|
loginData.password,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Login failed");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const submitRegister = async (registerData: AccountType) => {
|
export const submitRegister = async (registerData: AccountType) => {
|
||||||
const response = await fetch("http://localhost:8085/account", {
|
const response = await fetch("http://localhost:8085/account", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(registerData),
|
body: JSON.stringify(registerData),
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Login failed");
|
throw new Error("Login failed");
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchCustomer = async (userId: number) => {
|
export const fetchCustomer = async (userId: number) => {
|
||||||
const response = await fetch('http://localhost:8085/customer?id=' + userId);
|
const response = await fetch("http://localhost:8085/customer?id=" + userId);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Fehler beim Laden des Customers');
|
throw new Error("Fehler beim Laden des Customers");
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteAccount = async (user: User) => {
|
export const deleteAccount = async (user: User) => {
|
||||||
const response = await fetch('http://localhost:8085/account?email=' + user.email + '&password=' + user.password, {
|
const response = await fetch(
|
||||||
method: 'DELETE',
|
"http://localhost:8085/account?email=" +
|
||||||
});
|
user.email +
|
||||||
if (!response.ok) {
|
"&password=" +
|
||||||
throw new Error('Fehler beim Löschen des Accounts');
|
user.password,
|
||||||
}
|
{
|
||||||
return await response.json();
|
method: "DELETE",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Fehler beim Löschen des Accounts");
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteAccountAdmin = async (operation: AdminAccountOperation) => {
|
export const deleteAccountAdmin = async (operation: AdminAccountOperation) => {
|
||||||
const response = await fetch('http://localhost:8085/account/admin?email=' + operation.email + '&uuid=' + operation.uuid + '&id=' + operation.accountId, {
|
const response = await fetch(
|
||||||
method: 'DELETE',
|
"http://localhost:8085/account/admin?email=" +
|
||||||
});
|
operation.email +
|
||||||
if (!response.ok) {
|
"&uuid=" +
|
||||||
throw new Error('Fehler beim Löschen des Accounts');
|
operation.uuid +
|
||||||
}
|
"&id=" +
|
||||||
return await response.json();
|
operation.accountId,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Fehler beim Löschen des Accounts");
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchOrders = async (customerId: number) => {
|
export const fetchOrders = async (customerId: number) => {
|
||||||
const response = await fetch('http://localhost:8085/order/all?customerId=' + customerId);
|
const response = await fetch(
|
||||||
if (!response.ok) {
|
"http://localhost:8085/order/all?customerId=" + customerId,
|
||||||
throw new Error('Fehler beim Laden des Customers');
|
);
|
||||||
}
|
if (!response.ok) {
|
||||||
const data = await response.json();
|
throw new Error("Fehler beim Laden des Customers");
|
||||||
return data;
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchAccounts = async (loginData: User) => {
|
export const fetchAccounts = async (loginData: User) => {
|
||||||
const response = await fetch("http://localhost:8085/account/all?email=" + loginData.email + "&session=" + loginData.session);
|
const response = await fetch(
|
||||||
if (!response.ok) {
|
"http://localhost:8085/account/all?email=" +
|
||||||
throw new Error("Login failed");
|
loginData.email +
|
||||||
}
|
"&session=" +
|
||||||
return response.json();
|
loginData.session,
|
||||||
};
|
);
|
||||||
|
if (!response.ok) {
|
||||||
export const fetchItems = async () => { //TODO: remove and use above
|
throw new Error("Login failed");
|
||||||
const response = await fetch("http://localhost:8085/article/all");
|
}
|
||||||
if (!response.ok) {
|
return response.json();
|
||||||
throw new Error("fetching items failed");
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchStatisticsVolume = async (loginData: User) => {
|
export const fetchStatisticsVolume = async (loginData: User) => {
|
||||||
const response = await fetch("http://localhost:8085/statistics/volume?email=" + loginData.email + "&session=" + loginData.session);
|
const response = await fetch(
|
||||||
if (!response.ok) {
|
"http://localhost:8085/statistics/volume?email=" +
|
||||||
throw new Error("fetching satistics Volume failed");
|
loginData.email +
|
||||||
}
|
"&session=" +
|
||||||
return response.json();
|
loginData.session,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("fetching satistics Volume failed");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchStatisticsRevenue = async (loginData: User) => {
|
export const fetchStatisticsRevenue = async (loginData: User) => {
|
||||||
const response = await fetch("http://localhost:8085/statistics/revenue?email=" + loginData.email + "&session=" + loginData.session);
|
const response = await fetch(
|
||||||
if (!response.ok) {
|
"http://localhost:8085/statistics/revenue?email=" +
|
||||||
throw new Error("fetching satistics Revenue failed");
|
loginData.email +
|
||||||
}
|
"&session=" +
|
||||||
return response.json();
|
loginData.session,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("fetching satistics Revenue failed");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editAccount = async (customer: CustomerType) => {
|
export const editAccount = async (customer: CustomerType) => {
|
||||||
const response = await fetch('http://localhost:8085/customer?id=' + customer.id, {
|
const response = await fetch(
|
||||||
method: 'PUT',
|
"http://localhost:8085/customer?id=" + customer.id,
|
||||||
headers: {
|
{
|
||||||
"Content-Type": "application/json",
|
method: "PUT",
|
||||||
},
|
headers: {
|
||||||
body: JSON.stringify(customer),
|
"Content-Type": "application/json",
|
||||||
});
|
},
|
||||||
if (!response.ok) {
|
body: JSON.stringify(customer),
|
||||||
throw new Error('Fehler beim Löschen des Accounts');
|
},
|
||||||
}
|
);
|
||||||
return await response.json();
|
if (!response.ok) {
|
||||||
|
throw new Error("Fehler beim Löschen des Accounts");
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const orderPatch = async (order: OrderPatch) => {
|
export const orderPatch = async (order: OrderPatch) => {
|
||||||
const response = await fetch("http://localhost:8085/order?id=" + order.id + "&status=" + order.status, {
|
const response = await fetch(
|
||||||
method: "PATCH",
|
"http://localhost:8085/order?id=" + order.id + "&status=" + order.status,
|
||||||
}
|
{
|
||||||
);
|
method: "PATCH",
|
||||||
if (!response.ok) {
|
},
|
||||||
throw new Error("Order patch failed");
|
);
|
||||||
}
|
if (!response.ok) {
|
||||||
return response.json();
|
throw new Error("Order patch failed");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
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(
|
||||||
if (!response.ok) {
|
"http://localhost:8085/statistics/orderstatus?email=" +
|
||||||
throw new Error("fetching order status failed");
|
loginData.email +
|
||||||
}
|
"&session=" +
|
||||||
return response.json();
|
loginData.session,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("fetching order status failed");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchStockPercent = async (loginData: User) => {
|
export const fetchStockPercent = async (loginData: User) => {
|
||||||
const response = await fetch("http://localhost:8085/statistics/stockpercent?email=" + loginData.email + "&session=" + loginData.session);
|
const response = await fetch(
|
||||||
if (!response.ok) {
|
"http://localhost:8085/statistics/stockpercent?email=" +
|
||||||
throw new Error("fetching stock% failed");
|
loginData.email +
|
||||||
}
|
"&session=" +
|
||||||
return response.json();
|
loginData.session,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("fetching stock% failed");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const fetchOrdersAdmin = async (loginData: User) => {
|
export const fetchOrdersAdmin = async (loginData: User) => {
|
||||||
const response = await fetch("http://localhost:8085/order/all/all?email=" + loginData.email + "&session=" + loginData.session);
|
const response = await fetch(
|
||||||
if (!response.ok) {
|
"http://localhost:8085/order/all/all?email=" +
|
||||||
throw new Error("fetching admin orders failed");
|
loginData.email +
|
||||||
}
|
"&session=" +
|
||||||
return response.json();
|
loginData.session,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("fetching admin orders failed");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateCustomer = async (customer: CustomerType) => {
|
export const updateCustomer = async (customer: CustomerType) => {
|
||||||
const response = await fetch('http://localhost:8085/customer?id=' + customer.id, {
|
const response = await fetch(
|
||||||
method: 'PUT',
|
"http://localhost:8085/customer?id=" + customer.id,
|
||||||
headers: {
|
{
|
||||||
"Content-Type": "application/json",
|
method: "PUT",
|
||||||
},
|
headers: {
|
||||||
body: JSON.stringify(customer),
|
"Content-Type": "application/json",
|
||||||
});
|
},
|
||||||
if (!response.ok) {
|
body: JSON.stringify(customer),
|
||||||
throw new Error('Fehler beim Ändern des Customers');
|
},
|
||||||
}
|
);
|
||||||
return await response.json();
|
if (!response.ok) {
|
||||||
}
|
throw new Error("Fehler beim Ändern des Customers");
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchFarmingStationItemList = async (): Promise<ItemWithFSImage[]> => {
|
export const fetchFarmingStationItemList = async (): Promise<
|
||||||
const response = await fetch('http://localhost:8085/farm/articles');
|
ItemWithFSImage[]
|
||||||
if (!response.ok)
|
> => {
|
||||||
throw new Error('Failed to fetch items');
|
const response = await fetch("http://localhost:8085/farm/articles");
|
||||||
return response.json();
|
if (!response.ok) throw new Error("Failed to fetch items");
|
||||||
}
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
export const submitItem = async (item: Item) => {
|
export const submitItem = async (item: Item) => {
|
||||||
const response = await fetch("http://localhost:8085/article", {
|
const response = await fetch("http://localhost:8085/article", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(item),
|
body: JSON.stringify(item),
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Login failed");
|
throw new Error("Login failed");
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteItemQuery = async (uuid: string) => {
|
export const deleteItemQuery = async (uuid: string) => {
|
||||||
const response = await fetch("http://localhost:8085/article?uuid=" + uuid, {
|
const response = await fetch("http://localhost:8085/article?uuid=" + uuid, {
|
||||||
method: "DELETE"
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Login failed");
|
throw new Error("Login failed");
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateAccountAdmin = async (account: AccountType, user: User) => {
|
export const updateAccountAdmin = async (account: AccountType, user: User) => {
|
||||||
const response = await fetch('http://localhost:8085/account/admin?email=' + user.email + "&uuid=" + user.session + "&id=" + account.id + "&admin=" + account.admin, {
|
const response = await fetch(
|
||||||
method: 'POST',
|
"http://localhost:8085/account/admin?email=" +
|
||||||
});
|
user.email +
|
||||||
if (!response.ok) {
|
"&uuid=" +
|
||||||
throw new Error('Fehler beim Ändern des Customers');
|
user.session +
|
||||||
}
|
"&id=" +
|
||||||
return await response.json();
|
account.id +
|
||||||
}
|
"&admin=" +
|
||||||
|
account.admin,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Fehler beim Ändern des Customers");
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
export const updateItemAdmin = async (item: Item) => {
|
export const updateItemAdmin = async (item: Item) => {
|
||||||
const response = await fetch('http://localhost:8085/article?uuid=' + item.uuid, {
|
const response = await fetch(
|
||||||
method: 'PUT',
|
"http://localhost:8085/article?uuid=" + item.uuid,
|
||||||
headers: {
|
{
|
||||||
"Content-Type": "application/json",
|
method: "PUT",
|
||||||
},
|
headers: {
|
||||||
body: JSON.stringify(item),
|
"Content-Type": "application/json",
|
||||||
});
|
},
|
||||||
if (!response.ok) {
|
body: JSON.stringify(item),
|
||||||
throw new Error('Fehler beim Ändern des Items');
|
},
|
||||||
}
|
);
|
||||||
return await response.json();
|
if (!response.ok) {
|
||||||
}
|
throw new Error("Fehler beim Ändern des Items");
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,73 +1,73 @@
|
|||||||
:root {
|
:root {
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
color: rgba(255, 255, 255, 0.87);
|
color: rgba(255, 255, 255, 0.87);
|
||||||
background-color: #242424;
|
background-color: #242424;
|
||||||
|
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #646cff;
|
color: #646cff;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #535bf2;
|
color: #535bf2;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 3.2em;
|
font-size: 3.2em;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
padding: 0.6em 1.2em;
|
padding: 0.6em 1.2em;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.25s;
|
transition: border-color 0.25s;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
border-color: #646cff;
|
border-color: #646cff;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus,
|
button:focus,
|
||||||
button:focus-visible {
|
button:focus-visible {
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
:root {
|
:root {
|
||||||
color: #213547;
|
color: #213547;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #747bff;
|
color: #747bff;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import './components/i18n/i18n';
|
import "./components/i18n/i18n";
|
||||||
import {StrictMode} from 'react';
|
import { StrictMode } from "react";
|
||||||
import {createRoot} from 'react-dom/client';
|
import { createRoot } from "react-dom/client";
|
||||||
import './index.css';
|
import "./index.css";
|
||||||
import App from './App.tsx';
|
import App from "./App.tsx";
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById("root");
|
||||||
if (!rootElement) throw new Error("Root element not found");
|
if (!rootElement) throw new Error("Root element not found");
|
||||||
|
|
||||||
const root = createRoot(rootElement);
|
const root = createRoot(rootElement);
|
||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App/>
|
<App />
|
||||||
</StrictMode>
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,239 +1,252 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Divider,
|
Divider,
|
||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
TextField,
|
TextField,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {useNavigate} from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import {CustomerType, User} from "../components/Account";
|
import { CustomerType, User } from "../components/Account";
|
||||||
import {useAccount} from "../helper/AccountProvider";
|
import { useAccount } from "../helper/AccountProvider";
|
||||||
import {deleteAccount, editAccount, fetchCustomer} from "../helper/query/Queries";
|
import {
|
||||||
|
deleteAccount,
|
||||||
|
editAccount,
|
||||||
|
fetchCustomer,
|
||||||
|
} from "../helper/query/Queries";
|
||||||
import "./pages.css";
|
import "./pages.css";
|
||||||
|
|
||||||
export default function Account() {
|
export default function Account() {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {user: userData, logout} = useAccount();
|
const { user: userData, logout } = useAccount();
|
||||||
|
|
||||||
const [user, setUser] = useState<CustomerType>({
|
const [user, setUser] = useState<CustomerType>({
|
||||||
name: "",
|
name: "",
|
||||||
surname: "",
|
surname: "",
|
||||||
address: "",
|
address: "",
|
||||||
country: "",
|
country: "",
|
||||||
zip: "",
|
zip: "",
|
||||||
id: userData?.customerId || 0,
|
id: userData?.customerId || 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [userDataState, setUserDataState] = useState<User>(userData || {
|
const [userDataState, setUserDataState] = useState<User>(
|
||||||
password: "",
|
userData || {
|
||||||
email: "",
|
password: "",
|
||||||
customerId: 0,
|
email: "",
|
||||||
session: "",
|
customerId: 0,
|
||||||
isAdmin: false,
|
session: "",
|
||||||
});
|
isAdmin: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userData?.customerId) {
|
if (userData?.customerId) {
|
||||||
setUser((prev) => ({
|
setUser((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
id: userData.customerId,
|
id: userData.customerId,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [userData]);
|
}, [userData]);
|
||||||
|
|
||||||
const [edit, setEdit] = useState(false);
|
const [edit, setEdit] = useState(false);
|
||||||
const [form, setForm] = useState(user);
|
const [form, setForm] = useState(user);
|
||||||
|
|
||||||
// Neu: Passwort-Dialog-Status und Passwort-Input
|
// Neu: Passwort-Dialog-Status und Passwort-Input
|
||||||
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
||||||
const [passwordInput, setPasswordInput] = useState("");
|
const [passwordInput, setPasswordInput] = useState("");
|
||||||
|
|
||||||
const {data} = useQuery<CustomerType>({
|
const { data } = useQuery<CustomerType>({
|
||||||
queryKey: ["fetchCustomer", userData?.customerId],
|
queryKey: ["fetchCustomer", userData?.customerId],
|
||||||
queryFn: () => fetchCustomer(userData?.customerId || 0),
|
queryFn: () => fetchCustomer(userData?.customerId || 0),
|
||||||
retry: 1,
|
retry: 1,
|
||||||
retryDelay: 1000,
|
retryDelay: 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {refetch: deleteRefetch} = useQuery({
|
const { refetch: deleteRefetch } = useQuery({
|
||||||
queryKey: ["deleteAccount", userDataState],
|
queryKey: ["deleteAccount", userDataState],
|
||||||
queryFn: () => deleteAccount(userDataState!),
|
queryFn: () => deleteAccount(userDataState!),
|
||||||
enabled: false,
|
enabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {refetch: editRefetch} = useQuery({
|
const { refetch: editRefetch } = useQuery({
|
||||||
queryKey: ["editAccount", form],
|
queryKey: ["editAccount", form],
|
||||||
queryFn: () => editAccount(form),
|
queryFn: () => editAccount(form),
|
||||||
enabled: false,
|
enabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
setUser(data);
|
setUser(data);
|
||||||
setForm(data);
|
setForm(data);
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const handleEdit = () => setEdit(true);
|
const handleEdit = () => setEdit(true);
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setForm(user);
|
setForm(user);
|
||||||
setEdit(false);
|
setEdit(false);
|
||||||
};
|
};
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setForm({...form, [e.target.name]: e.target.value});
|
setForm({ ...form, [e.target.name]: e.target.value });
|
||||||
};
|
};
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setUser(form);
|
setUser(form);
|
||||||
setEdit(false);
|
setEdit(false);
|
||||||
await editRefetch();
|
await editRefetch();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Neu: Passwort-Dialog öffnen
|
// Neu: Passwort-Dialog öffnen
|
||||||
const handleDeleteClick = () => {
|
const handleDeleteClick = () => {
|
||||||
setPasswordInput("");
|
setPasswordInput("");
|
||||||
setPasswordDialogOpen(true);
|
setPasswordDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Neu: Passwort-Dialog schließen
|
// Neu: Passwort-Dialog schließen
|
||||||
const handlePasswordDialogClose = () => {
|
const handlePasswordDialogClose = () => {
|
||||||
setPasswordDialogOpen(false);
|
setPasswordDialogOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Neu: Passwort-Eingabe bestätigen
|
// Neu: Passwort-Eingabe bestätigen
|
||||||
const handlePasswordConfirm = async () => {
|
const handlePasswordConfirm = async () => {
|
||||||
if (!passwordInput) {
|
if (!passwordInput) {
|
||||||
alert(t("pleaseEnterPassword"));
|
alert(t("pleaseEnterPassword"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Passwort in Form aktualisieren (hier z.B. als field "password", anpassen falls anders)
|
// Passwort in Form aktualisieren (hier z.B. als field "password", anpassen falls anders)
|
||||||
setUserDataState({...userDataState, password: passwordInput});
|
setUserDataState({ ...userDataState, password: passwordInput });
|
||||||
|
|
||||||
// Erst User-Daten mit Passwort aktualisieren
|
// Erst User-Daten mit Passwort aktualisieren
|
||||||
try {
|
try {
|
||||||
await editRefetch(); // Achtung: editRefetch verwendet immer noch alten form, daher call direkt mit updatedForm:
|
await editRefetch(); // Achtung: editRefetch verwendet immer noch alten form, daher call direkt mit updatedForm:
|
||||||
// Danach Account löschen
|
// Danach Account löschen
|
||||||
await deleteRefetch();
|
await deleteRefetch();
|
||||||
logout();
|
logout();
|
||||||
navigate("/");
|
navigate("/");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Fehler beim Löschen des Accounts:", error);
|
console.error("Fehler beim Löschen des Accounts:", error);
|
||||||
alert(t("deleteAccountFailed"));
|
alert(t("deleteAccountFailed"));
|
||||||
} finally {
|
} finally {
|
||||||
setPasswordDialogOpen(false);
|
setPasswordDialogOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className="page-background page-background-center"
|
className="page-background page-background-center"
|
||||||
sx={{minHeight: "100vh", justifyContent: "flex-start", pt: 4}}
|
sx={{ minHeight: "100vh", justifyContent: "flex-start", pt: 4 }}
|
||||||
>
|
>
|
||||||
<Paper elevation={3} sx={{p: 4, maxWidth: 500, width: "100%", mx: "auto"}}>
|
<Paper
|
||||||
<Typography variant="h4" gutterBottom>
|
elevation={3}
|
||||||
{t("myAccount")}
|
sx={{ p: 4, maxWidth: 500, width: "100%", mx: "auto" }}
|
||||||
</Typography>
|
>
|
||||||
<Divider sx={{mb: 3}}/>
|
<Typography variant="h4" gutterBottom>
|
||||||
<Stack spacing={2}>
|
{t("myAccount")}
|
||||||
<TextField
|
</Typography>
|
||||||
label={t("name")}
|
<Divider sx={{ mb: 3 }} />
|
||||||
name="name"
|
<Stack spacing={2}>
|
||||||
value={edit ? form.name : user.name}
|
<TextField
|
||||||
onChange={handleChange}
|
label={t("name")}
|
||||||
disabled={!edit}
|
name="name"
|
||||||
fullWidth
|
value={edit ? form.name : user.name}
|
||||||
/>
|
onChange={handleChange}
|
||||||
<TextField
|
disabled={!edit}
|
||||||
label={t("surname")}
|
fullWidth
|
||||||
name="surname"
|
/>
|
||||||
value={edit ? form.surname : user.surname}
|
<TextField
|
||||||
onChange={handleChange}
|
label={t("surname")}
|
||||||
disabled={!edit}
|
name="surname"
|
||||||
fullWidth
|
value={edit ? form.surname : user.surname}
|
||||||
/>
|
onChange={handleChange}
|
||||||
<TextField
|
disabled={!edit}
|
||||||
label={t("address")}
|
fullWidth
|
||||||
name="address"
|
/>
|
||||||
value={edit ? form.address : user.address}
|
<TextField
|
||||||
onChange={handleChange}
|
label={t("address")}
|
||||||
disabled={!edit}
|
name="address"
|
||||||
fullWidth
|
value={edit ? form.address : user.address}
|
||||||
/>
|
onChange={handleChange}
|
||||||
<TextField
|
disabled={!edit}
|
||||||
label={t("country")}
|
fullWidth
|
||||||
name="country"
|
/>
|
||||||
value={edit ? form.country : user.country}
|
<TextField
|
||||||
onChange={handleChange}
|
label={t("country")}
|
||||||
disabled={!edit}
|
name="country"
|
||||||
fullWidth
|
value={edit ? form.country : user.country}
|
||||||
/>
|
onChange={handleChange}
|
||||||
<TextField
|
disabled={!edit}
|
||||||
label={t("zip")}
|
fullWidth
|
||||||
name="zip"
|
/>
|
||||||
value={edit ? form.zip : user.zip}
|
<TextField
|
||||||
onChange={handleChange}
|
label={t("zip")}
|
||||||
disabled={!edit}
|
name="zip"
|
||||||
fullWidth
|
value={edit ? form.zip : user.zip}
|
||||||
/>
|
onChange={handleChange}
|
||||||
</Stack>
|
disabled={!edit}
|
||||||
<Box sx={{display: "flex", gap: 2, mt: 4}}>
|
fullWidth
|
||||||
{edit ? (
|
/>
|
||||||
<>
|
</Stack>
|
||||||
<Button variant="contained" color="primary" onClick={handleSave}>
|
<Box sx={{ display: "flex", gap: 2, mt: 4 }}>
|
||||||
{t("save")}
|
{edit ? (
|
||||||
</Button>
|
<>
|
||||||
<Button variant="outlined" color="secondary" onClick={handleCancel}>
|
<Button variant="contained" color="primary" onClick={handleSave}>
|
||||||
{t("cancel")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
<Button
|
||||||
) : (
|
variant="outlined"
|
||||||
<Button variant="contained" color="primary" onClick={handleEdit}>
|
color="secondary"
|
||||||
{t("edit")}
|
onClick={handleCancel}
|
||||||
</Button>
|
>
|
||||||
)}
|
{t("cancel")}
|
||||||
<Button
|
</Button>
|
||||||
variant="outlined"
|
</>
|
||||||
color="error"
|
) : (
|
||||||
onClick={handleDeleteClick} // Neu: Passwort-Dialog öffnen
|
<Button variant="contained" color="primary" onClick={handleEdit}>
|
||||||
sx={{marginLeft: "auto"}}
|
{t("edit")}
|
||||||
>
|
</Button>
|
||||||
{t("deleteAccount")}
|
)}
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
variant="outlined"
|
||||||
</Paper>
|
color="error"
|
||||||
|
onClick={handleDeleteClick} // Neu: Passwort-Dialog öffnen
|
||||||
{/* Passwort-Dialog */}
|
sx={{ marginLeft: "auto" }}
|
||||||
<Dialog open={passwordDialogOpen} onClose={handlePasswordDialogClose}>
|
>
|
||||||
<DialogTitle>{t("confirmDeleteAccount")}</DialogTitle>
|
{t("deleteAccount")}
|
||||||
<DialogContent>
|
</Button>
|
||||||
<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>
|
</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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,81 +1,111 @@
|
|||||||
import {AccountCircle, Category, QueryStats, ReceiptLong,} from "@mui/icons-material";
|
import {
|
||||||
import {Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, useTheme,} from "@mui/material";
|
AccountCircle,
|
||||||
import {useState} from "react";
|
Category,
|
||||||
import {useTranslation} from "react-i18next";
|
QueryStats,
|
||||||
|
ReceiptLong,
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { fetchItemList } from "../helper/query/Queries.tsx";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useAccount } from "../helper/AccountProvider.tsx";
|
||||||
import AccountsInfo from "../helper/adminpanel/AccountsInfo";
|
import AccountsInfo from "../helper/adminpanel/AccountsInfo";
|
||||||
import ItemInfo from "../helper/adminpanel/ItemsInfo";
|
import ItemInfo from "../helper/adminpanel/ItemsInfo";
|
||||||
import OrdersInfo from "../helper/adminpanel/OrdersInfo";
|
import OrdersInfo from "../helper/adminpanel/OrdersInfo";
|
||||||
import StatisticsInfo from "../helper/adminpanel/StatisticsInfo";
|
import StatisticsInfo from "../helper/adminpanel/StatisticsInfo";
|
||||||
|
import Item from "../components/Item";
|
||||||
|
|
||||||
export default function AdminPanel() {
|
export default function AdminPanel() {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [infoStatus, setInfoStatus] = useState<string>("statistics");
|
const [infoStatus, setInfoStatus] = useState<string>("statistics");
|
||||||
|
|
||||||
const handleInfoStatus = (path: string) => {
|
const handleInfoStatus = (path: string) => {
|
||||||
setInfoStatus(path);
|
setInfoStatus(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderContent = () => {
|
const { user: loginData } = useAccount();
|
||||||
switch (infoStatus) {
|
|
||||||
case "statistics":
|
|
||||||
return <StatisticsInfo/>;
|
|
||||||
case "orders":
|
|
||||||
return <OrdersInfo/>;
|
|
||||||
case "accounts":
|
|
||||||
return <AccountsInfo/>;
|
|
||||||
case "items":
|
|
||||||
return <ItemInfo/>;
|
|
||||||
default:
|
|
||||||
return <StatisticsInfo/>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const menuItems = [
|
const { data: itemList, isLoading } = useQuery<Item[]>({
|
||||||
{key: "statistics", icon: <QueryStats/>, label: t("statistics")},
|
queryKey: ["fetchItemList", loginData],
|
||||||
{key: "orders", icon: <ReceiptLong/>, label: t("orders")},
|
queryFn: () => fetchItemList(),
|
||||||
{key: "accounts", icon: <AccountCircle/>, label: t("accounts")},
|
retry: 3,
|
||||||
{key: "items", icon: <Category/>, label: t("items")},
|
retryDelay: 1000,
|
||||||
];
|
});
|
||||||
|
|
||||||
return (
|
const renderContent = (itemList: Item[]) => {
|
||||||
<Box className="page-container" sx={{color: theme.palette.text.primary}}>
|
switch (infoStatus) {
|
||||||
<div
|
case "statistics":
|
||||||
className="home-page-background"
|
return <StatisticsInfo />;
|
||||||
style={{
|
case "orders":
|
||||||
color: theme.palette.text.primary,
|
return <OrdersInfo itemList={itemList} />;
|
||||||
backgroundColor: theme.palette.background.paper,
|
case "accounts":
|
||||||
}}>
|
return <AccountsInfo />;
|
||||||
{/* Sidebar */}
|
case "items":
|
||||||
<Box className="sidebar">
|
return <ItemInfo itemList={itemList} />;
|
||||||
<nav aria-label={t("mainAdminMenu")}>
|
default:
|
||||||
<List>
|
return <StatisticsInfo />;
|
||||||
{menuItems.map((item) => (
|
}
|
||||||
<ListItem key={item.key} disablePadding>
|
};
|
||||||
<ListItemButton
|
|
||||||
selected={infoStatus === item.key}
|
|
||||||
onClick={() => handleInfoStatus(item.key)}
|
|
||||||
sx={{
|
|
||||||
"&.Mui-selected": {
|
|
||||||
bgcolor: theme.palette.action.selected,
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
"&:hover": {
|
|
||||||
bgcolor: theme.palette.action.hover,
|
|
||||||
},
|
|
||||||
}}>
|
|
||||||
<ListItemIcon sx={{color: "inherit"}}>{item.icon}</ListItemIcon>
|
|
||||||
<ListItemText primary={item.label}/>
|
|
||||||
</ListItemButton>
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</nav>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Content */}
|
const menuItems = [
|
||||||
<Box className="page-background">{renderContent()}</Box>
|
{ key: "statistics", icon: <QueryStats />, label: t("statistics") },
|
||||||
</div>
|
{ key: "orders", icon: <ReceiptLong />, label: t("orders") },
|
||||||
|
{ key: "accounts", icon: <AccountCircle />, label: t("accounts") },
|
||||||
|
{ key: "items", icon: <Category />, label: t("items") },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="page-container" sx={{ color: theme.palette.text.primary }}>
|
||||||
|
<div
|
||||||
|
className="home-page-background"
|
||||||
|
style={{
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Sidebar */}
|
||||||
|
<Box className="sidebar">
|
||||||
|
<nav aria-label={t("mainAdminMenu")}>
|
||||||
|
<List>
|
||||||
|
{menuItems.map((item) => (
|
||||||
|
<ListItem key={item.key} disablePadding>
|
||||||
|
<ListItemButton
|
||||||
|
selected={infoStatus === item.key}
|
||||||
|
onClick={() => handleInfoStatus(item.key)}
|
||||||
|
sx={{
|
||||||
|
"&.Mui-selected": {
|
||||||
|
bgcolor: theme.palette.action.selected,
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
"&:hover": {
|
||||||
|
bgcolor: theme.palette.action.hover,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon sx={{ color: "inherit" }}>
|
||||||
|
{item.icon}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={item.label} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</nav>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
|
||||||
|
{/* Content */}
|
||||||
|
<Box className="page-background">{renderContent(itemList)}</Box>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +1,130 @@
|
|||||||
import {Box, Divider, Typography} from "@mui/material";
|
import { Box, Divider, Typography } from "@mui/material";
|
||||||
import "./pages.css";
|
import "./pages.css";
|
||||||
|
|
||||||
export default function Impressum() {
|
export default function Impressum() {
|
||||||
return (
|
return (
|
||||||
<Box className="impressum-container">
|
<Box className="impressum-container">
|
||||||
<Typography variant="h4" sx={{color: 'text.primary', mb: 2}}>
|
<Typography variant="h4" sx={{ color: "text.primary", mb: 2 }}>
|
||||||
Impressum
|
Impressum
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body1" sx={{color: 'text.primary', mb: 4}}>
|
<Typography variant="body1" sx={{ color: "text.primary", mb: 4 }}>
|
||||||
Hochschule für Technik und Wirtschaft<br/>
|
Hochschule für Technik und Wirtschaft
|
||||||
des Saarlandes<br/>
|
<br />
|
||||||
Goebenstraße 40<br/>
|
des Saarlandes
|
||||||
66117 Saarbrücken<br/><br/>
|
<br />
|
||||||
Telefon: (0681) 58 67 - 0<br/>
|
Goebenstraße 40
|
||||||
Telefax: (0681) 58 67 - 122<br/>
|
<br />
|
||||||
E-Mail: info@htwsaar.de<br/><br/>
|
66117 Saarbrücken
|
||||||
Aufsichtsbehörde:<br/>
|
<br />
|
||||||
Ministerium der Finanzen und für Wissenschaft des Saarlandes
|
<br />
|
||||||
</Typography>
|
Telefon: (0681) 58 67 - 0<br />
|
||||||
|
Telefax: (0681) 58 67 - 122
|
||||||
|
<br />
|
||||||
|
E-Mail: info@htwsaar.de
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
Aufsichtsbehörde:
|
||||||
|
<br />
|
||||||
|
Ministerium der Finanzen und für Wissenschaft des Saarlandes
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<Divider className="contact-divider"/>
|
<Divider className="contact-divider" />
|
||||||
|
|
||||||
<Typography variant="h5" sx={{color: 'text.primary', mt: 4, mb: 2}}>
|
<Typography variant="h5" sx={{ color: "text.primary", mt: 4, mb: 2 }}>
|
||||||
Datenschutzerklärung
|
Datenschutzerklärung
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body1" sx={{color: 'text.primary', mb: 2}}>
|
<Typography variant="body1" sx={{ color: "text.primary", mb: 2 }}>
|
||||||
Personenbezogene Daten (nachfolgend zumeist nur „Daten“ genannt) ...
|
Personenbezogene Daten (nachfolgend zumeist nur „Daten“ genannt) ...
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body1" sx={{color: 'text.primary', mb: 2}}>
|
<Typography variant="body1" sx={{ color: "text.primary", mb: 2 }}>
|
||||||
Gemäß Art. 4 Ziffer 1. der Verordnung (EU) 2016/679, also der Datenschutz-Grundverordnung ...
|
Gemäß Art. 4 Ziffer 1. der Verordnung (EU) 2016/679, also der
|
||||||
</Typography>
|
Datenschutz-Grundverordnung ...
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body1" sx={{color: 'text.primary', mb: 2}}>
|
<Typography variant="body1" sx={{ color: "text.primary", mb: 2 }}>
|
||||||
Unsere Datenschutzerklärung ist wie folgt gegliedert:<br/>
|
Unsere Datenschutzerklärung ist wie folgt gegliedert:
|
||||||
I. Informationen über uns als Verantwortliche<br/>
|
<br />
|
||||||
II. Rechte der Nutzer und Betroffenen<br/>
|
I. Informationen über uns als Verantwortliche
|
||||||
III. Informationen zur Datenverarbeitung
|
<br />
|
||||||
</Typography>
|
II. Rechte der Nutzer und Betroffenen
|
||||||
|
<br />
|
||||||
|
III. Informationen zur Datenverarbeitung
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="h6" sx={{color: 'text.primary', mt: 4, mb: 1}}>
|
<Typography variant="h6" sx={{ color: "text.primary", mt: 4, mb: 1 }}>
|
||||||
I. Informationen über uns als Verantwortliche
|
I. Informationen über uns als Verantwortliche
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body1" sx={{color: 'text.primary', mb: 2}}>
|
<Typography variant="body1" sx={{ color: "text.primary", mb: 2 }}>
|
||||||
Verantwortlicher Anbieter dieses Internetauftritts ...
|
Verantwortlicher Anbieter dieses Internetauftritts ...
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="h6" sx={{color: 'text.primary', mt: 4, mb: 1}}>
|
<Typography variant="h6" sx={{ color: "text.primary", mt: 4, mb: 1 }}>
|
||||||
II. Rechte der Nutzer und Betroffenen
|
II. Rechte der Nutzer und Betroffenen
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body1" sx={{color: 'text.primary', mb: 2}}>
|
<Typography variant="body1" sx={{ color: "text.primary", mb: 2 }}>
|
||||||
Mit Blick auf die nachfolgend noch näher beschriebene Datenverarbeitung haben die Nutzer und Betroffenen
|
Mit Blick auf die nachfolgend noch näher beschriebene Datenverarbeitung
|
||||||
...
|
haben die Nutzer und Betroffenen ...
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box component="ul" sx={{listStyle: 'none', p: 0, m: 0}}>
|
<Box component="ul" sx={{ listStyle: "none", p: 0, m: 0 }}>
|
||||||
<li><Typography variant="body2" sx={{color: 'text.primary', mb: 0.5}}> • Auskunft über die verarbeiteten
|
<li>
|
||||||
Daten (Art. 15 DSGVO)</Typography></li>
|
<Typography variant="body2" sx={{ color: "text.primary", mb: 0.5 }}>
|
||||||
<li><Typography variant="body2" sx={{color: 'text.primary', mb: 0.5}}> • Berichtigung unrichtiger Daten
|
{" "}
|
||||||
(Art. 16 DSGVO)</Typography></li>
|
• Auskunft über die verarbeiteten Daten (Art. 15 DSGVO)
|
||||||
<li><Typography variant="body2" sx={{color: 'text.primary', mb: 0.5}}> • Löschung der Daten (Art. 17
|
</Typography>
|
||||||
DSGVO)</Typography></li>
|
</li>
|
||||||
<li><Typography variant="body2" sx={{color: 'text.primary', mb: 0.5}}> • Einschränkung der Verarbeitung
|
<li>
|
||||||
(Art. 18 DSGVO)</Typography></li>
|
<Typography variant="body2" sx={{ color: "text.primary", mb: 0.5 }}>
|
||||||
<li><Typography variant="body2" sx={{color: 'text.primary', mb: 0.5}}> • Datenübertragbarkeit (Art. 20
|
{" "}
|
||||||
DSGVO)</Typography></li>
|
• Berichtigung unrichtiger Daten (Art. 16 DSGVO)
|
||||||
</Box>
|
</Typography>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Typography variant="body2" sx={{ color: "text.primary", mb: 0.5 }}>
|
||||||
|
{" "}
|
||||||
|
• Löschung der Daten (Art. 17 DSGVO)
|
||||||
|
</Typography>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Typography variant="body2" sx={{ color: "text.primary", mb: 0.5 }}>
|
||||||
|
{" "}
|
||||||
|
• Einschränkung der Verarbeitung (Art. 18 DSGVO)
|
||||||
|
</Typography>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Typography variant="body2" sx={{ color: "text.primary", mb: 0.5 }}>
|
||||||
|
{" "}
|
||||||
|
• Datenübertragbarkeit (Art. 20 DSGVO)
|
||||||
|
</Typography>
|
||||||
|
</li>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Typography variant="h6" sx={{color: 'text.primary', mt: 4, mb: 1}}>
|
<Typography variant="h6" sx={{ color: "text.primary", mt: 4, mb: 1 }}>
|
||||||
III. Informationen zur Datenverarbeitung
|
III. Informationen zur Datenverarbeitung
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body1" sx={{color: 'text.primary', mb: 2}}>
|
<Typography variant="body1" sx={{ color: "text.primary", mb: 2 }}>
|
||||||
Ihre bei Nutzung unseres Internetauftritts verarbeiteten Daten ...
|
Ihre bei Nutzung unseres Internetauftritts verarbeiteten Daten ...
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Du kannst einfach alle weiteren Absätze so fortsetzen – copy & paste,
|
{/* Du kannst einfach alle weiteren Absätze so fortsetzen – copy & paste,
|
||||||
jeweils in: <Typography variant="body1" sx={{ color: 'text.primary' }}>…</Typography> */}
|
jeweils in: <Typography variant="body1" sx={{ color: 'text.primary' }}>…</Typography> */}
|
||||||
|
|
||||||
<Typography variant="body2" sx={{color: 'text.primary', mt: 4}}>
|
<Typography variant="body2" sx={{ color: "text.primary", mt: 4 }}>
|
||||||
Mehr Infos unter: <a href="https://www.cloudflare.com/privacypolicy/" target="_blank"
|
Mehr Infos unter:{" "}
|
||||||
rel="noopener noreferrer">CloudFlare Datenschutzerklärung</a>
|
<a
|
||||||
</Typography>
|
href="https://www.cloudflare.com/privacypolicy/"
|
||||||
</Box>
|
target="_blank"
|
||||||
);
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
CloudFlare Datenschutzerklärung
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,106 +1,142 @@
|
|||||||
import {Box, Button, Typography, useTheme} from "@mui/material";
|
import { Box, Button, Typography, useTheme } from "@mui/material";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {useState} from "react";
|
import { useState } from "react";
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {useBasket} from "../helper/BasketProvider";
|
import { useBasket } from "../helper/BasketProvider";
|
||||||
import ItemCard from "../helper/homepage/ItemCard";
|
import ItemCard from "../helper/homepage/ItemCard";
|
||||||
import AddShoppingCartIcon from '@mui/icons-material/AddShoppingCart';
|
import AddShoppingCartIcon from "@mui/icons-material/AddShoppingCart";
|
||||||
|
|
||||||
import farmingStation from '../assets/fscomponents/fs_components_0.png';
|
import farmingStation from "../assets/fscomponents/fs_components_0.png";
|
||||||
import {ItemWithFSImage} from "../components/Item";
|
import { ItemWithFSImage } from "../components/Item";
|
||||||
import {fetchFarmingStationItemList} from "../helper/query/Queries";
|
import { fetchFarmingStationItemList } from "../helper/query/Queries";
|
||||||
|
|
||||||
"../components/Item";
|
("../components/Item");
|
||||||
|
|
||||||
export default function FSComponents() {
|
export default function FSComponents() {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const {addToBasket} = useBasket();
|
const { addToBasket } = useBasket();
|
||||||
const [hoverIndex, setHoverIndex] = useState<number | null>(null);
|
const [hoverIndex, setHoverIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
// Sehr sehr dummer Weg das zu machen, aber wird später noch refactored
|
// Sehr sehr dummer Weg das zu machen, aber wird später noch refactored
|
||||||
const wantedIds = ["60", "67", "68", "69", "70", "71", "72", "73", "74", "75"];
|
const wantedIds = [
|
||||||
|
"60",
|
||||||
|
"67",
|
||||||
|
"68",
|
||||||
|
"69",
|
||||||
|
"70",
|
||||||
|
"71",
|
||||||
|
"72",
|
||||||
|
"73",
|
||||||
|
"74",
|
||||||
|
"75",
|
||||||
|
];
|
||||||
|
|
||||||
// Daten mit react-query laden
|
// Daten mit react-query laden
|
||||||
const {data = [], isLoading, error} = useQuery<ItemWithFSImage[]>({
|
const {
|
||||||
queryKey: ['fetchFarmingStationItemList'],
|
data = [],
|
||||||
queryFn: fetchFarmingStationItemList,
|
isLoading,
|
||||||
retry: 3,
|
error,
|
||||||
retryDelay: 1000,
|
} = useQuery<ItemWithFSImage[]>({
|
||||||
|
queryKey: ["fetchFarmingStationItemList"],
|
||||||
|
queryFn: fetchFarmingStationItemList,
|
||||||
|
retry: 3,
|
||||||
|
retryDelay: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Button-Funktion: alle gefilterten Items in den Warenkorb packen
|
||||||
|
const handleAddAllToCart = () => {
|
||||||
|
data.forEach((item) => {
|
||||||
|
addToBasket(item, 1);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Button-Funktion: alle gefilterten Items in den Warenkorb packen
|
if (isLoading) return <Typography>{t("loading")}</Typography>;
|
||||||
const handleAddAllToCart = () => {
|
if (error)
|
||||||
data.forEach(item => {
|
return <Typography color="error">{t("errorLoadingItems")}</Typography>;
|
||||||
addToBasket(item, 1);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading) return <Typography>{t('loading')}</Typography>;
|
return (
|
||||||
if (error) return <Typography color="error">{t('errorLoadingItems')}</Typography>;
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
maxWidth: 1600,
|
||||||
|
mx: "auto",
|
||||||
|
pt: 2,
|
||||||
|
height: "100vh",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Bild links */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "auto",
|
||||||
|
height: "90vh",
|
||||||
|
position: "sticky",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: 2,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={
|
||||||
|
hoverIndex !== null ? data[hoverIndex].farmImage : farmingStation
|
||||||
|
}
|
||||||
|
alt={t("componentsFarmingStation")}
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
height: "auto",
|
||||||
|
maxHeight: "80vh",
|
||||||
|
objectFit: "contain",
|
||||||
|
borderRadius: 2,
|
||||||
|
border: `4px solid ${theme.palette.primary.main}`,
|
||||||
|
marginBottom: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
fullWidth
|
||||||
|
startIcon={<AddShoppingCartIcon />}
|
||||||
|
onClick={handleAddAllToCart}
|
||||||
|
>
|
||||||
|
{t("addAllToCart")}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
return (
|
{/* Items rechts */}
|
||||||
<Box sx={{width: '100%', display: 'flex', maxWidth: 1600, mx: 'auto', pt: 2, height: '100vh'}}>
|
<Box
|
||||||
{/* Bild links */}
|
sx={{
|
||||||
<Box sx={{
|
width: "60%",
|
||||||
width: 'auto',
|
height: "90vh",
|
||||||
height: '90vh',
|
overflowY: "auto",
|
||||||
position: 'sticky',
|
padding: 2,
|
||||||
display: 'flex',
|
}}
|
||||||
flexDirection: 'column',
|
>
|
||||||
alignItems: 'center',
|
<Box mb={2}>
|
||||||
justifyContent: 'center',
|
<Typography
|
||||||
padding: 2,
|
variant="h4"
|
||||||
overflow: 'hidden',
|
align="center"
|
||||||
}}>
|
gutterBottom
|
||||||
<Box
|
sx={{ color: "text.primary" }}
|
||||||
component="img"
|
>
|
||||||
src={hoverIndex !== null ? data[hoverIndex].farmImage : farmingStation}
|
{t("componentsFarmingStation")}
|
||||||
alt={t('componentsFarmingStation')}
|
</Typography>
|
||||||
sx={{
|
|
||||||
width: '100%',
|
|
||||||
height: 'auto',
|
|
||||||
maxHeight: '80vh',
|
|
||||||
objectFit: 'contain',
|
|
||||||
borderRadius: 2,
|
|
||||||
border: `4px solid ${theme.palette.primary.main}`,
|
|
||||||
marginBottom: 2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
fullWidth
|
|
||||||
startIcon={<AddShoppingCartIcon/>}
|
|
||||||
onClick={handleAddAllToCart}
|
|
||||||
>
|
|
||||||
{t('addAllToCart')}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Items rechts */}
|
|
||||||
<Box sx={{
|
|
||||||
width: '60%',
|
|
||||||
height: '90vh',
|
|
||||||
overflowY: 'auto',
|
|
||||||
padding: 2,
|
|
||||||
}}>
|
|
||||||
<Box mb={2}>
|
|
||||||
<Typography variant="h4" align="center" gutterBottom sx={{color: 'text.primary'}}>
|
|
||||||
{t('componentsFarmingStation')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box className="cardgrid">
|
|
||||||
{data.map((item, index) => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
onMouseEnter={() => setHoverIndex(index)}
|
|
||||||
onMouseLeave={() => setHoverIndex(null)}
|
|
||||||
>
|
|
||||||
<ItemCard item={item}/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
<Box className="cardgrid">
|
||||||
|
{data.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
onMouseEnter={() => setHoverIndex(index)}
|
||||||
|
onMouseLeave={() => setHoverIndex(null)}
|
||||||
|
>
|
||||||
|
<ItemCard item={item} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,201 +1,206 @@
|
|||||||
import {Alert, Box, useTheme} from "@mui/material";
|
import { Alert, Box, useTheme } from "@mui/material";
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {useEffect, useMemo, useRef, useState} from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {useLocation, useNavigate} from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import ItemWithImage from "../components/Item";
|
import ItemWithImage from "../components/Item";
|
||||||
import FilterItem from "../helper/homepage/FilterItem";
|
import FilterItem from "../helper/homepage/FilterItem";
|
||||||
import ItemCard from "../helper/homepage/ItemCard";
|
import ItemCard from "../helper/homepage/ItemCard";
|
||||||
import PriceSlider from "../helper/homepage/PriceSlider";
|
import PriceSlider from "../helper/homepage/PriceSlider";
|
||||||
import {fetchItemListWithImage} from '../helper/query/Queries';
|
import { fetchItemListWithImage } from "../helper/query/Queries";
|
||||||
import "./pages.css"; // Import der CSS-Datei
|
import "./pages.css"; // Import der CSS-Datei
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const theme = useTheme();
|
||||||
|
const [searchQuery, setSearchQuery] = useState<string | null>(null);
|
||||||
|
|
||||||
const {t} = useTranslation();
|
const categoriesFilter = useMemo(
|
||||||
const navigate = useNavigate();
|
() => [
|
||||||
const location = useLocation();
|
{ value: "", label: t("allCategories") },
|
||||||
const theme = useTheme();
|
{ value: "Seeds", label: t("seeds") },
|
||||||
const [searchQuery, setSearchQuery] = useState<string | null>(null);
|
{ value: "GardenSupplies", label: t("gardenSupplies") },
|
||||||
|
{ value: "TechnicalComponents", label: t("technicalComponents") },
|
||||||
|
{ value: "Other", label: t("other") },
|
||||||
|
],
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
const categoriesFilter = useMemo(() => [
|
const ratingFilter = [
|
||||||
{value: "", label: t("allCategories")},
|
{ value: "", label: t("allRatings") },
|
||||||
{value: "Seeds", label: t("seeds")},
|
...[5, 4, 3, 2, 1].map((value) => ({
|
||||||
{value: "GardenSupplies", label: t("gardenSupplies")},
|
value: value.toString(),
|
||||||
{value: "TechnicalComponents", label: t("technicalComponents")},
|
label: value.toString(),
|
||||||
{value: "Other", label: t("other")}
|
})),
|
||||||
], [t]);
|
];
|
||||||
|
|
||||||
const ratingFilter = [
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||||
{value: "", label: t('allRatings')},
|
const [selectedRating, setSelectedRating] = useState<string | null>(null);
|
||||||
...[5, 4, 3, 2, 1].map(value => ({
|
|
||||||
value: value.toString(),
|
|
||||||
label: value.toString()
|
|
||||||
}))
|
|
||||||
];
|
|
||||||
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
const { data = [], isLoading } = useQuery<ItemWithImage[]>({
|
||||||
const [selectedRating, setSelectedRating] = useState<string | null>(null);
|
queryKey: ["fetchItemListWithImage"],
|
||||||
|
queryFn: fetchItemListWithImage,
|
||||||
|
retry: 3, // Versucht es 3-mal erneut
|
||||||
|
retryDelay: 1000, // Wartezeit zwischen den Versuchen (in ms)
|
||||||
|
});
|
||||||
|
|
||||||
const {data = [], isLoading} = useQuery<ItemWithImage[]>({
|
const items: ItemWithImage[] = useMemo(() => data || [], [data]);
|
||||||
queryKey: ['fetchItemListWithImage'],
|
|
||||||
queryFn: fetchItemListWithImage,
|
|
||||||
retry: 3, // Versucht es 3-mal erneut
|
|
||||||
retryDelay: 1000, // Wartezeit zwischen den Versuchen (in ms)
|
|
||||||
});
|
|
||||||
|
|
||||||
const items: ItemWithImage[] = useMemo(() => data || [], [data]);
|
const discountedPrices = items.map(
|
||||||
|
(item) => item.price100 * (1 - item.discount100 / 100),
|
||||||
|
);
|
||||||
|
const minPrice =
|
||||||
|
discountedPrices.length > 0 ? Math.min(...discountedPrices) : 0;
|
||||||
|
const maxPrice =
|
||||||
|
discountedPrices.length > 0 ? Math.max(...discountedPrices) : 1000;
|
||||||
|
const [priceRange, setPriceRange] = useState<[number, number]>([
|
||||||
|
minPrice,
|
||||||
|
maxPrice,
|
||||||
|
]);
|
||||||
|
|
||||||
const discountedPrices = items.map(
|
// Filter aus URL übernehmen
|
||||||
(item) => item.price100 * (1 - item.discount100 / 100)
|
useEffect(() => {
|
||||||
);
|
const params = new URLSearchParams(location.search);
|
||||||
const minPrice = discountedPrices.length > 0 ? Math.min(...discountedPrices) : 0;
|
const category = params.get("category");
|
||||||
const maxPrice = discountedPrices.length > 0 ? Math.max(...discountedPrices) : 1000;
|
if (category && categoriesFilter.some((f) => f.value === category)) {
|
||||||
const [priceRange, setPriceRange] = useState<[number, number]>([
|
setSelectedCategory(category);
|
||||||
minPrice,
|
} else {
|
||||||
maxPrice,
|
setSelectedCategory(null);
|
||||||
]);
|
}
|
||||||
|
}, [location.search, categoriesFilter]);
|
||||||
|
|
||||||
// Filter aus URL übernehmen
|
// Filterfunktion bleibt gleich
|
||||||
useEffect(() => {
|
const filteredItems: ItemWithImage[] = useMemo(() => {
|
||||||
const params = new URLSearchParams(location.search);
|
return items
|
||||||
const category = params.get("category");
|
.filter((item) => {
|
||||||
if (category && categoriesFilter.some((f) => f.value === category)) {
|
const discountedPrice = item.price100 * (1 - item.discount100 / 100);
|
||||||
setSelectedCategory(category);
|
return (
|
||||||
} else {
|
discountedPrice >= priceRange[0] && discountedPrice <= priceRange[1]
|
||||||
setSelectedCategory(null);
|
);
|
||||||
}
|
})
|
||||||
}, [location.search, categoriesFilter]);
|
.filter((item) => {
|
||||||
|
if (!selectedCategory) return true;
|
||||||
|
return item.category.toLowerCase() === selectedCategory.toLowerCase();
|
||||||
|
})
|
||||||
|
.filter((item) => {
|
||||||
|
if (!selectedRating) return true;
|
||||||
|
const rating = item.rating;
|
||||||
|
return rating >= Number(selectedRating) * 2;
|
||||||
|
})
|
||||||
|
.filter((item) => {
|
||||||
|
if (!searchQuery) return true;
|
||||||
|
return item.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
});
|
||||||
|
}, [items, priceRange, selectedCategory, selectedRating, searchQuery]);
|
||||||
|
|
||||||
// Filterfunktion bleibt gleich
|
// Lese die Suchanfrage aus der URL
|
||||||
const filteredItems: ItemWithImage[] = useMemo(() => {
|
useEffect(() => {
|
||||||
return items
|
const params = new URLSearchParams(location.search);
|
||||||
.filter((item) => {
|
const query = params.get("search");
|
||||||
const discountedPrice = item.price100 * (1 - item.discount100 / 100);
|
setSearchQuery(query);
|
||||||
return discountedPrice >= priceRange[0] && discountedPrice <= priceRange[1];
|
}, [location.search]);
|
||||||
})
|
|
||||||
.filter((item) => {
|
|
||||||
if (!selectedCategory) return true;
|
|
||||||
return item.category.toLowerCase() === selectedCategory.toLowerCase();
|
|
||||||
})
|
|
||||||
.filter((item) => {
|
|
||||||
if (!selectedRating) return true;
|
|
||||||
const rating = item.rating;
|
|
||||||
return rating >= (Number(selectedRating) * 2);
|
|
||||||
})
|
|
||||||
.filter((item) => {
|
|
||||||
if (!searchQuery) return true;
|
|
||||||
return (item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [items, priceRange, selectedCategory, selectedRating, searchQuery]);
|
|
||||||
|
|
||||||
|
// Container Ref
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Lese die Suchanfrage aus der URL
|
const prevItemsLength = useRef(items.length);
|
||||||
useEffect(() => {
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
const query = params.get("search");
|
|
||||||
setSearchQuery(query);
|
|
||||||
}, [location.search]);
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (items.length >= prevItemsLength.current) {
|
||||||
|
prevItemsLength.current = items.length;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Container Ref
|
setTimeout(() => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
containerRef.current?.scrollTo(0, 0);
|
||||||
|
}, 50);
|
||||||
|
|
||||||
const prevItemsLength = useRef(items.length);
|
prevItemsLength.current = items.length;
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
useEffect(() => {
|
// Kategorie-Änderung
|
||||||
if (items.length >= prevItemsLength.current) {
|
const handleCategoryChange = (category: string) => {
|
||||||
prevItemsLength.current = items.length;
|
if (category === "") {
|
||||||
return;
|
setSelectedCategory(null);
|
||||||
}
|
navigate(`/`);
|
||||||
|
} else {
|
||||||
|
setSelectedCategory(category);
|
||||||
|
navigate(`/?category=${encodeURIComponent(category)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
setTimeout(() => {
|
// Rating-Änderung (bleibt gleich)
|
||||||
containerRef.current?.scrollTo(0, 0);
|
const handleRatingChange = (rating: string) => {
|
||||||
}, 50);
|
if (rating === "") {
|
||||||
|
setSelectedRating(null);
|
||||||
|
} else {
|
||||||
|
setSelectedRating(rating);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
prevItemsLength.current = items.length;
|
return (
|
||||||
}, [items]);
|
<div
|
||||||
|
className="home-page-background"
|
||||||
// Kategorie-Änderung
|
style={{ backgroundColor: theme.palette.homepage }}
|
||||||
const handleCategoryChange = (category: string) => {
|
>
|
||||||
if (category === "") {
|
<div className="sidebar sidebar-filter">
|
||||||
setSelectedCategory(null);
|
<FilterItem
|
||||||
navigate(`/`);
|
filterName={t("category")}
|
||||||
} else {
|
filterItems={categoriesFilter}
|
||||||
setSelectedCategory(category);
|
value={selectedCategory}
|
||||||
navigate(`/?category=${encodeURIComponent(category)}`);
|
onChange={handleCategoryChange}
|
||||||
}
|
/>
|
||||||
};
|
<PriceSlider
|
||||||
|
min={minPrice}
|
||||||
// Rating-Änderung (bleibt gleich)
|
max={maxPrice}
|
||||||
const handleRatingChange = (rating: string) => {
|
onChange={(range) => {
|
||||||
if (rating === "") {
|
setPriceRange(range);
|
||||||
setSelectedRating(null);
|
}}
|
||||||
} else {
|
/>
|
||||||
setSelectedRating(rating);
|
<FilterItem
|
||||||
}
|
filterName={t("ratingFrom")}
|
||||||
};
|
filterItems={ratingFilter}
|
||||||
|
value={selectedRating}
|
||||||
return (
|
onChange={handleRatingChange}
|
||||||
<div
|
/>
|
||||||
className="home-page-background"
|
</div>
|
||||||
style={{backgroundColor: theme.palette.homepage}}
|
<div
|
||||||
>
|
className="home-page-background"
|
||||||
<div className="sidebar sidebar-filter">
|
style={{ backgroundColor: theme.palette.homepage }}
|
||||||
<FilterItem
|
>
|
||||||
filterName={t("category")}
|
{isLoading && t("loading")}
|
||||||
filterItems={categoriesFilter}
|
{!isLoading && (
|
||||||
value={selectedCategory}
|
<main
|
||||||
onChange={handleCategoryChange}
|
className="page-background page-background-center"
|
||||||
/>
|
ref={containerRef}
|
||||||
<PriceSlider
|
>
|
||||||
min={minPrice}
|
<Box className="cardgrid">
|
||||||
max={maxPrice}
|
{filteredItems.length === 0 ? (
|
||||||
onChange={(range) => {
|
<Alert
|
||||||
setPriceRange(range);
|
variant="filled"
|
||||||
}}
|
severity="error"
|
||||||
/>
|
sx={{
|
||||||
<FilterItem
|
bgcolor: theme.palette.error.main,
|
||||||
filterName={t("rating")}
|
color: theme.palette.getContrastText(
|
||||||
filterItems={ratingFilter}
|
theme.palette.error.main,
|
||||||
value={selectedRating}
|
),
|
||||||
onChange={handleRatingChange}
|
width: "100%",
|
||||||
/>
|
justifyContent: "center",
|
||||||
</div>
|
}}
|
||||||
<div
|
|
||||||
className="home-page-background"
|
|
||||||
style={{backgroundColor: theme.palette.homepage}}
|
|
||||||
>
|
|
||||||
{isLoading && t('loading')}
|
|
||||||
{!isLoading && <main
|
|
||||||
className="page-background page-background-center"
|
|
||||||
ref={containerRef}
|
|
||||||
>
|
>
|
||||||
<Box className="cardgrid">
|
{t("noItemsFound")}
|
||||||
{filteredItems.length === 0 ? (
|
</Alert>
|
||||||
<Alert
|
) : (
|
||||||
variant="filled"
|
filteredItems.map((item) => (
|
||||||
severity="error"
|
<ItemCard key={item.id} item={item} />
|
||||||
sx={{
|
))
|
||||||
bgcolor: theme.palette.error.main,
|
)}
|
||||||
color: theme.palette.getContrastText(
|
</Box>
|
||||||
theme.palette.error.main
|
</main>
|
||||||
),
|
)}
|
||||||
width: "100%",
|
</div>
|
||||||
justifyContent: "center",
|
</div>
|
||||||
}}
|
);
|
||||||
>
|
|
||||||
{t("noItemsFound")}
|
|
||||||
</Alert>
|
|
||||||
) : (
|
|
||||||
filteredItems.map(item => (
|
|
||||||
<ItemCard key={item.id} item={item}/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</main>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,40 @@
|
|||||||
import {Box, Button, Typography, useTheme} from "@mui/material";
|
import { Box, Button, Typography, useTheme } from "@mui/material";
|
||||||
import {useNavigate} from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import "./pages.css";
|
import "./pages.css";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function NoPage() {
|
export default function NoPage() {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const theme = useTheme();
|
const handleGoHome = () => {
|
||||||
const {t} = useTranslation();
|
navigate("/");
|
||||||
const navigate = useNavigate();
|
};
|
||||||
|
|
||||||
const handleGoHome = () => {
|
return (
|
||||||
navigate("/");
|
<Box
|
||||||
};
|
className="no-page-container"
|
||||||
|
sx={{ color: theme.palette.text.primary }}
|
||||||
return (
|
>
|
||||||
<Box
|
<Typography variant="h1" className="no-page-title">
|
||||||
className="no-page-container"
|
404
|
||||||
sx={{color: theme.palette.text.primary}}
|
</Typography>
|
||||||
>
|
<Typography variant="h5" className="no-page-subtitle">
|
||||||
<Typography variant="h1" className="no-page-title">
|
{t("pageDoesNotExist")}
|
||||||
404
|
</Typography>
|
||||||
</Typography>
|
<Typography variant="body1" className="no-page-description">
|
||||||
<Typography variant="h5" className="no-page-subtitle">
|
{t("wrongTurn")}
|
||||||
{t('pageDoesNotExist')}
|
</Typography>
|
||||||
</Typography>
|
<Button
|
||||||
<Typography variant="body1" className="no-page-description">
|
variant="contained"
|
||||||
{t('wrongTurn')}
|
color="primary"
|
||||||
</Typography>
|
size="large"
|
||||||
<Button
|
className="no-page-button"
|
||||||
variant="contained"
|
onClick={handleGoHome}
|
||||||
color="primary"
|
>
|
||||||
size="large"
|
{t("backToHome")}
|
||||||
className="no-page-button"
|
</Button>
|
||||||
onClick={handleGoHome}
|
</Box>
|
||||||
>
|
);
|
||||||
{t('backToHome')}
|
}
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,149 +1,185 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Divider,
|
Divider,
|
||||||
List,
|
List,
|
||||||
ListItemButton,
|
ListItemButton,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
Tab,
|
Tab,
|
||||||
Tabs,
|
Tabs,
|
||||||
Typography
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import OrderType, {OrderStatusEnum} from "../components/Order";
|
import OrderType, { OrderStatusEnum } from "../components/Order";
|
||||||
import "./pages.css";
|
import "./pages.css";
|
||||||
import {useAccount} from "../helper/AccountProvider";
|
import { useAccount } from "../helper/AccountProvider";
|
||||||
import {fetchOrders, orderPatch} from "../helper/query/Queries";
|
import { fetchOrders, orderPatch } from "../helper/query/Queries";
|
||||||
|
|
||||||
export default function Orders() {
|
export default function Orders() {
|
||||||
|
const { user } = useAccount();
|
||||||
|
const [orders, setOrders] = useState<OrderType[]>([]);
|
||||||
|
|
||||||
const {user} = useAccount();
|
const { data: accountOrders, refetch } = useQuery<OrderType[]>({
|
||||||
const [orders, setOrders] = useState<OrderType[]>([])
|
queryKey: ["fetchOrders", user?.customerId], // Hier sollte die tatsächliche Kunden-ID verwendet werden
|
||||||
|
queryFn: () => (user ? fetchOrders(user.customerId) : Promise.resolve([])), // Simulierte API-Antwort
|
||||||
|
enabled: !!user, // Nur ausführen, wenn user existiert
|
||||||
|
retry: 3, // Versucht es 3-mal erneut
|
||||||
|
retryDelay: 1000, // Wartezeit zwischen den Versuchen (in ms)
|
||||||
|
});
|
||||||
|
|
||||||
const {data: accountOrders, refetch} = useQuery<OrderType[]>({
|
useEffect(() => {
|
||||||
queryKey: ['fetchOrders', user?.customerId], // Hier sollte die tatsächliche Kunden-ID verwendet werden
|
setOrders(accountOrders ?? []);
|
||||||
queryFn: () => user ? fetchOrders(user.customerId) : Promise.resolve([]), // Simulierte API-Antwort
|
console.log("Orders fetched:", accountOrders);
|
||||||
enabled: !!user, // Nur ausführen, wenn user existiert
|
}, [accountOrders]);
|
||||||
retry: 3, // Versucht es 3-mal erneut
|
|
||||||
retryDelay: 1000, // Wartezeit zwischen den Versuchen (in ms)
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
const { t } = useTranslation();
|
||||||
setOrders(accountOrders ?? []);
|
|
||||||
console.log("Orders fetched:", accountOrders);
|
|
||||||
}, [accountOrders]);
|
|
||||||
|
|
||||||
const {t} = useTranslation();
|
const [tab, setTab] = useState(0);
|
||||||
|
const [selectedOrder, setSelectedOrder] = useState<OrderType | null>(null);
|
||||||
|
|
||||||
const [tab, setTab] = useState(0);
|
const activeOrders = orders.filter(
|
||||||
const [selectedOrder, setSelectedOrder] = useState<OrderType | null>(null);
|
(o) =>
|
||||||
|
o.status === OrderStatusEnum.ISSUES ||
|
||||||
|
o.status === OrderStatusEnum.IN_PROGRESS ||
|
||||||
|
o.status === OrderStatusEnum.ORDERED,
|
||||||
|
);
|
||||||
|
const inactiveOrders = orders.filter(
|
||||||
|
(o) =>
|
||||||
|
o.status === OrderStatusEnum.CANCELLED ||
|
||||||
|
o.status === OrderStatusEnum.DELIVERED,
|
||||||
|
);
|
||||||
|
|
||||||
const activeOrders = orders.filter(o => o.status === OrderStatusEnum.ISSUES || o.status === OrderStatusEnum.IN_PROGRESS || o.status === OrderStatusEnum.ORDERED);
|
const handleTabChange = (_: React.SyntheticEvent, newValue: number) =>
|
||||||
const inactiveOrders = orders.filter(o => o.status === OrderStatusEnum.CANCELLED || o.status === OrderStatusEnum.DELIVERED);
|
setTab(newValue);
|
||||||
|
|
||||||
const handleTabChange = (_: React.SyntheticEvent, newValue: number) => setTab(newValue);
|
const { refetch: cancleOrder } = useQuery({
|
||||||
|
queryKey: [
|
||||||
|
"orderPatch",
|
||||||
|
{ id: selectedOrder?.id || -1, status: OrderStatusEnum.CANCELLED },
|
||||||
|
],
|
||||||
|
queryFn: () =>
|
||||||
|
orderPatch({
|
||||||
|
id: selectedOrder?.id || -1,
|
||||||
|
status: OrderStatusEnum.CANCELLED,
|
||||||
|
}),
|
||||||
|
retry: 0,
|
||||||
|
retryDelay: 1000,
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCancelOrder = async () => {
|
||||||
|
await cancleOrder();
|
||||||
|
setSelectedOrder(null);
|
||||||
|
refetch();
|
||||||
|
};
|
||||||
|
|
||||||
const {refetch: cancleOrder} = useQuery({
|
return (
|
||||||
queryKey: ["orderPatch", {id: selectedOrder?.id || -1, status: OrderStatusEnum.CANCELLED}],
|
<Box
|
||||||
queryFn: () => orderPatch({id: selectedOrder?.id || -1, status: OrderStatusEnum.CANCELLED}),
|
className="page-background page-background-center"
|
||||||
retry: 0,
|
sx={{ minHeight: "100vh", pt: 4 }}
|
||||||
retryDelay: 1000,
|
>
|
||||||
enabled: false,
|
<Paper
|
||||||
});
|
elevation={3}
|
||||||
|
sx={{ p: 4, maxWidth: 700, width: "100%", mx: "auto" }}
|
||||||
|
>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
{t("myOrders")}
|
||||||
|
</Typography>
|
||||||
|
<Tabs value={tab} onChange={handleTabChange} sx={{ mb: 3 }}>
|
||||||
|
<Tab label={t("active")} />
|
||||||
|
<Tab label={t("previous")} />
|
||||||
|
</Tabs>
|
||||||
|
<Divider sx={{ mb: 2 }} />
|
||||||
|
{tab === 0 ? (
|
||||||
|
activeOrders.length > 0 ? (
|
||||||
|
<List>
|
||||||
|
{activeOrders.map((order) => (
|
||||||
|
<ListItemButton
|
||||||
|
key={order.id}
|
||||||
|
onClick={() => setSelectedOrder(order)}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={`${t("order")} #${order.id} • ${order.status} • ${new Date(order.time).toUTCString()}`}
|
||||||
|
secondary={`${t("sum")}: ${(order.total / 100).toFixed(2)} € • ${order.orderItems.length} ${t("items")}`}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
) : (
|
||||||
|
<Typography color="text.secondary">
|
||||||
|
{t("noActiveOrders")}
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
) : inactiveOrders.length > 0 ? (
|
||||||
|
<List>
|
||||||
|
{inactiveOrders.map((order) => (
|
||||||
|
<ListItemButton
|
||||||
|
key={order.id}
|
||||||
|
onClick={() => setSelectedOrder(order)}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={`${t("order")} #${order.id} • ${order.status} • ${new Date(order.time).toUTCString()}`}
|
||||||
|
secondary={`${t("sum")}: ${(order.total / 100).toFixed(2)} € • ${order.orderItems.length} ${t("items")}`}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
) : (
|
||||||
|
<Typography color="text.secondary">
|
||||||
|
{t("noPreviousOrders")}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
const handleCancelOrder = async () => {
|
<Dialog
|
||||||
await cancleOrder();
|
open={!!selectedOrder}
|
||||||
setSelectedOrder(null);
|
onClose={() => setSelectedOrder(null)}
|
||||||
refetch();
|
maxWidth="sm"
|
||||||
};
|
fullWidth
|
||||||
|
>
|
||||||
return (
|
<DialogTitle>
|
||||||
<Box className="page-background page-background-center" sx={{minHeight: "100vh", pt: 4}}>
|
{`${selectedOrder?.status === OrderStatusEnum.IN_PROGRESS ? t("activeOrder") : t("previousOrder")} #${selectedOrder?.id}`}
|
||||||
<Paper elevation={3} sx={{p: 4, maxWidth: 700, width: "100%", mx: "auto"}}>
|
</DialogTitle>
|
||||||
<Typography variant="h4" gutterBottom>
|
<DialogContent dividers>
|
||||||
{t('myOrders')}
|
{selectedOrder && (
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Typography variant="subtitle1">{`${t("orderDate")}: ${new Date(selectedOrder.time).toUTCString()}`}</Typography>
|
||||||
|
<Divider />
|
||||||
|
<Typography variant="subtitle2">
|
||||||
|
{t("orderedItems")}:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Tabs value={tab} onChange={handleTabChange} sx={{mb: 3}}>
|
<List dense>
|
||||||
<Tab label={t('active')}/>
|
{selectedOrder.orderItems.map((item, idx) => (
|
||||||
<Tab label={t('previous')}/>
|
<ListItemText
|
||||||
</Tabs>
|
key={idx}
|
||||||
<Divider sx={{mb: 2}}/>
|
primary={`${item.article} x${item.amount}`}
|
||||||
{tab === 0 ? (
|
/>
|
||||||
activeOrders.length > 0 ? (
|
))}
|
||||||
<List>
|
</List>
|
||||||
{activeOrders.map(order => (
|
<Divider />
|
||||||
<ListItemButton key={order.id} onClick={() => setSelectedOrder(order)}>
|
<Typography variant="h6">{`${t("sum")}: ${(selectedOrder.total / 100).toFixed(2)} €`}</Typography>
|
||||||
<ListItemText
|
</Stack>
|
||||||
primary={`${t('order')} #${order.id} • ${order.status} • ${new Date(order.time).toUTCString()}`}
|
)}
|
||||||
secondary={`${t('sum')}: ${(order.total / 100).toFixed(2)} € • ${order.orderItems.length} ${t('items')}`}
|
</DialogContent>
|
||||||
/>
|
<DialogActions>
|
||||||
</ListItemButton>
|
{(selectedOrder?.status === OrderStatusEnum.ISSUES ||
|
||||||
))}
|
selectedOrder?.status === OrderStatusEnum.IN_PROGRESS ||
|
||||||
</List>
|
selectedOrder?.status === OrderStatusEnum.ORDERED) && (
|
||||||
) : (
|
<Button color="error" onClick={() => handleCancelOrder()}>
|
||||||
<Typography color="text.secondary">{t('noActiveOrders')}</Typography>
|
{t("cancelOrder")}
|
||||||
)
|
</Button>
|
||||||
) : (
|
)}
|
||||||
inactiveOrders.length > 0 ? (
|
<Button onClick={() => setSelectedOrder(null)}>{t("close")}</Button>
|
||||||
<List>
|
</DialogActions>
|
||||||
{inactiveOrders.map(order => (
|
</Dialog>
|
||||||
<ListItemButton key={order.id} onClick={() => setSelectedOrder(order)}>
|
</Paper>
|
||||||
<ListItemText
|
</Box>
|
||||||
primary={`${t('order')} #${order.id} • ${order.status} • ${new Date(order.time).toUTCString()}`}
|
);
|
||||||
secondary={`${t('sum')}: ${(order.total / 100).toFixed(2)} € • ${order.orderItems.length} ${t('items')}`}
|
|
||||||
/>
|
|
||||||
</ListItemButton>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
) : (
|
|
||||||
<Typography color="text.secondary">{t('noPreviousOrders')}</Typography>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Dialog open={!!selectedOrder} onClose={() => setSelectedOrder(null)} maxWidth="sm" fullWidth>
|
|
||||||
<DialogTitle>
|
|
||||||
{`${selectedOrder?.status === OrderStatusEnum.IN_PROGRESS ? t('activeOrder') : t('previousOrder')} #${selectedOrder?.id}`}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogContent dividers>
|
|
||||||
{selectedOrder && (
|
|
||||||
<Stack spacing={2}>
|
|
||||||
<Typography
|
|
||||||
variant="subtitle1">{`${t('orderDate')}: ${new Date(selectedOrder.time).toUTCString()}`}</Typography>
|
|
||||||
<Divider/>
|
|
||||||
<Typography variant="subtitle2">{t('orderedItems')}:</Typography>
|
|
||||||
<List dense>
|
|
||||||
{selectedOrder.orderItems.map((item, idx) => (
|
|
||||||
<ListItemText
|
|
||||||
key={idx}
|
|
||||||
primary={`${item.article} x${item.amount}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
<Divider/>
|
|
||||||
<Typography
|
|
||||||
variant="h6">{`${t('sum')}: ${(selectedOrder.total / 100).toFixed(2)} €`}</Typography>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
{(selectedOrder?.status === OrderStatusEnum.ISSUES || selectedOrder?.status === OrderStatusEnum.IN_PROGRESS || selectedOrder?.status === OrderStatusEnum.ORDERED) && (
|
|
||||||
<Button color="error" onClick={() => handleCancelOrder()}>
|
|
||||||
{t('cancelOrder')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button onClick={() => setSelectedOrder(null)}>{t('close')}</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,373 +1,403 @@
|
|||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Container,
|
Container,
|
||||||
Divider,
|
Divider,
|
||||||
Grid,
|
Grid,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Paper,
|
Paper,
|
||||||
Step,
|
Step,
|
||||||
StepLabel,
|
StepLabel,
|
||||||
Stepper,
|
Stepper,
|
||||||
TextField,
|
TextField,
|
||||||
Typography
|
Typography,
|
||||||
} from '@mui/material';
|
} from "@mui/material";
|
||||||
import {useMutation, useQuery} from '@tanstack/react-query';
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import {TFunction} from 'i18next';
|
import { TFunction } from "i18next";
|
||||||
import React, {useEffect, useState} from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {useNavigate} from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
import {CustomerType} from '../components/Account';
|
import { CustomerType } from "../components/Account";
|
||||||
import Item from '../components/Item';
|
import Item from "../components/Item";
|
||||||
import OrderType, {OrderStatusEnum} from '../components/Order';
|
import OrderType, { OrderStatusEnum } from "../components/Order";
|
||||||
import {useAccount} from '../helper/AccountProvider';
|
import { useAccount } from "../helper/AccountProvider";
|
||||||
import {BasketItem, useBasket} from '../helper/BasketProvider';
|
import { BasketItem, useBasket } from "../helper/BasketProvider";
|
||||||
import {fetchCustomer, submitCustomer, submitOrder} from '../helper/query/Queries';
|
import {
|
||||||
|
fetchCustomer,
|
||||||
|
submitCustomer,
|
||||||
|
submitOrder,
|
||||||
|
} from "../helper/query/Queries";
|
||||||
|
|
||||||
function getDiscountedPrice(item: Item): number {
|
function getDiscountedPrice(item: Item): number {
|
||||||
return (item.price100 / 100 * (100 - item.discount100) / 100);
|
return ((item.price100 / 100) * (100 - item.discount100)) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateBasket(t: TFunction<"translation", undefined>, basket: BasketItem[]) {
|
function generateBasket(
|
||||||
return basket.length === 0 ? (
|
t: TFunction<"translation", undefined>,
|
||||||
<Typography color="error" sx={{my: 2}}>
|
basket: BasketItem[],
|
||||||
{t('basketEmpty')}
|
) {
|
||||||
</Typography>
|
return basket.length === 0 ? (
|
||||||
) : (
|
<Typography color="error" sx={{ my: 2 }}>
|
||||||
<List>
|
{t("basketEmpty")}
|
||||||
{basket.map((item) => (
|
</Typography>
|
||||||
<ListItem key={item.item.uuid}>
|
) : (
|
||||||
<ListItemText
|
<List>
|
||||||
primary={`${item.quantity}x ${item.item.name}`}
|
{basket.map((item) => (
|
||||||
secondary={`Item ID: ${item.item.uuid}`}
|
<ListItem key={item.item.uuid}>
|
||||||
/>
|
<ListItemText
|
||||||
|
primary={`${item.quantity}x ${item.item.name}`}
|
||||||
|
secondary={`Item ID: ${item.item.uuid}`}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="rightBound">
|
<div className="rightBound">
|
||||||
{`${(item.quantity * getDiscountedPrice(item.item)).toFixed(2)} €`}<br/>
|
{`${(item.quantity * getDiscountedPrice(item.item)).toFixed(2)} €`}
|
||||||
{item.item.discount100 > 0 ? <a className='rightBound red'>{-item.item.discount100}%</a> : ""}
|
<br />
|
||||||
</div>
|
{item.item.discount100 > 0 ? (
|
||||||
</ListItem>
|
<a className="rightBound red">{-item.item.discount100}%</a>
|
||||||
))}
|
) : (
|
||||||
</List>
|
""
|
||||||
)
|
)}
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateTotal(t: TFunction<"translation", undefined>, basket: BasketItem[]) {
|
function generateTotal(
|
||||||
return basket.length === 0 ? "" :
|
t: TFunction<"translation", undefined>,
|
||||||
<div className='rightBound'>
|
basket: BasketItem[],
|
||||||
{t('total') + ": " + basket.map((item) => item.quantity * getDiscountedPrice(item.item))
|
) {
|
||||||
.reduce((prev: number, cur: number) => prev + cur, 0).toFixed(2) + ` €`}
|
return basket.length === 0 ? (
|
||||||
</div>
|
""
|
||||||
|
) : (
|
||||||
|
<div className="rightBound">
|
||||||
|
{t("total") +
|
||||||
|
": " +
|
||||||
|
basket
|
||||||
|
.map((item) => item.quantity * getDiscountedPrice(item.item))
|
||||||
|
.reduce((prev: number, cur: number) => prev + cur, 0)
|
||||||
|
.toFixed(2) +
|
||||||
|
` €`}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Payment() {
|
export default function Payment() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { basket, clearBasket } = useBasket();
|
||||||
|
const navigator = useNavigate();
|
||||||
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
|
const [shippingDetails, setShippingDetails] = useState<CustomerType>({
|
||||||
|
id: 0, // This will be set by the backend or user data
|
||||||
|
name: "",
|
||||||
|
surname: "",
|
||||||
|
address: "",
|
||||||
|
zip: "",
|
||||||
|
country: "Deutschland",
|
||||||
|
});
|
||||||
|
const [orderNumber, setOrderNumber] = useState<string | null>(null);
|
||||||
|
const steps = [
|
||||||
|
t("reviewCart"),
|
||||||
|
t("shippingDetails"),
|
||||||
|
t("payment"),
|
||||||
|
t("orderSummary"),
|
||||||
|
];
|
||||||
|
const { user } = useAccount();
|
||||||
|
|
||||||
|
const submitOrderData: OrderType = {
|
||||||
|
id: 0, // This will be set by the backend
|
||||||
|
customerId: user ? user.customerId : 0, // Use user ID if logged in, otherwise 0
|
||||||
|
time: Date.now(),
|
||||||
|
status: OrderStatusEnum.ORDERED, // Initial status when order is placed
|
||||||
|
orderItems: basket.map((item) => ({
|
||||||
|
id: item.item.id,
|
||||||
|
amount: item.quantity,
|
||||||
|
article: item.item.uuid, // Assuming UUID is the identifier for the item
|
||||||
|
})),
|
||||||
|
total: basket.reduce(
|
||||||
|
(total, item) => total + item.quantity * getDiscountedPrice(item.item),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
const {t} = useTranslation();
|
const { refetch: refetchCustomer } = useQuery({
|
||||||
const {basket, clearBasket} = useBasket();
|
queryKey: ["submitCustomer", shippingDetails],
|
||||||
const navigator = useNavigate();
|
queryFn: () => submitCustomer(shippingDetails),
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
retry: 0,
|
||||||
const [shippingDetails, setShippingDetails] = useState<CustomerType>({
|
retryDelay: 1000,
|
||||||
id: 0, // This will be set by the backend or user data
|
enabled: false,
|
||||||
name: '',
|
});
|
||||||
surname: '',
|
|
||||||
address: '',
|
|
||||||
zip: '',
|
|
||||||
country: 'Deutschland',
|
|
||||||
});
|
|
||||||
const [orderNumber, setOrderNumber] = useState<string | null>(null);
|
|
||||||
const steps = [t('reviewCart'), t('shippingDetails'), t('payment'), t('orderSummary')];
|
|
||||||
const {user} = useAccount();
|
|
||||||
|
|
||||||
const submitOrderData: OrderType = {
|
const showAlert = () => {
|
||||||
id: 0, // This will be set by the backend
|
return <Alert>//TODO:</Alert>;
|
||||||
customerId: user ? user.customerId : 0, // Use user ID if logged in, otherwise 0
|
};
|
||||||
time: Date.now(),
|
|
||||||
status: OrderStatusEnum.ORDERED, // Initial status when order is placed
|
|
||||||
orderItems: basket.map(item => ({
|
|
||||||
id: item.item.id,
|
|
||||||
amount: item.quantity,
|
|
||||||
article: item.item.uuid, // Assuming UUID is the identifier for the item
|
|
||||||
})),
|
|
||||||
total: basket.reduce((total, item) => total + (item.quantity * getDiscountedPrice(item.item)), 0),
|
|
||||||
};
|
|
||||||
|
|
||||||
const {refetch: refetchCustomer} = useQuery({
|
const { refetch: customerData } = useQuery<CustomerType>({
|
||||||
queryKey: ["submitCustomer", shippingDetails],
|
queryKey: ["fetchCustomer", user?.customerId],
|
||||||
queryFn: () => submitCustomer(shippingDetails),
|
queryFn: () => fetchCustomer(user?.customerId || 0), // Funktion zum Abrufen der Kundendaten
|
||||||
retry: 0,
|
enabled: false,
|
||||||
retryDelay: 1000,
|
});
|
||||||
enabled: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const showAlert = () => {
|
useEffect(() => {
|
||||||
return <Alert>
|
const fetchShippingDetails = async () => {
|
||||||
//TODO:
|
if (user) {
|
||||||
</Alert>
|
try {
|
||||||
};
|
const userShippingDetails = (await customerData()).data;
|
||||||
|
setShippingDetails(userShippingDetails || shippingDetails);
|
||||||
const {refetch: customerData} = useQuery<CustomerType>({
|
} catch (error) {
|
||||||
queryKey: ['fetchCustomer', user?.customerId],
|
console.error("Fehler beim Laden der Kundendaten:", error);
|
||||||
queryFn: () => fetchCustomer(user?.customerId || 0), // Funktion zum Abrufen der Kundendaten
|
|
||||||
enabled: false
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchShippingDetails = async () => {
|
|
||||||
if (user) {
|
|
||||||
try {
|
|
||||||
const userShippingDetails = (await customerData()).data;
|
|
||||||
setShippingDetails(userShippingDetails || shippingDetails);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Fehler beim Laden der Kundendaten:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchShippingDetails();
|
|
||||||
}, [user, customerData]);
|
|
||||||
|
|
||||||
|
|
||||||
// Verwende useMutation statt useQuery für submitOrder
|
|
||||||
const {mutateAsync: submitOrderMutation} = useMutation({
|
|
||||||
mutationFn: (orderData: OrderType) => submitOrder(orderData),
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleNext = async () => {
|
|
||||||
let next: boolean = true;
|
|
||||||
|
|
||||||
if (activeStep === steps.length - 2) {
|
|
||||||
// Simulate order placement and generate order number
|
|
||||||
const generatedOrderNumber = `ORD-${Math.floor(Math.random() * 1000000)}`;
|
|
||||||
setOrderNumber(generatedOrderNumber);
|
|
||||||
|
|
||||||
let customerId = user ? user.customerId : 0;
|
|
||||||
if (!customerId) {
|
|
||||||
const customerResponse = await refetchCustomer();
|
|
||||||
customerId = customerResponse.data.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Erzeuge die Orderdaten mit der richtigen customerId
|
|
||||||
const orderData: OrderType = {
|
|
||||||
...submitOrderData,
|
|
||||||
customerId,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
await submitOrderMutation(orderData);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
} catch (e) {
|
|
||||||
next = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (next) {
|
|
||||||
setActiveStep((prevStep) => prevStep + 1);
|
|
||||||
} else {
|
|
||||||
showAlert();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBack = () => {
|
fetchShippingDetails();
|
||||||
setActiveStep((prevStep) => prevStep - 1);
|
}, [user, customerData]);
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
// Verwende useMutation statt useQuery für submitOrder
|
||||||
const {name, value} = e.target;
|
const { mutateAsync: submitOrderMutation } = useMutation({
|
||||||
setShippingDetails((prevDetails) => ({
|
mutationFn: (orderData: OrderType) => submitOrder(orderData),
|
||||||
...prevDetails,
|
});
|
||||||
[name]: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
const handleClearBasket = () => {
|
|
||||||
clearBasket();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hilfsfunktion prüfen, ob alle Pflichtfelder ausgefüllt sind
|
const handleNext = async () => {
|
||||||
const isShippingDetailsValid = () => {
|
let next: boolean = true;
|
||||||
return (
|
|
||||||
shippingDetails.name.trim() !== '' &&
|
|
||||||
shippingDetails.surname.trim() !== '' &&
|
|
||||||
shippingDetails.address.trim() !== '' &&
|
|
||||||
shippingDetails.zip.trim() !== '' &&
|
|
||||||
shippingDetails.country.trim() !== ''
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderStepContent = (step: number) => {
|
if (activeStep === steps.length - 2) {
|
||||||
switch (step) {
|
// Simulate order placement and generate order number
|
||||||
case 0:
|
const generatedOrderNumber = `ORD-${Math.floor(Math.random() * 1000000)}`;
|
||||||
return (
|
setOrderNumber(generatedOrderNumber);
|
||||||
<Box>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
{t('reviewCart')}
|
|
||||||
</Typography>
|
|
||||||
{generateBasket(t, basket)}
|
|
||||||
<Divider sx={{my: 2}}/>
|
|
||||||
<Button variant="outlined" color="error" onClick={handleClearBasket}
|
|
||||||
disabled={basket.length === 0}>
|
|
||||||
{t('clearCart')}
|
|
||||||
</Button>
|
|
||||||
{generateTotal(t, basket)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
case 1:
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
{t('shippingDetails')}
|
|
||||||
</Typography>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
|
|
||||||
<TextField
|
let customerId = user ? user.customerId : 0;
|
||||||
fullWidth
|
if (!customerId) {
|
||||||
label={t('firstName')}
|
const customerResponse = await refetchCustomer();
|
||||||
name="name"
|
customerId = customerResponse.data.id;
|
||||||
value={shippingDetails.name}
|
}
|
||||||
onChange={handleInputChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
// Erzeuge die Orderdaten mit der richtigen customerId
|
||||||
fullWidth
|
const orderData: OrderType = {
|
||||||
label={t('lastName')}
|
...submitOrderData,
|
||||||
name="surname"
|
customerId,
|
||||||
value={shippingDetails.surname}
|
};
|
||||||
onChange={handleInputChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
try {
|
||||||
fullWidth
|
await submitOrderMutation(orderData);
|
||||||
label={t('address')}
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
name="address"
|
} catch (e) {
|
||||||
value={shippingDetails.address}
|
next = false;
|
||||||
onChange={handleInputChange}
|
}
|
||||||
required
|
}
|
||||||
/>
|
if (next) {
|
||||||
|
setActiveStep((prevStep) => prevStep + 1);
|
||||||
|
} else {
|
||||||
|
showAlert();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
<TextField
|
const handleBack = () => {
|
||||||
fullWidth
|
setActiveStep((prevStep) => prevStep - 1);
|
||||||
label={t('postalCode')}
|
};
|
||||||
name="zip"
|
|
||||||
value={shippingDetails.zip}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
fullWidth
|
const { name, value } = e.target;
|
||||||
label={t('country')}
|
setShippingDetails((prevDetails) => ({
|
||||||
name="country"
|
...prevDetails,
|
||||||
value={shippingDetails.country}
|
[name]: value,
|
||||||
onChange={handleInputChange}
|
}));
|
||||||
required
|
};
|
||||||
/>
|
const handleClearBasket = () => {
|
||||||
</Grid>
|
clearBasket();
|
||||||
</Box>
|
};
|
||||||
);
|
|
||||||
case 2:
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
{t('payment')}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body1">
|
|
||||||
{t('paymentNotAvailable')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
case 3:
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
{t('orderSummary')}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body1" gutterBottom>
|
|
||||||
{t('thanksForOrder')}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" gutterBottom>
|
|
||||||
{t('yourOrderNumber')}: <strong>{orderNumber}</strong>
|
|
||||||
</Typography>
|
|
||||||
<Divider sx={{my: 2}}/>
|
|
||||||
<Typography variant="h6">{t('shippingDetails')}:</Typography>
|
|
||||||
<Typography variant="body2">
|
|
||||||
{shippingDetails.name} {shippingDetails.surname}<br/>
|
|
||||||
{shippingDetails.address}<br/>
|
|
||||||
{shippingDetails.zip} {shippingDetails.country}<br/>
|
|
||||||
</Typography>
|
|
||||||
<Divider sx={{my: 2}}/>
|
|
||||||
<Typography variant="h6">{t('orderedItems')}:</Typography>
|
|
||||||
{generateBasket(t, basket)}
|
|
||||||
<Divider sx={{my: 2}}/>
|
|
||||||
{generateTotal(t, basket)}
|
|
||||||
<br/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return <div>Unknown step</div>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Hilfsfunktion prüfen, ob alle Pflichtfelder ausgefüllt sind
|
||||||
|
const isShippingDetailsValid = () => {
|
||||||
return (
|
return (
|
||||||
<div className="page-background">
|
shippingDetails.name.trim() !== "" &&
|
||||||
<Container
|
shippingDetails.surname.trim() !== "" &&
|
||||||
maxWidth="md"
|
shippingDetails.address.trim() !== "" &&
|
||||||
sx={{
|
shippingDetails.zip.trim() !== "" &&
|
||||||
py: 4,
|
shippingDetails.country.trim() !== ""
|
||||||
maxHeight: '90vh',
|
|
||||||
overflowY: 'auto'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Paper elevation={3} sx={{p: 4}}>
|
|
||||||
<Typography variant="h4" align="center" gutterBottom>
|
|
||||||
{t('completeYourOrder')}
|
|
||||||
</Typography>
|
|
||||||
<Stepper activeStep={activeStep} alternativeLabel>
|
|
||||||
{steps.map((label) => (
|
|
||||||
<Step key={label}>
|
|
||||||
<StepLabel>{label}</StepLabel>
|
|
||||||
</Step>
|
|
||||||
))}
|
|
||||||
</Stepper>
|
|
||||||
<Box sx={{mt: 4}}>{renderStepContent(activeStep)}</Box>
|
|
||||||
<Box sx={{display: 'flex', justifyContent: 'space-between', mt: 4}}>
|
|
||||||
<Button
|
|
||||||
disabled={activeStep === 0}
|
|
||||||
onClick={handleBack}
|
|
||||||
variant="outlined"
|
|
||||||
>
|
|
||||||
{t('back')}
|
|
||||||
</Button>
|
|
||||||
{activeStep === steps.length - 1 ? (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={() => {
|
|
||||||
handleClearBasket();
|
|
||||||
navigator('/');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('finish')}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={handleNext}
|
|
||||||
disabled={
|
|
||||||
basket.length === 0 ||
|
|
||||||
(activeStep === 1 && !isShippingDetailsValid())
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{activeStep === steps.length - 2 ? t('placeOrder') : t('next')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const renderStepContent = (step: number) => {
|
||||||
|
switch (step) {
|
||||||
|
case 0:
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
{t("reviewCart")}
|
||||||
|
</Typography>
|
||||||
|
{generateBasket(t, basket)}
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
onClick={handleClearBasket}
|
||||||
|
disabled={basket.length === 0}
|
||||||
|
>
|
||||||
|
{t("clearCart")}
|
||||||
|
</Button>
|
||||||
|
{generateTotal(t, basket)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
{t("shippingDetails")}
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={t("firstName")}
|
||||||
|
name="name"
|
||||||
|
value={shippingDetails.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={t("lastName")}
|
||||||
|
name="surname"
|
||||||
|
value={shippingDetails.surname}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={t("address")}
|
||||||
|
name="address"
|
||||||
|
value={shippingDetails.address}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={t("postalCode")}
|
||||||
|
name="zip"
|
||||||
|
value={shippingDetails.zip}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={t("country")}
|
||||||
|
name="country"
|
||||||
|
value={shippingDetails.country}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
case 2:
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
{t("payment")}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">{t("paymentNotAvailable")}</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
case 3:
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
{t("orderSummary")}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" gutterBottom>
|
||||||
|
{t("thanksForOrder")}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
{t("yourOrderNumber")}: <strong>{orderNumber}</strong>
|
||||||
|
</Typography>
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
<Typography variant="h6">{t("shippingDetails")}:</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{shippingDetails.name} {shippingDetails.surname}
|
||||||
|
<br />
|
||||||
|
{shippingDetails.address}
|
||||||
|
<br />
|
||||||
|
{shippingDetails.zip} {shippingDetails.country}
|
||||||
|
<br />
|
||||||
|
</Typography>
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
<Typography variant="h6">{t("orderedItems")}:</Typography>
|
||||||
|
{generateBasket(t, basket)}
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
{generateTotal(t, basket)}
|
||||||
|
<br />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <div>Unknown step</div>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-background">
|
||||||
|
<Container
|
||||||
|
maxWidth="md"
|
||||||
|
sx={{
|
||||||
|
py: 4,
|
||||||
|
maxHeight: "90vh",
|
||||||
|
overflowY: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper elevation={3} sx={{ p: 4 }}>
|
||||||
|
<Typography variant="h4" align="center" gutterBottom>
|
||||||
|
{t("completeYourOrder")}
|
||||||
|
</Typography>
|
||||||
|
<Stepper activeStep={activeStep} alternativeLabel>
|
||||||
|
{steps.map((label) => (
|
||||||
|
<Step key={label}>
|
||||||
|
<StepLabel>{label}</StepLabel>
|
||||||
|
</Step>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
<Box sx={{ mt: 4 }}>{renderStepContent(activeStep)}</Box>
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "space-between", mt: 4 }}>
|
||||||
|
<Button
|
||||||
|
disabled={activeStep === 0}
|
||||||
|
onClick={handleBack}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
{t("back")}
|
||||||
|
</Button>
|
||||||
|
{activeStep === steps.length - 1 ? (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
handleClearBasket();
|
||||||
|
navigator("/");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("finish")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={handleNext}
|
||||||
|
disabled={
|
||||||
|
basket.length === 0 ||
|
||||||
|
(activeStep === 1 && !isShippingDetailsValid())
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{activeStep === steps.length - 2 ? t("placeOrder") : t("next")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,92 +1,88 @@
|
|||||||
import {Box, Button, Container, Divider, Typography} from '@mui/material';
|
import { Box, Button, Container, Divider, Typography } from "@mui/material";
|
||||||
import {useLocation, useNavigate} from 'react-router-dom';
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import Item from '../components/Item';
|
import Item from "../components/Item";
|
||||||
import ProductInfo from '../helper/productpage/ProductInfo';
|
import ProductInfo from "../helper/productpage/ProductInfo";
|
||||||
import Ratings from '../helper/productpage/Ratings';
|
import Ratings from "../helper/productpage/Ratings";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {useTheme} from '@mui/material/styles';
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
export default function Product() {
|
export default function Product() {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const item = location.state?.item as Item;
|
const item = location.state?.item as Item;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const theme = useTheme(); // Zugriff auf das aktive Theme
|
const theme = useTheme(); // Zugriff auf das aktive Theme
|
||||||
|
|
||||||
const handleGoHome = () => {
|
const handleGoHome = () => {
|
||||||
navigate("/");
|
navigate("/");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wenn kein Produkt vorhanden ist
|
|
||||||
if (!item) {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
height: '100vh',
|
|
||||||
textAlign: 'center',
|
|
||||||
backgroundColor: theme.palette.background.default,
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
gap: '2rem',
|
|
||||||
overflow: 'auto',
|
|
||||||
px: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h1">
|
|
||||||
{t('productNotFound')}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="h5">
|
|
||||||
{t('productDoesNotExist')}
|
|
||||||
</Typography>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
size="large"
|
|
||||||
onClick={handleGoHome}
|
|
||||||
>
|
|
||||||
{t('backToHome')}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Wenn kein Produkt vorhanden ist
|
||||||
|
if (!item) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.background.default,
|
display: "flex",
|
||||||
color: theme.palette.text.primary,
|
flexDirection: "column",
|
||||||
height: '100vh',
|
alignItems: "center",
|
||||||
overflow: 'auto',
|
justifyContent: "center",
|
||||||
pt: 4,
|
height: "100vh",
|
||||||
pb: 10,
|
textAlign: "center",
|
||||||
}}
|
backgroundColor: theme.palette.background.default,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
gap: "2rem",
|
||||||
|
overflow: "auto",
|
||||||
|
px: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h1">{t("productNotFound")}</Typography>
|
||||||
|
<Typography variant="h5">{t("productDoesNotExist")}</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
onClick={handleGoHome}
|
||||||
>
|
>
|
||||||
<Container maxWidth="lg">
|
{t("backToHome")}
|
||||||
<ProductInfo item={item}/>
|
</Button>
|
||||||
|
</Box>
|
||||||
<Box sx={{my: 4}}>
|
|
||||||
<Divider sx={{backgroundColor: theme.palette.text.secondary}}/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{color: theme.palette.text.secondary, mb: 1}}
|
|
||||||
>
|
|
||||||
{t('articleNumber')}: {item.uuid}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
sx={{color: theme.palette.text.primary, mb: 3}}
|
|
||||||
>
|
|
||||||
{item.description}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Ratings itemId={item.uuid}/>
|
|
||||||
</Container>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
height: "100vh",
|
||||||
|
overflow: "auto",
|
||||||
|
pt: 4,
|
||||||
|
pb: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container maxWidth="lg">
|
||||||
|
<ProductInfo item={item} />
|
||||||
|
|
||||||
|
<Box sx={{ my: 4 }}>
|
||||||
|
<Divider sx={{ backgroundColor: theme.palette.text.secondary }} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: theme.palette.text.secondary, mb: 1 }}
|
||||||
|
>
|
||||||
|
{t("articleNumber")}: {item.uuid}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{ color: theme.palette.text.primary, mb: 3 }}
|
||||||
|
>
|
||||||
|
{item.description}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Ratings itemId={item.uuid} />
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,170 +1,170 @@
|
|||||||
.no-page-container {
|
.no-page-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
gap: 2%;
|
gap: 2%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-page-title {
|
.no-page-title {
|
||||||
font-size: 8rem;
|
font-size: 8rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-page-subtitle {
|
.no-page-subtitle {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-page-description {
|
.no-page-description {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-page-button {
|
.no-page-button {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
padding: 12px 24px;
|
padding: 12px 24px;
|
||||||
background-color: #0fd13f;
|
background-color: #0fd13f;
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-page-button:hover {
|
.no-page-button:hover {
|
||||||
background-color: #0cc634;
|
background-color: #0cc634;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardgrid {
|
.cardgrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
padding: 16px 16px 64px 16px;
|
padding: 16px 16px 64px 16px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-background {
|
.page-background {
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
min-height: var(--page-height);
|
min-height: var(--page-height);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toppad {
|
.toppad {
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-background-center {
|
.page-background-center {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-background.page-background--no-space-between {
|
.page-background.page-background--no-space-between {
|
||||||
justify-content: flex-start !important;
|
justify-content: flex-start !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-table {
|
.page-table {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
height: var(--page-height);
|
height: var(--page-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.impressum-container {
|
.impressum-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
height: var(--page-height);
|
height: var(--page-height);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.impressum-title {
|
.impressum-title {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.impressum-content {
|
.impressum-content {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-divider {
|
.contact-divider {
|
||||||
background-color: var(--text-color);
|
background-color: var(--text-color);
|
||||||
height: 2px;
|
height: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-divider-box {
|
.contact-divider-box {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 25px 0;
|
margin: 25px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-page-background {
|
.product-page-background {
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 600px;
|
min-height: 600px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-page-background {
|
.home-page-background {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-self: start;
|
place-self: start;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin-top: 2vh;
|
margin-top: 2vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-filter {
|
.sidebar-filter {
|
||||||
margin: 30px
|
margin: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-results {
|
.no-results {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rightBound {
|
.rightBound {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.red {
|
.red {
|
||||||
color: #F00;
|
color: #f00;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,185 +1,204 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import React, {createContext, ReactNode, useContext, useEffect, useState} from 'react';
|
import React, {
|
||||||
import {createTheme, ThemeProvider} from '@mui/material/styles';
|
createContext,
|
||||||
import {CssBaseline, GlobalStyles} from '@mui/material';
|
ReactNode,
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { createTheme, ThemeProvider } from "@mui/material/styles";
|
||||||
|
import { CssBaseline, GlobalStyles } from "@mui/material";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
|
||||||
type ThemeMode = 'light' | 'dark';
|
type ThemeMode = "light" | "dark";
|
||||||
|
|
||||||
interface ThemeContextType {
|
interface ThemeContextType {
|
||||||
mode: ThemeMode;
|
mode: ThemeMode;
|
||||||
toggleMode: () => void;
|
toggleMode: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemeContext = createContext<ThemeContextType>({
|
const ThemeContext = createContext<ThemeContextType>({
|
||||||
mode: 'light',
|
mode: "light",
|
||||||
toggleMode: () => {
|
toggleMode: () => {},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useThemeMode = () => useContext(ThemeContext);
|
export const useThemeMode = () => useContext(ThemeContext);
|
||||||
|
|
||||||
interface CustomThemeProviderProps {
|
interface CustomThemeProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({children}) => {
|
export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({
|
||||||
// SSR-sichere System-Präferenz-Erkennung
|
children,
|
||||||
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', {noSsr: true});
|
}) => {
|
||||||
|
// SSR-sichere System-Präferenz-Erkennung
|
||||||
|
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)", {
|
||||||
|
noSsr: true,
|
||||||
|
});
|
||||||
|
|
||||||
// SSR-sichere Initialisierung
|
// SSR-sichere Initialisierung
|
||||||
const [mode, setMode] = useState<ThemeMode>('light');
|
const [mode, setMode] = useState<ThemeMode>("light");
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
// Nach dem ersten Render ausführen (SSR-sicher)
|
// Nach dem ersten Render ausführen (SSR-sicher)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
const savedMode = localStorage.getItem('themeMode') as ThemeMode;
|
const savedMode = localStorage.getItem("themeMode") as ThemeMode;
|
||||||
if (savedMode === 'light' || savedMode === 'dark') {
|
if (savedMode === "light" || savedMode === "dark") {
|
||||||
setMode(savedMode);
|
setMode(savedMode);
|
||||||
} else {
|
} else {
|
||||||
setMode(prefersDarkMode ? 'dark' : 'light');
|
setMode(prefersDarkMode ? "dark" : "light");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [prefersDarkMode]);
|
|
||||||
|
|
||||||
// Mode in localStorage speichern
|
|
||||||
useEffect(() => {
|
|
||||||
if (mounted && typeof window !== 'undefined') {
|
|
||||||
localStorage.setItem('themeMode', mode);
|
|
||||||
}
|
|
||||||
}, [mode, mounted]);
|
|
||||||
|
|
||||||
// Browser-only DOM-Manipulation
|
|
||||||
useEffect(() => {
|
|
||||||
if (mounted && typeof window !== 'undefined') {
|
|
||||||
const backgroundColor = mode === 'dark' ? '#121212' : '#fafafa';
|
|
||||||
|
|
||||||
// Warten bis DOM geladen ist
|
|
||||||
const updateBackground = () => {
|
|
||||||
document.documentElement.style.setProperty('--background-color', backgroundColor);
|
|
||||||
document.documentElement.style.backgroundColor = backgroundColor;
|
|
||||||
document.body.style.backgroundColor = backgroundColor;
|
|
||||||
|
|
||||||
const root = document.getElementById('root');
|
|
||||||
if (root) {
|
|
||||||
root.style.backgroundColor = backgroundColor;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sofort ausführen
|
|
||||||
updateBackground();
|
|
||||||
|
|
||||||
// Nach einem kurzen Delay nochmal (für hartnäckige Cases)
|
|
||||||
const timeoutId = setTimeout(updateBackground, 100);
|
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
}, [mode, mounted]);
|
|
||||||
|
|
||||||
const toggleMode = () => {
|
|
||||||
setMode(prevMode => prevMode === 'light' ? 'dark' : 'light');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Theme basierend auf Mode erstellen
|
|
||||||
const theme = React.useMemo(() =>
|
|
||||||
createTheme({
|
|
||||||
palette: {
|
|
||||||
mode,
|
|
||||||
primary: {
|
|
||||||
main: '#0fd13f', // Grüne NavBar
|
|
||||||
light: mode === 'dark' ? '#4caf50' : '#42a5f5',
|
|
||||||
dark: mode === 'dark' ? '#388e3c' : '#1565c0',
|
|
||||||
contrastText: '#fff',
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
main: mode === 'dark' ? '#bb86fc' : '#9c27b0',
|
|
||||||
light: mode === 'dark' ? '#d7aefb' : '#ba68c8',
|
|
||||||
dark: mode === 'dark' ? '#985eff' : '#7b1fa2',
|
|
||||||
contrastText: '#fff',
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
default: mode === 'dark' ? '#121212' : '#fafafa',
|
|
||||||
paper: mode === 'dark' ? '#1e1e1e' : '#ffffff',
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
primary: mode === 'dark' ? '#ffffff' : '#000000',
|
|
||||||
secondary: mode === 'dark' ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.6)',
|
|
||||||
},
|
|
||||||
homepage: mode === 'dark' ? '#1e1e1e' : '#f4f4f4',
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
MuiCssBaseline: {
|
|
||||||
styleOverrides: {
|
|
||||||
html: {
|
|
||||||
backgroundColor: `${mode === 'dark' ? '#121212' : '#fafafa'} !important`,
|
|
||||||
transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)',
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
backgroundColor: `${mode === 'dark' ? '#121212' : '#fafafa'} !important`,
|
|
||||||
transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)',
|
|
||||||
margin: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiAppBar: {
|
|
||||||
styleOverrides: {
|
|
||||||
colorPrimary: {
|
|
||||||
backgroundColor: mode === 'dark' ? '#388e3c' : '#0fd13f',
|
|
||||||
color: '#ffffff',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
[mode]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Aggressive GlobalStyles mit CSS-Variablen
|
|
||||||
const globalStyles = mounted ? (
|
|
||||||
<GlobalStyles
|
|
||||||
styles={{
|
|
||||||
':root': {
|
|
||||||
'--background-color': mode === 'dark' ? '#121212' : '#fafafa',
|
|
||||||
'--text-color': mode === 'dark' ? '#ffffff' : '#000000',
|
|
||||||
},
|
|
||||||
'*': {
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
},
|
|
||||||
'html, body, #root': {
|
|
||||||
backgroundColor: `var(--background-color) !important`,
|
|
||||||
color: `var(--text-color) !important`,
|
|
||||||
transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1), color 300ms cubic-bezier(0.4, 0, 0.2, 1)',
|
|
||||||
margin: 0,
|
|
||||||
padding: 0,
|
|
||||||
minHeight: '100vh',
|
|
||||||
},
|
|
||||||
'div, section, main, article': {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
// SSR-Fallback während mounted = false
|
|
||||||
if (!mounted) {
|
|
||||||
return (
|
|
||||||
<ThemeProvider theme={createTheme({palette: {mode: 'light'}})}>
|
|
||||||
<CssBaseline/>
|
|
||||||
{children}
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}, [prefersDarkMode]);
|
||||||
|
|
||||||
|
// Mode in localStorage speichern
|
||||||
|
useEffect(() => {
|
||||||
|
if (mounted && typeof window !== "undefined") {
|
||||||
|
localStorage.setItem("themeMode", mode);
|
||||||
|
}
|
||||||
|
}, [mode, mounted]);
|
||||||
|
|
||||||
|
// Browser-only DOM-Manipulation
|
||||||
|
useEffect(() => {
|
||||||
|
if (mounted && typeof window !== "undefined") {
|
||||||
|
const backgroundColor = mode === "dark" ? "#121212" : "#fafafa";
|
||||||
|
|
||||||
|
// Warten bis DOM geladen ist
|
||||||
|
const updateBackground = () => {
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
"--background-color",
|
||||||
|
backgroundColor,
|
||||||
|
);
|
||||||
|
document.documentElement.style.backgroundColor = backgroundColor;
|
||||||
|
document.body.style.backgroundColor = backgroundColor;
|
||||||
|
|
||||||
|
const root = document.getElementById("root");
|
||||||
|
if (root) {
|
||||||
|
root.style.backgroundColor = backgroundColor;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sofort ausführen
|
||||||
|
updateBackground();
|
||||||
|
|
||||||
|
// Nach einem kurzen Delay nochmal (für hartnäckige Cases)
|
||||||
|
const timeoutId = setTimeout(updateBackground, 100);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}, [mode, mounted]);
|
||||||
|
|
||||||
|
const toggleMode = () => {
|
||||||
|
setMode((prevMode) => (prevMode === "light" ? "dark" : "light"));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Theme basierend auf Mode erstellen
|
||||||
|
const theme = React.useMemo(
|
||||||
|
() =>
|
||||||
|
createTheme({
|
||||||
|
palette: {
|
||||||
|
mode,
|
||||||
|
primary: {
|
||||||
|
main: "#0fd13f", // Grüne NavBar
|
||||||
|
light: mode === "dark" ? "#4caf50" : "#42a5f5",
|
||||||
|
dark: mode === "dark" ? "#388e3c" : "#1565c0",
|
||||||
|
contrastText: "#fff",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: mode === "dark" ? "#bb86fc" : "#9c27b0",
|
||||||
|
light: mode === "dark" ? "#d7aefb" : "#ba68c8",
|
||||||
|
dark: mode === "dark" ? "#985eff" : "#7b1fa2",
|
||||||
|
contrastText: "#fff",
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: mode === "dark" ? "#121212" : "#fafafa",
|
||||||
|
paper: mode === "dark" ? "#1e1e1e" : "#ffffff",
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: mode === "dark" ? "#ffffff" : "#000000",
|
||||||
|
secondary:
|
||||||
|
mode === "dark"
|
||||||
|
? "rgba(255, 255, 255, 0.7)"
|
||||||
|
: "rgba(0, 0, 0, 0.6)",
|
||||||
|
},
|
||||||
|
homepage: mode === "dark" ? "#1e1e1e" : "#f4f4f4",
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MuiCssBaseline: {
|
||||||
|
styleOverrides: {
|
||||||
|
html: {
|
||||||
|
backgroundColor: `${mode === "dark" ? "#121212" : "#fafafa"} !important`,
|
||||||
|
transition:
|
||||||
|
"background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
backgroundColor: `${mode === "dark" ? "#121212" : "#fafafa"} !important`,
|
||||||
|
transition:
|
||||||
|
"background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiAppBar: {
|
||||||
|
styleOverrides: {
|
||||||
|
colorPrimary: {
|
||||||
|
backgroundColor: mode === "dark" ? "#388e3c" : "#0fd13f",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[mode],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Aggressive GlobalStyles mit CSS-Variablen
|
||||||
|
const globalStyles = mounted ? (
|
||||||
|
<GlobalStyles
|
||||||
|
styles={{
|
||||||
|
":root": {
|
||||||
|
"--background-color": mode === "dark" ? "#121212" : "#fafafa",
|
||||||
|
"--text-color": mode === "dark" ? "#ffffff" : "#000000",
|
||||||
|
},
|
||||||
|
"*": {
|
||||||
|
boxSizing: "border-box",
|
||||||
|
},
|
||||||
|
"html, body, #root": {
|
||||||
|
backgroundColor: `var(--background-color) !important`,
|
||||||
|
color: `var(--text-color) !important`,
|
||||||
|
transition:
|
||||||
|
"background-color 300ms cubic-bezier(0.4, 0, 0.2, 1), color 300ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
minHeight: "100vh",
|
||||||
|
},
|
||||||
|
"div, section, main, article": {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
// SSR-Fallback während mounted = false
|
||||||
|
if (!mounted) {
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{mode, toggleMode}}>
|
<ThemeProvider theme={createTheme({ palette: { mode: "light" } })}>
|
||||||
<ThemeProvider theme={theme}>
|
<CssBaseline />
|
||||||
<CssBaseline enableColorScheme/>
|
{children}
|
||||||
{globalStyles}
|
</ThemeProvider>
|
||||||
{children}
|
|
||||||
</ThemeProvider>
|
|
||||||
</ThemeContext.Provider>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ mode, toggleMode }}>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline enableColorScheme />
|
||||||
|
{globalStyles}
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
// theme/ThemeToggle.tsx
|
// theme/ThemeToggle.tsx
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import {IconButton, Tooltip} from '@mui/material';
|
import { IconButton, Tooltip } from "@mui/material";
|
||||||
import {Brightness4, Brightness7} from '@mui/icons-material';
|
import { Brightness4, Brightness7 } from "@mui/icons-material";
|
||||||
import {useThemeMode} from './ThemeContext';
|
import { useThemeMode } from "./ThemeContext";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const ThemeToggle: React.FC = () => {
|
const ThemeToggle: React.FC = () => {
|
||||||
const {mode, toggleMode} = useThemeMode();
|
const { mode, toggleMode } = useThemeMode();
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={mode === 'dark' ? t('lightMode') : t('darkMode')}>
|
<Tooltip title={mode === "dark" ? t("lightMode") : t("darkMode")}>
|
||||||
|
<IconButton
|
||||||
<IconButton
|
onClick={toggleMode}
|
||||||
onClick={toggleMode}
|
color="inherit"
|
||||||
color="inherit"
|
sx={{
|
||||||
sx={{
|
transition: "transform 0.2s",
|
||||||
transition: 'transform 0.2s',
|
"&:hover": {
|
||||||
'&:hover': {
|
transform: "scale(1.1)",
|
||||||
transform: 'scale(1.1)',
|
},
|
||||||
},
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{mode === "dark" ? <Brightness7 /> : <Brightness4 />}
|
||||||
{mode === 'dark' ? <Brightness7/> : <Brightness4/>}
|
</IconButton>
|
||||||
</IconButton>
|
</Tooltip>
|
||||||
</Tooltip>
|
);
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ThemeToggle;
|
export default ThemeToggle;
|
||||||
|
|||||||
20
01-frontend/src/theme/theme-augmentation.d.ts
vendored
20
01-frontend/src/theme/theme-augmentation.d.ts
vendored
@@ -1,13 +1,13 @@
|
|||||||
import '@mui/material/styles';
|
import "@mui/material/styles";
|
||||||
|
|
||||||
declare module '@mui/material/styles' {
|
declare module "@mui/material/styles" {
|
||||||
interface Palette {
|
interface Palette {
|
||||||
tertiary: Palette['primary'];
|
tertiary: Palette["primary"];
|
||||||
homepage: string;
|
homepage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PaletteOptions {
|
interface PaletteOptions {
|
||||||
tertiary?: PaletteOptions['primary'];
|
tertiary?: PaletteOptions["primary"];
|
||||||
homepage?: string;
|
homepage?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,206 +1,206 @@
|
|||||||
import {createTheme} from '@mui/material/styles';
|
import { createTheme } from "@mui/material/styles";
|
||||||
import './theme-augmentation.d.ts'; // Falls vorhanden
|
import "./theme-augmentation.d.ts"; // Falls vorhanden
|
||||||
|
|
||||||
export const darkmode = createTheme({
|
export const darkmode = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
primary: {
|
primary: {
|
||||||
main: '#0fd13f', // Grüne Standardfarbe
|
main: "#0fd13f", // Grüne Standardfarbe
|
||||||
light: '#42a5f5',
|
light: "#42a5f5",
|
||||||
dark: '#15650',
|
dark: "#15650",
|
||||||
contrastText: '#fff',
|
contrastText: "#fff",
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
main: '#9c27b0',
|
|
||||||
light: '#ba68c8',
|
|
||||||
dark: '#7b1fa2',
|
|
||||||
contrastText: '#fff',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
main: '#f44336',
|
|
||||||
light: '#e57373',
|
|
||||||
dark: '#d32f2f',
|
|
||||||
contrastText: '#fff',
|
|
||||||
},
|
|
||||||
warning: {
|
|
||||||
main: '#ed6c02',
|
|
||||||
light: '#ff9800',
|
|
||||||
dark: '#e65100',
|
|
||||||
contrastText: '#fff',
|
|
||||||
},
|
|
||||||
info: {
|
|
||||||
main: '#0288d1',
|
|
||||||
light: '#03a9f4',
|
|
||||||
dark: '#01579b',
|
|
||||||
contrastText: '#fff',
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
main: '#2e7d32',
|
|
||||||
light: '#4caf50',
|
|
||||||
dark: '#1b5e20',
|
|
||||||
contrastText: '#fff',
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
default: '#fafafa',
|
|
||||||
paper: '#ffffff',
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
primary: '#000000',
|
|
||||||
secondary: 'rgba(0, 0, 0, 0.6)',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
// Sanfte Übergänge
|
secondary: {
|
||||||
transitions: {
|
main: "#9c27b0",
|
||||||
duration: {
|
light: "#ba68c8",
|
||||||
shortest: 150,
|
dark: "#7b1fa2",
|
||||||
shorter: 200,
|
contrastText: "#fff",
|
||||||
short: 250,
|
|
||||||
standard: 300,
|
|
||||||
complex: 375,
|
|
||||||
enteringScreen: 225,
|
|
||||||
leavingScreen: 195,
|
|
||||||
},
|
|
||||||
easing: {
|
|
||||||
easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
||||||
easeOut: 'cubic-bezier(0.0, 0, 0.2, 1)',
|
|
||||||
easeIn: 'cubic-bezier(0.4, 0, 1, 1)',
|
|
||||||
sharp: 'cubic-bezier(0.4, 0, 0.6, 1)',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
// Verbesserte Komponenten-Overrides
|
error: {
|
||||||
components: {
|
main: "#f44336",
|
||||||
MuiCssBaseline: {
|
light: "#e57373",
|
||||||
styleOverrides: {
|
dark: "#d32f2f",
|
||||||
body: {
|
contrastText: "#fff",
|
||||||
backgroundColor: '#fafafa',
|
|
||||||
transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)',
|
|
||||||
},
|
|
||||||
html: {
|
|
||||||
backgroundColor: '#fafafa',
|
|
||||||
transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiAppBar: {
|
|
||||||
styleOverrides: {
|
|
||||||
colorPrimary: {
|
|
||||||
backgroundColor: '#0fd13f', // Grüne NavBar
|
|
||||||
color: '#ffffff',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiButton: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
borderRadius: 8,
|
|
||||||
textTransform: 'none', // Entfernt automatische Großschreibung
|
|
||||||
fontWeight: 600,
|
|
||||||
},
|
|
||||||
contained: {
|
|
||||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
|
||||||
'&:hover': {
|
|
||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiCard: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
borderRadius: 12,
|
|
||||||
boxShadow: '0 2px 12px rgba(0,0,0,0.08)',
|
|
||||||
transition: 'box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1)',
|
|
||||||
'&:hover': {
|
|
||||||
boxShadow: '0 4px 20px rgba(0,0,0,0.12)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiTextField: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
'& .MuiOutlinedInput-root': {
|
|
||||||
borderRadius: 8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiChip: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
borderRadius: 16,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiPaper: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
borderRadius: 8,
|
|
||||||
},
|
|
||||||
elevation1: {
|
|
||||||
boxShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)',
|
|
||||||
},
|
|
||||||
elevation2: {
|
|
||||||
boxShadow: '0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
// Benutzerdefinierte Typografie
|
warning: {
|
||||||
typography: {
|
main: "#ed6c02",
|
||||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
light: "#ff9800",
|
||||||
h1: {
|
dark: "#e65100",
|
||||||
fontWeight: 700,
|
contrastText: "#fff",
|
||||||
fontSize: '2.5rem',
|
|
||||||
lineHeight: 1.2,
|
|
||||||
},
|
|
||||||
h2: {
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: '2rem',
|
|
||||||
lineHeight: 1.3,
|
|
||||||
},
|
|
||||||
h3: {
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: '1.75rem',
|
|
||||||
lineHeight: 1.3,
|
|
||||||
},
|
|
||||||
h4: {
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: '1.5rem',
|
|
||||||
lineHeight: 1.4,
|
|
||||||
},
|
|
||||||
h5: {
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: '1.25rem',
|
|
||||||
lineHeight: 1.4,
|
|
||||||
},
|
|
||||||
h6: {
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: '1rem',
|
|
||||||
lineHeight: 1.4,
|
|
||||||
},
|
|
||||||
body1: {
|
|
||||||
fontSize: '1rem',
|
|
||||||
lineHeight: 1.5,
|
|
||||||
},
|
|
||||||
body2: {
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
lineHeight: 1.4,
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
fontWeight: 600,
|
|
||||||
textTransform: 'none',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
// Benutzerdefinierte Breakpoints
|
info: {
|
||||||
breakpoints: {
|
main: "#0288d1",
|
||||||
values: {
|
light: "#03a9f4",
|
||||||
xs: 0,
|
dark: "#01579b",
|
||||||
sm: 600,
|
contrastText: "#fff",
|
||||||
md: 960,
|
|
||||||
lg: 1280,
|
|
||||||
xl: 1920,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
// Angepasste Spacing-Funktion
|
success: {
|
||||||
spacing: 8, // 8px als Basis-Einheit
|
main: "#2e7d32",
|
||||||
|
light: "#4caf50",
|
||||||
|
dark: "#1b5e20",
|
||||||
|
contrastText: "#fff",
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: "#fafafa",
|
||||||
|
paper: "#ffffff",
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: "#000000",
|
||||||
|
secondary: "rgba(0, 0, 0, 0.6)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Sanfte Übergänge
|
||||||
|
transitions: {
|
||||||
|
duration: {
|
||||||
|
shortest: 150,
|
||||||
|
shorter: 200,
|
||||||
|
short: 250,
|
||||||
|
standard: 300,
|
||||||
|
complex: 375,
|
||||||
|
enteringScreen: 225,
|
||||||
|
leavingScreen: 195,
|
||||||
|
},
|
||||||
|
easing: {
|
||||||
|
easeInOut: "cubic-bezier(0.4, 0, 0.2, 1)",
|
||||||
|
easeOut: "cubic-bezier(0.0, 0, 0.2, 1)",
|
||||||
|
easeIn: "cubic-bezier(0.4, 0, 1, 1)",
|
||||||
|
sharp: "cubic-bezier(0.4, 0, 0.6, 1)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Verbesserte Komponenten-Overrides
|
||||||
|
components: {
|
||||||
|
MuiCssBaseline: {
|
||||||
|
styleOverrides: {
|
||||||
|
body: {
|
||||||
|
backgroundColor: "#fafafa",
|
||||||
|
transition: "background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||||
|
},
|
||||||
|
html: {
|
||||||
|
backgroundColor: "#fafafa",
|
||||||
|
transition: "background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiAppBar: {
|
||||||
|
styleOverrides: {
|
||||||
|
colorPrimary: {
|
||||||
|
backgroundColor: "#0fd13f", // Grüne NavBar
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiButton: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
borderRadius: 8,
|
||||||
|
textTransform: "none", // Entfernt automatische Großschreibung
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
contained: {
|
||||||
|
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
|
||||||
|
"&:hover": {
|
||||||
|
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiCard: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
borderRadius: 12,
|
||||||
|
boxShadow: "0 2px 12px rgba(0,0,0,0.08)",
|
||||||
|
transition: "box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||||
|
"&:hover": {
|
||||||
|
boxShadow: "0 4px 20px rgba(0,0,0,0.12)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiTextField: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiChip: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
borderRadius: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiPaper: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
elevation1: {
|
||||||
|
boxShadow: "0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)",
|
||||||
|
},
|
||||||
|
elevation2: {
|
||||||
|
boxShadow: "0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Benutzerdefinierte Typografie
|
||||||
|
typography: {
|
||||||
|
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||||
|
h1: {
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: "2.5rem",
|
||||||
|
lineHeight: 1.2,
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "2rem",
|
||||||
|
lineHeight: 1.3,
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "1.75rem",
|
||||||
|
lineHeight: 1.3,
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "1.5rem",
|
||||||
|
lineHeight: 1.4,
|
||||||
|
},
|
||||||
|
h5: {
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "1.25rem",
|
||||||
|
lineHeight: 1.4,
|
||||||
|
},
|
||||||
|
h6: {
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "1rem",
|
||||||
|
lineHeight: 1.4,
|
||||||
|
},
|
||||||
|
body1: {
|
||||||
|
fontSize: "1rem",
|
||||||
|
lineHeight: 1.5,
|
||||||
|
},
|
||||||
|
body2: {
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
lineHeight: 1.4,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
fontWeight: 600,
|
||||||
|
textTransform: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Benutzerdefinierte Breakpoints
|
||||||
|
breakpoints: {
|
||||||
|
values: {
|
||||||
|
xs: 0,
|
||||||
|
sm: 600,
|
||||||
|
md: 960,
|
||||||
|
lg: 1280,
|
||||||
|
xl: 1920,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Angepasste Spacing-Funktion
|
||||||
|
spacing: 8, // 8px als Basis-Einheit
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,58 +1,66 @@
|
|||||||
export function mapValueToColor(minVal: number, maxVal: number, actualVal: number): string {
|
export function mapValueToColor(
|
||||||
const clamped = Math.min(Math.max(actualVal, minVal), maxVal);
|
minVal: number,
|
||||||
|
maxVal: number,
|
||||||
|
actualVal: number,
|
||||||
|
): string {
|
||||||
|
const clamped = Math.min(Math.max(actualVal, minVal), maxVal);
|
||||||
|
|
||||||
// Calculate interpolation ratio (0-1)
|
// Calculate interpolation ratio (0-1)
|
||||||
const ratio = maxVal !== minVal
|
const ratio = maxVal !== minVal ? (clamped - minVal) / (maxVal - minVal) : 0;
|
||||||
? (clamped - minVal) / (maxVal - minVal)
|
return hsvDegToHex(120 * ratio); //120° is green, 0° is red
|
||||||
: 0;
|
|
||||||
return hsvDegToHex(120 * ratio);//120° is green, 0° is red
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getColorFromPercent(percent: string): string {
|
export function getColorFromPercent(percent: string): string {
|
||||||
let perc = Number(percent) / 100
|
let perc = Number(percent) / 100;
|
||||||
if (perc > 1) {
|
if (perc > 1) {
|
||||||
perc = 2.5;
|
perc = 2.5;
|
||||||
}
|
}
|
||||||
return hsvDegToHex(120 * perc);
|
return hsvDegToHex(120 * perc);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hsvDegToHex(h: number): string {
|
export function hsvDegToHex(h: number): string {
|
||||||
h = Math.max(0, Math.min(360, h));
|
h = Math.max(0, Math.min(360, h));
|
||||||
|
|
||||||
const c = 1;
|
const c = 1;
|
||||||
const x = (1 - Math.abs(((h / 60) % 2) - 1));
|
const x = 1 - Math.abs(((h / 60) % 2) - 1);
|
||||||
|
|
||||||
let r, g, b;
|
let r, g, b;
|
||||||
if (h < 60) {
|
if (h < 60) {
|
||||||
r = c;
|
r = c;
|
||||||
g = x;
|
g = x;
|
||||||
b = 0;
|
b = 0;
|
||||||
} else if (h < 120) {
|
} else if (h < 120) {
|
||||||
r = x;
|
r = x;
|
||||||
g = c;
|
g = c;
|
||||||
b = 0;
|
b = 0;
|
||||||
} else if (h < 180) {
|
} else if (h < 180) {
|
||||||
r = 0;
|
r = 0;
|
||||||
g = c;
|
g = c;
|
||||||
b = x;
|
b = x;
|
||||||
} else if (h < 240) {
|
} else if (h < 240) {
|
||||||
r = 0;
|
r = 0;
|
||||||
g = x;
|
g = x;
|
||||||
b = c;
|
b = c;
|
||||||
} else if (h < 300) {
|
} else if (h < 300) {
|
||||||
r = x;
|
r = x;
|
||||||
g = 0;
|
g = 0;
|
||||||
b = c;
|
b = c;
|
||||||
} else {
|
} else {
|
||||||
r = c;
|
r = c;
|
||||||
g = 0;
|
g = 0;
|
||||||
b = x;
|
b = x;
|
||||||
}
|
}
|
||||||
|
|
||||||
// scale to 0-255
|
// scale to 0-255
|
||||||
const rHex = Math.round(r * 255).toString(16).padStart(2, '0');
|
const rHex = Math.round(r * 255)
|
||||||
const gHex = Math.round(g * 255).toString(16).padStart(2, '0');
|
.toString(16)
|
||||||
const bHex = Math.round(b * 255).toString(16).padStart(2, '0');
|
.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}`;
|
return `#${rHex}${gHex}${bHex}`;
|
||||||
}
|
}
|
||||||
|
|||||||
13
start.sh
13
start.sh
@@ -1,16 +1,16 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
LANG="C"
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
|
trap - INT EXIT TERM
|
||||||
echo "[DPS] Cleaning up..."
|
echo "[DPS] Cleaning up..."
|
||||||
pkill $backend_pid # pkill because subshells
|
pgrep -P "$backend_pid" | xargs kill -TERM > /dev/null 2>&1
|
||||||
pkill $frontend_pid
|
pgrep -P "$frontend_pid" | xargs kill -TERM > /dev/null 2>&1
|
||||||
echo "[DPS] Cleaned up..."
|
echo "[DPS] Cleaned up..."
|
||||||
exit
|
exit
|
||||||
}
|
}
|
||||||
|
|
||||||
# Trap INT (Ctrl+C)
|
trap cleanup INT EXIT TERM
|
||||||
trap cleanup INT
|
|
||||||
echo "[DPS] trapped"
|
echo "[DPS] trapped"
|
||||||
|
|
||||||
cd ./00-backend
|
cd ./00-backend
|
||||||
@@ -25,4 +25,5 @@ echo "[DPS] Frontend started with PID $frontend_pid"
|
|||||||
|
|
||||||
echo "[DPS] Ctrl+C to stop"
|
echo "[DPS] Ctrl+C to stop"
|
||||||
# Wait for cleanup
|
# Wait for cleanup
|
||||||
wait
|
wait $backend_pid
|
||||||
|
wait $frontend_pid
|
||||||
|
|||||||
Reference in New Issue
Block a user