Kgshop commited on
Commit
7b02684
·
verified ·
1 Parent(s): 9c21034

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +188 -601
app.py CHANGED
@@ -39,50 +39,36 @@ def load_data():
39
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
40
  data = json.load(file)
41
  if not isinstance(data, dict):
42
- data = {'products': [], 'history': []}
43
  if 'settings' not in data:
44
- data['settings'] = {
45
- 'warehouses': [{'id': 'w_main', 'name': 'Magazyn główny'}],
46
- 'shops': [{'id': 's_main', 'name': 'Główny sklep'}],
47
- 'employees': []
48
- }
49
  if 'products' not in data:
50
  data['products'] = []
51
  if 'history' not in data:
52
  data['history'] = []
 
 
53
 
54
  for p in data['products']:
55
- if 'price_retail' not in p:
56
- p['price_retail'] = p.get('price', 0)
57
- if 'price_wholesale' not in p:
58
- p['price_wholesale'] = p.get('price', 0)
59
  if 'variants' not in p:
60
  p['variants'] = []
61
  if 'photo' not in p:
62
  p['photo'] = ''
63
- if 'box_qty' not in p:
64
- p['box_qty'] = 1
65
  if 'stocks' not in p:
66
- p['stocks'] = {}
67
- if 'colors' in p:
68
- for c in p['colors']:
69
- for loc, amt in c.get('stocks', {}).items():
70
- p['stocks'][loc] = p['stocks'].get(loc, 0) + amt
71
- del p['colors']
72
 
73
  for v in p['variants']:
74
  if 'stocks' not in v:
75
- v['stocks'] = {}
76
  return data
77
  except Exception:
78
  return {
79
- 'settings': {
80
- 'warehouses': [{'id': 'w_main', 'name': 'Magazyn główny'}],
81
- 'shops': [{'id': 's_main', 'name': 'Główny sklep'}],
82
- 'employees': []
83
- },
84
  'products': [],
85
- 'history': []
 
86
  }
87
 
88
  def save_data(data):
@@ -112,81 +98,59 @@ def periodic_backup():
112
  upload_db_to_hf()
113
  time.sleep(800)
114
 
115
- def add_history(data, action, product, amount, price, from_loc, to_loc, emp_name):
116
  data['history'].insert(0, {
117
  'time': get_almaty_time(),
118
  'action': action,
119
  'product': product,
120
  'amount': amount,
121
- 'price': price,
122
- 'from_loc_name': from_loc,
123
- 'to_loc_name': to_loc,
124
  'emp_name': emp_name
125
  })
126
  data['history'] = data['history'][:1000]
127
 
128
- def get_loc_name(data, loc_id):
129
- for w in data['settings']['warehouses']:
130
- if w['id'] == loc_id: return w['name']
131
- for s in data['settings']['shops']:
132
- if s['id'] == loc_id: return s['name']
133
- return loc_id
134
-
135
  def handle_post(req, data, role, emp):
136
  action = req.form.get('action')
137
- emp_name = emp['name'] if emp else 'Admin'
 
 
 
 
 
138
 
139
  if action == 'add' and role == 'admin':
140
- loc_id = req.form.get('loc_id')
141
- try:
142
- price_retail = int(req.form.get('price_retail', 0))
143
- except ValueError:
144
- price_retail = 0
145
- try:
146
- price_wholesale = int(req.form.get('price_wholesale', 0))
147
- except ValueError:
148
- price_wholesale = 0
149
-
150
  photo = req.form.get('photo', '')
151
  try:
152
  rolls = int(req.form.get('rolls', 0))
153
  except ValueError:
154
  rolls = 0
155
  try:
156
- box_qty = int(req.form.get('box_qty', 1))
157
  except ValueError:
158
- box_qty = 1
159
 
160
  v_names = req.form.getlist('v_name[]')
161
- v_prices = req.form.getlist('v_price[]')
162
  v_stocks = req.form.getlist('v_stock[]')
163
 
164
  variants = []
165
  has_variants = False
166
- for n, pr, st in zip(v_names, v_prices, v_stocks):
167
  if n.strip():
168
  has_variants = True
169
- try:
170
- pr_val = int(pr)
171
- except ValueError:
172
- pr_val = 0
173
  try:
174
  st_val = int(st)
175
  except ValueError:
176
  st_val = 0
177
- variants.append({'name': n.strip(), 'price': pr_val, 'stocks': {loc_id: st_val}})
178
 
179
- p_stocks = {} if has_variants else {loc_id: rolls}
180
 
181
  data['products'].insert(0, {
182
  'id': str(time.time()),
183
  'name': req.form['name'],
184
  'description': req.form['description'],
185
- 'price_retail': price_retail,
186
- 'price_wholesale': price_wholesale,
187
  'variants': variants,
188
  'photo': photo,
189
- 'box_qty': box_qty,
190
  'stocks': p_stocks
191
  })
192
 
@@ -197,54 +161,33 @@ def handle_post(req, data, role, emp):
197
  p['name'] = req.form.get('name', p['name'])
198
  p['description'] = req.form.get('description', p.get('description', ''))
199
  try:
200
- p['price_retail'] = int(req.form.get('price_retail', p.get('price_retail', 0)))
201
- except ValueError:
202
- pass
203
- try:
204
- p['price_wholesale'] = int(req.form.get('price_wholesale', p.get('price_wholesale', 0)))
205
- except ValueError:
206
- pass
207
- try:
208
- p['box_qty'] = int(req.form.get('box_qty', p.get('box_qty', 1)))
209
  except ValueError:
210
  pass
211
 
212
- edit_loc_id = req.form.get('edit_loc_id')
213
  try:
214
  new_rolls = int(req.form.get('rolls', 0))
215
  except ValueError:
216
  new_rolls = 0
217
 
218
  v_names = req.form.getlist('v_name[]')
219
- v_prices = req.form.getlist('v_price[]')
220
  v_stocks = req.form.getlist('v_stock[]')
221
 
222
  new_variants = []
223
- for i, (n, pr, st) in enumerate(zip(v_names, v_prices, v_stocks)):
224
  if n.strip():
225
- existing_stocks = p['variants'][i].get('stocks', {}) if 'variants' in p and i < len(p['variants']) else {}
226
  try:
227
  st_val = int(st)
228
  except ValueError:
229
  st_val = 0
230
- if edit_loc_id:
231
- existing_stocks[edit_loc_id] = st_val
232
- try:
233
- pr_val = int(pr)
234
- except ValueError:
235
- pr_val = 0
236
-
237
- new_variants.append({'name': n.strip(), 'price': pr_val, 'stocks': existing_stocks})
238
 
239
  p['variants'] = new_variants
240
 
241
- if new_variants:
242
- pass
243
- else:
244
  if 'stocks' not in p:
245
  p['stocks'] = {}
246
- if edit_loc_id:
247
- p['stocks'][edit_loc_id] = new_rolls
248
 
249
  photo = req.form.get('photo', '')
250
  if photo:
@@ -255,12 +198,11 @@ def handle_post(req, data, role, emp):
255
  if 0 <= idx < len(data['products']):
256
  del_name = data['products'][idx]['name']
257
  data['products'].pop(idx)
258
- add_history(data, 'Usunięcie', del_name, 0, 0, '', '', emp_name)
259
 
260
  elif action == 'income' and role == 'admin':
261
  idx = int(req.form['product_index'])
262
  amt = int(req.form['rolls'])
263
- loc_id = req.form['loc_id']
264
  v_idx_str = req.form.get('v_index')
265
  p = data['products'][idx]
266
 
@@ -270,96 +212,60 @@ def handle_post(req, data, role, emp):
270
  if 0 <= v_idx < len(p.get('variants', [])):
271
  v = p['variants'][v_idx]
272
  if 'stocks' not in v: v['stocks'] = {}
273
- v['stocks'][loc_id] = v['stocks'].get(loc_id, 0) + amt
274
  prod_name_history += f" ({v['name']})"
275
  else:
276
  if 'stocks' not in p: p['stocks'] = {}
277
- p['stocks'][loc_id] = p.get('stocks', {}).get(loc_id, 0) + amt
278
 
279
- add_history(data, 'Dostawa', prod_name_history, amt, p.get('price_wholesale', 0), '', get_loc_name(data, loc_id), emp_name)
280
-
281
- elif action == 'transfer' and role == 'admin':
282
- idx = int(req.form['pIndex'])
283
- try:
284
- v_idx = int(req.form.get('vIndex', -1))
285
- except ValueError:
286
- v_idx = -1
287
- amt = int(req.form['amt'])
288
- from_loc = req.form['from_loc']
289
- to_loc = req.form['to_loc']
290
- p = data['products'][idx]
291
-
292
- prod_name_history = p['name']
293
- if v_idx != -1 and 0 <= v_idx < len(p.get('variants', [])):
294
- v = p['variants'][v_idx]
295
- av = v.get('stocks', {}).get(from_loc, 0)
296
- if amt > av: amt = av
297
- if amt > 0:
298
- v['stocks'][from_loc] -= amt
299
- v['stocks'][to_loc] = v.get('stocks', {}).get(to_loc, 0) + amt
300
- prod_name_history += f" ({v['name']})"
301
- else:
302
- av = p.get('stocks', {}).get(from_loc, 0)
303
- if amt > av: amt = av
304
- if amt > 0:
305
- p['stocks'][from_loc] -= amt
306
- p['stocks'][to_loc] = p.get('stocks', {}).get(to_loc, 0) + amt
307
-
308
- if amt > 0:
309
- from_name = get_loc_name(data, from_loc)
310
- to_name = get_loc_name(data, to_loc)
311
- add_history(data, 'Przeniesienie', prod_name_history, amt, 0, from_name, to_name, emp_name)
312
 
313
- elif action == 'checkout':
314
  cart_data = req.form.get('cart', '[]')
315
  cart = json.loads(cart_data)
316
- for item in cart:
317
- idx = item.get('pIndex')
318
- v_idx = item.get('vIndex', -1)
319
- amt = item.get('amt')
320
- loc = item.get('loc')
321
- pName = item.get('pName')
322
- itemPrice = item.get('price', 0)
323
-
324
- if 0 <= idx < len(data['products']):
325
- p = data['products'][idx]
326
- if v_idx != -1 and 0 <= v_idx < len(p.get('variants', [])):
327
- v = p['variants'][v_idx]
328
- av = v.get('stocks', {}).get(loc, 0)
329
- if amt > av: amt = av
330
- if amt > 0:
331
- v['stocks'][loc] -= amt
332
- loc_name = get_loc_name(data, loc)
333
- add_history(data, 'Sprzedaż', pName, amt, itemPrice, loc_name, '', emp_name)
334
- else:
335
- av = p.get('stocks', {}).get(loc, 0)
336
- if amt > av: amt = av
337
- if amt > 0:
338
- p['stocks'][loc] -= amt
339
- loc_name = get_loc_name(data, loc)
340
- add_history(data, 'Sprzedaż', pName, amt, itemPrice, loc_name, '', emp_name)
341
-
342
- elif action == 'add_loc' and role == 'admin':
343
- l_type = req.form['l_type']
344
- l_name = req.form['l_name']
345
- l_id = f"loc_{int(time.time()*1000)}"
346
- data['settings'][l_type].append({'id': l_id, 'name': l_name})
347
-
348
- elif action == 'del_loc' and role == 'admin':
349
- l_type = req.form['l_type']
350
- l_id = req.form['l_id']
351
- data['settings'][l_type] = [x for x in data['settings'][l_type] if x['id'] != l_id]
352
 
353
  elif action == 'add_emp' and role == 'admin':
354
  e_name = req.form['e_name']
355
- e_shop = req.form['e_shop']
356
  e_id = str(uuid.uuid4())[:8]
357
- data['settings']['employees'].append({'id': e_id, 'name': e_name, 'token': e_id, 'shop_id': e_shop})
358
 
359
  elif action == 'del_emp' and role == 'admin':
360
  e_id = req.form['e_id']
361
  data['settings']['employees'] = [x for x in data['settings']['employees'] if x['id'] != e_id]
362
 
 
363
  MAIN_HTML = '''
364
  <!DOCTYPE html>
365
  <html lang="pl">
@@ -398,12 +304,6 @@ MAIN_HTML = '''
398
  .nav-btn { flex: 0 0 auto; padding: 10px 16px; border-radius: 8px; border: none; background: #e5e7eb; color: var(--text-muted); font-weight: 600; font-size: 0.9rem; cursor: pointer; transition: background 0.2s, color 0.2s; }
399
  .nav-btn.active { background: var(--primary); color: white; }
400
 
401
- .loc-nav { display: flex; gap: 8px; margin-bottom: 16px; overflow-x: auto; padding-bottom: 4px; }
402
- .loc-btn { flex: 0 0 auto; padding: 8px 14px; border-radius: 20px; border: 1px solid var(--border); background: var(--surface); color: var(--text-main); font-weight: 500; font-size: 0.85rem; cursor: pointer; transition: all 0.2s; }
403
- .loc-btn.active { background: var(--text-main); color: white; border-color: var(--text-main); }
404
- .loc-btn.has-search-stock { border-color: var(--success) !important; color: var(--success) !important; background: #ecfdf5 !important; font-weight: 700; }
405
- .loc-btn.has-search-stock.active { background: var(--success) !important; color: white !important; }
406
-
407
  .search-box { width: 100%; padding: 14px; border: 1px solid var(--border); border-radius: 12px; outline: none; font-size: 1rem; margin-bottom: 16px; transition: border-color 0.2s; }
408
  .search-box:focus { border-color: var(--primary); }
409
 
@@ -415,8 +315,7 @@ MAIN_HTML = '''
415
  .product-img { width: 100%; height: 100%; object-fit: cover; }
416
  .product-info { display: flex; flex-direction: column; flex: 1; justify-content: center; min-width: 0; }
417
  .product-title { font-size: 0.95rem; font-weight: 600; line-height: 1.2; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
418
- .product-price { font-size: 0.85rem; font-weight: 700; color: var(--primary); }
419
- .product-stock { font-size: 0.75rem; color: var(--text-muted); }
420
  .product-actions { display: flex; flex-direction: column; gap: 6px; width: 100%; margin-top: 4px; }
421
 
422
  .qty-input { width: 60px; padding: 6px; border: 1px solid var(--border); border-radius: 6px; text-align: center; font-weight: 600; outline: none; font-size: 0.85rem; transition: border-color 0.2s; }
@@ -448,15 +347,11 @@ MAIN_HTML = '''
448
 
449
  .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; }
450
  .cart-float:active { transform: scale(0.95); }
451
-
452
- .invoice-table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
453
- .invoice-table th, .invoice-table td { padding: 10px; border-bottom: 1px solid var(--border); text-align: left; font-size:0.9rem; }
454
- .invoice-total { font-size: 1.2rem; font-weight: 700; text-align: right; }
455
 
456
  @media (min-width: 768px) {
457
  body { padding: 24px; }
458
  .top-actions { max-width: 400px; }
459
- .settings-grid { grid-template-columns: repeat(3, 1fr); gap: 24px; }
460
  .modal-overlay { align-items: center; }
461
  .modal-content { border-radius: 20px; max-width: 500px; }
462
  }
@@ -468,7 +363,7 @@ MAIN_HTML = '''
468
  </div>
469
 
470
  <div class="container">
471
- <h1 id="main_title">Zarządzanie zapasami</h1>
472
 
473
  {% if role == 'admin' %}
474
  <div class="top-actions">
@@ -477,6 +372,7 @@ MAIN_HTML = '''
477
  </div>
478
  <div class="main-nav">
479
  <button id="navbtn_catalog" class="nav-btn active" onclick="switchNav('catalog')">Katalog</button>
 
480
  <button id="navbtn_reports" class="nav-btn" onclick="switchNav('reports')">Raporty</button>
481
  <button id="navbtn_settings" class="nav-btn" onclick="switchNav('settings')">Ustawienia</button>
482
  </div>
@@ -487,11 +383,14 @@ MAIN_HTML = '''
487
  {% endif %}
488
 
489
  <div id="section_catalog">
490
- <div class="loc-nav" id="loc_nav_container"></div>
491
  <input type="text" id="search" class="search-box" placeholder="Szukaj produktu..." oninput="onSearchInput()">
492
  <div id="catalog"></div>
493
  </div>
494
 
 
 
 
 
495
  <div id="section_reports" style="display:none;">
496
  <div style="display:flex; flex-wrap:wrap; gap:12px; margin-bottom:12px;">
497
  <input type="date" id="hist_from" class="form-control" style="flex:1; margin:0;" onchange="renderHistory()">
@@ -501,65 +400,32 @@ MAIN_HTML = '''
501
  <select id="hist_emp" class="form-control" style="flex:1; margin:0;" onchange="renderHistory()"></select>
502
  <select id="hist_action_type" class="form-control" style="flex:1; margin:0;" onchange="renderHistory()">
503
  <option value="all">Wszystkie operacje</option>
504
- <option value="Sprzedaż">Wszystkie sprzedaże</option>
505
- <option value="Sprzedaż_retail">Sprzedaż (Detaliczna)</option>
506
- <option value="Sprzedaż_wholesale">Sprzedaż (Hurtowa)</option>
507
  <option value="Dostawa">Dostawy</option>
508
- <option value="Przeniesienie">Przeniesienia</option>
509
  <option value="Usunięcie">Usunięcia</option>
510
  </select>
511
  </div>
512
- <h3 id="total_sales" style="margin-bottom:16px; color:var(--primary);"></h3>
513
  <div id="history_container"></div>
514
  </div>
515
 
516
  <div id="section_settings" style="display:none;">
517
  <div class="settings-grid">
518
  <div class="settings-card">
519
- <h3>Katalogi publiczne</h3>
520
  <div class="settings-list-item">
521
- <div class="settings-list-item-header"><span style="font-weight:600;">Katalog detaliczny</span></div>
522
  <div class="settings-link-box">
523
- <input type="text" class="settings-link-input" value="{{ request.host_url }}catalog/retail" readonly onclick="this.select()">
524
- <button type="button" onclick="navigator.clipboard.writeText('{{ request.host_url }}catalog/retail');alert('Link skopiowany')" class="btn btn-primary" style="padding:8px 12px; font-size:0.85rem; flex:0 0 auto;">Kopiuj</button>
525
- </div>
526
- </div>
527
- <div class="settings-list-item">
528
- <div class="settings-list-item-header"><span style="font-weight:600;">Katalog hurtowy</span></div>
529
- <div class="settings-link-box">
530
- <input type="text" class="settings-link-input" value="{{ request.host_url }}catalog/wholesale" readonly onclick="this.select()">
531
- <button type="button" onclick="navigator.clipboard.writeText('{{ request.host_url }}catalog/wholesale');alert('Link skopiowany')" class="btn btn-primary" style="padding:8px 12px; font-size:0.85rem; flex:0 0 auto;">Kopiuj</button>
532
  </div>
533
  </div>
534
  </div>
535
 
536
- <div class="settings-card">
537
- <h3>Magazyny</h3>
538
- <div id="set_warehouses"></div>
539
- <form method="POST" onsubmit="return showLoader(this)" style="display:flex; gap:8px; margin-top:12px;">
540
- <input type="hidden" name="action" value="add_loc">
541
- <input type="hidden" name="l_type" value="warehouses">
542
- <input type="text" name="l_name" class="form-control" style="margin:0;" placeholder="Nazwa" required>
543
- <button type="submit" class="btn btn-primary" style="flex:0 0 auto; padding:0 20px;">+</button>
544
- </form>
545
- </div>
546
- <div class="settings-card">
547
- <h3>Sklepy</h3>
548
- <div id="set_shops"></div>
549
- <form method="POST" onsubmit="return showLoader(this)" style="display:flex; gap:8px; margin-top:12px;">
550
- <input type="hidden" name="action" value="add_loc">
551
- <input type="hidden" name="l_type" value="shops">
552
- <input type="text" name="l_name" class="form-control" style="margin:0;" placeholder="Nazwa" required>
553
- <button type="submit" class="btn btn-primary" style="flex:0 0 auto; padding:0 20px;">+</button>
554
- </form>
555
- </div>
556
  <div class="settings-card">
557
  <h3>Pracownicy</h3>
558
  <div id="set_emps"></div>
559
  <form method="POST" onsubmit="return showLoader(this)" style="display:flex; flex-direction:column; gap:8px; margin-top:12px;">
560
  <input type="hidden" name="action" value="add_emp">
561
  <input type="text" name="e_name" class="form-control" style="margin:0;" placeholder="Imię pracownika" required>
562
- <select name="e_shop" id="emp_shop_select" class="form-control" style="margin:0;" required></select>
563
  <button type="submit" class="btn btn-primary" style="padding:10px;">Dodaj pracownika</button>
564
  </form>
565
  </div>
@@ -568,7 +434,7 @@ MAIN_HTML = '''
568
  </div>
569
 
570
  <div class="cart-float" id="cart_btn" onclick="openCart()">
571
- Koszyk (<span id="cart_count">0</span>)
572
  </div>
573
 
574
  <div id="imageZoomModal" class="modal-overlay" onclick="closeModal('imageZoomModal')" style="z-index:2000; align-items:center;">
@@ -584,22 +450,15 @@ MAIN_HTML = '''
584
  <input type="text" name="name" class="form-control" placeholder="Nazwa" required>
585
  <textarea name="description" class="form-control" placeholder="Opis"></textarea>
586
 
587
- <div style="display:flex; gap:8px; margin-bottom:12px;">
588
- <input type="number" name="price_retail" class="form-control" style="margin-bottom:0;" placeholder="Cena detaliczna (₸)" required>
589
- <input type="number" name="price_wholesale" class="form-control" style="margin-bottom:0;" placeholder="Cena hurtowa (₸)" required>
590
- </div>
591
-
592
- <select name="loc_id" id="add_loc_select" class="form-control" required></select>
593
-
594
  <label style="display:block; margin-bottom:8px; font-weight:600; font-size:0.9rem;">Warianty produktu (opcjonalnie)</label>
595
  <div id="add_variants_container"></div>
596
  <button type="button" class="btn btn-success" style="padding:8px; font-size:0.85rem; margin-bottom:12px; width:100%; background:#eab308;" onclick="addVariantField('add_variants_container')">+ Dodaj wariant</button>
597
 
598
  <div id="add_base_stock_container">
599
- <input type="number" name="rolls" id="add_rolls" class="form-control" placeholder="Ilość początkowa" value="0" required>
600
  </div>
601
 
602
- <input type="number" name="box_qty" class="form-control" placeholder="Sztuk w kartonie" value="1" required>
603
 
604
  <label style="display:block; margin-bottom:8px; font-size:0.9rem; color:var(--text-muted);">Zdjęcie produktu (opcjonalnie)</label>
605
  <input type="file" accept="image/*" class="form-control" onchange="processImage(this, 'photo_b64', 'photo_preview')">
@@ -621,14 +480,7 @@ MAIN_HTML = '''
621
  <input type="text" name="name" id="edit_name" class="form-control" placeholder="Nazwa" required>
622
  <textarea name="description" id="edit_desc" class="form-control" placeholder="Opis"></textarea>
623
 
624
- <div style="display:flex; gap:8px; margin-bottom:12px;">
625
- <input type="number" name="price_retail" id="edit_price_retail" class="form-control" style="margin-bottom:0;" placeholder="Cena detaliczna (₸)" required>
626
- <input type="number" name="price_wholesale" id="edit_price_wholesale" class="form-control" style="margin-bottom:0;" placeholder="Cena hurtowa (₸)" required>
627
- </div>
628
-
629
- <label style="display:block; margin-bottom:4px; font-weight:600; font-size:0.9rem;">Stany magazynowe w lokalizacjach</label>
630
- <select name="edit_loc_id" id="edit_loc_id" class="form-control" onchange="loadEditStocks()"></select>
631
-
632
  <div id="edit_base_stock_container" style="margin-bottom:12px;">
633
  <input type="number" name="rolls" id="edit_rolls" class="form-control" placeholder="Całkowity stan (bez wariantów)">
634
  </div>
@@ -637,7 +489,7 @@ MAIN_HTML = '''
637
  <div id="edit_variants_container"></div>
638
  <button type="button" class="btn btn-success" style="padding:8px; font-size:0.85rem; margin-bottom:12px; width:100%; background:#eab308;" onclick="addVariantField('edit_variants_container')">+ Dodaj wariant</button>
639
 
640
- <input type="number" name="box_qty" id="edit_box_qty" class="form-control" placeholder="Sztuk w kartonie" required>
641
 
642
  <label style="display:block; margin-bottom:8px; font-size:0.9rem; color:var(--text-muted);">Nowe zdjęcie (opcjonalnie)</label>
643
  <input type="file" accept="image/*" class="form-control" onchange="processImage(this, 'edit_photo_b64', 'edit_photo_preview')">
@@ -652,10 +504,9 @@ MAIN_HTML = '''
652
  <div id="incomeModal" class="modal-overlay">
653
  <div class="modal-content">
654
  <button class="close-modal" onclick="closeModal('incomeModal')">×</button>
655
- <h2>Dostawa</h2>
656
  <form method="POST" style="margin-top:16px;" onsubmit="return showLoader(this)">
657
  <input type="hidden" name="action" value="income">
658
- <select name="loc_id" id="inc_loc_select" class="form-control" required></select>
659
  <select name="product_index" id="inc_product" class="form-control" onchange="updateIncVariants()" required></select>
660
  <select name="v_index" id="inc_variant" class="form-control" style="display:none;"></select>
661
  <input type="number" name="rolls" class="form-control" placeholder="Ilość" min="1" required>
@@ -664,38 +515,12 @@ MAIN_HTML = '''
664
  </div>
665
  </div>
666
 
667
- <div id="transferModal" class="modal-overlay">
668
- <div class="modal-content">
669
- <button class="close-modal" onclick="closeModal('transferModal')">×</button>
670
- <h2>Przeniesienie</h2>
671
- <form method="POST" style="margin-top:16px;" onsubmit="return showLoader(this)">
672
- <input type="hidden" name="action" value="transfer">
673
- <input type="hidden" name="pIndex" id="trans_pIndex">
674
- <input type="hidden" name="vIndex" id="trans_vIndex">
675
- <input type="hidden" name="from_loc" id="trans_from">
676
- <p id="trans_info" style="margin-bottom:16px; font-weight:600;"></p>
677
- <select name="to_loc" id="trans_dest_select" class="form-control" required></select>
678
- <input type="number" name="amt" id="trans_amt" class="form-control" placeholder="Ilość" min="1" required>
679
- <button type="submit" class="btn btn-primary" style="width:100%;">Przenieś</button>
680
- </form>
681
- </div>
682
- </div>
683
-
684
  <div id="cartModal" class="modal-overlay">
685
  <div class="modal-content">
686
  <button class="close-modal" onclick="closeModal('cartModal')">×</button>
687
- <h2>Koszyk</h2>
688
  <div id="cart_items" style="margin-bottom:16px; margin-top:16px;"></div>
689
- <button id="checkout_btn" class="btn btn-success" style="width:100%;" onclick="submitCart()">Wystaw rachunek</button>
690
- </div>
691
- </div>
692
-
693
- <div id="invoiceModal" class="modal-overlay">
694
- <div class="modal-content">
695
- <button class="close-modal" onclick="closeInvoice()">×</button>
696
- <h2 style="text-align:center; margin-bottom:16px;">Rachunek</h2>
697
- <div id="invoice_content"></div>
698
- <button class="btn btn-primary" style="width:100%; margin-top:20px;" onclick="closeInvoice()">Gotowe</button>
699
  </div>
700
  </div>
701
 
@@ -707,10 +532,8 @@ MAIN_HTML = '''
707
  <script>
708
  const data = {{ data|tojson }};
709
  const role = "{{ role }}";
710
- const price_type = "{{ price_type }}";
711
  const hostUrl = "{{ request.host_url }}";
712
- const currentEmp = {{ emp|tojson }};
713
- let activeLoc = 'all';
714
  let cart = [];
715
  let searchTimeout;
716
 
@@ -724,63 +547,30 @@ MAIN_HTML = '''
724
  }
725
 
726
  function init() {
727
- if (role === 'admin' || role === 'employee') {
728
- document.getElementById('main_title').innerText = role === 'admin' ? 'Panel administratora' : 'Panel pracownika';
729
- } else {
730
- document.getElementById('main_title').innerText = price_type === 'wholesale' ? 'Katalog hurtowy' : 'Katalog detaliczny';
731
- }
732
- buildLocNav();
733
  if (role === 'admin') {
 
734
  buildSettings();
735
  buildIncProducts();
736
- buildLocSelects();
737
  buildEmpSelect();
738
  initHistoryDates();
 
 
 
 
739
  }
740
  renderCatalog();
741
  }
742
 
743
  function switchNav(nav) {
744
- ['catalog', 'reports', 'settings'].forEach(id => {
745
  const el = document.getElementById('section_' + id);
746
  if(el) el.style.display = (id === nav) ? 'block' : 'none';
747
  });
748
  document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
749
- document.getElementById('navbtn_' + nav).classList.add('active');
 
750
  if (nav === 'reports') renderHistory();
751
- }
752
-
753
- function buildLocNav() {
754
- const cont = document.getElementById('loc_nav_container');
755
- if(!cont) return;
756
- let html = `<button class="loc-btn active" id="locbtn_all" onclick="setLoc('all')">Wszystkie lokalizacje</button>`;
757
-
758
- data.settings.warehouses.forEach(w => {
759
- html += `<button class="loc-btn" id="locbtn_${w.id}" onclick="setLoc('${w.id}')">${w.name}</button>`;
760
- });
761
-
762
- let allowedShops = data.settings.shops;
763
- if (role === 'employee' && currentEmp && currentEmp.shop_id) {
764
- allowedShops = data.settings.shops.filter(s => s.id === currentEmp.shop_id);
765
- }
766
-
767
- allowedShops.forEach(s => {
768
- html += `<button class="loc-btn" id="locbtn_${s.id}" onclick="setLoc('${s.id}')">${s.name}</button>`;
769
- });
770
- cont.innerHTML = html;
771
- }
772
-
773
- function setLoc(locId) {
774
- activeLoc = locId;
775
- document.querySelectorAll('.loc-btn').forEach(b => b.classList.remove('active'));
776
- document.getElementById('locbtn_' + locId).classList.add('active');
777
- renderCatalog();
778
- }
779
-
780
- function getLocName(locId) {
781
- let locs = [...data.settings.warehouses, ...data.settings.shops];
782
- let found = locs.find(l => l.id === locId);
783
- return found ? found.name : locId;
784
  }
785
 
786
  function zoomImage(src) {
@@ -798,113 +588,46 @@ MAIN_HTML = '''
798
  const q = document.getElementById('search').value.toLowerCase().trim();
799
  const cat = document.getElementById('catalog');
800
  let htmlArr = [];
801
- let locationsWithStock = new Set();
802
-
803
- let validLocs = (activeLoc === 'all')
804
- ? ((role === 'employee' && currentEmp && currentEmp.shop_id)
805
- ? [currentEmp.shop_id, ...data.settings.warehouses.map(w=>w.id)]
806
- : [...data.settings.warehouses.map(w=>w.id), ...data.settings.shops.map(s=>s.id)])
807
- : [activeLoc];
808
-
809
- let getLocStock = (stocksObj) => {
810
- let s = 0;
811
- for (let l of validLocs) {
812
- if (stocksObj && stocksObj[l]) s += stocksObj[l];
813
- }
814
- return s;
815
- };
816
 
817
  data.products.forEach((p, pIdx) => {
818
  let match = p.name.toLowerCase().includes(q) || (p.description && p.description.toLowerCase().includes(q));
819
  if (!match) return;
820
 
821
- if (q !== '') {
822
- if (p.variants && p.variants.length > 0) {
823
- p.variants.forEach(v => {
824
- for (let loc in v.stocks) {
825
- if (v.stocks[loc] > 0) locationsWithStock.add(loc);
826
- }
827
- });
828
- } else {
829
- for (let loc in p.stocks) {
830
- if (p.stocks[loc] > 0) locationsWithStock.add(loc);
831
- }
832
- }
833
- }
834
-
835
  let stock = 0;
836
  if (p.variants && p.variants.length > 0) {
837
- p.variants.forEach(v => { stock += getLocStock(v.stocks); });
838
  } else {
839
- stock += getLocStock(p.stocks);
840
  }
841
 
842
- if (activeLoc !== 'all' && stock <= 0) return;
843
-
844
- let imgHtml = p.photo ? `<img src="${p.photo}" class="product-img">` : `<span style="font-size:0.6rem; color:#9ca3af;">Brak zdjęcia</span>`;
845
  let imgClick = p.photo ? `onclick="zoomImage('${p.photo}')"` : ``;
846
 
847
- let priceHtml = '';
848
- if (role !== 'public') {
849
- priceHtml = `
850
- <div style="display:flex; gap:6px; align-items:center; font-size:0.8rem; margin:4px 0; flex-wrap:wrap;">
851
- Detal: <input type="number" id="price_ret_${pIdx}" value="${p.price_retail}" style="width:65px; padding:2px 4px; border:1px solid var(--border); border-radius:4px; font-weight:600;" onclick="this.select()"> ₸
852
- | Hurt: <input type="number" id="price_whl_${pIdx}" value="${p.price_wholesale}" style="width:65px; padding:2px 4px; border:1px solid var(--border); border-radius:4px; font-weight:600;" onclick="this.select()"> ₸
853
- </div>
854
- `;
855
- } else {
856
- priceHtml = `
857
- <div class="product-price" style="margin:4px 0;">
858
- ${price_type === 'both' ? `Detal: ${p.price_retail} ₸ | Hurt: ${p.price_wholesale} ₸` : (price_type === 'retail' ? `${p.price_retail} ₸` : `${p.price_wholesale} ₸`)}
859
- </div>
860
- `;
861
  }
862
 
863
- let actionsHtml = '';
864
- if (role !== 'public') {
865
- if (activeLoc !== 'all') {
866
- if (stock > 0) {
867
- let varHtml = '';
868
- if (p.variants && p.variants.length > 0) {
869
- varHtml = `<select id="var_${pIdx}" class="form-control" style="padding:6px; font-size:0.8rem; margin-bottom:4px; height:auto;" onchange="updateMaxQty(${pIdx})">
870
- ${p.variants.map((v, i) => `<option value="${i}">${v.name} (+${v.price} ₸)</option>`).join('')}
871
- </select>`;
872
- }
873
-
874
- actionsHtml += `
875
- <div class="product-actions">
876
- ${varHtml}
877
- <div style="display:flex; gap:6px; align-items:center; width:100%;">
878
- <input type="number" id="amt_${pIdx}" class="qty-input" value="1" min="1">
879
- <button class="btn btn-success" style="padding:8px 6px; font-size:0.75rem; flex:1;" onclick="addToCart(${pIdx}, false, 'retail')">Do koszyka (Detal)</button>
880
- <button class="btn btn-primary" style="padding:8px 6px; font-size:0.75rem; flex:1;" onclick="addToCart(${pIdx}, false, 'wholesale')">Do koszyka (Hurt)</button>
881
- </div>
882
- <div style="display:flex; gap:6px; width:100%; margin-top:2px;">
883
- <button class="btn btn-success" style="padding:8px 6px; font-size:0.75rem; flex:1; background:#059669;" onclick="addToCart(${pIdx}, true, 'retail')">Karton (Detal)</button>
884
- <button class="btn btn-primary" style="padding:8px 6px; font-size:0.75rem; flex:1; background:#4338ca;" onclick="addToCart(${pIdx}, true, 'wholesale')">Karton (Hurt)</button>
885
- </div>
886
- `;
887
- if (role === 'admin') {
888
- actionsHtml += `<div style="display:flex; gap:6px; margin-top:2px; width:100%;">
889
- <button class="btn btn-primary" style="padding:8px 6px; flex:1; font-size:0.75rem;" onclick="openTransfer(${pIdx})">Przenieś</button>
890
- <button class="btn btn-success" style="padding:8px 6px; flex:1; font-size:0.75rem; background:#eab308;" onclick="openEdit(${pIdx})">Edytuj</button>
891
- <button class="btn btn-danger" style="padding:8px 6px; flex:1; font-size:0.75rem;" onclick="delProd(${pIdx})">Usuń</button>
892
- </div>`;
893
- }
894
- actionsHtml += `</div>`;
895
- }
896
- } else {
897
- actionsHtml += `<div class="product-actions">
898
- <div style="font-size:0.8rem; color:var(--text-muted); text-align:center; margin:8px 0;">Wybierz lokalizację, aby wykonać akcję</div>`;
899
- if (role === 'admin') {
900
- actionsHtml += `<div style="display:flex; gap:6px; width:100%;">
901
- <button class="btn btn-success" style="padding:8px; flex:1; font-size:0.75rem; background:#eab308;" onclick="openEdit(${pIdx})">Edytuj</button>
902
- <button class="btn btn-danger" style="padding:8px; flex:1; font-size:0.75rem;" onclick="delProd(${pIdx})">Usuń</button>
903
- </div>`;
904
- }
905
- actionsHtml += `</div>`;
906
- }
907
  }
 
908
 
909
  let html = `<div class="product">
910
  <div class="product-main">
@@ -912,8 +635,7 @@ MAIN_HTML = '''
912
  <div class="product-info">
913
  <div class="product-title">${p.name}</div>
914
  ${p.description ? `<div style="font-size:0.75rem; color:var(--text-muted);">${p.description}</div>` : ''}
915
- ${priceHtml}
916
- <div class="product-stock">Dostępność: <span id="stock_display_${pIdx}">${stock}</span> szt. | W kartonie: ${p.box_qty || 1} szt.</div>
917
  </div>
918
  </div>
919
  ${actionsHtml}
@@ -921,16 +643,6 @@ MAIN_HTML = '''
921
  htmlArr.push(html);
922
  });
923
  cat.innerHTML = htmlArr.join('');
924
-
925
- document.querySelectorAll('.loc-btn').forEach(btn => {
926
- if (btn.id === 'locbtn_all') return;
927
- let locId = btn.id.replace('locbtn_', '');
928
- if (q !== '' && locationsWithStock.has(locId)) {
929
- btn.classList.add('has-search-stock');
930
- } else {
931
- btn.classList.remove('has-search-stock');
932
- }
933
- });
934
 
935
  data.products.forEach((p, pIdx) => {
936
  if(document.getElementById('stock_display_' + pIdx)) {
@@ -943,29 +655,15 @@ MAIN_HTML = '''
943
  let p = data.products[pIdx];
944
  let vSelect = document.getElementById('var_' + pIdx);
945
  let qtyInput = document.getElementById('amt_' + pIdx);
946
-
947
- let validLocs = (activeLoc === 'all')
948
- ? ((role === 'employee' && currentEmp && currentEmp.shop_id)
949
- ? [currentEmp.shop_id, ...data.settings.warehouses.map(w=>w.id)]
950
- : [...data.settings.warehouses.map(w=>w.id), ...data.settings.shops.map(s=>s.id)])
951
- : [activeLoc];
952
-
953
- let getLocStock = (stocksObj) => {
954
- let s = 0;
955
- for (let l of validLocs) {
956
- if (stocksObj && stocksObj[l]) s += stocksObj[l];
957
- }
958
- return s;
959
- };
960
 
961
  let max = 0;
962
  if (vSelect && vSelect.value !== "") {
963
  let vIdx = parseInt(vSelect.value);
964
- max = getLocStock(p.variants[vIdx].stocks);
965
  } else if (p.variants && p.variants.length > 0) {
966
- max = getLocStock(p.variants[0].stocks);
967
  } else {
968
- max = getLocStock(p.stocks);
969
  }
970
 
971
  if(qtyInput) qtyInput.max = max;
@@ -975,63 +673,28 @@ MAIN_HTML = '''
975
  }
976
  }
977
 
978
- function addToCart(pIdx, isBox, pType) {
979
  let p = data.products[pIdx];
980
  let vSelect = document.getElementById('var_'+pIdx);
981
  let vIdx = (vSelect && vSelect.value !== '') ? parseInt(vSelect.value) : -1;
 
982
 
983
- let amt = isBox ? parseInt(p.box_qty || 1) : parseInt(document.getElementById('amt_'+pIdx).value);
984
-
985
- let validLocs = (activeLoc === 'all')
986
- ? ((role === 'employee' && currentEmp && currentEmp.shop_id)
987
- ? [currentEmp.shop_id, ...data.settings.warehouses.map(w=>w.id)]
988
- : [...data.settings.warehouses.map(w=>w.id), ...data.settings.shops.map(s=>s.id)])
989
- : [activeLoc];
990
-
991
- let getLocStock = (stocksObj) => {
992
- let s = 0;
993
- for (let l of validLocs) {
994
- if (stocksObj && stocksObj[l]) s += stocksObj[l];
995
- }
996
- return s;
997
- };
998
-
999
- let stock = (vIdx !== -1) ? getLocStock(p.variants[vIdx].stocks) : getLocStock(p.stocks);
1000
 
1001
  if(amt <= 0 || isNaN(amt)) return alert('Nieprawidłowa ilość');
1002
- if(activeLoc !== 'all' && amt > stock) return alert('Przekroczono stan magazynowy');
1003
 
1004
- let basePrice = pType === 'wholesale' ? p.price_wholesale : p.price_retail;
1005
- if (role !== 'public') {
1006
- if (pType === 'wholesale') {
1007
- let customPriceInput = document.getElementById('price_whl_' + pIdx);
1008
- if (customPriceInput) basePrice = parseInt(customPriceInput.value) || basePrice;
1009
- } else {
1010
- let customPriceInput = document.getElementById('price_ret_' + pIdx);
1011
- if (customPriceInput) basePrice = parseInt(customPriceInput.value) || basePrice;
1012
- }
1013
- }
1014
 
1015
- let vName = '';
1016
- let vPrice = 0;
1017
- if (vIdx !== -1) {
1018
- let variant = p.variants[vIdx];
1019
- vName = ' (' + variant.name + ')';
1020
- vPrice = variant.price;
1021
- }
1022
 
1023
- let finalPrice = basePrice + vPrice;
1024
- let finalName = p.name + vName + (pType === 'wholesale' ? ' [Hurt]' : ' [Detal]');
1025
-
1026
- let existing = cart.find(i => i.pIndex === pIdx && i.vIndex === vIdx && i.loc === activeLoc && i.pName === finalName && i.price === finalPrice);
1027
- let cartTotalForProduct = cart.filter(i => i.pIndex === pIdx && i.vIndex === vIdx && i.loc === activeLoc).reduce((s, i) => s + i.amt, 0);
1028
-
1029
- if(activeLoc !== 'all' && (cartTotalForProduct + amt > stock)) return alert('Przekroczono stan w koszyku');
1030
 
1031
  if(existing) {
1032
  existing.amt += amt;
1033
  } else {
1034
- cart.push({pIndex: pIdx, vIndex: vIdx, pName: finalName, price: finalPrice, amt: amt, loc: activeLoc});
1035
  }
1036
  updateCartUI();
1037
  }
@@ -1045,22 +708,17 @@ MAIN_HTML = '''
1045
 
1046
  function openCart() {
1047
  let html = '';
1048
- let total = 0;
1049
  cart.forEach((c, i) => {
1050
- let sum = c.amt * c.price;
1051
- total += sum;
1052
  html += `<div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid var(--border); padding:10px 0;">
1053
  <div>
1054
  <div style="font-weight:600;">${c.pName}</div>
1055
- <div style="font-size:0.85rem; color:var(--text-muted);">${c.amt} szt. x ${c.price} ₸</div>
1056
  </div>
1057
  <div style="display:flex; align-items:center; gap:12px;">
1058
- <span style="font-weight:700;">${sum} ₸</span>
1059
  <button style="border:none; background:var(--danger); color:white; border-radius:6px; padding:6px 10px; cursor:pointer; font-weight:700;" onclick="removeFromCart(${i})">×</button>
1060
  </div>
1061
  </div>`;
1062
  });
1063
- html += `<div style="text-align:right; font-size:1.2rem; font-weight:700; margin-top:16px;">Razem: ${total} ₸</div>`;
1064
  document.getElementById('cart_items').innerHTML = html;
1065
  openModal('cartModal');
1066
  }
@@ -1083,29 +741,16 @@ MAIN_HTML = '''
1083
  document.getElementById('loader').style.display = 'flex';
1084
 
1085
  let fd = new FormData();
1086
- fd.append('action', 'checkout');
1087
  fd.append('cart', JSON.stringify(cart));
1088
 
1089
  try {
1090
  await fetch(window.location.href, {method: 'POST', body: fd});
1091
-
1092
- let dateStr = new Date().toLocaleString('pl-PL');
1093
- let html = `<p style="color:var(--text-muted); margin-bottom:16px;">Data: ${dateStr}</p>
1094
- <table class="invoice-table">
1095
- <tr><th>Produkt</th><th>Ilość</th><th>Kwota</th></tr>`;
1096
- let total = 0;
1097
- cart.forEach(c => {
1098
- let sum = c.amt * c.price;
1099
- total += sum;
1100
- html += `<tr><td>${c.pName}</td><td>${c.amt}</td><td>${sum} ₸</td></tr>`;
1101
- });
1102
- html += `</table><div class="invoice-total">Razem do zapłaty: ${total} ₸</div>`;
1103
-
1104
- document.getElementById('invoice_content').innerHTML = html;
1105
- closeModal('cartModal');
1106
- openModal('invoiceModal');
1107
  cart = [];
1108
  updateCartUI();
 
 
1109
  } catch (e) {
1110
  alert('Błąd podczas składania zamówienia');
1111
  } finally {
@@ -1114,51 +759,49 @@ MAIN_HTML = '''
1114
  }
1115
  }
1116
 
1117
- function closeInvoice() {
1118
- window.location.reload();
1119
- }
1120
-
1121
- function openTransfer(pIdx) {
1122
- let p = data.products[pIdx];
1123
- let vSelect = document.getElementById('var_' + pIdx);
1124
- let vIdx = (vSelect && vSelect.value !== '') ? parseInt(vSelect.value) : -1;
1125
-
1126
- let maxStock = 0;
1127
- let vName = '';
1128
- if(vIdx !== -1) {
1129
- maxStock = p.variants[vIdx].stocks[activeLoc] || 0;
1130
- vName = ' (' + p.variants[vIdx].name + ')';
1131
- } else {
1132
- maxStock = p.stocks[activeLoc] || 0;
1133
  }
1134
-
1135
- if(maxStock <= 0) return alert('Brak w magazynie');
1136
-
1137
- document.getElementById('trans_pIndex').value = pIdx;
1138
- document.getElementById('trans_vIndex').value = vIdx;
1139
- document.getElementById('trans_from').value = activeLoc;
1140
- document.getElementById('trans_info').innerText = p.name + vName + ' (Dostępne: ' + maxStock + ' szt.)';
1141
- document.getElementById('trans_amt').max = maxStock;
1142
- document.getElementById('trans_amt').value = 1;
1143
-
1144
- const dest = document.getElementById('trans_dest_select');
1145
- dest.innerHTML = '';
1146
- [...data.settings.warehouses, ...data.settings.shops].forEach(l => {
1147
- if (l.id !== activeLoc) {
1148
- dest.innerHTML += `<option value="${l.id}">${l.name}</option>`;
1149
- }
 
 
 
 
 
 
 
 
1150
  });
1151
- openModal('transferModal');
1152
  }
1153
 
1154
- function addVariantField(containerId, name='', price='', stock='') {
1155
  const cont = document.getElementById(containerId);
1156
  const div = document.createElement('div');
1157
  div.style = "display:flex; gap:8px; margin-bottom:8px; align-items:center;";
1158
  div.innerHTML = `
1159
  <input type="text" name="v_name[]" class="form-control" style="margin:0; flex:1;" placeholder="Nazwa" value="${name}" required>
1160
- <input type="number" name="v_price[]" class="form-control" style="margin:0; width:90px;" placeholder="Cena" value="${price}" required>
1161
- <input type="number" name="v_stock[]" class="form-control v-stock-input" style="margin:0; width:90px;" placeholder="Stan" value="${stock}" required>
1162
  <button type="button" style="border:none; background:var(--danger); color:white; border-radius:8px; padding:10px; cursor:pointer;" onclick="removeVariantField(this, '${containerId}')">×</button>
1163
  `;
1164
  cont.appendChild(div);
@@ -1188,18 +831,13 @@ MAIN_HTML = '''
1188
  document.getElementById('edit_index').value = pIdx;
1189
  document.getElementById('edit_name').value = p.name;
1190
  document.getElementById('edit_desc').value = p.description || '';
1191
- document.getElementById('edit_price_retail').value = p.price_retail || p.price || 0;
1192
- document.getElementById('edit_price_wholesale').value = p.price_wholesale || p.price || 0;
1193
- document.getElementById('edit_box_qty').value = p.box_qty || 1;
1194
  document.getElementById('edit_photo_b64').value = '';
1195
 
1196
- let locSelect = document.getElementById('edit_loc_id');
1197
- locSelect.innerHTML = [...data.settings.warehouses, ...data.settings.shops].map(l => `<option value="${l.id}">${l.name}</option>`).join('');
1198
-
1199
  document.getElementById('edit_variants_container').innerHTML = '';
1200
  if (p.variants && p.variants.length > 0) {
1201
  p.variants.forEach(v => {
1202
- addVariantField('edit_variants_container', v.name, v.price, 0);
1203
  });
1204
  }
1205
 
@@ -1219,18 +857,10 @@ MAIN_HTML = '''
1219
  function loadEditStocks() {
1220
  let pIdx = document.getElementById('edit_index').value;
1221
  let p = data.products[pIdx];
1222
- let locId = document.getElementById('edit_loc_id').value;
1223
-
1224
  if (p.variants && p.variants.length > 0) {
1225
- let stockInputs = document.querySelectorAll('#edit_variants_container .v-stock-input');
1226
- p.variants.forEach((v, i) => {
1227
- if(stockInputs[i]) {
1228
- stockInputs[i].value = (v.stocks && v.stocks[locId]) ? v.stocks[locId] : 0;
1229
- }
1230
- });
1231
  document.getElementById('edit_base_stock_container').style.display = 'none';
1232
  } else {
1233
- document.getElementById('edit_rolls').value = (p.stocks && p.stocks[locId]) ? p.stocks[locId] : 0;
1234
  document.getElementById('edit_base_stock_container').style.display = 'block';
1235
  }
1236
  }
@@ -1276,20 +906,11 @@ MAIN_HTML = '''
1276
  function closeModal(id) { document.getElementById(id).classList.remove('active'); }
1277
 
1278
  function buildSettings() {
1279
- const wCont = document.getElementById('set_warehouses');
1280
- wCont.innerHTML = data.settings.warehouses.map(w => `<div class="settings-list-item"><div class="settings-list-item-header"><span style="font-weight:600;">${w.name}</span><form method="POST" onsubmit="return showLoader(this)" style="margin:0;"><input type="hidden" name="action" value="del_loc"><input type="hidden" name="l_type" value="warehouses"><input type="hidden" name="l_id" value="${w.id}"><button type="submit" class="btn btn-danger" style="padding:6px 12px; font-size:0.85rem;">Usuń</button></form></div></div>`).join('');
1281
-
1282
- const sCont = document.getElementById('set_shops');
1283
- sCont.innerHTML = data.settings.shops.map(s => `<div class="settings-list-item"><div class="settings-list-item-header"><span style="font-weight:600;">${s.name}</span><form method="POST" onsubmit="return showLoader(this)" style="margin:0;"><input type="hidden" name="action" value="del_loc"><input type="hidden" name="l_type" value="shops"><input type="hidden" name="l_id" value="${s.id}"><button type="submit" class="btn btn-danger" style="padding:6px 12px; font-size:0.85rem;">Usuń</button></form></div></div>`).join('');
1284
-
1285
  const eCont = document.getElementById('set_emps');
1286
- eCont.innerHTML = data.settings.employees.map(e => {
1287
- let shop = data.settings.shops.find(s => s.id === e.shop_id);
1288
- let shopName = shop ? shop.name : 'Nie przypisano';
1289
- return `
1290
  <div class="settings-list-item">
1291
  <div class="settings-list-item-header">
1292
- <span style="font-weight:600;">${e.name} <small style="color:var(--text-muted)">(${shopName})</small></span>
1293
  <form method="POST" onsubmit="return showLoader(this)" style="margin:0;">
1294
  <input type="hidden" name="action" value="del_emp">
1295
  <input type="hidden" name="e_id" value="${e.id}">
@@ -1301,17 +922,7 @@ MAIN_HTML = '''
1301
  <button type="button" onclick="navigator.clipboard.writeText('${hostUrl}emp/${e.token}');alert('Link skopiowany')" class="btn btn-primary" style="padding:8px 12px; font-size:0.85rem; flex:0 0 auto;">Kopiuj</button>
1302
  </div>
1303
  </div>
1304
- `}).join('');
1305
- }
1306
-
1307
- function buildLocSelects() {
1308
- const locs = [...data.settings.warehouses, ...data.settings.shops];
1309
- const opts = locs.map(l => `<option value="${l.id}">${l.name}</option>`).join('');
1310
- document.getElementById('add_loc_select').innerHTML = opts;
1311
- document.getElementById('inc_loc_select').innerHTML = opts;
1312
-
1313
- const shopOpts = data.settings.shops.map(s => `<option value="${s.id}">${s.name}</option>`).join('');
1314
- document.getElementById('emp_shop_select').innerHTML = '<option value="">-- Wybierz sklep --</option>' + shopOpts;
1315
  }
1316
 
1317
  function buildIncProducts() {
@@ -1341,7 +952,7 @@ MAIN_HTML = '''
1341
 
1342
  function buildEmpSelect() {
1343
  const sel = document.getElementById('hist_emp');
1344
- sel.innerHTML = '<option value="all">Wszyscy pracownicy</option><option value="Admin">Admin</option>' + data.settings.employees.map(e => `<option value="${e.name}">${e.name}</option>`).join('');
1345
  }
1346
 
1347
  function initHistoryDates() {
@@ -1360,45 +971,30 @@ MAIN_HTML = '''
1360
  const eFilter = document.getElementById('hist_emp').value;
1361
  const actFilter = document.getElementById('hist_action_type').value;
1362
 
1363
- let total = 0;
1364
-
1365
  data.history.forEach(h => {
1366
  const pts = h.time.split(' ')[0].split('.');
1367
  const hd = new Date(`${pts[2]}-${pts[1]}-${pts[0]}T${h.time.split(' ')[1]}`);
1368
 
1369
  let passAction = true;
1370
  if (actFilter !== 'all') {
1371
- if (actFilter === 'Sprzedaż') {
1372
- passAction = h.action === 'Sprzedaż';
1373
- } else if (actFilter === 'Sprzedaż_retail') {
1374
- passAction = h.action === 'Sprzedaż' && h.product.includes('[Detal]');
1375
- } else if (actFilter === 'Sprzedaż_wholesale') {
1376
- passAction = h.action === 'Sprzedaż' && h.product.includes('[Hurt]');
1377
- } else {
1378
- passAction = h.action === actFilter;
1379
- }
1380
  }
1381
 
1382
  if (hd >= fDate && hd <= tDate && (eFilter === 'all' || h.emp_name === eFilter) && passAction) {
1383
- if (h.action === 'Sprzedaż') total += (h.amount * h.price);
1384
-
1385
  let bCls = h.action.includes('Dostawa') ? 'badge-green' : 'badge-orange';
1386
- let locInfo = h.action === 'Przeniesienie' ? `${h.from_loc_name} ➔ ${h.to_loc_name}` : (h.from_loc_name || h.to_loc_name);
1387
-
1388
  htmlArr.push(`<div class="history-item">
1389
  <div class="history-header">
1390
  <span>${h.time} | ${h.emp_name}</span>
1391
  <span class="history-badge ${bCls}">${h.action}</span>
1392
  </div>
1393
  <div class="history-body">
1394
- <span class="history-prod">${h.product}<br><small style="color:var(--text-muted)">${locInfo}</small></span>
1395
- <span style="font-weight:700; text-align:right;">${h.amount} szt. ${h.price > 0 ? `<br><small style="color:var(--primary)">${h.amount * h.price} ₸</small>` : ''}</span>
1396
  </div>
1397
  </div>`);
1398
  }
1399
  });
1400
  hc.innerHTML = htmlArr.join('');
1401
- document.getElementById('total_sales').innerText = `Suma sprzedaży: ${total} ₸`;
1402
  }
1403
 
1404
  init();
@@ -1409,17 +1005,8 @@ MAIN_HTML = '''
1409
 
1410
  @app.route('/')
1411
  def public_index():
1412
- return redirect(url_for('public_retail'))
1413
-
1414
- @app.route('/catalog/retail')
1415
- def public_retail():
1416
- data = load_data()
1417
- return render_template_string(MAIN_HTML, data=data, role='public', emp=None, price_type='retail')
1418
-
1419
- @app.route('/catalog/wholesale')
1420
- def public_wholesale():
1421
  data = load_data()
1422
- return render_template_string(MAIN_HTML, data=data, role='public', emp=None, price_type='wholesale')
1423
 
1424
  @app.route('/admin', methods=['GET', 'POST'])
1425
  def admin_panel():
@@ -1428,7 +1015,7 @@ def admin_panel():
1428
  handle_post(request, data, 'admin', None)
1429
  save_data(data)
1430
  return redirect(url_for('admin_panel'))
1431
- return render_template_string(MAIN_HTML, data=data, role='admin', emp=None, price_type='both')
1432
 
1433
  @app.route('/emp/<token>', methods=['GET', 'POST'])
1434
  def employee_panel(token):
@@ -1440,7 +1027,7 @@ def employee_panel(token):
1440
  handle_post(request, data, 'employee', emp)
1441
  save_data(data)
1442
  return redirect(f'/emp/{token}')
1443
- return render_template_string(MAIN_HTML, data=data, role='employee', emp=emp, price_type='both')
1444
 
1445
  if __name__ == '__main__':
1446
  load_data()
 
39
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
40
  data = json.load(file)
41
  if not isinstance(data, dict):
42
+ data = {'products': [], 'history': [], 'orders': []}
43
  if 'settings' not in data:
44
+ data['settings'] = {'employees': []}
 
 
 
 
45
  if 'products' not in data:
46
  data['products'] = []
47
  if 'history' not in data:
48
  data['history'] = []
49
+ if 'orders' not in data:
50
+ data['orders'] = []
51
 
52
  for p in data['products']:
53
+ if 'sack_qty' not in p:
54
+ p['sack_qty'] = p.get('box_qty', 1)
 
 
55
  if 'variants' not in p:
56
  p['variants'] = []
57
  if 'photo' not in p:
58
  p['photo'] = ''
 
 
59
  if 'stocks' not in p:
60
+ p['stocks'] = {'w_main': 0}
 
 
 
 
 
61
 
62
  for v in p['variants']:
63
  if 'stocks' not in v:
64
+ v['stocks'] = {'w_main': 0}
65
  return data
66
  except Exception:
67
  return {
68
+ 'settings': {'employees': []},
 
 
 
 
69
  'products': [],
70
+ 'history': [],
71
+ 'orders': []
72
  }
73
 
74
  def save_data(data):
 
98
  upload_db_to_hf()
99
  time.sleep(800)
100
 
101
+ def add_history(data, action, product, amount, emp_name):
102
  data['history'].insert(0, {
103
  'time': get_almaty_time(),
104
  'action': action,
105
  'product': product,
106
  'amount': amount,
 
 
 
107
  'emp_name': emp_name
108
  })
109
  data['history'] = data['history'][:1000]
110
 
 
 
 
 
 
 
 
111
  def handle_post(req, data, role, emp):
112
  action = req.form.get('action')
113
+ if emp:
114
+ emp_name = emp['name']
115
+ elif role == 'admin':
116
+ emp_name = 'Admin'
117
+ else:
118
+ emp_name = 'Sklep'
119
 
120
  if action == 'add' and role == 'admin':
 
 
 
 
 
 
 
 
 
 
121
  photo = req.form.get('photo', '')
122
  try:
123
  rolls = int(req.form.get('rolls', 0))
124
  except ValueError:
125
  rolls = 0
126
  try:
127
+ sack_qty = int(req.form.get('sack_qty', 1))
128
  except ValueError:
129
+ sack_qty = 1
130
 
131
  v_names = req.form.getlist('v_name[]')
 
132
  v_stocks = req.form.getlist('v_stock[]')
133
 
134
  variants = []
135
  has_variants = False
136
+ for n, st in zip(v_names, v_stocks):
137
  if n.strip():
138
  has_variants = True
 
 
 
 
139
  try:
140
  st_val = int(st)
141
  except ValueError:
142
  st_val = 0
143
+ variants.append({'name': n.strip(), 'stocks': {'w_main': st_val}})
144
 
145
+ p_stocks = {} if has_variants else {'w_main': rolls}
146
 
147
  data['products'].insert(0, {
148
  'id': str(time.time()),
149
  'name': req.form['name'],
150
  'description': req.form['description'],
 
 
151
  'variants': variants,
152
  'photo': photo,
153
+ 'sack_qty': sack_qty,
154
  'stocks': p_stocks
155
  })
156
 
 
161
  p['name'] = req.form.get('name', p['name'])
162
  p['description'] = req.form.get('description', p.get('description', ''))
163
  try:
164
+ p['sack_qty'] = int(req.form.get('sack_qty', p.get('sack_qty', 1)))
 
 
 
 
 
 
 
 
165
  except ValueError:
166
  pass
167
 
 
168
  try:
169
  new_rolls = int(req.form.get('rolls', 0))
170
  except ValueError:
171
  new_rolls = 0
172
 
173
  v_names = req.form.getlist('v_name[]')
 
174
  v_stocks = req.form.getlist('v_stock[]')
175
 
176
  new_variants = []
177
+ for n, st in zip(v_names, v_stocks):
178
  if n.strip():
 
179
  try:
180
  st_val = int(st)
181
  except ValueError:
182
  st_val = 0
183
+ new_variants.append({'name': n.strip(), 'stocks': {'w_main': st_val}})
 
 
 
 
 
 
 
184
 
185
  p['variants'] = new_variants
186
 
187
+ if not new_variants:
 
 
188
  if 'stocks' not in p:
189
  p['stocks'] = {}
190
+ p['stocks']['w_main'] = new_rolls
 
191
 
192
  photo = req.form.get('photo', '')
193
  if photo:
 
198
  if 0 <= idx < len(data['products']):
199
  del_name = data['products'][idx]['name']
200
  data['products'].pop(idx)
201
+ add_history(data, 'Usunięcie', del_name, 0, emp_name)
202
 
203
  elif action == 'income' and role == 'admin':
204
  idx = int(req.form['product_index'])
205
  amt = int(req.form['rolls'])
 
206
  v_idx_str = req.form.get('v_index')
207
  p = data['products'][idx]
208
 
 
212
  if 0 <= v_idx < len(p.get('variants', [])):
213
  v = p['variants'][v_idx]
214
  if 'stocks' not in v: v['stocks'] = {}
215
+ v['stocks']['w_main'] = v['stocks'].get('w_main', 0) + amt
216
  prod_name_history += f" ({v['name']})"
217
  else:
218
  if 'stocks' not in p: p['stocks'] = {}
219
+ p['stocks']['w_main'] = p.get('stocks', {}).get('w_main', 0) + amt
220
 
221
+ add_history(data, 'Dostawa', prod_name_history, amt, emp_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
+ elif action == 'place_order':
224
  cart_data = req.form.get('cart', '[]')
225
  cart = json.loads(cart_data)
226
+ if cart:
227
+ data['orders'].append({
228
+ 'id': str(time.time()),
229
+ 'time': get_almaty_time(),
230
+ 'emp_name': emp_name,
231
+ 'items': cart,
232
+ 'status': 'pending'
233
+ })
234
+
235
+ elif action == 'approve_order' and role == 'admin':
236
+ order_id = req.form.get('order_id')
237
+ order = next((o for o in data['orders'] if o['id'] == order_id), None)
238
+ if order and order['status'] == 'pending':
239
+ for item in order['items']:
240
+ idx = item.get('pIndex')
241
+ v_idx = item.get('vIndex', -1)
242
+ amt = item.get('amt')
243
+ if 0 <= idx < len(data['products']):
244
+ p = data['products'][idx]
245
+ if v_idx != -1 and 0 <= v_idx < len(p.get('variants', [])):
246
+ v = p['variants'][v_idx]
247
+ v['stocks']['w_main'] = v.get('stocks', {}).get('w_main', 0) - amt
248
+ else:
249
+ p['stocks']['w_main'] = p.get('stocks', {}).get('w_main', 0) - amt
250
+ add_history(data, 'Wydano do sklepu', item.get('pName'), amt, order['emp_name'])
251
+ order['status'] = 'approved'
252
+
253
+ elif action == 'reject_order' and role == 'admin':
254
+ order_id = req.form.get('order_id')
255
+ order = next((o for o in data['orders'] if o['id'] == order_id), None)
256
+ if order and order['status'] == 'pending':
257
+ order['status'] = 'rejected'
 
 
 
 
258
 
259
  elif action == 'add_emp' and role == 'admin':
260
  e_name = req.form['e_name']
 
261
  e_id = str(uuid.uuid4())[:8]
262
+ data['settings']['employees'].append({'id': e_id, 'name': e_name, 'token': e_id})
263
 
264
  elif action == 'del_emp' and role == 'admin':
265
  e_id = req.form['e_id']
266
  data['settings']['employees'] = [x for x in data['settings']['employees'] if x['id'] != e_id]
267
 
268
+
269
  MAIN_HTML = '''
270
  <!DOCTYPE html>
271
  <html lang="pl">
 
304
  .nav-btn { flex: 0 0 auto; padding: 10px 16px; border-radius: 8px; border: none; background: #e5e7eb; color: var(--text-muted); font-weight: 600; font-size: 0.9rem; cursor: pointer; transition: background 0.2s, color 0.2s; }
305
  .nav-btn.active { background: var(--primary); color: white; }
306
 
 
 
 
 
 
 
307
  .search-box { width: 100%; padding: 14px; border: 1px solid var(--border); border-radius: 12px; outline: none; font-size: 1rem; margin-bottom: 16px; transition: border-color 0.2s; }
308
  .search-box:focus { border-color: var(--primary); }
309
 
 
315
  .product-img { width: 100%; height: 100%; object-fit: cover; }
316
  .product-info { display: flex; flex-direction: column; flex: 1; justify-content: center; min-width: 0; }
317
  .product-title { font-size: 0.95rem; font-weight: 600; line-height: 1.2; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
318
+ .product-stock { font-size: 0.75rem; color: var(--text-muted); margin-top:4px; }
 
319
  .product-actions { display: flex; flex-direction: column; gap: 6px; width: 100%; margin-top: 4px; }
320
 
321
  .qty-input { width: 60px; padding: 6px; border: 1px solid var(--border); border-radius: 6px; text-align: center; font-weight: 600; outline: none; font-size: 0.85rem; transition: border-color 0.2s; }
 
347
 
348
  .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; }
349
  .cart-float:active { transform: scale(0.95); }
 
 
 
 
350
 
351
  @media (min-width: 768px) {
352
  body { padding: 24px; }
353
  .top-actions { max-width: 400px; }
354
+ .settings-grid { grid-template-columns: repeat(2, 1fr); gap: 24px; }
355
  .modal-overlay { align-items: center; }
356
  .modal-content { border-radius: 20px; max-width: 500px; }
357
  }
 
363
  </div>
364
 
365
  <div class="container">
366
+ <h1 id="main_title">Sklep - Katalog</h1>
367
 
368
  {% if role == 'admin' %}
369
  <div class="top-actions">
 
372
  </div>
373
  <div class="main-nav">
374
  <button id="navbtn_catalog" class="nav-btn active" onclick="switchNav('catalog')">Katalog</button>
375
+ <button id="navbtn_orders" class="nav-btn" onclick="switchNav('orders')">Zamówienia</button>
376
  <button id="navbtn_reports" class="nav-btn" onclick="switchNav('reports')">Raporty</button>
377
  <button id="navbtn_settings" class="nav-btn" onclick="switchNav('settings')">Ustawienia</button>
378
  </div>
 
383
  {% endif %}
384
 
385
  <div id="section_catalog">
 
386
  <input type="text" id="search" class="search-box" placeholder="Szukaj produktu..." oninput="onSearchInput()">
387
  <div id="catalog"></div>
388
  </div>
389
 
390
+ <div id="section_orders" style="display:none;">
391
+ <div id="orders_container"></div>
392
+ </div>
393
+
394
  <div id="section_reports" style="display:none;">
395
  <div style="display:flex; flex-wrap:wrap; gap:12px; margin-bottom:12px;">
396
  <input type="date" id="hist_from" class="form-control" style="flex:1; margin:0;" onchange="renderHistory()">
 
400
  <select id="hist_emp" class="form-control" style="flex:1; margin:0;" onchange="renderHistory()"></select>
401
  <select id="hist_action_type" class="form-control" style="flex:1; margin:0;" onchange="renderHistory()">
402
  <option value="all">Wszystkie operacje</option>
403
+ <option value="Wydano do sklepu">Sprzedane do sklepu</option>
 
 
404
  <option value="Dostawa">Dostawy</option>
 
405
  <option value="Usunięcie">Usunięcia</option>
406
  </select>
407
  </div>
 
408
  <div id="history_container"></div>
409
  </div>
410
 
411
  <div id="section_settings" style="display:none;">
412
  <div class="settings-grid">
413
  <div class="settings-card">
414
+ <h3>Katalog publiczny</h3>
415
  <div class="settings-list-item">
 
416
  <div class="settings-link-box">
417
+ <input type="text" class="settings-link-input" value="{{ request.host_url }}" readonly onclick="this.select()">
418
+ <button type="button" onclick="navigator.clipboard.writeText('{{ request.host_url }}');alert('Link skopiowany')" class="btn btn-primary" style="padding:8px 12px; font-size:0.85rem; flex:0 0 auto;">Kopiuj</button>
 
 
 
 
 
 
 
419
  </div>
420
  </div>
421
  </div>
422
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  <div class="settings-card">
424
  <h3>Pracownicy</h3>
425
  <div id="set_emps"></div>
426
  <form method="POST" onsubmit="return showLoader(this)" style="display:flex; flex-direction:column; gap:8px; margin-top:12px;">
427
  <input type="hidden" name="action" value="add_emp">
428
  <input type="text" name="e_name" class="form-control" style="margin:0;" placeholder="Imię pracownika" required>
 
429
  <button type="submit" class="btn btn-primary" style="padding:10px;">Dodaj pracownika</button>
430
  </form>
431
  </div>
 
434
  </div>
435
 
436
  <div class="cart-float" id="cart_btn" onclick="openCart()">
437
+ Zamówienie (<span id="cart_count">0</span>)
438
  </div>
439
 
440
  <div id="imageZoomModal" class="modal-overlay" onclick="closeModal('imageZoomModal')" style="z-index:2000; align-items:center;">
 
450
  <input type="text" name="name" class="form-control" placeholder="Nazwa" required>
451
  <textarea name="description" class="form-control" placeholder="Opis"></textarea>
452
 
 
 
 
 
 
 
 
453
  <label style="display:block; margin-bottom:8px; font-weight:600; font-size:0.9rem;">Warianty produktu (opcjonalnie)</label>
454
  <div id="add_variants_container"></div>
455
  <button type="button" class="btn btn-success" style="padding:8px; font-size:0.85rem; margin-bottom:12px; width:100%; background:#eab308;" onclick="addVariantField('add_variants_container')">+ Dodaj wariant</button>
456
 
457
  <div id="add_base_stock_container">
458
+ <input type="number" name="rolls" id="add_rolls" class="form-control" placeholder="Ilość początkowa w magazynie" value="0" required>
459
  </div>
460
 
461
+ <input type="number" name="sack_qty" class="form-control" placeholder="Sztuk w worku" value="1" required>
462
 
463
  <label style="display:block; margin-bottom:8px; font-size:0.9rem; color:var(--text-muted);">Zdjęcie produktu (opcjonalnie)</label>
464
  <input type="file" accept="image/*" class="form-control" onchange="processImage(this, 'photo_b64', 'photo_preview')">
 
480
  <input type="text" name="name" id="edit_name" class="form-control" placeholder="Nazwa" required>
481
  <textarea name="description" id="edit_desc" class="form-control" placeholder="Opis"></textarea>
482
 
483
+ <label style="display:block; margin-bottom:4px; font-weight:600; font-size:0.9rem;">Stan na magazynie</label>
 
 
 
 
 
 
 
484
  <div id="edit_base_stock_container" style="margin-bottom:12px;">
485
  <input type="number" name="rolls" id="edit_rolls" class="form-control" placeholder="Całkowity stan (bez wariantów)">
486
  </div>
 
489
  <div id="edit_variants_container"></div>
490
  <button type="button" class="btn btn-success" style="padding:8px; font-size:0.85rem; margin-bottom:12px; width:100%; background:#eab308;" onclick="addVariantField('edit_variants_container')">+ Dodaj wariant</button>
491
 
492
+ <input type="number" name="sack_qty" id="edit_sack_qty" class="form-control" placeholder="Sztuk w worku" required>
493
 
494
  <label style="display:block; margin-bottom:8px; font-size:0.9rem; color:var(--text-muted);">Nowe zdjęcie (opcjonalnie)</label>
495
  <input type="file" accept="image/*" class="form-control" onchange="processImage(this, 'edit_photo_b64', 'edit_photo_preview')">
 
504
  <div id="incomeModal" class="modal-overlay">
505
  <div class="modal-content">
506
  <button class="close-modal" onclick="closeModal('incomeModal')">×</button>
507
+ <h2>Dostawa do magazynu</h2>
508
  <form method="POST" style="margin-top:16px;" onsubmit="return showLoader(this)">
509
  <input type="hidden" name="action" value="income">
 
510
  <select name="product_index" id="inc_product" class="form-control" onchange="updateIncVariants()" required></select>
511
  <select name="v_index" id="inc_variant" class="form-control" style="display:none;"></select>
512
  <input type="number" name="rolls" class="form-control" placeholder="Ilość" min="1" required>
 
515
  </div>
516
  </div>
517
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  <div id="cartModal" class="modal-overlay">
519
  <div class="modal-content">
520
  <button class="close-modal" onclick="closeModal('cartModal')">×</button>
521
+ <h2>Twoje zamówienie</h2>
522
  <div id="cart_items" style="margin-bottom:16px; margin-top:16px;"></div>
523
+ <button id="checkout_btn" class="btn btn-success" style="width:100%;" onclick="submitCart()">Wyślij zamówienie</button>
 
 
 
 
 
 
 
 
 
524
  </div>
525
  </div>
526
 
 
532
  <script>
533
  const data = {{ data|tojson }};
534
  const role = "{{ role }}";
 
535
  const hostUrl = "{{ request.host_url }}";
536
+ const currentEmp = {{ emp|tojson if emp else 'null' }};
 
537
  let cart = [];
538
  let searchTimeout;
539
 
 
547
  }
548
 
549
  function init() {
 
 
 
 
 
 
550
  if (role === 'admin') {
551
+ document.getElementById('main_title').innerText = 'Panel administratora';
552
  buildSettings();
553
  buildIncProducts();
 
554
  buildEmpSelect();
555
  initHistoryDates();
556
+ } else if (role === 'employee') {
557
+ document.getElementById('main_title').innerText = 'Katalog - Pracownik';
558
+ } else {
559
+ document.getElementById('main_title').innerText = 'Sklep - Katalog';
560
  }
561
  renderCatalog();
562
  }
563
 
564
  function switchNav(nav) {
565
+ ['catalog', 'orders', 'reports', 'settings'].forEach(id => {
566
  const el = document.getElementById('section_' + id);
567
  if(el) el.style.display = (id === nav) ? 'block' : 'none';
568
  });
569
  document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
570
+ const btn = document.getElementById('navbtn_' + nav);
571
+ if(btn) btn.classList.add('active');
572
  if (nav === 'reports') renderHistory();
573
+ if (nav === 'orders') renderOrders();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
574
  }
575
 
576
  function zoomImage(src) {
 
588
  const q = document.getElementById('search').value.toLowerCase().trim();
589
  const cat = document.getElementById('catalog');
590
  let htmlArr = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
 
592
  data.products.forEach((p, pIdx) => {
593
  let match = p.name.toLowerCase().includes(q) || (p.description && p.description.toLowerCase().includes(q));
594
  if (!match) return;
595
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
596
  let stock = 0;
597
  if (p.variants && p.variants.length > 0) {
598
+ p.variants.forEach(v => { stock += (v.stocks && v.stocks['w_main']) ? v.stocks['w_main'] : 0; });
599
  } else {
600
+ stock += (p.stocks && p.stocks['w_main']) ? p.stocks['w_main'] : 0;
601
  }
602
 
603
+ let imgHtml = p.photo ? `<img src="${p.photo}" class="product-img">` : `<span style="font-size:0.6rem; color:#9ca3af;">Brak</span>`;
 
 
604
  let imgClick = p.photo ? `onclick="zoomImage('${p.photo}')"` : ``;
605
 
606
+ let varHtml = '';
607
+ if (p.variants && p.variants.length > 0) {
608
+ varHtml = `<select id="var_${pIdx}" class="form-control" style="padding:6px; font-size:0.8rem; margin-bottom:4px; height:auto;" onchange="updateMaxQty(${pIdx})">
609
+ ${p.variants.map((v, i) => `<option value="${i}">${v.name}</option>`).join('')}
610
+ </select>`;
 
 
 
 
 
 
 
 
 
611
  }
612
 
613
+ let actionsHtml = `
614
+ <div class="product-actions">
615
+ ${varHtml}
616
+ <div style="display:flex; gap:6px; align-items:center; width:100%;">
617
+ <input type="number" id="amt_${pIdx}" class="qty-input" value="1" min="1">
618
+ <button class="btn btn-primary" style="padding:8px 6px; font-size:0.75rem; flex:1;" onclick="addToCart(${pIdx}, false)">Dodaj do zamówienia (Szt.)</button>
619
+ </div>
620
+ <div style="display:flex; gap:6px; width:100%; margin-top:2px;">
621
+ <button class="btn btn-success" style="padding:8px 6px; font-size:0.75rem; flex:1; background:#059669;" onclick="addToCart(${pIdx}, true)">Cały worek do zamówienia</button>
622
+ </div>
623
+ `;
624
+ if (role === 'admin') {
625
+ actionsHtml += `<div style="display:flex; gap:6px; margin-top:2px; width:100%;">
626
+ <button class="btn btn-success" style="padding:8px 6px; flex:1; font-size:0.75rem; background:#eab308;" onclick="openEdit(${pIdx})">Edytuj</button>
627
+ <button class="btn btn-danger" style="padding:8px 6px; flex:1; font-size:0.75rem;" onclick="delProd(${pIdx})">Usuń</button>
628
+ </div>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
629
  }
630
+ actionsHtml += `</div>`;
631
 
632
  let html = `<div class="product">
633
  <div class="product-main">
 
635
  <div class="product-info">
636
  <div class="product-title">${p.name}</div>
637
  ${p.description ? `<div style="font-size:0.75rem; color:var(--text-muted);">${p.description}</div>` : ''}
638
+ <div class="product-stock">Dostępność na magazynie: <span id="stock_display_${pIdx}">${stock}</span> szt. | W worku: ${p.sack_qty || 1} szt.</div>
 
639
  </div>
640
  </div>
641
  ${actionsHtml}
 
643
  htmlArr.push(html);
644
  });
645
  cat.innerHTML = htmlArr.join('');
 
 
 
 
 
 
 
 
 
 
646
 
647
  data.products.forEach((p, pIdx) => {
648
  if(document.getElementById('stock_display_' + pIdx)) {
 
655
  let p = data.products[pIdx];
656
  let vSelect = document.getElementById('var_' + pIdx);
657
  let qtyInput = document.getElementById('amt_' + pIdx);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
 
659
  let max = 0;
660
  if (vSelect && vSelect.value !== "") {
661
  let vIdx = parseInt(vSelect.value);
662
+ max = p.variants[vIdx].stocks['w_main'] || 0;
663
  } else if (p.variants && p.variants.length > 0) {
664
+ max = p.variants[0].stocks['w_main'] || 0;
665
  } else {
666
+ max = p.stocks['w_main'] || 0;
667
  }
668
 
669
  if(qtyInput) qtyInput.max = max;
 
673
  }
674
  }
675
 
676
+ function addToCart(pIdx, isSack) {
677
  let p = data.products[pIdx];
678
  let vSelect = document.getElementById('var_'+pIdx);
679
  let vIdx = (vSelect && vSelect.value !== '') ? parseInt(vSelect.value) : -1;
680
+ let amt = isSack ? parseInt(p.sack_qty || 1) : parseInt(document.getElementById('amt_'+pIdx).value);
681
 
682
+ let stock = (vIdx !== -1) ? (p.variants[vIdx].stocks['w_main'] || 0) : (p.stocks['w_main'] || 0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
683
 
684
  if(amt <= 0 || isNaN(amt)) return alert('Nieprawidłowa ilość');
 
685
 
686
+ let vName = vIdx !== -1 ? ' (' + p.variants[vIdx].name + ')' : '';
687
+ let finalName = p.name + vName;
 
 
 
 
 
 
 
 
688
 
689
+ let existing = cart.find(i => i.pIndex === pIdx && i.vIndex === vIdx && i.pName === finalName);
690
+ let cartTotalForProduct = cart.filter(i => i.pIndex === pIdx && i.vIndex === vIdx).reduce((s, i) => s + i.amt, 0);
 
 
 
 
 
691
 
692
+ if(cartTotalForProduct + amt > stock) return alert('Przekroczono stan w koszyku (brak na magazynie)');
 
 
 
 
 
 
693
 
694
  if(existing) {
695
  existing.amt += amt;
696
  } else {
697
+ cart.push({pIndex: pIdx, vIndex: vIdx, pName: finalName, amt: amt});
698
  }
699
  updateCartUI();
700
  }
 
708
 
709
  function openCart() {
710
  let html = '';
 
711
  cart.forEach((c, i) => {
 
 
712
  html += `<div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid var(--border); padding:10px 0;">
713
  <div>
714
  <div style="font-weight:600;">${c.pName}</div>
715
+ <div style="font-size:0.85rem; color:var(--text-muted);">${c.amt} szt.</div>
716
  </div>
717
  <div style="display:flex; align-items:center; gap:12px;">
 
718
  <button style="border:none; background:var(--danger); color:white; border-radius:6px; padding:6px 10px; cursor:pointer; font-weight:700;" onclick="removeFromCart(${i})">×</button>
719
  </div>
720
  </div>`;
721
  });
 
722
  document.getElementById('cart_items').innerHTML = html;
723
  openModal('cartModal');
724
  }
 
741
  document.getElementById('loader').style.display = 'flex';
742
 
743
  let fd = new FormData();
744
+ fd.append('action', 'place_order');
745
  fd.append('cart', JSON.stringify(cart));
746
 
747
  try {
748
  await fetch(window.location.href, {method: 'POST', body: fd});
749
+ alert('Zamówienie zostało pomyślnie wysłane!');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
750
  cart = [];
751
  updateCartUI();
752
+ closeModal('cartModal');
753
+ window.location.reload();
754
  } catch (e) {
755
  alert('Błąd podczas składania zamówienia');
756
  } finally {
 
759
  }
760
  }
761
 
762
+ function renderOrders() {
763
+ const cont = document.getElementById('orders_container');
764
+ let html = '';
765
+ const pending = data.orders.filter(o => o.status === 'pending');
766
+ if(pending.length === 0) {
767
+ cont.innerHTML = '<p style="text-align:center; margin-top:20px; color:var(--text-muted);">Brak nowych zamówień.</p>';
768
+ return;
 
 
 
 
 
 
 
 
 
769
  }
770
+ pending.forEach(o => {
771
+ let itemsHtml = o.items.map(i => `<li style="font-size:0.9rem; margin-bottom:4px;"><b>${i.pName}</b> - ${i.amt} szt.</li>`).join('');
772
+ html += `<div class="history-item">
773
+ <div class="history-header">
774
+ <span>Data: ${o.time}</span>
775
+ <span class="history-badge badge-orange">Oczekujące</span>
776
+ </div>
777
+ <div style="font-size:0.85rem; margin-bottom:8px;">Zleceniodawca: <b>${o.emp_name}</b></div>
778
+ <ul style="list-style-type:none; margin-bottom:12px; padding:10px; background:#f9fafb; border-radius:8px;">
779
+ ${itemsHtml}
780
+ </ul>
781
+ <div style="display:flex; gap:8px;">
782
+ <form method="POST" onsubmit="return showLoader(this)" style="margin:0; flex:1;">
783
+ <input type="hidden" name="action" value="approve_order">
784
+ <input type="hidden" name="order_id" value="${o.id}">
785
+ <button type="submit" class="btn btn-success" style="width:100%; font-size:0.85rem;">Zatwierdź i wydaj</button>
786
+ </form>
787
+ <form method="POST" onsubmit="return showLoader(this)" style="margin:0; flex:1;">
788
+ <input type="hidden" name="action" value="reject_order">
789
+ <input type="hidden" name="order_id" value="${o.id}">
790
+ <button type="submit" class="btn btn-danger" style="width:100%; font-size:0.85rem;">Odrzuć</button>
791
+ </form>
792
+ </div>
793
+ </div>`;
794
  });
795
+ cont.innerHTML = html;
796
  }
797
 
798
+ function addVariantField(containerId, name='', stock='') {
799
  const cont = document.getElementById(containerId);
800
  const div = document.createElement('div');
801
  div.style = "display:flex; gap:8px; margin-bottom:8px; align-items:center;";
802
  div.innerHTML = `
803
  <input type="text" name="v_name[]" class="form-control" style="margin:0; flex:1;" placeholder="Nazwa" value="${name}" required>
804
+ <input type="number" name="v_stock[]" class="form-control v-stock-input" style="margin:0; width:90px;" placeholder="Stan mag." value="${stock}" required>
 
805
  <button type="button" style="border:none; background:var(--danger); color:white; border-radius:8px; padding:10px; cursor:pointer;" onclick="removeVariantField(this, '${containerId}')">×</button>
806
  `;
807
  cont.appendChild(div);
 
831
  document.getElementById('edit_index').value = pIdx;
832
  document.getElementById('edit_name').value = p.name;
833
  document.getElementById('edit_desc').value = p.description || '';
834
+ document.getElementById('edit_sack_qty').value = p.sack_qty || 1;
 
 
835
  document.getElementById('edit_photo_b64').value = '';
836
 
 
 
 
837
  document.getElementById('edit_variants_container').innerHTML = '';
838
  if (p.variants && p.variants.length > 0) {
839
  p.variants.forEach(v => {
840
+ addVariantField('edit_variants_container', v.name, v.stocks['w_main'] || 0);
841
  });
842
  }
843
 
 
857
  function loadEditStocks() {
858
  let pIdx = document.getElementById('edit_index').value;
859
  let p = data.products[pIdx];
 
 
860
  if (p.variants && p.variants.length > 0) {
 
 
 
 
 
 
861
  document.getElementById('edit_base_stock_container').style.display = 'none';
862
  } else {
863
+ document.getElementById('edit_rolls').value = p.stocks['w_main'] || 0;
864
  document.getElementById('edit_base_stock_container').style.display = 'block';
865
  }
866
  }
 
906
  function closeModal(id) { document.getElementById(id).classList.remove('active'); }
907
 
908
  function buildSettings() {
 
 
 
 
 
 
909
  const eCont = document.getElementById('set_emps');
910
+ eCont.innerHTML = data.settings.employees.map(e => `
 
 
 
911
  <div class="settings-list-item">
912
  <div class="settings-list-item-header">
913
+ <span style="font-weight:600;">${e.name}</span>
914
  <form method="POST" onsubmit="return showLoader(this)" style="margin:0;">
915
  <input type="hidden" name="action" value="del_emp">
916
  <input type="hidden" name="e_id" value="${e.id}">
 
922
  <button type="button" onclick="navigator.clipboard.writeText('${hostUrl}emp/${e.token}');alert('Link skopiowany')" class="btn btn-primary" style="padding:8px 12px; font-size:0.85rem; flex:0 0 auto;">Kopiuj</button>
923
  </div>
924
  </div>
925
+ `).join('');
 
 
 
 
 
 
 
 
 
 
926
  }
927
 
928
  function buildIncProducts() {
 
952
 
953
  function buildEmpSelect() {
954
  const sel = document.getElementById('hist_emp');
955
+ sel.innerHTML = '<option value="all">Wszyscy</option><option value="Admin">Admin</option><option value="Sklep">Sklep</option>' + data.settings.employees.map(e => `<option value="${e.name}">${e.name}</option>`).join('');
956
  }
957
 
958
  function initHistoryDates() {
 
971
  const eFilter = document.getElementById('hist_emp').value;
972
  const actFilter = document.getElementById('hist_action_type').value;
973
 
 
 
974
  data.history.forEach(h => {
975
  const pts = h.time.split(' ')[0].split('.');
976
  const hd = new Date(`${pts[2]}-${pts[1]}-${pts[0]}T${h.time.split(' ')[1]}`);
977
 
978
  let passAction = true;
979
  if (actFilter !== 'all') {
980
+ passAction = h.action === actFilter;
 
 
 
 
 
 
 
 
981
  }
982
 
983
  if (hd >= fDate && hd <= tDate && (eFilter === 'all' || h.emp_name === eFilter) && passAction) {
 
 
984
  let bCls = h.action.includes('Dostawa') ? 'badge-green' : 'badge-orange';
 
 
985
  htmlArr.push(`<div class="history-item">
986
  <div class="history-header">
987
  <span>${h.time} | ${h.emp_name}</span>
988
  <span class="history-badge ${bCls}">${h.action}</span>
989
  </div>
990
  <div class="history-body">
991
+ <span class="history-prod">${h.product}</span>
992
+ <span style="font-weight:700; text-align:right;">${h.amount} szt.</span>
993
  </div>
994
  </div>`);
995
  }
996
  });
997
  hc.innerHTML = htmlArr.join('');
 
998
  }
999
 
1000
  init();
 
1005
 
1006
  @app.route('/')
1007
  def public_index():
 
 
 
 
 
 
 
 
 
1008
  data = load_data()
1009
+ return render_template_string(MAIN_HTML, data=data, role='public', emp=None)
1010
 
1011
  @app.route('/admin', methods=['GET', 'POST'])
1012
  def admin_panel():
 
1015
  handle_post(request, data, 'admin', None)
1016
  save_data(data)
1017
  return redirect(url_for('admin_panel'))
1018
+ return render_template_string(MAIN_HTML, data=data, role='admin', emp=None)
1019
 
1020
  @app.route('/emp/<token>', methods=['GET', 'POST'])
1021
  def employee_panel(token):
 
1027
  handle_post(request, data, 'employee', emp)
1028
  save_data(data)
1029
  return redirect(f'/emp/{token}')
1030
+ return render_template_string(MAIN_HTML, data=data, role='employee', emp=emp)
1031
 
1032
  if __name__ == '__main__':
1033
  load_data()