Допка 5 лабы

This commit is contained in:
2025-11-28 11:40:33 +04:00
parent 67d3ee7916
commit d78269303f
18 changed files with 458 additions and 408 deletions

BIN
backend.zip Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -1,4 +0,0 @@
2025-11-14 20:25:48.248270+04:00 jdbc[3]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Таблица "DATABASECHANGELOGLOCK" не найдена
Table "DATABASECHANGELOGLOCK" not found; SQL statement:
SELECT COUNT(*) FROM PUBLIC.DATABASECHANGELOGLOCK [42102-224]

View File

@@ -10,14 +10,4 @@ public class SpaController {
public String index() {
return "forward:/index.html";
}
@GetMapping("/{path:^(?!api$)(?!v3$)(?!swagger-ui$).*$}")
public String any1() {
return "forward:/index.html";
}
@GetMapping("/{path:^(?!api$)(?!v3$)(?!swagger-ui$).*$}/**")
public String any2() {
return "forward:/index.html";
}
}

View File

@@ -1,16 +1,43 @@
package ru.ulstu.is.server.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public class SubscriptionRq {
@NotBlank
private String level;
@NotNull @Min(0)
@NotNull
@Min(0)
private Integer price;
public String getLevel() { return level; }
public void setLevel(String level) { this.level = level; }
public Integer getPrice() { return price; }
public void setPrice(Integer price) { this.price = price; }
}
@Min(0)
@Max(100)
private Integer discount;
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
public Integer getDiscount() {
return discount;
}
public void setDiscount(Integer discount) {
this.discount = discount;
}
}

View File

@@ -1,21 +1,62 @@
package ru.ulstu.is.server.dto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public class SubscriptionRs {
private Long id;
private String level;
private Integer price;
public SubscriptionRs() { }
public SubscriptionRs(Long id, String level, Integer price) {
this.id = id; this.level = level; this.price = price;
private Integer discount;
private Integer priceWithDiscount;
public SubscriptionRs() {
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getLevel() { return level; }
public void setLevel(String level) { this.level = level; }
public Integer getPrice() { return price; }
public void setPrice(Integer price) { this.price = price; }
}
public SubscriptionRs(Long id, String level, Integer price,
Integer discount, Integer priceWithDiscount) {
this.id = id;
this.level = level;
this.price = price;
this.discount = discount;
this.priceWithDiscount = priceWithDiscount;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
public Integer getDiscount() {
return discount;
}
public void setDiscount(Integer discount) {
this.discount = discount;
}
public Integer getPriceWithDiscount() {
return priceWithDiscount;
}
public void setPriceWithDiscount(Integer priceWithDiscount) {
this.priceWithDiscount = priceWithDiscount;
}
}

View File

@@ -1,9 +1,13 @@
package ru.ulstu.is.server.entity;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
@Entity
@Table(name = "subscriptions")
public class Subscription extends BaseEntity {
@@ -17,7 +21,11 @@ public class Subscription extends BaseEntity {
@OneToMany(mappedBy = "subscription", orphanRemoval = false)
private List<User> users = new ArrayList<>();
public Subscription() { }
@Column(nullable = false)
private Integer discount = 0;
public Subscription() {
}
public Subscription(Long id, String level, Integer price) {
super(id);
@@ -26,22 +34,48 @@ public class Subscription extends BaseEntity {
}
public void addUser(User u) {
if (u == null) return;
if (u == null) {
return;
}
users.add(u);
u.setSubscription(this);
}
public Integer getDiscount() {
return discount;
}
public void setDiscount(Integer discount) {
this.discount = discount != null ? discount : 0;
}
public void removeUser(User u) {
if (u == null) return;
if (u == null) {
return;
}
users.remove(u);
if (u.getSubscription() == this) {
u.setSubscription(null);
}
}
public String getLevel() { return level; }
public void setLevel(String level) { this.level = level; }
public Integer getPrice() { return price; }
public void setPrice(Integer price) { this.price = price; }
public List<User> getUsers() { return users; }
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
public List<User> getUsers() {
return users;
}
}

View File

@@ -1,23 +1,46 @@
package ru.ulstu.is.server.mapper;
import org.springframework.stereotype.Component;
import ru.ulstu.is.server.dto.SubscriptionRq;
import ru.ulstu.is.server.dto.SubscriptionRs;
import ru.ulstu.is.server.dto.SubscriptionRq;
import ru.ulstu.is.server.entity.Subscription;
@Component
public class SubscriptionMapper {
public Subscription toEntity(SubscriptionRq rq) {
Subscription s = new Subscription();
s.setLevel(rq.getLevel());
s.setPrice(rq.getPrice());
s.setDiscount(rq.getDiscount() != null ? rq.getDiscount() : 0);
return s;
}
public void updateEntity(Subscription s, SubscriptionRq rq) {
s.setLevel(rq.getLevel());
s.setPrice(rq.getPrice());
s.setDiscount(rq.getDiscount() != null ? rq.getDiscount() : 0);
}
public SubscriptionRs toRs(Subscription s) {
return new SubscriptionRs(s.getId(), s.getLevel(), s.getPrice());
int price = s.getPrice() != null ? s.getPrice() : 0;
int discount = s.getDiscount() != null ? s.getDiscount() : 0;
if (discount < 0) {
discount = 0;
}
if (discount > 100) {
discount = 100;
}
int priceWithDiscount = Math.round(price * (100 - discount) / 100.0f);
return new SubscriptionRs(
s.getId(),
s.getLevel(),
price,
discount,
priceWithDiscount
);
}
}

View File

@@ -1,20 +1,32 @@
package ru.ulstu.is.server.mvc;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import java.util.List;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import ru.ulstu.is.server.dto.*;
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 jakarta.validation.Valid;
import ru.ulstu.is.server.dto.DirectorRefRq;
import ru.ulstu.is.server.dto.DirectorRs;
import ru.ulstu.is.server.dto.GenreRefRq;
import ru.ulstu.is.server.dto.GenreRs;
import ru.ulstu.is.server.dto.MovieRq;
import ru.ulstu.is.server.dto.MovieRs;
import ru.ulstu.is.server.dto.PageHelper;
import ru.ulstu.is.server.dto.PageRs;
import ru.ulstu.is.server.service.DirectorService;
import ru.ulstu.is.server.service.GenreService;
import ru.ulstu.is.server.service.MovieService;
import ru.ulstu.is.server.session.MovieSearchSession;
import java.util.List;
@Controller
@RequestMapping("/movies")
public class MovieMvcController {
@@ -25,16 +37,15 @@ public class MovieMvcController {
private final MovieSearchSession movieSearchSession;
public MovieMvcController(MovieService movieService,
GenreService genreService,
DirectorService directorService,
MovieSearchSession movieSearchSession) {
GenreService genreService,
DirectorService directorService,
MovieSearchSession movieSearchSession) {
this.movieService = movieService;
this.genreService = genreService;
this.directorService = directorService;
this.movieSearchSession = movieSearchSession;
}
@GetMapping
public String listMovies(
@RequestParam(value = "page", required = false) Integer page,
@@ -42,15 +53,18 @@ public class MovieMvcController {
@RequestParam(required = false) String title,
@RequestParam(required = false) Long genreId,
@RequestParam(required = false) Float minGrade,
@RequestParam(required = false) Boolean exact,
Model model
) {
int pageNumber = (page == null || page < 1) ? 1 : page;
int pageSize = (size == null || size < 1) ? 12 : size;
Boolean exactMatch = exact;
boolean filtersPassedInRequest =
(title != null && !title.isBlank())
|| genreId != null
|| minGrade != null;
boolean filtersPassedInRequest
= (title != null && !title.isBlank())
|| genreId != null
|| minGrade != null
|| exact != null;
if (!filtersPassedInRequest) {
if (movieSearchSession.getLastTitle() != null) {
@@ -65,22 +79,27 @@ public class MovieMvcController {
if (movieSearchSession.getLastPageSize() != null) {
pageSize = movieSearchSession.getLastPageSize();
}
if (movieSearchSession.getLastExactMatch() != null) {
exactMatch = movieSearchSession.getLastExactMatch();
}
} else {
movieSearchSession.setLastTitle(title);
movieSearchSession.setLastGenreId(genreId);
movieSearchSession.setLastMinGrade(minGrade);
movieSearchSession.setLastExactMatch(exactMatch);
}
movieSearchSession.setLastPageSize(pageSize);
boolean hasFilters =
(title != null && !title.isBlank())
|| genreId != null
|| minGrade != null;
boolean hasFilters
= (title != null && !title.isBlank())
|| genreId != null
|| minGrade != null
|| Boolean.TRUE.equals(exactMatch);
PageRs<MovieRs> pageRs;
if (hasFilters) {
pageRs = movieService.search(title, genreId, minGrade, pageNumber, pageSize);
pageRs = movieService.search(title, genreId, minGrade, exactMatch, pageNumber, pageSize);
} else {
Pageable pageable = PageHelper.toPageable(pageNumber, pageSize);
pageRs = movieService.getPage(pageable);
@@ -95,11 +114,11 @@ public class MovieMvcController {
model.addAttribute("titleFilter", title);
model.addAttribute("genreIdFilter", genreId);
model.addAttribute("minGradeFilter", minGrade);
model.addAttribute("exactMatchFilter", Boolean.TRUE.equals(exactMatch));
return "movies/list";
}
@GetMapping("/new")
public String showCreateForm(Model model) {
MovieRq movieRq = new MovieRq();

View File

@@ -50,23 +50,34 @@ public class MovieService {
}
@Transactional(readOnly = true)
public PageRs<MovieRs> search(String title, Long genreId, Float minGrade, int page, int size) {
public PageRs<MovieRs> search(String title,
Long genreId,
Float minGrade,
Boolean exactMatch,
int page,
int size) {
List<MovieRs> all = getAll();
var stream = all.stream();
if (title != null && !title.isBlank()) {
String lower = title.toLowerCase();
stream = stream.filter(m ->
m.getTitle() != null && m.getTitle().toLowerCase().contains(lower)
);
if (Boolean.TRUE.equals(exactMatch)) {
String t = title;
stream = stream.filter(m
-> m.getTitle() != null && m.getTitle().equalsIgnoreCase(t)
);
} else {
String lower = title.toLowerCase();
stream = stream.filter(m
-> m.getTitle() != null && m.getTitle().toLowerCase().contains(lower)
);
}
}
if (genreId != null) {
stream = stream.filter(m ->
m.getGenre() != null
&& m.getGenre().getId() != null
&& m.getGenre().getId().equals(genreId)
stream = stream.filter(m
-> m.getGenre() != null
&& m.getGenre().getId() != null
&& m.getGenre().getId().equals(genreId)
);
}
@@ -114,7 +125,6 @@ public class MovieService {
);
}
@Transactional(readOnly = true)
public MovieRs get(Long id) {
Movie m = movies.findById(id).orElseThrow(() -> new NotFoundException(ru.ulstu.is.server.entity.Movie.class, "movie " + id + " not found"));

View File

@@ -7,40 +7,49 @@ import org.springframework.web.context.annotation.SessionScope;
@SessionScope
public class MovieSearchSession {
private String lastTitle;
private Long lastGenreId;
private Float lastMinGrade;
private Integer lastPageSize;
private String lastTitle;
private Long lastGenreId;
private Float lastMinGrade;
private Integer lastPageSize;
private Boolean lastExactMatch;
public String getLastTitle() {
return lastTitle;
}
public String getLastTitle() {
return lastTitle;
}
public void setLastTitle(String lastTitle) {
this.lastTitle = lastTitle;
}
public void setLastTitle(String lastTitle) {
this.lastTitle = lastTitle;
}
public Long getLastGenreId() {
return lastGenreId;
}
public Long getLastGenreId() {
return lastGenreId;
}
public void setLastGenreId(Long lastGenreId) {
this.lastGenreId = lastGenreId;
}
public void setLastGenreId(Long lastGenreId) {
this.lastGenreId = lastGenreId;
}
public Float getLastMinGrade() {
return lastMinGrade;
}
public Float getLastMinGrade() {
return lastMinGrade;
}
public void setLastMinGrade(Float lastMinGrade) {
this.lastMinGrade = lastMinGrade;
}
public void setLastMinGrade(Float lastMinGrade) {
this.lastMinGrade = lastMinGrade;
}
public Integer getLastPageSize() {
return lastPageSize;
}
public Integer getLastPageSize() {
return lastPageSize;
}
public void setLastPageSize(Integer lastPageSize) {
this.lastPageSize = lastPageSize;
}
public void setLastPageSize(Integer lastPageSize) {
this.lastPageSize = lastPageSize;
}
public Boolean getLastExactMatch() {
return lastExactMatch;
}
public void setLastExactMatch(Boolean lastExactMatch) {
this.lastExactMatch = lastExactMatch;
}
}

View File

@@ -0,0 +1,14 @@
databaseChangeLog:
- changeSet:
id: 4-add-subscription-discount
author: busla
changes:
- addColumn:
tableName: SUBSCRIPTIONS
columns:
- column:
name: DISCOUNT
type: INT
defaultValueNumeric: 0
constraints:
nullable: false

View File

@@ -4,4 +4,7 @@ databaseChangeLog:
relativeToChangelogFile: true
- include:
file: changeset-3-add-review.yaml
relativeToChangelogFile: true
- include:
file: changeset-4-add-subscription-discount.yaml
relativeToChangelogFile: true

View File

@@ -1,314 +1,175 @@
body {
font-family: Arial, sans-serif;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #181818;
color: #fff;
text-align: center;
}
nav a {
.navbar-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.8);
transition: color 0.3s ease;
}
.card-title{
color: white;
}
.navbar-dark .navbar-nav .nav-link:hover,
.navbar-dark .navbar-nav .nav-link:focus {
color: #FF0033;
}
.navbar-dark .navbar-nav .nav-link.active {
color: #fff;
margin: 10px;
text-decoration: none;
font-weight: 600;
}
nav a:hover {
color: #FF0033;
}
.movie-list {
display: flex;
justify-content: space-around;
width: 500px;
flex-wrap: wrap;
margin: 0 auto;
}
.movie-list a {
color: #fff;
}
.movie-list img {
width: 150px;
height: auto;
}
table {
margin: auto;
/* Стили для карточек фильмов */
.movie-card {
background-color: #282828;
color: #fff;
border: 1px solid #404040;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.user_photo {
height: 130px;
width: 130px;
border-radius: 50px;
.movie-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 15px rgba(255, 0, 51, 0.2);
}
.movie-card img {
width: 100%;
height: 260px;
object-fit: cover;
}
#changeInfo {
font-size: 20px;
padding: 10px;
background-color: gray;
cursor: pointer;
}
#current_page {
color: red;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #181818;
color: #fff;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #202020;
}
.logo {
display: flex;
align-items: center;
}
.logo img {
height: 40px;
margin-right: 10px;
}
nav ul {
display: flex;
list-style: none;
}
nav ul li {
position: relative;
margin: 0 15px;
}
nav ul li a {
color: #fff;
text-decoration: none;
padding: 10px;
}
.dropdown-menu {
display: none;
position: absolute;
background-color: #333;
list-style: none;
top: 100%;
left: -40px;
min-width: 150px;
}
.dropdown-menu li {
padding: 5px 10px;
}
.dropdown-movies:hover .dropdown-menu {
display: block;
}
.user-actions a {
color: #fff;
text-decoration: none;
margin-left: 10px;
}
.content {
padding: 20px;
text-align: center;
}
footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #202020;
}
.social-links img {
height: 30px;
margin: 0 5px;
}
html, body {
height: 100%;
margin: 0;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
footer {
background-color: #202020;
padding: 10px 20px;
text-align: center;
}
.form-container {
width: 300px;
margin: 50px auto;
padding: 20px;
/* Стили для таблиц */
.table-dark {
background-color: #282828;
border-radius: 10px;
text-align: center;
border-color: #404040;
}
.form-container h2 {
margin-bottom: 20px;
}
.form-container input {
width: 100%;
padding: 10px;
margin: 10px 0;
border: none;
border-radius: 5px;
}
.form-container button {
width: 100%;
padding: 10px;
background-color: red;
border: none;
color: white;
font-size: 16px;
border-radius: 5px;
cursor: pointer;
}
.form-container button:hover {
background-color: darkred;
}
.form-container a {
color: red;
text-decoration: none;
}
.user-menu {
position: relative;
display: inline-block;
.table-dark th,
.table-dark td {
border-color: #404040;
vertical-align: middle;
}
.account-btn {
color: #fff;
text-decoration: none;
padding: 10px;
}
.user-menu:hover .dropdown {
display: block;
}
.dropdown {
display: none;
position: absolute;
/* Стили для форм */
.form-control,
.form-select {
background-color: #333;
list-style: none;
padding: 5px;
left: -22px;
margin: 0;
border-radius: 5px;
width: 120px;
border: 1px solid #555;
color: #fff;
}
.dropdown li {
padding: 8px;
text-align: center;
}
.dropdown li a {
color: #fff;
text-decoration: none;
display: block;
}
.dropdown li a:hover {
background-color: red;
}
@media (max-width: 1024px) {
header {
flex-direction: row;
justify-content: space-between;
padding: 10px;
}
nav ul {
flex-direction: row;
}
.movie-list {
max-width: 90%;
}
.form-control:focus,
.form-select:focus {
background-color: #3a3a3a;
border-color: #FF0033;
color: #fff;
box-shadow: 0 0 0 0.25rem rgba(255, 0, 51, 0.25);
}
.form-label {
color: #e9ecef;
font-weight: 500;
}
/* Стили для кнопок */
.btn-primary {
background-color: #FF0033;
border-color: #FF0033;
}
.btn-primary:hover {
background-color: #cc0029;
border-color: #cc0029;
}
.btn-outline-light:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* Стили для пагинации */
.page-link {
background-color: #282828;
border-color: #404040;
color: #fff;
}
.page-link:hover {
background-color: #333;
border-color: #555;
color: #fff;
}
.page-item.active .page-link {
background-color: #FF0033;
border-color: #FF0033;
}
/* Стили для пользовательского фото */
.user-photo {
height: 130px;
width: 130px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #FF0033;
}
/* Стили для выпадающего меню пользователя */
.dropdown-menu-dark {
background-color: #282828;
border: 1px solid #404040;
}
.dropdown-item {
color: rgba(255, 255, 255, 0.8);
transition: background-color 0.2s ease;
}
.dropdown-item:hover {
background-color: #FF0033;
color: #fff;
}
/* Стили для футера */
footer {
background-color: #202020;
border-top: 1px solid #404040;
}
/* Кастомные утилиты */
.bg-custom-dark {
background-color: #181818 !important;
}
.bg-custom-gray {
background-color: #282828 !important;
}
.border-custom {
border-color: #404040 !important;
}
/* Адаптивность */
@media (max-width: 768px) {
header {
flex-direction: column;
text-align: center;
.movie-card img {
height: 200px;
}
nav ul {
flex-direction: column;
padding: 10px;
}
.movie-list {
flex-direction: column;
align-items: center;
}
footer {
flex-direction: column;
text-align: center;
}
.user-menu {
display: block;
position: static;
background-color: transparent;
text-align: center;
}
.user-menu li {
display: block;
}
.dropdown:hover .user-menu {
display: block;
.user-photo {
height: 100px;
width: 100px;
}
}
@media (max-width: 480px) {
.side-menu {
width: 200px;
@media (max-width: 576px) {
.container {
padding-left: 15px;
padding-right: 15px;
}
.movie-list img {
width: 120px;
.movie-card {
margin-bottom: 1rem;
}
footer {
padding: 5px;
}
}
}

View File

@@ -4,24 +4,19 @@
<meta charset="UTF-8">
<title>Онлайн-кинотеатр</title>
<!-- Bootstrap + иконки + твой CSS, как в фронте -->
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" th:href="@{/css/style.css}">
<link rel="stylesheet" th:href="@{../css/style.css}">
</head>
<body class="bg-dark text-light">
<!-- ====== HEADER ====== -->
<header th:fragment="siteHeader">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark border-bottom border-secondary">
<div class="container">
<!-- Лого -->
<a class="navbar-brand d-flex align-items-center gap-2" th:href="@{/}">
<img th:src="@{/img/logo.png}" alt="Логотип" width="40"/>
<!-- Если хочешь можно убрать текст -->
<span class="fw-semibold">Cinema</span>
</a>
@@ -36,7 +31,6 @@
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<!-- Левые ссылки -->
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" th:href="@{/}">Главная</a>
@@ -49,7 +43,6 @@
</li>
</ul>
<!-- Правая часть (пока статично, без useAuth) -->
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" th:href="@{/login}">Войти</a>
@@ -57,14 +50,12 @@
<li class="nav-item">
<a class="nav-link" th:href="@{/register}">Регистрация</a>
</li>
<!-- потом можем сделать дропдаун "Профиль" по данным из сессии -->
</ul>
</div>
</div>
</nav>
</header>
<!-- ====== FOOTER ====== -->
<footer th:fragment="siteFooter" class="bg-dark text-light mt-4 py-3 border-top border-secondary">
<div class="container text-center small">
<p class="mb-1">Контакты: cinemaSupport@gmail.com</p>
@@ -73,7 +64,6 @@
</div>
</footer>
<!-- Bootstrap JS (для бургер-меню и дропдаунов) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -11,7 +11,8 @@
</head>
<body class="bg-dark text-light">
<div th:insert="layout :: siteHeader"></div>
<div th:replace="layout :: siteHeader"></div>
<main class="container my-4">
<div class="d-flex justify-content-between align-items-center mb-3">
@@ -57,6 +58,19 @@
<input type="hidden" name="page" value="1">
</div>
<div class="col-md-2 d-flex align-items-end">
<div class="form-check">
<input class="form-check-input"
type="checkbox"
name="exact"
th:checked="${exactMatchFilter}">
<label class="form-check-label text-light">
Точный поиск
</label>
</div>
</div>
<div class="col-md-1 d-grid">
<button type="submit" class="btn btn-success">OK</button>
</div>

View File

@@ -53,13 +53,28 @@
<div class="mb-3">
<label class="form-label">Цена (₽)</label>
<input type="number"
min="0"
th:field="*{price}"
class="form-control bg-secondary text-light border-0"/>
min="0"
th:field="*{price}"
class="form-control bg-secondary text-light border-0"/>
<div class="text-danger mt-1"
th:if="${#fields.hasErrors('price')}"
th:errors="*{price}">
th:if="${#fields.hasErrors('price')}"
th:errors="*{price}">
</div>
</div>
<!-- 🔹 Скидка -->
<div class="mb-3">
<label class="form-label">Скидка (%)</label>
<input type="number"
min="0" max="100"
th:field="*{discount}"
class="form-control bg-secondary text-light border-0"
placeholder="0"/>
<div class="text-danger mt-1"
th:if="${#fields.hasErrors('discount')}"
th:errors="*{discount}">
</div>
</div>

View File

@@ -28,10 +28,12 @@
<table class="table table-dark table-hover mb-0 align-middle">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Уровень</th>
<th scope="col">Цена (₽)</th>
<th scope="col" class="text-end">Действия</th>
<th>ID</th>
<th>Уровень</th>
<th>Цена, ₽</th>
<th>Скидка, %</th>
<th>Цена со скидкой, ₽</th>
<th class="text-end">Действия</th>
</tr>
</thead>
<tbody>
@@ -39,6 +41,8 @@
<td th:text="${sub.id}">1</td>
<td th:text="${sub.level}">Базовая</td>
<td th:text="${sub.price}">299</td>
<td th:text="${sub.discount}">0</td>
<td th:text="${sub.priceWithDiscount}">299</td>
<td class="text-end">
<a th:href="@{|/subscriptions/${sub.id}/edit|}"
class="btn btn-sm btn-outline-light me-2">