Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -18,7 +18,7 @@ TRANSLATIONS = {
|
|
| 18 |
},
|
| 19 |
"SK": {
|
| 20 |
"title": "🏆 TradeFlow AI", "caption": "Chytry operačný systém pre novú generáciu remeselníkov.",
|
| 21 |
-
"tab_dash": "📈 Prehľad & Metriky", "tab_gen": "📝 AI Generátor Ponúk", "tab_team": "👥 Správa Týmu", "tab_crm": "👥
|
| 22 |
"profit": "Skutočný zisk", "pending": "Čakajúce peniaze", "potential": "Otvorené AI Ponuky", "limit": "Využitie Free limitu",
|
| 23 |
"cust_name": "Meno a priezvisko", "company": "Firma (Voliteľné)", "phone": "Telefónne číslo", "save_cust": "Uložiť klienta",
|
| 24 |
"generate_btn": "⚡ Vygenerovať/Uložiť rozpočet", "invoice_btn": "🧾 VYSTAVIŤ FAKTÚRU JEDNÝM KLIKNUTÍM",
|
|
@@ -47,7 +47,7 @@ CURRENCIES = {
|
|
| 47 |
# -----------------------------------------------------------------------------
|
| 48 |
# INICIALIZACE STAVU (ČISTÉ PRODUKČNÍ PROSTŘEDÍ)
|
| 49 |
# -----------------------------------------------------------------------------
|
| 50 |
-
VERSION = "2.1.
|
| 51 |
|
| 52 |
st.set_page_config(page_title="TradeFlow AI", page_icon="🏆", layout="wide", initial_sidebar_state="expanded")
|
| 53 |
|
|
@@ -57,15 +57,8 @@ if "admin_authenticated" not in st.session_state: st.session_state.admin_authent
|
|
| 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 |
-
#
|
| 61 |
-
if "db_team" not in st.session_state:
|
| 62 |
-
st.session_state.db_team = [
|
| 63 |
-
{"id": 1, "jmeno": "Šimon (Director)", "sazba": 45.0},
|
| 64 |
-
{"id": 2, "jmeno": "Nikola (Prezentace & Podpora)", "sazba": 30.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 = []
|
|
@@ -77,10 +70,10 @@ if "sms_presets" not in st.session_state:
|
|
| 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 |
-
#
|
| 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": "
|
| 84 |
])
|
| 85 |
|
| 86 |
def format_price(amount_in_eur):
|
|
@@ -119,6 +112,12 @@ with st.sidebar:
|
|
| 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"},
|
|
@@ -151,6 +150,7 @@ with st.sidebar:
|
|
| 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 = []
|
|
@@ -193,7 +193,6 @@ with tab_dashboard:
|
|
| 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:
|
|
@@ -256,7 +255,7 @@ with tab_dashboard:
|
|
| 256 |
st.info("Kniha nabídek je prázdná.")
|
| 257 |
|
| 258 |
# -----------------------------------------------------------------------------
|
| 259 |
-
# TAB 2: AI GENERÁTOR (
|
| 260 |
# -----------------------------------------------------------------------------
|
| 261 |
with tab_generator:
|
| 262 |
st.header(t["tab_gen"])
|
|
@@ -283,50 +282,53 @@ with tab_generator:
|
|
| 283 |
|
| 284 |
st.markdown("### 🕒 Personální alokace (Odpracované hodiny týmu)")
|
| 285 |
|
| 286 |
-
if st.
|
| 287 |
-
st.
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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",
|
| 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,
|
|
@@ -336,9 +338,8 @@ with tab_generator:
|
|
| 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ů
|
| 342 |
if csv_file is not None:
|
| 343 |
try:
|
| 344 |
uploaded_df = pd.read_csv(csv_file)
|
|
@@ -347,20 +348,19 @@ with tab_generator:
|
|
| 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("
|
| 351 |
time.sleep(0.4)
|
| 352 |
st.rerun()
|
| 353 |
else:
|
| 354 |
-
st.error("Chyba
|
| 355 |
except Exception as e:
|
| 356 |
-
st.error(f"Soubor
|
| 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
|
| 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
|
|
@@ -372,7 +372,6 @@ with tab_generator:
|
|
| 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,
|
|
@@ -381,7 +380,6 @@ with tab_generator:
|
|
| 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({
|
|
@@ -405,7 +403,6 @@ with tab_generator:
|
|
| 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)
|
|
@@ -416,7 +413,7 @@ with tab_generator:
|
|
| 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ů.
|
| 420 |
|
| 421 |
t_col1, t_col2 = st.columns([2, 1])
|
| 422 |
|
|
@@ -432,7 +429,7 @@ with tab_team:
|
|
| 432 |
if team_display_list:
|
| 433 |
st.dataframe(pd.DataFrame(team_display_list), use_container_width=True, hide_index=True)
|
| 434 |
else:
|
| 435 |
-
st.
|
| 436 |
|
| 437 |
st.markdown("**❌ Odstranění pracovníka z týmu**")
|
| 438 |
if st.session_state.db_team:
|
|
@@ -494,7 +491,7 @@ with tab_crm:
|
|
| 494 |
st.error("Jméno klienta je povinné pole.")
|
| 495 |
|
| 496 |
# -----------------------------------------------------------------------------
|
| 497 |
-
# TAB 5: FAKTURACE
|
| 498 |
# -----------------------------------------------------------------------------
|
| 499 |
with tab_fakturace:
|
| 500 |
st.header(t["tab_inv"])
|
|
@@ -514,11 +511,11 @@ with tab_fakturace:
|
|
| 514 |
"stav": "Odesláno",
|
| 515 |
"datum": time.strftime("%Y-%m-%d")
|
| 516 |
})
|
| 517 |
-
st.success("Faktura byla vystavena
|
| 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
|
| 522 |
|
| 523 |
st.markdown("---")
|
| 524 |
st.subheader("Kompletní přehled vydaných faktur a stavu plateb")
|
|
@@ -540,7 +537,7 @@ with tab_fakturace:
|
|
| 540 |
st.info("Kniha faktur je prázdná.")
|
| 541 |
|
| 542 |
# -----------------------------------------------------------------------------
|
| 543 |
-
# TAB 6: PRO BONUSY
|
| 544 |
# -----------------------------------------------------------------------------
|
| 545 |
with tab_pro_features:
|
| 546 |
st.header(t["tab_pro"])
|
|
@@ -619,13 +616,11 @@ with tab_pro_features:
|
|
| 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;
|
|
@@ -639,13 +634,11 @@ with tab_pro_features:
|
|
| 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}'));
|
|
|
|
| 18 |
},
|
| 19 |
"SK": {
|
| 20 |
"title": "🏆 TradeFlow AI", "caption": "Chytry operačný systém pre novú generáciu remeselníkov.",
|
| 21 |
+
"tab_dash": "📈 Prehľad & Metriky", "tab_gen": "📝 AI Generátor Ponúk", "tab_team": "👥 Správa Týmu", "tab_crm": "👥 Adresář (CRM)", "tab_inv": "🧾 Fakturácia jedným klikom", "tab_pro": "🚀 PRO Exkluzívne Funkcie",
|
| 22 |
"profit": "Skutočný zisk", "pending": "Čakajúce peniaze", "potential": "Otvorené AI Ponuky", "limit": "Využitie Free limitu",
|
| 23 |
"cust_name": "Meno a priezvisko", "company": "Firma (Voliteľné)", "phone": "Telefónne číslo", "save_cust": "Uložiť klienta",
|
| 24 |
"generate_btn": "⚡ Vygenerovať/Uložiť rozpočet", "invoice_btn": "🧾 VYSTAVIŤ FAKTÚRU JEDNÝM KLIKNUTÍM",
|
|
|
|
| 47 |
# -----------------------------------------------------------------------------
|
| 48 |
# INICIALIZACE STAVU (ČISTÉ PRODUKČNÍ PROSTŘEDÍ)
|
| 49 |
# -----------------------------------------------------------------------------
|
| 50 |
+
VERSION = "2.1.1-ENTERPRISE"
|
| 51 |
|
| 52 |
st.set_page_config(page_title="TradeFlow AI", page_icon="🏆", layout="wide", initial_sidebar_state="expanded")
|
| 53 |
|
|
|
|
| 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 |
+
# Ostré databáze startují 100% čisté a prázdné
|
| 61 |
+
if "db_team" not in st.session_state: st.session_state.db_team = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
if "db_customers" not in st.session_state: st.session_state.db_customers = []
|
| 63 |
if "db_quotes" not in st.session_state: st.session_state.db_quotes = []
|
| 64 |
if "db_invoices" not in st.session_state: st.session_state.db_invoices = []
|
|
|
|
| 70 |
{"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!"}
|
| 71 |
]
|
| 72 |
|
| 73 |
+
# Čistý výchozí řádek editoru bez předvyplněných částek
|
| 74 |
if "cost_items_df" not in st.session_state:
|
| 75 |
st.session_state.cost_items_df = pd.DataFrame([
|
| 76 |
+
{"Sekce nákladu": "Materiál", "Název položky / Popis": "", "Částka": 0.0}
|
| 77 |
])
|
| 78 |
|
| 79 |
def format_price(amount_in_eur):
|
|
|
|
| 112 |
|
| 113 |
st.markdown("### 🧪 Správa ukázkových dat")
|
| 114 |
if st.button("📥 Importovat komplexní Demo Data", use_container_width=True):
|
| 115 |
+
# Demo struktura týmu se nahraje až teď!
|
| 116 |
+
st.session_state.db_team = [
|
| 117 |
+
{"id": 1, "jmeno": "Šimon (Director)", "sazba": 45.0},
|
| 118 |
+
{"id": 2, "jmeno": "Nikola (Prezentace & Podpora)", "sazba": 30.0},
|
| 119 |
+
{"id": 3, "jmeno": "Matěj (Technik)", "sazba": 25.0}
|
| 120 |
+
]
|
| 121 |
st.session_state.db_customers = [
|
| 122 |
{"id": 1, "jmeno": "Jan Novák", "firma": "Stavebniny s.r.o.", "telefon": "+420 777 123 456"},
|
| 123 |
{"id": 2, "jmeno": "Marie Podlahová", "firma": "Bytové družstvo", "telefon": "+420 602 987 654"},
|
|
|
|
| 150 |
st.rerun()
|
| 151 |
|
| 152 |
if st.button("🗑️ Kompletní vyčištění databází", use_container_width=True):
|
| 153 |
+
st.session_state.db_team = []
|
| 154 |
st.session_state.db_customers = []
|
| 155 |
st.session_state.db_quotes = []
|
| 156 |
st.session_state.db_invoices = []
|
|
|
|
| 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 |
flat_costs = []
|
| 197 |
for q in st.session_state.db_quotes:
|
| 198 |
if q.get("labor_eur", 0) > 0:
|
|
|
|
| 255 |
st.info("Kniha nabídek je prázdná.")
|
| 256 |
|
| 257 |
# -----------------------------------------------------------------------------
|
| 258 |
+
# TAB 2: AI GENERÁTOR (OPRAVENÝ EDITOR BEZ CHYBY + VOLNÝ TEXT KLIENTA)
|
| 259 |
# -----------------------------------------------------------------------------
|
| 260 |
with tab_generator:
|
| 261 |
st.header(t["tab_gen"])
|
|
|
|
| 282 |
|
| 283 |
st.markdown("### 🕒 Personální alokace (Odpracované hodiny týmu)")
|
| 284 |
|
| 285 |
+
if not st.session_state.db_team:
|
| 286 |
+
st.info("ℹ️ V interní databázi zatím nejsou žádní pracovníci. Přidejte je v záložce 'Správa Týmu' nebo zapněte Demo Data v boční liště.")
|
| 287 |
+
team_hours = {}
|
| 288 |
+
else:
|
| 289 |
+
if st.button("🪄 Spustit AI simulaci a předvyplnit položky i hodiny"):
|
| 290 |
+
st.session_state["ai_mock_active"] = True
|
| 291 |
+
st.session_state.cost_items_df = pd.DataFrame([
|
| 292 |
+
{"Sekce nákladu": "Materiál", "Název položky / Popis": "AI Odhad: Potrubí, ventily a fitinky", "Částka": 480.0},
|
| 293 |
+
{"Sekce nákladu": "Benzín / Doprava", "Název položky / Popis": "AI Odhad: Nákladní závoz materiálu z velkoobchodu", "Částka": 65.0},
|
| 294 |
+
{"Sekce nákladu": "Pronájem vybavení", "Název položky / Popis": "AI Odhad: Zapůjčení tlakové zkoušečky", "Částka": 110.0}
|
| 295 |
+
])
|
| 296 |
+
st.toast("AI úspěšně zanalyzovala text a navrhla optimální strukturu nákladů!", icon="🪄")
|
| 297 |
+
|
| 298 |
+
team_hours = {}
|
| 299 |
+
cols_team = st.columns(len(st.session_state.db_team))
|
| 300 |
+
|
| 301 |
+
for idx, member in enumerate(st.session_state.db_team):
|
| 302 |
+
with cols_team[idx]:
|
| 303 |
+
default_val = 0
|
| 304 |
+
if st.session_state.get("ai_mock_active", False):
|
| 305 |
+
if "director" in member["jmeno"].lower(): default_val = 10
|
| 306 |
+
elif "prezentace" in member["jmeno"].lower(): default_val = 6
|
| 307 |
+
else: default_val = 24
|
| 308 |
+
|
| 309 |
+
hours_input = st.number_input(
|
| 310 |
+
f"{member['jmeno']} ({format_price(member['sazba'])}/h)",
|
| 311 |
+
min_value=0, max_value=500, value=default_val, key=f"gen_hours_{member['id']}"
|
| 312 |
+
)
|
| 313 |
+
team_hours[member["id"]] = hours_input
|
| 314 |
+
|
| 315 |
st.markdown("### 🚚 Přímé materiálové a vedlejší náklady")
|
| 316 |
+
st.caption("💡 Nové řádky přidáte symbolem `+` vespod tabulky. Řádek smažete označením a stiskem klávesy `Delete` na klávesnici.")
|
| 317 |
|
| 318 |
curr_sym = CURRENCIES[st.session_state.curr]["symbol"]
|
| 319 |
curr_rate = CURRENCIES[st.session_state.curr]["rate"]
|
| 320 |
|
| 321 |
+
# 🔥 OPRAVENO: Odstraněn nepovolený parametr 'placeholder' z TextColumn, kvůli kterému aplikace padala
|
| 322 |
column_configuration = {
|
| 323 |
"Sekce nákladu": st.column_config.SelectboxColumn(
|
| 324 |
"Sekce nákladu",
|
| 325 |
options=["Materiál", "Benzín / Doprava", "Pronájem vybavení", "Ostatní režie"],
|
| 326 |
required=True
|
| 327 |
),
|
| 328 |
+
"Název položky / Popis": st.column_config.TextColumn("Název položky / Popis", required=True),
|
| 329 |
"Částka": st.column_config.NumberColumn(f"Částka ({curr_sym})", min_value=0.0, required=True, format="%.2f")
|
| 330 |
}
|
| 331 |
|
|
|
|
| 332 |
edited_df = st.data_editor(
|
| 333 |
st.session_state.cost_items_df,
|
| 334 |
column_config=column_configuration,
|
|
|
|
| 338 |
)
|
| 339 |
st.session_state.cost_items_df = edited_df
|
| 340 |
|
|
|
|
| 341 |
st.markdown("### 📂 Hromadný import externích nákladů přes CSV")
|
| 342 |
+
csv_file = st.file_uploader("Nahrajte soubor .csv (Struktura sloupců: Sekce nákladu, Název položky / Popis, Částka)", type=["csv"])
|
| 343 |
if csv_file is not None:
|
| 344 |
try:
|
| 345 |
uploaded_df = pd.read_csv(csv_file)
|
|
|
|
| 348 |
sanitized_df = uploaded_df[needed_headers].copy()
|
| 349 |
sanitized_df["Částka"] = pd.to_numeric(sanitized_df["Částka"], errors='coerce').fillna(0.0)
|
| 350 |
st.session_state.cost_items_df = pd.concat([st.session_state.cost_items_df, sanitized_df], ignore_index=True)
|
| 351 |
+
st.success("CSV data byla úspěšně nahrána do tabulky výše!")
|
| 352 |
time.sleep(0.4)
|
| 353 |
st.rerun()
|
| 354 |
else:
|
| 355 |
+
st.error("Chyba: CSV neodpovídá požadované struktuře záhlaví.")
|
| 356 |
except Exception as e:
|
| 357 |
+
st.error(f"Soubor nelze přečíst: {e}")
|
| 358 |
|
| 359 |
st.markdown("---")
|
| 360 |
|
|
|
|
| 361 |
total_labor_eur = 0.0
|
| 362 |
for member in st.session_state.db_team:
|
| 363 |
+
total_labor_eur += team_hours.get(member["id"], 0) * 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
|
|
|
|
| 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 |
if selected_cust not in cust_names:
|
| 376 |
st.session_state.db_customers.append({
|
| 377 |
"id": len(st.session_state.db_customers) + 1,
|
|
|
|
| 380 |
"telefon": "Nezadáno"
|
| 381 |
})
|
| 382 |
|
|
|
|
| 383 |
saved_extra_costs = []
|
| 384 |
for _, row in edited_df.iterrows():
|
| 385 |
saved_extra_costs.append({
|
|
|
|
| 403 |
st.session_state.quotes_count += 1
|
| 404 |
st.session_state["ai_mock_active"] = False
|
| 405 |
|
|
|
|
| 406 |
st.session_state.cost_items_df = pd.DataFrame([{"Sekce nákladu": "Materiál", "Název položky / Popis": "", "Částka": 0.0}])
|
| 407 |
st.success("Rozpočet byl úspěšně zanesen do firemní databáze.")
|
| 408 |
time.sleep(0.4)
|
|
|
|
| 413 |
# -----------------------------------------------------------------------------
|
| 414 |
with tab_team:
|
| 415 |
st.header("👥 Správa firemního týmu a hodinových sazeb")
|
| 416 |
+
st.write("Editujte profily a interní hodinové sazby svých pracovníků.")
|
| 417 |
|
| 418 |
t_col1, t_col2 = st.columns([2, 1])
|
| 419 |
|
|
|
|
| 429 |
if team_display_list:
|
| 430 |
st.dataframe(pd.DataFrame(team_display_list), use_container_width=True, hide_index=True)
|
| 431 |
else:
|
| 432 |
+
st.info("V týmu se aktuálně nenachází žádný pracovník. Použijte formulář vpravo nebo načtěte Demo data.")
|
| 433 |
|
| 434 |
st.markdown("**❌ Odstranění pracovníka z týmu**")
|
| 435 |
if st.session_state.db_team:
|
|
|
|
| 491 |
st.error("Jméno klienta je povinné pole.")
|
| 492 |
|
| 493 |
# -----------------------------------------------------------------------------
|
| 494 |
+
# TAB 5: FAKTURACE
|
| 495 |
# -----------------------------------------------------------------------------
|
| 496 |
with tab_fakturace:
|
| 497 |
st.header(t["tab_inv"])
|
|
|
|
| 511 |
"stav": "Odesláno",
|
| 512 |
"datum": time.strftime("%Y-%m-%d")
|
| 513 |
})
|
| 514 |
+
st.success("Faktura byla vystavena a uložena do knihy!")
|
| 515 |
time.sleep(0.3)
|
| 516 |
st.rerun()
|
| 517 |
else:
|
| 518 |
+
st.info("V systému se nenachází žádná schválená pevná nabídka (Orientační odhady nelze fakturovat).")
|
| 519 |
|
| 520 |
st.markdown("---")
|
| 521 |
st.subheader("Kompletní přehled vydaných faktur a stavu plateb")
|
|
|
|
| 537 |
st.info("Kniha faktur je prázdná.")
|
| 538 |
|
| 539 |
# -----------------------------------------------------------------------------
|
| 540 |
+
# TAB 6: PRO BONUSY
|
| 541 |
# -----------------------------------------------------------------------------
|
| 542 |
with tab_pro_features:
|
| 543 |
st.header(t["tab_pro"])
|
|
|
|
| 616 |
st.markdown("### 📱 3. Finální vygenerovaná zpráva pro klienta")
|
| 617 |
|
| 618 |
if active_debtor:
|
|
|
|
| 619 |
raw_template = active_preset["text"]
|
| 620 |
final_compiled_sms = raw_template.replace("{jmeno}", active_debtor["zakaznik"])\
|
| 621 |
.replace("{cislo}", active_debtor["cislo"])\
|
| 622 |
.replace("{castka}", format_price(active_debtor["castka"]))
|
| 623 |
|
|
|
|
| 624 |
st.markdown(f"""
|
| 625 |
<div style="
|
| 626 |
background-color: #f8f9fa;
|
|
|
|
| 634 |
word-wrap: break-word;
|
| 635 |
line-height: 1.6;
|
| 636 |
margin-bottom: 12px;
|
| 637 |
+
;" id="sms_display_box">{final_compiled_sms}</div>
|
| 638 |
""", unsafe_allow_html=True)
|
| 639 |
|
|
|
|
| 640 |
safe_encoded_sms = urllib.parse.quote(final_compiled_sms)
|
| 641 |
|
|
|
|
| 642 |
copy_button_html = f"""
|
| 643 |
<button onclick="
|
| 644 |
navigator.clipboard.writeText(decodeURIComponent('{safe_encoded_sms}'));
|