Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -20,7 +20,7 @@ TRANSLATIONS = {
|
|
| 20 |
},
|
| 21 |
"SK": {
|
| 22 |
"title": "🏆 TradeFlow AI", "caption": "Chytry operačný systém pre novú generáciu remeselníkov.",
|
| 23 |
-
"tab_dash": "📈 Prehľad & Metriky", "tab_gen": "📝 AI Generátor Ponúk", "tab_team": "👥 Správa
|
| 24 |
"profit": "Skutočný zisk", "pending": "Čakajúce peniaze", "potential": "Otvorené AI Ponuky", "limit": "Využitie Free limitu",
|
| 25 |
"cust_name": "Meno a priezvisko", "company": "Firma (Voliteľné)", "phone": "Telefónne číslo", "save_cust": "Uložiť klienta",
|
| 26 |
"generate_btn": "⚡ Vygenerovať/Uložiť rozpočet", "invoice_btn": "🧾 VYSTAVIŤ FAKTÚRU JEDNÝM KLIKNUTÍM",
|
|
@@ -35,41 +35,6 @@ TRANSLATIONS = {
|
|
| 35 |
"generate_btn": "⚡ Generate/Save Quote", "invoice_btn": "🧾 ISSUE 1-CLICK INVOICE",
|
| 36 |
"paywall_msg": "🔒 You have reached the 3 free quotes limit. Unlock PRO tier for unlimited access.",
|
| 37 |
"pro_router": "📍 AI Route & Logistics Planner", "pro_sms": "📲 Intelligent SMS Reminder Center"
|
| 38 |
-
},
|
| 39 |
-
"DE": {
|
| 40 |
-
"title": "🏆 TradeFlow AI", "caption": "Intelligentes Betriebssystem für Handwerker.",
|
| 41 |
-
"tab_dash": "📈 Dashboard & Metriken", "tab_gen": "📝 AI-Angebotsgenerator", "tab_team": "👥 Team-Management", "tab_crm": "👥 Kontakte (CRM)", "tab_inv": "🧾 1-Klick-Rechnungsstellung", "tab_pro": "🚀 PRO-Funktionen",
|
| 42 |
-
"profit": "Tatsächlicher Gewinn", "pending": "Ausstehende Einnahmen", "potential": "Offene KI-Angebote", "limit": "Free-Tier-Nutzung",
|
| 43 |
-
"cust_name": "Vollständiger Name", "company": "Firma (Optional)", "phone": "Telefonnummer", "save_cust": "Kunde保存",
|
| 44 |
-
"generate_btn": "⚡ Angebot generieren/speichern", "invoice_btn": "🧾 1-KLICK-RECHNUNG ERSTELLEN",
|
| 45 |
-
"paywall_msg": "🔒 Sie haben das Limit von 3 kostenlosen Angeboten erreicht. Aktivieren Sie PRO.",
|
| 46 |
-
"pro_router": "📍 KI-Routen- & Logistikplaner", "pro_sms": "📲 Intelligentes SMS-Mahnzentrum"
|
| 47 |
-
},
|
| 48 |
-
"SPA": {
|
| 49 |
-
"title": "🏆 TradeFlow AI", "caption": "Sistema operativo inteligente para profesionales.",
|
| 50 |
-
"tab_dash": "📈 Panel y Métricas", "tab_gen": "📝 Generador AI", "tab_team": "👥 Gestión de Equipo", "tab_crm": "👥 Contactos (CRM)", "tab_inv": "🧾 Facturación 1 Clic", "tab_pro": "🚀 Funciones PRO",
|
| 51 |
-
"profit": "Ganancia Real", "pending": "Ingresos Pendientes", "potential": "Presupuestos AI", "limit": "Uso Free",
|
| 52 |
-
"cust_name": "Nombre Completo", "company": "Empresa", "phone": "Teléfono", "save_cust": "Guardar",
|
| 53 |
-
"generate_btn": "⚡ Guardar Presupuesto", "invoice_btn": "🧾 FACTURA EN 1 CLIC",
|
| 54 |
-
"paywall_msg": "🔒 Límite gratis alcanzado. Active PRO.",
|
| 55 |
-
"pro_router": "📍 Planificador IA", "pro_sms": "📲 Centro SMS Inteligente"
|
| 56 |
-
},
|
| 57 |
-
"ITA": {
|
| 58 |
-
"title": "🏆 TradeFlow AI", "caption": "Sistema operativo intelligente per artigiani.",
|
| 59 |
-
"tab_dash": "📈 Dashboard", "tab_gen": "📝 Generatore AI", "tab_team": "👥 Gestione Team", "tab_crm": "👥 Contatti (CRM)", "tab_inv": "🧾 Fatturazione 1 Clic", "tab_pro": "🚀 Info PRO",
|
| 60 |
-
"profit": "Profitto Reale", "pending": "In Sospeso", "potential": "Preventivi AI", "limit": "Free Tier",
|
| 61 |
-
"cust_name": "Nome", "company": "Azienda", "phone": "Telefono", "save_cust": "Salva",
|
| 62 |
-
"generate_btn": "⚡ Salva Preventivo", "invoice_btn": "🧾 GENERA FATTURA",
|
| 63 |
-
"paywall_msg": "🔒 Limite raggiunto. Attiva PRO.",
|
| 64 |
-
"pro_router": "📍 Logistica IA", "pro_sms": "📲 Centro SMS"
|
| 65 |
-
},
|
| 66 |
-
"FRA": {
|
| 67 |
-
"title": "🏆 TradeFlow AI", "caption": "Système d'exploitation intelligent pour les artisans.",
|
| 68 |
-
"tab_dash": "📈 Tableau de bord", "tab_gen": "📝 Générateur IA", "tab_team": "👥 Gestion de l'équipe", "tab_crm": "👥 Contacts", "tab_inv": "🧾 Facturation 1 Clic", "tab_pro": "🚀 PRO",
|
| 69 |
-
"profit": "Bénéfice Réel", "pending": "En Attente", "potential": "Devis IA", "limit": "Usage Free",
|
| 70 |
-
"cust_name": "Nom", "company": "Entreprise", "phone": "Numéro", "save_cust": "Enregistrer",
|
| 71 |
-
"generate_btn": "⚡ Enregistrer le Devis", "invoice_btn": "🧾 CRÉER LA FACTURE",
|
| 72 |
-
"paywall_msg": "🔒 Limite atteinte. Activez PRO."
|
| 73 |
}
|
| 74 |
}
|
| 75 |
|
|
@@ -84,7 +49,7 @@ CURRENCIES = {
|
|
| 84 |
# -----------------------------------------------------------------------------
|
| 85 |
# INICIALIZACE STAVU
|
| 86 |
# -----------------------------------------------------------------------------
|
| 87 |
-
VERSION = "1.
|
| 88 |
|
| 89 |
st.set_page_config(page_title="TradeFlow AI", page_icon="🏆", layout="wide", initial_sidebar_state="expanded")
|
| 90 |
|
|
@@ -97,7 +62,13 @@ if "quotes_count" not in st.session_state: st.session_state.quotes_count = 1
|
|
| 97 |
if "stripe_mode" not in st.session_state: st.session_state.stripe_mode = "Sandbox (Simulace)"
|
| 98 |
if "stripe_api_key" not in st.session_state: st.session_state.stripe_api_key = ""
|
| 99 |
|
| 100 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
if "db_team" not in st.session_state:
|
| 102 |
st.session_state.db_team = [
|
| 103 |
{"id": 1, "jmeno": "Šimon (Director)", "sazba": 45.0},
|
|
@@ -112,11 +83,27 @@ if "db_customers" not in st.session_state:
|
|
| 112 |
{"id": 3, "jmeno": "Zdeněk Urban", "firma": "Instalatérství Urban", "telefon": "+420 731 555 999"}
|
| 113 |
]
|
| 114 |
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
if "db_invoices" not in st.session_state:
|
| 118 |
st.session_state.db_invoices = [
|
| 119 |
-
{"id": 1, "cislo": "20260001", "zakaznik": "Jan Novák", "castka":
|
| 120 |
{"id": 2, "cislo": "20260002", "zakaznik": "Marie Podlahová", "castka": 1580.50, "stav": "Odesláno", "datum": "2026-06-10"},
|
| 121 |
{"id": 3, "cislo": "20260003", "zakaznik": "Zdeněk Urban", "castka": 290.00, "stav": "Zaplaceno", "datum": "2026-06-12"}
|
| 122 |
]
|
|
@@ -135,7 +122,6 @@ def format_price(amount_in_eur):
|
|
| 135 |
return f"{converted:,.2f} {c_info['symbol']}"
|
| 136 |
return f"{c_info['symbol']}{converted:,.2f}"
|
| 137 |
|
| 138 |
-
# Spuštění jednorázových balónků při zakoupení / aktivaci PRO
|
| 139 |
if st.session_state.user_subscribed:
|
| 140 |
if not st.session_state.balloons_triggered:
|
| 141 |
st.balloons()
|
|
@@ -143,7 +129,7 @@ if st.session_state.user_subscribed:
|
|
| 143 |
else:
|
| 144 |
st.session_state.balloons_triggered = False
|
| 145 |
|
| 146 |
-
t = TRANSLATIONS[st.session_state.lang]
|
| 147 |
|
| 148 |
# -----------------------------------------------------------------------------
|
| 149 |
# SIDEBAR: CONFIG & ADMIN
|
|
@@ -161,7 +147,7 @@ with st.sidebar:
|
|
| 161 |
if st.button("Odemknout Admin Panel"):
|
| 162 |
if pwd_input == "admin123":
|
| 163 |
st.session_state.admin_authenticated = True
|
| 164 |
-
st.success("
|
| 165 |
st.rerun()
|
| 166 |
else:
|
| 167 |
st.error("Nesprávné heslo!")
|
|
@@ -169,16 +155,11 @@ with st.sidebar:
|
|
| 169 |
st.info("🔓 Úspěšně autorizováno")
|
| 170 |
st.session_state.user_subscribed = st.toggle("Uživatel má zakoupen PRO tarif", value=st.session_state.user_subscribed)
|
| 171 |
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)
|
| 172 |
-
st.session_state.stripe_mode = st.radio("Režim Stripe", ["Sandbox (Simulace)", "Stripe Live API"])
|
| 173 |
|
| 174 |
if st.button("Zamknout panel & Odhlásit se"):
|
| 175 |
st.session_state.admin_authenticated = False
|
| 176 |
st.rerun()
|
| 177 |
-
if st.button("Resetovat kompletní aplikaci"):
|
| 178 |
-
st.session_state.clear()
|
| 179 |
-
st.rerun()
|
| 180 |
|
| 181 |
-
t = TRANSLATIONS[st.session_state.lang]
|
| 182 |
st.title(t["title"])
|
| 183 |
st.caption(f"{t['caption']} | Version: {VERSION}")
|
| 184 |
|
|
@@ -189,7 +170,7 @@ tab_dashboard, tab_generator, tab_team, tab_crm, tab_fakturace, tab_pro_features
|
|
| 189 |
])
|
| 190 |
|
| 191 |
# -----------------------------------------------------------------------------
|
| 192 |
-
# TAB 1: METRIKY
|
| 193 |
# -----------------------------------------------------------------------------
|
| 194 |
with tab_dashboard:
|
| 195 |
st.header(t["tab_dash"])
|
|
@@ -203,6 +184,71 @@ with tab_dashboard:
|
|
| 203 |
c3.metric(t["potential"], format_price(potencial), help="Počítá pouze Přesné fixní nabídky. Orientační odhady se sem nepromítají.")
|
| 204 |
c4.metric(t["limit"], f"{st.session_state.quotes_count} / 3", delta="⚠️ LIMIT" if is_locked else None)
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
st.subheader("Přehled vytvořených nabídek a kalkulací")
|
| 207 |
if st.session_state.db_quotes:
|
| 208 |
df_raw = pd.DataFrame(st.session_state.db_quotes)
|
|
@@ -210,28 +256,15 @@ with tab_dashboard:
|
|
| 210 |
df_display["celkem"] = df_display["celkem"].apply(format_price)
|
| 211 |
df_display["typ"] = df_display["typ"].map({"odhad": "🔮 Orientační odhad", "fixni": "🎯 Přesná fixní"})
|
| 212 |
st.dataframe(df_display[["datum", "zakaznik", "typ", "popis", "celkem"]], use_container_width=True)
|
| 213 |
-
else:
|
| 214 |
-
st.info("Žádné vytvořené nabídky přes AI. Historie faktur je k dispozici v samostatné záložce.")
|
| 215 |
|
| 216 |
# -----------------------------------------------------------------------------
|
| 217 |
-
# TAB 2: AI GENERÁTOR NABÍDEK (
|
| 218 |
# -----------------------------------------------------------------------------
|
| 219 |
with tab_generator:
|
| 220 |
st.header(t["tab_gen"])
|
| 221 |
if is_locked:
|
| 222 |
st.error(t["paywall_msg"])
|
| 223 |
-
with st.form("stripe_pay_form"):
|
| 224 |
-
st.subheader("TradeFlow PRO Subscription Gateway")
|
| 225 |
-
st.write(f"Částka k úhradě: {format_price(29.00)} / měsíčně")
|
| 226 |
-
st.text_input("Card Number", value="4242 4242 4242 4242")
|
| 227 |
-
st.text_input("CVC", value="123")
|
| 228 |
-
if st.form_submit_button("Aktivovat neomezený účet PRO"):
|
| 229 |
-
st.session_state.user_subscribed = True
|
| 230 |
-
st.success("Platba proběhla úspěšně!")
|
| 231 |
-
time.sleep(0.5)
|
| 232 |
-
st.rerun()
|
| 233 |
else:
|
| 234 |
-
# Volba typu rozpočtu
|
| 235 |
quote_type = st.radio(
|
| 236 |
"Vyberte typ výstupu a rozpočtování:",
|
| 237 |
["🔮 Orientační odhad od AI (Nenahrává se do dlužníků/faktur, čistě orientační budget)",
|
|
@@ -244,22 +277,22 @@ with tab_generator:
|
|
| 244 |
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.")
|
| 245 |
|
| 246 |
st.markdown("### 🕒 Odpracované hodiny týmu na projektu")
|
| 247 |
-
st.caption("Systém automaticky načítá hodinové sazby z karty 'Správa Týmu'.")
|
| 248 |
|
| 249 |
-
# Simulace AI odhadu hodin, pokud si uživatel vyžádá pomoc AI
|
| 250 |
if st.button("🪄 Předvyplnit hodiny a materiál pomocí AI odhadu"):
|
| 251 |
st.session_state[f"hours_mock_triggered"] = True
|
| 252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
team_hours = {}
|
| 255 |
cols_team = st.columns(max(len(st.session_state.db_team), 1))
|
| 256 |
|
| 257 |
for idx, member in enumerate(st.session_state.db_team):
|
| 258 |
with cols_team[idx]:
|
| 259 |
-
# Přepočet sazby do aktuální měny pro zobrazení
|
| 260 |
-
local_rate = member["sazba"] * CURRENCIES[st.session_state.curr]["rate"]
|
| 261 |
-
|
| 262 |
-
# Výchozí hodiny na základě stisknutého tlačítka AI odhadu
|
| 263 |
default_hours = 0
|
| 264 |
if st.session_state.get(f"hours_mock_triggered", False):
|
| 265 |
if "director" in member["jmeno"].lower(): default_hours = 12
|
|
@@ -272,53 +305,92 @@ with tab_generator:
|
|
| 272 |
)
|
| 273 |
team_hours[member["id"]] = hours_input
|
| 274 |
|
| 275 |
-
|
| 276 |
-
|
|
|
|
|
|
|
| 277 |
curr_sym = CURRENCIES[st.session_state.curr]["symbol"]
|
| 278 |
curr_rate = CURRENCIES[st.session_state.curr]["rate"]
|
| 279 |
|
| 280 |
-
#
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
total_labor_eur = 0.0
|
| 294 |
for member in st.session_state.db_team:
|
| 295 |
total_labor_eur += team_hours[member["id"]] * member["sazba"]
|
| 296 |
|
| 297 |
-
total_extra_local =
|
| 298 |
total_extra_eur = total_extra_local / curr_rate
|
| 299 |
total_project_eur = total_labor_eur + total_extra_eur
|
| 300 |
|
| 301 |
-
st.markdown(f"### 📈 Celková kalkulace: {format_price(total_project_eur)}")
|
| 302 |
|
| 303 |
if st.button("💾 Uložit rozpočet do systému", type="primary"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
new_quote = {
|
| 305 |
"id": len(st.session_state.db_quotes) + 1,
|
| 306 |
"zakaznik": selected_cust,
|
| 307 |
"popis": input_task,
|
| 308 |
"celkem": total_project_eur,
|
| 309 |
"typ": "fixni" if is_fixed else "odhad",
|
| 310 |
-
"datum": time.strftime("%Y-%m-%d")
|
|
|
|
|
|
|
| 311 |
}
|
| 312 |
st.session_state.db_quotes.append(new_quote)
|
| 313 |
st.session_state.quotes_count += 1
|
| 314 |
st.success(f"Uloženo jako {'🎯 PŘESNÁ FIXNÍ NABÍDKA' if is_fixed else '🔮 ORIENTAČNÍ ODHAD'}")
|
| 315 |
-
|
|
|
|
| 316 |
st.session_state[f"hours_mock_triggered"] = False
|
|
|
|
| 317 |
time.sleep(0.5)
|
| 318 |
st.rerun()
|
| 319 |
|
| 320 |
# -----------------------------------------------------------------------------
|
| 321 |
-
# TAB 3:
|
| 322 |
# -----------------------------------------------------------------------------
|
| 323 |
with tab_team:
|
| 324 |
st.header("👥 Správa firemního týmu a hodinových sazeb")
|
|
@@ -340,7 +412,6 @@ with tab_team:
|
|
| 340 |
else:
|
| 341 |
st.warning("Tým nemá žádné členy. Přidejte lidi v pravém panelu.")
|
| 342 |
|
| 343 |
-
# Smazání člena týmu
|
| 344 |
st.markdown("**❌ Odebrat člena z týmu**")
|
| 345 |
if st.session_state.db_team:
|
| 346 |
member_to_remove = st.selectbox("Vyberte osobu k propuštění/odebrání", [m["jmeno"] for m in st.session_state.db_team])
|
|
@@ -354,17 +425,12 @@ with tab_team:
|
|
| 354 |
st.markdown("### ➕ Přidat / Upravit člena")
|
| 355 |
with st.form("add_team_member_form"):
|
| 356 |
tm_name = st.text_input("Jméno a příjmení / Role", placeholder="Např. Honza (Zedník)")
|
| 357 |
-
|
| 358 |
-
# Sazbu zadává uživatel v aktuálně zvolené měně dashboardu, systém ji interně přepočte do EUR základny
|
| 359 |
c_symbol = CURRENCIES[st.session_state.curr]["symbol"]
|
| 360 |
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)
|
| 361 |
|
| 362 |
if st.form_submit_button("Uložit do týmu"):
|
| 363 |
if tm_name:
|
| 364 |
-
# Přepočet zpět do interní EUR měny
|
| 365 |
rate_in_eur = tm_rate_local / CURRENCIES[st.session_state.curr]["rate"]
|
| 366 |
-
|
| 367 |
-
# Kontrola, zda už jméno neexistuje pro update, jinak insert
|
| 368 |
existing = next((m for m in st.session_state.db_team if m["jmeno"].lower() == tm_name.lower()), None)
|
| 369 |
if existing:
|
| 370 |
existing["sazba"] = rate_in_eur
|
|
@@ -375,8 +441,6 @@ with tab_team:
|
|
| 375 |
st.success(f"Člen {tm_name} byl úspěšně přidán do týmu.")
|
| 376 |
time.sleep(0.4)
|
| 377 |
st.rerun()
|
| 378 |
-
else:
|
| 379 |
-
st.error("Vyplňte jméno člena týmu.")
|
| 380 |
|
| 381 |
# -----------------------------------------------------------------------------
|
| 382 |
# TAB 4: CRM LITE
|
|
@@ -394,15 +458,13 @@ with tab_crm:
|
|
| 394 |
p = st.text_input(t["phone"])
|
| 395 |
if st.form_submit_button(t["save_cust"]) and n:
|
| 396 |
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})
|
| 397 |
-
st.success("
|
| 398 |
|
| 399 |
# -----------------------------------------------------------------------------
|
| 400 |
# TAB 5: FAKTURACE (POUZE PRO FIXNÍ NABÍDKY)
|
| 401 |
# -----------------------------------------------------------------------------
|
| 402 |
with tab_fakturace:
|
| 403 |
st.header(t["tab_inv"])
|
| 404 |
-
|
| 405 |
-
# FILTROVÁNÍ: Do výběru jdou VÝHRADNĚ fixní přesné nabídky! Odhady jsou skryty!
|
| 406 |
valid_quotes = [q for q in st.session_state.db_quotes if q.get("typ") == "fixni"]
|
| 407 |
|
| 408 |
if valid_quotes:
|
|
@@ -419,7 +481,7 @@ with tab_fakturace:
|
|
| 419 |
"stav": "Odesláno",
|
| 420 |
"datum": time.strftime("%Y-%m-%d")
|
| 421 |
})
|
| 422 |
-
st.success("Faktura vystavena a
|
| 423 |
st.rerun()
|
| 424 |
else:
|
| 425 |
st.info("V systému nejsou žádné hotové 'Přesné fixní nabídky' k fakturaci. (Orientační odhady nelze fakturovat).")
|
|
@@ -435,17 +497,12 @@ with tab_fakturace:
|
|
| 435 |
st.rerun()
|
| 436 |
|
| 437 |
# -----------------------------------------------------------------------------
|
| 438 |
-
# TAB 6: PRO BONUSY (AUTOMATIZOVANÉ SMS CENTRUM S
|
| 439 |
# -----------------------------------------------------------------------------
|
| 440 |
with tab_pro_features:
|
| 441 |
st.header(t["tab_pro"])
|
| 442 |
if not st.session_state.user_subscribed:
|
| 443 |
st.warning("🔒 Tato sekce obsahuje prémiové enterprise moduly dostupné pouze pro uživatele s aktivovaným tarifem TradeFlow PRO.")
|
| 444 |
-
st.markdown("""
|
| 445 |
-
### Co se zde nachází?
|
| 446 |
-
1. **AI Smart Logistics Router:** Spočítá nejoptimálnější trasu výjezdů mezi klienty pro maximální úsporu paliva a času.
|
| 447 |
-
2. **Pokročilé SMS Centrum dlužníků:** Kompletní přehled neplatičů, vyhledávání, pokročilé řazení pohledávek a správa vlastních SMS šablon s inteligentním párováním dat.
|
| 448 |
-
""")
|
| 449 |
else:
|
| 450 |
p_subtab1, p_subtab2 = st.tabs([t["pro_router"], t["pro_sms"]])
|
| 451 |
|
|
@@ -458,143 +515,79 @@ with tab_pro_features:
|
|
| 458 |
with p_subtab2:
|
| 459 |
st.subheader("📱 Pokročilé SMS Centrum upomínek")
|
| 460 |
|
| 461 |
-
# 1. FILTRACE A ŘAZENÍ NEPLATIČŮ
|
| 462 |
st.markdown("### 🔍 1. Vyhledání a seřazení dlužníků")
|
| 463 |
unpaid_invoices = [inv for inv in st.session_state.db_invoices if inv["stav"] == "Odesláno"]
|
| 464 |
|
| 465 |
sc1, sc2 = st.columns([1, 1])
|
| 466 |
with sc1:
|
| 467 |
-
search_query = st.text_input("Hledat dlužníka podle jména
|
| 468 |
with sc2:
|
| 469 |
-
sort_option = st.selectbox("Seřadit neplatiče podle
|
| 470 |
-
"Od A do Z", "Od Z do A",
|
| 471 |
-
"Od nejstarší pohledávky", "Od nejnovější pohledávky",
|
| 472 |
-
"Od nejvyšší částky", "Od nejnižší částky"
|
| 473 |
-
])
|
| 474 |
|
| 475 |
-
if
|
| 476 |
-
unpaid_invoices = [i for i in unpaid_invoices if search_query.lower() in i["zakaznik"].lower() or search_query in i["cislo"]]
|
| 477 |
-
|
| 478 |
-
if sort_option == "Od A do Z": unpaid_invoices.sort(key=lambda x: x["zakaznik"].lower())
|
| 479 |
-
elif sort_option == "Od Z do A": unpaid_invoices.sort(key=lambda x: x["zakaznik"].lower(), reverse=True)
|
| 480 |
-
elif sort_option == "Od nejstarší pohledávky": unpaid_invoices.sort(key=lambda x: x["datum"])
|
| 481 |
-
elif sort_option == "Od nejnovější pohledávky": unpaid_invoices.sort(key=lambda x: x["datum"], reverse=True)
|
| 482 |
-
elif sort_option == "Od nejvyšší částky": unpaid_invoices.sort(key=lambda x: x["castka"], reverse=True)
|
| 483 |
elif sort_option == "Od nejnižší částky": unpaid_invoices.sort(key=lambda x: x["castka"])
|
| 484 |
|
| 485 |
if not unpaid_invoices:
|
| 486 |
-
st.info("
|
| 487 |
chosen_debtor = None
|
| 488 |
else:
|
| 489 |
-
debtor_options = {f"👤 {inv['zakaznik']} (FA: {inv['cislo']} | Dluží: {format_price(inv['castka'])}
|
| 490 |
-
selected_debtor_label = st.selectbox("🎯 Vyberte dlužníka
|
| 491 |
chosen_debtor = debtor_options[selected_debtor_label]
|
| 492 |
|
| 493 |
st.markdown("---")
|
| 494 |
-
|
| 495 |
-
# 2. SPRÁVA PRESETŮ (CRUD + NOVÝ AI GENERÁTOR TEXTU PRESETŮ)
|
| 496 |
st.markdown("### 📋 2. Výběr a správa šablon (Presetů)")
|
| 497 |
|
| 498 |
preset_names = [p["název"] for p in st.session_state.sms_presets]
|
| 499 |
selected_preset_name = st.selectbox("Zvolte aktivní SMS šablonu", preset_names)
|
| 500 |
current_preset = next(p for p in st.session_state.sms_presets if p["název"] == selected_preset_name)
|
| 501 |
|
| 502 |
-
with st.expander("⚙️ Nastavení presetů & AI psaní šablon
|
| 503 |
-
st.write(f"Aktuální počet šablon: {len(st.session_state.sms_presets)} / 10")
|
| 504 |
-
|
| 505 |
-
# NOVÁ FUNKCE: AI GENERÁTOR TEXTU PRO PRESETY
|
| 506 |
st.markdown("🤖 **AI Asistent pro tvorbu textu šablony**")
|
| 507 |
-
ai_preset_prompt = st.text_input("Zadejte krátké zadání pro AI
|
| 508 |
|
| 509 |
if st.button("🪄 Vygenerovat text šablony pomocí AI"):
|
| 510 |
if ai_preset_prompt:
|
| 511 |
-
# Simulace komplexní generace na základě promptu klienta
|
| 512 |
p_lower = ai_preset_prompt.lower()
|
| 513 |
-
if "soud" in p_lower or "urg" in p_lower
|
| 514 |
-
generated_text = "DŮRAZNÁ VÝZVA: Vážený pane/paní {jmeno}.
|
| 515 |
-
elif "přátel" in p_lower or "kamarád" in p_lower or "mil" in p_lower:
|
| 516 |
-
generated_text = "Zdravím, {jmeno}! Jen přátelské echo, že se nám někde zatoulala platba za fakturu {cislo} ({castka}). Mrkneš na to prosím, až budeš mít chvilku? Díky moc!"
|
| 517 |
else:
|
| 518 |
-
generated_text = "
|
| 519 |
-
|
| 520 |
st.session_state["ai_generated_preset_text"] = generated_text
|
| 521 |
-
st.success("AI text úspěšně sestaven!
|
| 522 |
-
|
| 523 |
-
st.markdown("---")
|
| 524 |
-
st.markdown("**➕ Přidat nový preset**")
|
| 525 |
-
new_name = st.text_input("Název nové šablony", key="new_preset_name")
|
| 526 |
|
| 527 |
-
|
| 528 |
-
default_preset_body = st.session_state.get("ai_generated_preset_text", "Dobrý den, {jmeno}, ...
|
| 529 |
-
new_text = st.text_area("Text šablony
|
| 530 |
|
| 531 |
-
if st.button("Uložit novou šablonu
|
| 532 |
-
if
|
| 533 |
-
st.error("Dosaženo limitu maximálně 10 šablon.")
|
| 534 |
-
elif not new_name or not new_text:
|
| 535 |
-
st.error("Vyplňte název i text šablony.")
|
| 536 |
-
else:
|
| 537 |
st.session_state.sms_presets.append({"název": new_name, "text": new_text})
|
| 538 |
-
st.success("
|
| 539 |
-
if "ai_generated_preset_text" in st.session_state: del st.session_state["ai_generated_preset_text"]
|
| 540 |
-
st.rerun()
|
| 541 |
-
|
| 542 |
-
st.markdown("---")
|
| 543 |
-
st.markdown(f"**📝 Upravit aktuální šablonu: {selected_preset_name}**")
|
| 544 |
-
edited_text = st.text_area("Upravit tělo šablony", value=current_preset["text"], key="edit_preset_text")
|
| 545 |
-
if st.button("Uložit provedené změny"):
|
| 546 |
-
current_preset["text"] = edited_text
|
| 547 |
-
st.success("Změny uloženy!")
|
| 548 |
-
st.rerun()
|
| 549 |
-
|
| 550 |
-
st.markdown("---")
|
| 551 |
-
st.markdown(f"**❌ Odstranit šablonu: {selected_preset_name}**")
|
| 552 |
-
if st.button("Smazat vybranou šablonu z databáze", type="secondary"):
|
| 553 |
-
if len(st.session_state.sms_presets) <= 1:
|
| 554 |
-
st.error("Musíte v systému nechat alespoň jednu výchozí šablonu.")
|
| 555 |
-
else:
|
| 556 |
-
st.session_state.sms_presets = [p for p in st.session_state.sms_presets if p["název"] != selected_preset_name]
|
| 557 |
-
st.success("Šablona byla vymazána.")
|
| 558 |
st.rerun()
|
| 559 |
|
| 560 |
st.markdown("---")
|
| 561 |
-
|
| 562 |
-
# 3. GENERÁTOR VÝSLEDNÉ SMS (ZDE JE OPRAVA HNUSNÉHO FONTU A ZOBRAZENÍ Z OBRÁZKU)
|
| 563 |
st.markdown("### 📱 3. Finální vygenerovaná zpráva pro klienta")
|
| 564 |
if chosen_debtor:
|
| 565 |
raw_sms_template = current_preset["text"]
|
| 566 |
-
|
| 567 |
-
# Propojení reálných dat dlužníka do tagů v šabloně
|
| 568 |
compiled_sms = raw_sms_template.replace("{jmeno}", chosen_debtor["zakaznik"])\
|
| 569 |
.replace("{cislo}", chosen_debtor["cislo"])\
|
| 570 |
.replace("{castka}", format_price(chosen_debtor["castka"]))
|
| 571 |
|
| 572 |
-
# OPRAVA FONTU A ZALAMOVÁNÍ TEXTU (Zpětná vazba k obrázku image_dd77dc.png):
|
| 573 |
-
# Používáme čistý systémový bezpatkový font a vynucujeme zalamování slov pomocí CSS stylů.
|
| 574 |
st.markdown(f"""
|
| 575 |
<div style="
|
| 576 |
background-color: #f8f9fa;
|
| 577 |
border: 1px solid #dee2e6;
|
| 578 |
padding: 18px;
|
| 579 |
border-radius: 8px;
|
| 580 |
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
| 581 |
font-size: 16px;
|
| 582 |
color: #212529;
|
| 583 |
white-space: pre-wrap;
|
| 584 |
word-wrap: break-word;
|
| 585 |
-
line-height: 1.6;
|
| 586 |
-
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
|
| 587 |
margin-bottom: 15px;
|
| 588 |
">{compiled_sms}</div>
|
| 589 |
""", unsafe_allow_html=True)
|
| 590 |
|
| 591 |
-
|
| 592 |
-
# Použijeme pomocný schovaný text-area pro nativní kopírování, které Streamlit umí, ale upravený tak, aby netrpěl neduhem zkrácení
|
| 593 |
-
st.text_area("📋 Rychlá schránka pro zkopírování (Ctrl+A -> Ctrl+C):", value=compiled_sms, height=100, help="Klikněte dovnitř a zkopírujte text.")
|
| 594 |
-
|
| 595 |
-
if st.button("📲 Odeslat na mobilní telefon přes API bránu", type="primary", use_container_width=True):
|
| 596 |
-
with st.spinner("Odesílám zprávu dlužníkovi..."):
|
| 597 |
-
time.sleep(1.0)
|
| 598 |
-
st.success(f"SMS byla úspěšně předána operátorovi a odeslána na číslo klienta {chosen_debtor['zakaznik']}.")
|
| 599 |
-
else:
|
| 600 |
-
st.warning("Chcete-li sestavit upomínku, musíte mít v sekci 1 vybraného dlužníka (fakturu ve stavu Nezaplaceno).")
|
|
|
|
| 20 |
},
|
| 21 |
"SK": {
|
| 22 |
"title": "🏆 TradeFlow AI", "caption": "Chytry operačný systém pre novú generáciu remeselníkov.",
|
| 23 |
+
"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",
|
| 24 |
"profit": "Skutočný zisk", "pending": "Čakajúce peniaze", "potential": "Otvorené AI Ponuky", "limit": "Využitie Free limitu",
|
| 25 |
"cust_name": "Meno a priezvisko", "company": "Firma (Voliteľné)", "phone": "Telefónne číslo", "save_cust": "Uložiť klienta",
|
| 26 |
"generate_btn": "⚡ Vygenerovať/Uložiť rozpočet", "invoice_btn": "🧾 VYSTAVIŤ FAKTÚRU JEDNÝM KLIKNUTÍM",
|
|
|
|
| 35 |
"generate_btn": "⚡ Generate/Save Quote", "invoice_btn": "🧾 ISSUE 1-CLICK INVOICE",
|
| 36 |
"paywall_msg": "🔒 You have reached the 3 free quotes limit. Unlock PRO tier for unlimited access.",
|
| 37 |
"pro_router": "📍 AI Route & Logistics Planner", "pro_sms": "📲 Intelligent SMS Reminder Center"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
}
|
| 40 |
|
|
|
|
| 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 |
|
|
|
|
| 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},
|
|
|
|
| 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 |
]
|
|
|
|
| 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()
|
|
|
|
| 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
|
|
|
|
| 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!")
|
|
|
|
| 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 |
|
|
|
|
| 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"])
|
|
|
|
| 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)
|
|
|
|
| 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"])
|
| 265 |
if is_locked:
|
| 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)",
|
|
|
|
| 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
|
|
|
|
| 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")
|
|
|
|
| 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])
|
|
|
|
| 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
|
|
|
|
| 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
|
|
|
|
| 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:
|
|
|
|
| 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).")
|
|
|
|
| 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 |
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|