From 25a92e4b986ab66077ebb3ad0bc85483febea8e7 Mon Sep 17 00:00:00 2001 From: ParkSuMin Date: Sat, 3 Jan 2026 18:21:09 +0300 Subject: [PATCH] Switch ID --- public/app.js | 576 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 576 insertions(+) create mode 100644 public/app.js diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..74b98f5 --- /dev/null +++ b/public/app.js @@ -0,0 +1,576 @@ +const notification = document.getElementById('notification'); +const currentDateEl = document.getElementById('current-date'); +const orderDateInput = document.getElementById('order-date'); +const customerNameInput = document.getElementById('customer-name'); +const orderForm = document.getElementById('order-form'); +const ordersContainer = document.getElementById('orders'); +const inventoryList = document.getElementById('inventory'); +const advanceButton = document.getElementById('advance-date'); + +const API_BASE = + window.location.protocol === 'file:' ? 'http://localhost:3000' : window.location.origin; + +let products = []; +let orders = []; +let currentDate = ''; + +function getLocalISODate() { + const now = new Date(); + const offsetMs = now.getTimezoneOffset() * 60 * 1000; + return new Date(now.getTime() - offsetMs).toISOString().slice(0, 10); +} + +class OrderCard { + constructor(order, allOrders, allProducts) { + this.order = order; + this.orders = allOrders; + this.products = allProducts; + } + + build() { + const card = document.createElement('div'); + card.className = 'card shadow-sm order-card'; + + const body = document.createElement('div'); + body.className = 'card-body'; + + body.appendChild(this.buildHeader()); + body.appendChild(this.buildItemsSection()); + + card.appendChild(body); + return card; + } + + buildHeader() { + const header = document.createElement('div'); + header.className = 'd-flex justify-content-between flex-wrap gap-2'; + + const info = document.createElement('div'); + const name = document.createElement('h3'); + name.className = 'h5 mb-1'; + name.textContent = this.order.customer_name; + + const idMeta = document.createElement('div'); + idMeta.className = 'text-muted order-meta'; + idMeta.textContent = `ID: ${this.order.id}`; + + const dateMeta = document.createElement('div'); + dateMeta.className = 'text-muted order-meta'; + dateMeta.textContent = `Дата: ${this.order.order_date}`; + + info.appendChild(name); + info.appendChild(idMeta); + info.appendChild(dateMeta); + + const actions = document.createElement('div'); + actions.className = 'd-flex gap-2 align-items-start'; + actions.appendChild(this.buildButton('Изменить', 'btn-outline-secondary', { + action: 'edit-order', + id: this.order.id + })); + actions.appendChild(this.buildButton('Удалить', 'btn-outline-danger', { + action: 'delete-order', + id: this.order.id + })); + + header.appendChild(info); + header.appendChild(actions); + + return header; + } + + buildItemsSection() { + const wrapper = document.createElement('div'); + wrapper.className = 'mt-3'; + + wrapper.appendChild(this.buildItemsTable()); + wrapper.appendChild(this.buildAddItemForm()); + + return wrapper; + } + + buildItemsTable() { + const table = document.createElement('table'); + table.className = 'table table-sm align-middle'; + + table.innerHTML = ` + + + ID + Товар + Количество + Действия + + + `; + + const body = document.createElement('tbody'); + this.order.items.forEach((item) => { + body.appendChild(this.buildItemRow(item)); + }); + + table.appendChild(body); + return table; + } + + buildItemRow(item) { + const row = document.createElement('tr'); + + const idCell = document.createElement('td'); + idCell.className = 'text-muted small'; + idCell.textContent = item.product_id ?? item.id; + + const productCell = document.createElement('td'); + const productSelect = this.buildProductSelect(item); + productSelect.classList.add('form-select-sm'); + productSelect.dataset.action = 'item-product'; + productSelect.dataset.itemId = item.id; + productSelect.setAttribute('aria-label', 'Товар'); + productCell.appendChild(productSelect); + + const quantityCell = document.createElement('td'); + const quantityInput = document.createElement('input'); + quantityInput.type = 'number'; + quantityInput.min = '1'; + quantityInput.className = 'form-control form-control-sm'; + quantityInput.value = item.quantity; + quantityInput.dataset.action = 'item-quantity'; + quantityInput.dataset.itemId = item.id; + quantityInput.setAttribute('aria-label', 'Количество'); + quantityCell.appendChild(quantityInput); + + const actionsCell = document.createElement('td'); + actionsCell.className = 'text-end item-actions'; + actionsCell.appendChild(this.buildItemActionButtons(item)); + actionsCell.appendChild(this.buildMoveControls(item)); + + row.appendChild(idCell); + row.appendChild(productCell); + row.appendChild(quantityCell); + row.appendChild(actionsCell); + + return row; + } + + buildItemActionButtons(item) { + const wrapper = document.createElement('div'); + wrapper.className = 'd-flex gap-2 justify-content-end'; + + wrapper.appendChild( + this.buildButton('Сохранить', 'btn-outline-primary', { + action: 'save-item', + orderId: this.order.id, + itemId: item.id + }) + ); + wrapper.appendChild( + this.buildButton('Удалить', 'btn-outline-danger', { + action: 'delete-item', + orderId: this.order.id, + itemId: item.id + }) + ); + + return wrapper; + } + + buildMoveControls(item) { + const wrapper = document.createElement('div'); + wrapper.className = 'd-flex gap-2 justify-content-end mt-2'; + + const select = document.createElement('select'); + select.className = 'form-select form-select-sm'; + select.dataset.action = 'move-target'; + select.dataset.itemId = item.id; + select.setAttribute('aria-label', 'Выбор заказа для перемещения'); + + const placeholder = document.createElement('option'); + placeholder.value = ''; + placeholder.textContent = 'Переместить в...'; + select.appendChild(placeholder); + + this.orders + .filter((otherOrder) => otherOrder.id !== this.order.id) + .forEach((otherOrder) => { + const option = document.createElement('option'); + option.value = otherOrder.id; + option.textContent = otherOrder.customer_name; + select.appendChild(option); + }); + + wrapper.appendChild(select); + wrapper.appendChild( + this.buildButton('Перенести', 'btn-outline-secondary', { + action: 'move-item', + itemId: item.id + }) + ); + + return wrapper; + } + + buildAddItemForm() { + const form = document.createElement('form'); + form.className = 'row g-2 align-items-end'; + form.dataset.action = 'add-item'; + form.dataset.orderId = this.order.id; + + const productCol = document.createElement('div'); + productCol.className = 'col-md-6'; + const productLabel = document.createElement('label'); + productLabel.className = 'form-label'; + productLabel.textContent = 'Товар'; + const productSelect = this.buildProductSelect(); + const productSelectId = `add-item-product-${this.order.id}`; + productSelect.name = 'productId'; + productSelect.id = productSelectId; + productLabel.htmlFor = productSelectId; + productCol.appendChild(productLabel); + productCol.appendChild(productSelect); + + const quantityCol = document.createElement('div'); + quantityCol.className = 'col-md-3'; + const quantityLabel = document.createElement('label'); + quantityLabel.className = 'form-label'; + quantityLabel.textContent = 'Кол-во'; + const quantityInput = document.createElement('input'); + const quantityInputId = `add-item-quantity-${this.order.id}`; + quantityInput.type = 'number'; + quantityInput.min = '1'; + quantityInput.className = 'form-control'; + quantityInput.name = 'quantity'; + quantityInput.id = quantityInputId; + quantityLabel.htmlFor = quantityInputId; + quantityInput.required = true; + quantityCol.appendChild(quantityLabel); + quantityCol.appendChild(quantityInput); + + const actionCol = document.createElement('div'); + actionCol.className = 'col-md-3'; + const addButton = document.createElement('button'); + addButton.type = 'submit'; + addButton.className = 'btn btn-success w-100'; + addButton.textContent = 'Добавить позицию'; + actionCol.appendChild(addButton); + + form.appendChild(productCol); + form.appendChild(quantityCol); + form.appendChild(actionCol); + + return form; + } + + buildProductSelect(selectedId) { + const select = document.createElement('select'); + select.className = 'form-select'; + this.products.forEach((product) => { + const option = document.createElement('option'); + option.value = product.id; + option.textContent = product.name; + if (selectedId && product.id === selectedId) { + option.selected = true; + } + select.appendChild(option); + }); + return select; + } + + buildButton(label, style, data = {}) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = `btn ${style} btn-sm`; + button.textContent = label; + Object.entries(data).forEach(([key, value]) => { + button.dataset[key] = value; + }); + return button; + } +} + +function showNotification(message, type = 'warning') { + notification.textContent = message; + notification.className = `alert alert-${type}`; + notification.classList.remove('d-none'); + + setTimeout(() => { + notification.classList.add('d-none'); + }, 4000); +} + +function resolveApiUrl(url) { + if (url.startsWith('http://') || url.startsWith('https://')) { + return url; + } + if (url.startsWith('/')) { + return `${API_BASE}${url}`; + } + return `${API_BASE}/${url}`; +} + +async function fetchJson(url, options) { + const response = await fetch(resolveApiUrl(url), options); + if (!response.ok) { + const payload = await response.json().catch(() => ({ message: 'Ошибка запроса.' })); + throw new Error(payload.message || 'Ошибка запроса.'); + } + if (response.status === 204) { + return null; + } + return response.json(); +} + +function renderInventory() { + inventoryList.innerHTML = ''; + products.forEach((product) => { + const item = document.createElement('li'); + item.className = 'list-group-item d-flex justify-content-between align-items-center'; + item.innerHTML = ` + ${product.name} + ${product.quantity} + `; + inventoryList.appendChild(item); + }); +} + +function renderOrders() { + ordersContainer.innerHTML = ''; + if (orders.length === 0) { + ordersContainer.innerHTML = '

Заказы отсутствуют.

'; + return; + } + + orders.forEach((order) => { + const card = new OrderCard(order, orders, products).build(); + ordersContainer.appendChild(card); + }); +} + +function normalizeProducts(data) { + if (!Array.isArray(data)) { + return []; + } + return data.map((product) => ({ + id: product.id ?? product.ID, + name: product.name ?? product.pr_name, + quantity: product.quantity ?? product.count + })); +} + +function normalizeOrderItems(items) { + if (!Array.isArray(items)) { + return []; + } + return items.map((item, index) => ({ + id: item.id ?? item.item_id ?? `${item.product_id ?? item.productId ?? 'item'}-${index}`, + product_id: item.product_id ?? item.productId ?? item.product, + product_name: item.product_name ?? item.name, + quantity: item.quantity, + price: item.price + })); +} + +function normalizeOrders(data) { + if (!Array.isArray(data)) { + return []; + } + return data.map((order) => ({ + id: order.id ?? order.ID, + customer_name: order.customer_name ?? order.customer, + order_date: order.order_date ?? order.date, + status: order.status, + total_amount: order.total_amount ?? order.total, + items: normalizeOrderItems(order.items) + })); +} + +async function refreshData() { + const [currentDateData, productsData, ordersData] = await Promise.all([ + fetchJson('/api/current-date'), + fetchJson('/api/products'), + fetchJson('/api/orders') + ]); + currentDate = currentDateData.currentDate; + products = normalizeProducts(productsData); + orders = normalizeOrders(ordersData); + + currentDateEl.textContent = currentDate; + orderDateInput.min = currentDate; + if (!orderDateInput.value) { + orderDateInput.value = currentDate; + } + + renderInventory(); + renderOrders(); +} + +function initializeApp() { + const requiredElements = [ + notification, + currentDateEl, + orderDateInput, + customerNameInput, + orderForm, + ordersContainer, + inventoryList, + advanceButton + ]; + + if (requiredElements.some((element) => !element)) { + console.error('Missing required elements. Проверьте разметку страницы.'); + return; + } + + const initialDate = getLocalISODate(); + currentDate = initialDate; + currentDateEl.textContent = initialDate; + orderDateInput.min = initialDate; + if (!orderDateInput.value) { + orderDateInput.value = initialDate; + } + + orderForm.addEventListener('submit', async (event) => { + event.preventDefault(); + try { + await fetchJson('/api/orders', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + customerName: customerNameInput.value.trim(), + orderDate: orderDateInput.value + }) + }); + customerNameInput.value = ''; + orderDateInput.value = currentDate; + await refreshData(); + } catch (error) { + showNotification(error.message, 'warning'); + } + }); + + advanceButton.addEventListener('click', async () => { + try { + await fetchJson('/api/current-date/advance', { method: 'POST' }); + await refreshData(); + showNotification('Дата переведена. Заказы за сегодня отправлены.', 'success'); + } catch (error) { + showNotification(error.message, 'warning'); + } + }); + + ordersContainer.addEventListener('submit', async (event) => { + if (event.target.matches('form[data-action="add-item"]')) { + event.preventDefault(); + const orderId = event.target.dataset.orderId; + const productId = event.target.productId.value; + const quantity = Number.parseInt(event.target.quantity.value, 10); + + try { + const response = await fetchJson(`/api/orders/${orderId}/items`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ productId, quantity }) + }); + const order = orders.find((entry) => entry.id === orderId); + const product = products.find((entry) => entry.id === productId); + if (order) { + const newItem = normalizeOrderItems([ + { + id: response?.itemId, + product_id: productId, + product_name: product?.name, + quantity + } + ])[0]; + order.items = [...order.items, newItem]; + renderOrders(); + } + event.target.reset(); + await refreshData(); + } catch (error) { + showNotification(error.message, 'warning'); + } + } + }); + + ordersContainer.addEventListener('click', async (event) => { + const { action, id, itemId, orderId } = event.target.dataset; + + try { + if (action === 'delete-order') { + await fetchJson(`/api/orders/${id}`, { method: 'DELETE' }); + await refreshData(); + return; + } + + if (action === 'edit-order') { + const newName = prompt('Введите новое ФИО заказчика', ''); + if (newName === null) { + return; + } + const newDate = prompt('Введите дату заказа (YYYY-MM-DD)', currentDate); + if (!newDate) { + return; + } + Date + await fetchJson(`/api/orders/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ customerName: newName, orderDate: newDate }) + }); + await refreshData(); + return; + } + + if (action === 'save-item') { + const quantityInput = ordersContainer.querySelector( + `[data-action="item-quantity"][data-item-id="${itemId}"]` + ); + const productSelect = ordersContainer.querySelector( + `[data-action="item-product"][data-item-id="${itemId}"]` + ); + + await fetchJson(`/api/orders/${orderId}/items/${itemId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + productId: productSelect.value, + quantity: Number.parseInt(quantityInput.value, 10) + }) + }); + await refreshData(); + return; + } + + if (action === 'delete-item') { + await fetchJson(`/api/orders/${orderId}/items/${itemId}`, { method: 'DELETE' }); + await refreshData(); + return; + } + + if (action === 'move-item') { + const targetSelect = ordersContainer.querySelector( + `[data-action="move-target"][data-item-id="${itemId}"]` + ); + if (!targetSelect.value) { + showNotification('Выберите заказ для перемещения.', 'warning'); + return; + } + await fetchJson(`/api/order-items/${itemId}/move`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ targetOrderId: targetSelect.value }) + }); + await refreshData(); + return; + } + } catch (error) { + showNotification(error.message, 'warning'); + } + }); + + refreshData().catch((error) => { + showNotification(error.message, 'warning'); + }); +} + +initializeApp(); \ No newline at end of file