Update app.py
Browse files
app.py
CHANGED
|
@@ -56,6 +56,8 @@ def load_data():
|
|
| 56 |
p['price'] = 0
|
| 57 |
if 'photo' not in p:
|
| 58 |
p['photo'] = ''
|
|
|
|
|
|
|
| 59 |
if 'stocks' not in p:
|
| 60 |
p['stocks'] = {}
|
| 61 |
if 'colors' in p:
|
|
@@ -134,6 +136,10 @@ def handle_post(req, data, role, emp):
|
|
| 134 |
rolls = int(req.form.get('rolls', 0))
|
| 135 |
except ValueError:
|
| 136 |
rolls = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
data['products'].insert(0, {
|
| 139 |
'id': str(time.time()),
|
|
@@ -141,8 +147,27 @@ def handle_post(req, data, role, emp):
|
|
| 141 |
'description': req.form['description'],
|
| 142 |
'price': price,
|
| 143 |
'photo': photo,
|
|
|
|
| 144 |
'stocks': {loc_id: rolls}
|
| 145 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
elif action == 'delete' and role == 'admin':
|
| 148 |
idx = int(req.form['index'])
|
|
@@ -259,7 +284,7 @@ MAIN_HTML = '''
|
|
| 259 |
.product-info { padding: 12px; display: flex; flex-direction: column; flex: 1; }
|
| 260 |
.product-title { font-size: 1rem; font-weight: 600; margin-bottom: 4px; line-height: 1.2; }
|
| 261 |
.product-price { font-size: 1.1rem; font-weight: 700; color: var(--primary); margin-bottom: 4px; }
|
| 262 |
-
.product-stock { font-size: 0.85rem; color: var(--text-muted); margin-bottom:
|
| 263 |
.product-actions { display: flex; gap: 8px; align-items: center; margin-top: auto; }
|
| 264 |
.qty-input { width: 60px; padding: 8px; border: 1px solid var(--border); border-radius: 8px; text-align: center; font-weight: 600; outline: none; transition: border-color 0.2s; }
|
| 265 |
.qty-input:focus { border-color: var(--primary); }
|
|
@@ -389,8 +414,9 @@ MAIN_HTML = '''
|
|
| 389 |
<input type="text" name="name" class="form-control" placeholder="Название" required>
|
| 390 |
<textarea name="description" class="form-control" placeholder="Описание"></textarea>
|
| 391 |
<input type="number" name="price" class="form-control" placeholder="Цена (₸)" required>
|
|
|
|
| 392 |
<label style="display:block; margin-bottom:8px; font-size:0.9rem; color:var(--text-muted);">Фото товара (не обязательно)</label>
|
| 393 |
-
<input type="file" accept="image/*" class="form-control" onchange="processImage(this)">
|
| 394 |
<input type="hidden" name="photo" id="photo_b64">
|
| 395 |
<img id="photo_preview" style="display:none; width:100%; height:200px; object-fit:cover; border-radius:12px; margin-bottom:16px;">
|
| 396 |
<select name="loc_id" id="add_loc_select" class="form-control" required></select>
|
|
@@ -400,6 +426,26 @@ MAIN_HTML = '''
|
|
| 400 |
</div>
|
| 401 |
</div>
|
| 402 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
<div id="incomeModal" class="modal-overlay">
|
| 404 |
<div class="modal-content">
|
| 405 |
<button class="close-modal" onclick="closeModal('incomeModal')">×</button>
|
|
@@ -540,26 +586,32 @@ MAIN_HTML = '''
|
|
| 540 |
<div class="product-title">${p.name}</div>
|
| 541 |
<div class="product-price">${p.price} ₸</div>
|
| 542 |
<div class="product-stock">В наличии: ${stock} шт</div>
|
|
|
|
| 543 |
`;
|
| 544 |
|
| 545 |
if (role !== 'public') {
|
| 546 |
if (activeLoc !== 'all') {
|
| 547 |
if (stock > 0) {
|
| 548 |
-
html += `<div class="product-actions">
|
| 549 |
<input type="number" id="amt_${pIdx}" class="qty-input" value="1" min="1" max="${stock}">
|
| 550 |
-
<button class="btn btn-success" style="padding:8px; flex:1;" onclick="addToCart(${pIdx})">В корзину</button>
|
| 551 |
-
</div>
|
|
|
|
| 552 |
}
|
| 553 |
if (role === 'admin') {
|
| 554 |
-
html += `<div style="display:flex; gap:6px; margin-top:
|
| 555 |
<button class="btn btn-primary" style="padding:6px; flex:1; font-size:0.8rem;" onclick="openTransfer(${pIdx}, ${stock})">Переместить</button>
|
|
|
|
| 556 |
<button class="btn btn-danger" style="padding:6px; flex:1; font-size:0.8rem;" onclick="delProd(${pIdx})">Удалить</button>
|
| 557 |
</div>`;
|
| 558 |
}
|
| 559 |
} else {
|
| 560 |
html += `<div style="font-size:0.8rem; color:var(--text-muted); margin-top:auto;">Выберите конкретную локацию вверху для действий</div>`;
|
| 561 |
if (role === 'admin') {
|
| 562 |
-
html += `<div style="
|
|
|
|
|
|
|
|
|
|
| 563 |
}
|
| 564 |
}
|
| 565 |
}
|
|
@@ -570,9 +622,9 @@ MAIN_HTML = '''
|
|
| 570 |
cat.innerHTML = htmlArr.join('');
|
| 571 |
}
|
| 572 |
|
| 573 |
-
function addToCart(pIdx) {
|
| 574 |
-
let amt = parseInt(document.getElementById('amt_'+pIdx).value);
|
| 575 |
let p = data.products[pIdx];
|
|
|
|
| 576 |
let stock = p.stocks[activeLoc] || 0;
|
| 577 |
if(amt <= 0 || isNaN(amt)) return alert('Неверное количество');
|
| 578 |
|
|
@@ -676,7 +728,27 @@ MAIN_HTML = '''
|
|
| 676 |
openModal('transferModal');
|
| 677 |
}
|
| 678 |
|
| 679 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 680 |
if (input.files && input.files[0]) {
|
| 681 |
const reader = new FileReader();
|
| 682 |
reader.onload = function(e) {
|
|
@@ -693,8 +765,8 @@ MAIN_HTML = '''
|
|
| 693 |
const ctx = canvas.getContext('2d');
|
| 694 |
ctx.drawImage(img, 0, 0, w, h);
|
| 695 |
const b64 = canvas.toDataURL('image/jpeg', 0.6);
|
| 696 |
-
document.getElementById(
|
| 697 |
-
const p = document.getElementById(
|
| 698 |
p.src = b64; p.style.display = 'block';
|
| 699 |
}
|
| 700 |
img.src = e.target.result;
|
|
|
|
| 56 |
p['price'] = 0
|
| 57 |
if 'photo' not in p:
|
| 58 |
p['photo'] = ''
|
| 59 |
+
if 'box_qty' not in p:
|
| 60 |
+
p['box_qty'] = 1
|
| 61 |
if 'stocks' not in p:
|
| 62 |
p['stocks'] = {}
|
| 63 |
if 'colors' in p:
|
|
|
|
| 136 |
rolls = int(req.form.get('rolls', 0))
|
| 137 |
except ValueError:
|
| 138 |
rolls = 0
|
| 139 |
+
try:
|
| 140 |
+
box_qty = int(req.form.get('box_qty', 1))
|
| 141 |
+
except ValueError:
|
| 142 |
+
box_qty = 1
|
| 143 |
|
| 144 |
data['products'].insert(0, {
|
| 145 |
'id': str(time.time()),
|
|
|
|
| 147 |
'description': req.form['description'],
|
| 148 |
'price': price,
|
| 149 |
'photo': photo,
|
| 150 |
+
'box_qty': box_qty,
|
| 151 |
'stocks': {loc_id: rolls}
|
| 152 |
})
|
| 153 |
+
|
| 154 |
+
elif action == 'edit' and role == 'admin':
|
| 155 |
+
idx = int(req.form['index'])
|
| 156 |
+
if 0 <= idx < len(data['products']):
|
| 157 |
+
p = data['products'][idx]
|
| 158 |
+
p['name'] = req.form.get('name', p['name'])
|
| 159 |
+
p['description'] = req.form.get('description', p.get('description', ''))
|
| 160 |
+
try:
|
| 161 |
+
p['price'] = int(req.form.get('price', p.get('price', 0)))
|
| 162 |
+
except ValueError:
|
| 163 |
+
pass
|
| 164 |
+
try:
|
| 165 |
+
p['box_qty'] = int(req.form.get('box_qty', p.get('box_qty', 1)))
|
| 166 |
+
except ValueError:
|
| 167 |
+
pass
|
| 168 |
+
photo = req.form.get('photo', '')
|
| 169 |
+
if photo:
|
| 170 |
+
p['photo'] = photo
|
| 171 |
|
| 172 |
elif action == 'delete' and role == 'admin':
|
| 173 |
idx = int(req.form['index'])
|
|
|
|
| 284 |
.product-info { padding: 12px; display: flex; flex-direction: column; flex: 1; }
|
| 285 |
.product-title { font-size: 1rem; font-weight: 600; margin-bottom: 4px; line-height: 1.2; }
|
| 286 |
.product-price { font-size: 1.1rem; font-weight: 700; color: var(--primary); margin-bottom: 4px; }
|
| 287 |
+
.product-stock { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 4px; }
|
| 288 |
.product-actions { display: flex; gap: 8px; align-items: center; margin-top: auto; }
|
| 289 |
.qty-input { width: 60px; padding: 8px; border: 1px solid var(--border); border-radius: 8px; text-align: center; font-weight: 600; outline: none; transition: border-color 0.2s; }
|
| 290 |
.qty-input:focus { border-color: var(--primary); }
|
|
|
|
| 414 |
<input type="text" name="name" class="form-control" placeholder="Название" required>
|
| 415 |
<textarea name="description" class="form-control" placeholder="Описание"></textarea>
|
| 416 |
<input type="number" name="price" class="form-control" placeholder="Цена (₸)" required>
|
| 417 |
+
<input type="number" name="box_qty" class="form-control" placeholder="Штук в коробке" value="1" required>
|
| 418 |
<label style="display:block; margin-bottom:8px; font-size:0.9rem; color:var(--text-muted);">Фото товара (не обязательно)</label>
|
| 419 |
+
<input type="file" accept="image/*" class="form-control" onchange="processImage(this, 'photo_b64', 'photo_preview')">
|
| 420 |
<input type="hidden" name="photo" id="photo_b64">
|
| 421 |
<img id="photo_preview" style="display:none; width:100%; height:200px; object-fit:cover; border-radius:12px; margin-bottom:16px;">
|
| 422 |
<select name="loc_id" id="add_loc_select" class="form-control" required></select>
|
|
|
|
| 426 |
</div>
|
| 427 |
</div>
|
| 428 |
|
| 429 |
+
<div id="editModal" class="modal-overlay">
|
| 430 |
+
<div class="modal-content">
|
| 431 |
+
<button class="close-modal" onclick="closeModal('editModal')">×</button>
|
| 432 |
+
<h2>Изменить товар</h2>
|
| 433 |
+
<form method="POST" style="margin-top:16px;">
|
| 434 |
+
<input type="hidden" name="action" value="edit">
|
| 435 |
+
<input type="hidden" name="index" id="edit_index">
|
| 436 |
+
<input type="text" name="name" id="edit_name" class="form-control" placeholder="Название" required>
|
| 437 |
+
<textarea name="description" id="edit_desc" class="form-control" placeholder="Описание"></textarea>
|
| 438 |
+
<input type="number" name="price" id="edit_price" class="form-control" placeholder="Цена (₸)" required>
|
| 439 |
+
<input type="number" name="box_qty" id="edit_box_qty" class="form-control" placeholder="Штук в коробке" required>
|
| 440 |
+
<label style="display:block; margin-bottom:8px; font-size:0.9rem; color:var(--text-muted);">Новое фото (не обязательно)</label>
|
| 441 |
+
<input type="file" accept="image/*" class="form-control" onchange="processImage(this, 'edit_photo_b64', 'edit_photo_preview')">
|
| 442 |
+
<input type="hidden" name="photo" id="edit_photo_b64">
|
| 443 |
+
<img id="edit_photo_preview" style="display:none; width:100%; height:200px; object-fit:cover; border-radius:12px; margin-bottom:16px;">
|
| 444 |
+
<button type="submit" class="btn btn-success" style="width:100%; background:var(--primary);">Сохранить изменения</button>
|
| 445 |
+
</form>
|
| 446 |
+
</div>
|
| 447 |
+
</div>
|
| 448 |
+
|
| 449 |
<div id="incomeModal" class="modal-overlay">
|
| 450 |
<div class="modal-content">
|
| 451 |
<button class="close-modal" onclick="closeModal('incomeModal')">×</button>
|
|
|
|
| 586 |
<div class="product-title">${p.name}</div>
|
| 587 |
<div class="product-price">${p.price} ₸</div>
|
| 588 |
<div class="product-stock">В наличии: ${stock} шт</div>
|
| 589 |
+
<div class="product-stock" style="margin-bottom: 12px;">В коробке: ${p.box_qty || 1} шт</div>
|
| 590 |
`;
|
| 591 |
|
| 592 |
if (role !== 'public') {
|
| 593 |
if (activeLoc !== 'all') {
|
| 594 |
if (stock > 0) {
|
| 595 |
+
html += `<div class="product-actions" style="margin-bottom:8px;">
|
| 596 |
<input type="number" id="amt_${pIdx}" class="qty-input" value="1" min="1" max="${stock}">
|
| 597 |
+
<button class="btn btn-success" style="padding:8px; flex:1; font-size:0.9rem;" onclick="addToCart(${pIdx}, false)">В корзину</button>
|
| 598 |
+
</div>
|
| 599 |
+
<button class="btn btn-primary" style="width:100%; padding:8px; font-size:0.9rem; margin-bottom:8px;" onclick="addToCart(${pIdx}, true)">Коробка + (${p.box_qty || 1} шт)</button>`;
|
| 600 |
}
|
| 601 |
if (role === 'admin') {
|
| 602 |
+
html += `<div style="display:flex; gap:6px; margin-top:auto;">
|
| 603 |
<button class="btn btn-primary" style="padding:6px; flex:1; font-size:0.8rem;" onclick="openTransfer(${pIdx}, ${stock})">Переместить</button>
|
| 604 |
+
<button class="btn btn-success" style="padding:6px; flex:1; font-size:0.8rem; background:#eab308; color:#fff;" onclick="openEdit(${pIdx})">Изменить</button>
|
| 605 |
<button class="btn btn-danger" style="padding:6px; flex:1; font-size:0.8rem;" onclick="delProd(${pIdx})">Удалить</button>
|
| 606 |
</div>`;
|
| 607 |
}
|
| 608 |
} else {
|
| 609 |
html += `<div style="font-size:0.8rem; color:var(--text-muted); margin-top:auto;">Выберите конкретную локацию вверху для действий</div>`;
|
| 610 |
if (role === 'admin') {
|
| 611 |
+
html += `<div style="display:flex; gap:6px; margin-top:8px;">
|
| 612 |
+
<button class="btn btn-success" style="padding:6px; flex:1; font-size:0.8rem; background:#eab308; color:#fff;" onclick="openEdit(${pIdx})">Изменить</button>
|
| 613 |
+
<button class="btn btn-danger" style="padding:6px; flex:1; font-size:0.8rem;" onclick="delProd(${pIdx})">Удалить</button>
|
| 614 |
+
</div>`;
|
| 615 |
}
|
| 616 |
}
|
| 617 |
}
|
|
|
|
| 622 |
cat.innerHTML = htmlArr.join('');
|
| 623 |
}
|
| 624 |
|
| 625 |
+
function addToCart(pIdx, isBox = false) {
|
|
|
|
| 626 |
let p = data.products[pIdx];
|
| 627 |
+
let amt = isBox ? parseInt(p.box_qty || 1) : parseInt(document.getElementById('amt_'+pIdx).value);
|
| 628 |
let stock = p.stocks[activeLoc] || 0;
|
| 629 |
if(amt <= 0 || isNaN(amt)) return alert('Неверное количество');
|
| 630 |
|
|
|
|
| 728 |
openModal('transferModal');
|
| 729 |
}
|
| 730 |
|
| 731 |
+
function openEdit(pIdx) {
|
| 732 |
+
let p = data.products[pIdx];
|
| 733 |
+
document.getElementById('edit_index').value = pIdx;
|
| 734 |
+
document.getElementById('edit_name').value = p.name;
|
| 735 |
+
document.getElementById('edit_desc').value = p.description || '';
|
| 736 |
+
document.getElementById('edit_price').value = p.price;
|
| 737 |
+
document.getElementById('edit_box_qty').value = p.box_qty || 1;
|
| 738 |
+
document.getElementById('edit_photo_b64').value = '';
|
| 739 |
+
|
| 740 |
+
let preview = document.getElementById('edit_photo_preview');
|
| 741 |
+
if(p.photo) {
|
| 742 |
+
preview.src = p.photo;
|
| 743 |
+
preview.style.display = 'block';
|
| 744 |
+
} else {
|
| 745 |
+
preview.src = '';
|
| 746 |
+
preview.style.display = 'none';
|
| 747 |
+
}
|
| 748 |
+
openModal('editModal');
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
function processImage(input, b64Id, previewId) {
|
| 752 |
if (input.files && input.files[0]) {
|
| 753 |
const reader = new FileReader();
|
| 754 |
reader.onload = function(e) {
|
|
|
|
| 765 |
const ctx = canvas.getContext('2d');
|
| 766 |
ctx.drawImage(img, 0, 0, w, h);
|
| 767 |
const b64 = canvas.toDataURL('image/jpeg', 0.6);
|
| 768 |
+
document.getElementById(b64Id).value = b64;
|
| 769 |
+
const p = document.getElementById(previewId);
|
| 770 |
p.src = b64; p.style.display = 'block';
|
| 771 |
}
|
| 772 |
img.src = e.target.result;
|