Compare commits

...

13 Commits

Author SHA1 Message Date
Алексей Крюков
ab0b1cb0bf done 2024-06-03 17:05:34 +04:00
Алексей Крюков
e59d4c9465 что-то мутится 2024-05-20 00:55:53 +04:00
Алексей Крюков
d47f50ba29 lab_3 2024-05-11 20:08:02 +04:00
Алексей Крюков
0f7b373fa2 75% 2024-04-15 19:20:00 +04:00
Алексей Крюков
b8cedd8ec3 половина работы 2024-04-15 17:20:34 +04:00
Алексей Крюков
cc1a023999 54 2024-04-15 15:25:29 +04:00
Алексей Крюков
c24dace777 чете 2024-04-15 15:14:43 +04:00
Алексей Крюков
8a89da8daf начало конца 2024-04-15 15:12:00 +04:00
Алексей Крюков
325faa8791 сумму починил ) готово 2024-04-14 18:44:51 +04:00
Алексей Крюков
8f95825cb0 Осталось только подправить сумму в строке заказа 2024-04-14 17:58:19 +04:00
Алексей Крюков
80fee84170 что-то есть, но не правильно работает (возможно я слишком много придумал) сделал сумму в строке заказа, сделал добавление строк заказа в заказ, но это не правильно работает, по сути я создаю новою строку в самом заказе 2024-04-09 23:57:40 +04:00
Алексей Крюков
176d2bbbb4 lab_2 done ( new entityes order orderline, refresh entity product) 2024-04-01 13:48:00 +04:00
Алексей Крюков
7995d3d885 lab_1_done 2024-03-18 12:47:29 +04:00
121 changed files with 4926 additions and 0 deletions

37
.gitignore copy Normal file
View File

@ -0,0 +1,37 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

View File

@ -0,0 +1,2 @@
#Mon Mar 04 17:42:53 GMT+04:00 2024
gradle.version=8.5

Binary file not shown.

BIN
.gradle/file-system.probe Normal file

Binary file not shown.

View File

12
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"recommendations": [
// fronted
"AndersEAndersen.html-class-suggestions",
"dbaeumer.vscode-eslint",
// backend
"vscjava.vscode-java-pack",
"vmware.vscode-boot-dev-pack",
"vscjava.vscode-gradle",
"redhat.vscode-xml"
]
}

14
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,14 @@
{
"configurations": [
{
"type": "java",
"name": "Spring Boot-DemoApplication<IP_PIbd-21_Kryukov_Backend>",
"request": "launch",
"cwd": "${workspaceFolder}",
"mainClass": "com.example.demo.DemoApplication",
"projectName": "IP_PIbd-21_Kryukov_Backend",
"args": "--populate",
"envFile": "${workspaceFolder}/.env"
}
]
}

19
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,19 @@
{
"editor.tabSize": 4,
"editor.insertSpaces": true,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.formatOnType": false,
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "automatic",
"[java]": {
"editor.pasteAs.enabled": false
},
"gradle.nestedProjects": true,
"java.saveActions.organizeImports": true,
"java.dependency.packagePresentation": "hierarchical",
"spring-boot.ls.problem.boot2.JAVA_CONSTRUCTOR_PARAMETER_INJECTION": "WARNING",
"spring.initializr.defaultLanguage": "Java",
"java.format.settings.url": ".vscode/eclipse-formatter.xml",
"java.project.explorer.showNonJavaResources": true,
}

51
build.gradle Normal file
View File

@ -0,0 +1,51 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.4'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
defaultTasks 'bootRun'
jar {
enabled = false
}
bootJar {
archiveFileName = String.format('%s-%s.jar', rootProject.name, version)
}
assert System.properties['java.specification.version'] == '17' || '21'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
implementation 'org.modelmapper:modelmapper:3.2.0'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.h2database:h2:2.2.224'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.3.0'
runtimeOnly 'org.webjars.npm:bootstrap:5.3.3'
runtimeOnly 'org.webjars.npm:bootstrap-icons:1.11.3'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}

View File

@ -0,0 +1,20 @@
# Server
spring.main.banner-mode=off
server.port=8080
# Logger settings
# Available levels are: TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF
logging.level.com.example.demo=DEBUG
# JPA Settings
spring.datasource.url=jdbc:h2:file:./data
spring.datasource.username=sa
spring.datasource.password=password
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=create
spring.jpa.open-in-view=false
# spring.jpa.show-sql=true
# spring.jpa.properties.hibernate.format_sql=true
# H2 console
spring.h2.console.enabled=true

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-emoji-sunglasses-fill" viewBox="0 0 16 16">
<path
d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16M2.31 5.243A1 1 0 0 1 3.28 4H6a1 1 0 0 1 1 1v.116A4.2 4.2 0 0 1 8 5c.35 0 .69.04 1 .116V5a1 1 0 0 1 1-1h2.72a1 1 0 0 1 .97 1.243l-.311 1.242A2 2 0 0 1 11.439 8H11a2 2 0 0 1-1.994-1.839A3 3 0 0 0 8 6c-.393 0-.74.064-1.006.161A2 2 0 0 1 5 8h-.438a2 2 0 0 1-1.94-1.515zM4.969 9.75A3.5 3.5 0 0 0 8 11.5a3.5 3.5 0 0 0 3.032-1.75.5.5 0 1 1 .866.5A4.5 4.5 0 0 1 8 12.5a4.5 4.5 0 0 1-3.898-2.25.5.5 0 0 1 .866-.5z" />
</svg>

After

Width:  |  Height:  |  Size: 615 B

View File

@ -0,0 +1,67 @@
html,
body {
height: 100%;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.25em;
}
h3 {
font-size: 1.1em;
}
td form {
margin: 0;
padding: 0;
margin-top: -.25em;
}
.button-fixed-width {
width: 150px;
}
.button-link {
padding: 0;
}
.invalid-feedback {
display: block;
}
.w-10 {
width: 10% !important;
}
.my-navbar {
background-color: #3c3c3c !important;
}
.my-navbar .link a:hover {
text-decoration: underline;
}
.my-navbar .logo {
width: 26px;
height: 26px;
}
.my-footer {
background-color: #2c2c2c;
height: 32px;
color: rgba(255, 255, 255, 0.5);
}
.cart-image {
width: 3.1rem;
padding: 0.25rem;
border-radius: 0.5rem;
}
.cart-item {
height: auto;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Ошибка</title>
</head>
<body>
<main layout:fragment="content">
<ul class="list-group mb-2">
<th:block th:if="${#strings.isEmpty(message)}">
<li class="list-group-item">
Неизвестная ошибка
</li>
</th:block>
<th:block th:if="${not #strings.isEmpty(message)}">
<li class="list-group-item">
<strong>Ошибка:</strong> [[${message}]]
</li>
</th:block>
<th:block th:if="${not #strings.isEmpty(url)}">
<li class="list-group-item">
<strong>Адрес:</strong> [[${url}]]
</li>
<li class="list-group-item">
<strong>Класс исключения:</strong> [[${exception}]]
</li>
<li class="list-group-item">
[[${method}]] ([[${file}]]:[[${line}]])
</li>
</th:block>
</ul>
<a class="btn btn-primary button-fixed-width" href="/">На главную</a>
</main>
</body>
</html>

View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Корзина</title>
</head>
<body>
<main layout:fragment="content" class="w-50 mx-auto py-3">
<strong class="flex-fill">Корзина</strong>
<div class="d-flex flex-column align-items-center">
<div class="card col-12 col-md-8 col-lg-6 w-75 align-items-center" th:each="cartItem : ${cart}">
<div class="card-body col-12 p-2 d-flex flex-row align-items-center justify-content-center">
<div class="col-5"><img src="/kola.jpg" class="w-75"></div>
<div class="col-3">
Название: [[${cartItem.productName}]]
</div>
<div class="col-4">
Цена: [[${cartItem.Price}]]
</div>
</div>
</div>
<div class=" mb-2 col-12 col-md-8 col-lg-6 d-flex justify-content-end">
</div>
<div class="mb-2 col-12 col-md-8 col-lg-6 d-flex justify-content-center"
th:if="${not #lists.isEmpty(cart)}">
<form action="#" th:action="@{/cart/save}" method="post">
<button type="submit" class="btn btn-primary" onclick="return confirm('Вы уверены?')">
Оформить заказ
</button>
</form>
</div>
<div class="mb-2 col-12 col-md-8 col-lg-6 d-flex align-items-center">
<form action="#" th:action="@{/cart/clear}" method="post">
<button type="submit" class="btn btn-danger button-fixed-width"
onclick="return confirm('Вы уверены?')">
<i class="bi bi-x-lg"></i> Очистить
</button>
</form>
</div>
</div>
</main>
</body>
</html>
</html>

View File

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Каталог</title>
</head>
<body>
<main layout:fragment="content" class="w-50 mx-auto main">
<div class="tab-content mt-2">
<h2 class="text-center">Меню</h2>
<div class="row mx-auto">
<div class="pt-4 ps-2 text-center" style="font-size: larger;">Обязательно возьми</div>
<div><img class="p-2" src="/kola.jpg" width="100%" /></div>
</div>
<div class="tab-pane container active table-responsive" id="orders">
<form th:action="@{/catalog}" method="get" class="row mt-2 w-50 mx-auto pb-3">
<div class="pb-3">
<input type="hidden" th:name="page" th:value="${page}">
<select th:name="typeId" id="typeId" class="form-select">
<option selected value="">Фильтр по продукции</option>
<option th:each="type : ${types}" th:value="${type.id}" th:selected="${type.id==typeId}">
[[${type.name}]]
</option>
</select>
<input type="hidden" th:name="page" th:value="${page}">
</div>
<button type="submit" class="btn btn-primary">Показать</button>
</form>
<table class="table">
<thead>
<th class="w-25"></th>
<th>Название</th>
<th>Тип</th>
<th class="w-auto">Описание</th>
<th>Цена</th>
<th></th>
</thead>
<tbody>
<tr th:each="producte : ${items}">
<th scope="row"><img src="/kola.jpg" class="w-100"></th>
<th scope="row" th:text="${producte.name}"></th>
<th:block th:each="type : ${types}">
<th:block th:if="${type.Id} eq ${producte.typeId}">
<th scope="row" th:text="${type.name}"></th>
</th:block>
</th:block>
<th scope="row" th:text="${producte.description}"></th>
<th scope="row" th:text="${producte.Price}"></th>
<th>
<form th:action="@{/catalog}" th:object="${order}" method="post">
<input type="hidden" th:name="page" th:value="${page}">
<select hidden="hidden" th:field="*{product}" class="form-select">
<option th:value="${producte.id}"></option>
</select>
<button type="submit" class="btn btn-link button-link"><i
class="bi bi-cart2 d-inline-block align-top me-1 logo"></i></button>
</form>
</th>
</tr>
</tbody>
</table>
<th:block th:replace="~{ pagination :: pagination (
url='catalog',
totalPages=${totalPages},
currentPage=${currentPage}) }" />
</div>
</div>
</main>
</body>
</html>

View File

@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="ru" data-bs-theme="dark" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title layout:title-pattern="$LAYOUT_TITLE - $CONTENT_TITLE">FasterPizza.ru</title>
<script type="text/javascript" src="/webjars/bootstrap/5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="/webjars/bootstrap/5.3.3/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="/webjars/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css" />
<link rel="stylesheet" href="/css/style.css" />
</head>
<body class="h-100 d-flex flex-column">
<nav class="navbar navbar-expand-md my-navbar" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<i class="bi-pizza-slice d-inline-block align-top me-1 logo"></i>
БыстроПицца
</a>
<th:block sec:authorize="isAuthenticated()" th:with="userName=${#authentication.name}">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#main-navbar"
aria-controls="main-navbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="main-navbar">
<ul class="navbar-nav me-auto link" th:with="activeLink=${#objects.nullSafe(servletPath, '')}">
<th:block sec:authorize="hasRole('ADMIN')">
<a class="nav-link" href="/admin/user"
th:classappend="${activeLink.startsWith('/admin/user') ? 'active' : ''}">
Пользователи
</a>
<a class="nav-link" href="/admin/type"
th:classappend="${activeLink.startsWith('/admin/type') ? 'active' : ''}">
Типы Продукции
</a>
<a class="nav-link" href="/admin/product"
th:classappend="${activeLink.startsWith('/admin/product') ? 'active' : ''}">
Продукция
</a>
<a class="nav-link" href="/admin/product/top"
th:classappend="${activeLink.startsWith('/admin/product/top') ? 'active' : ''}">
Топ Продуктов
</a>
<a class="nav-link" href="/h2-console/" target="_blank">Консоль H2</a>
</th:block>
<a class="nav-link" href="/catalog"
th:classappend="${activeLink.startsWith('/catalog') ? 'active' : ''}">
Каталог
</a>
<a class="nav-link" href="/">
Заказы
</a>
</ul>
<ul class="navbar-nav" th:if="${not #strings.isEmpty(userName)}">
<form th:action="@{/logout}" method="post">
<button type="submit" class="navbar-brand nav-link" onclick="return confirm('Вы уверены?')">
Выход ([[${userName}]])
</button>
</form>
<a class="navbar-brand" href="/cart">
<i class="bi bi-cart2 d-inline-block align-top me-1 logo"></i>
[[${#numbers.formatDecimal(totalCart, 1, 2)}]] &#8381;
</a>
</ul>
</div>
</th:block>
</div>
</nav>
<main class="container-fluid p-2" layout:fragment="content">
</main>
<footer class="my-footer mt-auto d-flex flex-shrink-0 justify-content-center align-items-center">
Крюков А.И., [[${#dates.year(#dates.createNow())}]]
</footer>
</body>
</html>

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Ошибка</title>
</head>
<body>
<main layout:fragment="content">
<ul class="list-group mb-2">
<th:block th:if="${#strings.isEmpty(message)}">
<li class="list-group-item">
Неизвестная ошибка
</li>
</th:block>
<th:block th:if="${not #strings.isEmpty(message)}">
<li class="list-group-item">
<strong>Ошибка:</strong> [[${message}]]
</li>
</th:block>
<th:block th:if="${not #strings.isEmpty(url)}">
<li class="list-group-item">
<strong>Адрес:</strong> [[${url}]]
</li>
<li class="list-group-item">
<strong>Класс исключения:</strong> [[${exception}]]
</li>
<li class="list-group-item">
[[${method}]] ([[${file}]]:[[${line}]])
</li>
</th:block>
</ul>
<a class="btn btn-primary button-fixed-width" href="/">На главную</a>
</main>
</body>
</html>

View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Вход</title>
</head>
<body>
<main layout:fragment="content">
<form action="#" th:action="@{/login}" method="post" class="w-25 mx-auto">
<div th:if="${param.error}" class="alert alert-danger">
Неверный логин или пароль
</div>
<div th:if="${param.logout}" class="alert alert-success">
Выход успешно произведен
</div>
<div th:if="${param.signup}" class="alert alert-success">
Пользователь успешно создан
</div>
<div class="mb-3">
<label for="username" class="form-label">Имя пользователя</label>
<input type="text" id="username" name="username" class="form-control" required minlength="3"
maxlength="20">
</div>
<div class="mb-3">
<label for="password" class="form-label">Пароль</label>
<input type="password" id="password" name="password" class="form-control" required minlength="3"
maxlength="20">
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="remember-me" name="remember-me" checked>
<label class="form-check-label" for="remember-me">Запомнить меня</label>
</div>
<div class="mb-3 d-flex flex-row">
<button class="btn btn-primary me-2 button-fixed-width" type="submit">Войти</button>
<a class="btn btn-secondary button-fixed-width" href="/signup">Регистрация</a>
</div>
</form>
</main>
</body>
</html>

View File

@ -0,0 +1,70 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<th:block th:fragment="orders (items, totalPages, currentPage, products)">
<th:block th:switch="${items.size()}">
<h2 th:case="0">Данные отсутствуют</h2>
<th:block th:case="*" class="w-50 mx-auto">
<table class="table mt-2">
<caption></caption>
<thead>
<tr>
<th scope="col" class="w-10">ID</th>
<th scope="col" class="w-auto">Игры</th>
<th scope="col" class="w-10"></th>
</tr>
</thead>
<tbody>
<tr th:each="order : ${items}">
<th scope="row" th:text="${order.id}"></th>
<td>
<table class="table">
<thead>
<tr>
<th scope="col" class="w-10"></th>
<th scope="col" class="w-25"></th>
<th scope="col" class="w-auto">Название продукции</th>
<th scope="col" class="w-10">Цена</th>
</tr>
</thead>
<tbody>
<tr th:each="productId : ${order.products}">
<th:block th:each="product : ${products}">
<th:block th:if="${productId} eq ${product.id}">
<th scope="row" th:text="${productId}"></th>
<th scope="row"><img src="/kola.jpg" class="w-100">
</th>
<th scope="row" th:text="${product.name}"></th>
<th scope="row" th:text="${product.price}"></th>
</th:block>
</th:block>
</tr>
</tbody>
</table>
</td>
<td>
<form th:action="@{/delete/{id}(id=${order.id})}" method="post">
<input type="hidden" th:name="page" th:value="${page}">
<button type="submit" class="btn btn-link button-link"
onclick="return confirm('Вы уверены?')">Удалить</button>
</form>
</td>
</tr>
</tbody>
</table>
</th:block>
<th:block th:replace="~{ pagination :: pagination (
url='',
totalPages=${totalPages},
currentPage=${currentPage}) }" />
<div class="mb-2 d-flex justify-content-center">
<a class="btn btn-primary" href="/cart">Создать заказ</a>
</div>
</th:block>
</th:block>
</body>
</html>

View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<th:block th:fragment="pagination (url, totalPages, currentPage)">
<nav th:if="${totalPages > 1}" th:with="
maxPage=2,
currentPage=${currentPage + 1}">
<ul class="pagination justify-content-center"
th:with="
seqFrom=${currentPage - maxPage < 1 ? 1 : currentPage - maxPage},
seqTo=${currentPage + maxPage > totalPages ? totalPages : currentPage + maxPage}">
<th:block th:if="${currentPage > maxPage + 1}">
<li class="page-item">
<a class="page-link" aria-label="Previous" th:href="@{/{url}?page=0(url=${url})}">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
<li class="page-item disabled">
<span class="page-link" aria-label="Previous">
<span aria-hidden="true">&hellip;</span>
</span>
</li>
</th:block>
<li class="page-item" th:each="page : ${#numbers.sequence(seqFrom, seqTo)}"
th:classappend="${page == currentPage} ? 'active' : ''">
<a class=" page-link" th:href="@{/{url}?page={page}(url=${url},page=${page - 1})}">
<span th:text="${page}" />
</a>
</li>
<th:block th:if="${currentPage < totalPages - maxPage}">
<li class="page-item disabled">
<span class="page-link" aria-label="Previous">
<span aria-hidden="true">&hellip;</span>
</span>
</li>
<li class="page-item">
<a class="page-link" aria-label="Next"
th:href="@{/{url}?page={page}(url=${url},page=${totalPages - 1})}">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</th:block>
</ul>
</nav>
</th:block>
</body>
</html>

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Редакторовать игру</title>
</head>
<body>
<main layout:fragment="content" class="w-50 mx-auto">
<form action="#" th:action="@{/admin/product/edit/{id}(id=${product.id})}" th:object="${product}" method="post">
<div class="mb-3">
<label for="id" class="form-label">ID</label>
<input type="text" th:value="*{id}" id="id" class="form-control" readonly disabled>
</div>
<div class="mb-3">
<label for="name" class="form-label">Название</label>
<input type="text" th:field="*{name}" id="name" class="form-control">
<div th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="invalid-feedback"></div>
</div>
<div class="mb-2">
<label for="typeId" class="form-label">Тип</label>
<select th:field="*{typeId}" id="typeId" class="form-select">
<option selected value="">Укажите тип</option>
<option th:each="type : ${types}" th:value="${type.id}">[[${type.name}]]</option>
</select>
</div>
<div class="mb-3">
<label for="price" class="form-label">Цена</label>
<input type="number" th:field="*{price}" id="price" class="form-control">
<div th:if="${#fields.hasErrors('price')}" th:errors="*{price}" class="invalid-feedback"></div>
</div>
<div class="mb-3">
<label for="description" class="form-label">Описание</label>
<textarea type="textarea" rows="5" cols="80" th:field="*{description}" id="description"
class="form-control"></textarea>
<div th:if="${#fields.hasErrors('description')}" th:errors="*{description}" class="invalid-feedback">
</div>
</div>
<div class="mb-3 d-flex flex-row">
<button class="btn btn-primary me-2 button-fixed-width" type="submit">Сохранить</button>
<a class="btn btn-secondary button-fixed-width" href="/admin/product">Отмена</a>
</div>
</form>
</main>
</body>
</html>

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Меню</title>
</head>
<body>
<main layout:fragment="content" class="w-50 mx-auto">
<th:block th:switch="${items.size()}">
<h2 th:case="0">Данные отсутствуют</h2>
<th:block th:case="*">
<h2 class="text-center">Меню</h2>
<div>
<a href="/admin/product/edit/" class="btn btn-primary">Добавить продукцию</a>
</div>
<table class="table">
<caption></caption>
<thead>
<tr>
<th scope="col" class="w-10">ID</th>
<th scope="col" class="w-10">Название</th>
<th scope="col" class="w-10">Цена</th>
<th scope="col" class="w-auto">Описание</th>
<th scope="col" class="w-10"></th>
<th scope="col" class="w-10"></th>
</tr>
</thead>
<tbody>
<tr th:each="product : ${items}">
<th scope="row" th:text="${product.id}"></th>
<td th:text="${product.name}"></td>
<td th:text="${product.price}"></td>
<td th:text="${product.description}"></td>
<td>
<form th:action="@{/admin/product/edit/{id}(id=${product.id})}" method="get">
<button type="submit" class="btn btn-link button-link">Редактировать</button>
</form>
</td>
<td>
<form th:action="@{/admin/product/delete/{id}(id=${product.id})}" method="post">
<button type="submit" class="btn btn-link button-link"
onclick="return confirm('Вы уверены?')">Удалить</button>
</form>
</td>
</tr>
</tbody>
</table>
</th:block>
</th:block>
</main>
</body>
</html>

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Личный кабинет</title>
</head>
<body>
<main layout:fragment="content" class="w-50 mx-auto main">
<div class="tab-content mt-2">
<h2 class="text-center">Заказы</h2>
<div class="tab-pane container active" id="orders">
<th:block
th:replace="~{ orders :: orders (items=${items}, totalPages=${totalPages}, currentPage=${currentPage}, products=${products})}" />
</div>
</div>
</main>
</body>
</html>

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Вход</title>
</head>
<body>
<main layout:fragment="content" class="w-25 mx-auto">
<form action="#" th:action="@{/signup}" th:object="${user}" method="post">
<div class="mb-3">
<label for="login" class="form-label">Имя пользователя</label>
<input type="text" th:field="*{login}" id="login" class="form-control">
<div th:if="${#fields.hasErrors('login')}" th:errors="*{login}" class="invalid-feedback"></div>
</div>
<div class="mb-3">
<label for="email" class="form-label">Почта</label>
<input type="email" th:field="*{email}" id="email" class="form-control">
<div th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="invalid-feedback"></div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Пароль</label>
<input type="password" th:field="*{password}" id="password" class="form-control">
<div th:if="${#fields.hasErrors('password')}" th:errors="*{password}" class="invalid-feedback"></div>
</div>
<div class="mb-3">
<label for="passwordConfirm" class="form-label">Пароль (подтверждение)</label>
<input type="password" th:field="*{passwordConfirm}" id="passwordConfirm" class="form-control">
<div th:if="${#fields.hasErrors('passwordConfirm')}" th:errors="*{passwordConfirm}"
class="invalid-feedback"></div>
</div>
<div class="mb-3 d-flex flex-row">
<button class="btn btn-primary me-2 button-fixed-width" type="submit">Регистрация</button>
<a class="btn btn-secondary button-fixed-width" href="/">Отмена</a>
</div>
</form>
</main>
</body>
</html>

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<meta charset="UTF-8">
<title>Top 5 Products</title>
</head>
<body>
<main layout:fragment="content" class="w-25 mx-auto">
<h1>Top 5 Most Purchased Products</h1>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Description</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr th:each="product : ${topProducts}">
<td th:text="${product.id}"></td>
<td th:text="${product.name}"></td>
<td th:text="${product.description}"></td>
<td th:text="${product.price}"></td>
</tr>
</tbody>
</table>
</main>
</body>
</html>

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Редакторовать тип продукции</title>
</head>
<body>
<main layout:fragment="content" class="w-50 mx-auto">
<form action="#" th:action="@{/admin/type/edit/{id}(id=${type.id})}" th:object="${type}" method="post">
<div class="mb-3">
<label for="id" class="form-label">ID</label>
<input type="text" th:value="*{id}" id="id" class="form-control" readonly disabled>
</div>
<div class="mb-3">
<label for="name" class="form-label">Тип продукции</label>
<input type="text" th:field="*{name}" id="name" class="form-control">
<div th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="invalid-feedback"></div>
</div>
<div class="mb-3 d-flex flex-row">
<button class="btn btn-primary me-2 button-fixed-width" type="submit">Сохранить</button>
<a class="btn btn-secondary button-fixed-width" href="/admin/type">Отмена</a>
</div>
</form>
</main>
</body>
</html>

View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Типы продукции</title>
</head>
<body>
<main layout:fragment="content" class="w-50 mx-auto">
<th:block th:switch="${items.size()}">
<h2 th:case="0">Данные отсутствуют</h2>
<th:block th:case="*">
<h2 class="text-center">Типы продукции</h2>
<div>
<a href="/admin/type/edit/" class="btn btn-primary">Добавить тип продукции</a>
</div>
<table class="table">
<caption></caption>
<thead>
<tr>
<th scope="col" class="w-10">ID</th>
<th scope="col" class="w-auto">Тип продукции</th>
<th scope="col" class="w-10"></th>
<th scope="col" class="w-10"></th>
</tr>
</thead>
<tbody>
<tr th:each="type : ${items}">
<th scope="row" th:text="${type.id}"></th>
<td th:text="${type.name}"></td>
<td>
<form th:action="@{/admin/type/edit/{id}(id=${type.id})}" method="get">
<button type="submit" class="btn btn-link button-link">Редактировать</button>
</form>
</td>
<td>
<form th:action="@{/admin/type/delete/{id}(id=${type.id})}" method="post">
<button type="submit" class="btn btn-link button-link"
onclick="return confirm('Вы уверены?')">Удалить</button>
</form>
</td>
</tr>
</tbody>
</table>
</th:block>
</th:block>
</main>
</body>
</html>

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Редакторовать пользователя</title>
</head>
<body>
<main layout:fragment="content" class="w-50 mx-auto">
<form action="#" th:action="@{/admin/user/edit/{id}(id=${user.id},page=${page})}" th:object="${user}"
method="post">
<div class="mb-3">
<label for="id" class="form-label">ID</label>
<input type="text" th:value="*{id}" id="id" class="form-control" readonly disabled>
</div>
<div class="mb-3">
<label for="login" class="form-label">Имя пользователя</label>
<input type="text" th:field="*{login}" id="login" class="form-control">
<div th:if="${#fields.hasErrors('login')}" th:errors="*{login}" class="invalid-feedback"></div>
</div>
<div class="mb-3">
<label for="email" class="form-label">Почта</label>
<input type="text" th:field="*{email}" id="email" class="form-control">
<div th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="invalid-feedback"></div>
</div>
<div class="mb-3 d-flex flex-row">
<button class="btn btn-primary me-2 button-fixed-width" type="submit">Сохранить</button>
<a class="btn btn-secondary button-fixed-width" th:href="@{/admin/user(page=${page})}">Отмена</a>
</div>
</form>
</main>
</body>
</html>

View File

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Пользователи</title>
</head>
<body>
<main layout:fragment="content" class="w-50 mx-auto">
<th:block th:switch="${items.size()}">
<h2 th:case="0">Данные отсутствуют</h2>
<th:block th:case="*">
<h2 class=" text-center">Пользователи</h2>
<div>
<a th:href="@{/admin/user/edit/(page=${page})}" class="btn btn-primary">Добавить пользователя</a>
</div>
<table class="table">
<caption></caption>
<thead>
<tr>
<th scope="col" class="w-10">ID</th>
<th scope="col" class="w-auto">Имя пользователя</th>
<th scope="col" class="w-auto">Почта</th>
<th scope="col" class="w-10"></th>
<th scope="col" class="w-10"></th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${items}">
<th scope="row" th:text="${user.id}"></th>
<td th:text="${user.login}"></td>
<td th:text="${user.email}"></td>
<td>
<form th:action="@{/admin/user/edit/{id}(id=${user.id})}" method="get">
<input type="hidden" th:name="page" th:value="${page}">
<button type="submit" class="btn btn-link button-link">Редактировать</button>
</form>
</td>
<td>
<form th:action="@{/admin/user/delete/{id}(id=${user.id})}" method="post">
<input type="hidden" th:name="page" th:value="${page}">
<button type="submit" class="btn btn-link button-link"
onclick="return confirm('Вы уверены?')">Удалить</button>
</form>
</td>
</tr>
</tbody>
</table>
</th:block>
<th:block th:replace="~{ pagination :: pagination (
url=${'admin/user'},
totalPages=${totalPages},
currentPage=${currentPage}) }" />
</th:block>
</main>
</body>
</html>

Binary file not shown.

BIN
data.mv.db Normal file

Binary file not shown.

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

249
gradlew vendored Normal file
View File

@ -0,0 +1,249 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

92
gradlew.bat vendored Normal file
View File

@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

1
settings.gradle Normal file
View File

@ -0,0 +1 @@
rootProject.name = 'demo'

View File

@ -0,0 +1,101 @@
package com.example.demo;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import com.example.demo.core.configuration.Constants;
import com.example.demo.products.model.ProductEntity;
import com.example.demo.products.service.ProductService;
import com.example.demo.orders.model.OrderEntity;
import com.example.demo.orders.service.OrderService;
import com.example.demo.types.model.TypeEntity;
import com.example.demo.types.service.TypeService;
import com.example.demo.users.model.UserEntity;
import com.example.demo.users.model.UserRole;
import com.example.demo.users.service.UserService;
@SpringBootApplication
public class DemoApplication implements CommandLineRunner {
private final Logger log = LoggerFactory.getLogger(DemoApplication.class);
private final TypeService typeService;
private final ProductService productService;
private final OrderService orderService;
private final UserService userService;
public DemoApplication(TypeService typeService, ProductService productService,
OrderService orderService, UserService userService) {
this.typeService = typeService;
this.productService = productService;
this.orderService = orderService;
this.userService = userService;
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
if (args.length > 0 && Objects.equals("--populate", args[0])) {
log.info("start");
log.info("Create default user values");
final var admin = new UserEntity("admin", "admin@mail.com", "admin");
admin.setRole(UserRole.ADMIN);
userService.create(admin);
final var user1 = userService.create(new UserEntity("user", "user@gmail.com",
Constants.DEFAULT_PASSWORD));
final var type1 = typeService.create(new TypeEntity("Пицца"));
final var type2 = typeService.create(new TypeEntity("Лимонад"));
final var product1 = productService.create(new ProductEntity(type1, "Цыпленок барбекью", 510.0,
"Сочная пицца с кусочками пиццы под вкуснейшим соусом барбекью"));
final var product2 = productService.create(new ProductEntity(type2, "Кола", 100.0, "Старая добрая кола"));
productService
.create(new ProductEntity(type1, "Мексиканская", 500.0,
"Обжигающая пицца с БОЛЬШИМ количеством мяса"));
productService.create(new ProductEntity(type2, "Фанта", 80.0, "Добрый Фанта"));
productService.create(new ProductEntity(type1, "4 сыра", 400.0, "4 нежнейших сыра на хрустящей корочке"));
productService.create(new ProductEntity(type2, "Щай", 60.0, "Татарам с молоком)"));
productService.create(new ProductEntity(type1, "Пепперени", 400.0, "Классика"));
final var product4 = productService
.create(new ProductEntity(type1, "Пеппирони", 400.0, "много мяса и сыра"));
final var product5 = productService.create(new ProductEntity(type2, "Фанта", 100.0, "Старая добрая кола"));
final var product3 = productService
.create(new ProductEntity(type1, "Гавайская", 400.0, "много мяса и сыра и ананасик"));
final var product6 = productService
.create(new ProductEntity(type2, "Спрайт", 100.0, "Освежает, не правда-ли?"));
final var product7 = productService
.create(new ProductEntity(type1, "Маргарита", 400.0, "много мяса и сыра"));
final List<ProductEntity> products = new ArrayList<ProductEntity>();
products.add(product1);
products.add(product2);
final List<ProductEntity> products2 = new ArrayList<ProductEntity>();
products.add(product4);
products.add(product5);
final List<ProductEntity> products3 = new ArrayList<ProductEntity>();
products.add(product3);
products.add(product6);
products.add(product7);
log.info("Create default order values");
orderService.create(user1.getId(), new OrderEntity(products2));
orderService.create(admin.getId(), new OrderEntity(products));
orderService.create(user1.getId(), new OrderEntity(products3));
orderService.create(admin.getId(), new OrderEntity(products3));
}
}
}

View File

@ -0,0 +1,28 @@
package com.example.demo.core.api;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
import com.example.demo.core.session.SessionCart;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
@ControllerAdvice
public class GlobalController {
private final SessionCart cart;
public GlobalController(SessionCart cart) {
this.cart = cart;
}
@ModelAttribute("servletPath")
String getRequestServletPath(HttpServletRequest request) {
return request.getServletPath();
}
@ModelAttribute("totalCart")
double getTotalCart(HttpSession session) {
return cart.getSum();
}
}

View File

@ -0,0 +1,18 @@
package com.example.demo.core.api;
import java.util.Map;
import java.util.function.Function;
import org.springframework.data.domain.Page;
public class PageAttributesMapper {
private PageAttributesMapper() {
}
public static <E, D> Map<String, Object> toAttributes(Page<E> page, Function<E, D> mapper) {
return Map.of(
"items", page.getContent().stream().map(mapper::apply).toList(),
"currentPage", page.getNumber(),
"totalPages", page.getTotalPages());
}
}

View File

@ -0,0 +1,19 @@
package com.example.demo.core.configuration;
public class Constants {
public static final String SEQUENCE_NAME = "hibernate_sequence";
public static final int DEFAULT_PAGE_SIZE = 5;
public static final String REDIRECT_VIEW = "redirect:";
public static final String ADMIN_PREFIX = "/admin";
public static final String LOGIN_URL = "/login";
public static final String LOGOUT_URL = "/logout";
public static final String DEFAULT_PASSWORD = "123456";
private Constants() {
}
}

View File

@ -0,0 +1,23 @@
package com.example.demo.core.configuration;
import org.modelmapper.ModelMapper;
import org.modelmapper.PropertyMap;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.example.demo.core.model.BaseEntity;
@Configuration
public class MapperConfiguration {
@Bean
ModelMapper modelMapper() {
final ModelMapper mapper = new ModelMapper();
mapper.addMappings(new PropertyMap<Object, BaseEntity>() {
@Override
protected void configure() {
skip(destination.getId());
}
});
return mapper;
}
}

View File

@ -0,0 +1,13 @@
package com.example.demo.core.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
}
}

View File

@ -0,0 +1,53 @@
package com.example.demo.core.error;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
@ControllerAdvice
public class AdviceController {
private final Logger log = LoggerFactory.getLogger(AdviceController.class);
private static Throwable getRootCause(Throwable throwable) {
Throwable rootCause = throwable;
while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
rootCause = rootCause.getCause();
}
return rootCause;
}
private static Map<String, Object> getAttributes(HttpServletRequest request, Throwable throwable) {
final Throwable rootCause = getRootCause(throwable);
final StackTraceElement firstError = rootCause.getStackTrace()[0];
return Map.of(
"message", rootCause.getMessage(),
"url", request.getRequestURL(),
"exception", rootCause.getClass().getName(),
"file", firstError.getFileName(),
"method", firstError.getMethodName(),
"line", firstError.getLineNumber());
}
@ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest request, Throwable throwable) throws Throwable {
if (AnnotationUtils.findAnnotation(throwable.getClass(),
ResponseStatus.class) != null) {
throw throwable;
}
log.error("{}", throwable.getMessage());
throwable.printStackTrace();
final ModelAndView model = new ModelAndView();
model.addAllObjects(getAttributes(request, throwable));
model.setViewName("error");
return model;
}
}

View File

@ -0,0 +1,7 @@
package com.example.demo.core.error;
public class NotFoundException extends RuntimeException {
public <T> NotFoundException(Class<T> clazz, Long id) {
super(String.format("%s with id [%s] is not found or not exists", clazz.getSimpleName(), id));
}
}

View File

@ -0,0 +1,28 @@
package com.example.demo.core.model;
import com.example.demo.core.configuration.Constants;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.SequenceGenerator;
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = Constants.SEQUENCE_NAME)
@SequenceGenerator(name = Constants.SEQUENCE_NAME, sequenceName = Constants.SEQUENCE_NAME, allocationSize = 1)
protected Long id;
protected BaseEntity() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}

View File

@ -0,0 +1,63 @@
package com.example.demo.core.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import com.example.demo.core.configuration.Constants;
import com.example.demo.users.api.UserSignupController;
import com.example.demo.users.model.UserRole;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.headers(headers -> headers.frameOptions(FrameOptionsConfig::sameOrigin));
httpSecurity.csrf(AbstractHttpConfigurer::disable);
httpSecurity.cors(Customizer.withDefaults());
httpSecurity.authorizeHttpRequests(requests -> requests
.requestMatchers("/css/**", "/webjars/**", "/*.svg")
.permitAll());
httpSecurity.authorizeHttpRequests(requests -> requests
.requestMatchers(Constants.ADMIN_PREFIX + "/**").hasRole(UserRole.ADMIN.name())
.requestMatchers("/h2-console/**").hasRole(UserRole.ADMIN.name())
.requestMatchers(UserSignupController.URL).anonymous()
.requestMatchers(Constants.LOGIN_URL).anonymous()
.anyRequest().authenticated());
httpSecurity.formLogin(formLogin -> formLogin
.loginPage(Constants.LOGIN_URL));
httpSecurity.rememberMe(rememberMe -> rememberMe.key("uniqueAndSecret"));
httpSecurity.logout(logout -> logout
.deleteCookies("JSESSIONID"));
return httpSecurity.build();
}
@Bean
DaoAuthenticationProvider authenticationProvider(UserDetailsService userDetailsService) {
final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@ -0,0 +1,64 @@
package com.example.demo.core.security;
import java.util.Collection;
import java.util.Set;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.example.demo.users.model.UserEntity;
public class UserPrincipal implements UserDetails{
private final long id;
private final String username;
private final String password;
private final Set<? extends GrantedAuthority> roles;
private final boolean active;
public UserPrincipal(UserEntity user) {
this.id = user.getId();
this.username = user.getLogin();
this.password = user.getPassword();
this.roles = Set.of(user.getRole());
this.active = true;
}
public Long getId() {
return id;
}
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles;
}
@Override
public boolean isEnabled() {
return active;
}
@Override
public boolean isAccountNonExpired() {
return isEnabled();
}
@Override
public boolean isAccountNonLocked() {
return isEnabled();
}
@Override
public boolean isCredentialsNonExpired() {
return isEnabled();
}
}

View File

@ -0,0 +1,14 @@
package com.example.demo.core.session;
import java.util.HashMap;
import com.example.demo.users.api.UserCartDto;
public class SessionCart extends HashMap<Integer, UserCartDto> {
public double getSum() {
return this.values().stream()
.map(item -> item.getPrice())
.mapToDouble(Double::doubleValue)
.sum();
}
}

View File

@ -0,0 +1,16 @@
package com.example.demo.core.session;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.web.context.WebApplicationContext;
@Configuration
public class SessionHelper {
@Bean
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
SessionCart todos() {
return new SessionCart();
}
}

View File

@ -0,0 +1,40 @@
package com.example.demo.order_lines.api;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
public class OrderLineDto {
@NotNull
@Min(1)
private Long productId;
@NotNull
private Integer count;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private Double totalPriceLine;
public Double getTotalPriceLine() {
return totalPriceLine;
}
public void setTotalPriceLine(Double totalPriceLine) {
this.totalPriceLine = totalPriceLine;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
}

View File

@ -0,0 +1,88 @@
package com.example.demo.order_lines.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.util.Objects;
import com.example.demo.core.model.BaseEntity;
import com.example.demo.products.model.ProductEntity;
@Entity
@Table(name = "lines")
public class OrderLineEntity extends BaseEntity {
@Column(nullable = false)
private Integer count;
@ManyToOne
@JoinColumn(name = "productId", nullable = false)
private ProductEntity product;
@Column(nullable = false)
private Double totalPrice;
public OrderLineEntity() {
// Конструктор
}
public OrderLineEntity(ProductEntity product, Integer count) {
this.product = product;
this.count = count;
calculateTotalPrice(); // Рассчитываем сумму при создании
}
public ProductEntity getProduct() {
return product;
}
public void setProduct(ProductEntity product) {
this.product = product;
calculateTotalPrice(); // Пересчитываем сумму при установке продукта
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
calculateTotalPrice(); // Пересчитываем сумму при установке количества
}
public Double getTotalPrice() {
return totalPrice;
}
public void setTotalPrice(Double totalPrice) {
this.totalPrice = totalPrice;
}
private void calculateTotalPrice() {
if (product != null && product.getPrice() != null && count != null) {
totalPrice = product.getPrice() * count;
} else {
totalPrice = 0.0;
}
}
@Override
public int hashCode() {
return Objects.hash(id, product, count, totalPrice);
}
@SuppressWarnings("unlikely-arg-order-product")
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
final OrderLineEntity other = (OrderLineEntity) obj;
return Objects.equals(other.getProduct(), product)
&& Objects.equals(other.getCount(), count)
&& Objects.equals(other.getTotalPrice(), totalPrice);
}
}

View File

@ -0,0 +1,8 @@
package com.example.demo.order_lines.repository;
import org.springframework.data.repository.CrudRepository;
import com.example.demo.order_lines.model.OrderLineEntity;
public interface OrderLineRepository extends CrudRepository<OrderLineEntity, Long> {
}

View File

@ -0,0 +1,29 @@
package com.example.demo.orders.api;
import java.util.ArrayList;
import java.util.List;
import jakarta.validation.constraints.NotNull;
public class OrderDto {
private Long id;
@NotNull
private final List<Long> products = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public List<Long> getProducts() {
return products;
}
public void setProducts(List<Long> products) {
this.products.clear();
this.products.addAll(products);
}
}

View File

@ -0,0 +1,71 @@
package com.example.demo.orders.model;
import java.util.List;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import com.example.demo.core.model.BaseEntity;
import com.example.demo.products.model.ProductEntity;
import com.example.demo.users.model.UserEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "orders")
public class OrderEntity extends BaseEntity {
@ManyToOne
@JoinColumn(name = "userId", nullable = false)
private UserEntity user;
@ManyToMany()
private Set<ProductEntity> products = new HashSet<>();
public OrderEntity() {
}
public OrderEntity(List<ProductEntity> products) {
this.products.clear();
this.products.addAll(products);
}
public UserEntity getUser() {
return user;
}
public void setUser(UserEntity user) {
this.user = user;
if (!user.getOrders().contains(this)) {
user.getOrders().add(this);
}
}
public Set<ProductEntity> getProducts() {
return products;
}
public void setProducts(ProductEntity product) {
this.products.add(product);
}
@Override
public int hashCode() {
return Objects.hash(id, products);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
final OrderEntity other = (OrderEntity) obj;
return Objects.equals(other.getId(), id)
&& Objects.equals(other.getProducts(), products);
}
}

View File

@ -0,0 +1,26 @@
package com.example.demo.orders.repository;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import com.example.demo.orders.model.OrderEntity;
public interface OrderRepository
extends CrudRepository<OrderEntity, Long>, PagingAndSortingRepository<OrderEntity, Long> {
Optional<OrderEntity> findOneByUserIdAndId(long userId, long id);
@Query("select o from OrderEntity o join fetch o.products where o.user.id = ?1")
List<OrderEntity> findByUserId(long userId);
@Query("select o from OrderEntity o join fetch o.products where o.user.id = ?1")
Page<OrderEntity> findByUserId(long userId, Pageable pageable);
List<OrderEntity> findAll();
}

View File

@ -0,0 +1,79 @@
package com.example.demo.orders.service;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.core.error.NotFoundException;
import com.example.demo.orders.model.OrderEntity;
import com.example.demo.orders.repository.OrderRepository;
import com.example.demo.users.model.UserEntity;
import com.example.demo.users.service.UserService;
@Service
public class OrderService {
private final OrderRepository repository;
private final UserService userService;
public OrderService(OrderRepository repository, UserService userService) {
this.repository = repository;
this.userService = userService;
}
@Transactional(readOnly = true)
public Page<OrderEntity> getAll(long userId, int page, int size) {
final Pageable pageRequest = PageRequest.of(page, size, Sort.by("id"));
userService.get(userId);
return repository.findByUserId(userId, pageRequest);
}
@Transactional(readOnly = true)
public List<OrderEntity> getAll(long userId) {
userService.get(userId);
return repository.findByUserId(userId);
}
public List<OrderEntity> getAll() {
return repository.findAll();
}
@Transactional(readOnly = true)
public OrderEntity get(long userId, long id) {
userService.get(userId);
return repository.findOneByUserIdAndId(userId, id)
.orElseThrow(() -> new NotFoundException(OrderEntity.class, id));
}
@Transactional
public OrderEntity create(long userId, OrderEntity entity) {
if (entity == null) {
throw new IllegalArgumentException("Entity is null");
}
final UserEntity existsUser = userService.get(userId);
entity.setUser(existsUser);
return repository.save(entity);
}
@Transactional
public OrderEntity createAll(long userId, OrderEntity entitiy) {
if (entitiy == null) {
throw new IllegalArgumentException("Orders list is null or empty");
}
final UserEntity existsUser = userService.get(userId);
entitiy.setUser(existsUser);
return repository.save(entitiy);
}
@Transactional
public OrderEntity delete(long userId, long id) {
userService.get(userId);
final OrderEntity existsEntity = get(userId, id);
repository.delete(existsEntity);
return existsEntity;
}
}

View File

@ -0,0 +1,156 @@
package com.example.demo.products.api;
import java.util.List;
import java.util.Map;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.example.demo.core.api.PageAttributesMapper;
import com.example.demo.core.configuration.Constants;
import com.example.demo.products.model.ProductEntity;
import com.example.demo.products.service.ProductService;
import com.example.demo.types.api.TypeDto;
import com.example.demo.types.model.TypeEntity;
import com.example.demo.types.service.TypeService;
import jakarta.validation.Valid;
@Controller
@RequestMapping(ProductController.URL)
public class ProductController {
public static final String URL = Constants.ADMIN_PREFIX + "/product";
private static final String PRODUCT_VIEW = "product";
private static final String TOP_PRODUCTS_VIEW = "top-products";
private static final String PRODUCT_EDIT_VIEW = "product-edit";
private static final String PAGE_ATTRIBUTE = "page";
private static final String PRODUCT_ATTRIBUTE = "product";
private final ProductService productService;
private final TypeService typeService;
private final ModelMapper modelMapper;
public ProductController(ProductService productService, TypeService typeService, ModelMapper modelMapper) {
this.productService = productService;
this.modelMapper = modelMapper;
this.typeService = typeService;
}
private ProductDto toDto(ProductEntity entity) {
var dto = new ProductDto();
dto.setId(entity.getId());
dto.setDescription(entity.getDescription());
dto.setName(entity.getName());
dto.setPrice(entity.getPrice());
dto.setTypeId(entity.getType().getId());
return dto;
}
private TypeDto toTypeDto(TypeEntity entity) {
return modelMapper.map(entity, TypeDto.class);
}
private ProductEntity toEntity(ProductDto dto) {
final ProductEntity entity = modelMapper.map(dto, ProductEntity.class);
entity.setType(typeService.get(dto.getTypeId()));
return entity;
}
@GetMapping
public String getAll(
@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page,
Model model) {
final Map<String, Object> attributes = PageAttributesMapper.toAttributes(
productService.getAll(0, page, Constants.DEFAULT_PAGE_SIZE), this::toDto);
model.addAllAttributes(attributes);
model.addAttribute(PAGE_ATTRIBUTE, page);
return PRODUCT_VIEW;
}
@GetMapping("/edit/")
public String create(
@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page,
Model model) {
model.addAttribute(PRODUCT_ATTRIBUTE, new ProductDto());
model.addAttribute("types", typeService.getAll().stream().map(this::toTypeDto).toList());
model.addAttribute(PAGE_ATTRIBUTE, page);
return PRODUCT_EDIT_VIEW;
}
@PostMapping("/edit/")
public String create(
@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page,
@ModelAttribute(name = PRODUCT_ATTRIBUTE) @Valid ProductDto product,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
model.addAttribute(PAGE_ATTRIBUTE, page);
return PRODUCT_EDIT_VIEW;
}
redirectAttributes.addAttribute(PAGE_ATTRIBUTE, page);
productService.create(toEntity(product));
return Constants.REDIRECT_VIEW + URL;
}
@GetMapping("/edit/{id}")
public String update(
@PathVariable(name = "id") Long id,
@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page,
Model model) {
if (id <= 0) {
throw new IllegalArgumentException();
}
model.addAttribute(PRODUCT_ATTRIBUTE, toDto(productService.get(id)));
model.addAttribute(PAGE_ATTRIBUTE, page);
model.addAttribute("types", typeService.getAll().stream().map(this::toTypeDto).toList());
return PRODUCT_EDIT_VIEW;
}
@PostMapping("/edit/{id}")
public String update(
@PathVariable(name = "id") Long id,
@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page,
@ModelAttribute(name = PRODUCT_ATTRIBUTE) @Valid ProductDto product,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
model.addAttribute(PAGE_ATTRIBUTE, page);
return PRODUCT_EDIT_VIEW;
}
if (id <= 0) {
throw new IllegalArgumentException();
}
redirectAttributes.addAttribute(PAGE_ATTRIBUTE, page);
productService.update(id, toEntity(product));
return Constants.REDIRECT_VIEW + URL;
}
@PostMapping("/delete/{id}")
public String delete(
@PathVariable(name = "id") Long id,
@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page,
RedirectAttributes redirectAttributes) {
redirectAttributes.addAttribute(PAGE_ATTRIBUTE, page);
productService.delete(id);
return Constants.REDIRECT_VIEW + URL;
}
@GetMapping("/top")
public String getTopProducts(Model model) {
List<ProductEntity> topProducts = productService.getTopFiveMostSoldProducts();
model.addAttribute("topProducts", topProducts.stream().map(this::toDto).toList());
return TOP_PRODUCTS_VIEW;
}
}

View File

@ -0,0 +1,74 @@
package com.example.demo.products.api;
import java.util.ArrayList;
import java.util.List;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public class ProductDto {
private Long id;
@NotNull
@Min(1)
private Long typeId;
@NotNull
private final List<Long> genres = new ArrayList<>();
@NotNull
@Min(100)
private Double price;
@NotBlank
private String name;
@NotBlank
private String description;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getTypeId() {
return typeId;
}
public void setTypeId(Long typeId) {
this.typeId = typeId;
}
public List<Long> getGenres() {
return genres;
}
public void setGenres(List<Long> genres) {
this.genres.clear();
this.genres.addAll(genres);
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

View File

@ -0,0 +1,90 @@
package com.example.demo.products.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import com.example.demo.core.model.BaseEntity;
import com.example.demo.types.model.TypeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "products")
public class ProductEntity extends BaseEntity {
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Double price;
@Column(nullable = false)
private String description;
@ManyToOne
@JoinColumn(name = "typeId", nullable = false, unique = false)
private TypeEntity type;
public ProductEntity() {
}
public ProductEntity(TypeEntity type, String name, Double price, String description) {
this.type = type;
this.name = name;
this.price = price;
this.description = description;
}
public TypeEntity getType() {
return type;
}
public void setType(TypeEntity type) {
this.type = type;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public int hashCode() {
return Objects.hash(id, type, price, description, name);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
final ProductEntity other = (ProductEntity) obj;
return Objects.equals(other.getId(), id)
&& Objects.equals(other.getType(), type)
&& Objects.equals(other.getPrice(), price)
&& Objects.equals(other.getDescription(), description)
&& Objects.equals(other.getName(), name);
}
}

View File

@ -0,0 +1,35 @@
package com.example.demo.products.repository;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import com.example.demo.products.model.ProductEntity;
public interface ProductRepository
extends CrudRepository<ProductEntity, Long>, PagingAndSortingRepository<ProductEntity, Long> {
@Query("select distinct p from ProductEntity p join fetch p.type t where p.id = ?1")
Optional<ProductEntity> findOneById(long id);
@Query("select distinct p from ProductEntity p join fetch p.type t where t.id = ?1")
Page<ProductEntity> findByTypeId(long typeId, Pageable pageable);
@Query("select distinct p from ProductEntity p join fetch p.type t where t.id = ?1")
List<ProductEntity> findByTypeId(long typeId);
@Query("select distinct p from ProductEntity p join fetch p.type t")
Page<ProductEntity> findAll(Pageable pageable);
@Query("select distinct p from ProductEntity p join fetch p.type t")
List<ProductEntity> findAll();
@Query("SELECT p FROM OrderEntity o JOIN o.products p GROUP BY p.id ORDER BY COUNT(p.id) DESC")
List<ProductEntity> findTop5MostPurchasedProducts(Pageable pageable);
}

View File

@ -0,0 +1,91 @@
package com.example.demo.products.service;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.StreamSupport;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.data.domain.Pageable;
import com.example.demo.core.error.NotFoundException;
import com.example.demo.orders.repository.OrderRepository;
import com.example.demo.products.model.ProductEntity;
import com.example.demo.products.repository.ProductRepository;
@Service
public class ProductService {
private final ProductRepository repository;
private final OrderRepository orderRepository;
public ProductService(ProductRepository repository, OrderRepository orderRepository) {
this.repository = repository;
this.orderRepository = orderRepository;
}
@Transactional(readOnly = true)
public List<ProductEntity> getAll(long typeId) {
if (!Objects.equals(typeId, 0L)) {
return repository.findByTypeId(typeId);
}
return repository.findAll();
}
@Transactional(readOnly = true)
public Page<ProductEntity> getAll(long typeId, int page, int size) {
final Pageable pageRequest = PageRequest.of(page, size);
if (!Objects.equals(typeId, 0L)) {
return repository.findByTypeId(typeId, pageRequest);
}
return repository.findAll(pageRequest);
}
@Transactional(readOnly = true)
public ProductEntity get(Long id) {
return repository.findOneById(id).orElseThrow(() -> new NotFoundException(ProductEntity.class, id));
}
@Transactional(readOnly = true)
public List<ProductEntity> getByIds(Collection<Long> ids) {
final List<ProductEntity> products = StreamSupport.stream(repository.findAllById(ids).spliterator(), false)
.toList();
if (products.size() < ids.size()) {
throw new IllegalArgumentException("Invalid type");
}
return products;
}
@Transactional
public ProductEntity create(ProductEntity entity) {
if (entity == null) {
throw new IllegalArgumentException("Entity is null");
}
return repository.save(entity);
}
@Transactional
public ProductEntity update(Long id, ProductEntity entity) {
final ProductEntity existEntity = get(id);
existEntity.setName(entity.getName());
existEntity.setPrice(entity.getPrice());
existEntity.setDescription(entity.getDescription());
existEntity.setType(entity.getType());
return repository.save(existEntity);
}
@Transactional
public ProductEntity delete(Long id) {
final ProductEntity existEntity = get(id);
repository.delete(existEntity);
return existEntity;
}
public List<ProductEntity> getTopFiveMostSoldProducts() {
Pageable topFive = PageRequest.of(0, 5);
return repository.findTop5MostPurchasedProducts(topFive);
}
}

View File

@ -0,0 +1,105 @@
package com.example.demo.types.api;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.demo.core.configuration.Constants;
import com.example.demo.types.model.TypeEntity;
import com.example.demo.types.service.TypeService;
import jakarta.validation.Valid;
@Controller
@RequestMapping(TypeController.URL)
public class TypeController {
public static final String URL = Constants.ADMIN_PREFIX + "/type";
private static final String TYPE_VIEW = "type";
private static final String TYPE_EDIT_VIEW = "type-edit";
private static final String TYPE_ATTRIBUTE = "type";
private final TypeService typeService;
private final ModelMapper modelMapper;
public TypeController(TypeService typeService, ModelMapper modelMapper) {
this.typeService = typeService;
this.modelMapper = modelMapper;
}
private TypeDto toDto(TypeEntity entity) {
return modelMapper.map(entity, TypeDto.class);
}
private TypeEntity toEntity(TypeDto dto) {
return modelMapper.map(dto, TypeEntity.class);
}
@GetMapping
public String getAll(Model model) {
model.addAttribute(
"items",
typeService.getAll().stream()
.map(this::toDto)
.toList());
return TYPE_VIEW;
}
@GetMapping("/edit/")
public String create(Model model) {
model.addAttribute(TYPE_ATTRIBUTE, new TypeDto());
return TYPE_EDIT_VIEW;
}
@PostMapping("/edit/")
public String create(
@ModelAttribute(name = TYPE_ATTRIBUTE) @Valid TypeDto type,
BindingResult bindingResult,
Model model) {
if (bindingResult.hasErrors()) {
return TYPE_EDIT_VIEW;
}
typeService.create(toEntity(type));
return Constants.REDIRECT_VIEW + URL;
}
@GetMapping("/edit/{id}")
public String update(
@PathVariable(name = "id") Long id,
Model model) {
if (id <= 0) {
throw new IllegalArgumentException();
}
model.addAttribute(TYPE_ATTRIBUTE, toDto(typeService.get(id)));
return TYPE_EDIT_VIEW;
}
@PostMapping("/edit/{id}")
public String update(
@PathVariable(name = "id") Long id,
@ModelAttribute(name = TYPE_ATTRIBUTE) @Valid TypeDto type,
BindingResult bindingResult,
Model model) {
if (bindingResult.hasErrors()) {
return TYPE_EDIT_VIEW;
}
if (id <= 0) {
throw new IllegalArgumentException();
}
typeService.update(id, toEntity(type));
return Constants.REDIRECT_VIEW + URL;
}
@PostMapping("/delete/{id}")
public String delete(
@PathVariable(name = "id") Long id) {
typeService.delete(id);
return Constants.REDIRECT_VIEW + URL;
}
}

View File

@ -0,0 +1,27 @@
package com.example.demo.types.api;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class TypeDto {
private Long id;
@NotBlank
@Size(min = 1, max = 50)
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@ -0,0 +1,46 @@
package com.example.demo.types.model;
import java.util.Objects;
import com.example.demo.core.model.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
@Entity
@Table(name = "types")
public class TypeEntity extends BaseEntity{
@Column(nullable = false, unique = true, length = 50)
private String name;
public TypeEntity(){
}
public TypeEntity(String name){
this.name = name;
}
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
@Override
public int hashCode(){
return Objects.hash(id,name);
}
@Override
public boolean equals(Object obj){
if(this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
final TypeEntity other = (TypeEntity) obj;
return Objects.equals(other.getId(), id) && Objects.equals(other.getName(), name);
}
}

View File

@ -0,0 +1,12 @@
package com.example.demo.types.repository;
import java.util.Optional;
import org.springframework.data.repository.CrudRepository;
import com.example.demo.types.model.TypeEntity;
public interface TypeRepository extends CrudRepository<TypeEntity, Long> {
Optional<TypeEntity> findByNameIgnoreCase(String name);
Optional<TypeEntity> findById(long id);
}

View File

@ -0,0 +1,53 @@
package com.example.demo.types.service;
import java.util.List;
import java.util.stream.StreamSupport;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.core.error.NotFoundException;
import com.example.demo.types.model.TypeEntity;
import com.example.demo.types.repository.TypeRepository;
@Service
public class TypeService {
private final TypeRepository repository;
public TypeService(TypeRepository repository){
this.repository = repository;
}
@Transactional(readOnly = true)
public List<TypeEntity> getAll(){
return StreamSupport.stream(repository.findAll().spliterator(), false).toList();
}
@Transactional(readOnly = true)
public TypeEntity get(Long id){
return repository.findById(id).orElseThrow(() -> new NotFoundException(TypeEntity.class, id));
}
@Transactional
public TypeEntity create(TypeEntity entity){
if(entity == null){
throw new IllegalArgumentException("Entity is null");
}
return repository.save(entity);
}
@Transactional
public TypeEntity update(Long id, TypeEntity entity){
final TypeEntity existsEntity = get(id);
existsEntity.setName(entity.getName());
return repository.save(existsEntity);
}
@Transactional
public TypeEntity delete(Long id){
final TypeEntity existsEntity = get(id);
repository.delete(existsEntity);
return existsEntity;
}
}

View File

@ -0,0 +1,114 @@
package com.example.demo.users.api;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import com.example.demo.core.configuration.Constants;
import com.example.demo.core.security.UserPrincipal;
import com.example.demo.core.session.SessionCart;
import com.example.demo.products.api.ProductDto;
import com.example.demo.products.model.ProductEntity;
import com.example.demo.products.service.ProductService;
import com.example.demo.orders.model.OrderEntity;
import com.example.demo.orders.service.OrderService;
import jakarta.validation.Valid;
@Controller
@RequestMapping(UserCartController.URL)
@SessionAttributes("products")
public class UserCartController {
public static final String URL = "/cart";
private static final String ORDER_VIEW = "cart";
private static final String ORDER_ATTRIBUTE = "order";
private static final String CART_ATTRIBUTE = "cart";
private final ProductService productService;
private final OrderService orderService;
private final SessionCart cart;
public UserCartController(
ProductService productService,
OrderService orderService,
SessionCart cart) {
this.productService = productService;
this.orderService = orderService;
this.cart = cart;
}
private ProductDto toProductDto(ProductEntity entity) {
var dto = new ProductDto();
dto.setId(entity.getId());
dto.setDescription(entity.getDescription());
dto.setName(entity.getName());
dto.setPrice(entity.getPrice());
dto.setTypeId(entity.getType().getId());
return dto;
}
private OrderEntity toOrderEntities(Collection<UserCartDto> dtos) {
final Set<Long> productIds = dtos.stream()
.map(UserCartDto::getProduct)
.collect(Collectors.toSet());
final List<ProductEntity> products = productService.getByIds(productIds);
return new OrderEntity(products);
}
@GetMapping
public String getCart(Model model) {
model.addAttribute("products",
productService.getAll(0).stream()
.map(this::toProductDto)
.toList());
model.addAttribute(ORDER_ATTRIBUTE, new UserCartDto());
model.addAttribute(CART_ATTRIBUTE, cart.values());
return ORDER_VIEW;
}
@PostMapping
public String addOrderToCart(
@ModelAttribute(name = ORDER_ATTRIBUTE) @Valid UserCartDto order,
BindingResult bindingResult,
SessionStatus status,
Model model) {
if (bindingResult.hasErrors()) {
return ORDER_VIEW;
}
status.setComplete();
order.setProductName(productService.get(order.getProduct()).getName());
order.setPrice(productService.get(order.getProduct()).getPrice());
cart.computeIfPresent(order.hashCode(), (key, value) -> {
return value;
});
cart.put(order.hashCode(), order);
return Constants.REDIRECT_VIEW + URL;
}
@PostMapping("/save")
public String saveCart(
Model model,
@AuthenticationPrincipal UserPrincipal principal) {
orderService.create(principal.getId(), toOrderEntities(cart.values()));
cart.clear();
return Constants.REDIRECT_VIEW + URL;
}
@PostMapping("/clear")
public String clearCart() {
cart.clear();
return Constants.REDIRECT_VIEW + URL;
}
}

View File

@ -0,0 +1,53 @@
package com.example.demo.users.api;
import java.util.Objects;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
public class UserCartDto {
@NotNull
private Long product;
private String productName;
@Min(1000)
private Double price;
public Long getProduct() {
return product;
}
public void setProduct(Long productId) {
this.product = productId;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
@Override
public int hashCode() {
return Objects.hash(product, price);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
UserCartDto other = (UserCartDto) obj;
return Objects.equals(product, other.product) && Objects.equals(price, other.price);
}
}

View File

@ -0,0 +1,98 @@
package com.example.demo.users.api;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.support.SessionStatus;
import org.modelmapper.ModelMapper;
import com.example.demo.core.api.PageAttributesMapper;
import com.example.demo.core.configuration.Constants;
import com.example.demo.core.session.SessionCart;
import com.example.demo.products.api.ProductDto;
import com.example.demo.products.model.ProductEntity;
import com.example.demo.products.service.ProductService;
import com.example.demo.types.api.TypeDto;
import com.example.demo.types.model.TypeEntity;
import com.example.demo.types.service.TypeService;
import jakarta.validation.Valid;
@Controller
@RequestMapping(UserCatalogController.URL)
public class UserCatalogController {
private static final String CATALOG_VIEW = "catalog";
public static final String URL = "/catalog";
private static final String TYPEID_ATTRIBUTE = "typeId";
private static final String PAGE_ATTRIBUTE = "page";
private static final String ORDER_ATTRIBUTE = "order";
private final ProductService productService;
private final TypeService typeService;
private final ModelMapper modelMapper;
private final SessionCart cart;
public UserCatalogController(ProductService productService, SessionCart cart, TypeService typeService,
ModelMapper modelMapper) {
this.productService = productService;
this.modelMapper = modelMapper;
this.typeService = typeService;
this.cart = cart;
}
private ProductDto toProductDto(ProductEntity entity) {
var dto = new ProductDto();
dto.setId(entity.getId());
dto.setDescription(entity.getDescription());
dto.setName(entity.getName());
dto.setPrice(entity.getPrice());
dto.setTypeId(entity.getType().getId());
return dto;
}
private TypeDto toTypeDto(TypeEntity entity) {
return modelMapper.map(entity, TypeDto.class);
}
@GetMapping
public String getCatalog(
@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page,
@RequestParam(name = TYPEID_ATTRIBUTE, defaultValue = "0") int typeId,
Model model) {
model.addAttribute(ORDER_ATTRIBUTE, new UserCartDto());
model.addAttribute(PAGE_ATTRIBUTE, page);
model.addAllAttributes(PageAttributesMapper.toAttributes(
productService.getAll(typeId, page, Constants.DEFAULT_PAGE_SIZE), this::toProductDto));
model.addAttribute("types",
typeService.getAll().stream()
.map(this::toTypeDto)
.toList());
return CATALOG_VIEW;
}
@PostMapping
public String addOrderToCart(
@ModelAttribute(name = ORDER_ATTRIBUTE) @Valid UserCartDto order,
BindingResult bindingResult,
SessionStatus status,
Model model) {
if (bindingResult.hasErrors()) {
return CATALOG_VIEW;
}
status.setComplete();
order.setProductName(productService.get(order.getProduct()).getName());
order.setPrice(productService.get(order.getProduct()).getPrice());
cart.computeIfPresent(order.hashCode(), (key, value) -> {
return value;
});
cart.put(order.hashCode(), order);
return Constants.REDIRECT_VIEW + URL;
}
}

View File

@ -0,0 +1,126 @@
package com.example.demo.users.api;
import java.util.Map;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.example.demo.core.api.PageAttributesMapper;
import com.example.demo.core.configuration.Constants;
import com.example.demo.orders.service.OrderService;
import com.example.demo.users.model.UserEntity;
import com.example.demo.users.service.UserService;
import jakarta.validation.Valid;
@Controller
@RequestMapping(UserController.URL)
public class UserController {
public static final String URL = Constants.ADMIN_PREFIX + "/user";
private static final String USER_VIEW = "user";
private static final String USER_EDIT_VIEW = "user-edit";
private static final String PAGE_ATTRIBUTE = "page";
private static final String USER_ATTRIBUTE = "user";
private final ModelMapper modelMapper;
private final UserService userService;
public UserController(OrderService orderService, ModelMapper modelMapper, UserService userService) {
this.modelMapper = modelMapper;
this.userService = userService;
}
private UserDto toDto(UserEntity entity) {
return modelMapper.map(entity, UserDto.class);
}
private UserEntity toEntity(UserDto dto) {
return modelMapper.map(dto, UserEntity.class);
}
@GetMapping
public String getAll(
@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page,
Model model) {
final Map<String, Object> attributes = PageAttributesMapper.toAttributes(
userService.getAll(page, Constants.DEFAULT_PAGE_SIZE), this::toDto);
model.addAllAttributes(attributes);
model.addAttribute(PAGE_ATTRIBUTE, page);
return USER_VIEW;
}
@GetMapping("/edit/")
public String create(
@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page,
Model model) {
model.addAttribute(USER_ATTRIBUTE, new UserDto());
model.addAttribute(PAGE_ATTRIBUTE, page);
return USER_EDIT_VIEW;
}
@PostMapping("/edit/")
public String create(
@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page,
@ModelAttribute(name = USER_ATTRIBUTE) @Valid UserDto user,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
model.addAttribute(PAGE_ATTRIBUTE, page);
return USER_EDIT_VIEW;
}
redirectAttributes.addAttribute(PAGE_ATTRIBUTE, page);
userService.create(toEntity(user));
return Constants.REDIRECT_VIEW + URL;
}
@GetMapping("/edit/{id}")
public String update(
@PathVariable(name = "id") Long id,
@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page,
Model model) {
if (id <= 0) {
throw new IllegalArgumentException();
}
model.addAttribute(USER_ATTRIBUTE, toDto(userService.get(id)));
model.addAttribute(PAGE_ATTRIBUTE, page);
return USER_EDIT_VIEW;
}
@PostMapping("/edit/{id}")
public String update(
@PathVariable(name = "id") Long id,
@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page,
@ModelAttribute(name = USER_ATTRIBUTE) @Valid UserDto user,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
model.addAttribute(PAGE_ATTRIBUTE, page);
return USER_EDIT_VIEW;
}
if (id <= 0) {
throw new IllegalArgumentException();
}
redirectAttributes.addAttribute(PAGE_ATTRIBUTE, page);
userService.update(id, toEntity(user));
return Constants.REDIRECT_VIEW + URL;
}
@PostMapping("/delete/{id}")
public UserDto delete(@PathVariable(name = "id") Long id) {
return toDto(userService.delete(id));
}
}

View File

@ -0,0 +1,47 @@
package com.example.demo.users.api;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class UserDto {
private Long id;
@NotBlank
@Size(min = 2, max = 20)
private String login;
@NotBlank
@Size(min = 2, max = 20)
private String email;
private String role;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}

View File

@ -0,0 +1,82 @@
package com.example.demo.users.api;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.example.demo.core.api.PageAttributesMapper;
import com.example.demo.core.configuration.Constants;
import com.example.demo.core.security.UserPrincipal;
import com.example.demo.products.api.ProductDto;
import com.example.demo.products.model.ProductEntity;
import com.example.demo.products.service.ProductService;
import com.example.demo.orders.api.OrderDto;
import com.example.demo.orders.model.OrderEntity;
import com.example.demo.orders.service.OrderService;
@Controller
public class UserProfileController {
private static final String PROFILE_VIEW = "profile";
private static final String PAGE_ATTRIBUTE = "page";
private final OrderService orderService;
private final ProductService productService;
public UserProfileController(
OrderService orderService,
ProductService productService) {
this.orderService = orderService;
this.productService = productService;
}
private OrderDto toDto(OrderEntity entity) {
var dto = new OrderDto();
dto.setId(entity.getId());
dto.setProducts(entity.getProducts().stream().map(ProductEntity::getId).toList());
return dto;
}
private ProductDto toProductDto(ProductEntity entity) {
var dto = new ProductDto();
dto.setId(entity.getId());
dto.setDescription(entity.getDescription());
dto.setName(entity.getName());
dto.setPrice(entity.getPrice());
dto.setTypeId(entity.getType().getId());
return dto;
}
@GetMapping
public String getProfile(
@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page,
Model model,
@AuthenticationPrincipal UserPrincipal principal) {
final long userId = principal.getId();
model.addAttribute(PAGE_ATTRIBUTE, page);
model.addAllAttributes(PageAttributesMapper.toAttributes(
orderService.getAll(userId, page, Constants.DEFAULT_PAGE_SIZE),
this::toDto));
model.addAttribute("products",
productService.getAll(0).stream()
.map(this::toProductDto)
.toList());
return PROFILE_VIEW;
}
@PostMapping("/delete/{id}")
public String deleteOrder(
@PathVariable(name = "id") Long id,
@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page,
RedirectAttributes redirectAttributes,
@AuthenticationPrincipal UserPrincipal principal) {
redirectAttributes.addAttribute(PAGE_ATTRIBUTE, page);
orderService.delete(principal.getId(), id);
return Constants.REDIRECT_VIEW + "/";
}
}

View File

@ -0,0 +1,63 @@
package com.example.demo.users.api;
import java.util.Objects;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.demo.core.configuration.Constants;
import com.example.demo.users.model.UserEntity;
import com.example.demo.users.service.UserService;
import jakarta.validation.Valid;
@Controller
@RequestMapping(UserSignupController.URL)
public class UserSignupController {
public static final String URL = "/signup";
private static final String SIGNUP_VIEW = "signup";
private static final String USER_ATTRIBUTE = "user";
private final UserService userService;
private final ModelMapper modelMapper;
public UserSignupController(
UserService userService,
ModelMapper modelMapper) {
this.userService = userService;
this.modelMapper = modelMapper;
}
private UserEntity toEntity(UserSignupDto dto) {
return modelMapper.map(dto, UserEntity.class);
}
@GetMapping
public String getSignup(Model model) {
model.addAttribute(USER_ATTRIBUTE, new UserSignupDto());
return SIGNUP_VIEW;
}
@PostMapping
public String signup(
@ModelAttribute(name = USER_ATTRIBUTE) @Valid UserSignupDto user,
BindingResult bindingResult,
Model model) {
if (bindingResult.hasErrors()) {
return SIGNUP_VIEW;
}
if (!Objects.equals(user.getPassword(), user.getPasswordConfirm())) {
bindingResult.rejectValue("password", "signup:passwords", "Пароли не совпадают.");
model.addAttribute(USER_ATTRIBUTE, user);
return SIGNUP_VIEW;
}
userService.create(toEntity(user));
return Constants.REDIRECT_VIEW + Constants.LOGIN_URL + "?signup";
}
}

View File

@ -0,0 +1,51 @@
package com.example.demo.users.api;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class UserSignupDto {
@NotBlank
@Size(min = 3, max = 20)
private String login;
@NotBlank
@Size(min = 3, max = 20)
private String email;
@NotBlank
@Size(min = 3, max = 20)
private String password;
@NotBlank
@Size(min = 3, max = 20)
private String passwordConfirm;
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPasswordConfirm() {
return passwordConfirm;
}
public void setPasswordConfirm(String passwordConfirm) {
this.passwordConfirm = passwordConfirm;
}
}

View File

@ -0,0 +1,79 @@
package com.example.demo.users.model;
import java.util.ArrayList;
import java.util.List;
import com.example.demo.core.model.BaseEntity;
import com.example.demo.orders.model.OrderEntity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderBy;
import jakarta.persistence.Table;
@Entity
@Table(name = "users")
public class UserEntity extends BaseEntity{
@Column(nullable = false, unique = true, length = 20)
private String login;
@Column(nullable = false, unique = true, length = 20)
private String email;
@Column(nullable = false, length = 60)
private String password;
private UserRole role;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
@OrderBy("id ASC")
private List<OrderEntity> orders = new ArrayList<>();
public UserEntity(){
}
public UserEntity(String login, String email, String password){
this.login = login;
this.email = email;
this.password = password;
this.role = UserRole.USER;
}
public String getLogin(){
return login;
}
public void setLogin(String login){
this.login = login;
}
public String getEmail(){
return email;
}
public void setEmail(String email){
this.email = email;
}
public String getPassword(){
return password;
}
public void setPassword(String password){
this.password = password;
}
public UserRole getRole() {
return role;
}
public void setRole(UserRole role) {
this.role = role;
}
public List<OrderEntity> getOrders(){
return orders;
}
public void addOrder(OrderEntity order){
if(order.getUser() != this){
order.setUser(this);
}
orders.add(order);
}
}

View File

@ -0,0 +1,15 @@
package com.example.demo.users.model;
import org.springframework.security.core.GrantedAuthority;
public enum UserRole implements GrantedAuthority{
ADMIN,
USER;
private static final String PREFIX = "ROLE_";
@Override
public String getAuthority() {
return PREFIX + this.name();
}
}

View File

@ -0,0 +1,12 @@
package com.example.demo.users.repository;
import java.util.Optional;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import com.example.demo.users.model.UserEntity;
public interface UserRepository extends CrudRepository<UserEntity, Long>, PagingAndSortingRepository<UserEntity, Long> {
Optional<UserEntity> findByLoginIgnoreCase(String login);
}

View File

@ -0,0 +1,103 @@
package com.example.demo.users.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import java.util.stream.StreamSupport;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.data.domain.PageRequest;
import org.springframework.util.StringUtils;
import com.example.demo.core.configuration.Constants;
import com.example.demo.core.error.NotFoundException;
import com.example.demo.core.security.UserPrincipal;
import com.example.demo.users.model.UserEntity;
import com.example.demo.users.model.UserRole;
import com.example.demo.users.repository.UserRepository;
@Service
public class UserService implements UserDetailsService {
private final UserRepository repository;
private final PasswordEncoder passwordEncoder;
public UserService(UserRepository repository, PasswordEncoder passwordEncoder) {
this.repository = repository;
this.passwordEncoder = passwordEncoder;
}
private void checkLogin(Long id, String login) {
final Optional<UserEntity> existsUser = repository.findByLoginIgnoreCase(login);
if (existsUser.isPresent() && !existsUser.get().getId().equals(id)) {
throw new IllegalArgumentException(
String.format("User with login %s is already exists", login));
}
}
@Transactional(readOnly = true)
public List<UserEntity> getAll() {
return StreamSupport.stream(repository.findAll().spliterator(), false).toList();
}
@Transactional(readOnly = true)
public Page<UserEntity> getAll(int page, int size) {
return repository.findAll(PageRequest.of(page, size, Sort.by("id")));
}
@Transactional(readOnly = true)
public UserEntity get(Long id) {
return repository.findById(id).orElseThrow(() -> new NotFoundException(UserEntity.class, id));
}
@Transactional(readOnly = true)
public UserEntity getByLogin(String login) {
return repository.findByLoginIgnoreCase(login)
.orElseThrow(() -> new IllegalArgumentException("Invalid login"));
}
@Transactional
public UserEntity create(UserEntity entity) {
if (entity == null) {
throw new IllegalArgumentException("Entity is null");
}
checkLogin(null, entity.getLogin());
final String password = Optional.ofNullable(entity.getPassword()).orElse("");
entity.setPassword(
passwordEncoder.encode(
StringUtils.hasText(password.strip()) ? password : Constants.DEFAULT_PASSWORD));
entity.setRole(Optional.ofNullable(entity.getRole()).orElse(UserRole.USER));
repository.save(entity);
return repository.save(entity);
}
@Transactional
public UserEntity update(Long id, UserEntity entity) {
final UserEntity existEntity = get(id);
checkLogin(id, entity.getLogin());
existEntity.setLogin(entity.getLogin());
existEntity.setEmail(entity.getEmail());
repository.save(existEntity);
return existEntity;
}
@Transactional
public UserEntity delete(Long id) {
final UserEntity existEntity = get(id);
repository.delete(existEntity);
return existEntity;
}
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
final UserEntity existsUser = getByLogin(username);
return new UserPrincipal(existsUser);
}
}

View File

@ -0,0 +1,20 @@
# Server
spring.main.banner-mode=off
server.port=8080
# Logger settings
# Available levels are: TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF
logging.level.com.example.demo=DEBUG
# JPA Settings
spring.datasource.url=jdbc:h2:file:./data
spring.datasource.username=sa
spring.datasource.password=password
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=create
spring.jpa.open-in-view=false
# spring.jpa.show-sql=true
# spring.jpa.properties.hibernate.format_sql=true
# H2 console
spring.h2.console.enabled=true

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-emoji-sunglasses-fill" viewBox="0 0 16 16">
<path
d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16M2.31 5.243A1 1 0 0 1 3.28 4H6a1 1 0 0 1 1 1v.116A4.2 4.2 0 0 1 8 5c.35 0 .69.04 1 .116V5a1 1 0 0 1 1-1h2.72a1 1 0 0 1 .97 1.243l-.311 1.242A2 2 0 0 1 11.439 8H11a2 2 0 0 1-1.994-1.839A3 3 0 0 0 8 6c-.393 0-.74.064-1.006.161A2 2 0 0 1 5 8h-.438a2 2 0 0 1-1.94-1.515zM4.969 9.75A3.5 3.5 0 0 0 8 11.5a3.5 3.5 0 0 0 3.032-1.75.5.5 0 1 1 .866.5A4.5 4.5 0 0 1 8 12.5a4.5 4.5 0 0 1-3.898-2.25.5.5 0 0 1 .866-.5z" />
</svg>

After

Width:  |  Height:  |  Size: 615 B

View File

@ -0,0 +1,67 @@
html,
body {
height: 100%;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.25em;
}
h3 {
font-size: 1.1em;
}
td form {
margin: 0;
padding: 0;
margin-top: -.25em;
}
.button-fixed-width {
width: 150px;
}
.button-link {
padding: 0;
}
.invalid-feedback {
display: block;
}
.w-10 {
width: 10% !important;
}
.my-navbar {
background-color: #3c3c3c !important;
}
.my-navbar .link a:hover {
text-decoration: underline;
}
.my-navbar .logo {
width: 26px;
height: 26px;
}
.my-footer {
background-color: #2c2c2c;
height: 32px;
color: rgba(255, 255, 255, 0.5);
}
.cart-image {
width: 3.1rem;
padding: 0.25rem;
border-radius: 0.5rem;
}
.cart-item {
height: auto;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Some files were not shown because too many files have changed in this diff Show More