Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pandas as pd | |
| import time | |
| import urllib.parse | |
| import stripe # Připraveno pro splnění požadavků v requirements.txt | |
| import streamlit.components.v1 as components # Bezpečný import pro HTML komponenty na Hugging Face | |
| # ----------------------------------------------------------------------------- | |
| # KOMPLETNÍ JAZYKOVÉ LOKALIZACE PRO VŠECHNY PRVKY NA STRÁNCE (CZ, SK, EN) | |
| # ----------------------------------------------------------------------------- | |
| TRANSLATIONS = { | |
| "CZ": { | |
| # Obecné a záložky | |
| "title": "🏆 TradeFlow AI", | |
| "caption": "Chytrý operační systém pro novou generaci řemeslníků.", | |
| "tab_dash": "📈 Přehled & Metriky", | |
| "tab_gen": "📝 AI Generátor Nabídek", | |
| "tab_team": "👥 Správa Týmu", | |
| "tab_crm": "👥 Adresář (CRM)", | |
| "tab_inv": "🧾 Fakturace jedním klikem", | |
| "tab_pro": "🚀 PRO Exkluzivní Funkce", | |
| # Sidebar & Admin Panel | |
| "sb_settings": "🌐 Nastavení", | |
| "sb_lang": "Jazyk aplikace", | |
| "sb_curr": "Aktivní měna", | |
| "sb_dev_panel": "🛡️ Dev Admin Panel", | |
| "sb_pwd_label": "Zadejte administrátorské heslo", | |
| "sb_pwd_hint": "Heslo pro testování je: admin123", | |
| "sb_unlock_btn": "Odemknout Admin Panel", | |
| "sb_auth_ok": "Přístup povolen!", | |
| "sb_auth_err": "Nesprávné heslo!", | |
| "sb_auth_status": "🔓 Vývojářská autorizace", | |
| "sb_pro_toggle": "Uživatel má aktivní PRO tarif", | |
| "sb_quotes_count": "Počet vytvořených nabídek", | |
| "sb_demo_section": "🧪 Správa ukázkových dat", | |
| "sb_import_demo": "📥 Importovat komplexní Demo Data", | |
| "sb_clear_demo": "🗑️ Kompletní vyčištění databází", | |
| "sb_logout": "Zamknout panel & Odhlásit se", | |
| "toast_demo_ok": "Systém byl úspěšně naplněn cvičnými daty!", | |
| "toast_clear_ok": "Aplikace byla kompletně vyčištěna.", | |
| # Dashboard Metriky | |
| "profit": "Skutečný zisk", | |
| "pending": "Čekající peníze", | |
| "potential": "Otevřené AI Nabídky", | |
| "limit": "Využití Free limitu", | |
| "dash_help_potential": "Počítá pouze Přesné fixní nabídky.", | |
| "dash_limit_warn": "⚠️ LIMIT UKONČEN", | |
| # Dashboard Analýza | |
| "dash_analysis_title": "📊 Pokročilá analýza a finanční filtrování sekcí", | |
| "dash_analysis_desc": "Sledujte finanční toky seskupené podle konkrétních nákladových sekcí napříč celou firmou.", | |
| "dash_filter_cat": "Zvolte nákladovou kategorii k odfiltrování:", | |
| "dash_filter_type": "Filtrovat typ zakázky:", | |
| "dash_filter_all_cats": "Zobrazit vše", | |
| "dash_type_all": "Všechny rozpočty", | |
| "dash_type_fixed": "Pouze závazné přesné nabídky", | |
| "dash_type_estimated": "Pouze orientační odhady", | |
| "dash_aggregated_spend": "💰 Agregovaná útrata ve zvoleném filtru:", | |
| "dash_no_records": "Pro zadaná kritéria a filtry nebyly nalezeny žádné záznamy.", | |
| "dash_no_costs_yet": "V systému zatím nejsou evidovány žádné podrobné položky nákladů.", | |
| "dash_book_title": "Kniha vytvořených kalkulací a nabídek", | |
| "dash_book_empty": "Kniha nabídek je prázdná.", | |
| # Sloupce tabulek v Dashboardu | |
| "col_project": "Projekt / Klient", | |
| "col_section": "Sekce nákladu", | |
| "col_item_name": "Název položky / Popis", | |
| "col_amount": "Částka", | |
| "col_type": "Typ rozpočtu", | |
| "col_date": "Datum", | |
| # AI Generátor Nabídek | |
| "gen_type_label": "Vyberte povahu výstupu a nacenění:", | |
| "gen_type_est": "🔮 Orientační odhad od AI (Čistě orientační kalkulace, nepropisuje se do dlužníků)", | |
| "gen_type_fix": "🎯 Přesná fixní nabídka (Závazný finanční planar, slouží jako podklad pro fakturu)", | |
| "gen_client_section": "### 👤 Přiřazení klienta", | |
| "gen_client_mode": "Způsob zadání zákazníka:", | |
| "gen_client_crm": "Vybrat z adresáře CRM", | |
| "gen_client_manual": "✍️ Zadat úplně nového klienta ručně", | |
| "gen_select_client": "Zvolte klienta:", | |
| "gen_input_client_name": "Zadejte jméno a příjmení nového klienta:", | |
| "gen_client_placeholder": "Např. František Novotný", | |
| "gen_spec_label": "Detailní specifikace projektu a zadání pro AI", | |
| "gen_spec_default": "Kompletní rekonstrukce rodinného domu, rozvody vody a finální zapojení sanitární techniky.", | |
| "gen_team_alloc": "### 🕒 Personální alokace (Odpracované hodiny týmu)", | |
| "gen_no_team_warn": "ℹ️ 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ě.", | |
| "gen_ai_btn": "🪄 Spustit AI simulaci a předvyplnit položky i hodiny", | |
| "gen_ai_toast": "AI úspěšně zanalyzovala text a navrhla optimální strukturu nákladů!", | |
| "gen_direct_costs": "### 🚚 Přímé materiálové a vedlejší náklady", | |
| "gen_table_hint": "💡 Nové řádky přidáte symbolem `+` vespod tabulky. Řádek smažete označením a stiskem klávesy `Delete` na klávesnici.", | |
| "gen_csv_title": "### 📂 Hromadný import externích nákladů přes CSV", | |
| "gen_csv_uploader": "Nahrajte soubor .csv (Struktura sloupců: Sekce nákladu, Název položky / Popis, Částka)", | |
| "gen_csv_success": "CSV data byla úspěšně nahrána do tabulky výše!", | |
| "gen_csv_err_headers": "Chyba: CSV neodpovídá požadované struktuře záhlaví.", | |
| "gen_csv_err_read": "Soubor nelze přečíst:", | |
| "gen_total_calc": "### 📈 Celková kalkulace rozpočtu:", | |
| "gen_save_btn": "💾 Schválit a uložit rozpočet do systému", | |
| "gen_err_no_client": "Chyba: Před uložením musíte uvést nebo vybrat jméno klienta!", | |
| "gen_save_success": "Rozpočet byl úspěšně zanesen do firemní databáze.", | |
| # Správa týmu | |
| "team_title": "👥 Správa firemního týmu a mzdových sazeb", | |
| "team_desc": "Editujte profily a interní hodinové sazby svých pracovníků.", | |
| "team_sub_list": "Evidovaní pracovníci a specialisté", | |
| "team_col_id": "Interní ID", | |
| "team_col_role": "Jméno člena / Profesní role", | |
| "team_col_rate": "Hodinová mzdová sazba", | |
| "team_empty": "V týmu se aktuálně nenachází žádný pracovník. Použijte formulář vpravo nebo načtěte Demo data.", | |
| "team_remove_section": "**❌ Odstranění pracovníka z týmu**", | |
| "team_remove_select": "Vyberte osobu k odebrání:", | |
| "team_remove_btn": "Definitivně odebrat člena", | |
| "team_remove_toast": "Pracovník byl vyřazen z interní databáze.", | |
| "team_add_section": "### ➕ Přidat / Upravit kartu pracovníka", | |
| "team_input_name": "Jméno příjmení / Pracovní zařazení", | |
| "team_input_placeholder": "Např. Kamil (Zedník)", | |
| "team_input_rate": "Hodinová sazba v aktivní měně", | |
| "team_submit_btn": "Uložit profil", | |
| "team_update_toast": "Sazba u pracovníka byla zaktualizována.", | |
| "team_add_toast": "Nový člen byl úspěšně zaevidován.", | |
| # CRM Adresář | |
| "crm_title": "👥 Adresář zákazníků (CRM)", | |
| "crm_empty": "Adresář klientů je momentálně prázdný.", | |
| "crm_add_section": "### ➕ Rychlé ruční založení klienta", | |
| "cust_name": "Jméno a příjmení", | |
| "company": "Firma (Volitelné)", | |
| "phone": "Telefonní číslo", | |
| "save_cust": "Uložit klienta", | |
| "crm_err_name": "Jméno klienta je povinné pole.", | |
| "crm_success_toast": "Klient byl bezpečně uložen do CRM.", | |
| # Fakturace | |
| "inv_title": "🧾 Fakturace jedním klikem", | |
| "inv_select_quote": "Zvolte schválený pevný rozpočet k okamžité fakturaci:", | |
| "invoice_btn": "🧾 VYSTAVIT FAKTURU JEDNÍM KLIKNUTÍM", | |
| "inv_success_toast": "Faktura byla vystavena a uložena do knihy!", | |
| "inv_no_fixed_quotes": "V systému se nenachází žádná schválená pevná nabídka (Orientační odhady nelze fakturovat).", | |
| "inv_book_title": "Kompletní přehled vydaných faktur a stavu plateb", | |
| "inv_status_paid": "🟢 Uhrazeno", | |
| "inv_status_sent": "🔵 Odesláno (Po splatnosti)", | |
| "inv_mark_paid": "Označit jako ZAPLACENO", | |
| "inv_empty": "Kniha faktur je prázdná.", | |
| # PRO Funkce | |
| "paywall_msg": "🔒 Dosáhli jste limitu 3 bezplatných nabídek. Pro neomezený přístup aktivujte tarif PRO.", | |
| "pro_router": "📍 AI Plánovač tras a logistiky", | |
| "pro_sms": "📲 Inteligentní SMS Centrum upomínek", | |
| "pro_locked_warn": "🔒 Tento panel obsahuje pokročilé moduly TradeFlow PRO. Aktivujte zkušební verzi v Dev Admin Panelu na boční liště.", | |
| "pro_route_title": "📍 AI Geografický optimalizátor tras výjezdů", | |
| "pro_route_label": "Seznam adres plánovaných zásahů", | |
| "pro_route_btn": "🗺️ Spočítat matematicky nejkratší trasu", | |
| "pro_route_result": "🚀 AI Výsledek kalkulace: Výjezd z Plzně -> Beroun -> Praha. Celková úspora paliva a logistických nákladů činí: 42 km.", | |
| "pro_sms_title": "📱 Inteligentní manažerské SMS centrum pohledávek", | |
| "pro_sms_step1": "### 🔍 1. Vyhledání dlužníka v systému", | |
| "pro_sms_filter_name": "Filtrovat neplatiče dle jména:", | |
| "pro_sms_sort_label": "Seřadit dlužníky dle kritéria:", | |
| "pro_sms_sort_high": "Od nejvyšší dlužné částky", | |
| "pro_sms_sort_low": "Od nejnižší dlužné částky", | |
| "pro_sms_no_debts": "V systému aktuálně neevidujeme žádné neuhrazené faktury odpovídající filtrům.", | |
| "pro_sms_select_debtor": "🎯 Vyberte cílového dlužníka k obeslání:", | |
| "pro_sms_step2": "### 📋 2. Výběr textové šablony", | |
| "pro_sms_template_label": "Zvolte aktivní komunikační šablonu:", | |
| "pro_sms_expander": "⚙️ Pokročilý editor šablon & Generátor nových textů přes AI", | |
| "pro_sms_ai_assistant": "🤖 **AI Asistent pro tvorbu tónu textu**", | |
| "pro_sms_ai_prompt": "Zadejte zadání pro AI (např. 'Napiš velmi naléhavou předžalobní výzvu')", | |
| "pro_sms_ai_btn": "🪄 Sestavit novou šablonu pomocí AI", | |
| "pro_sms_ai_success": "AI text úspěšně vygenerován do pole níže!", | |
| "pro_sms_new_title": "Název nové šablony:", | |
| "pro_sms_new_body": "Text těla zprávy (používejte značky {jmeno}, {cislo}, {castka})", | |
| "pro_sms_save_template_btn": "Uložit šablonu do trvalého seznamu", | |
| "pro_sms_template_success": "Šablona úspěšně uložena!", | |
| "pro_sms_step3": "### 📱 3. Finální vygenerovaná zpráva pro klienta", | |
| "pro_sms_copy_btn": "📋 Kopírovat text zprávy", | |
| "pro_sms_copy_ok": "✅ Zkopírováno do schránky!", | |
| "pro_sms_select_info": "Vyberte dlužníka pro zobrazení a zkopírování SMS zprávy." | |
| }, | |
| "SK": { | |
| # Obecné a záložky | |
| "title": "🏆 TradeFlow AI", | |
| "caption": "Chytrý operačný systém pre novú generáciu remeselníkov.", | |
| "tab_dash": "📈 Prehľad & Metriky", | |
| "tab_gen": "📝 AI Generátor Ponúk", | |
| "tab_team": "👥 Správa Tímu", | |
| "tab_crm": "👥 Adresár (CRM)", | |
| "tab_inv": "🧾 Fakturácia jedným klikom", | |
| "tab_pro": "🚀 PRO Exkluzívne Funkcie", | |
| # Sidebar & Admin Panel | |
| "sb_settings": "🌐 Nastavenia", | |
| "sb_lang": "Jazyk aplikácie", | |
| "sb_curr": "Aktívna mena", | |
| "sb_dev_panel": "🛡️ Dev Admin Panel", | |
| "sb_pwd_label": "Zadajte administrátorské heslo", | |
| "sb_pwd_hint": "Heslo pre testovanie je: admin123", | |
| "sb_unlock_btn": "Odomknúť Admin Panel", | |
| "sb_auth_ok": "Prístup povolený!", | |
| "sb_auth_err": "Nesprávne heslo!", | |
| "sb_auth_status": "🔓 Vývojárska autorizácia", | |
| "sb_pro_toggle": "Užívateľ má aktívny PRO tarif", | |
| "sb_quotes_count": "Počet vytvorených ponúk", | |
| "sb_demo_section": "🧪 Správa ukážkových dát", | |
| "sb_import_demo": "📥 Importovať komplexné Demo Dáta", | |
| "sb_clear_demo": "🗑️ Kompletné vyčistenie databáz", | |
| "sb_logout": "Zamknúť panel & Odhlásiť sa", | |
| "toast_demo_ok": "Systém bol úspešne naplnený cvičnými dátami!", | |
| "toast_clear_ok": "Aplikácia bola kompletne vyčistená.", | |
| # Dashboard Metriky | |
| "profit": "Skutočný zisk", | |
| "pending": "Čakajúce peniaze", | |
| "potential": "Otvorené AI Ponuky", | |
| "limit": "Využitie Free limitu", | |
| "dash_help_potential": "Počíta iba Presné fixné ponuky.", | |
| "dash_limit_warn": "⚠️ LIMIT UKONČENÝ", | |
| # Dashboard Analýza | |
| "dash_analysis_title": "📊 Pokročilá analýza a finančné filtrovanie sekcií", | |
| "dash_analysis_desc": "Sledujte finančné toky zoskupené podľa konkrétnych nákladových sekcií naprieč celou firmou.", | |
| "dash_filter_cat": "Zvoľte nákladovú kategóriu na odfiltrovanie:", | |
| "dash_filter_type": "Filtrovať typ zákazky:", | |
| "dash_filter_all_cats": "Zobraziť všetko", | |
| "dash_type_all": "Všetky rozpočty", | |
| "dash_type_fixed": "Iba záväzné presné ponuky", | |
| "dash_type_estimated": "Iba orientačné odhady", | |
| "dash_aggregated_spend": "💰 Agregovaná útrata vo zvolenom filtri:", | |
| "dash_no_records": "Pre zadané kritériá a filtre neboli nájdené žiadne záznamy.", | |
| "dash_no_costs_yet": "V systéme zatiaľ nie sú evidované žiadne podrobné položky nákladov.", | |
| "dash_book_title": "Kniha vytvorených kalkulácií a ponúk", | |
| "dash_book_empty": "Kniha ponúk je prázdna.", | |
| # Sloupce tabulek v Dashboardu | |
| "col_project": "Projekt / Klient", | |
| "col_section": "Sekcia nákladu", | |
| "col_item_name": "Názov položky / Popis", | |
| "col_amount": "Čiastka", | |
| "col_type": "Typ rozpočtu", | |
| "col_date": "Dátum", | |
| # AI Generátor Nabídek | |
| "gen_type_label": "Vyberte povahu výstupu a nacenenie:", | |
| "gen_type_est": "🔮 Orientačný odhad od AI (Čisto orientačná kalkulácia, neprepisuje sa do dlžníkov)", | |
| "gen_type_fix": "🎯 Presná fixná ponuka (Záväzný finančný plán, slúži ako podklad pre faktúru)", | |
| "gen_client_section": "### 👤 Priradenie klienta", | |
| "gen_client_mode": "Spôsob zadania zákazníka:", | |
| "gen_client_crm": "Vybrať z adresára CRM", | |
| "gen_client_manual": "✍️ Zadať úplne nového klienta ručne", | |
| "gen_select_client": "Zvoľte klienta:", | |
| "gen_input_client_name": "Zadajte meno a priezvisko nového klienta:", | |
| "gen_client_placeholder": "Napr. František Novotný", | |
| "gen_spec_label": "Detailná špecifikácia projektu a zadanie pre AI", | |
| "gen_spec_default": "Kompletná rekonštrukcia rodinného domu, rozvody vody a finálne zapojenie sanitárnej techniky.", | |
| "gen_team_alloc": "### 🕒 Personálna alokácia (Odpracované hodiny tímu)", | |
| "gen_no_team_warn": "ℹ️ V internej databáze zatiaľ nie sú žiadni pracovníci. Pridajte ich v záložke 'Správa Tímu' alebo zapnite Demo Data v bočnej lište.", | |
| "gen_ai_btn": "🪄 Spustiť AI simuláciu a predvyplniť položky aj hodiny", | |
| "gen_ai_toast": "AI úspešne zanalyzovala text a navrhla optimálnu štruktúru nákladov!", | |
| "gen_direct_costs": "### 🚚 Priame materiálové a vedľajšie náklady", | |
| "gen_table_hint": "💡 Nové riadky pridáte symbolom `+` naspodku tabuľky. Riadok smažete označením a stlačením klávesy `Delete` na klávesnici.", | |
| "gen_csv_title": "### 📂 Hromadný import externých nákladov cez CSV", | |
| "gen_csv_uploader": "Nahrajte súbor .csv (Štruktúra stĺpcov: Sekcia nákladu, Název položky / Popis, Částka)", | |
| "gen_csv_success": "CSV dáta boli úspešne nahrané do tabuľky vyššie!", | |
| "gen_csv_err_headers": "Chyba: CSV neodpovedá požadovanej štruktúre záhlavia.", | |
| "gen_csv_err_read": "Súbor nie je možné prečítať:", | |
| "gen_total_calc": "### 📈 Celková kalkulácia rozpočtu:", | |
| "gen_save_btn": "💾 Schváliť a uložiť rozpočet do systému", | |
| "gen_err_no_client": "Chyba: Pred uložením musíte uviesť alebo vybrať meno klienta!", | |
| "gen_save_success": "Rozpočet bol úspešne zanesený do firemnej databázy.", | |
| # Správa týmu | |
| "team_title": "👥 Správa firemného tímu a mzdových sadzieb", | |
| "team_desc": "Editujte profily a interné hodinové sadzby svojich pracovníkov.", | |
| "team_sub_list": "Evidovaní pracovníci a špecialisti", | |
| "team_col_id": "Interné ID", | |
| "team_col_role": "Meno člena / Profesijná rola", | |
| "team_col_rate": "Hodinová mzdová sadzba", | |
| "team_empty": "V tíme sa aktuálne nenachádza žiadny pracovník. Použite formulár vpravo alebo načítajte Demo dáta.", | |
| "team_remove_section": "**❌ Odstránenie pracovníka z tímu**", | |
| "team_remove_select": "Vyberte osobu na odobratie:", | |
| "team_remove_btn": "Definitívne odobrať člena", | |
| "team_remove_toast": "Pracovník bol vyradený z internej databázy.", | |
| "team_add_section": "### ➕ Pridať / Upraviť kartu pracovníka", | |
| "team_input_name": "Meno priezvisko / Pracovné zaradenie", | |
| "team_input_placeholder": "Napr. Kamil (Murár)", | |
| "team_input_rate": "Hodinová sadzba v aktívnej mene", | |
| "team_submit_btn": "Uložiť profil", | |
| "team_update_toast": "Sadzba u pracovníka bola zaktualizovaná.", | |
| "team_add_toast": "Nový člen bol úspešne zaevidovaný.", | |
| # CRM Adresář | |
| "crm_title": "👥 Adresár zákazníkov (CRM)", | |
| "crm_empty": "Adresár klientov je momentálne prázdny.", | |
| "crm_add_section": "### ➕ Rýchle ručné založenie klienta", | |
| "cust_name": "Meno a priezvisko", | |
| "company": "Firma (Voliteľné)", | |
| "phone": "Telefónne číslo", | |
| "save_cust": "Uložiť klienta", | |
| "crm_err_name": "Meno klienta je povinné pole.", | |
| "crm_success_toast": "Klient bol bezpečne uložený do CRM.", | |
| # Fakturace | |
| "inv_title": "🧾 Fakturácia jedným klikom", | |
| "inv_select_quote": "Zvoľte schválený pevný rozpočet k okamžitej fakturácii:", | |
| "invoice_btn": "🧾 VYSTAVIŤ FAKTÚRU JEDNÝM KLIKNUTÍM", | |
| "inv_success_toast": "Faktúra bola vystavená a uložená do knihy!", | |
| "inv_no_fixed_quotes": "V systéme sa nenachádza žiadna schválená pevná ponuka (Orientačné odhady nie je možné fakturovať).", | |
| "inv_book_title": "Kompletný prehľad vydaných faktúr a stavu platieb", | |
| "inv_status_paid": "🟢 Uhradené", | |
| "inv_status_sent": "🔵 Odoslané (Po splatnosti)", | |
| "inv_mark_paid": "Označiť ako ZAPLATENÉ", | |
| "inv_empty": "Kniha faktúr je prázdna.", | |
| # PRO Funkce | |
| "paywall_msg": "🔒 Dosiahli ste limitu 3 bezplatných ponúk. Pre neobmedzený prístup aktivujte tarif PRO.", | |
| "pro_router": "📍 AI Plánovač trás a logistiky", | |
| "pro_sms": "📲 Inteligentné SMS Centrum upomienok", | |
| "pro_locked_warn": "🔒 Tento panel obsahuje pokročilé moduly TradeFlow PRO. Aktivujte skúšobnú verziu v Dev Admin Panelu na bočnej lište.", | |
| "pro_route_title": "📍 AI Geografický optimalizátor trás výjazdov", | |
| "pro_route_label": "Zoznam adries plánovaných zásahov", | |
| "pro_route_btn": "🗺️ Spočítať matematicky najkratšiu trasu", | |
| "pro_route_result": "🚀 AI Výsledok kalkulácie: Výjazd z Plzne -> Beroun -> Praha. Celková úspora paliva a logistických nákladov činí: 42 km.", | |
| "pro_sms_title": "📱 Inteligentné manažérske SMS centrum pohľadávok", | |
| "pro_sms_step1": "### 🔍 1. Vyhľadanie dlžníka v systéme", | |
| "pro_sms_filter_name": "Filtrovať neplatičov podľa mena:", | |
| "pro_sms_sort_label": "Zoradiť dlžníkov podľa kritéria:", | |
| "pro_sms_sort_high": "Od najvyššej dlžnej sumy", | |
| "pro_sms_sort_low": "Od najnižšej dlžnej sumy", | |
| "pro_sms_no_debts": "V systéme aktuálne neevidujeme žiadne neuhradené faktúry zodpovedajúce filtrom.", | |
| "pro_sms_select_debtor": "🎯 Vyberte cieľového dlžníka na obeslanie:", | |
| "pro_sms_step2": "### 📋 2. Výber textovej šablóny", | |
| "pro_sms_template_label": "Zvoľte aktívnu komunikačnú šablónu:", | |
| "pro_sms_expander": "⚙️ Pokročilý editor šablón & Generátor nových textov cez AI", | |
| "pro_sms_ai_assistant": "🤖 **AI Asistent pre tvorbu tónu textu**", | |
| "pro_sms_ai_prompt": "Zadajte zadanie pre AI (napr. 'Napíš veľmi naliehavú predžalobnú výzvu')", | |
| "pro_sms_ai_btn": "🪄 Zostaviť novú šablónu pomocou AI", | |
| "pro_sms_ai_success": "AI text úspešne vygenerovaný do poľa nižšie!", | |
| "pro_sms_new_title": "Názov novej šablóny:", | |
| "pro_sms_new_body": "Text tela správy (používajte značky {jmeno}, {cislo}, {castka})", | |
| "pro_sms_save_template_btn": "Uložiť šablónu do trvalého zoznamu", | |
| "pro_sms_template_success": "Šablóna úspešne uložená!", | |
| "pro_sms_step3": "### 📱 3. Finálna vygenerovaná správa pre klienta", | |
| "pro_sms_copy_btn": "📋 Kopírovať text správy", | |
| "pro_sms_copy_ok": "✅ Skopírované do schránky!", | |
| "pro_sms_select_info": "Vyberte dlžníka pre zobrazenie a skopírovanie SMS správy." | |
| }, | |
| "EN": { | |
| # Obecné a záložky | |
| "title": "🏆 TradeFlow AI", | |
| "caption": "Smart operating system for the next-gen skilled trades.", | |
| "tab_dash": "📈 Dashboard & Metrics", | |
| "tab_gen": "📝 AI Quote Generator", | |
| "tab_team": "👥 Team Management", | |
| "tab_crm": "👥 Contacts (CRM)", | |
| "tab_inv": "🧾 1-Click Invoicing", | |
| "tab_pro": "🚀 PRO Features", | |
| # Sidebar & Admin Panel | |
| "sb_settings": "🌐 Settings", | |
| "sb_lang": "App Language", | |
| "sb_curr": "Active Currency", | |
| "sb_dev_panel": "🛡️ Dev Admin Panel", | |
| "sb_pwd_label": "Enter Admin Password", | |
| "sb_pwd_hint": "Testing password is: admin123", | |
| "sb_unlock_btn": "Unlock Admin Panel", | |
| "sb_auth_ok": "Access Granted!", | |
| "sb_auth_err": "Incorrect Password!", | |
| "sb_auth_status": "🔓 Developer Authorization", | |
| "sb_pro_toggle": "User has active PRO tier", | |
| "sb_quotes_count": "Created quotes counter", | |
| "sb_demo_section": "🧪 Sample Data Management", | |
| "sb_import_demo": "📥 Import Complex Demo Data", | |
| "sb_clear_demo": "🗑️ Complete Databases Wipe", | |
| "sb_logout": "Lock Panel & Log Out", | |
| "toast_demo_ok": "System filled with sample data successfully!", | |
| "toast_clear_ok": "Application databases wiped completely.", | |
| # Dashboard Metriky | |
| "profit": "Actual Profit", | |
| "pending": "Pending Revenue", | |
| "potential": "Open AI Quotes", | |
| "limit": "Free Tier Usage", | |
| "dash_help_potential": "Counts strict Fixed Price quotes only.", | |
| "dash_limit_warn": "⚠️ LIMIT EXCEEDED", | |
| # Dashboard Analýza | |
| "dash_analysis_title": "📊 Advanced Section Analysis & Financial Filtering", | |
| "dash_analysis_desc": "Monitor financial flows grouped by specific cost sections across the entire company.", | |
| "dash_filter_cat": "Select cost category to filter by:", | |
| "dash_filter_type": "Filter project type:", | |
| "dash_filter_all_cats": "Show all", | |
| "dash_type_all": "All budgets", | |
| "dash_type_fixed": "Binding fixed quotes only", | |
| "dash_type_estimated": "Rough estimates only", | |
| "dash_aggregated_spend": "💰 Aggregated spend in selected filter:", | |
| "dash_no_records": "No records found matching the given criteria and filters.", | |
| "dash_no_costs_yet": "No itemized cost entries are registered in the system yet.", | |
| "dash_book_title": "Log of Created Calculations & Quotes", | |
| "dash_book_empty": "The quotes logbook is currently empty.", | |
| # Sloupce tabulek v Dashboardu | |
| "col_project": "Project / Client", | |
| "col_section": "Cost Section", | |
| "col_item_name": "Item Name / Description", | |
| "col_amount": "Amount", | |
| "col_type": "Budget Type", | |
| "col_date": "Date", | |
| # AI Generátor Nabídek | |
| "gen_type_label": "Select output scope and pricing strategy:", | |
| "gen_type_est": "🔮 Rough AI Estimate (Purely indicative calculation, does not map into debtors ledger)", | |
| "gen_type_fix": "🎯 Binding Fixed Quote (Strict financial plan, serves as an invoice baseline)", | |
| "gen_client_section": "### 👤 Client Assignment", | |
| "gen_client_mode": "Customer input method:", | |
| "gen_client_crm": "Select from CRM address book", | |
| "gen_client_manual": "✍️ Enter brand new client manually", | |
| "gen_select_client": "Choose client:", | |
| "gen_input_client_name": "Enter full name of the new client:", | |
| "gen_client_placeholder": "E.g., John Smith", | |
| "gen_spec_label": "Detailed project specifications and instructions for AI", | |
| "gen_spec_default": "Complete residential house reconstruction, plumbing pipelines layout and final sanitary alignment.", | |
| "gen_team_alloc": "### 🕒 Workforce Allocation (Team hours worked)", | |
| "gen_no_team_warn": "ℹ️ There are no workers registered in the internal database yet. Add them in 'Team Management' or seed Demo Data via sidebar.", | |
| "gen_ai_btn": "🪄 Execute AI Simulation and pre-fill rows & hours", | |
| "gen_ai_toast": "AI successfully analyzed text and proposed optimal cost structure!", | |
| "gen_direct_costs": "### 🚚 Direct Material & Secondary Expenses", | |
| "gen_table_hint": "💡 Add new rows using the `+` icon at the bottom. Delete rows by selecting them and pressing `Delete` on your keyboard.", | |
| "gen_csv_title": "### 📂 Bulk External Costs CSV Import", | |
| "gen_csv_uploader": "Upload a .csv file (Required column layout: Sekce nákladu, Název položky / Popis, Částka)", | |
| "gen_csv_success": "CSV data successfully mapped into the table above!", | |
| "gen_csv_err_headers": "Error: CSV does not match the required headers layout.", | |
| "gen_csv_err_read": "File cannot be parsed:", | |
| "gen_total_calc": "### 📈 Total Budget Calculation:", | |
| "gen_save_btn": "💾 Approve and record budget into the system", | |
| "gen_err_no_client": "Error: You must state or select a client name before saving!", | |
| "gen_save_success": "Budget was successfully recorded into company database.", | |
| # Správa týmu | |
| "team_title": "👥 Internal Team & Hourly Sazby Management", | |
| "team_desc": "Edit profiles and corporate hourly rates of your specialists.", | |
| "team_sub_list": "Registered Workers & Specialists", | |
| "team_col_id": "Internal ID", | |
| "team_col_role": "Member Name / Professional Role", | |
| "team_col_rate": "Hourly Wage Rate", | |
| "team_empty": "No workers inside the team database. Use the form on the right or turn on Demo Data.", | |
| "team_remove_section": "**❌ Exclude worker from team**", | |
| "team_remove_select": "Select person to remove:", | |
| "team_remove_btn": "Permanently remove member", | |
| "team_remove_toast": "Worker was removed from internal database.", | |
| "team_add_section": "### ➕ Add / Modify Worker Card", | |
| "team_input_name": "Full Name / Job Position", | |
| "team_input_placeholder": "E.g., Carl (Mason)", | |
| "team_input_rate": "Hourly rate in active currency", | |
| "team_submit_btn": "Save Profile", | |
| "team_update_toast": "Worker wage rate updated.", | |
| "team_add_toast": "New member successfully registered.", | |
| # CRM Adresář | |
| "crm_title": "👥 Client Address Book (CRM)", | |
| "crm_empty": "Client book is empty at the moment.", | |
| "crm_add_section": "### ➕ Quick Manual Client Entry", | |
| "cust_name": "Full Name", | |
| "company": "Company (Optional)", | |
| "phone": "Phone Number", | |
| "save_cust": "Save Client", | |
| "crm_err_name": "Client name is a mandatory field.", | |
| "crm_success_toast": "Client records safely saved to CRM.", | |
| # Fakturace | |
| "inv_title": "🧾 1-Click Invoicing System", | |
| "inv_select_quote": "Choose an approved fixed budget for immediate invoicing:", | |
| "invoice_btn": "🧾 ISSUE 1-CLICK INVOICE NOW", | |
| "inv_success_toast": "Invoice issued and stored into ledger successfully!", | |
| "inv_no_fixed_quotes": "No approved fixed quotes available (Rough estimations cannot be converted into an invoice).", | |
| "inv_book_title": "Complete Issued Invoices Overview & Payments Status", | |
| "inv_status_paid": "🟢 Settled & Paid", | |
| "inv_status_sent": "🔵 Sent (Overdue)", | |
| "inv_mark_paid": "Mark as PAID / SETTLED", | |
| "inv_empty": "The invoice ledger is empty.", | |
| # PRO Funkce | |
| "paywall_msg": "🔒 You have reached the 3 free quotes limit. Unlock PRO tier for unlimited access.", | |
| "pro_router": "📍 AI Route & Logistics Planner", | |
| "pro_sms": "📲 Intelligent SMS Reminder Center", | |
| "pro_locked_warn": "🔒 This panel contains advanced TradeFlow PRO modules. Activate evaluation license in Dev Admin Panel via sidebar.", | |
| "pro_route_title": "📍 AI Geographical Route Optimization Engine", | |
| "pro_route_label": "Target list of planned on-site addresses", | |
| "pro_route_btn": "🗺️ Compute Mathematical Shortest Path", | |
| "pro_route_result": "🚀 AI Optimization outcome: Departure Plzeň -> Beroun -> Praha. Total fuel and logistics distance saved: 42 km.", | |
| "pro_sms_title": "📱 Smart Corporate Debt Collection SMS Center", | |
| "pro_sms_step1": "### 🔍 1. Locating Delinquent Accounts in System", | |
| "pro_sms_filter_name": "Filter debtors by name lookup:", | |
| "pro_sms_sort_label": "Sort debtors by criteria order:", | |
| "pro_sms_sort_high": "Highest outstanding debt first", | |
| "pro_sms_sort_low": "Lowest outstanding debt first", | |
| "pro_sms_no_debts": "No outstanding overdue invoices matching current filters discovered.", | |
| "pro_sms_select_debtor": "🎯 Choose target account to contact:", | |
| "pro_sms_step2": "### 📋 2. Template Preset Assignment", | |
| "pro_sms_template_label": "Choose active communications preset:", | |
| "pro_sms_expander": "⚙️ Advanced Presets Editor & AI Content Generator Engine", | |
| "pro_sms_ai_assistant": "🤖 **AI Tone Modulation Assistant**", | |
| "pro_sms_ai_prompt": "State assignment for AI (e.g., 'Write highly urgent pre-legal warning letter')", | |
| "pro_sms_ai_btn": "🪄 Compose New Template via AI", | |
| "pro_sms_ai_success": "AI copy successfully synthesized into the area below!", | |
| "pro_sms_new_title": "New template label name:", | |
| "pro_sms_new_body": "Message body code (use placeholder tags {jmeno}, {cislo}, {castka})", | |
| "pro_sms_save_template_btn": "Save Preset into permanent storage", | |
| "pro_sms_template_success": "Template preset recorded!", | |
| "pro_sms_step3": "### 📱 3. Final Compiled Client Text Copy", | |
| "pro_sms_copy_btn": "📋 Copy Message Text", | |
| "pro_sms_copy_ok": "✅ Copy to clipboard successful!", | |
| "pro_sms_select_info": "Select an overdue account to preview and extract SMS dispatch copy." | |
| } | |
| } | |
| CURRENCIES = { | |
| "EUR": {"symbol": "EUR", "rate": 1.0}, | |
| "CZK": {"symbol": "Kč", "rate": 25.12}, | |
| "USD": {"symbol": "$", "rate": 1.09}, | |
| "GBP": {"symbol": "£", "rate": 0.84}, | |
| "CHF": {"symbol": "CHF", "rate": 0.96} | |
| } | |
| # ----------------------------------------------------------------------------- | |
| # INICIALIZACE STAVU | |
| # ----------------------------------------------------------------------------- | |
| VERSION = "2.1.2-ENTERPRISE" | |
| st.set_page_config(page_title="TradeFlow AI", page_icon="🏆", layout="wide", initial_sidebar_state="expanded") | |
| if "lang" not in st.session_state: st.session_state.lang = "CZ" | |
| if "curr" not in st.session_state: st.session_state.curr = "EUR" | |
| if "admin_authenticated" not in st.session_state: st.session_state.admin_authenticated = False | |
| if "user_subscribed" not in st.session_state: st.session_state.user_subscribed = False | |
| if "quotes_count" not in st.session_state: st.session_state.quotes_count = 0 | |
| if "db_team" not in st.session_state: st.session_state.db_team = [] | |
| if "db_customers" not in st.session_state: st.session_state.db_customers = [] | |
| if "db_quotes" not in st.session_state: st.session_state.db_quotes = [] | |
| if "db_invoices" not in st.session_state: st.session_state.db_invoices = [] | |
| if "sms_presets" not in st.session_state: | |
| st.session_state.sms_presets = [ | |
| {"název": "Standardní upomínka", "text": "Dobrý den, {jmeno}, evidujeme, že faktura číslo {cislo} na částku {castka} je po splatnosti. Prosíme o neprodlené uhrazení. Tým TradeFlow AI."}, | |
| {"název": "Důrazná výzva", "text": "Upozornění pro: {jmeno}. Faktura {cislo} ({castka}) je po splatnosti. Žádáme o okamžitou úhradu na náš bankovní účet, abychom předešli dalším krokům."}, | |
| {"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!"} | |
| ] | |
| t = TRANSLATIONS[st.session_state.lang if st.session_state.lang in TRANSLATIONS else "CZ"] | |
| if "cost_items_df" not in st.session_state: | |
| st.session_state.cost_items_df = pd.DataFrame([ | |
| {"Sekce nákladu": "Materiál", "Název položky / Popis": "", "Částka": 0.0} | |
| ]) | |
| def format_price(amount_in_eur): | |
| c_info = CURRENCIES[st.session_state.curr] | |
| converted = amount_in_eur * c_info["rate"] | |
| if c_info["symbol"] in ["Kč", "CHF"]: | |
| return f"{converted:,.2f} {c_info['symbol']}" | |
| return f"{c_info['symbol']}{converted:,.2f}" | |
| # ----------------------------------------------------------------------------- | |
| # SIDEBAR: PANEL S NASTAVENÍM & VÝVOJÁŘSKÝM PUSH SEEDINGEM | |
| # ----------------------------------------------------------------------------- | |
| with st.sidebar: | |
| st.header(t["sb_settings"]) | |
| st.session_state.lang = st.selectbox(t["sb_lang"], list(TRANSLATIONS.keys()), index=list(TRANSLATIONS.keys()).index(st.session_state.lang)) | |
| # Po změně jazyka musíme hned aktualizovat překladový slovník, aby zbytek kódu běžel v novém kontextu | |
| t = TRANSLATIONS[st.session_state.lang] | |
| st.session_state.curr = st.selectbox(t["sb_curr"], list(CURRENCIES.keys()), index=list(CURRENCIES.keys()).index(st.session_state.curr)) | |
| st.markdown("---") | |
| st.header(t["sb_dev_panel"]) | |
| if not st.session_state.admin_authenticated: | |
| pwd_input = st.text_input(t["sb_pwd_label"], type="password", help=t["sb_pwd_hint"]) | |
| if st.button(t["sb_unlock_btn"]): | |
| if pwd_input == "admin123": | |
| st.session_state.admin_authenticated = True | |
| st.success(t["sb_auth_ok"]) | |
| st.rerun() | |
| else: | |
| st.error(t["sb_auth_err"]) | |
| else: | |
| st.info(t["sb_auth_status"]) | |
| st.session_state.user_subscribed = st.toggle(t["sb_pro_toggle"], value=st.session_state.user_subscribed) | |
| st.session_state.quotes_count = st.number_input(t["sb_quotes_count"], min_value=0, max_value=20, value=st.session_state.quotes_count) | |
| st.markdown(f"### {t['sb_demo_section']}") | |
| if st.button(t["sb_import_demo"], use_container_width=True): | |
| st.session_state.db_team = [ | |
| {"id": 1, "jmeno": "Šimon (Director)", "sazba": 45.0}, | |
| {"id": 2, "jmeno": "Nikola (Prezentace & Podpora)", "sazba": 30.0}, | |
| {"id": 3, "jmeno": "Matěj (Technik)", "sazba": 25.0} | |
| ] | |
| st.session_state.db_customers = [ | |
| {"id": 1, "jmeno": "Jan Novák", "firma": "Stavebniny s.r.o.", "telefon": "+420 777 123 456"}, | |
| {"id": 2, "jmeno": "Marie Podlahová", "firma": "Bytové družstvo", "telefon": "+420 602 987 654"}, | |
| {"id": 3, "jmeno": "Zdeněk Urban", "firma": "Instalatérství Urban", "telefon": "+420 731 555 999"} | |
| ] | |
| st.session_state.db_quotes = [ | |
| { | |
| "id": 1, | |
| "zakaznik": "Jan Novák", | |
| "popis": "Kompletní instalace rozvodů vody a topení v novostavbě", | |
| "celkem": 1250.0, | |
| "typ": "fixni", | |
| "datum": "2026-06-05", | |
| "labor_eur": 750.0, | |
| "extra_costs": [ | |
| {"kategorie": "Materiál", "nazev": "Měděné potrubí a fitinky", "eur": 400.0}, | |
| {"kategorie": "Benzín / Doprava", "nazev": "Doprava techniky a závoz materiálu", "eur": 100.0} | |
| ] | |
| } | |
| ] | |
| st.session_state.db_invoices = [ | |
| {"id": 1, "cislo": "20260001", "zakaznik": "Jan Novák", "castka": 1250.00, "stav": "Zaplaceno", "datum": "2026-06-05"}, | |
| {"id": 2, "cislo": "20260002", "zakaznik": "Marie Podlahová", "castka": 1580.50, "stav": "Odesláno", "datum": "2026-06-10"}, | |
| {"id": 3, "cislo": "20260003", "zakaznik": "Zdeněk Urban", "castka": 290.00, "stav": "Zaplaceno", "datum": "2026-06-12"}, | |
| {"id": 4, "cislo": "20260004", "zakaznik": "Jan Novák", "castka": 5010.00, "stav": "Odesláno", "datum": "2026-06-15"} | |
| ] | |
| st.session_state.quotes_count = 1 | |
| st.toast(t["toast_demo_ok"], icon="📥") | |
| time.sleep(0.3) | |
| st.rerun() | |
| if st.button(t["sb_clear_demo"], use_container_width=True): | |
| st.session_state.db_team = [] | |
| st.session_state.db_customers = [] | |
| st.session_state.db_quotes = [] | |
| st.session_state.db_invoices = [] | |
| st.session_state.quotes_count = 0 | |
| st.session_state.cost_items_df = pd.DataFrame([{"Sekce nákladu": "Materiál", "Název položky / Popis": "", "Částka": 0.0}]) | |
| st.toast(t["toast_clear_ok"], icon="🗑️") | |
| time.sleep(0.3) | |
| st.rerun() | |
| st.markdown("---") | |
| if st.button(t["sb_logout"], use_container_width=True): | |
| st.session_state.admin_authenticated = False | |
| st.rerun() | |
| st.title(t["title"]) | |
| st.caption(f"{t['caption']} | Engine Verze: {VERSION}") | |
| is_locked = st.session_state.quotes_count >= 3 and not st.session_state.user_subscribed | |
| tab_dashboard, tab_generator, tab_team, tab_crm, tab_fakturace, tab_pro_features = st.tabs([ | |
| t["tab_dash"], t["tab_gen"], t["tab_team"], t["tab_crm"], t["tab_inv"], t["tab_pro"] | |
| ]) | |
| # ----------------------------------------------------------------------------- | |
| # TAB 1: DASHBOARD & METRIKY | |
| # ----------------------------------------------------------------------------- | |
| with tab_dashboard: | |
| st.header(t["tab_dash"]) | |
| total_revenue = sum(inv["castka"] for inv in st.session_state.db_invoices if inv["stav"] == "Zaplaceno") | |
| pending_revenue = sum(inv["castka"] for inv in st.session_state.db_invoices if inv["stav"] == "Odesláno") | |
| potencial = sum(q["celkem"] for q in st.session_state.db_quotes if q.get("typ") == "fixni") | |
| c1, c2, c3, c4 = st.columns(4) | |
| c1.metric(t["profit"], format_price(total_revenue)) | |
| c2.metric(t["pending"], format_price(pending_revenue)) | |
| c3.metric(t["potential"], format_price(potencial), help=t["dash_help_potential"]) | |
| c4.metric(t["limit"], f"{st.session_state.quotes_count} / 3", delta=t["dash_limit_warn"] if is_locked else None) | |
| st.markdown("---") | |
| st.subheader(t["dash_analysis_title"]) | |
| st.write(t["dash_analysis_desc"]) | |
| flat_costs = [] | |
| for q in st.session_state.db_quotes: | |
| if q.get("labor_eur", 0) > 0: | |
| flat_costs.append({ | |
| t["col_project"]: f"{q['zakaznik']} ({q['datum']})", | |
| t["col_section"]: "Výplaty zaměstnancům" if st.session_state.lang != "EN" else "Employee Wages", | |
| t["col_item_name"]: "Mzda týmu na projektu (odpracované hodiny)" if st.session_state.lang != "EN" else "Team payroll on project (hours worked)", | |
| "Částka (Základ v EUR)": q["labor_eur"], | |
| t["col_type"]: "🎯 Přesná fixní" if q["typ"] == "fixni" else "🔮 Orientační odhad" | |
| }) | |
| for ec in q.get("extra_costs", []): | |
| flat_costs.append({ | |
| t["col_project"]: f"{q['zakaznik']} ({q['datum']})", | |
| t["col_section"]: ec["kategorie"], | |
| t["col_item_name"]: ec["nazev"] if ec["nazev"] else "Přímý výdaj", | |
| "Částka (Základ v EUR)": ec["eur"], | |
| t["col_type"]: "🎯 Přesná fixní" if q["typ"] == "fixni" else "🔮 Orientační odhad" | |
| }) | |
| if flat_costs: | |
| df_costs = pd.DataFrame(flat_costs) | |
| f_col1, f_col2 = st.columns(2) | |
| with f_col1: | |
| available_cats = [t["dash_filter_all_cats"]] + list(df_costs[t["col_section"]].unique()) | |
| selected_cat_filter = st.selectbox(t["dash_filter_cat"], available_cats) | |
| with f_col2: | |
| selected_type_filter = st.radio(t["dash_filter_type"], [t["dash_type_all"], t["dash_type_fixed"], t["dash_type_estimated"]], horizontal=True) | |
| if selected_cat_filter != t["dash_filter_all_cats"]: | |
| df_costs = df_costs[df_costs[t["col_section"]] == selected_cat_filter] | |
| if selected_type_filter == t["dash_type_fixed"]: | |
| df_costs = df_costs[df_costs[t["col_type"]] == "🎯 Přesná fixní"] | |
| elif selected_type_filter == t["dash_type_estimated"]: | |
| df_costs = df_costs[df_costs[t["col_type"]] == "🔮 Orientační odhad"] | |
| if not df_costs.empty: | |
| total_filtered_eur = df_costs["Částka (Základ v EUR)"].sum() | |
| st.markdown(f"#### {t['dash_aggregated_spend']} **{format_price(total_filtered_eur)}**") | |
| df_costs_display = df_costs.copy() | |
| df_costs_display[t["col_amount"]] = df_costs_display["Částka (Základ v EUR)"].apply(format_price) | |
| df_costs_display = df_costs_display.drop(columns=["Částka (Základ v EUR)"]) | |
| st.dataframe(df_costs_display, use_container_width=True, hide_index=True) | |
| else: | |
| st.info(t["dash_no_records"]) | |
| else: | |
| st.info(t["dash_no_costs_yet"]) | |
| st.markdown("---") | |
| st.subheader(t["dash_book_title"]) | |
| if st.session_state.db_quotes: | |
| df_raw = pd.DataFrame(st.session_state.db_quotes) | |
| df_display = df_raw.copy() | |
| # Přejmenování klíčů a lokalizace pro tabulku | |
| df_display[t["col_date"]] = df_display["datum"] | |
| df_display[t["col_project"]] = df_display["zakaznik"] | |
| df_display[t["col_item_name"]] = df_display["popis"] | |
| df_display[t["col_amount"]] = df_display["celkem"].apply(format_price) | |
| df_display[t["col_type"]] = df_display["typ"].map({"odhad": "🔮 " + t["dash_type_estimated"], "fixni": "🎯 " + t["dash_type_fixed"]}) | |
| cols_to_show = [t["col_date"], t["col_project"], t["col_type"], t["col_item_name"], t["col_amount"]] | |
| st.dataframe(df_display[cols_to_show], use_container_width=True, hide_index=True) | |
| else: | |
| st.info(t["dash_book_empty"]) | |
| # ----------------------------------------------------------------------------- | |
| # TAB 2: AI GENERÁTOR NABÍDEK (OPRAVENÝ & S PŘELOŽENÝM SOUBOREM NÁKLADŮ) | |
| # ----------------------------------------------------------------------------- | |
| with tab_generator: | |
| st.header(t["tab_gen"]) | |
| if is_locked: | |
| st.error(t["paywall_msg"]) | |
| else: | |
| quote_type = st.radio(t["gen_type_label"], [t["gen_type_est"], t["gen_type_fix"]]) | |
| is_fixed = t["gen_type_fix"] in quote_type | |
| st.markdown(t["gen_client_section"]) | |
| cust_names = [c["jmeno"] for c in st.session_state.db_customers] | |
| client_mode = st.radio(t["gen_client_mode"], [t["gen_client_crm"], t["gen_client_manual"]], horizontal=True) | |
| if client_mode == t["gen_client_crm"] and cust_names: | |
| selected_cust = st.selectbox(t["gen_select_client"], cust_names) | |
| else: | |
| selected_cust = st.text_input(t["gen_input_client_name"], value="", placeholder=t["gen_client_placeholder"]) | |
| input_task = st.text_area(t["gen_spec_label"], value=t["gen_spec_default"]) | |
| st.markdown(t["gen_team_alloc"]) | |
| if not st.session_state.db_team: | |
| st.info(t["gen_no_team_warn"]) | |
| team_hours = {} | |
| else: | |
| if st.button(t["gen_ai_btn"]): | |
| st.session_state["ai_mock_active"] = True | |
| st.session_state.cost_items_df = pd.DataFrame([ | |
| {"Sekce nákladu": "Materiál", "Název položky / Popis": "AI Odhad: Potrubí, ventily a fitinky", "Částka": 480.0}, | |
| {"Sekce nákladu": "Benzín / Doprava", "Název položky / Popis": "AI Odhad: Nákladní závoz materiálu", "Částka": 65.0}, | |
| {"Sekce nákladu": "Pronájem vybavení", "Název položky / Popis": "AI Odhad: Zapůjčení tlakové zkoušečky", "Částka": 110.0} | |
| ]) | |
| st.toast(t["gen_ai_toast"], icon="🪄") | |
| team_hours = {} | |
| cols_team = st.columns(len(st.session_state.db_team)) | |
| for idx, member in enumerate(st.session_state.db_team): | |
| with cols_team[idx]: | |
| default_val = 0 | |
| if st.session_state.get("ai_mock_active", False): | |
| if "director" in member["jmeno"].lower(): default_val = 10 | |
| elif "prezentace" in member["jmeno"].lower(): default_val = 6 | |
| else: default_val = 24 | |
| hours_input = st.number_input( | |
| f"{member['jmeno']} ({format_price(member['sazba'])}/h)", | |
| min_value=0, max_value=500, value=default_val, key=f"gen_hours_{member['id']}" | |
| ) | |
| team_hours[member["id"]] = hours_input | |
| st.markdown(t["gen_direct_costs"]) | |
| st.caption(t["gen_table_hint"]) | |
| curr_rate = CURRENCIES[st.session_state.curr]["rate"] | |
| # Úprava konfigurace tabulky - odstraněna měna ze záhlaví sloupce (pouze t["col_amount"]) | |
| column_configuration = { | |
| "Sekce nákladu": st.column_config.SelectboxColumn( | |
| t["col_section"], | |
| options=["Materiál", "Benzín / Doprava", "Pronájem vybavení", "Ostatní režie"] if st.session_state.lang != "EN" else ["Material", "Logistics / Fuel", "Equipment Rental", "Other Overhead"], | |
| required=True | |
| ), | |
| "Název položky / Popis": st.column_config.TextColumn(t["col_item_name"], required=True), | |
| "Částka": st.column_config.NumberColumn(t["col_amount"], min_value=0.0, required=True, format="%.2f") | |
| } | |
| edited_df = st.data_editor( | |
| st.session_state.cost_items_df, | |
| column_config=column_configuration, | |
| num_rows="dynamic", | |
| use_container_width=True, | |
| key="main_cost_data_editor" | |
| ) | |
| st.session_state.cost_items_df = edited_df | |
| st.markdown(t["gen_csv_title"]) | |
| def zpracuj_csv_import(): | |
| if st.session_state.csv_uploader_key is not None: | |
| try: | |
| uploaded_df = pd.read_csv(st.session_state.csv_uploader_key) | |
| needed_headers = ["Sekce nákladu", "Název položky / Popis", "Částka"] | |
| if all(head in uploaded_df.columns for head in needed_headers): | |
| sanitized_df = uploaded_df[needed_headers].copy() | |
| sanitized_df["Částka"] = pd.to_numeric(sanitized_df["Částka"], errors='coerce').fillna(0.0) | |
| st.session_state.cost_items_df = pd.concat([st.session_state.cost_items_df, sanitized_df], ignore_index=True) | |
| st.toast(t["gen_csv_success"], icon="📥") | |
| else: | |
| st.error(t["gen_csv_err_headers"]) | |
| except Exception as e: | |
| st.error(f"{t['gen_csv_err_read']} {e}") | |
| st.file_uploader( | |
| t["gen_csv_uploader"], | |
| type=["csv"], | |
| key="csv_uploader_key", | |
| on_change=zprocessed_csv_import if 'zprocessed_csv_import' in locals() else zpracuj_csv_import | |
| ) | |
| st.markdown("---") | |
| total_labor_eur = 0.0 | |
| for member in st.session_state.db_team: | |
| total_labor_eur += team_hours.get(member["id"], 0) * member["sazba"] | |
| total_extra_local = edited_df["Částka"].sum() if not edited_df.empty else 0.0 | |
| total_extra_eur = total_extra_local / curr_rate | |
| total_project_eur = total_labor_eur + total_extra_eur | |
| st.markdown(f"{t['gen_total_calc']} **{format_price(total_project_eur)}**") | |
| if st.button(t["gen_save_btn"], type="primary"): | |
| if not selected_cust or selected_cust.strip() == "": | |
| st.error(t["gen_err_no_client"]) | |
| else: | |
| if selected_cust not in cust_names: | |
| st.session_state.db_customers.append({ | |
| "id": len(st.session_state.db_customers) + 1, | |
| "jmeno": selected_cust.strip(), | |
| "firma": "Fyzická osoba" if st.session_state.lang != "EN" else "Individual Client", | |
| "telefon": "Nezadáno" if st.session_state.lang != "EN" else "N/A" | |
| }) | |
| saved_extra_costs = [] | |
| for _, row in edited_df.iterrows(): | |
| saved_extra_costs.append({ | |
| "kategorie": row["Sekce nákladu"], | |
| "nazev": row["Název položky / Popis"], | |
| "eur": float(row["Částka"]) / curr_rate | |
| }) | |
| new_quote = { | |
| "id": len(st.session_state.db_quotes) + 1, | |
| "zakaznik": selected_cust.strip(), | |
| "popis": input_task, | |
| "celkem": total_project_eur, | |
| "typ": "fixni" if is_fixed else "odhad", | |
| "datum": time.strftime("%Y-%m-%d"), | |
| "labor_eur": total_labor_eur, | |
| "extra_costs": saved_extra_costs | |
| } | |
| st.session_state.db_quotes.append(new_quote) | |
| st.session_state.quotes_count += 1 | |
| st.session_state["ai_mock_active"] = False | |
| st.session_state.cost_items_df = pd.DataFrame([{"Sekce nákladu": "Materiál", "Název položky / Popis": "", "Částka": 0.0}]) | |
| st.success(t["gen_save_success"]) | |
| time.sleep(0.4) | |
| st.rerun() | |
| # ----------------------------------------------------------------------------- | |
| # TAB 3: SPRÁVA TÝMU | |
| # ----------------------------------------------------------------------------- | |
| with tab_team: | |
| st.header(t["team_title"]) | |
| st.write(t["team_desc"]) | |
| t_col1, t_col2 = st.columns([2, 1]) | |
| with t_col1: | |
| st.markdown(f"### {t['team_sub_list']}") | |
| team_display_list = [] | |
| for m in st.session_state.db_team: | |
| team_display_list.append({ | |
| t["team_col_id"]: m["id"], | |
| t["team_col_role"]: m["jmeno"], | |
| t["team_col_rate"]: format_price(m["sazba"]) | |
| }) | |
| if team_display_list: | |
| st.dataframe(pd.DataFrame(team_display_list), use_container_width=True, hide_index=True) | |
| else: | |
| st.info(t["team_empty"]) | |
| st.markdown(t["team_remove_section"]) | |
| if st.session_state.db_team: | |
| member_to_remove = st.selectbox(t["team_remove_select"], [m["jmeno"] for m in st.session_state.db_team]) | |
| if st.button(t["team_remove_btn"]): | |
| st.session_state.db_team = [m for m in st.session_state.db_team if m["jmeno"] != member_to_remove] | |
| st.success(f"{t['team_remove_toast']} ({member_to_remove})") | |
| time.sleep(0.3) | |
| st.rerun() | |
| with t_col2: | |
| st.markdown(t["team_add_section"]) | |
| with st.form("team_management_form"): | |
| new_tm_name = st.text_input(t["team_input_name"], placeholder=t["team_input_placeholder"]) | |
| new_tm_rate = st.number_input(t["team_input_rate"], min_value=1, value=400 if st.session_state.curr == "CZK" else 20) | |
| if st.form_submit_button(t["team_submit_btn"]): | |
| if new_tm_name: | |
| rate_in_eur = new_tm_rate / CURRENCIES[st.session_state.curr]["rate"] | |
| matched_member = next((m for m in st.session_state.db_team if m["jmeno"].lower() == new_tm_name.lower()), None) | |
| if matched_member: | |
| matched_member["sazba"] = rate_in_eur | |
| st.success(t["team_update_toast"]) | |
| else: | |
| next_id = max([m["id"] for m in st.session_state.db_team]) + 1 if st.session_state.db_team else 1 | |
| st.session_state.db_team.append({"id": next_id, "jmeno": new_tm_name, "sazba": rate_in_eur}) | |
| st.success(t["team_add_toast"]) | |
| time.sleep(0.3) | |
| st.rerun() | |
| # ----------------------------------------------------------------------------- | |
| # TAB 4: CRM ADRESÁŘ | |
| # ----------------------------------------------------------------------------- | |
| with tab_crm: | |
| st.header(t["crm_title"]) | |
| col_crm1, col_crm2 = st.columns([2, 1]) | |
| with col_crm1: | |
| if st.session_state.db_customers: | |
| df_crm = pd.DataFrame(st.session_state.db_customers) | |
| df_crm_display = pd.DataFrame({ | |
| t["cust_name"]: df_crm["jmeno"], | |
| t["company"]: df_crm["firma"], | |
| t["phone"]: df_crm["telefon"] | |
| }) | |
| st.dataframe(df_crm_display, use_container_width=True, hide_index=True) | |
| else: | |
| st.info(t["crm_empty"]) | |
| with col_crm2: | |
| st.markdown(t["crm_add_section"]) | |
| with st.form("manual_crm_form"): | |
| c_n = st.text_input(t["cust_name"], placeholder="Jan Novák") | |
| c_f = st.text_input(t["company"], placeholder="Stavebniny s.r.o.") | |
| c_p = st.text_input(t["phone"], placeholder="+420 777 888 999") | |
| if st.form_submit_button(t["save_cust"]): | |
| if c_n: | |
| st.session_state.db_customers.append({ | |
| "id": len(st.session_state.db_customers) + 1, | |
| "jmeno": c_n, | |
| "firma": c_f if c_f else ("Fyzická osoba" if st.session_state.lang != "EN" else "Individual"), | |
| "telefon": c_p | |
| }) | |
| st.success(t["crm_success_toast"]) | |
| time.sleep(0.3) | |
| st.rerun() | |
| else: | |
| st.error(t["crm_err_name"]) | |
| # ----------------------------------------------------------------------------- | |
| # TAB 5: FAKTURACE | |
| # ----------------------------------------------------------------------------- | |
| with tab_fakturace: | |
| st.header(t["inv_title"]) | |
| only_fixni_quotes = [q for q in st.session_state.db_quotes if q.get("typ") == "fixni"] | |
| if only_fixni_quotes: | |
| invoice_options = {f"ID: {q['id']} | {q['zakaznik']} — {format_price(q['celkem'])}": q for q in only_fixni_quotes} | |
| selected_quote_to_inv = st.selectbox(t["inv_select_quote"], list(invoice_options.keys())) | |
| targeted_quote = invoice_options[selected_quote_to_inv] | |
| if st.button(t["invoice_btn"], type="primary", use_container_width=True): | |
| st.session_state.db_invoices.append({ | |
| "id": len(st.session_state.db_invoices) + 1, | |
| "cislo": f"2026{len(st.session_state.db_invoices)+1:04d}", | |
| "zakaznik": targeted_quote["zakaznik"], | |
| "castka": targeted_quote["celkem"], | |
| "stav": "Odesláno", | |
| "datum": time.strftime("%Y-%m-%d") | |
| }) | |
| st.success(t["inv_success_toast"]) | |
| time.sleep(0.3) | |
| st.rerun() | |
| else: | |
| st.info(t["inv_no_fixed_quotes"]) | |
| st.markdown("---") | |
| st.subheader(t["inv_book_title"]) | |
| if st.session_state.db_invoices: | |
| for index_inv, invoice_obj in enumerate(st.session_state.db_invoices): | |
| div_c1, div_c2, div_c3 = st.columns([2, 1, 1]) | |
| div_c1.write(f"🧾 **{t['col_amount']} č. {invoice_obj['cislo']}** — {invoice_obj['zakaznik']} [**{format_price(invoice_obj['castka'])}**]") | |
| if invoice_obj["stav"] == "Zaplaceno": | |
| div_c2.markdown(f"<span style='color:green; font-weight:bold;'>{t['inv_status_paid']}</span>", unsafe_allow_html=True) | |
| div_c3.write("") | |
| else: | |
| div_c2.markdown(f"<span style='color:orange; font-weight:bold;'>{t['inv_status_sent']}</span>", unsafe_allow_html=True) | |
| if div_c3.button(t["inv_mark_paid"], key=f"payment_trigger_key_{index_inv}", use_container_width=True): | |
| invoice_obj["stav"] = "Zaplaceno" | |
| st.rerun() | |
| else: | |
| st.info(t["inv_empty"]) | |
| # ----------------------------------------------------------------------------- | |
| # TAB 6: PRO BONUSY | |
| # ----------------------------------------------------------------------------- | |
| with tab_pro_features: | |
| st.header(t["tab_pro"]) | |
| if not st.session_state.user_subscribed: | |
| st.warning(t["pro_locked_warn"]) | |
| else: | |
| pro_sub1, pro_sub2 = st.tabs([t["pro_router"], t["pro_sms"]]) | |
| with pro_sub1: | |
| st.subheader(t["pro_route_title"]) | |
| st.text_area(t["pro_route_label"], value="Plzeňská 15, Praha\nPrůmyslová 4, Beroun\nSukova 12, Plzeň") | |
| if st.button(t["pro_route_btn"], use_container_width=True): | |
| st.info(t["pro_route_result"]) | |
| with pro_sub2: | |
| st.subheader(t["pro_sms_title"]) | |
| st.markdown(t["pro_sms_step1"]) | |
| open_debts = [invoice for invoice in st.session_state.db_invoices if invoice["stav"] == "Odesláno"] | |
| search_col1, search_col2 = st.columns([1, 1]) | |
| with search_col1: | |
| query_name = st.text_input(t["pro_sms_filter_name"], value="") | |
| with search_col2: | |
| sorting_order = st.selectbox(t["pro_sms_sort_label"], [t["pro_sms_sort_high"], t["pro_sms_sort_low"]]) | |
| if query_name: | |
| open_debts = [d for d in open_debts if query_name.lower() in d["zakaznik"].lower()] | |
| if sorting_order == t["pro_sms_sort_high"]: | |
| open_debts.sort(key=lambda x: x["castka"], reverse=True) | |
| else: | |
| open_debts.sort(key=lambda x: x["castka"]) | |
| if not open_debts: | |
| st.info(t["pro_sms_no_debts"]) | |
| active_debtor = None | |
| else: | |
| debtor_mapping = {f"👤 {d['zakaznik']} (Faktura č. {d['cislo']} | Dluh: {format_price(d['castka'])})": d for d in open_debts} | |
| selected_debtor_string = st.selectbox(t["pro_sms_select_debtor"], list(debtor_mapping.keys())) | |
| active_debtor = debtor_mapping[selected_debtor_string] | |
| st.markdown("---") | |
| st.markdown(t["pro_sms_step2"]) | |
| preset_labels = [p["název"] for p in st.session_state.sms_presets] | |
| chosen_preset_label = st.selectbox(t["pro_sms_template_label"], preset_labels) | |
| active_preset = next(p for p in st.session_state.sms_presets if p["název"] == chosen_preset_label) | |
| with st.expander(t["pro_sms_expander"]): | |
| st.markdown(t["pro_sms_ai_assistant"]) | |
| prompt_text = st.text_input(t["pro_sms_ai_prompt"], placeholder="...") | |
| if st.button(t["pro_sms_ai_btn"]): | |
| if prompt_text: | |
| p_low = prompt_text.lower() | |
| if "předžal" in p_low or "naléh" in p_low or "soud" in p_low: | |
| 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." | |
| else: | |
| result_sms = "Dobrý den {jmeno}, připomínáme zapomenutou fakturu č. {cislo} na částku {castka}. Prosíme o co nejrychlejší proplacení." | |
| st.session_state["ai_sms_output_buffer"] = result_sms | |
| st.success(t["pro_sms_ai_success"]) | |
| new_preset_title = st.text_input(t["pro_sms_new_title"], value="") | |
| default_body = st.session_state.get("ai_sms_output_buffer", "Vážený pane/paní {jmeno}, ...") | |
| new_preset_body = st.text_area(t["pro_sms_new_body"], value=default_body) | |
| if st.button(t["pro_sms_save_template_btn"]): | |
| if new_preset_title and new_preset_body: | |
| st.session_state.sms_presets.append({"název": new_preset_title, "text": new_preset_body}) | |
| st.success(t["pro_sms_template_success"]) | |
| time.sleep(0.3) | |
| st.rerun() | |
| st.markdown("---") | |
| st.markdown(t["pro_sms_step3"]) | |
| if active_debtor: | |
| raw_template = active_preset["text"] | |
| final_compiled_sms = raw_template.replace("{jmeno}", active_debtor["zakaznik"])\ | |
| .replace("{cislo}", active_debtor["cislo"])\ | |
| .replace("{castka}", format_price(active_debtor["castka"])) | |
| st.markdown(f""" | |
| <div style=" | |
| background-color: #f8f9fa; | |
| border: 1px solid #dee2e6; | |
| padding: 18px; | |
| border-radius: 8px; | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| font-size: 15px; | |
| color: #212529; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| line-height: 1.6; | |
| margin-bottom: 12px; | |
| ;">{final_compiled_sms}</div> | |
| """, unsafe_allow_html=True) | |
| safe_encoded_sms = urllib.parse.quote(final_compiled_sms) | |
| copy_button_html = f""" | |
| <button onclick=" | |
| navigator.clipboard.writeText(decodeURIComponent('{safe_encoded_sms}')); | |
| let btn = document.getElementById('js_copy_btn'); | |
| btn.innerText = '{t['pro_sms_copy_ok']}'; | |
| btn.style.backgroundColor = '#28a745'; | |
| setTimeout(() => {{ btn.innerText = '{t['pro_sms_copy_btn']}'; btn.style.backgroundColor = '#007bff'; }}, 2000); | |
| " id="js_copy_btn" style=" | |
| background-color: #007bff; | |
| color: white; | |
| border: none; | |
| padding: 12px 20px; | |
| font-size: 14px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-weight: bold; | |
| width: 100%; | |
| ">{t['pro_sms_copy_btn']}</button> | |
| """ | |
| components.html(copy_button_html, height=50) | |
| else: | |
| st.info(t["pro_sms_select_info"]) |