simikkk commited on
Commit
90ff4be
·
verified ·
1 Parent(s): e171ca9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +380 -300
app.py CHANGED
@@ -1,12 +1,10 @@
1
  import streamlit as st
2
- import stripe
3
  import pandas as pd
4
  import time
5
- import random
6
- import traceback
7
 
8
  # -----------------------------------------------------------------------------
9
- # JAZYKOVÉ LOKALIZACE (CZ, SK, DE, EN, ES, IT, FR)
10
  # -----------------------------------------------------------------------------
11
  TRANSLATIONS = {
12
  "CZ": {
@@ -47,9 +45,9 @@ CURRENCIES = {
47
  }
48
 
49
  # -----------------------------------------------------------------------------
50
- # INICIALIZACE STAVU
51
  # -----------------------------------------------------------------------------
52
- VERSION = "1.9.0-ENTERPRISE"
53
 
54
  st.set_page_config(page_title="TradeFlow AI", page_icon="🏆", layout="wide", initial_sidebar_state="expanded")
55
 
@@ -57,18 +55,9 @@ if "lang" not in st.session_state: st.session_state.lang = "CZ"
57
  if "curr" not in st.session_state: st.session_state.curr = "EUR"
58
  if "admin_authenticated" not in st.session_state: st.session_state.admin_authenticated = False
59
  if "user_subscribed" not in st.session_state: st.session_state.user_subscribed = False
60
- if "balloons_triggered" not in st.session_state: st.session_state.balloons_triggered = False
61
- if "quotes_count" not in st.session_state: st.session_state.quotes_count = 1
62
- if "stripe_mode" not in st.session_state: st.session_state.stripe_mode = "Sandbox (Simulace)"
63
- if "stripe_api_key" not in st.session_state: st.session_state.stripe_api_key = ""
64
-
65
- # Dočasný paměťový zásobník pro dynamické řádky nákladů v generátoru
66
- if "temp_extra_costs" not in st.session_state:
67
- st.session_state.temp_extra_costs = [
68
- {"kategorie": "Materiál", "nazev": "Spojovací materiál & Sanitární trubky", "castka": 250.0}
69
- ]
70
 
71
- # Inicializace výchozího týmu
72
  if "db_team" not in st.session_state:
73
  st.session_state.db_team = [
74
  {"id": 1, "jmeno": "Šimon (Director)", "sazba": 45.0},
@@ -76,37 +65,10 @@ if "db_team" not in st.session_state:
76
  {"id": 3, "jmeno": "Matěj (Technik)", "sazba": 25.0}
77
  ]
78
 
79
- if "db_customers" not in st.session_state:
80
- st.session_state.db_customers = [
81
- {"id": 1, "jmeno": "Jan Novák", "firma": "Stavebniny s.r.o.", "telefon": "+420 777 123 456"},
82
- {"id": 2, "jmeno": "Marie Podlahová", "firma": "Bytové družstvo", "telefon": "+420 602 987 654"},
83
- {"id": 3, "jmeno": "Zdeněk Urban", "firma": "Instalatérství Urban", "telefon": "+420 731 555 999"}
84
- ]
85
-
86
- # Předvyplněná vzorová nabídka, aby analýza nákladů hned fungovala a ukazovala data
87
- if "db_quotes" not in st.session_state:
88
- st.session_state.db_quotes = [
89
- {
90
- "id": 1,
91
- "zakaznik": "Jan Novák",
92
- "popis": "Kompletní instalace rozvodů",
93
- "celkem": 1250.0,
94
- "typ": "fixni",
95
- "datum": "2026-06-05",
96
- "labor_eur": 750.0,
97
- "extra_costs": [
98
- {"kategorie": "Materiál", "nazev": "Měděné trubky", "eur": 400.0},
99
- {"kategorie": "Benzín / Doprava", "nazev": "Doprava na stavbu", "eur": 100.0}
100
- ]
101
- }
102
- ]
103
-
104
- if "db_invoices" not in st.session_state:
105
- st.session_state.db_invoices = [
106
- {"id": 1, "cislo": "20260001", "zakaznik": "Jan Novák", "castka": 1250.00, "stav": "Zaplaceno", "datum": "2026-06-05"},
107
- {"id": 2, "cislo": "20260002", "zakaznik": "Marie Podlahová", "castka": 1580.50, "stav": "Odesláno", "datum": "2026-06-10"},
108
- {"id": 3, "cislo": "20260003", "zakaznik": "Zdeněk Urban", "castka": 290.00, "stav": "Zaplaceno", "datum": "2026-06-12"}
109
- ]
110
 
111
  if "sms_presets" not in st.session_state:
112
  st.session_state.sms_presets = [
@@ -115,6 +77,12 @@ if "sms_presets" not in st.session_state:
115
  {"název": "Kamarádská připomínka", "text": "Ahoj {jmeno}, jen rychlá připomínka, že faktura {cislo} na částku {castka} už čeká na proplacení. Díky moc!"}
116
  ]
117
 
 
 
 
 
 
 
118
  def format_price(amount_in_eur):
119
  c_info = CURRENCIES[st.session_state.curr]
120
  converted = amount_in_eur * c_info["rate"]
@@ -122,17 +90,10 @@ def format_price(amount_in_eur):
122
  return f"{converted:,.2f} {c_info['symbol']}"
123
  return f"{c_info['symbol']}{converted:,.2f}"
124
 
125
- if st.session_state.user_subscribed:
126
- if not st.session_state.balloons_triggered:
127
- st.balloons()
128
- st.session_state.balloons_triggered = True
129
- else:
130
- st.session_state.balloons_triggered = False
131
-
132
  t = TRANSLATIONS[st.session_state.lang if st.session_state.lang in TRANSLATIONS else "CZ"]
133
 
134
  # -----------------------------------------------------------------------------
135
- # SIDEBAR: CONFIG & ADMIN
136
  # -----------------------------------------------------------------------------
137
  with st.sidebar:
138
  st.header("🌐 Nastavení / Settings")
@@ -143,25 +104,69 @@ with st.sidebar:
143
  st.header("🛡️ Dev Admin Panel")
144
 
145
  if not st.session_state.admin_authenticated:
146
- pwd_input = st.text_input("Zadejte administrátorské heslo", type="password", help="Heslo pro porotu je: admin123")
147
  if st.button("Odemknout Admin Panel"):
148
  if pwd_input == "admin123":
149
  st.session_state.admin_authenticated = True
150
- st.success("Přístupováno!")
151
  st.rerun()
152
  else:
153
  st.error("Nesprávné heslo!")
154
  else:
155
- st.info("🔓 Úspěšně autorizováno")
156
- st.session_state.user_subscribed = st.toggle("Uživatel má zakoupen PRO tarif", value=st.session_state.user_subscribed)
157
- st.session_state.quotes_count = st.number_input("Počet vygenerovaných nabídek", min_value=0, max_value=10, value=st.session_state.quotes_count)
158
 
159
- if st.button("Zamknout panel & Odhlásit se"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  st.session_state.admin_authenticated = False
161
  st.rerun()
162
 
163
  st.title(t["title"])
164
- st.caption(f"{t['caption']} | Version: {VERSION}")
165
 
166
  is_locked = st.session_state.quotes_count >= 3 and not st.session_state.user_subscribed
167
 
@@ -170,7 +175,7 @@ tab_dashboard, tab_generator, tab_team, tab_crm, tab_fakturace, tab_pro_features
170
  ])
171
 
172
  # -----------------------------------------------------------------------------
173
- # TAB 1: METRIKY & NOVÉ FILTROVÁNÍ NÁKLADOVÝCH SEKCÍ
174
  # -----------------------------------------------------------------------------
175
  with tab_dashboard:
176
  st.header(t["tab_dash"])
@@ -181,84 +186,77 @@ with tab_dashboard:
181
  c1, c2, c3, c4 = st.columns(4)
182
  c1.metric(t["profit"], format_price(total_revenue))
183
  c2.metric(t["pending"], format_price(pending_revenue))
184
- c3.metric(t["potential"], format_price(potencial), help="Počítá pouze Přesné fixní nabídky. Orientační odhady se sem nepromítají.")
185
- c4.metric(t["limit"], f"{st.session_state.quotes_count} / 3", delta="⚠️ LIMIT" if is_locked else None)
186
 
187
- # 🌟 NOVÁ FUNKCE: POKROČILÉ FILTROVÁNÍ SEKCÍ NÁKLADŮ
188
  st.markdown("---")
189
- st.subheader("📊 Analýza a filtrování nákladových sekcí")
190
- st.write("Zde vidíte globální přehled investovaných financí roztříděný podle konkrétních typů nákladů napříč všemi uloženými rozpočty.")
191
 
192
- # Sestavení ploché tabulky všech existujících nákladů z paměti
193
  flat_costs = []
194
  for q in st.session_state.db_quotes:
195
- # Přidání výplat z personálních hodin
196
  if q.get("labor_eur", 0) > 0:
197
  flat_costs.append({
198
  "Projekt / Klient": f"{q['zakaznik']} ({q['datum']})",
199
  "Sekce nákladu": "Výplaty zaměstnancům",
200
  "Název položky": "Mzda týmu na projektu (odpracované hodiny)",
201
  "Částka (Základ v EUR)": q["labor_eur"],
202
- "Typ nabídky": "🎯 Přesná fixní" if q["typ"] == "fixni" else "🔮 Orientační odhad"
203
  })
204
- # Přidání dynamických vedlejších nákladů
205
  for ec in q.get("extra_costs", []):
206
  flat_costs.append({
207
  "Projekt / Klient": f"{q['zakaznik']} ({q['datum']})",
208
  "Sekce nákladu": ec["kategorie"],
209
- "Název položky": ec["nazev"] if ec["nazev"] else "Nespecifikováno",
210
  "Částka (Základ v EUR)": ec["eur"],
211
- "Typ nabídky": "🎯 Přesná fixní" if q["typ"] == "fixni" else "🔮 Orientační odhad"
212
  })
213
 
214
  if flat_costs:
215
  df_costs = pd.DataFrame(flat_costs)
216
 
217
- # Filtrační lišta
218
  f_col1, f_col2 = st.columns(2)
219
  with f_col1:
220
  available_cats = ["Zobrazit vše"] + list(df_costs["Sekce nákladu"].unique())
221
- selected_cat_filter = st.selectbox("Vyberte sekci pro filtr:", available_cats)
222
  with f_col2:
223
- selected_type_filter = st.radio("Filtrovat typ budgetu:", ["Všechny rozpočty", "Pouze závazné přesné nabídky", "Pouze orientační odhady"], horizontal=True)
224
 
225
- # Aplikace filtrů
226
  if selected_cat_filter != "Zobrazit vše":
227
  df_costs = df_costs[df_costs["Sekce nákladu"] == selected_cat_filter]
228
 
229
  if selected_type_filter == "Pouze závazné přesné nabídky":
230
- df_costs = df_costs[df_costs["Typ nabídky"] == "🎯 Přesná fixní"]
231
  elif selected_type_filter == "Pouze orientační odhady":
232
- df_costs = df_costs[df_costs["Typ nabídky"] == "🔮 Orientační odhad"]
233
 
234
  if not df_costs.empty:
235
- # Výpočet sumy v měně uživatele
236
  total_filtered_eur = df_costs["Částka (Základ v EUR)"].sum()
 
237
 
238
- st.markdown(f"#### 💰 Celková útrata ve vyfiltrované sekci: **{format_price(total_filtered_eur)}**")
239
-
240
- # Formátování tabulky pro zobrazení uživateli
241
  df_costs_display = df_costs.copy()
242
  df_costs_display["Částka"] = df_costs_display["Částka (Základ v EUR)"].apply(format_price)
243
  df_costs_display = df_costs_display.drop(columns=["Částka (Základ v EUR)"])
244
-
245
  st.dataframe(df_costs_display, use_container_width=True, hide_index=True)
246
  else:
247
- st.info("Pro vybranou kombinaci filtrů nebyly nalezeny žádné uložené položky.")
248
  else:
249
- st.info("Zatím nebyly uloženy žádné nabídky s podrobným rozpadem nákladů.")
250
 
251
  st.markdown("---")
252
- st.subheader("Přehled vytvořených nabídek a kalkulací")
253
  if st.session_state.db_quotes:
254
  df_raw = pd.DataFrame(st.session_state.db_quotes)
255
  df_display = df_raw.copy()
256
  df_display["celkem"] = df_display["celkem"].apply(format_price)
257
  df_display["typ"] = df_display["typ"].map({"odhad": "🔮 Orientační odhad", "fixni": "🎯 Přesná fixní"})
258
- st.dataframe(df_display[["datum", "zakaznik", "typ", "popis", "celkem"]], use_container_width=True)
 
 
259
 
260
  # -----------------------------------------------------------------------------
261
- # TAB 2: AI GENERÁTOR NABÍDEK (S DYNAMICKÝMI KOLONKAMI NÁKLADŮ)
262
  # -----------------------------------------------------------------------------
263
  with tab_generator:
264
  st.header(t["tab_gen"])
@@ -266,328 +264,410 @@ with tab_generator:
266
  st.error(t["paywall_msg"])
267
  else:
268
  quote_type = st.radio(
269
- "Vyberte typ výstupu a rozpočtování:",
270
- ["🔮 Orientační odhad od AI (Nenahrává se do dlužníků/faktur, čistě orientační budget)",
271
- "🎯 Přesná fixní nabídka (Závazná kalkulace, kterou klient zaplatí a propíše se do fakturace)"]
272
  )
273
  is_fixed = "Přesná" in quote_type
274
 
 
275
  cust_names = [c["jmeno"] for c in st.session_state.db_customers]
276
- selected_cust = st.selectbox("Klient pro nabídku", cust_names if cust_names else ["-"])
277
- input_task = st.text_area("Specifikace projektu a popis zakázky", value="Kompletní rekonstrukce rodinného domu, rozvody vody a finální zapojení sanitární techniky.")
278
 
279
- st.markdown("### 🕒 Odpracované hodiny týmu na projektu")
 
 
 
 
 
 
280
 
281
- if st.button("🪄 Předvyplnit hodiny a materiál pomocí AI odhadu"):
282
- st.session_state[f"hours_mock_triggered"] = True
283
- # AI přepíše dynamické kolonky na odhadované položky
284
- st.session_state.temp_extra_costs = [
285
- {"kategorie": "Materiál", "nazev": "AI Odhad: Potrubí a fitinky", "castka": 480.0},
286
- {"kategorie": "Benzín / Doprava", "nazev": "AI Odhad: Závoz materiálu", "castka": 50.0},
287
- {"kategorie": "Pronájem vybavení", "nazev": "AI Odhad: Zapůjčení svářečky plastů", "castka": 120.0}
288
- ]
289
- st.toast("AI provedla analýzu textu a kompletně navrhla časy i položky!", icon="🪄")
 
290
 
291
  team_hours = {}
292
  cols_team = st.columns(max(len(st.session_state.db_team), 1))
293
 
294
  for idx, member in enumerate(st.session_state.db_team):
295
  with cols_team[idx]:
296
- default_hours = 0
297
- if st.session_state.get(f"hours_mock_triggered", False):
298
- if "director" in member["jmeno"].lower(): default_hours = 12
299
- elif "prezentace" in member["jmeno"].lower(): default_hours = 8
300
- else: default_hours = 32
301
 
302
  hours_input = st.number_input(
303
  f"{member['jmeno']} ({format_price(member['sazba'])}/h)",
304
- min_value=0, max_value=500, value=default_hours, key=f"hours_{member['id']}"
305
  )
306
  team_hours[member["id"]] = hours_input
307
 
308
- # 🌟 VYLEPŠENÁ SEKCE: DYNAMICKÉ DODATEČNÉ NÁKLADY S PŘIDÁVÁNÍM A MAZÁNÍM
309
- st.markdown("### 🚚 Přímé dodatečné náklady zakázky")
310
- st.write("Přidejte libovolné položky nákladů. Každé položce přiřaďte správnou nákladovou sekci pro pozdější filtrování.")
311
-
312
  curr_sym = CURRENCIES[st.session_state.curr]["symbol"]
313
  curr_rate = CURRENCIES[st.session_state.curr]["rate"]
314
-
315
- # Vykreslení dynamických kolonek
316
- indices_to_remove = []
317
- for idx, cost_item in enumerate(st.session_state.temp_extra_costs):
318
- c_col1, c_col2, c_col3, c_col4 = st.columns([2, 3, 2, 1])
319
-
320
- with c_col1:
321
- cost_item["kategorie"] = st.selectbox(
322
- f"Sekce nákladu #{idx+1}",
323
- ["Materiál", "Benzín / Doprava", "Pronájem vybavení", "Ostatní režie"],
324
- key=f"item_cat_{idx}",
325
- index=["Materiál", "Benzín / Doprava", "Pronájem vybavení", "Ostatní režie"].index(cost_item["kategorie"])
326
- )
327
- with c_col2:
328
- cost_item["nazev"] = st.text_input(f"Název položky / popis #{idx+1}", value=cost_item.get("nazev", ""), key=f"item_name_{idx}", placeholder="Např. Střešní latě 4m")
329
- with c_col3:
330
- cost_item["castka"] = st.number_input(f"Částka ({curr_sym}) #{idx+1}", min_value=0.0, value=float(cost_item.get("castka", 0.0)), key=f"item_amt_{idx}")
331
- with c_col4:
332
- st.markdown("<div style='padding-top: 24px;'></div>", unsafe_allow_html=True)
333
- if st.button("🗑️", key=f"item_del_{idx}", help="Odebrat tuto kolonku"):
334
- indices_to_remove.append(idx)
335
-
336
- # Zpracování smazání řádku
337
- if indices_to_remove:
338
- for i in reversed(indices_to_remove):
339
- st.session_state.temp_extra_costs.pop(i)
340
- st.rerun()
341
 
342
- # Tlačítka pro správu formuláře nákladů
343
- act_col1, act_col2 = st.columns([1, 3])
344
- with act_col1:
345
- if st.button("➕ Přidat kolonku nákladu", use_container_width=True):
346
- st.session_state.temp_extra_costs.append({"kategorie": "Materiál", "nazev": "", "castka": 0.0})
347
- st.rerun()
348
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  st.markdown("---")
350
 
351
- # Výpočty celkových sum
352
  total_labor_eur = 0.0
353
  for member in st.session_state.db_team:
354
  total_labor_eur += team_hours[member["id"]] * member["sazba"]
355
-
356
- total_extra_local = sum(item["castka"] for item in st.session_state.temp_extra_costs)
357
  total_extra_eur = total_extra_local / curr_rate
358
  total_project_eur = total_labor_eur + total_extra_eur
359
 
360
  st.markdown(f"### 📈 Celková kalkulace rozpočtu: {format_price(total_project_eur)}")
361
 
362
- if st.button("💾 Uložit rozpočet do systému", type="primary"):
363
- # Transformace dynamických nákladů do trvalé databáze (přepočteno na základní EUR)
364
- saved_extra_costs = []
365
- for item in st.session_state.temp_extra_costs:
366
- saved_extra_costs.append({
367
- "kategorie": item["kategorie"],
368
- "nazev": item["nazev"],
369
- "eur": item["castka"] / curr_rate
370
- })
 
 
 
371
 
372
- new_quote = {
373
- "id": len(st.session_state.db_quotes) + 1,
374
- "zakaznik": selected_cust,
375
- "popis": input_task,
376
- "celkem": total_project_eur,
377
- "typ": "fixni" if is_fixed else "odhad",
378
- "datum": time.strftime("%Y-%m-%d"),
379
- "labor_eur": total_labor_eur,
380
- "extra_costs": saved_extra_costs
381
- }
382
- st.session_state.db_quotes.append(new_quote)
383
- st.session_state.quotes_count += 1
384
- st.success(f"Uloženo jako {'🎯 PŘESNÁ FIXNÍ NABÍDKA' if is_fixed else '🔮 ORIENTAČNÍ ODHAD'}")
385
-
386
- # Reset stavů formuláře
387
- st.session_state[f"hours_mock_triggered"] = False
388
- st.session_state.temp_extra_costs = [{"kategorie": "Materiál", "nazev": "", "castka": 0.0}]
389
- time.sleep(0.5)
390
- st.rerun()
 
 
 
 
 
 
 
 
 
 
391
 
392
  # -----------------------------------------------------------------------------
393
  # TAB 3: SPRÁVA TÝMU
394
  # -----------------------------------------------------------------------------
395
  with tab_team:
396
  st.header("👥 Správa firemního týmu a hodinových sazeb")
397
- st.write("Zde můžete spravovat jednotlivé členy týmu. Jejich hodinové sazby se automaticky promítají do kalkulátoru nabídek.")
398
 
399
  t_col1, t_col2 = st.columns([2, 1])
400
 
401
  with t_col1:
402
- st.markdown("### Aktuální členové týmu")
403
- team_df_data = []
404
  for m in st.session_state.db_team:
405
- team_df_data.append({
406
- "ID": m["id"],
407
- "Jméno / Role": m["jmeno"],
408
- "Základní hodinová sazba": format_price(m["sazba"])
409
  })
410
- if team_df_data:
411
- st.dataframe(pd.DataFrame(team_df_data), use_container_width=True, hide_index=True)
412
  else:
413
- st.warning("Tým nemá žádné členy. Přidejte lidi v pravém panelu.")
414
 
415
- st.markdown("**❌ Odebrat člena z týmu**")
416
  if st.session_state.db_team:
417
- member_to_remove = st.selectbox("Vyberte osobu k propuštění/odebrání", [m["jmeno"] for m in st.session_state.db_team])
418
- if st.button("Potvrdit odebrání člena"):
419
  st.session_state.db_team = [m for m in st.session_state.db_team if m["jmeno"] != member_to_remove]
420
- st.success(f"Člen {member_to_remove} byl odebrán z týmu.")
421
- time.sleep(0.4)
422
  st.rerun()
423
 
424
  with t_col2:
425
- st.markdown("### ➕ Přidat / Upravit člena")
426
- with st.form("add_team_member_form"):
427
- tm_name = st.text_input("Jméno a příjmení / Role", placeholder="Např. Honza (Zedník)")
428
- c_symbol = CURRENCIES[st.session_state.curr]["symbol"]
429
- tm_rate_local = st.number_input(f"Hodinová sazba ({c_symbol}/hod)", min_value=1, value=500 if st.session_state.curr == "CZK" else 25)
430
 
431
- if st.form_submit_button("Uložit do týmu"):
432
- if tm_name:
433
- rate_in_eur = tm_rate_local / CURRENCIES[st.session_state.curr]["rate"]
434
- existing = next((m for m in st.session_state.db_team if m["jmeno"].lower() == tm_name.lower()), None)
435
- if existing:
436
- existing["sazba"] = rate_in_eur
437
- st.success(f"Sazba u člena {tm_name} byla upravena.")
438
  else:
439
- new_id = max([m["id"] for m in st.session_state.db_team]) + 1 if st.session_state.db_team else 1
440
- st.session_state.db_team.append({"id": new_id, "jmeno": tm_name, "sazba": rate_in_eur})
441
- st.success(f"Člen {tm_name} byl úspěšně přidán do týmu.")
442
- time.sleep(0.4)
443
  st.rerun()
444
 
445
  # -----------------------------------------------------------------------------
446
- # TAB 4: CRM LITE
447
  # -----------------------------------------------------------------------------
448
  with tab_crm:
449
  st.header(t["tab_crm"])
450
- col1, col2 = st.columns([2, 1])
451
- with col1:
452
  if st.session_state.db_customers:
453
- st.dataframe(pd.DataFrame(st.session_state.db_customers)[["jmeno", "firma", "telefon"]], use_container_width=True)
454
- with col2:
455
- with st.form("add_crm"):
456
- n = st.text_input(t["cust_name"])
457
- f = st.text_input(t["company"])
458
- p = st.text_input(t["phone"])
459
- if st.form_submit_button(t["save_cust"]) and n:
460
- st.session_state.db_customers.append({"id": len(st.session_state.db_customers)+1, "jmeno": n, "firma": f if f else "Fyzická osoba", "telefon": p})
461
- st.success("Uloženo"); time.sleep(0.3); st.rerun()
 
 
 
 
 
 
 
 
 
 
 
462
 
463
  # -----------------------------------------------------------------------------
464
- # TAB 5: FAKTURACE (POUZE PRO FIXNÍ NABÍDKY)
465
  # -----------------------------------------------------------------------------
466
  with tab_fakturace:
467
  st.header(t["tab_inv"])
468
- valid_quotes = [q for q in st.session_state.db_quotes if q.get("typ") == "fixni"]
469
 
470
- if valid_quotes:
471
- quote_options = {f"ID: {q['id']} | {q['zakaznik']} — {format_price(q['celkem'])}": q for q in valid_quotes}
472
- selected_quote_label = st.selectbox("Vyberte schválenou PŘESNOU zakázku k okamžité fakturaci", list(quote_options.keys()))
473
- chosen_quote = quote_options[selected_quote_label]
474
 
475
  if st.button(t["invoice_btn"], type="primary", use_container_width=True):
476
  st.session_state.db_invoices.append({
477
- "id": len(st.session_state.db_invoices)+1,
478
  "cislo": f"2026{len(st.session_state.db_invoices)+1:04d}",
479
- "zakaznik": chosen_quote["zakaznik"],
480
- "castka": chosen_quote["celkem"],
481
  "stav": "Odesláno",
482
  "datum": time.strftime("%Y-%m-%d")
483
  })
484
- st.success("Faktura vystavena a propsána dlužníkovi!")
 
485
  st.rerun()
486
  else:
487
- st.info("V systému nejsou žádné hotové 'Přesné fixní nabídky' k fakturaci. (Orientační odhady nelze fakturovat).")
488
 
489
  st.markdown("---")
490
- st.subheader("Kompletní kniha vystavených faktur")
491
- for idx, inv in enumerate(st.session_state.db_invoices):
492
- cx1, cx2, cx3 = st.columns([2, 1, 1])
493
- cx1.write(f"**FA {inv['cislo']}** — {inv['zakaznik']} ({format_price(inv['castka'])})")
494
- cx2.write("🟢 Zaplaceno" if inv["stav"] == "Zaplaceno" else "🔵 Odesláno (Nezaplaceno)")
495
- if inv["stav"] == "Odesláno" and cx3.button("Označit jako zaplacené", key=f"p_btn_{idx}"):
496
- inv["stav"] = "Zaplaceno"
497
- st.rerun()
 
 
 
 
 
 
 
 
 
498
 
499
  # -----------------------------------------------------------------------------
500
- # TAB 6: PRO BONUSY (AUTOMATIZOVANÉ SMS CENTRUM S PÁROVÁNÍM DAT)
501
  # -----------------------------------------------------------------------------
502
  with tab_pro_features:
503
  st.header(t["tab_pro"])
504
  if not st.session_state.user_subscribed:
505
- st.warning("🔒 Tato sekce obsahuje prémiové enterprise moduly dostupné pouze pro uživatele s aktivovaným tarifem TradeFlow PRO.")
506
  else:
507
- p_subtab1, p_subtab2 = st.tabs([t["pro_router"], t["pro_sms"]])
508
 
509
- with p_subtab1:
510
- st.subheader("Optimalizace logistiky")
511
- st.text_area("Adresy zakázek", value="Plzeňská 15, Praha\nPrůmyslová 4, Beroun\nSukova 12, Plzeň")
512
- if st.button("🗺️ Spočítat trasu"):
513
- st.info("🚀 AI Výsledek: Beroun -> Praha -> Plzeň. Úspora: 42 km.")
514
 
515
- with p_subtab2:
516
- st.subheader("📱 Pokročilé SMS Centrum upomínek")
517
 
518
- st.markdown("### 🔍 1. Vyhledání a seřazení dlužníků")
519
- unpaid_invoices = [inv for inv in st.session_state.db_invoices if inv["stav"] == "Odesláno"]
520
 
521
- sc1, sc2 = st.columns([1, 1])
522
- with sc1:
523
- search_query = st.text_input("Hledat dlužníka podle jména", value="")
524
- with sc2:
525
- sort_option = st.selectbox("Seřadit neplatiče podle:", ["Od nejvyšší částky", "Od nejnižší částky"])
526
 
527
- if sort_option == "Od nejvyšší částky": unpaid_invoices.sort(key=lambda x: x["castka"], reverse=True)
528
- elif sort_option == "Od nejnižší částky": unpaid_invoices.sort(key=lambda x: x["castka"])
 
 
 
 
 
529
 
530
- if not unpaid_invoices:
531
- st.info("Žádné nezaplacené faktury.")
532
- chosen_debtor = None
533
  else:
534
- debtor_options = {f"👤 {inv['zakaznik']} (FA: {inv['cislo']} | Dluží: {format_price(inv['castka'])})": inv for inv in unpaid_invoices}
535
- selected_debtor_label = st.selectbox("🎯 Vyberte dlužníka", list(debtor_options.keys()))
536
- chosen_debtor = debtor_options[selected_debtor_label]
537
 
538
  st.markdown("---")
539
- st.markdown("### 📋 2. Výběr a správa šablon (Presetů)")
540
 
541
- preset_names = [p["název"] for p in st.session_state.sms_presets]
542
- selected_preset_name = st.selectbox("Zvolte aktivní SMS šablonu", preset_names)
543
- current_preset = next(p for p in st.session_state.sms_presets if p["název"] == selected_preset_name)
544
 
545
- with st.expander("⚙️ Nastavení presetů & AI psaní šablon"):
546
- st.markdown("🤖 **AI Asistent pro tvorbu textu šablony**")
547
- ai_preset_prompt = st.text_input("Zadejte krátké zadání pro AI", placeholder="Napište jak má šablona působit...")
548
 
549
- if st.button("🪄 Vygenerovat text šablony pomocí AI"):
550
- if ai_preset_prompt:
551
- p_lower = ai_preset_prompt.lower()
552
- if "soud" in p_lower or "urg" in p_lower:
553
- generated_text = "DŮRAZNÁ VÝZVA: Vážený pane/paní {jmeno}. Faktura {cislo} na částku {castka} je po splatnosti. Žádáme o okamžitou úhradu, abychom předešli právním krokům."
554
  else:
555
- generated_text = "Ahoj {jmeno}, jen rychlé upozornění, že faktura {cislo} ({castka}) je po splatnosti. Díky za vyřešení!"
556
- st.session_state["ai_generated_preset_text"] = generated_text
557
- st.success("AI text úspěšně sestaven!")
558
 
559
- new_name = st.text_input("Název nové šablony")
560
- default_preset_body = st.session_state.get("ai_generated_preset_text", "Dobrý den, {jmeno}, ...")
561
- new_text = st.text_area("Text šablony ({jmeno}, {cislo}, {castka})", value=default_preset_body)
562
 
563
- if st.button("Uložit novou šablonu"):
564
- if new_name and new_text:
565
- st.session_state.sms_presets.append({"název": new_name, "text": new_text})
566
- st.success("Přidáno!")
 
567
  st.rerun()
568
 
569
  st.markdown("---")
570
  st.markdown("### 📱 3. Finální vygenerovaná zpráva pro klienta")
571
- if chosen_debtor:
572
- raw_sms_template = current_preset["text"]
573
- compiled_sms = raw_sms_template.replace("{jmeno}", chosen_debtor["zakaznik"])\
574
- .replace("{cislo}", chosen_debtor["cislo"])\
575
- .replace("{castka}", format_price(chosen_debtor["castka"]))
 
 
576
 
 
577
  st.markdown(f"""
578
  <div style="
579
  background-color: #f8f9fa;
580
  border: 1px solid #dee2e6;
581
  padding: 18px;
582
  border-radius: 8px;
583
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
584
- font-size: 16px;
585
  color: #212529;
586
  white-space: pre-wrap;
587
  word-wrap: break-word;
588
  line-height: 1.6;
589
- margin-bottom: 15px;
590
- ">{compiled_sms}</div>
591
  """, unsafe_allow_html=True)
592
 
593
- st.text_area("📋 Rychlá schránka pro zkopírování (Ctrl+A -> Ctrl+C):", value=compiled_sms, height=100)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
 
2
  import pandas as pd
3
  import time
4
+ import urllib.parse
 
5
 
6
  # -----------------------------------------------------------------------------
7
+ # JAZYKOVÉ LOKALIZACE (CZ, SK, EN)
8
  # -----------------------------------------------------------------------------
9
  TRANSLATIONS = {
10
  "CZ": {
 
45
  }
46
 
47
  # -----------------------------------------------------------------------------
48
+ # INICIALIZACE STAVU (ČISTÉ PRODUKČNÍ PROSTŘEDÍ)
49
  # -----------------------------------------------------------------------------
50
+ VERSION = "2.1.0-ENTERPRISE"
51
 
52
  st.set_page_config(page_title="TradeFlow AI", page_icon="🏆", layout="wide", initial_sidebar_state="expanded")
53
 
 
55
  if "curr" not in st.session_state: st.session_state.curr = "EUR"
56
  if "admin_authenticated" not in st.session_state: st.session_state.admin_authenticated = False
57
  if "user_subscribed" not in st.session_state: st.session_state.user_subscribed = False
58
+ if "quotes_count" not in st.session_state: st.session_state.quotes_count = 0
 
 
 
 
 
 
 
 
 
59
 
60
+ # Výchozí fixní struktura týmu (hodnoty uloženy v základní měně EUR)
61
  if "db_team" not in st.session_state:
62
  st.session_state.db_team = [
63
  {"id": 1, "jmeno": "Šimon (Director)", "sazba": 45.0},
 
65
  {"id": 3, "jmeno": "Matěj (Technik)", "sazba": 25.0}
66
  ]
67
 
68
+ # Ostré databáze začínají prázdné
69
+ if "db_customers" not in st.session_state: st.session_state.db_customers = []
70
+ if "db_quotes" not in st.session_state: st.session_state.db_quotes = []
71
+ if "db_invoices" not in st.session_state: st.session_state.db_invoices = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  if "sms_presets" not in st.session_state:
74
  st.session_state.sms_presets = [
 
77
  {"název": "Kamarádská připomínka", "text": "Ahoj {jmeno}, jen rychlá připomínka, že faktura {cislo} na částku {castka} už čeká na proplacení. Díky moc!"}
78
  ]
79
 
80
+ # Nativní DataFrame pro bezrefreshový editor přímých nákladů v Generátoru
81
+ if "cost_items_df" not in st.session_state:
82
+ st.session_state.cost_items_df = pd.DataFrame([
83
+ {"Sekce nákladu": "Materiál", "Název položky / Popis": "Spojovací materiál & Sanitární trubky", "Částka": 250.0}
84
+ ])
85
+
86
  def format_price(amount_in_eur):
87
  c_info = CURRENCIES[st.session_state.curr]
88
  converted = amount_in_eur * c_info["rate"]
 
90
  return f"{converted:,.2f} {c_info['symbol']}"
91
  return f"{c_info['symbol']}{converted:,.2f}"
92
 
 
 
 
 
 
 
 
93
  t = TRANSLATIONS[st.session_state.lang if st.session_state.lang in TRANSLATIONS else "CZ"]
94
 
95
  # -----------------------------------------------------------------------------
96
+ # SIDEBAR: CONFIG & ROBUSTNÍ DATA SEEDING PRO POROTU / INVESTORY
97
  # -----------------------------------------------------------------------------
98
  with st.sidebar:
99
  st.header("🌐 Nastavení / Settings")
 
104
  st.header("🛡️ Dev Admin Panel")
105
 
106
  if not st.session_state.admin_authenticated:
107
+ pwd_input = st.text_input("Zadejte administrátorské heslo", type="password", help="Heslo pro testování je: admin123")
108
  if st.button("Odemknout Admin Panel"):
109
  if pwd_input == "admin123":
110
  st.session_state.admin_authenticated = True
111
+ st.success("Přístup povolen!")
112
  st.rerun()
113
  else:
114
  st.error("Nesprávné heslo!")
115
  else:
116
+ st.info("🔓 Vývojářská autorizace")
117
+ st.session_state.user_subscribed = st.toggle("Uživatel má aktivní PRO tarif", value=st.session_state.user_subscribed)
118
+ st.session_state.quotes_count = st.number_input("Počet vytvořených nabídek", min_value=0, max_value=20, value=st.session_state.quotes_count)
119
 
120
+ st.markdown("### 🧪 Správa ukázkových dat")
121
+ if st.button("📥 Importovat komplexní Demo Data", use_container_width=True):
122
+ st.session_state.db_customers = [
123
+ {"id": 1, "jmeno": "Jan Novák", "firma": "Stavebniny s.r.o.", "telefon": "+420 777 123 456"},
124
+ {"id": 2, "jmeno": "Marie Podlahová", "firma": "Bytové družstvo", "telefon": "+420 602 987 654"},
125
+ {"id": 3, "jmeno": "Zdeněk Urban", "firma": "Instalatérství Urban", "telefon": "+420 731 555 999"}
126
+ ]
127
+ st.session_state.db_quotes = [
128
+ {
129
+ "id": 1,
130
+ "zakaznik": "Jan Novák",
131
+ "popis": "Kompletní instalace rozvodů vody a topení v novostavbě",
132
+ "celkem": 1250.0,
133
+ "typ": "fixni",
134
+ "datum": "2026-06-05",
135
+ "labor_eur": 750.0,
136
+ "extra_costs": [
137
+ {"kategorie": "Materiál", "nazev": "Měděné potrubí a fitinky", "eur": 400.0},
138
+ {"kategorie": "Benzín / Doprava", "nazev": "Doprava techniky a závoz materiálu", "eur": 100.0}
139
+ ]
140
+ }
141
+ ]
142
+ st.session_state.db_invoices = [
143
+ {"id": 1, "cislo": "20260001", "zakaznik": "Jan Novák", "castka": 1250.00, "stav": "Zaplaceno", "datum": "2026-06-05"},
144
+ {"id": 2, "cislo": "20260002", "zakaznik": "Marie Podlahová", "castka": 1580.50, "stav": "Odesláno", "datum": "2026-06-10"},
145
+ {"id": 3, "cislo": "20260003", "zakaznik": "Zdeněk Urban", "castka": 290.00, "stav": "Zaplaceno", "datum": "2026-06-12"},
146
+ {"id": 4, "cislo": "20260004", "zakaznik": "Jan Novák", "castka": 5010.00, "stav": "Odesláno", "datum": "2026-06-15"}
147
+ ]
148
+ st.session_state.quotes_count = 1
149
+ st.toast("Systém byl úspěšně naplněn cvičnými daty!", icon="📥")
150
+ time.sleep(0.3)
151
+ st.rerun()
152
+
153
+ if st.button("🗑️ Kompletní vyčištění databází", use_container_width=True):
154
+ st.session_state.db_customers = []
155
+ st.session_state.db_quotes = []
156
+ st.session_state.db_invoices = []
157
+ st.session_state.quotes_count = 0
158
+ st.session_state.cost_items_df = pd.DataFrame([{"Sekce nákladu": "Materiál", "Název položky / Popis": "", "Částka": 0.0}])
159
+ st.toast("Aplikace byla kompletně vyčištěna.", icon="🗑️")
160
+ time.sleep(0.3)
161
+ st.rerun()
162
+
163
+ st.markdown("---")
164
+ if st.button("Zamknout panel & Odhlásit se", use_container_width=True):
165
  st.session_state.admin_authenticated = False
166
  st.rerun()
167
 
168
  st.title(t["title"])
169
+ st.caption(f"{t['caption']} | Engine Verze: {VERSION}")
170
 
171
  is_locked = st.session_state.quotes_count >= 3 and not st.session_state.user_subscribed
172
 
 
175
  ])
176
 
177
  # -----------------------------------------------------------------------------
178
+ # TAB 1: METRIKY & ROBUSTNÍ FILTROVÁNÍ NÁKLADOVÝCH SEKCÍ
179
  # -----------------------------------------------------------------------------
180
  with tab_dashboard:
181
  st.header(t["tab_dash"])
 
186
  c1, c2, c3, c4 = st.columns(4)
187
  c1.metric(t["profit"], format_price(total_revenue))
188
  c2.metric(t["pending"], format_price(pending_revenue))
189
+ c3.metric(t["potential"], format_price(potencial), help="Počítá pouze Přesné fixní nabídky.")
190
+ c4.metric(t["limit"], f"{st.session_state.quotes_count} / 3", delta="⚠️ LIMIT UKONČEN" if is_locked else None)
191
 
 
192
  st.markdown("---")
193
+ st.subheader("📊 Pokročilá analýza a finanční filtrování sekcí")
194
+ st.write("Sledujte finanční toky seskupené podle konkrétních nákladových sekcí napříč celou firmou.")
195
 
196
+ # Rekonstrukce ploché tabulky nákladů z databáze uložených nabídek
197
  flat_costs = []
198
  for q in st.session_state.db_quotes:
 
199
  if q.get("labor_eur", 0) > 0:
200
  flat_costs.append({
201
  "Projekt / Klient": f"{q['zakaznik']} ({q['datum']})",
202
  "Sekce nákladu": "Výplaty zaměstnancům",
203
  "Název položky": "Mzda týmu na projektu (odpracované hodiny)",
204
  "Částka (Základ v EUR)": q["labor_eur"],
205
+ "Typ rozpočtu": "🎯 Přesná fixní" if q["typ"] == "fixni" else "🔮 Orientační odhad"
206
  })
 
207
  for ec in q.get("extra_costs", []):
208
  flat_costs.append({
209
  "Projekt / Klient": f"{q['zakaznik']} ({q['datum']})",
210
  "Sekce nákladu": ec["kategorie"],
211
+ "Název položky": ec["nazev"] if ec["nazev"] else "Nespecifikovaný přímý výdaj",
212
  "Částka (Základ v EUR)": ec["eur"],
213
+ "Typ rozpočtu": "🎯 Přesná fixní" if q["typ"] == "fixni" else "🔮 Orientační odhad"
214
  })
215
 
216
  if flat_costs:
217
  df_costs = pd.DataFrame(flat_costs)
218
 
 
219
  f_col1, f_col2 = st.columns(2)
220
  with f_col1:
221
  available_cats = ["Zobrazit vše"] + list(df_costs["Sekce nákladu"].unique())
222
+ selected_cat_filter = st.selectbox("Zvolte nákladovou kategorii k odfiltrování:", available_cats)
223
  with f_col2:
224
+ selected_type_filter = st.radio("Filtrovat typ zakázky:", ["Všechny rozpočty", "Pouze závazné přesné nabídky", "Pouze orientační odhady"], horizontal=True)
225
 
 
226
  if selected_cat_filter != "Zobrazit vše":
227
  df_costs = df_costs[df_costs["Sekce nákladu"] == selected_cat_filter]
228
 
229
  if selected_type_filter == "Pouze závazné přesné nabídky":
230
+ df_costs = df_costs[df_costs["Typ rozpočtu"] == "🎯 Přesná fixní"]
231
  elif selected_type_filter == "Pouze orientační odhady":
232
+ df_costs = df_costs[df_costs["Typ rozpočtu"] == "🔮 Orientační odhad"]
233
 
234
  if not df_costs.empty:
 
235
  total_filtered_eur = df_costs["Částka (Základ v EUR)"].sum()
236
+ st.markdown(f"#### 💰 Agregovaná útrata ve zvoleném filtru: **{format_price(total_filtered_eur)}**")
237
 
 
 
 
238
  df_costs_display = df_costs.copy()
239
  df_costs_display["Částka"] = df_costs_display["Částka (Základ v EUR)"].apply(format_price)
240
  df_costs_display = df_costs_display.drop(columns=["Částka (Základ v EUR)"])
 
241
  st.dataframe(df_costs_display, use_container_width=True, hide_index=True)
242
  else:
243
+ st.info("Pro zadaná kritéria a filtry nebyly nalezeny žádné záznamy.")
244
  else:
245
+ st.info("V systému zatím nejsou evidovány žádné podrobné položky nákladů.")
246
 
247
  st.markdown("---")
248
+ st.subheader("Kniha vytvořených kalkulací a nabídek")
249
  if st.session_state.db_quotes:
250
  df_raw = pd.DataFrame(st.session_state.db_quotes)
251
  df_display = df_raw.copy()
252
  df_display["celkem"] = df_display["celkem"].apply(format_price)
253
  df_display["typ"] = df_display["typ"].map({"odhad": "🔮 Orientační odhad", "fixni": "🎯 Přesná fixní"})
254
+ st.dataframe(df_display[["datum", "zakaznik", "typ", "popis", "celkem"]], use_container_width=True, hide_index=True)
255
+ else:
256
+ st.info("Kniha nabídek je prázdná.")
257
 
258
  # -----------------------------------------------------------------------------
259
+ # TAB 2: AI GENERÁTOR (DYNAMICKÝ EDITOR BEZ REFRESHŮ + CSV IMPORT + VOLNÝ TEXT KLIENTA)
260
  # -----------------------------------------------------------------------------
261
  with tab_generator:
262
  st.header(t["tab_gen"])
 
264
  st.error(t["paywall_msg"])
265
  else:
266
  quote_type = st.radio(
267
+ "Vyberte povahu výstupu a nacenění:",
268
+ ["🔮 Orientační odhad od AI (Čistě orientační kalkulace, nepropisuje se do dlužníků)",
269
+ "🎯 Přesná fixní nabídka (Závazný finanční plán, slouží jako podklad pro fakturu)"]
270
  )
271
  is_fixed = "Přesná" in quote_type
272
 
273
+ st.markdown("### 👤 Přiřazení klienta")
274
  cust_names = [c["jmeno"] for c in st.session_state.db_customers]
 
 
275
 
276
+ client_mode = st.radio("Způsob zadání zákazníka:", ["Vybrat z adresáře CRM", "✍️ Zadat úplně nového klienta ručně"], horizontal=True)
277
+ if client_mode == "Vybrat z adresáře CRM" and cust_names:
278
+ selected_cust = st.selectbox("Zvolte klienta:", cust_names)
279
+ else:
280
+ selected_cust = st.text_input("Zadejte jméno a příjmení nového klienta:", value="", placeholder="Např. František Novotný")
281
+
282
+ input_task = st.text_area("Detailní specifikace projektu a zadání pro AI", value="Kompletní rekonstrukce rodinného domu, rozvody vody a finální zapojení sanitární techniky.")
283
 
284
+ st.markdown("### 🕒 Personální alokace (Odpracované hodiny týmu)")
285
+
286
+ if st.button("🪄 Spustit AI simulaci a předvyplnit položky i hodiny"):
287
+ st.session_state["ai_mock_active"] = True
288
+ st.session_state.cost_items_df = pd.DataFrame([
289
+ {"Sekce nákladu": "Materiál", "Název položky / Popis": "AI Odhad: Potrubí, ventily a fitinky", "Částka": 480.0},
290
+ {"Sekce nákladu": "Benzín / Doprava", "Název položky / Popis": "AI Odhad: Nákladní závoz materiálu z velkoobchodu", "Částka": 65.0},
291
+ {"Sekce nákladu": "Pronájem vybavení", "Název položky / Popis": "AI Odhad: Zapůjčení tlakové zkoušečky", "Částka": 110.0}
292
+ ])
293
+ st.toast("AI úspěšně zanalyzovala text a navrhla optimální strukturu nákladů!", icon="🪄")
294
 
295
  team_hours = {}
296
  cols_team = st.columns(max(len(st.session_state.db_team), 1))
297
 
298
  for idx, member in enumerate(st.session_state.db_team):
299
  with cols_team[idx]:
300
+ default_val = 0
301
+ if st.session_state.get("ai_mock_active", False):
302
+ if "director" in member["jmeno"].lower(): default_val = 10
303
+ elif "prezentace" in member["jmeno"].lower(): default_val = 6
304
+ else: default_val = 24
305
 
306
  hours_input = st.number_input(
307
  f"{member['jmeno']} ({format_price(member['sazba'])}/h)",
308
+ min_value=0, max_value=500, value=default_val, key=f"gen_hours_{member['id']}"
309
  )
310
  team_hours[member["id"]] = hours_input
311
 
312
+ # 🌟 VYŘEŠENÍ REFRESHŮ: DYNAMICKÁ TABULKA S INTERAKTIVNÍM EDITORŮ BEZ RELOADŮ
313
+ st.markdown("### 🚚 Přímé materiálové a vedlejší náklady")
314
+ st.caption("💡 Nové řádky přidáte symbolem `+` vespod tabulky. Řádek smažete kliknutím na jeho okraj a stiskem klávesy `Delete` na klávesnici. Změny se aplikují bez překreslování stránky.")
315
+
316
  curr_sym = CURRENCIES[st.session_state.curr]["symbol"]
317
  curr_rate = CURRENCIES[st.session_state.curr]["rate"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
 
319
+ column_configuration = {
320
+ "Sekce nákladu": st.column_config.SelectboxColumn(
321
+ "Sekce nákladu",
322
+ options=["Materiál", "Benzín / Doprava", "Pronájem vybavení", "Ostatní režie"],
323
+ required=True
324
+ ),
325
+ "Název položky / Popis": st.column_config.TextColumn("Název položky / Popis", placeholder="Např. Sádrokartonová deska", required=True),
326
+ "Částka": st.column_config.NumberColumn(f"Částka ({curr_sym})", min_value=0.0, required=True, format="%.2f")
327
+ }
328
+
329
+ # Zobrazení nativního editoru
330
+ edited_df = st.data_editor(
331
+ st.session_state.cost_items_df,
332
+ column_config=column_configuration,
333
+ num_rows="dynamic",
334
+ use_container_width=True,
335
+ key="main_cost_data_editor"
336
+ )
337
+ st.session_state.cost_items_df = edited_df
338
+
339
+ # 🌟 ROZŠÍŘENÍ: HROMADNÝ IMPORT CSV TABULKY S AUTOMATICKÝM ROZTŘÍDĚNÍM
340
+ st.markdown("### 📂 Hromadný import externích nákladů přes CSV")
341
+ csv_file = st.file_uploader("Nahrajte soubor .csv (Struktura sloupců musí přesně být: Sekce nákladu, Název položky / Popis, Částka)", type=["csv"])
342
+ if csv_file is not None:
343
+ try:
344
+ uploaded_df = pd.read_csv(csv_file)
345
+ needed_headers = ["Sekce nákladu", "Název položky / Popis", "Částka"]
346
+ if all(head in uploaded_df.columns for head in needed_headers):
347
+ sanitized_df = uploaded_df[needed_headers].copy()
348
+ sanitized_df["Částka"] = pd.to_numeric(sanitized_df["Částka"], errors='coerce').fillna(0.0)
349
+ st.session_state.cost_items_df = pd.concat([st.session_state.cost_items_df, sanitized_df], ignore_index=True)
350
+ st.success("Externí CSV data byla úspěšně zpracována a integrována do tabulky výše!")
351
+ time.sleep(0.4)
352
+ st.rerun()
353
+ else:
354
+ st.error("Chyba validace: CSV neodpovídá požadované struktuře záhlaví.")
355
+ except Exception as e:
356
+ st.error(f"Soubor se nepodařilo přečíst: {e}")
357
+
358
  st.markdown("---")
359
 
360
+ # Finální sumace z aktuálního stavu editoru a personálních polí
361
  total_labor_eur = 0.0
362
  for member in st.session_state.db_team:
363
  total_labor_eur += team_hours[member["id"]] * member["sazba"]
364
+
365
+ total_extra_local = edited_df["Částka"].sum() if not edited_df.empty else 0.0
366
  total_extra_eur = total_extra_local / curr_rate
367
  total_project_eur = total_labor_eur + total_extra_eur
368
 
369
  st.markdown(f"### 📈 Celková kalkulace rozpočtu: {format_price(total_project_eur)}")
370
 
371
+ if st.button("💾 Schválit a uložit rozpočet do systému", type="primary"):
372
+ if not selected_cust or selected_cust.strip() == "":
373
+ st.error("Chyba: Před uložením musíte uvést nebo vybrat jméno klienta!")
374
+ else:
375
+ # Automatické zanesení klienta do CRM, pokud jde o nový textový záznam
376
+ if selected_cust not in cust_names:
377
+ st.session_state.db_customers.append({
378
+ "id": len(st.session_state.db_customers) + 1,
379
+ "jmeno": selected_cust.strip(),
380
+ "firma": "Fyzická osoba (Automatický import)",
381
+ "telefon": "Nezadáno"
382
+ })
383
 
384
+ # Transformace dat z editoru do čisté databáze v EUR
385
+ saved_extra_costs = []
386
+ for _, row in edited_df.iterrows():
387
+ saved_extra_costs.append({
388
+ "kategorie": row["Sekce nákladu"],
389
+ "nazev": row["Název položky / Popis"],
390
+ "eur": float(row["Částka"]) / curr_rate
391
+ })
392
+
393
+ new_quote = {
394
+ "id": len(st.session_state.db_quotes) + 1,
395
+ "zakaznik": selected_cust.strip(),
396
+ "popis": input_task,
397
+ "celkem": total_project_eur,
398
+ "typ": "fixni" if is_fixed else "odhad",
399
+ "datum": time.strftime("%Y-%m-%d"),
400
+ "labor_eur": total_labor_eur,
401
+ "extra_costs": saved_extra_costs
402
+ }
403
+
404
+ st.session_state.db_quotes.append(new_quote)
405
+ st.session_state.quotes_count += 1
406
+ st.session_state["ai_mock_active"] = False
407
+
408
+ # Vyčištění tabulky na výchozí prázdný stav
409
+ st.session_state.cost_items_df = pd.DataFrame([{"Sekce nákladu": "Materiál", "Název položky / Popis": "", "Částka": 0.0}])
410
+ st.success("Rozpočet byl úspěšně zanesen do firemní databáze.")
411
+ time.sleep(0.4)
412
+ st.rerun()
413
 
414
  # -----------------------------------------------------------------------------
415
  # TAB 3: SPRÁVA TÝMU
416
  # -----------------------------------------------------------------------------
417
  with tab_team:
418
  st.header("👥 Správa firemního týmu a hodinových sazeb")
419
+ st.write("Editujte profily a interní hodinové sazby svých pracovníků. Změny se okamžitě projevují v kalkulačním modulu.")
420
 
421
  t_col1, t_col2 = st.columns([2, 1])
422
 
423
  with t_col1:
424
+ st.markdown("### Evidovaní pracovníci a specialisté")
425
+ team_display_list = []
426
  for m in st.session_state.db_team:
427
+ team_display_list.append({
428
+ "Interní ID": m["id"],
429
+ "Jméno člena / Profesní role": m["jmeno"],
430
+ "Hodinová mzdová sazba": format_price(m["sazba"])
431
  })
432
+ if team_display_list:
433
+ st.dataframe(pd.DataFrame(team_display_list), use_container_width=True, hide_index=True)
434
  else:
435
+ st.warning("V týmu se nenachází žádný pracovník.")
436
 
437
+ st.markdown("**❌ Odstranění pracovníka z týmu**")
438
  if st.session_state.db_team:
439
+ member_to_remove = st.selectbox("Vyberte osobu k odebrání:", [m["jmeno"] for m in st.session_state.db_team])
440
+ if st.button("Definitivně odebrat člena"):
441
  st.session_state.db_team = [m for m in st.session_state.db_team if m["jmeno"] != member_to_remove]
442
+ st.success(f"Pracovník {member_to_remove} byl vyřazen z interní databáze.")
443
+ time.sleep(0.3)
444
  st.rerun()
445
 
446
  with t_col2:
447
+ st.markdown("### ➕ Přidat / Upravit kartu pracovníka")
448
+ with st.form("team_management_form"):
449
+ new_tm_name = st.text_input("Jméno příjmení / Pracovní zařazení", placeholder="Např. Kamil (Zedník)")
450
+ local_symbol = CURRENCIES[st.session_state.curr]["symbol"]
451
+ new_tm_rate = st.number_input(f"Hodinová sazba v aktivní měně ({local_symbol}/h)", min_value=1, value=400 if st.session_state.curr == "CZK" else 20)
452
 
453
+ if st.form_submit_button("Uložit profil"):
454
+ if new_tm_name:
455
+ rate_in_eur = new_tm_rate / CURRENCIES[st.session_state.curr]["rate"]
456
+ matched_member = next((m for m in st.session_state.db_team if m["jmeno"].lower() == new_tm_name.lower()), None)
457
+ if matched_member:
458
+ matched_member["sazba"] = rate_in_eur
459
+ st.success(f"Sazba u pracovníka {new_tm_name} byla zaktualizována.")
460
  else:
461
+ next_id = max([m["id"] for m in st.session_state.db_team]) + 1 if st.session_state.db_team else 1
462
+ st.session_state.db_team.append({"id": next_id, "jmeno": new_tm_name, "sazba": rate_in_eur})
463
+ st.success(f"Nový člen {new_tm_name} byl úspěšně zaevidován.")
464
+ time.sleep(0.3)
465
  st.rerun()
466
 
467
  # -----------------------------------------------------------------------------
468
+ # TAB 4: CRM ADRESÁŘ
469
  # -----------------------------------------------------------------------------
470
  with tab_crm:
471
  st.header(t["tab_crm"])
472
+ col_crm1, col_crm2 = st.columns([2, 1])
473
+ with col_crm1:
474
  if st.session_state.db_customers:
475
+ st.dataframe(pd.DataFrame(st.session_state.db_customers)[["jmeno", "firma", "telefon"]], use_container_width=True, hide_index=True)
476
+ else:
477
+ st.info("Adresář klientů je momentálně prázdný.")
478
+ with col_crm2:
479
+ st.markdown("### ➕ Rychlé ruční založení klienta")
480
+ with st.form("manual_crm_form"):
481
+ c_n = st.text_input(t["cust_name"], placeholder="Jan Novák")
482
+ c_f = st.text_input(t["company"], placeholder="Stavebniny s.r.o.")
483
+ c_p = st.text_input(t["phone"], placeholder="+420 777 888 999")
484
+ if st.form_submit_button(t["save_cust"]):
485
+ if c_n:
486
+ st.session_state.db_customers.append({
487
+ "id": len(st.session_state.db_customers) + 1,
488
+ "jmeno": c_n,
489
+ "firma": c_f if c_f else "Fyzická osoba",
490
+ "telefon": c_p
491
+ })
492
+ st.success("Klient byl bezpečně uložen do CRM."); time.sleep(0.3); st.rerun()
493
+ else:
494
+ st.error("Jméno klienta je povinné pole.")
495
 
496
  # -----------------------------------------------------------------------------
497
+ # TAB 5: FAKTURACE (ZÁVAZNÉ POSTUPY PRO FIXNÍ NABÍDKY)
498
  # -----------------------------------------------------------------------------
499
  with tab_fakturace:
500
  st.header(t["tab_inv"])
501
+ only_fixni_quotes = [q for q in st.session_state.db_quotes if q.get("typ") == "fixni"]
502
 
503
+ if only_fixni_quotes:
504
+ invoice_options = {f"Zakázka ID: {q['id']} | Odběratel: {q['zakaznik']} — {format_price(q['celkem'])}": q for q in only_fixni_quotes}
505
+ selected_quote_to_inv = st.selectbox("Zvolte schválený pevný rozpočet k okamžité fakturaci:", list(invoice_options.keys()))
506
+ targeted_quote = invoice_options[selected_quote_to_inv]
507
 
508
  if st.button(t["invoice_btn"], type="primary", use_container_width=True):
509
  st.session_state.db_invoices.append({
510
+ "id": len(st.session_state.db_invoices) + 1,
511
  "cislo": f"2026{len(st.session_state.db_invoices)+1:04d}",
512
+ "zakaznik": targeted_quote["zakaznik"],
513
+ "castka": targeted_quote["celkem"],
514
  "stav": "Odesláno",
515
  "datum": time.strftime("%Y-%m-%d")
516
  })
517
+ st.success("Faktura byla vystavena, uložena do knihy a odeslána zákazníkovi!")
518
+ time.sleep(0.3)
519
  st.rerun()
520
  else:
521
+ st.info("V systému se nenachází žádná schválená pevná nabídka (Orientační odhady nelze legálně fakturovat).")
522
 
523
  st.markdown("---")
524
+ st.subheader("Kompletní přehled vydaných faktur a stavu plateb")
525
+
526
+ if st.session_state.db_invoices:
527
+ for index_inv, invoice_obj in enumerate(st.session_state.db_invoices):
528
+ div_c1, div_c2, div_c3 = st.columns([2, 1, 1])
529
+ div_c1.write(f"🧾 **Faktura č. {invoice_obj['cislo']}** — {invoice_obj['zakaznik']} [**{format_price(invoice_obj['castka'])}**]")
530
+
531
+ if invoice_obj["stav"] == "Zaplaceno":
532
+ div_c2.markdown("<span style='color:green; font-weight:bold;'>🟢 Uhrazeno</span>", unsafe_allow_html=True)
533
+ div_c3.write("")
534
+ else:
535
+ div_c2.markdown("<span style='color:orange; font-weight:bold;'>🔵 Odesláno (Po splatnosti)</span>", unsafe_allow_html=True)
536
+ if div_c3.button("Označit jako ZAPLACENO", key=f"payment_trigger_key_{index_inv}", use_container_width=True):
537
+ invoice_obj["stav"] = "Zaplaceno"
538
+ st.rerun()
539
+ else:
540
+ st.info("Kniha faktur je prázdná.")
541
 
542
  # -----------------------------------------------------------------------------
543
+ # TAB 6: PRO BONUSY (AUTOMATIZOVANÉ SMS CENTRUM S JAVASCRIPT KOPÍROVÁNÍM BEZ REFRESHŮ)
544
  # -----------------------------------------------------------------------------
545
  with tab_pro_features:
546
  st.header(t["tab_pro"])
547
  if not st.session_state.user_subscribed:
548
+ st.warning("🔒 Tento panel obsahuje pokročilé moduly TradeFlow PRO. Aktivujte zkušební verzi v Dev Admin Panelu na boční liště.")
549
  else:
550
+ pro_sub1, pro_sub2 = st.tabs([t["pro_router"], t["pro_sms"]])
551
 
552
+ with pro_sub1:
553
+ st.subheader("📍 AI Geografický optimalizátor tras výjezdů")
554
+ st.text_area("Seznam adres plánovaných zásahů", value="Plzeňská 15, Praha\nPrůmyslová 4, Beroun\nSukova 12, Plzeň")
555
+ if st.button("🗺️ Spočítat matematicky nejkratší trasu", use_container_width=True):
556
+ st.info("🚀 AI Výsledek kalkulace: Výjezd z Plzně -> Beroun -> Praha. Celková úspora paliva a logistických nákladů činí: 42 km.")
557
 
558
+ with pro_sub2:
559
+ st.subheader("📱 Inteligentní manažerské SMS centrum pohledávek")
560
 
561
+ st.markdown("### 🔍 1. Vyhledání dlužníka v systému")
562
+ open_debts = [invoice for invoice in st.session_state.db_invoices if invoice["stav"] == "Odesláno"]
563
 
564
+ search_col1, search_col2 = st.columns([1, 1])
565
+ with search_col1:
566
+ query_name = st.text_input("Filtrovat neplatiče dle jména:", value="")
567
+ with search_col2:
568
+ sorting_order = st.selectbox("Seřadit dlužníky dle kritéria:", ["Od nejvyšší dlužné částky", "Od nejnižší dlužné částky"])
569
 
570
+ if query_name:
571
+ open_debts = [d for d in open_debts if query_name.lower() in d["zakaznik"].lower()]
572
+
573
+ if sorting_order == "Od nejvyšší dlužné částky":
574
+ open_debts.sort(key=lambda x: x["castka"], reverse=True)
575
+ else:
576
+ open_debts.sort(key=lambda x: x["castka"])
577
 
578
+ if not open_debts:
579
+ st.info("V systému aktuálně neevidujeme žádné neuhrazené faktury odpovídající filtrům.")
580
+ active_debtor = None
581
  else:
582
+ debtor_mapping = {f"👤 {d['zakaznik']} (Faktura č. {d['cislo']} | Dluh: {format_price(d['castka'])})": d for d in open_debts}
583
+ selected_debtor_string = st.selectbox("🎯 Vyberte cílového dlužníka k obeslání:", list(debtor_mapping.keys()))
584
+ active_debtor = debtor_mapping[selected_debtor_string]
585
 
586
  st.markdown("---")
587
+ st.markdown("### 📋 2. Výběr textové šablony")
588
 
589
+ preset_labels = [p["název"] for p in st.session_state.sms_presets]
590
+ chosen_preset_label = st.selectbox("Zvolte aktivní komunikační šablonu:", preset_labels)
591
+ active_preset = next(p for p in st.session_state.sms_presets if p["název"] == chosen_preset_label)
592
 
593
+ with st.expander("⚙️ Pokročilý editor šablon & Generátor nových textů přes AI"):
594
+ st.markdown("🤖 **AI Asistent pro tvorbu tónu textu**")
595
+ prompt_text = st.text_input("Zadejte zadání pro AI (např. 'Napiš velmi naléhavou předžalobní výzvu')", placeholder="Napište zadání...")
596
 
597
+ if st.button("🪄 Sestavit novou šablonu pomocí AI"):
598
+ if prompt_text:
599
+ p_low = prompt_text.lower()
600
+ if "předžal" in p_low or "naléh" in p_low or "soud" in p_low:
601
+ result_sms = "PŘEDŽALOBNÍ VÝZVA: Vážený kliente {jmeno}. Upozorňujeme Vás, že faktura č. {cislo} na částku {castka} je dlouhodobě po splatnosti. Žádáme o okamžitou úhradu, abychom předešli soudnímu vymáhání."
602
  else:
603
+ result_sms = "Dobrý den {jmeno}, připomínáme zapomenutou fakturu č. {cislo} na částku {castka}. Prosíme o co nejrychlejší proplacení. Děkujeme, Tým TradeFlow AI."
604
+ st.session_state["ai_sms_output_buffer"] = result_sms
605
+ st.success("AI text úspěšně vygenerován do pole níže!")
606
 
607
+ new_preset_title = st.text_input("Název nové šablony:", value="")
608
+ default_body = st.session_state.get("ai_sms_output_buffer", "Vážený pane/paní {jmeno}, ...")
609
+ new_preset_body = st.text_area("Text těla zprávy (používejte značky {jmeno}, {cislo}, {castka})", value=default_body)
610
 
611
+ if st.button("Uložit šablonu do trvalého seznamu"):
612
+ if new_preset_title and new_preset_body:
613
+ st.session_state.sms_presets.append({"název": new_preset_title, "text": new_preset_body})
614
+ st.success("Šablona úspěšně uložena!")
615
+ time.sleep(0.3)
616
  st.rerun()
617
 
618
  st.markdown("---")
619
  st.markdown("### 📱 3. Finální vygenerovaná zpráva pro klienta")
620
+
621
+ if active_debtor:
622
+ # Sestavení finální textace
623
+ raw_template = active_preset["text"]
624
+ final_compiled_sms = raw_template.replace("{jmeno}", active_debtor["zakaznik"])\
625
+ .replace("{cislo}", active_debtor["cislo"])\
626
+ .replace("{castka}", format_price(active_debtor["castka"]))
627
 
628
+ # Výpis textu v přesném vizuálním formátu dle image_dca906.png
629
  st.markdown(f"""
630
  <div style="
631
  background-color: #f8f9fa;
632
  border: 1px solid #dee2e6;
633
  padding: 18px;
634
  border-radius: 8px;
635
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
636
+ font-size: 15px;
637
  color: #212529;
638
  white-space: pre-wrap;
639
  word-wrap: break-word;
640
  line-height: 1.6;
641
+ margin-bottom: 12px;
642
+ " id="sms_display_box">{final_compiled_sms}</div>
643
  """, unsafe_allow_html=True)
644
 
645
+ # Bezpečné zakódování řetězce pro předání do JavaScriptu bez syntax errorů způsobených odřádkováním
646
+ safe_encoded_sms = urllib.parse.quote(final_compiled_sms)
647
+
648
+ # 🌟 KOPÍROVACÍ TLAČÍTKO POMOCÍ JAVASCRIPTU - ZKOPÍRUJE TEXT OKAMŽITĚ BEZ REFRESHNUTÍ STRÁNKY
649
+ copy_button_html = f"""
650
+ <button onclick="
651
+ navigator.clipboard.writeText(decodeURIComponent('{safe_encoded_sms}'));
652
+ let btn = document.getElementById('js_copy_btn');
653
+ btn.innerText = '✅ Zkopírováno do schránky!';
654
+ btn.style.backgroundColor = '#28a745';
655
+ setTimeout(() => {{ btn.innerText = '📋 Kopírovat text zprávy'; btn.style.backgroundColor = '#007bff'; }}, 2000);
656
+ " id="js_copy_btn" style="
657
+ background-color: #007bff;
658
+ color: white;
659
+ border: none;
660
+ padding: 12px 20px;
661
+ font-size: 14px;
662
+ border-radius: 6px;
663
+ cursor: pointer;
664
+ font-family: -apple-system, sans-serif;
665
+ font-weight: bold;
666
+ width: 100%;
667
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
668
+ transition: background-color 0.2s;
669
+ ">📋 Kopírovat text zprávy</button>
670
+ """
671
+ st.components.v1.html(copy_button_html, height=50)
672
+ else:
673
+ st.info("Vyberte dlužníka pro zobrazení a zkopírování SMS zprávy.")