// нахожу экран входа и главное окно
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 += "
";
// кнопки показываю только админу
if (isAdmin) {
html += "
";
html += "";
html += "";
html += "
";
}
html += "
";
}
html += "
";
pageContent.innerHTML = html; // показываю все
}
// рисую категории таблицей
function renderCategories(categories) {
let html = "
ID
Название
Действия
";
for (const c of categories) {
html += "
#" + c.id + "
" + escapeHtml(c.name) + "
";
// кнопки только для админа
if (isAdmin) {
html += "";
html += "";
}
html += "
";
}
html += "
";
pageContent.innerHTML = html;
}
// рисую склады
function renderWarehouses(warehouses) {
let html = "
ID
Название
Адрес
Действия
";
for (const w of warehouses) {
html += "
#" + w.id + "
" + escapeHtml(w.name) + "
" + escapeHtml(w.address) + "
";
// кнопки только для админа
if (isAdmin) {
html += "";
html += "";
}
html += "
";
}
html += "
";
pageContent.innerHTML = html;
}
// рисую остатки
function renderStock(stocks) {
let html = "
ID
Товар
Склад
Кол-во
Действия
";
for (const s of stocks) {
const pname = s.product ? s.product.name : "—";
const wname = s.warehouse ? s.warehouse.name : "—";
html += "
#" + s.id + "
" + escapeHtml(pname) + "
" + escapeHtml(wname) + "
" + s.quantity + "
";
// кнопки только для админа
if (isAdmin) {
html += "";
html += "";
}
html += "
";
}
html += "
";
pageContent.innerHTML = html;
}
// рисую пользователей
function renderUsers(users) {
// сюда пускаю только админа (на всякий случай)
if (!isAdmin) {
pageContent.innerHTML = "
Нет прав
";
return;
}
let html = "
ID
Логин
Почта
Роль
Действия
";
for (const u of users) {
const rname = u.role ? u.role.name : "—";
html += "
#" + u.id + "
" + escapeHtml(u.login) + "
" + escapeHtml(u.email || "—") + "
" + escapeHtml(rname) + "
";
html += "";
html += "";
html += "
";
}
html += "
";
pageContent.innerHTML = html;
}
// рисую заказы
function renderOrders(orders) {
let html = "
ID
Кто заказал
Дата
Статус
Действия
";
for (const o of orders) {
const uname = o.user ? o.user.login : "—";
html += "
#" + o.id + "
" + escapeHtml(uname) + "
" + formatDate(o.date) + "
" + escapeHtml(o.status) + "
";
html += "";
// менять и удалять заказы может только админ, смотреть товары могут все
if (isAdmin) {
html += "";
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 += "