From e9cef81f89584110776c84ee511e26097eb8323c Mon Sep 17 00:00:00 2001
From: mrfoxygmfr <mrfoxygmfr@sch9.ru>
Date: Mon, 28 Apr 2025 00:45:56 +0300
Subject: feat: implement products controller + pages

---
 .../http/controllers/ProductsController.java       | 132 +++++++++++++++++++++
 src/main/resources/templates/productEdit.html      |  48 ++++++++
 src/main/resources/templates/products.html         |  84 +++++++++++++
 src/main/resources/templates/storage.html          |  82 +++++++++++++
 4 files changed, 346 insertions(+)
 create mode 100644 src/main/java/ru/mrfoxygmfr/warehouse_accounting/http/controllers/ProductsController.java
 create mode 100644 src/main/resources/templates/productEdit.html
 create mode 100644 src/main/resources/templates/products.html
 create mode 100644 src/main/resources/templates/storage.html

(limited to 'src/main')

diff --git a/src/main/java/ru/mrfoxygmfr/warehouse_accounting/http/controllers/ProductsController.java b/src/main/java/ru/mrfoxygmfr/warehouse_accounting/http/controllers/ProductsController.java
new file mode 100644
index 0000000..b17ec47
--- /dev/null
+++ b/src/main/java/ru/mrfoxygmfr/warehouse_accounting/http/controllers/ProductsController.java
@@ -0,0 +1,132 @@
+package ru.mrfoxygmfr.warehouse_accounting.http.controllers;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import ru.mrfoxygmfr.warehouse_accounting.db.dao.*;
+import ru.mrfoxygmfr.warehouse_accounting.db.dao.specs.PartnerSpecs;
+import ru.mrfoxygmfr.warehouse_accounting.db.dao.specs.ProductSlotSpecs;
+import ru.mrfoxygmfr.warehouse_accounting.db.dao.specs.ProductSpecs;
+import ru.mrfoxygmfr.warehouse_accounting.db.models.*;
+
+import java.time.Duration;
+import java.util.List;
+
+@Controller
+public class ProductsController {
+    @Autowired
+    private ProductDAO productDAO;
+    @Autowired
+    private ProductSlotDAO productSlotDAO;
+
+    @GetMapping("products")
+    public String products(@RequestParam(name = "productName", required = false) String name,
+                           @RequestParam(name = "productHeightLess", required = false) Integer heightLess,
+                           @RequestParam(name = "productHeightGreater", required = false) Integer heightGreater,
+                           @RequestParam(name = "productWidthLess", required = false) Integer widthLess,
+                           @RequestParam(name = "productWidthGreater", required = false) Integer widthGreater,
+                           @RequestParam(name = "productDepthLess", required = false) Integer depthLess,
+                           @RequestParam(name = "productDepthGreater", required = false) Integer depthGreater,
+                           Model model) {
+        Specification<Product> spec = Specification.where(null);
+        if (name != null && !name.isEmpty()) {
+            spec = spec.and(ProductSpecs.productNameLike(name));
+            model.addAttribute("productNameFilter", name);
+        }
+        if (heightLess != null) {
+            spec = spec.and(ProductSpecs.productHeightLess(heightLess));
+            model.addAttribute("productHeightLessFilter", heightLess);
+        }
+        if (heightGreater != null) {
+            spec = spec.and(ProductSpecs.productHeightGreater(heightGreater));
+            model.addAttribute("productHeightGreaterFilter", heightGreater);
+        }
+        if (widthLess != null) {
+            spec = spec.and(ProductSpecs.productWidthLess(widthLess));
+            model.addAttribute("productWidthLessFilter", widthLess);
+        }
+        if (widthGreater != null) {
+            spec = spec.and(ProductSpecs.productWidthGreater(widthGreater));
+            model.addAttribute("productWidthGreaterFilter", widthGreater);
+        }
+        if (depthLess != null) {
+            spec = spec.and(ProductSpecs.productDepthLess(depthLess));
+            model.addAttribute("productDepthLessFilter", depthLess);
+        }
+        if (depthGreater != null) {
+            spec = spec.and(ProductSpecs.productDepthGreater(depthGreater));
+            model.addAttribute("productDepthGreaterFilter", depthGreater);
+        }
+
+        List<Product> products = productDAO.findAll(spec);
+        model.addAttribute("products", products);
+        return "products";
+    }
+
+    @GetMapping("product")
+    public String product(@RequestParam(name = "id") Integer id, Model model) {
+        Product product = productDAO.findById(id).orElseThrow();
+        model.addAttribute("product", product);
+        return "productEdit";
+    }
+
+
+    @PostMapping("product")
+    public String product(@RequestParam(name = "productId") Integer id,
+                              @RequestParam(name = "productName") String name,
+                              @RequestParam(name = "productHeight") Integer height,
+                              @RequestParam(name = "productWidth") Integer width,
+                              @RequestParam(name = "productDepth") Integer depth,
+                              @RequestParam(name = "productMaxStorageDuration", required = false) Integer maxStorageDurationInteger) {
+        Product product;
+        Duration maxStorageDuration = null;
+        if (maxStorageDurationInteger != null && maxStorageDurationInteger != 0) {
+            maxStorageDuration = Duration.ofDays(maxStorageDurationInteger);
+        }
+        if (id == -1) {
+            product = new Product(name, ProductType.UNKNOWN, height, width, depth);
+            product.setMaxStorageDuration(maxStorageDuration);
+        } else {
+            product = productDAO.findById(id).orElseThrow();
+            product.setName(name);
+            product.setHeight(height);
+            product.setWidth(width);
+            product.setDepth(depth);
+            product.setMaxStorageDuration(maxStorageDuration);
+        }
+        productDAO.save(product);
+        return "redirect:/products";
+    }
+    
+    @GetMapping("storage")
+    public String productsStorage(@RequestParam(name = "storageName", required = false) String name,
+                                  @RequestParam(name = "storageStatus", required = false) ProductStorageStatus storageStatus,
+                                  Model model) {
+        Specification<ProductSlot> spec = Specification.where(null);
+        if (name != null && !name.isEmpty()) {
+            spec = spec.and(ProductSlotSpecs.productSlotContainsLike(name));
+            model.addAttribute("storageNameFilter", name);
+        }
+        if (storageStatus != null) {
+            spec = spec.and(ProductSlotSpecs.productStorageStatusEqual(storageStatus));
+            model.addAttribute("storageStatusFilter", storageStatus.toString());
+        }
+
+        List<ProductSlot> storage = productSlotDAO.findAll(spec);
+        model.addAttribute("storage", storage);
+        return "storage";
+    }
+
+    @GetMapping("newProduct")
+    public String newProduct(Model model) {
+        Product product = new Product();
+        product.setId(-1);
+        model.addAttribute("product", product);
+        model.addAttribute("newItem", true);
+        return "productEdit";
+    }
+}
diff --git a/src/main/resources/templates/productEdit.html b/src/main/resources/templates/productEdit.html
new file mode 100644
index 0000000..dac8ba6
--- /dev/null
+++ b/src/main/resources/templates/productEdit.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html xmlns:th="http://www.thymeleaf.org" lang="en">
+<div th:replace="~{common :: head}"></div>
+
+<body>
+<div th:replace="~{common :: page-header}"></div>
+
+<div class="indent">
+    <div id="updateToggleSelector">
+        <button id="updateBtn"  class="btn btn-primary" onclick="toggleDisabled()">Изменить</button> <br><br>
+        <a th:href="@{/operations(operationProductName=${product.getName()})}">
+            <button class="btn btn-primary">Операции с товаром</button>
+        </a><br><br>
+        <a th:href="@{/storage(storageName=${product.getName()})}">
+            <button class="btn btn-primary">К хранению товара</button>
+        </a><br><br>
+    </div>
+
+    <form method="post" action="/product">
+        <input disabled hidden id="productId" name="productId" th:value="${product.getId()}">
+
+        <label for="productName">Название:</label>
+        <input disabled type="text" id="productName" name="productName" required th:value="${product.getName()}"><br><br>
+
+        <label for="productHeight">Высота:</label>
+        <input disabled type="text" id="productHeight" name="productHeight" required th:value="${product.getHeight()}"><br><br>
+
+        <label for="productWidth">Ширина:</label>
+        <input disabled type="text" id="productWidth" name="productWidth" required th:value="${product.getWidth()}"><br><br>
+
+        <label for="productDepth">Глубина:</label>
+        <input disabled type="text" id="productDepth" name="productDepth" required th:value="${product.getDepth()}"><br><br>
+
+        <label for="productMaxStorageDuration">Срок хранения (дни):</label>
+        <input disabled type="text" id="productMaxStorageDuration" name="productMaxStorageDuration" th:value="${product.getMaxStorageDuration() == null ? '' : product.getMaxStorageDuration().toDays()}"><br><br>
+
+        <input id="saveBtn" type="submit" value="Сохранить" class="btn btn-primary" hidden>
+
+    </form>
+</div>
+
+
+<div th:replace="~{common :: site-footer}"></div>
+<div th:replace="~{common :: site-script}"></div>
+<div th:replace="~{common :: editFieldsToggle}"></div>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/src/main/resources/templates/products.html b/src/main/resources/templates/products.html
new file mode 100644
index 0000000..4db7f21
--- /dev/null
+++ b/src/main/resources/templates/products.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html xmlns:th="http://www.thymeleaf.org" lang="en">
+<div th:replace="~{common :: head}"></div>
+
+<body>
+<div th:replace="~{common :: page-header}"></div>
+
+<div class="indent">
+  <form method="get" action="/newProduct">
+    <button id="newOperationBtn" type="submit" class="btn btn-primary">Создать новый продукт</button>
+  </form>
+  <br>
+
+  <form method="get" action="/products">
+    <table class="table">
+      <thead class="theme-dark">
+      <tr>
+        <th colspan="6">Фильтры</th>
+      </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <td>Название</td>
+          <td>
+            <input type="text" id="productNameFilter" name="productName" th:value="${productNameFilter}">
+          </td>
+        </tr>
+        <tr>
+          <td>Высота</td>
+          <td>
+            от <input type="text" id="productHeightGreaterFilter" name="productHeightGreater" th:value="${productHeightGreaterFilter}">
+            до <input type="text" id="productHeightLessFilter" name="productHeightLess" th:value="${productHeightLessFilter}">
+          </td>
+        </tr>
+        <tr>
+          <td>Ширина</td>
+          <td>
+            от <input type="text" id="productWidthGreaterFilter" name="productWidthGreater" th:value="${productWidthGreaterFilter}">
+            до <input type="text" id="productWidthLessFilter" name="productWidthLess" th:value="${productWidthLessFilter}">
+          </td>
+        </tr>
+        <tr>
+          <td>Глубина</td>
+          <td>
+            от <input type="text" id="productDepthGreaterFilter" name="productDepthGreater" th:value="${productDepthGreaterFilter}">
+            до <input type="text" id="productDepthLessFilter" name="productDepthLess" th:value="${productDepthLessFilter}">
+          </td>
+        </tr>
+        <tr>
+          <td colspan="6"><input id="saveBtn" type="submit" value="Применить" class="btn btn-primary"></td>
+        </tr>
+      </tbody>
+    </table>
+  </form>
+
+  <table class="table table-bordered table-warning">
+    <thead class="thead-dark">
+    <tr>
+      <th scope="col">Название</th>
+      <th scope="col">Габариты (В*Ш*Г)</th>
+    </tr>
+    </thead>
+    <tbody>
+    <tr th:if="${products.isEmpty()}">
+      <td colspan="6">Данному фильтру не удовлетворяет ни одного продукта.</td>
+    </tr>
+    <tr th:each="product : ${products}">
+      <td>
+        <a th:href="'/product?id=' + ${product.getId()}">
+          <span th:text="${product.getName()}"></span>
+        </a>
+      </td>
+      <td>
+        <span th:text="${product.getHeight()} + ' * ' + ${product.getWidth()} + ' * ' + ${product.getDepth()}"></span>
+      </td>
+    </tr>
+    </tbody>
+  </table>
+</div>
+
+<div th:replace="~{common :: site-footer}"></div>
+<div th:replace="~{common :: site-script}"></div>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/main/resources/templates/storage.html b/src/main/resources/templates/storage.html
new file mode 100644
index 0000000..975af0f
--- /dev/null
+++ b/src/main/resources/templates/storage.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html xmlns:th="http://www.thymeleaf.org" lang="en">
+<div th:replace="~{common :: head}"></div>
+
+<body>
+<div th:replace="~{common :: page-header}"></div>
+
+<div class="indent">
+    <form method="get" action="/storage">
+        <table class="table">
+            <thead class="theme-dark">
+                <tr>
+                    <th colspan="6">Фильтры</th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr>
+                    <td>Название</td>
+                    <td>
+                        <input type="text" id="storageNameFilter" name="storageName" th:value="${storageNameFilter}">
+                    </td>
+                </tr>
+                <tr>
+                    <td>Статус</td>
+                    <td>
+                        <select id="storageStatusFilter" name="storageStatus">
+                            <option value="">Любой</option>
+                            <option th:value="'RESERVED_FOR_SUPPLY'" th:text="RESERVED_FOR_SUPPLY" th:selected="${storageStatusFilter == 'RESERVED_FOR_SUPPLY'}"></option>
+                            <option th:value="'PLACED'" th:text="PLACED" th:selected="${storageStatusFilter == 'PLACED'}"></option>
+                            <option th:value="'RESERVED_FOR_ISSUE'" th:text="RESERVED_FOR_ISSUE" th:selected="${storageStatusFilter == 'RESERVED_FOR_ISSUE'}"></option>
+                            <option th:value="'PROHIBITED'" th:text="PROHIBITED" th:selected="${storageStatusFilter == 'PROHIBITED'}"></option>
+                        </select>
+                    </td>
+                </tr>
+                <tr>
+                    <td colspan="6"><input id="saveBtn" type="submit" value="Применить" class="btn btn-primary"></td>
+                </tr>
+            </tbody>
+        </table>
+    </form>
+
+    <table class="table table-bordered table-warning">
+        <thead class="thead-dark">
+        <tr>
+            <th scope="col">Название</th>
+            <th scope="col">Локация</th>
+            <th scope="col">Количество</th>
+            <th scope="col">Время размещения</th>
+            <th scope="col">Статус хранения</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr th:if="${storage.isEmpty()}">
+            <td colspan="6">Данному фильтру не удовлетворяет ни одного хранящегося продукта.</td>
+        </tr>
+        <tr th:each="productStorage : ${storage}">
+            <td>
+                <a th:href="'/product?id=' + ${productStorage.getProduct().getId()}">
+                    <span th:text="${productStorage.getProduct().getName()}"></span>
+                </a>
+            </td>
+            <td>
+                <span th:text="${productStorage.getSlot().getLocation()}"></span>
+            </td>
+            <td>
+                <span th:text="${productStorage.getAmount()}"></span>
+            </td>
+            <td>
+                <span th:text="${productStorage.getPlacementTime()}"></span>
+            </td>
+            <td>
+                <span th:text="${productStorage.getStatus()}"></span>
+            </td>
+        </tr>
+        </tbody>
+    </table>
+</div>
+
+<div th:replace="~{common :: site-footer}"></div>
+<div th:replace="~{common :: site-script}"></div>
+</body>
+</html>
\ No newline at end of file
-- 
cgit mrf-deployment