// нахожу экран входа и главное окно const authScreen = document.getElementById("authScreen"); const app = document.getElementById("app"); // нахожу формы входа и регистрации const loginFormContainer = document.getElementById("loginFormContainer"); const registerFormContainer = document.getElementById("registerFormContainer"); const loginForm = document.getElementById("loginForm"); const registerForm = document.getElementById("registerForm"); // сюда пишу ошибки const loginError = document.getElementById("loginError"); const registerError = document.getElementById("registerError"); // кнопки переключения вход / регистрация const showRegisterButton = document.getElementById("showRegister"); const showLoginButton = document.getElementById("showLogin"); // кнопка выйти const logoutButton = document.getElementById("logoutButton"); // где показываю кто вошел const currentUser = document.getElementById("currentUser"); const currentRole = document.getElementById("currentRole"); const userAvatar = document.getElementById("userAvatar"); // заголовок страницы и место для таблицы 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"); const modalTitle = document.getElementById("modalTitle"); const modalForm = document.getElementById("modalForm"); const closeModal = document.getElementById("closeModal"); // маленькое сообщение снизу const toast = document.getElementById("toast"); // список всех страниц и откуда брать данные const pages = { products: { title: "Товары", description: "Все товары магазина", endpoint: "/api/products" }, categories: { title: "Категории", description: "Категории товаров", endpoint: "/api/categories" }, warehouses: { title: "Склады", description: "Все склады", endpoint: "/api/warehouses" }, stock: { title: "Остатки", description: "Что лежит на складах", endpoint: "/api/stocks" }, users: { title: "Пользователи", description: "Все люди в системе", endpoint: "/api/users" }, orders: { title: "Заказы", description: "Все заказы", endpoint: "/api/orders" } }; // какая страница открыта сейчас let currentPage = "products"; // кого изменяю (если null значит добавляю нового) 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) { toast.textContent = message; // пишу текст toast.classList.add("show"); // показываю setTimeout(() => { toast.classList.remove("show"); // прячу обратно }, 2500); } // защита от плохих символов чтоб не ломали страницу function escapeHtml(value) { if (value === null || value === undefined) { return ""; } return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } // делаю цену красивой: 1000 -> 1 000,00 ₽ function formatMoney(value) { return Number(value).toLocaleString("ru-RU", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + " ₽"; } // делаю дату понятной function formatDate(value) { if (!value) { return ""; } return new Date(value).toLocaleString("ru-RU"); } // одна функция для всех запросов на сервер async function api(url, options = {}) { // отправляю запрос const response = await fetch(url, { headers: { "Content-Type": "application/json" }, ...options }); // если ошибка то показываю что случилось if (!response.ok) { let message = "Ошибка."; const text = await response.text(); if (text) { try { const data = JSON.parse(text); message = data.title || data.message || text; } catch { message = text; } } throw new Error(message); } // если пусто то возвращаю null if (response.status === 204) { return null; } const text = await response.text(); if (!text) { return null; } return JSON.parse(text); // превращаю ответ в объект } // показываю форму входа function showLoginForm() { loginFormContainer.classList.remove("hidden"); registerFormContainer.classList.add("hidden"); loginError.textContent = ""; registerError.textContent = ""; } // показываю форму регистрации function showRegisterForm() { loginFormContainer.classList.add("hidden"); registerFormContainer.classList.remove("hidden"); loginError.textContent = ""; registerError.textContent = ""; } // захожу в программу после входа 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"); // открываю товары } // выхожу из аккаунта function logout() { sessionStorage.removeItem("demoUser"); // забываю пользователя currentAuthUser = null; app.classList.add("hidden"); // прячу программу authScreen.classList.remove("hidden"); // показываю вход loginForm.reset(); // чищу поля registerForm.reset(); showLoginForm(); } // когда жму войти loginForm.addEventListener("submit", async (event) => { event.preventDefault(); // чтоб страница не перезагружалась loginError.textContent = ""; const login = document.getElementById("login").value.trim(); const password = document.getElementById("password").value; if (!login || !password) { loginError.textContent = "Напишите логин и пароль."; return; } try { const user = await api("/api/auth/login", { method: "POST", body: JSON.stringify({ login, password }) }); enterApplication(user); // захожу loginForm.reset(); } catch (error) { loginError.textContent = error.message || "Не получилось войти."; } }); // когда жму зарегистрироваться registerForm.addEventListener("submit", async (event) => { event.preventDefault(); registerError.textContent = ""; 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 || !email) { registerError.textContent = "Заполните все поля."; return; } if (login.length < 2) { registerError.textContent = "Логин слишком короткий."; return; } if (password.length < 8) { registerError.textContent = "Пароль должен быть минимум 8 символов."; return; } if (!isEmailOk(email)) { registerError.textContent = "Почта неправильная, проверьте ее."; return; } if (password !== passwordRepeat) { registerError.textContent = "Пароли не совпадают."; return; } try { await api("/api/auth/register", { method: "POST", body: JSON.stringify({ login, password, email, roleId: 2 }) }); registerForm.reset(); showLoginForm(); // иду на вход document.getElementById("login").value = login; loginError.textContent = "Готово. Теперь войдите."; } catch (error) { registerError.textContent = error.message || "Не получилось."; } }); // кнопки переключения showRegisterButton.addEventListener("click", showRegisterForm); showLoginButton.addEventListener("click", showLoginForm); logoutButton.addEventListener("click", logout); // кнопки меню слева document.querySelectorAll(".nav-button").forEach((button) => { button.addEventListener("click", () => { // убираю синий цвет у всех document.querySelectorAll(".nav-button").forEach((b) => b.classList.remove("active")); button.classList.add("active"); // крашу нажатую showPage(button.dataset.page); // открываю страницу }); }); // открываю страницу (товары, заказы и тд) async function showPage(page) { // в пользователи пускаю только админа if (page === "users" && !isAdmin) { pageTitle.textContent = "Пользователи"; pageDescription.textContent = "Все люди в системе"; pageContent.innerHTML = "
Нет правТолько админ видит эту страницу
"; return; } currentPage = page; // запоминаю где я const config = pages[page]; if (!config) { return; } pageTitle.textContent = config.title; // пишу название pageDescription.textContent = config.description; // кнопку добавить показываю админу везде, а обычному только в заказах addButton.classList.toggle("hidden", !(isAdmin || page === "orders")); searchInput.value = ""; // чищу поиск pageContent.innerHTML = "
Загрузка...
"; // пока грузится try { const data = await api(config.endpoint); // беру данные с сервера currentData = data; // запоминаю чтоб искать по ним renderPage(page, data); // рисую } catch (error) { pageContent.innerHTML = "
Не получилось: " + escapeHtml(error.message) + "
"; } } // ищу по странице когда печатаю в поиск 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 = "
Ничего не нашлось
"; return; } renderPage(currentPage, found); // рисую найденное }); // выбираю что рисовать function renderPage(page, data) { // если пусто то так и пишу if (!Array.isArray(data) || data.length === 0) { pageContent.innerHTML = "
Пока пустоНажмите Добавить
"; return; } if (page === "products") { renderProducts(data); } if (page === "categories") { renderCategories(data); } if (page === "warehouses") { renderWarehouses(data); } if (page === "stock") { renderStock(data); } if (page === "users") { renderUsers(data); } if (page === "orders") { renderOrders(data); } } // рисую товары карточками function renderProducts(products) { let html = "
"; // начало сетки for (const p of products) { html += "
"; html += "
#" + p.id + "
"; html += "

" + escapeHtml(p.name) + "

"; html += "
" + formatMoney(p.price) + "
"; html += "
Категория: " + escapeHtml(p.category ? p.category.name : "—") + "
"; // кнопки показываю только админу if (isAdmin) { html += "
"; html += ""; html += ""; html += "
"; } html += "
"; } html += "
"; pageContent.innerHTML = html; // показываю все } // рисую категории таблицей function renderCategories(categories) { let html = "
"; for (const c of categories) { html += ""; } html += "
IDНазваниеДействия
#" + c.id + "" + escapeHtml(c.name) + "
"; // кнопки только для админа if (isAdmin) { html += ""; html += ""; } html += "
"; pageContent.innerHTML = html; } // рисую склады function renderWarehouses(warehouses) { let html = "
"; for (const w of warehouses) { html += ""; } html += "
IDНазваниеАдресДействия
#" + w.id + "" + escapeHtml(w.name) + "" + escapeHtml(w.address) + "
"; // кнопки только для админа if (isAdmin) { html += ""; html += ""; } html += "
"; pageContent.innerHTML = html; } // рисую остатки function renderStock(stocks) { let html = "
"; for (const s of stocks) { const pname = s.product ? s.product.name : "—"; const wname = s.warehouse ? s.warehouse.name : "—"; html += ""; } html += "
IDТоварСкладКол-воДействия
#" + s.id + "" + escapeHtml(pname) + "" + escapeHtml(wname) + "" + s.quantity + "
"; // кнопки только для админа if (isAdmin) { html += ""; html += ""; } html += "
"; pageContent.innerHTML = html; } // рисую пользователей function renderUsers(users) { // сюда пускаю только админа (на всякий случай) if (!isAdmin) { pageContent.innerHTML = "
Нет прав
"; return; } let html = "
"; for (const u of users) { const rname = u.role ? u.role.name : "—"; html += ""; } html += "
IDЛогинПочтаРольДействия
#" + u.id + "" + escapeHtml(u.login) + "" + escapeHtml(u.email || "—") + "" + escapeHtml(rname) + "
"; html += ""; html += ""; html += "
"; pageContent.innerHTML = html; } // рисую заказы function renderOrders(orders) { let html = "
"; for (const o of orders) { const uname = o.user ? o.user.login : "—"; html += ""; } html += "
IDКто заказалДатаСтатусДействия
#" + o.id + "" + escapeHtml(uname) + "" + formatDate(o.date) + "" + escapeHtml(o.status) + "
"; html += ""; // менять и удалять заказы может только админ, смотреть товары могут все if (isAdmin) { html += ""; html += ""; } html += "
"; pageContent.innerHTML = html; } // кнопка Добавить сверху addButton.addEventListener("click", () => { editingId = null; // значит добавляю а не изменяю // заказы создавать можно всем, остальное только админу if (currentPage !== "orders" && !needAdmin()) { return; } if (currentPage === "products") { openProductForm(); } if (currentPage === "categories") { openCategoryForm(); } if (currentPage === "warehouses") { openWarehouseForm(); } if (currentPage === "stock") { openStockForm(); } if (currentPage === "users") { openUserForm(); } if (currentPage === "orders") { openOrderForm(); } }); // открываю всплывающее окно function openModal(title, content) { modalTitle.textContent = title; // пишу заголовок modalForm.innerHTML = content; // рисую поля modal.classList.remove("hidden"); // показываю } // закрываю окно function closeModalWindow() { modal.classList.add("hidden"); // прячу modalForm.innerHTML = ""; // чищу editingId = null; // забываю кого изменял } // закрытие по крестику и по темному фону closeModal.addEventListener("click", closeModalWindow); document.querySelector(".modal-overlay").addEventListener("click", closeModalWindow); // форма товара async function openProductForm(product = null) { const categories = await api("/api/categories"); // беру категории для списка let options = ""; for (const c of categories) { const sel = product && product.categoryId === c.id ? "selected" : ""; options += ""; } const title = product ? "Изменить товар" : "Добавить товар"; const name = product ? escapeHtml(product.name) : ""; const price = product ? product.price : ""; openModal(title, "
" + "" + "" + "" + "
" + "
" ); } // сохраняю товар async function saveProduct() { const name = document.getElementById("productName").value.trim(); const price = Number(document.getElementById("productPrice").value); const categoryId = Number(document.getElementById("productCategory").value); if (!name || name.length < 2) { showToast("Название слишком короткое."); return; } if (!price || price <= 0) { showToast("Цена должна быть больше 0."); return; } try { const data = { name, price, categoryId }; if (editingId) { await api("/api/products/" + editingId, { method: "PUT", body: JSON.stringify(data) }); // изменяю showToast("Товар изменен."); } else { await api("/api/products", { method: "POST", body: JSON.stringify(data) }); // добавляю showToast("Товар добавлен."); } closeModalWindow(); showPage("products"); // обновляю страницу } catch (error) { showToast(error.message); } } // жму изменить на товаре async function editProduct(id) { if (!needAdmin()) { // только админ return; } const products = await api("/api/products"); // беру все const product = products.find((x) => x.id === id); // ищу нужный if (!product) { return; } editingId = id; // запоминаю кого меняю await openProductForm(product); } // форма категории function openCategoryForm(category = null) { const title = category ? "Изменить категорию" : "Добавить категорию"; const name = category ? escapeHtml(category.name) : ""; openModal(title, "
" + "" + "
" + "
" ); } // сохраняю категорию async function saveCategory() { const name = document.getElementById("categoryName").value.trim(); if (!name) { showToast("Напишите название."); return; } try { if (editingId) { await api("/api/categories/" + editingId, { method: "PUT", body: JSON.stringify({ name }) }); showToast("Категория изменена."); } else { await api("/api/categories", { method: "POST", body: JSON.stringify({ name }) }); showToast("Категория добавлена."); } closeModalWindow(); showPage("categories"); } catch (error) { showToast(error.message); } } // жму изменить на категории async function editCategory(id) { if (!needAdmin()) { // только админ return; } const categories = await api("/api/categories"); const category = categories.find((x) => x.id === id); if (!category) { return; } editingId = id; openCategoryForm(category); } // форма склада function openWarehouseForm(warehouse = null) { const title = warehouse ? "Изменить склад" : "Добавить склад"; const name = warehouse ? escapeHtml(warehouse.name) : ""; const address = warehouse ? escapeHtml(warehouse.address) : ""; openModal(title, "
" + "" + "" + "
" + "
" ); } // сохраняю склад async function saveWarehouse() { const name = document.getElementById("warehouseName").value.trim(); const address = document.getElementById("warehouseAddress").value.trim(); if (!name || !address) { showToast("Заполните все поля."); return; } try { const data = { name, address }; if (editingId) { await api("/api/warehouses/" + editingId, { method: "PUT", body: JSON.stringify(data) }); showToast("Склад изменен."); } else { await api("/api/warehouses", { method: "POST", body: JSON.stringify(data) }); showToast("Склад добавлен."); } closeModalWindow(); showPage("warehouses"); } catch (error) { showToast(error.message); } } // жму изменить на складе async function editWarehouse(id) { if (!needAdmin()) { // только админ return; } const warehouses = await api("/api/warehouses"); const warehouse = warehouses.find((x) => x.id === id); if (!warehouse) { return; } editingId = id; openWarehouseForm(warehouse); } // форма остатка async function openStockForm(stock = null) { const products = await api("/api/products"); // беру товары const warehouses = await api("/api/warehouses"); // беру склады let pOptions = ""; for (const p of products) { const sel = stock && stock.productId === p.id ? "selected" : ""; pOptions += ""; } let wOptions = ""; for (const w of warehouses) { const sel = stock && stock.warehouseId === w.id ? "selected" : ""; wOptions += ""; } const qty = stock ? stock.quantity : 0; const title = stock ? "Изменить остаток" : "Добавить остаток"; openModal(title, "
" + "" + "" + "" + "
" + "
" ); } // сохраняю остаток async function saveStock() { const productId = Number(document.getElementById("stockProduct").value); const warehouseId = Number(document.getElementById("stockWarehouse").value); const quantity = Number(document.getElementById("stockQuantity").value); if (quantity < 0) { showToast("Количество не может быть минусом."); return; } try { const data = { productId, warehouseId, quantity }; if (editingId) { await api("/api/stocks/" + editingId, { method: "PUT", body: JSON.stringify(data) }); showToast("Остаток изменен."); } else { await api("/api/stocks", { method: "POST", body: JSON.stringify(data) }); showToast("Остаток добавлен."); } closeModalWindow(); showPage("stock"); } catch (error) { showToast(error.message); } } // жму изменить на остатке async function editStock(id) { if (!needAdmin()) { // только админ return; } const stocks = await api("/api/stocks"); const stock = stocks.find((x) => x.id === id); if (!stock) { return; } editingId = id; await openStockForm(stock); } // форма пользователя async function openUserForm(user = null) { const roles = await api("/api/roles"); // беру роли let options = ""; for (const r of roles) { const sel = user && user.roleId === r.id ? "selected" : ""; options += ""; } const title = user ? "Изменить пользователя" : "Добавить пользователя"; const login = user ? escapeHtml(user.login) : ""; const password = user ? escapeHtml(user.password) : ""; const email = user ? escapeHtml(user.email || "") : ""; openModal(title, "
" + "" + "" + "" + "" + "
" + "
" ); } // сохраняю пользователя 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 || !email) { showToast("Заполните все поля."); return; } if (password.length < 8) { showToast("Пароль должен быть минимум 8 символов."); return; } if (!isEmailOk(email)) { showToast("Почта неправильная, проверьте ее."); return; } try { const data = { login, password, email, roleId }; if (editingId) { await api("/api/users/" + editingId, { method: "PUT", body: JSON.stringify(data) }); showToast("Пользователь изменен."); } else { await api("/api/users", { method: "POST", body: JSON.stringify(data) }); showToast("Пользователь добавлен."); } closeModalWindow(); showPage("users"); } catch (error) { showToast(error.message); } } // жму изменить на пользователе async function editUser(id) { if (!needAdmin()) { // только админ return; } const users = await api("/api/users"); const user = users.find((x) => x.id === id); if (!user) { return; } editingId = id; openUserForm(user); // тут без await специально, форма и так откроется } // форма заказа async function openOrderForm(order = null) { const users = await api("/api/users"); // беру людей для списка let options = ""; for (const u of users) { const sel = order && order.userId === u.id ? "selected" : ""; options += ""; } // дата для поля (просто сегодня или дата заказа) const dateValue = order ? new Date(order.date).toISOString().slice(0, 16) : new Date().toISOString().slice(0, 16); const title = order ? "Изменить заказ" : "Добавить заказ"; // какой статус выбран const st = order ? order.status : "New"; openModal(title, "
" + "" + "" + "" + "
" + "
" ); } // сохраняю заказ async function saveOrder() { const userId = Number(document.getElementById("orderUser").value); const date = document.getElementById("orderDate").value; const status = document.getElementById("orderStatus").value; if (!date) { showToast("Выберите дату."); return; } try { const data = { userId, date: new Date(date).toISOString(), status }; if (editingId) { await api("/api/orders/" + editingId, { method: "PUT", body: JSON.stringify(data) }); showToast("Заказ изменен."); } else { await api("/api/orders", { method: "POST", body: JSON.stringify(data) }); showToast("Заказ добавлен."); } closeModalWindow(); showPage("orders"); } catch (error) { showToast(error.message); } } // жму изменить на заказе async function editOrder(id) { if (!needAdmin()) { // только админ return; } const orders = await api("/api/orders"); const order = orders.find((x) => x.id === id); if (!order) { return; } editingId = id; await openOrderForm(order); } // показываю товары внутри заказа async function showOrderItems(orderId) { try { const items = await api("/api/orderitems"); // беру все позиции const products = await api("/api/products"); // беру товары чтоб узнать названия const orderItems = items.filter((i) => i.orderId === orderId); // оставляю только этот заказ let html = "
"; if (orderItems.length === 0) { html += "
Тут пока пусто
"; // если ничего нет } else { for (const item of orderItems) { const prod = products.find((x) => x.id === item.productId); // ищу название товара const pname = prod ? prod.name : "Товар"; html += "
" + escapeHtml(pname) + "Кол-во: " + item.quantity + "
"; html += "" + formatMoney(item.price) + "
"; } } html += "
"; openModal("Товары заказа #" + orderId, html); } catch (error) { showToast(error.message); } } // форма добавить товар в заказ async function openOrderItemForm(orderId) { const products = await api("/api/products"); let options = ""; for (const p of products) { options += ""; } openModal("Товар в заказ #" + orderId, "
" + "" + "" + "" + "
" + "
" ); } // сохраняю товар в заказ async function saveOrderItem(orderId) { const productId = Number(document.getElementById("orderItemProduct").value); const quantity = Number(document.getElementById("orderItemQuantity").value); const price = Number(document.getElementById("orderItemPrice").value); if (quantity <= 0) { showToast("Количество должно быть больше 0."); return; } if (price <= 0) { showToast("Цена должна быть больше 0."); return; } try { await api("/api/orderitems", { method: "POST", body: JSON.stringify({ orderId, productId, quantity, price }) }); showToast("Товар добавлен."); showOrderItems(orderId); // обновляю список } catch (error) { showToast(error.message); } } // удаляю что угодно (товар, заказ и тд) async function deleteItem(type, id) { if (!needAdmin()) { // удалять может только админ return; } // красивые названия для вопроса const names = { products: "товар", categories: "категорию", warehouses: "склад", stock: "остаток", users: "пользователя", orders: "заказ" }; // откуда удалять const endpoints = { products: "/api/products", categories: "/api/categories", warehouses: "/api/warehouses", stock: "/api/stocks", users: "/api/users", orders: "/api/orders" }; const name = names[type] || "запись"; if (!confirm("Удалить " + name + "?")) { // спрашиваю точно ли надо return; } try { await api(endpoints[type] + "/" + id, { method: "DELETE" }); // удаляю showToast("Удалено."); showPage(currentPage); // обновляю страницу } catch (error) { showToast(error.message); } } // при запуске смотрю входил ли уже const savedUser = sessionStorage.getItem("demoUser"); if (savedUser) { try { const user = JSON.parse(savedUser); if (user && user.login) { enterApplication(user); // да, уже входил, сразу захожу } else { app.classList.add("hidden"); authScreen.classList.remove("hidden"); showLoginForm(); } } catch { sessionStorage.removeItem("demoUser"); app.classList.add("hidden"); authScreen.classList.remove("hidden"); showLoginForm(); } } else { // нет, показываю вход app.classList.add("hidden"); authScreen.classList.remove("hidden"); showLoginForm(); }