Поиск, права админ клиент, почта, пароль от 8 символов

This commit is contained in:
Erman 2026-09-09 05:29:20 +05:00
parent f09fa1ecc9
commit 33a8506f9b
17 changed files with 631 additions and 24 deletions

View File

@ -18,6 +18,22 @@ public class AuthController : ControllerBase
_context = context;
}
// простая проверка почты: есть @ и точка после нее
private static bool IsEmailOk(string email)
{
if (string.IsNullOrWhiteSpace(email))
return false; // пустая не годится
int at = email.IndexOf('@'); // ищу собаку
if (at < 1)
return false; // @ нет или стоит первой
int dot = email.IndexOf('.', at); // ищу точку после @
return dot > at + 1 && dot < email.Length - 1; // точка не сразу и не в конце
}
// войти, адрес POST api/auth/login
[HttpPost("login")]
public async Task<IActionResult> Login(User user) // приходят логин и пароль
@ -38,21 +54,43 @@ public class AuthController : ControllerBase
{
existingUser.Id,
existingUser.Login,
existingUser.Email,
Role = existingUser.Role?.Name
});
}
// регистрация, адрес POST api/auth/register
[HttpPost("register")]
public async Task<IActionResult> Register(User user) // приходят логин и пароль
public async Task<IActionResult> Register(User user) // приходят логин пароль и почта
{
// чищу пробелы
user.Login = (user.Login ?? "").Trim();
user.Email = (user.Email ?? "").Trim();
// проверки по порядку, чтоб человек понял что не так
if (user.Login.Length < 2)
return BadRequest("Логин слишком короткий, надо минимум 2 буквы.");
if (user.Password == null || user.Password.Length < 8)
return BadRequest("Пароль слишком короткий, надо минимум 8 символов.");
if (!IsEmailOk(user.Email))
return BadRequest("Почта неправильная, проверьте ее.");
// проверяю нет ли уже такого логина
var exists = await _context.Users
var loginExists = await _context.Users
.AnyAsync(u => u.Login == user.Login);
if (exists)
if (loginExists)
return BadRequest("Такой логин уже занят.");
// проверяю не занята ли почта
var emailExists = await _context.Users
.AnyAsync(u => u.Email == user.Email);
if (emailExists)
return BadRequest("Такая почта уже зарегистрирована.");
// ищу роль клиент чтоб дать ее новичку
var clientRole = await _context.Roles
.FirstOrDefaultAsync(r => r.Name == "Client");
@ -70,6 +108,7 @@ public class AuthController : ControllerBase
{
user.Id,
user.Login,
user.Email,
Role = clientRole.Name
});
}

View File

@ -30,6 +30,11 @@ public class CategoriesController : ControllerBase
[HttpPost]
public async Task<IActionResult> CreateCategory(Category category) // приходит название
{
category.Name = (category.Name ?? "").Trim(); // убираю пробелы
if (category.Name.Length < 2)
return BadRequest("Название слишком короткое, надо минимум 2 буквы.");
_context.Categories.Add(category); // добавляю
await _context.SaveChangesAsync(); // сохраняю
return Ok(category);
@ -44,6 +49,11 @@ public class CategoriesController : ControllerBase
if (existingCategory == null)
return NotFound(); // нет такой
category.Name = (category.Name ?? "").Trim();
if (category.Name.Length < 2)
return BadRequest("Название слишком короткое, надо минимум 2 буквы.");
existingCategory.Name = category.Name; // меняю название
await _context.SaveChangesAsync(); // сохраняю

View File

@ -35,6 +35,12 @@ public class OrderItemsController : ControllerBase
[HttpPost]
public async Task<IActionResult> CreateOrderItem(OrderItem orderItem) // приходит заказ товар колво цена
{
if (orderItem.Quantity <= 0)
return BadRequest("Количество должно быть больше нуля.");
if (orderItem.Price <= 0)
return BadRequest("Цена должна быть больше нуля.");
_context.OrderItems.Add(orderItem); // добавляю
await _context.SaveChangesAsync(); // сохраняю
return Ok(orderItem);
@ -49,6 +55,12 @@ public class OrderItemsController : ControllerBase
if (existingItem == null)
return NotFound();
if (orderItem.Quantity <= 0)
return BadRequest("Количество должно быть больше нуля.");
if (orderItem.Price <= 0)
return BadRequest("Цена должна быть больше нуля.");
// меняю поля
existingItem.OrderId = orderItem.OrderId;
existingItem.ProductId = orderItem.ProductId;

View File

@ -30,10 +30,16 @@ public class OrdersController : ControllerBase
return Ok(orders);
}
// какие статусы можно ставить
private static readonly string[] OkStatuses = ["New", "Processing", "Completed", "Cancelled"];
// добавить заказ
[HttpPost]
public async Task<IActionResult> CreateOrder(Order order) // приходит человек дата статус
{
if (!OkStatuses.Contains(order.Status))
return BadRequest("Такого статуса нет.");
_context.Orders.Add(order); // добавляю
await _context.SaveChangesAsync(); // сохраняю
return Ok(order);
@ -48,6 +54,9 @@ public class OrdersController : ControllerBase
if (existingOrder == null)
return NotFound();
if (!OkStatuses.Contains(order.Status))
return BadRequest("Такого статуса нет.");
// меняю поля
existingOrder.UserId = order.UserId;
existingOrder.Date = order.Date;

View File

@ -34,6 +34,14 @@ public class ProductsController : ControllerBase
[HttpPost]
public async Task<IActionResult> CreateProduct(Product product) // приходит товар
{
product.Name = (product.Name ?? "").Trim(); // убираю пробелы
if (product.Name.Length < 2)
return BadRequest("Название слишком короткое, надо минимум 2 буквы.");
if (product.Price <= 0)
return BadRequest("Цена должна быть больше нуля.");
// проверяю есть ли такая категория
var categoryExists = await _context.Categories
.AnyAsync(c => c.Id == product.CategoryId);
@ -57,6 +65,14 @@ public class ProductsController : ControllerBase
if (existingProduct == null)
return NotFound(); // нет такого
product.Name = (product.Name ?? "").Trim();
if (product.Name.Length < 2)
return BadRequest("Название слишком короткое, надо минимум 2 буквы.");
if (product.Price <= 0)
return BadRequest("Цена должна быть больше нуля.");
// меняю поля
existingProduct.Name = product.Name;
existingProduct.Price = product.Price;

View File

@ -35,6 +35,9 @@ public class StocksController : ControllerBase
[HttpPost]
public async Task<IActionResult> CreateStock(Stock stock) // приходит товар склад и количество
{
if (stock.Quantity < 0)
return BadRequest("Количество не может быть меньше нуля.");
_context.Stocks.Add(stock); // добавляю
await _context.SaveChangesAsync(); // сохраняю
return Ok(stock);
@ -49,6 +52,9 @@ public class StocksController : ControllerBase
if (existingStock == null)
return NotFound();
if (stock.Quantity < 0)
return BadRequest("Количество не может быть меньше нуля.");
// меняю поля
existingStock.ProductId = stock.ProductId;
existingStock.WarehouseId = stock.WarehouseId;

View File

@ -18,6 +18,54 @@ public class UsersController : ControllerBase
_context = context;
}
// простая проверка почты: есть @ и точка после нее
private static bool IsEmailOk(string email)
{
if (string.IsNullOrWhiteSpace(email))
return false; // пустая не годится
int at = email.IndexOf('@'); // ищу собаку
if (at < 1)
return false; // @ нет или стоит первой
int dot = email.IndexOf('.', at); // ищу точку после @
return dot > at + 1 && dot < email.Length - 1; // точка не сразу и не в конце
}
// проверяю человека перед сохранением, возвращаю текст ошибки или пусто
private async Task<string> CheckUser(User user, int skipId)
{
user.Login = (user.Login ?? "").Trim(); // убираю пробелы
user.Email = (user.Email ?? "").Trim();
if (user.Login.Length < 2)
return "Логин слишком короткий, надо минимум 2 буквы.";
if (user.Password == null || user.Password.Length < 8)
return "Пароль слишком короткий, надо минимум 8 символов.";
if (!IsEmailOk(user.Email))
return "Почта неправильная, проверьте ее.";
// логин должен быть только у одного (себя не считаю)
bool loginBusy = await _context.Users
.AnyAsync(u => u.Login == user.Login && u.Id != skipId);
if (loginBusy)
return "Такой логин уже занят.";
// почта тоже только у одного
bool emailBusy = await _context.Users
.AnyAsync(u => u.Email == user.Email && u.Id != skipId);
if (emailBusy)
return "Такая почта уже зарегистрирована.";
return ""; // все хорошо
}
// отдать всех людей
[HttpGet]
public async Task<IActionResult> GetUsers()
@ -32,8 +80,13 @@ public class UsersController : ControllerBase
// добавить человека
[HttpPost]
public async Task<IActionResult> CreateUser(User user) // приходит логин пароль роль
public async Task<IActionResult> CreateUser(User user) // приходит логин пароль почта роль
{
string error = await CheckUser(user, 0); // проверяю, 0 значит нового
if (error != "")
return BadRequest(error); // что-то не так, говорю что
_context.Users.Add(user); // добавляю
await _context.SaveChangesAsync(); // сохраняю
return Ok(user);
@ -48,9 +101,15 @@ public class UsersController : ControllerBase
if (existingUser == null)
return NotFound();
string error = await CheckUser(user, id); // проверяю, себя пропускаю
if (error != "")
return BadRequest(error);
// меняю поля
existingUser.Login = user.Login;
existingUser.Password = user.Password;
existingUser.Email = user.Email;
existingUser.RoleId = user.RoleId;
await _context.SaveChangesAsync(); // сохраняю

View File

@ -30,6 +30,15 @@ public class WarehousesController : ControllerBase
[HttpPost]
public async Task<IActionResult> CreateWarehouse(Warehouse warehouse) // приходит название и адрес
{
warehouse.Name = (warehouse.Name ?? "").Trim();
warehouse.Address = (warehouse.Address ?? "").Trim();
if (warehouse.Name.Length < 2)
return BadRequest("Название склада слишком короткое.");
if (warehouse.Address.Length < 3)
return BadRequest("Адрес слишком короткий.");
_context.Warehouses.Add(warehouse); // добавляю
await _context.SaveChangesAsync(); // сохраняю
return Ok(warehouse);
@ -44,6 +53,15 @@ public class WarehousesController : ControllerBase
if (existingWarehouse == null)
return NotFound();
warehouse.Name = (warehouse.Name ?? "").Trim();
warehouse.Address = (warehouse.Address ?? "").Trim();
if (warehouse.Name.Length < 2)
return BadRequest("Название склада слишком короткое.");
if (warehouse.Address.Length < 3)
return BadRequest("Адрес слишком короткий.");
// меняю поля
existingWarehouse.Name = warehouse.Name;
existingWarehouse.Address = warehouse.Address;

BIN
DEMO/DemoDB.db-shm Normal file

Binary file not shown.

BIN
DEMO/DemoDB.db-wal Normal file

Binary file not shown.

View File

@ -0,0 +1,270 @@
// <auto-generated />
using System;
using DEMO.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace DEMO.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260909001244_AddUserEmail")]
partial class AddUserEmail
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.12");
modelBuilder.Entity("DEMO.Models.Category", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Categories");
});
modelBuilder.Entity("DEMO.Models.Order", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Date")
.HasColumnType("TEXT");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Orders");
});
modelBuilder.Entity("DEMO.Models.OrderItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("OrderId")
.HasColumnType("INTEGER");
b.Property<decimal>("Price")
.HasColumnType("TEXT");
b.Property<int>("ProductId")
.HasColumnType("INTEGER");
b.Property<int>("Quantity")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("OrderId");
b.HasIndex("ProductId");
b.ToTable("OrderItems");
});
modelBuilder.Entity("DEMO.Models.Product", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<decimal>("Price")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.ToTable("Products");
});
modelBuilder.Entity("DEMO.Models.Role", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Roles");
});
modelBuilder.Entity("DEMO.Models.Stock", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ProductId")
.HasColumnType("INTEGER");
b.Property<int>("Quantity")
.HasColumnType("INTEGER");
b.Property<int>("WarehouseId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ProductId");
b.HasIndex("WarehouseId");
b.ToTable("Stocks");
});
modelBuilder.Entity("DEMO.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Login")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("Users");
});
modelBuilder.Entity("DEMO.Models.Warehouse", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Address")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Warehouses");
});
modelBuilder.Entity("DEMO.Models.Order", b =>
{
b.HasOne("DEMO.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("DEMO.Models.OrderItem", b =>
{
b.HasOne("DEMO.Models.Order", "Order")
.WithMany()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DEMO.Models.Product", "Product")
.WithMany()
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Order");
b.Navigation("Product");
});
modelBuilder.Entity("DEMO.Models.Product", b =>
{
b.HasOne("DEMO.Models.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Category");
});
modelBuilder.Entity("DEMO.Models.Stock", b =>
{
b.HasOne("DEMO.Models.Product", "Product")
.WithMany()
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DEMO.Models.Warehouse", "Warehouse")
.WithMany()
.HasForeignKey("WarehouseId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Product");
b.Navigation("Warehouse");
});
modelBuilder.Entity("DEMO.Models.User", b =>
{
b.HasOne("DEMO.Models.Role", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DEMO.Migrations
{
/// <inheritdoc />
public partial class AddUserEmail : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Email",
table: "Users",
type: "TEXT",
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Email",
table: "Users");
}
}
}

View File

@ -150,6 +150,10 @@ namespace DEMO.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Login")
.IsRequired()
.HasColumnType("TEXT");

View File

@ -9,6 +9,8 @@ public class User
public string Password { get; set; } = string.Empty; // пароль
public string Email { get; set; } = string.Empty; // почта
public int RoleId { get; set; } // какая роль (номер)
public Role? Role { get; set; } // сама роль (подтянется)

View File

@ -72,6 +72,11 @@
<input id="registerPasswordRepeat" type="password" placeholder="Еще раз пароль" required>
</label>
<label>
Почта
<input id="registerEmail" type="email" placeholder="Ваша почта" required>
</label>
<!-- сюда пишу ошибку регистрации -->
<div id="registerError" class="auth-error"></div>
@ -128,6 +133,9 @@
<p id="pageDescription">Управление товарами магазина</p>
</div>
<!-- поиск по странице -->
<input id="searchInput" class="search-input" type="text" placeholder="Поиск...">
<!-- кнопка добавить новое -->
<button id="addButton" class="primary-button">+ Добавить</button>
</header>

View File

@ -29,6 +29,7 @@ const pageTitle = document.getElementById("pageTitle");
const pageDescription = document.getElementById("pageDescription");
const pageContent = document.getElementById("pageContent");
const addButton = document.getElementById("addButton");
const searchInput = document.getElementById("searchInput"); // поле поиска
// всплывающее окно
const modal = document.getElementById("modal");
@ -55,6 +56,29 @@ let currentPage = "products";
let editingId = null;
// кто вошел в систему
let currentAuthUser = null;
// я админ или нет (у админа больше кнопок)
let isAdmin = false;
// данные текущей страницы, чтоб искать по ним
let currentData = [];
// проверяю админ ли я, если нет то ругаюсь и не пускаю
function needAdmin() {
if (isAdmin) {
return true;
}
showToast("Только админ может это делать.");
return false;
}
// проверяю почту: есть @ и точка после нее
function isEmailOk(email) {
const at = email.indexOf("@"); // ищу собаку
if (at < 1) {
return false;
}
const dot = email.indexOf(".", at); // ищу точку после @
return dot > at + 1 && dot < email.length - 1;
}
// показываю сообщение снизу на 2 секунды
function showToast(message) {
@ -145,10 +169,13 @@ function showRegisterForm() {
// захожу в программу после входа
function enterApplication(user) {
currentAuthUser = user; // запоминаю кто вошел
isAdmin = user.role === "Admin"; // проверяю админ ли я
sessionStorage.setItem("demoUser", JSON.stringify(user)); // сохраняю чтоб не выкидывало
currentUser.textContent = user.login; // пишу логин
currentRole.textContent = user.role || "Client"; // пишу роль
userAvatar.textContent = user.login ? user.login.charAt(0).toUpperCase() : "U"; // первая буква в кружок
// кнопку Пользователи показываю только админу
document.querySelector('.nav-button[data-page="users"]').classList.toggle("hidden", !isAdmin);
authScreen.classList.add("hidden"); // прячу вход
app.classList.remove("hidden"); // показываю программу
showPage("products"); // открываю товары
@ -194,8 +221,9 @@ registerForm.addEventListener("submit", async (event) => {
const login = document.getElementById("registerLogin").value.trim();
const password = document.getElementById("registerPassword").value;
const passwordRepeat = document.getElementById("registerPasswordRepeat").value;
const email = document.getElementById("registerEmail").value.trim();
// простые проверки
if (!login || !password || !passwordRepeat) {
if (!login || !password || !passwordRepeat || !email) {
registerError.textContent = "Заполните все поля.";
return;
}
@ -203,8 +231,12 @@ registerForm.addEventListener("submit", async (event) => {
registerError.textContent = "Логин слишком короткий.";
return;
}
if (password.length < 4) {
registerError.textContent = "Пароль слишком короткий.";
if (password.length < 8) {
registerError.textContent = "Пароль должен быть минимум 8 символов.";
return;
}
if (!isEmailOk(email)) {
registerError.textContent = "Почта неправильная, проверьте ее.";
return;
}
if (password !== passwordRepeat) {
@ -214,7 +246,7 @@ registerForm.addEventListener("submit", async (event) => {
try {
await api("/api/auth/register", {
method: "POST",
body: JSON.stringify({ login, password, roleId: 2 })
body: JSON.stringify({ login, password, email, roleId: 2 })
});
registerForm.reset();
showLoginForm(); // иду на вход
@ -242,6 +274,13 @@ document.querySelectorAll(".nav-button").forEach((button) => {
// открываю страницу (товары, заказы и тд)
async function showPage(page) {
// в пользователи пускаю только админа
if (page === "users" && !isAdmin) {
pageTitle.textContent = "Пользователи";
pageDescription.textContent = "Все люди в системе";
pageContent.innerHTML = "<div class='empty-page'><strong>Нет прав</strong><span>Только админ видит эту страницу</span></div>";
return;
}
currentPage = page; // запоминаю где я
const config = pages[page];
if (!config) {
@ -249,15 +288,35 @@ async function showPage(page) {
}
pageTitle.textContent = config.title; // пишу название
pageDescription.textContent = config.description;
// кнопку добавить показываю админу везде, а обычному только в заказах
addButton.classList.toggle("hidden", !(isAdmin || page === "orders"));
searchInput.value = ""; // чищу поиск
pageContent.innerHTML = "<div class='empty-page'><strong>Загрузка...</strong></div>"; // пока грузится
try {
const data = await api(config.endpoint); // беру данные с сервера
currentData = data; // запоминаю чтоб искать по ним
renderPage(page, data); // рисую
} catch (error) {
pageContent.innerHTML = "<div class='error-page'>Не получилось: " + escapeHtml(error.message) + "</div>";
}
}
// ищу по странице когда печатаю в поиск
searchInput.addEventListener("input", () => {
const q = searchInput.value.trim().toLowerCase(); // что ищу
if (!q) {
renderPage(currentPage, currentData); // пусто показываю все
return;
}
// ищу совпадение где угодно (название, логин, адрес и тд)
const found = currentData.filter((item) => JSON.stringify(item).toLowerCase().includes(q));
if (found.length === 0) {
pageContent.innerHTML = "<div class='empty-page'><strong>Ничего не нашлось</strong></div>";
return;
}
renderPage(currentPage, found); // рисую найденное
});
// выбираю что рисовать
function renderPage(page, data) {
// если пусто то так и пишу
@ -294,10 +353,14 @@ function renderProducts(products) {
html += "<h3>" + escapeHtml(p.name) + "</h3>";
html += "<div class=\"card-price\">" + formatMoney(p.price) + "</div>";
html += "<div class=\"card-info\">Категория: <strong>" + escapeHtml(p.category ? p.category.name : "—") + "</strong></div>";
// кнопки показываю только админу
if (isAdmin) {
html += "<div class=\"card-actions\">";
html += "<button class=\"action-button\" onclick=\"editProduct(" + p.id + ")\">Изменить</button>";
html += "<button class=\"action-button delete\" onclick=\"deleteItem('products'," + p.id + ")\">Удалить</button>";
html += "</div></div>";
html += "</div>";
}
html += "</div>";
}
html += "</div>";
pageContent.innerHTML = html; // показываю все
@ -308,8 +371,11 @@ function renderCategories(categories) {
let html = "<div class=\"table-wrapper\"><table><thead><tr><th>ID</th><th>Название</th><th>Действия</th></tr></thead><tbody>";
for (const c of categories) {
html += "<tr><td>#" + c.id + "</td><td>" + escapeHtml(c.name) + "</td><td><div class=\"table-actions\">";
// кнопки только для админа
if (isAdmin) {
html += "<button class=\"action-button\" onclick=\"editCategory(" + c.id + ")\">Изменить</button>";
html += "<button class=\"action-button delete\" onclick=\"deleteItem('categories'," + c.id + ")\">Удалить</button>";
}
html += "</div></td></tr>";
}
html += "</tbody></table></div>";
@ -321,8 +387,11 @@ function renderWarehouses(warehouses) {
let html = "<div class=\"table-wrapper\"><table><thead><tr><th>ID</th><th>Название</th><th>Адрес</th><th>Действия</th></tr></thead><tbody>";
for (const w of warehouses) {
html += "<tr><td>#" + w.id + "</td><td>" + escapeHtml(w.name) + "</td><td>" + escapeHtml(w.address) + "</td><td><div class=\"table-actions\">";
// кнопки только для админа
if (isAdmin) {
html += "<button class=\"action-button\" onclick=\"editWarehouse(" + w.id + ")\">Изменить</button>";
html += "<button class=\"action-button delete\" onclick=\"deleteItem('warehouses'," + w.id + ")\">Удалить</button>";
}
html += "</div></td></tr>";
}
html += "</tbody></table></div>";
@ -336,8 +405,11 @@ function renderStock(stocks) {
const pname = s.product ? s.product.name : "—";
const wname = s.warehouse ? s.warehouse.name : "—";
html += "<tr><td>#" + s.id + "</td><td>" + escapeHtml(pname) + "</td><td>" + escapeHtml(wname) + "</td><td>" + s.quantity + "</td><td><div class=\"table-actions\">";
// кнопки только для админа
if (isAdmin) {
html += "<button class=\"action-button\" onclick=\"editStock(" + s.id + ")\">Изменить</button>";
html += "<button class=\"action-button delete\" onclick=\"deleteItem('stock'," + s.id + ")\">Удалить</button>";
}
html += "</div></td></tr>";
}
html += "</tbody></table></div>";
@ -346,10 +418,15 @@ function renderStock(stocks) {
// рисую пользователей
function renderUsers(users) {
let html = "<div class=\"table-wrapper\"><table><thead><tr><th>ID</th><th>Логин</th><th>Роль</th><th>Действия</th></tr></thead><tbody>";
// сюда пускаю только админа (на всякий случай)
if (!isAdmin) {
pageContent.innerHTML = "<div class='empty-page'><strong>Нет прав</strong></div>";
return;
}
let html = "<div class=\"table-wrapper\"><table><thead><tr><th>ID</th><th>Логин</th><th>Почта</th><th>Роль</th><th>Действия</th></tr></thead><tbody>";
for (const u of users) {
const rname = u.role ? u.role.name : "—";
html += "<tr><td>#" + u.id + "</td><td>" + escapeHtml(u.login) + "</td><td><span class=\"status\">" + escapeHtml(rname) + "</span></td><td><div class=\"table-actions\">";
html += "<tr><td>#" + u.id + "</td><td>" + escapeHtml(u.login) + "</td><td>" + escapeHtml(u.email || "—") + "</td><td><span class=\"status\">" + escapeHtml(rname) + "</span></td><td><div class=\"table-actions\">";
html += "<button class=\"action-button\" onclick=\"editUser(" + u.id + ")\">Изменить</button>";
html += "<button class=\"action-button delete\" onclick=\"deleteItem('users'," + u.id + ")\">Удалить</button>";
html += "</div></td></tr>";
@ -365,8 +442,11 @@ function renderOrders(orders) {
const uname = o.user ? o.user.login : "—";
html += "<tr><td>#" + o.id + "</td><td>" + escapeHtml(uname) + "</td><td>" + formatDate(o.date) + "</td><td><span class=\"status\">" + escapeHtml(o.status) + "</span></td><td><div class=\"table-actions\">";
html += "<button class=\"action-button\" onclick=\"showOrderItems(" + o.id + ")\">Товары</button>";
// менять и удалять заказы может только админ, смотреть товары могут все
if (isAdmin) {
html += "<button class=\"action-button\" onclick=\"editOrder(" + o.id + ")\">Изменить</button>";
html += "<button class=\"action-button delete\" onclick=\"deleteItem('orders'," + o.id + ")\">Удалить</button>";
}
html += "</div></td></tr>";
}
html += "</tbody></table></div>";
@ -376,6 +456,10 @@ function renderOrders(orders) {
// кнопка Добавить сверху
addButton.addEventListener("click", () => {
editingId = null; // значит добавляю а не изменяю
// заказы создавать можно всем, остальное только админу
if (currentPage !== "orders" && !needAdmin()) {
return;
}
if (currentPage === "products") {
openProductForm();
}
@ -466,6 +550,9 @@ async function saveProduct() {
// жму изменить на товаре
async function editProduct(id) {
if (!needAdmin()) { // только админ
return;
}
const products = await api("/api/products"); // беру все
const product = products.find((x) => x.id === id); // ищу нужный
if (!product) {
@ -511,6 +598,9 @@ async function saveCategory() {
// жму изменить на категории
async function editCategory(id) {
if (!needAdmin()) { // только админ
return;
}
const categories = await api("/api/categories");
const category = categories.find((x) => x.id === id);
if (!category) {
@ -560,6 +650,9 @@ async function saveWarehouse() {
// жму изменить на складе
async function editWarehouse(id) {
if (!needAdmin()) { // только админ
return;
}
const warehouses = await api("/api/warehouses");
const warehouse = warehouses.find((x) => x.id === id);
if (!warehouse) {
@ -622,6 +715,9 @@ async function saveStock() {
// жму изменить на остатке
async function editStock(id) {
if (!needAdmin()) { // только админ
return;
}
const stocks = await api("/api/stocks");
const stock = stocks.find((x) => x.id === id);
if (!stock) {
@ -642,10 +738,12 @@ async function openUserForm(user = null) {
const title = user ? "Изменить пользователя" : "Добавить пользователя";
const login = user ? escapeHtml(user.login) : "";
const password = user ? escapeHtml(user.password) : "";
const email = user ? escapeHtml(user.email || "") : "";
openModal(title,
"<div class=\"form\">" +
"<label>Логин<input id=\"userLogin\" type=\"text\" value=\"" + login + "\"></label>" +
"<label>Пароль<input id=\"userPassword\" type=\"text\" value=\"" + password + "\"></label>" +
"<label>Почта<input id=\"userEmail\" type=\"email\" value=\"" + email + "\"></label>" +
"<label>Роль<select id=\"userRole\">" + options + "</select></label>" +
"<div class=\"form-actions\"><button type=\"button\" class=\"secondary-button\" onclick=\"closeModalWindow()\">Отмена</button>" +
"<button type=\"button\" class=\"primary-button\" onclick=\"saveUser()\">Сохранить</button></div></div>"
@ -656,13 +754,22 @@ async function openUserForm(user = null) {
async function saveUser() {
const login = document.getElementById("userLogin").value.trim();
const password = document.getElementById("userPassword").value;
const email = document.getElementById("userEmail").value.trim();
const roleId = Number(document.getElementById("userRole").value);
if (!login || !password) {
if (!login || !password || !email) {
showToast("Заполните все поля.");
return;
}
if (password.length < 8) {
showToast("Пароль должен быть минимум 8 символов.");
return;
}
if (!isEmailOk(email)) {
showToast("Почта неправильная, проверьте ее.");
return;
}
try {
const data = { login, password, roleId };
const data = { login, password, email, roleId };
if (editingId) {
await api("/api/users/" + editingId, { method: "PUT", body: JSON.stringify(data) });
showToast("Пользователь изменен.");
@ -679,6 +786,9 @@ async function saveUser() {
// жму изменить на пользователе
async function editUser(id) {
if (!needAdmin()) { // только админ
return;
}
const users = await api("/api/users");
const user = users.find((x) => x.id === id);
if (!user) {
@ -743,6 +853,9 @@ async function saveOrder() {
// жму изменить на заказе
async function editOrder(id) {
if (!needAdmin()) { // только админ
return;
}
const orders = await api("/api/orders");
const order = orders.find((x) => x.id === id);
if (!order) {
@ -820,6 +933,9 @@ async function saveOrderItem(orderId) {
// удаляю что угодно (товар, заказ и тд)
async function deleteItem(type, id) {
if (!needAdmin()) { // удалять может только админ
return;
}
// красивые названия для вопроса
const names = {
products: "товар",

View File

@ -235,6 +235,15 @@ button {
margin-top: 5px;
}
/* поле поиска */
.search-input {
height: 44px;
padding: 0 12px;
border: 1px solid #ccc;
border-radius: 8px;
width: 220px;
}
/* синяя кнопка добавить */
.primary-button {
padding: 10px 16px;