/
Coderdev
/
web-nodejs-labs
Обзор
Документация
Войти
/
Coderdev
/
web-nodejs-labs
Код
Запросы
0
Задачи
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
public/lab10/task1/index.html
619 строк
29 KB
Coderdev
1
12 май 2026, 12:34
12 май 2026, 12:34
f5e899f
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <title>Задание 1 — Интернет-магазин</title> <style> *{box-sizing:border-box;margin:0;padding:0} body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f0f2f5;color:#1e293b} .app{display:grid;grid-template-columns:210px 1fr 290px;min-height:100vh} /* ── Sidebar ── */ .sidebar{background:#1e293b;color:#e2e8f0;padding:16px 0;display:flex;flex-direction:column} .sidebar-logo{padding:0 16px 16px;font-size:1rem;font-weight:700;color:#fff;border-bottom:1px solid #334155} .sidebar-section{padding:12px 16px 4px;font-size:0.7rem;text-transform:uppercase; letter-spacing:1px;color:#64748b;font-weight:600} .sidebar a{display:block;padding:8px 16px;color:#cbd5e1;text-decoration:none; font-size:0.88rem;cursor:pointer;border-radius:0;transition:background .12s} .sidebar a:hover{background:#334155;color:#fff} .sidebar a.active{background:#2563eb;color:#fff} /* ── Main ── */ .main{padding:22px;overflow-y:auto} .page-head{display:flex;align-items:center;gap:12px;margin-bottom:18px;flex-wrap:wrap} .page-head h1{font-size:1.3rem;font-weight:700;flex:1} /* ── Cards ── */ .card{background:#fff;border-radius:12px;border:1px solid #e2e8f0;overflow:hidden;margin-bottom:16px} .card-head{padding:12px 16px;border-bottom:1px solid #f1f5f9;display:flex; align-items:center;justify-content:space-between} .card-head h2{font-size:0.95rem;font-weight:700} .card-body{padding:16px} /* ── Inputs / Buttons ── */ input[type=text],input[type=number],select{ padding:7px 10px;border:1px solid #e2e8f0;border-radius:8px; font-size:0.85rem;background:#fff;width:100%} input:focus,select:focus{outline:2px solid #2563eb;border-color:#2563eb} button{padding:7px 14px;border:none;border-radius:8px;cursor:pointer; font-size:0.85rem;font-weight:500;transition:background .15s} .btn{background:#2563eb;color:#fff}.btn:hover{background:#1d4ed8} .btn-sm{padding:3px 9px;font-size:0.78rem} .btn-outline{background:#fff;border:1px solid #e2e8f0;color:#475569}.btn-outline:hover{background:#f8fafc} .btn-danger{background:#dc2626;color:#fff}.btn-danger:hover{background:#b91c1c} .btn-success{background:#16a34a;color:#fff}.btn-success:hover{background:#15803d} button:disabled{opacity:.4;cursor:not-allowed} label{font-size:0.78rem;color:#64748b;font-weight:500;display:flex;flex-direction:column;gap:3px} /* ── Products grid ── */ .products-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:14px} .product-card{background:#fff;border-radius:10px;padding:14px;border:1px solid #e2e8f0; display:flex;flex-direction:column;gap:7px} .product-card:hover{box-shadow:0 2px 10px rgba(0,0,0,.08)} .pc-cat{font-size:0.7rem;color:#94a3b8;text-transform:uppercase;font-weight:600} .pc-title{font-size:0.9rem;font-weight:600;color:#2563eb;cursor:pointer} .pc-title:hover{text-decoration:underline} .pc-price{font-size:1rem;color:#16a34a;font-weight:700} .pc-stock{font-size:0.75rem;color:#94a3b8} .pc-actions{display:flex;gap:5px;margin-top:2px} /* ── Table ── */ table{width:100%;border-collapse:collapse} th{background:#f8fafc;padding:8px 12px;text-align:left;font-size:0.77rem; color:#64748b;font-weight:600;border-bottom:1px solid #f1f5f9} td{padding:9px 12px;border-bottom:1px solid #f8fafc;font-size:0.85rem;vertical-align:middle} tr:last-child td{border-bottom:none} tr:hover td{background:#fafafa} /* ── Pagination ── */ .pagination{display:flex;gap:8px;margin-top:16px;align-items:center;font-size:0.85rem} /* ── Status ── */ .status{padding:7px 10px;border-radius:8px;font-size:0.82rem;margin-top:8px} .ok{background:#dcfce7;color:#15803d} .err{background:#fee2e2;color:#b91c1c} /* ── Search bar ── */ .search-row{display:flex;gap:8px;align-items:center;margin-bottom:14px;flex-wrap:wrap} .search-row input{width:200px} /* ── Form grid ── */ .form-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px} .form-grid .full{grid-column:1/-1} /* ── Cart panel ── */ .cart-panel{background:#fff;border-left:1px solid #e2e8f0;padding:18px; display:flex;flex-direction:column;gap:10px;overflow-y:auto} .cart-panel h2{font-size:0.95rem;font-weight:700;border-bottom:1px solid #f1f5f9;padding-bottom:8px} .cart-empty{color:#94a3b8;font-size:0.85rem;text-align:center;padding:20px 0} .cart-item{padding:8px 0;border-bottom:1px solid #f5f5f5} .ci-title{font-size:0.83rem;font-weight:600;margin-bottom:4px} .ci-row{display:flex;align-items:center;gap:6px;font-size:0.78rem;color:#555} .qty-btn{width:22px;height:22px;padding:0;border:1px solid #e2e8f0; background:#f8fafc;border-radius:4px;cursor:pointer;font-weight:700;font-size:0.85rem} .qty-btn:hover{background:#e2e8f0} .cart-total{font-weight:700;font-size:0.95rem;margin-top:auto;padding-top:8px;border-top:1px solid #f1f5f9} /* ── Modal ── */ .modal-bg{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5); z-index:100;align-items:center;justify-content:center} .modal-bg.open{display:flex} .modal{background:#fff;border-radius:14px;padding:24px;max-width:460px; width:90%;max-height:90vh;overflow-y:auto;display:flex;flex-direction:column;gap:12px} .modal h2{font-size:1.1rem;font-weight:700} .modal .close-btn{position:absolute;top:12px;right:16px;background:none;border:none; font-size:1.3rem;cursor:pointer;color:#94a3b8} .modal-inner{position:relative} .badge{display:inline-block;padding:1px 8px;border-radius:10px;font-size:0.72rem;font-weight:600} .badge-g{background:#dcfce7;color:#15803d} .order-card{border:1px solid #e2e8f0;border-radius:10px;padding:12px;cursor:pointer;margin-bottom:8px} .order-card:hover{box-shadow:0 2px 8px rgba(0,0,0,.07)} </style> </head> <body> <div class="app"> <!-- ══ Sidebar ══ --> <nav class="sidebar"> <div class="sidebar-logo">🛍 Магазин</div> <div class="sidebar-section">Каталог</div> <a class="active" onclick="showPage('catalog',this)">📦 Товары</a> <a onclick="showPage('orders',this)">📋 Заказы</a> <div class="sidebar-section">Управление</div> <a onclick="showPage('manage-cats',this)">🗂 Категории</a> <a onclick="showPage('manage-products',this)">✏️ Товары</a> <div style="margin-top:auto;padding:16px"> <a href="/lab10/" style="font-size:0.8rem;color:#64748b">← Назад</a> </div> </nav> <!-- ══ Main ══ --> <main class="main"> <!-- ─ Каталог ─ --> <div id="page-catalog"> <div class="page-head"> <h1>Каталог товаров</h1> <input type="text" id="searchInput" placeholder="Поиск..." style="width:180px" oninput="onSearch()"> <select id="catFilter" onchange="onCatChange()" style="width:160px"> <option value="">Все категории</option> </select> </div> <div id="productGrid" class="products-grid">Загрузка...</div> <div class="pagination" id="pagination"></div> </div> <!-- ─ Заказы ─ --> <div id="page-orders" style="display:none"> <div class="page-head"> <h1>Заказы</h1> <button class="btn-outline" onclick="loadOrders()">↻ Обновить</button> </div> <div id="ordersList">Загрузка...</div> </div> <!-- ─ Управление категориями ─ --> <div id="page-manage-cats" style="display:none"> <div class="page-head"><h1>Категории</h1></div> <div class="card"> <div class="card-head"><h2 id="cat-form-title">Добавить категорию</h2></div> <div class="card-body"> <input type="hidden" id="cat-edit-id"> <div style="display:flex;gap:8px;align-items:flex-end"> <label style="flex:1">Название категории <input type="text" id="cat-name" placeholder="например: Смартфоны"> </label> <button class="btn" onclick="saveCat()">Сохранить</button> <button class="btn-outline" onclick="cancelCat()">Сброс</button> </div> <div id="cat-status"></div> </div> </div> <div class="card"> <div class="card-head"><h2>Список категорий</h2></div> <table> <thead><tr><th>ID</th><th>Название</th><th></th></tr></thead> <tbody id="catsTbody"><tr><td colspan="3" style="padding:20px;color:#94a3b8;text-align:center">Загрузка...</td></tr></tbody> </table> </div> </div> <!-- ─ Управление товарами ─ --> <div id="page-manage-products" style="display:none"> <div class="page-head"><h1>Управление товарами</h1></div> <div class="card"> <div class="card-head"><h2 id="prod-form-title">Добавить товар</h2></div> <div class="card-body"> <input type="hidden" id="prod-edit-id"> <div class="form-grid"> <label class="full">Название <input type="text" id="prod-title" placeholder="Название товара"> </label> <label>Цена (в рублях) <input type="number" id="prod-price" placeholder="например: 1500.00" step="0.01" min="0"> </label> <label>Количество на складе <input type="number" id="prod-amount" placeholder="0" min="0"> </label> <label class="full">Категория <select id="prod-cat"><option value="">— без категории —</option></select> </label> </div> <div style="display:flex;gap:8px;margin-top:10px"> <button class="btn" onclick="saveProduct()">Сохранить</button> <button class="btn-outline" onclick="cancelProduct()">Сброс</button> </div> <div id="prod-status"></div> </div> </div> <div class="card"> <div class="card-head"> <h2>Все товары</h2> <div style="display:flex;gap:8px"> <input type="text" id="mp-search" placeholder="Поиск..." style="width:150px" oninput="loadManageProducts()"> <select id="mp-cat" onchange="loadManageProducts()" style="width:140px"> <option value="">Все категории</option> </select> </div> </div> <table> <thead> <tr> <th>ID</th><th>Название</th><th>Категория</th> <th>Цена</th><th>Склад</th><th></th> </tr> </thead> <tbody id="manageProdTbody"> <tr><td colspan="6" style="padding:20px;color:#94a3b8;text-align:center">Загрузка...</td></tr> </tbody> </table> </div> </div> </main> <!-- ══ Cart ══ --> <aside class="cart-panel"> <h2>🛒 Корзина <span id="cartCount" style="color:#2563eb;font-weight:400"></span></h2> <div id="cartItems"><p class="cart-empty">Корзина пуста</p></div> <div class="cart-total" id="cartTotal"></div> <button class="btn-success" id="buyBtn" onclick="buyCart()" style="display:none">Оформить заказ</button> <div id="cartStatus"></div> </aside> </div> <!-- ── Modal: карточка товара ── --> <div class="modal-bg" id="productModal"> <div class="modal modal-inner"> <button class="close-btn" onclick="closeModal('productModal')">×</button> <h2 id="pm-title"></h2> <div><span style="color:#94a3b8;font-size:0.8rem">Категория:</span> <span id="pm-cat"></span></div> <div style="font-size:1.4rem;font-weight:700;color:#16a34a" id="pm-price"></div> <div style="font-size:0.85rem;color:#64748b">На складе: <span id="pm-amount"></span> шт.</div> <button class="btn" id="pm-add" onclick="addToCartFromModal()">Добавить в корзину</button> </div> </div> <!-- ── Modal: детали заказа ── --> <div class="modal-bg" id="orderModal"> <div class="modal modal-inner"> <button class="close-btn" onclick="closeModal('orderModal')">×</button> <h2 id="om-title"></h2> <div style="font-size:0.82rem;color:#64748b" id="om-date"></div> <div id="om-items"></div> <div style="font-weight:700;font-size:1.1rem" id="om-total"></div> </div> </div> <script> const API = '/api/lab10'; const fmt = v => (v/100).toLocaleString('ru-RU',{style:'currency',currency:'RUB'}); const fmtDate = s => new Date(s).toLocaleString('ru-RU'); let currentPage = 1; let currentCat = ''; let searchQ = ''; let searchTimer = null; let cart = {}; // {id: qty} let modalProductId = null; let allProducts = []; // кэш для корзины let categories = []; // ══ Навигация ══════════════════════════════════════════════════════════ function showPage(name, el) { ['catalog','orders','manage-cats','manage-products'].forEach(p => { document.getElementById('page-' + p).style.display = 'none'; }); document.getElementById('page-' + name).style.display = ''; document.querySelectorAll('.sidebar a').forEach(a => a.classList.remove('active')); if (el) el.classList.add('active'); if (name === 'orders') loadOrders(); if (name === 'manage-cats') loadCatsTable(); if (name === 'manage-products') { loadManageProducts(); fillCatSelects(); } if (name === 'catalog') loadProducts(); } // ══ Категории (фронтенд) ═══════════════════════════════════════════════ async function loadCategories() { categories = await fetch(`${API}/categories`).then(r => r.json()); // Заполняем фильтр в каталоге const catFilter = document.getElementById('catFilter'); catFilter.innerHTML = '<option value="">Все категории</option>' + categories.map(c => `<option value="${c.id}">${c.name}</option>`).join(''); fillCatSelects(); } function fillCatSelects() { const opts = categories.map(c => `<option value="${c.id}">${c.name}</option>`).join(''); document.getElementById('prod-cat').innerHTML = '<option value="">— без категории —</option>' + opts; document.getElementById('mp-cat').innerHTML = '<option value="">Все категории</option>' + opts; } // ── Таблица категорий ── async function loadCatsTable() { await loadCategories(); document.getElementById('catsTbody').innerHTML = categories.map(c => ` <tr> <td style="color:#94a3b8">${c.id}</td> <td><strong>${c.name}</strong></td> <td style="white-space:nowrap"> <button class="btn-outline btn-sm" onclick="editCat(${c.id},'${c.name.replace(/'/g,"\\'")}')">✏ Изменить</button> <button class="btn-danger btn-sm" onclick="deleteCat(${c.id})">✕</button> </td> </tr>`).join(''); } async function saveCat() { const id = document.getElementById('cat-edit-id').value; const name = document.getElementById('cat-name').value.trim(); if (!name) return setStatus('cat-status','Введите название','err'); const url = id ? `${API}/categories/${id}` : `${API}/categories`; const method = id ? 'PATCH' : 'POST'; const res = await fetch(url, { method, headers:{'Content-Type':'application/json'}, body: JSON.stringify({name}) }); const data = await res.json(); if (res.ok) { setStatus('cat-status', id ? '✓ Категория обновлена' : '✓ Категория добавлена', 'ok'); cancelCat(); loadCatsTable(); } else { setStatus('cat-status', '✕ ' + (data.error||'Ошибка'), 'err'); } } function editCat(id, name) { document.getElementById('cat-edit-id').value = id; document.getElementById('cat-name').value = name; document.getElementById('cat-form-title').textContent = 'Редактировать категорию'; document.getElementById('cat-status').textContent = ''; } function cancelCat() { document.getElementById('cat-edit-id').value = ''; document.getElementById('cat-name').value = ''; document.getElementById('cat-form-title').textContent = 'Добавить категорию'; document.getElementById('cat-status').textContent = ''; } async function deleteCat(id) { if (!confirm(`Удалить категорию #${id}? Товары потеряют категорию.`)) return; const res = await fetch(`${API}/categories/${id}`, {method:'DELETE'}); if (res.ok) loadCatsTable(); else { const d = await res.json(); alert(d.error||'Ошибка'); } } // ══ Товары (управление) ════════════════════════════════════════════════ async function loadManageProducts() { const search = document.getElementById('mp-search').value.trim(); const cat = document.getElementById('mp-cat').value; const p = new URLSearchParams({limit:100}); if (search) p.set('q', search); if (cat) p.set('category', cat); const data = await fetch(`${API}/products?${p}`).then(r => r.json()); const prods = data.data || []; document.getElementById('manageProdTbody').innerHTML = prods.length ? prods.map(p => ` <tr> <td style="color:#94a3b8">${p.id}</td> <td><strong>${p.title}</strong></td> <td>${p.category_title||'—'}</td> <td>${fmt(p.price)}</td> <td>${p.amount} шт.</td> <td style="white-space:nowrap"> <button class="btn-outline btn-sm" onclick='prefillProduct(${JSON.stringify({id:p.id,title:p.title,price:p.price,amount:p.amount,category_id:p.category_id}).replace(/'/g,"'")})'>✏</button> <button class="btn-danger btn-sm" onclick="deleteProduct(${p.id})">✕</button> </td> </tr>`).join('') : '<tr><td colspan="6" style="padding:20px;color:#94a3b8;text-align:center">Товаров нет</td></tr>'; } function prefillProduct(p) { document.getElementById('prod-edit-id').value = p.id; document.getElementById('prod-title').value = p.title; document.getElementById('prod-price').value = (p.price/100).toFixed(2); document.getElementById('prod-amount').value = p.amount; document.getElementById('prod-cat').value = p.category_id || ''; document.getElementById('prod-form-title').textContent = 'Редактировать товар'; document.getElementById('prod-status').textContent = ''; window.scrollTo({top:0,behavior:'smooth'}); } function cancelProduct() { document.getElementById('prod-edit-id').value = ''; document.getElementById('prod-form-title').textContent = 'Добавить товар'; ['prod-title','prod-price','prod-amount'].forEach(id => document.getElementById(id).value = ''); document.getElementById('prod-cat').value = ''; document.getElementById('prod-status').textContent = ''; } async function saveProduct() { const id = document.getElementById('prod-edit-id').value; const title = document.getElementById('prod-title').value.trim(); const priceR = parseFloat(document.getElementById('prod-price').value); const amount = parseInt(document.getElementById('prod-amount').value); const catId = document.getElementById('prod-cat').value; if (!title || isNaN(priceR) || isNaN(amount)) { return setStatus('prod-status','Заполните все обязательные поля','err'); } const price = Math.round(priceR * 100); const body = { title, price, amount, category_id: catId ? parseInt(catId) : null }; const url = id ? `${API}/products/${id}` : `${API}/products`; const method = id ? 'PATCH' : 'POST'; const res = await fetch(url, { method, headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) }); const data = await res.json(); if (res.ok) { setStatus('prod-status', id ? '✓ Товар обновлён' : '✓ Товар добавлен', 'ok'); cancelProduct(); loadManageProducts(); loadCategories(); } else { setStatus('prod-status','✕ ' + (data.error||'Ошибка'), 'err'); } } async function deleteProduct(id) { if (!confirm(`Удалить товар #${id}?`)) return; const res = await fetch(`${API}/products/${id}`, {method:'DELETE'}); if (res.ok) loadManageProducts(); else { const d = await res.json(); alert(d.error||'Ошибка'); } } // ══ Каталог ════════════════════════════════════════════════════════════ async function loadProducts() { const p = new URLSearchParams({page: currentPage}); if (currentCat) p.set('category', currentCat); if (searchQ) p.set('q', searchQ); const data = await fetch(`${API}/products?${p}`).then(r => r.json()); allProducts = [...allProducts.filter(x => !data.data.find(d => d.id===x.id)), ...data.data]; renderCatalog(data); } function renderCatalog({data, total, page, limit}) { if (!data.length) { document.getElementById('productGrid').innerHTML = '<p style="color:#94a3b8">Товаров не найдено</p>'; document.getElementById('pagination').innerHTML = ''; return; } document.getElementById('productGrid').innerHTML = data.map(p => ` <div class="product-card"> <div class="pc-cat">${p.category_title||'—'}</div> <div class="pc-title" onclick="openProduct(${p.id})">${p.title}</div> <div class="pc-price">${fmt(p.price)}</div> <div class="pc-stock">Склад: ${p.amount} шт.${cart[p.id]?' | В корзине: '+cart[p.id]:''}</div> <div class="pc-actions"> <button class="btn btn-sm" style="flex:1" onclick="addToCart(${p.id})" ${p.amount===0?'disabled':''}> ${p.amount===0?'Нет в наличии':'+ В корзину'} </button> </div> </div>`).join(''); const pages = Math.ceil(total/limit); document.getElementById('pagination').innerHTML = pages > 1 ? ` <button class="btn-outline" onclick="gotoPage(${page-1})" ${page<=1?'disabled':''}>← Пред.</button> <span>Стр. ${page} из ${pages} (${total} товаров)</span> <button class="btn-outline" onclick="gotoPage(${page+1})" ${page>=pages?'disabled':''}>След. →</button> ` : `<span style="color:#94a3b8;font-size:0.83rem">Всего: ${total} товаров</span>`; } function gotoPage(p) { currentPage = p; loadProducts(); } function onCatChange() { currentCat = document.getElementById('catFilter').value; currentPage=1; loadProducts(); } function onSearch() { clearTimeout(searchTimer); searchTimer = setTimeout(() => { searchQ = document.getElementById('searchInput').value.trim(); currentPage = 1; loadProducts(); }, 350); } // ── Карточка товара ── async function openProduct(id) { const p = await fetch(`${API}/task1/product/${id}`).then(r=>r.json()); document.getElementById('pm-title').textContent = p.title; document.getElementById('pm-cat').textContent = p.category_title||'—'; document.getElementById('pm-price').textContent = fmt(p.price); document.getElementById('pm-amount').textContent = p.amount; document.getElementById('pm-add').disabled = p.amount===0; modalProductId = id; document.getElementById('productModal').classList.add('open'); } function addToCartFromModal() { if(modalProductId) addToCart(modalProductId); } function closeModal(id) { document.getElementById(id).classList.remove('open'); } // ══ Корзина ════════════════════════════════════════════════════════════ function addToCart(id) { cart[id] = (cart[id]||0) + 1; renderCart(); loadProducts(); } function changeQty(id, d) { const next = (cart[id]||0) + d; if (next < 1) delete cart[id]; else cart[id] = next; renderCart(); loadProducts(); } function renderCart() { const ids = Object.keys(cart); const cnt = ids.reduce((s,k)=>s+cart[k],0); document.getElementById('cartCount').textContent = cnt ? `(${cnt})` : ''; if (!ids.length) { document.getElementById('cartItems').innerHTML = '<p class="cart-empty">Корзина пуста</p>'; document.getElementById('cartTotal').textContent = ''; document.getElementById('buyBtn').style.display = 'none'; return; } let total = 0; document.getElementById('cartItems').innerHTML = ids.map(id => { const p = allProducts.find(x => x.id == id); if (!p) return ''; const sub = p.price * cart[id]; total += sub; return `<div class="cart-item"> <div class="ci-title">${p.title}</div> <div class="ci-row"> <button class="qty-btn" onclick="changeQty(${id},-1)">−</button> <span>${cart[id]}</span> <button class="qty-btn" onclick="changeQty(${id},+1)">+</button> <span style="margin-left:auto">${fmt(sub)}</span> </div> </div>`; }).join(''); document.getElementById('cartTotal').textContent = `Итого: ${fmt(total)}`; document.getElementById('buyBtn').style.display = ''; } async function buyCart() { const items = Object.entries(cart).map(([id,qty])=>({id:parseInt(id),qty})); document.getElementById('buyBtn').disabled = true; try { const res = await fetch(`${API}/cart/buy`,{ method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({items}) }); const data = await res.json(); const el = document.getElementById('cartStatus'); if (data.ok) { cart = {}; renderCart(); el.className='status ok'; el.textContent=`✓ Заказ #${data.orderId} оформлен!`; loadProducts(); } else { el.className='status err'; el.textContent='✕ '+(data.error||'Ошибка'); } } finally { document.getElementById('buyBtn').disabled=false; } } // ══ Заказы ═════════════════════════════════════════════════════════════ async function loadOrders() { const orders = await fetch(`${API}/task1/orders`).then(r=>r.json()); if (!orders.length) { document.getElementById('ordersList').innerHTML='<p style="color:#94a3b8;padding:20px">Заказов пока нет</p>'; return; } document.getElementById('ordersList').innerHTML = orders.map(o=>` <div class="order-card" onclick="openOrder(${o.id})"> <div style="display:flex;align-items:center;gap:8px"> <strong>Заказ #${o.id}</strong> <span class="badge badge-g">✓</span> </div> <div style="font-size:0.8rem;color:#64748b;margin-top:4px"> ${fmtDate(o.created_at)} · Позиций: ${o.items_count} · Сумма: <strong>${fmt(o.total_sum||0)}</strong> </div> </div>`).join(''); } async function openOrder(id) { const o = await fetch(`${API}/task1/orders/${id}`).then(r=>r.json()); document.getElementById('om-title').textContent = `Заказ #${o.id}`; document.getElementById('om-date').textContent = fmtDate(o.created_at); document.getElementById('om-items').innerHTML = o.items.map(i=>` <div style="padding:6px 0;border-bottom:1px solid #f1f5f9;font-size:0.85rem"> <strong>${i.title}</strong><br> <span style="color:#64748b">${fmt(i.unit_price)} × ${i.qty} = <span style="color:#16a34a;font-weight:700">${fmt(i.subtotal)}</span></span> </div>`).join(''); document.getElementById('om-total').textContent = `Итого: ${fmt(o.total_sum)}`; document.getElementById('orderModal').classList.add('open'); } // ══ Утилиты ════════════════════════════════════════════════════════════ function setStatus(id, msg, type) { const el = document.getElementById(id); el.className = 'status ' + type; el.textContent = msg; } // ══ Инициализация ══════════════════════════════════════════════════════ loadCategories().then(() => loadProducts()); </script> </body> </html>