Update app.py
Browse files
app.py
CHANGED
|
@@ -508,13 +508,6 @@ MAIN_HTML = '''
|
|
| 508 |
.settings-link-box { display: flex; gap: 8px; align-items: center; width: 100%; }
|
| 509 |
.settings-link-input { flex: 1; padding: 10px; border: 1px solid var(--border); border-radius: 8px; font-size: 0.85rem; background: var(--bg); outline: none; color: var(--text-main); }
|
| 510 |
|
| 511 |
-
.cart-float { display: none; position: fixed; bottom: 24px; right: 24px; background: var(--text-main); color: white; padding: 14px 24px; border-radius: 30px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); cursor: pointer; z-index: 100; font-weight: 600; align-items: center; gap: 8px; transition: transform 0.2s; }
|
| 512 |
-
.cart-float:active { transform: scale(0.95); }
|
| 513 |
-
|
| 514 |
-
.invoice-table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
|
| 515 |
-
.invoice-table th, .invoice-table td { padding: 10px; border-bottom: 1px solid var(--border); text-align: left; font-size:0.9rem; }
|
| 516 |
-
.invoice-total { font-size: 1.2rem; font-weight: 700; text-align: right; }
|
| 517 |
-
|
| 518 |
@media (min-width: 768px) {
|
| 519 |
body { padding: 24px; }
|
| 520 |
.top-actions { max-width: 400px; }
|
|
@@ -629,10 +622,6 @@ MAIN_HTML = '''
|
|
| 629 |
</div>
|
| 630 |
</div>
|
| 631 |
|
| 632 |
-
<div class="cart-float" id="cart_btn" onclick="openCart()">
|
| 633 |
-
Корзина (<span id="cart_count">0</span>)
|
| 634 |
-
</div>
|
| 635 |
-
|
| 636 |
<div id="imageZoomModal" class="modal-overlay" onclick="closeModal('imageZoomModal')" style="z-index:2000; align-items:center;">
|
| 637 |
<img id="zoomedImage" style="max-width:90%; max-height:90%; object-fit:contain; border-radius:8px;">
|
| 638 |
</div>
|
|
@@ -743,24 +732,6 @@ MAIN_HTML = '''
|
|
| 743 |
</div>
|
| 744 |
</div>
|
| 745 |
|
| 746 |
-
<div id="cartModal" class="modal-overlay">
|
| 747 |
-
<div class="modal-content">
|
| 748 |
-
<button class="close-modal" onclick="closeModal('cartModal')">×</button>
|
| 749 |
-
<h2>Корзина</h2>
|
| 750 |
-
<div id="cart_items" style="margin-bottom:16px; margin-top:16px;"></div>
|
| 751 |
-
<button id="checkout_btn" class="btn btn-success" style="width:100%;" onclick="submitCart()">Оформить накладную</button>
|
| 752 |
-
</div>
|
| 753 |
-
</div>
|
| 754 |
-
|
| 755 |
-
<div id="invoiceModal" class="modal-overlay">
|
| 756 |
-
<div class="modal-content">
|
| 757 |
-
<button class="close-modal" onclick="closeInvoice()">×</button>
|
| 758 |
-
<h2 style="text-align:center; margin-bottom:16px;">Накладная</h2>
|
| 759 |
-
<div id="invoice_content"></div>
|
| 760 |
-
<button class="btn btn-primary" style="width:100%; margin-top:20px;" onclick="closeInvoice()">Готово</button>
|
| 761 |
-
</div>
|
| 762 |
-
</div>
|
| 763 |
-
|
| 764 |
<form id="delForm" method="POST" style="display:none;" onsubmit="return showLoader(this)">
|
| 765 |
<input type="hidden" name="action" value="delete">
|
| 766 |
<input type="hidden" name="index" id="del_index">
|
|
@@ -773,7 +744,6 @@ MAIN_HTML = '''
|
|
| 773 |
const hostUrl = "{{ request.host_url }}";
|
| 774 |
const currentEmp = {{ emp|tojson }};
|
| 775 |
let activeLoc = 'all';
|
| 776 |
-
let cart = [];
|
| 777 |
let searchTimeout;
|
| 778 |
|
| 779 |
function showLoader(formElement) {
|
|
@@ -968,13 +938,9 @@ MAIN_HTML = '''
|
|
| 968 |
<div class="product-actions">
|
| 969 |
${varHtml}
|
| 970 |
<div style="display:flex; gap:6px; align-items:center; width:100%;">
|
| 971 |
-
<input type="number" id="amt_${pIdx}" class="qty-input" value="1" min="1">
|
| 972 |
-
<button class="btn btn-
|
| 973 |
-
<button class="btn btn-
|
| 974 |
-
</div>
|
| 975 |
-
<div style="display:flex; gap:6px; width:100%; margin-top:2px;">
|
| 976 |
-
<button class="btn btn-success" style="padding:8px 6px; font-size:0.75rem; flex:1; background:#059669;" onclick="addToCart(${pIdx}, true, 'retail')">Коробка (Розн)</button>
|
| 977 |
-
<button class="btn btn-primary" style="padding:8px 6px; font-size:0.75rem; flex:1; background:#4338ca;" onclick="addToCart(${pIdx}, true, 'wholesale')">Коробка (Опт)</button>
|
| 978 |
</div>
|
| 979 |
`;
|
| 980 |
if (role === 'admin') {
|
|
@@ -1086,149 +1052,65 @@ MAIN_HTML = '''
|
|
| 1086 |
}
|
| 1087 |
}
|
| 1088 |
|
| 1089 |
-
function
|
| 1090 |
let p = data.products[pIdx];
|
|
|
|
| 1091 |
let vSelect = document.getElementById('var_'+pIdx);
|
| 1092 |
let vIdx = (vSelect && vSelect.value !== '') ? parseInt(vSelect.value) : -1;
|
| 1093 |
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
let validLocs = (activeLoc === 'all')
|
| 1097 |
-
? ((role === 'employee' && currentEmp && currentEmp.shop_id)
|
| 1098 |
-
? [currentEmp.shop_id, ...data.settings.warehouses.map(w=>w.id)]
|
| 1099 |
-
: [...data.settings.warehouses.map(w=>w.id), ...data.settings.shops.map(s=>s.id)])
|
| 1100 |
-
: [activeLoc];
|
| 1101 |
-
|
| 1102 |
-
let getLocStock = (stocksObj) => {
|
| 1103 |
-
let s = 0;
|
| 1104 |
-
for (let l of validLocs) {
|
| 1105 |
-
if (stocksObj && stocksObj[l]) s += parseInt(stocksObj[l]);
|
| 1106 |
-
}
|
| 1107 |
-
return s;
|
| 1108 |
-
};
|
| 1109 |
|
| 1110 |
-
let stock =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1111 |
|
| 1112 |
-
if(amt
|
| 1113 |
-
if(activeLoc !== 'all' && amt > stock) return alert('Превышен остаток');
|
| 1114 |
|
| 1115 |
let basePrice = pType === 'wholesale' ? parseInt(p.price_wholesale) : parseInt(p.price_retail);
|
| 1116 |
if (role !== 'public') {
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
if (customPriceInput) basePrice = parseInt(customPriceInput.value) || basePrice;
|
| 1120 |
-
} else {
|
| 1121 |
-
let customPriceInput = document.getElementById('price_ret_' + pIdx);
|
| 1122 |
-
if (customPriceInput) basePrice = parseInt(customPriceInput.value) || basePrice;
|
| 1123 |
-
}
|
| 1124 |
}
|
| 1125 |
|
| 1126 |
let vName = '';
|
| 1127 |
let vPrice = 0;
|
| 1128 |
if (vIdx !== -1) {
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
vPrice = parseInt(variant.price) || 0;
|
| 1132 |
}
|
| 1133 |
|
| 1134 |
let finalPrice = basePrice + vPrice;
|
| 1135 |
let finalName = p.name + vName + (pType === 'wholesale' ? ' [Опт]' : ' [Розн]');
|
| 1136 |
|
| 1137 |
-
|
| 1138 |
-
let cartTotalForProduct = cart.filter(i => i.pIndex === pIdx && i.vIndex === vIdx && i.loc === activeLoc).reduce((s, i) => s + i.amt, 0);
|
| 1139 |
|
| 1140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1141 |
|
| 1142 |
-
if(existing) {
|
| 1143 |
-
existing.amt += amt;
|
| 1144 |
-
} else {
|
| 1145 |
-
cart.push({pIndex: pIdx, vIndex: vIdx, pName: finalName, price: finalPrice, amt: amt, loc: activeLoc});
|
| 1146 |
-
}
|
| 1147 |
-
updateCartUI();
|
| 1148 |
-
}
|
| 1149 |
-
|
| 1150 |
-
function updateCartUI() {
|
| 1151 |
-
let totalItems = cart.reduce((sum, item) => sum + item.amt, 0);
|
| 1152 |
-
const btn = document.getElementById('cart_btn');
|
| 1153 |
-
document.getElementById('cart_count').innerText = totalItems;
|
| 1154 |
-
btn.style.display = totalItems > 0 ? 'flex' : 'none';
|
| 1155 |
-
}
|
| 1156 |
-
|
| 1157 |
-
function openCart() {
|
| 1158 |
-
let html = '';
|
| 1159 |
-
let total = 0;
|
| 1160 |
-
cart.forEach((c, i) => {
|
| 1161 |
-
let sum = c.amt * c.price;
|
| 1162 |
-
total += sum;
|
| 1163 |
-
html += `<div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid var(--border); padding:10px 0;">
|
| 1164 |
-
<div>
|
| 1165 |
-
<div style="font-weight:600;">${c.pName}</div>
|
| 1166 |
-
<div style="font-size:0.85rem; color:var(--text-muted);">${c.amt} шт x ${c.price} ₸</div>
|
| 1167 |
-
</div>
|
| 1168 |
-
<div style="display:flex; align-items:center; gap:12px;">
|
| 1169 |
-
<span style="font-weight:700;">${sum} ₸</span>
|
| 1170 |
-
<button style="border:none; background:var(--danger); color:white; border-radius:6px; padding:6px 10px; cursor:pointer; font-weight:700;" onclick="removeFromCart(${i})">×</button>
|
| 1171 |
-
</div>
|
| 1172 |
-
</div>`;
|
| 1173 |
-
});
|
| 1174 |
-
html += `<div style="text-align:right; font-size:1.2rem; font-weight:700; margin-top:16px;">Итого: ${total} ₸</div>`;
|
| 1175 |
-
document.getElementById('cart_items').innerHTML = html;
|
| 1176 |
-
openModal('cartModal');
|
| 1177 |
-
}
|
| 1178 |
-
|
| 1179 |
-
function removeFromCart(idx) {
|
| 1180 |
-
cart.splice(idx, 1);
|
| 1181 |
-
updateCartUI();
|
| 1182 |
-
if(cart.length === 0) {
|
| 1183 |
-
closeModal('cartModal');
|
| 1184 |
-
} else {
|
| 1185 |
-
openCart();
|
| 1186 |
-
}
|
| 1187 |
-
}
|
| 1188 |
-
|
| 1189 |
-
async function submitCart() {
|
| 1190 |
-
if (cart.length === 0) return;
|
| 1191 |
-
const btn = document.getElementById('checkout_btn');
|
| 1192 |
-
if (btn.disabled) return;
|
| 1193 |
-
btn.disabled = true;
|
| 1194 |
document.getElementById('loader').style.display = 'flex';
|
| 1195 |
-
|
| 1196 |
let fd = new FormData();
|
| 1197 |
fd.append('action', 'checkout');
|
| 1198 |
-
fd.append('cart', JSON.stringify(
|
| 1199 |
|
| 1200 |
try {
|
| 1201 |
await fetch(window.location.href, {method: 'POST', body: fd});
|
| 1202 |
-
|
| 1203 |
-
let dateStr = new Date().toLocaleString('ru-RU');
|
| 1204 |
-
let html = `<p style="color:var(--text-muted); margin-bottom:16px;">Дата: ${dateStr}</p>
|
| 1205 |
-
<table class="invoice-table">
|
| 1206 |
-
<tr><th>Товар</th><th>Кол.</th><th>Сумма</th></tr>`;
|
| 1207 |
-
let total = 0;
|
| 1208 |
-
cart.forEach(c => {
|
| 1209 |
-
let sum = c.amt * c.price;
|
| 1210 |
-
total += sum;
|
| 1211 |
-
html += `<tr><td>${c.pName}</td><td>${c.amt}</td><td>${sum} ₸</td></tr>`;
|
| 1212 |
-
});
|
| 1213 |
-
html += `</table><div class="invoice-total">Итого к оплате: ${total} ₸</div>`;
|
| 1214 |
-
|
| 1215 |
-
document.getElementById('invoice_content').innerHTML = html;
|
| 1216 |
-
closeModal('cartModal');
|
| 1217 |
-
openModal('invoiceModal');
|
| 1218 |
-
cart = [];
|
| 1219 |
-
updateCartUI();
|
| 1220 |
} catch (e) {
|
| 1221 |
-
alert('Ошибка при
|
| 1222 |
-
} finally {
|
| 1223 |
document.getElementById('loader').style.display = 'none';
|
| 1224 |
-
btn.disabled = false;
|
| 1225 |
}
|
| 1226 |
}
|
| 1227 |
|
| 1228 |
-
function closeInvoice() {
|
| 1229 |
-
window.location.reload();
|
| 1230 |
-
}
|
| 1231 |
-
|
| 1232 |
function openTransfer(pIdx) {
|
| 1233 |
let p = data.products[pIdx];
|
| 1234 |
let vSelect = document.getElementById('var_' + pIdx);
|
|
@@ -1308,7 +1190,9 @@ MAIN_HTML = '''
|
|
| 1308 |
locSelect.innerHTML = [...data.settings.warehouses, ...data.settings.shops].map(l => `<option value="${l.id}">${l.name}</option>`).join('');
|
| 1309 |
|
| 1310 |
let lastLoc = localStorage.getItem('lastFormLoc');
|
| 1311 |
-
if (
|
|
|
|
|
|
|
| 1312 |
locSelect.value = lastLoc;
|
| 1313 |
}
|
| 1314 |
|
|
@@ -1320,7 +1204,7 @@ MAIN_HTML = '''
|
|
| 1320 |
document.getElementById('edit_variants_container').innerHTML = '';
|
| 1321 |
if (p.variants && p.variants.length > 0) {
|
| 1322 |
p.variants.forEach(v => {
|
| 1323 |
-
addVariantField('edit_variants_container', v.name, v.price,
|
| 1324 |
});
|
| 1325 |
}
|
| 1326 |
|
|
|
|
| 508 |
.settings-link-box { display: flex; gap: 8px; align-items: center; width: 100%; }
|
| 509 |
.settings-link-input { flex: 1; padding: 10px; border: 1px solid var(--border); border-radius: 8px; font-size: 0.85rem; background: var(--bg); outline: none; color: var(--text-main); }
|
| 510 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
@media (min-width: 768px) {
|
| 512 |
body { padding: 24px; }
|
| 513 |
.top-actions { max-width: 400px; }
|
|
|
|
| 622 |
</div>
|
| 623 |
</div>
|
| 624 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
<div id="imageZoomModal" class="modal-overlay" onclick="closeModal('imageZoomModal')" style="z-index:2000; align-items:center;">
|
| 626 |
<img id="zoomedImage" style="max-width:90%; max-height:90%; object-fit:contain; border-radius:8px;">
|
| 627 |
</div>
|
|
|
|
| 732 |
</div>
|
| 733 |
</div>
|
| 734 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
<form id="delForm" method="POST" style="display:none;" onsubmit="return showLoader(this)">
|
| 736 |
<input type="hidden" name="action" value="delete">
|
| 737 |
<input type="hidden" name="index" id="del_index">
|
|
|
|
| 744 |
const hostUrl = "{{ request.host_url }}";
|
| 745 |
const currentEmp = {{ emp|tojson }};
|
| 746 |
let activeLoc = 'all';
|
|
|
|
| 747 |
let searchTimeout;
|
| 748 |
|
| 749 |
function showLoader(formElement) {
|
|
|
|
| 938 |
<div class="product-actions">
|
| 939 |
${varHtml}
|
| 940 |
<div style="display:flex; gap:6px; align-items:center; width:100%;">
|
| 941 |
+
<input type="number" id="amt_${pIdx}" class="qty-input" value="1" min="1" style="flex:1;">
|
| 942 |
+
<button class="btn btn-danger" style="padding:8px 6px; font-size:0.75rem; flex:1;" onclick="directSell(${pIdx}, 'retail')">Списать (Розн)</button>
|
| 943 |
+
<button class="btn btn-danger" style="padding:8px 6px; font-size:0.75rem; flex:1;" onclick="directSell(${pIdx}, 'wholesale')">Списать (Опт)</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 944 |
</div>
|
| 945 |
`;
|
| 946 |
if (role === 'admin') {
|
|
|
|
| 1052 |
}
|
| 1053 |
}
|
| 1054 |
|
| 1055 |
+
async function directSell(pIdx, pType) {
|
| 1056 |
let p = data.products[pIdx];
|
| 1057 |
+
let amt = parseInt(document.getElementById('amt_'+pIdx).value);
|
| 1058 |
let vSelect = document.getElementById('var_'+pIdx);
|
| 1059 |
let vIdx = (vSelect && vSelect.value !== '') ? parseInt(vSelect.value) : -1;
|
| 1060 |
|
| 1061 |
+
if(amt <= 0 || isNaN(amt)) return alert('Неверное количество');
|
| 1062 |
+
if(activeLoc === 'all') return alert('Выберите локацию для списания');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1063 |
|
| 1064 |
+
let stock = 0;
|
| 1065 |
+
if (vIdx !== -1) {
|
| 1066 |
+
stock = parseInt(p.variants[vIdx].stocks[activeLoc] || 0);
|
| 1067 |
+
} else {
|
| 1068 |
+
stock = parseInt(p.stocks[activeLoc] || 0);
|
| 1069 |
+
}
|
| 1070 |
|
| 1071 |
+
if(amt > stock) return alert('Превышен остаток');
|
|
|
|
| 1072 |
|
| 1073 |
let basePrice = pType === 'wholesale' ? parseInt(p.price_wholesale) : parseInt(p.price_retail);
|
| 1074 |
if (role !== 'public') {
|
| 1075 |
+
let customPriceInput = document.getElementById(pType === 'wholesale' ? 'price_whl_' + pIdx : 'price_ret_' + pIdx);
|
| 1076 |
+
if (customPriceInput) basePrice = parseInt(customPriceInput.value) || basePrice;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1077 |
}
|
| 1078 |
|
| 1079 |
let vName = '';
|
| 1080 |
let vPrice = 0;
|
| 1081 |
if (vIdx !== -1) {
|
| 1082 |
+
vName = ' (' + p.variants[vIdx].name + ')';
|
| 1083 |
+
vPrice = parseInt(p.variants[vIdx].price) || 0;
|
|
|
|
| 1084 |
}
|
| 1085 |
|
| 1086 |
let finalPrice = basePrice + vPrice;
|
| 1087 |
let finalName = p.name + vName + (pType === 'wholesale' ? ' [Опт]' : ' [Розн]');
|
| 1088 |
|
| 1089 |
+
if (!confirm(`Списать ${amt} шт. товара "${finalName}"?`)) return;
|
|
|
|
| 1090 |
|
| 1091 |
+
let singleItemCart = [{
|
| 1092 |
+
pIndex: pIdx,
|
| 1093 |
+
vIndex: vIdx,
|
| 1094 |
+
pName: finalName,
|
| 1095 |
+
price: finalPrice,
|
| 1096 |
+
amt: amt,
|
| 1097 |
+
loc: activeLoc
|
| 1098 |
+
}];
|
| 1099 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1100 |
document.getElementById('loader').style.display = 'flex';
|
|
|
|
| 1101 |
let fd = new FormData();
|
| 1102 |
fd.append('action', 'checkout');
|
| 1103 |
+
fd.append('cart', JSON.stringify(singleItemCart));
|
| 1104 |
|
| 1105 |
try {
|
| 1106 |
await fetch(window.location.href, {method: 'POST', body: fd});
|
| 1107 |
+
window.location.reload();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1108 |
} catch (e) {
|
| 1109 |
+
alert('Ошибка при списании');
|
|
|
|
| 1110 |
document.getElementById('loader').style.display = 'none';
|
|
|
|
| 1111 |
}
|
| 1112 |
}
|
| 1113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1114 |
function openTransfer(pIdx) {
|
| 1115 |
let p = data.products[pIdx];
|
| 1116 |
let vSelect = document.getElementById('var_' + pIdx);
|
|
|
|
| 1190 |
locSelect.innerHTML = [...data.settings.warehouses, ...data.settings.shops].map(l => `<option value="${l.id}">${l.name}</option>`).join('');
|
| 1191 |
|
| 1192 |
let lastLoc = localStorage.getItem('lastFormLoc');
|
| 1193 |
+
if (activeLoc !== 'all' && locSelect.querySelector(`option[value="${activeLoc}"]`)) {
|
| 1194 |
+
locSelect.value = activeLoc;
|
| 1195 |
+
} else if (lastLoc && locSelect.querySelector(`option[value="${lastLoc}"]`)) {
|
| 1196 |
locSelect.value = lastLoc;
|
| 1197 |
}
|
| 1198 |
|
|
|
|
| 1204 |
document.getElementById('edit_variants_container').innerHTML = '';
|
| 1205 |
if (p.variants && p.variants.length > 0) {
|
| 1206 |
p.variants.forEach(v => {
|
| 1207 |
+
addVariantField('edit_variants_container', v.name, v.price, '');
|
| 1208 |
});
|
| 1209 |
}
|
| 1210 |
|