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();