Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -2,39 +2,513 @@ import streamlit as st
|
|
| 2 |
import pandas as pd
|
| 3 |
import time
|
| 4 |
import urllib.parse
|
| 5 |
-
import stripe # Připraveno pro splnění
|
| 6 |
import streamlit.components.v1 as components # Bezpečný import pro HTML komponenty na Hugging Face
|
| 7 |
|
| 8 |
# -----------------------------------------------------------------------------
|
| 9 |
-
# JAZYKOVÉ LOKALIZACE (CZ, SK, EN)
|
| 10 |
# -----------------------------------------------------------------------------
|
| 11 |
TRANSLATIONS = {
|
| 12 |
"CZ": {
|
| 13 |
-
|
| 14 |
-
"
|
| 15 |
-
"
|
| 16 |
-
"
|
| 17 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
"paywall_msg": "🔒 Dosáhli jste limitu 3 bezplatných nabídek. Pro neomezený přístup aktivujte tarif PRO.",
|
| 19 |
-
"pro_router": "📍 AI Plánovač tras a logistiky",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
},
|
| 21 |
"SK": {
|
| 22 |
-
|
| 23 |
-
"
|
| 24 |
-
"
|
| 25 |
-
"
|
| 26 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
"paywall_msg": "🔒 Dosiahli ste limitu 3 bezplatných ponúk. Pre neobmedzený prístup aktivujte tarif PRO.",
|
| 28 |
-
"pro_router": "📍 AI Plánovač trás a logistiky",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
},
|
| 30 |
"EN": {
|
| 31 |
-
|
| 32 |
-
"
|
| 33 |
-
"
|
| 34 |
-
"
|
| 35 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
"paywall_msg": "🔒 You have reached the 3 free quotes limit. Unlock PRO tier for unlimited access.",
|
| 37 |
-
"pro_router": "📍 AI Route & Logistics Planner",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
}
|
| 40 |
|
|
@@ -47,9 +521,9 @@ CURRENCIES = {
|
|
| 47 |
}
|
| 48 |
|
| 49 |
# -----------------------------------------------------------------------------
|
| 50 |
-
# INICIALIZACE STAVU
|
| 51 |
# -----------------------------------------------------------------------------
|
| 52 |
-
VERSION = "2.1.
|
| 53 |
|
| 54 |
st.set_page_config(page_title="TradeFlow AI", page_icon="🏆", layout="wide", initial_sidebar_state="expanded")
|
| 55 |
|
|
@@ -59,7 +533,6 @@ if "admin_authenticated" not in st.session_state: st.session_state.admin_authent
|
|
| 59 |
if "user_subscribed" not in st.session_state: st.session_state.user_subscribed = False
|
| 60 |
if "quotes_count" not in st.session_state: st.session_state.quotes_count = 0
|
| 61 |
|
| 62 |
-
# Ostré databáze startují 100% čisté a prázdné
|
| 63 |
if "db_team" not in st.session_state: st.session_state.db_team = []
|
| 64 |
if "db_customers" not in st.session_state: st.session_state.db_customers = []
|
| 65 |
if "db_quotes" not in st.session_state: st.session_state.db_quotes = []
|
|
@@ -72,7 +545,8 @@ if "sms_presets" not in st.session_state:
|
|
| 72 |
{"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!"}
|
| 73 |
]
|
| 74 |
|
| 75 |
-
|
|
|
|
| 76 |
if "cost_items_df" not in st.session_state:
|
| 77 |
st.session_state.cost_items_df = pd.DataFrame([
|
| 78 |
{"Sekce nákladu": "Materiál", "Název položky / Popis": "", "Částka": 0.0}
|
|
@@ -85,36 +559,37 @@ def format_price(amount_in_eur):
|
|
| 85 |
return f"{converted:,.2f} {c_info['symbol']}"
|
| 86 |
return f"{c_info['symbol']}{converted:,.2f}"
|
| 87 |
|
| 88 |
-
t = TRANSLATIONS[st.session_state.lang if st.session_state.lang in TRANSLATIONS else "CZ"]
|
| 89 |
-
|
| 90 |
# -----------------------------------------------------------------------------
|
| 91 |
-
# SIDEBAR:
|
| 92 |
# -----------------------------------------------------------------------------
|
| 93 |
with st.sidebar:
|
| 94 |
-
st.header("
|
| 95 |
-
st.session_state.lang = st.selectbox("
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
st.markdown("---")
|
| 99 |
-
st.header("
|
| 100 |
|
| 101 |
if not st.session_state.admin_authenticated:
|
| 102 |
-
pwd_input = st.text_input("
|
| 103 |
-
if st.button("
|
| 104 |
if pwd_input == "admin123":
|
| 105 |
st.session_state.admin_authenticated = True
|
| 106 |
-
st.success("
|
| 107 |
st.rerun()
|
| 108 |
else:
|
| 109 |
-
st.error("
|
| 110 |
else:
|
| 111 |
-
st.info("
|
| 112 |
-
st.session_state.user_subscribed = st.toggle("
|
| 113 |
-
st.session_state.quotes_count = st.number_input("
|
| 114 |
|
| 115 |
-
st.markdown("###
|
| 116 |
-
if st.button("
|
| 117 |
-
# Demo struktura týmu se nahraje až teď!
|
| 118 |
st.session_state.db_team = [
|
| 119 |
{"id": 1, "jmeno": "Šimon (Director)", "sazba": 45.0},
|
| 120 |
{"id": 2, "jmeno": "Nikola (Prezentace & Podpora)", "sazba": 30.0},
|
|
@@ -147,23 +622,23 @@ with st.sidebar:
|
|
| 147 |
{"id": 4, "cislo": "20260004", "zakaznik": "Jan Novák", "castka": 5010.00, "stav": "Odesláno", "datum": "2026-06-15"}
|
| 148 |
]
|
| 149 |
st.session_state.quotes_count = 1
|
| 150 |
-
st.toast("
|
| 151 |
time.sleep(0.3)
|
| 152 |
st.rerun()
|
| 153 |
|
| 154 |
-
if st.button("
|
| 155 |
st.session_state.db_team = []
|
| 156 |
st.session_state.db_customers = []
|
| 157 |
st.session_state.db_quotes = []
|
| 158 |
st.session_state.db_invoices = []
|
| 159 |
st.session_state.quotes_count = 0
|
| 160 |
st.session_state.cost_items_df = pd.DataFrame([{"Sekce nákladu": "Materiál", "Název položky / Popis": "", "Částka": 0.0}])
|
| 161 |
-
st.toast("
|
| 162 |
time.sleep(0.3)
|
| 163 |
st.rerun()
|
| 164 |
|
| 165 |
st.markdown("---")
|
| 166 |
-
if st.button("
|
| 167 |
st.session_state.admin_authenticated = False
|
| 168 |
st.rerun()
|
| 169 |
|
|
@@ -177,7 +652,7 @@ tab_dashboard, tab_generator, tab_team, tab_crm, tab_fakturace, tab_pro_features
|
|
| 177 |
])
|
| 178 |
|
| 179 |
# -----------------------------------------------------------------------------
|
| 180 |
-
# TAB 1:
|
| 181 |
# -----------------------------------------------------------------------------
|
| 182 |
with tab_dashboard:
|
| 183 |
st.header(t["tab_dash"])
|
|
@@ -188,30 +663,30 @@ with tab_dashboard:
|
|
| 188 |
c1, c2, c3, c4 = st.columns(4)
|
| 189 |
c1.metric(t["profit"], format_price(total_revenue))
|
| 190 |
c2.metric(t["pending"], format_price(pending_revenue))
|
| 191 |
-
c3.metric(t["potential"], format_price(potencial), help="
|
| 192 |
-
c4.metric(t["limit"], f"{st.session_state.quotes_count} / 3", delta="
|
| 193 |
|
| 194 |
st.markdown("---")
|
| 195 |
-
st.subheader("
|
| 196 |
-
st.write("
|
| 197 |
|
| 198 |
flat_costs = []
|
| 199 |
for q in st.session_state.db_quotes:
|
| 200 |
if q.get("labor_eur", 0) > 0:
|
| 201 |
flat_costs.append({
|
| 202 |
-
"
|
| 203 |
-
"
|
| 204 |
-
"
|
| 205 |
"Částka (Základ v EUR)": q["labor_eur"],
|
| 206 |
-
"
|
| 207 |
})
|
| 208 |
for ec in q.get("extra_costs", []):
|
| 209 |
flat_costs.append({
|
| 210 |
-
"
|
| 211 |
-
"
|
| 212 |
-
"
|
| 213 |
"Částka (Základ v EUR)": ec["eur"],
|
| 214 |
-
"
|
| 215 |
})
|
| 216 |
|
| 217 |
if flat_costs:
|
|
@@ -219,83 +694,86 @@ with tab_dashboard:
|
|
| 219 |
|
| 220 |
f_col1, f_col2 = st.columns(2)
|
| 221 |
with f_col1:
|
| 222 |
-
available_cats = ["
|
| 223 |
-
selected_cat_filter = st.selectbox("
|
| 224 |
with f_col2:
|
| 225 |
-
selected_type_filter = st.radio("
|
| 226 |
|
| 227 |
-
if selected_cat_filter != "
|
| 228 |
-
df_costs = df_costs[df_costs["
|
| 229 |
|
| 230 |
-
if selected_type_filter == "
|
| 231 |
-
df_costs = df_costs[df_costs["
|
| 232 |
-
elif selected_type_filter == "
|
| 233 |
-
df_costs = df_costs[df_costs["
|
| 234 |
|
| 235 |
if not df_costs.empty:
|
| 236 |
total_filtered_eur = df_costs["Částka (Základ v EUR)"].sum()
|
| 237 |
-
st.markdown(f"####
|
| 238 |
|
| 239 |
df_costs_display = df_costs.copy()
|
| 240 |
-
df_costs_display["
|
| 241 |
df_costs_display = df_costs_display.drop(columns=["Částka (Základ v EUR)"])
|
| 242 |
st.dataframe(df_costs_display, use_container_width=True, hide_index=True)
|
| 243 |
else:
|
| 244 |
-
st.info("
|
| 245 |
else:
|
| 246 |
-
st.info("
|
| 247 |
|
| 248 |
st.markdown("---")
|
| 249 |
-
st.subheader("
|
| 250 |
if st.session_state.db_quotes:
|
| 251 |
df_raw = pd.DataFrame(st.session_state.db_quotes)
|
| 252 |
df_display = df_raw.copy()
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
else:
|
| 257 |
-
st.info("
|
| 258 |
|
| 259 |
# -----------------------------------------------------------------------------
|
| 260 |
-
# TAB 2: AI GENERÁTOR (
|
| 261 |
# -----------------------------------------------------------------------------
|
| 262 |
with tab_generator:
|
| 263 |
st.header(t["tab_gen"])
|
| 264 |
if is_locked:
|
| 265 |
st.error(t["paywall_msg"])
|
| 266 |
else:
|
| 267 |
-
quote_type = st.radio(
|
| 268 |
-
|
| 269 |
-
["🔮 Orientační odhad od AI (Čistě orientační kalkulace, nepropisuje se do dlužníků)",
|
| 270 |
-
"🎯 Přesná fixní nabídka (Závazný finanční plán, slouží jako podklad pro fakturu)"]
|
| 271 |
-
)
|
| 272 |
-
is_fixed = "Přesná" in quote_type
|
| 273 |
|
| 274 |
-
st.markdown("
|
| 275 |
cust_names = [c["jmeno"] for c in st.session_state.db_customers]
|
| 276 |
|
| 277 |
-
client_mode = st.radio("
|
| 278 |
-
if client_mode == "
|
| 279 |
-
selected_cust = st.selectbox("
|
| 280 |
else:
|
| 281 |
-
selected_cust = st.text_input("
|
| 282 |
|
| 283 |
-
input_task = st.text_area("
|
| 284 |
|
| 285 |
-
st.markdown("
|
| 286 |
|
| 287 |
if not st.session_state.db_team:
|
| 288 |
-
st.info("
|
| 289 |
team_hours = {}
|
| 290 |
else:
|
| 291 |
-
if st.button("
|
| 292 |
st.session_state["ai_mock_active"] = True
|
| 293 |
st.session_state.cost_items_df = pd.DataFrame([
|
| 294 |
{"Sekce nákladu": "Materiál", "Název položky / Popis": "AI Odhad: Potrubí, ventily a fitinky", "Částka": 480.0},
|
| 295 |
-
{"Sekce nákladu": "Benzín / Doprava", "Název položky / Popis": "AI Odhad: Nákladní závoz materiálu
|
| 296 |
{"Sekce nákladu": "Pronájem vybavení", "Název položky / Popis": "AI Odhad: Zapůjčení tlakové zkoušečky", "Částka": 110.0}
|
| 297 |
])
|
| 298 |
-
st.toast("
|
| 299 |
|
| 300 |
team_hours = {}
|
| 301 |
cols_team = st.columns(len(st.session_state.db_team))
|
|
@@ -314,20 +792,20 @@ with tab_generator:
|
|
| 314 |
)
|
| 315 |
team_hours[member["id"]] = hours_input
|
| 316 |
|
| 317 |
-
st.markdown("
|
| 318 |
-
st.caption("
|
| 319 |
|
| 320 |
-
curr_sym = CURRENCIES[st.session_state.curr]["symbol"]
|
| 321 |
curr_rate = CURRENCIES[st.session_state.curr]["rate"]
|
| 322 |
|
|
|
|
| 323 |
column_configuration = {
|
| 324 |
"Sekce nákladu": st.column_config.SelectboxColumn(
|
| 325 |
-
"
|
| 326 |
-
options=["Materiál", "Benzín / Doprava", "Pronájem vybavení", "Ostatní režie"],
|
| 327 |
required=True
|
| 328 |
),
|
| 329 |
-
"Název položky / Popis": st.column_config.TextColumn("
|
| 330 |
-
"Částka": st.column_config.NumberColumn(
|
| 331 |
}
|
| 332 |
|
| 333 |
edited_df = st.data_editor(
|
|
@@ -339,8 +817,7 @@ with tab_generator:
|
|
| 339 |
)
|
| 340 |
st.session_state.cost_items_df = edited_df
|
| 341 |
|
| 342 |
-
|
| 343 |
-
st.markdown("### 📂 Hromadný import externích nákladů přes CSV")
|
| 344 |
|
| 345 |
def zpracuj_csv_import():
|
| 346 |
if st.session_state.csv_uploader_key is not None:
|
|
@@ -351,21 +828,19 @@ with tab_generator:
|
|
| 351 |
sanitized_df = uploaded_df[needed_headers].copy()
|
| 352 |
sanitized_df["Částka"] = pd.to_numeric(sanitized_df["Částka"], errors='coerce').fillna(0.0)
|
| 353 |
|
| 354 |
-
# Bezpečné připojení dat do existující tabulky v session_state
|
| 355 |
st.session_state.cost_items_df = pd.concat([st.session_state.cost_items_df, sanitized_df], ignore_index=True)
|
| 356 |
-
st.toast("
|
| 357 |
else:
|
| 358 |
-
st.error("
|
| 359 |
except Exception as e:
|
| 360 |
-
st.error(f"
|
| 361 |
|
| 362 |
st.file_uploader(
|
| 363 |
-
"
|
| 364 |
type=["csv"],
|
| 365 |
key="csv_uploader_key",
|
| 366 |
-
on_change=zpracuj_csv_import
|
| 367 |
)
|
| 368 |
-
# --------------------------------------------------------
|
| 369 |
|
| 370 |
st.markdown("---")
|
| 371 |
|
|
@@ -377,18 +852,18 @@ with tab_generator:
|
|
| 377 |
total_extra_eur = total_extra_local / curr_rate
|
| 378 |
total_project_eur = total_labor_eur + total_extra_eur
|
| 379 |
|
| 380 |
-
st.markdown(f"
|
| 381 |
|
| 382 |
-
if st.button("
|
| 383 |
if not selected_cust or selected_cust.strip() == "":
|
| 384 |
-
st.error("
|
| 385 |
else:
|
| 386 |
if selected_cust not in cust_names:
|
| 387 |
st.session_state.db_customers.append({
|
| 388 |
"id": len(st.session_state.db_customers) + 1,
|
| 389 |
"jmeno": selected_cust.strip(),
|
| 390 |
-
"firma": "Fyzická osoba
|
| 391 |
-
"telefon": "Nezadáno"
|
| 392 |
})
|
| 393 |
|
| 394 |
saved_extra_costs = []
|
|
@@ -415,7 +890,7 @@ with tab_generator:
|
|
| 415 |
st.session_state["ai_mock_active"] = False
|
| 416 |
|
| 417 |
st.session_state.cost_items_df = pd.DataFrame([{"Sekce nákladu": "Materiál", "Název položky / Popis": "", "Částka": 0.0}])
|
| 418 |
-
st.success("
|
| 419 |
time.sleep(0.4)
|
| 420 |
st.rerun()
|
| 421 |
|
|
@@ -423,52 +898,51 @@ with tab_generator:
|
|
| 423 |
# TAB 3: SPRÁVA TÝMU
|
| 424 |
# -----------------------------------------------------------------------------
|
| 425 |
with tab_team:
|
| 426 |
-
st.header("
|
| 427 |
-
st.write("
|
| 428 |
|
| 429 |
t_col1, t_col2 = st.columns([2, 1])
|
| 430 |
|
| 431 |
with t_col1:
|
| 432 |
-
st.markdown("###
|
| 433 |
team_display_list = []
|
| 434 |
for m in st.session_state.db_team:
|
| 435 |
team_display_list.append({
|
| 436 |
-
"
|
| 437 |
-
"
|
| 438 |
-
"
|
| 439 |
})
|
| 440 |
if team_display_list:
|
| 441 |
st.dataframe(pd.DataFrame(team_display_list), use_container_width=True, hide_index=True)
|
| 442 |
else:
|
| 443 |
-
st.info("
|
| 444 |
|
| 445 |
-
st.markdown("
|
| 446 |
if st.session_state.db_team:
|
| 447 |
-
member_to_remove = st.selectbox("
|
| 448 |
-
if st.button("
|
| 449 |
st.session_state.db_team = [m for m in st.session_state.db_team if m["jmeno"] != member_to_remove]
|
| 450 |
-
st.success(f"
|
| 451 |
time.sleep(0.3)
|
| 452 |
st.rerun()
|
| 453 |
|
| 454 |
with t_col2:
|
| 455 |
-
st.markdown("
|
| 456 |
with st.form("team_management_form"):
|
| 457 |
-
new_tm_name = st.text_input("
|
| 458 |
-
|
| 459 |
-
new_tm_rate = st.number_input(f"Hodinová sazba v aktivní měně ({local_symbol}/h)", min_value=1, value=400 if st.session_state.curr == "CZK" else 20)
|
| 460 |
|
| 461 |
-
if st.form_submit_button("
|
| 462 |
if new_tm_name:
|
| 463 |
rate_in_eur = new_tm_rate / CURRENCIES[st.session_state.curr]["rate"]
|
| 464 |
matched_member = next((m for m in st.session_state.db_team if m["jmeno"].lower() == new_tm_name.lower()), None)
|
| 465 |
if matched_member:
|
| 466 |
matched_member["sazba"] = rate_in_eur
|
| 467 |
-
st.success(
|
| 468 |
else:
|
| 469 |
next_id = max([m["id"] for m in st.session_state.db_team]) + 1 if st.session_state.db_team else 1
|
| 470 |
st.session_state.db_team.append({"id": next_id, "jmeno": new_tm_name, "sazba": rate_in_eur})
|
| 471 |
-
st.success(
|
| 472 |
time.sleep(0.3)
|
| 473 |
st.rerun()
|
| 474 |
|
|
@@ -476,15 +950,21 @@ with tab_team:
|
|
| 476 |
# TAB 4: CRM ADRESÁŘ
|
| 477 |
# -----------------------------------------------------------------------------
|
| 478 |
with tab_crm:
|
| 479 |
-
st.header(t["
|
| 480 |
col_crm1, col_crm2 = st.columns([2, 1])
|
| 481 |
with col_crm1:
|
| 482 |
if st.session_state.db_customers:
|
| 483 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
else:
|
| 485 |
-
st.info("
|
| 486 |
with col_crm2:
|
| 487 |
-
st.markdown("
|
| 488 |
with st.form("manual_crm_form"):
|
| 489 |
c_n = st.text_input(t["cust_name"], placeholder="Jan Novák")
|
| 490 |
c_f = st.text_input(t["company"], placeholder="Stavebniny s.r.o.")
|
|
@@ -494,23 +974,25 @@ with tab_crm:
|
|
| 494 |
st.session_state.db_customers.append({
|
| 495 |
"id": len(st.session_state.db_customers) + 1,
|
| 496 |
"jmeno": c_n,
|
| 497 |
-
"firma": c_f if c_f else "Fyzická osoba",
|
| 498 |
"telefon": c_p
|
| 499 |
})
|
| 500 |
-
st.success("
|
|
|
|
|
|
|
| 501 |
else:
|
| 502 |
-
st.error("
|
| 503 |
|
| 504 |
# -----------------------------------------------------------------------------
|
| 505 |
# TAB 5: FAKTURACE
|
| 506 |
# -----------------------------------------------------------------------------
|
| 507 |
with tab_fakturace:
|
| 508 |
-
st.header(t["
|
| 509 |
only_fixni_quotes = [q for q in st.session_state.db_quotes if q.get("typ") == "fixni"]
|
| 510 |
|
| 511 |
if only_fixni_quotes:
|
| 512 |
-
invoice_options = {f"
|
| 513 |
-
selected_quote_to_inv = st.selectbox("
|
| 514 |
targeted_quote = invoice_options[selected_quote_to_inv]
|
| 515 |
|
| 516 |
if st.button(t["invoice_btn"], type="primary", use_container_width=True):
|
|
@@ -522,30 +1004,30 @@ with tab_fakturace:
|
|
| 522 |
"stav": "Odesláno",
|
| 523 |
"datum": time.strftime("%Y-%m-%d")
|
| 524 |
})
|
| 525 |
-
st.success("
|
| 526 |
time.sleep(0.3)
|
| 527 |
st.rerun()
|
| 528 |
else:
|
| 529 |
-
st.info("
|
| 530 |
|
| 531 |
st.markdown("---")
|
| 532 |
-
st.subheader("
|
| 533 |
|
| 534 |
if st.session_state.db_invoices:
|
| 535 |
for index_inv, invoice_obj in enumerate(st.session_state.db_invoices):
|
| 536 |
div_c1, div_c2, div_c3 = st.columns([2, 1, 1])
|
| 537 |
-
div_c1.write(f"🧾 **
|
| 538 |
|
| 539 |
if invoice_obj["stav"] == "Zaplaceno":
|
| 540 |
-
div_c2.markdown("<span style='color:green; font-weight:bold;'>
|
| 541 |
div_c3.write("")
|
| 542 |
else:
|
| 543 |
-
div_c2.markdown("<span style='color:orange; font-weight:bold;'>
|
| 544 |
-
if div_c3.button("
|
| 545 |
invoice_obj["stav"] = "Zaplaceno"
|
| 546 |
st.rerun()
|
| 547 |
else:
|
| 548 |
-
st.info("
|
| 549 |
|
| 550 |
# -----------------------------------------------------------------------------
|
| 551 |
# TAB 6: PRO BONUSY
|
|
@@ -553,78 +1035,77 @@ with tab_fakturace:
|
|
| 553 |
with tab_pro_features:
|
| 554 |
st.header(t["tab_pro"])
|
| 555 |
if not st.session_state.user_subscribed:
|
| 556 |
-
st.warning("
|
| 557 |
else:
|
| 558 |
pro_sub1, pro_sub2 = st.tabs([t["pro_router"], t["pro_sms"]])
|
| 559 |
|
| 560 |
with pro_sub1:
|
| 561 |
-
st.subheader("
|
| 562 |
-
st.text_area("
|
| 563 |
-
if st.button("
|
| 564 |
-
st.info("
|
| 565 |
|
| 566 |
with pro_sub2:
|
| 567 |
-
st.subheader("
|
| 568 |
-
|
| 569 |
-
st.markdown("### 🔍 1. Vyhledání dlužníka v systému")
|
| 570 |
open_debts = [invoice for invoice in st.session_state.db_invoices if invoice["stav"] == "Odesláno"]
|
| 571 |
|
| 572 |
search_col1, search_col2 = st.columns([1, 1])
|
| 573 |
with search_col1:
|
| 574 |
-
query_name = st.text_input("
|
| 575 |
with search_col2:
|
| 576 |
-
sorting_order = st.selectbox("
|
| 577 |
|
| 578 |
if query_name:
|
| 579 |
open_debts = [d for d in open_debts if query_name.lower() in d["zakaznik"].lower()]
|
| 580 |
|
| 581 |
-
if sorting_order == "
|
| 582 |
open_debts.sort(key=lambda x: x["castka"], reverse=True)
|
| 583 |
else:
|
| 584 |
open_debts.sort(key=lambda x: x["castka"])
|
| 585 |
|
| 586 |
if not open_debts:
|
| 587 |
-
st.info("
|
| 588 |
active_debtor = None
|
| 589 |
else:
|
| 590 |
debtor_mapping = {f"👤 {d['zakaznik']} (Faktura č. {d['cislo']} | Dluh: {format_price(d['castka'])})": d for d in open_debts}
|
| 591 |
-
selected_debtor_string = st.selectbox("
|
| 592 |
active_debtor = debtor_mapping[selected_debtor_string]
|
| 593 |
|
| 594 |
st.markdown("---")
|
| 595 |
-
st.markdown("
|
| 596 |
|
| 597 |
preset_labels = [p["název"] for p in st.session_state.sms_presets]
|
| 598 |
-
chosen_preset_label = st.selectbox("
|
| 599 |
active_preset = next(p for p in st.session_state.sms_presets if p["název"] == chosen_preset_label)
|
| 600 |
|
| 601 |
-
with st.expander("
|
| 602 |
-
st.markdown("
|
| 603 |
-
prompt_text = st.text_input("
|
| 604 |
|
| 605 |
-
if st.button("
|
| 606 |
if prompt_text:
|
| 607 |
p_low = prompt_text.lower()
|
| 608 |
if "předžal" in p_low or "naléh" in p_low or "soud" in p_low:
|
| 609 |
-
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
|
| 610 |
else:
|
| 611 |
-
result_sms = "Dobrý den {jmeno}, připomínáme zapomenutou fakturu č. {cislo} na částku {castka}. Prosíme o co nejrychlejší proplacení.
|
| 612 |
st.session_state["ai_sms_output_buffer"] = result_sms
|
| 613 |
-
st.success("
|
| 614 |
|
| 615 |
-
new_preset_title = st.text_input("
|
| 616 |
default_body = st.session_state.get("ai_sms_output_buffer", "Vážený pane/paní {jmeno}, ...")
|
| 617 |
-
new_preset_body = st.text_area("
|
| 618 |
|
| 619 |
-
if st.button("
|
| 620 |
if new_preset_title and new_preset_body:
|
| 621 |
st.session_state.sms_presets.append({"název": new_preset_title, "text": new_preset_body})
|
| 622 |
-
st.success("
|
| 623 |
time.sleep(0.3)
|
| 624 |
st.rerun()
|
| 625 |
|
| 626 |
st.markdown("---")
|
| 627 |
-
st.markdown("
|
| 628 |
|
| 629 |
if active_debtor:
|
| 630 |
raw_template = active_preset["text"]
|
|
@@ -638,14 +1119,14 @@ with tab_pro_features:
|
|
| 638 |
border: 1px solid #dee2e6;
|
| 639 |
padding: 18px;
|
| 640 |
border-radius: 8px;
|
| 641 |
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
| 642 |
font-size: 15px;
|
| 643 |
color: #212529;
|
| 644 |
white-space: pre-wrap;
|
| 645 |
word-wrap: break-word;
|
| 646 |
line-height: 1.6;
|
| 647 |
margin-bottom: 12px;
|
| 648 |
-
;"
|
| 649 |
""", unsafe_allow_html=True)
|
| 650 |
|
| 651 |
safe_encoded_sms = urllib.parse.quote(final_compiled_sms)
|
|
@@ -654,9 +1135,9 @@ with tab_pro_features:
|
|
| 654 |
<button onclick="
|
| 655 |
navigator.clipboard.writeText(decodeURIComponent('{safe_encoded_sms}'));
|
| 656 |
let btn = document.getElementById('js_copy_btn');
|
| 657 |
-
btn.innerText = '
|
| 658 |
btn.style.backgroundColor = '#28a745';
|
| 659 |
-
setTimeout(() => {{ btn.innerText = '
|
| 660 |
" id="js_copy_btn" style="
|
| 661 |
background-color: #007bff;
|
| 662 |
color: white;
|
|
@@ -665,13 +1146,10 @@ with tab_pro_features:
|
|
| 665 |
font-size: 14px;
|
| 666 |
border-radius: 6px;
|
| 667 |
cursor: pointer;
|
| 668 |
-
font-family: -apple-system, sans-serif;
|
| 669 |
font-weight: bold;
|
| 670 |
width: 100%;
|
| 671 |
-
|
| 672 |
-
transition: background-color 0.2s;
|
| 673 |
-
">📋 Kopírovat text zprávy</button>
|
| 674 |
"""
|
| 675 |
components.html(copy_button_html, height=50)
|
| 676 |
else:
|
| 677 |
-
st.info("
|
|
|
|
| 2 |
import pandas as pd
|
| 3 |
import time
|
| 4 |
import urllib.parse
|
| 5 |
+
import stripe # Připraveno pro splnění požadavků v requirements.txt
|
| 6 |
import streamlit.components.v1 as components # Bezpečný import pro HTML komponenty na Hugging Face
|
| 7 |
|
| 8 |
# -----------------------------------------------------------------------------
|
| 9 |
+
# KOMPLETNÍ JAZYKOVÉ LOKALIZACE PRO VŠECHNY PRVKY NA STRÁNCE (CZ, SK, EN)
|
| 10 |
# -----------------------------------------------------------------------------
|
| 11 |
TRANSLATIONS = {
|
| 12 |
"CZ": {
|
| 13 |
+
# Obecné a záložky
|
| 14 |
+
"title": "🏆 TradeFlow AI",
|
| 15 |
+
"caption": "Chytrý operační systém pro novou generaci řemeslníků.",
|
| 16 |
+
"tab_dash": "📈 Přehled & Metriky",
|
| 17 |
+
"tab_gen": "📝 AI Generátor Nabídek",
|
| 18 |
+
"tab_team": "👥 Správa Týmu",
|
| 19 |
+
"tab_crm": "👥 Adresář (CRM)",
|
| 20 |
+
"tab_inv": "🧾 Fakturace jedním klikem",
|
| 21 |
+
"tab_pro": "🚀 PRO Exkluzivní Funkce",
|
| 22 |
+
|
| 23 |
+
# Sidebar & Admin Panel
|
| 24 |
+
"sb_settings": "🌐 Nastavení",
|
| 25 |
+
"sb_lang": "Jazyk aplikace",
|
| 26 |
+
"sb_curr": "Aktivní měna",
|
| 27 |
+
"sb_dev_panel": "🛡️ Dev Admin Panel",
|
| 28 |
+
"sb_pwd_label": "Zadejte administrátorské heslo",
|
| 29 |
+
"sb_pwd_hint": "Heslo pro testování je: admin123",
|
| 30 |
+
"sb_unlock_btn": "Odemknout Admin Panel",
|
| 31 |
+
"sb_auth_ok": "Přístup povolen!",
|
| 32 |
+
"sb_auth_err": "Nesprávné heslo!",
|
| 33 |
+
"sb_auth_status": "🔓 Vývojářská autorizace",
|
| 34 |
+
"sb_pro_toggle": "Uživatel má aktivní PRO tarif",
|
| 35 |
+
"sb_quotes_count": "Počet vytvořených nabídek",
|
| 36 |
+
"sb_demo_section": "🧪 Správa ukázkových dat",
|
| 37 |
+
"sb_import_demo": "📥 Importovat komplexní Demo Data",
|
| 38 |
+
"sb_clear_demo": "🗑️ Kompletní vyčištění databází",
|
| 39 |
+
"sb_logout": "Zamknout panel & Odhlásit se",
|
| 40 |
+
"toast_demo_ok": "Systém byl úspěšně naplněn cvičnými daty!",
|
| 41 |
+
"toast_clear_ok": "Aplikace byla kompletně vyčištěna.",
|
| 42 |
+
|
| 43 |
+
# Dashboard Metriky
|
| 44 |
+
"profit": "Skutečný zisk",
|
| 45 |
+
"pending": "Čekající peníze",
|
| 46 |
+
"potential": "Otevřené AI Nabídky",
|
| 47 |
+
"limit": "Využití Free limitu",
|
| 48 |
+
"dash_help_potential": "Počítá pouze Přesné fixní nabídky.",
|
| 49 |
+
"dash_limit_warn": "⚠️ LIMIT UKONČEN",
|
| 50 |
+
|
| 51 |
+
# Dashboard Analýza
|
| 52 |
+
"dash_analysis_title": "📊 Pokročilá analýza a finanční filtrování sekcí",
|
| 53 |
+
"dash_analysis_desc": "Sledujte finanční toky seskupené podle konkrétních nákladových sekcí napříč celou firmou.",
|
| 54 |
+
"dash_filter_cat": "Zvolte nákladovou kategorii k odfiltrování:",
|
| 55 |
+
"dash_filter_type": "Filtrovat typ zakázky:",
|
| 56 |
+
"dash_filter_all_cats": "Zobrazit vše",
|
| 57 |
+
"dash_type_all": "Všechny rozpočty",
|
| 58 |
+
"dash_type_fixed": "Pouze závazné přesné nabídky",
|
| 59 |
+
"dash_type_estimated": "Pouze orientační odhady",
|
| 60 |
+
"dash_aggregated_spend": "💰 Agregovaná útrata ve zvoleném filtru:",
|
| 61 |
+
"dash_no_records": "Pro zadaná kritéria a filtry nebyly nalezeny žádné záznamy.",
|
| 62 |
+
"dash_no_costs_yet": "V systému zatím nejsou evidovány žádné podrobné položky nákladů.",
|
| 63 |
+
"dash_book_title": "Kniha vytvořených kalkulací a nabídek",
|
| 64 |
+
"dash_book_empty": "Kniha nabídek je prázdná.",
|
| 65 |
+
|
| 66 |
+
# Sloupce tabulek v Dashboardu
|
| 67 |
+
"col_project": "Projekt / Klient",
|
| 68 |
+
"col_section": "Sekce nákladu",
|
| 69 |
+
"col_item_name": "Název položky / Popis",
|
| 70 |
+
"col_amount": "Částka",
|
| 71 |
+
"col_type": "Typ rozpočtu",
|
| 72 |
+
"col_date": "Datum",
|
| 73 |
+
|
| 74 |
+
# AI Generátor Nabídek
|
| 75 |
+
"gen_type_label": "Vyberte povahu výstupu a nacenění:",
|
| 76 |
+
"gen_type_est": "🔮 Orientační odhad od AI (Čistě orientační kalkulace, nepropisuje se do dlužníků)",
|
| 77 |
+
"gen_type_fix": "🎯 Přesná fixní nabídka (Závazný finanční planar, slouží jako podklad pro fakturu)",
|
| 78 |
+
"gen_client_section": "### 👤 Přiřazení klienta",
|
| 79 |
+
"gen_client_mode": "Způsob zadání zákazníka:",
|
| 80 |
+
"gen_client_crm": "Vybrat z adresáře CRM",
|
| 81 |
+
"gen_client_manual": "✍️ Zadat úplně nového klienta ručně",
|
| 82 |
+
"gen_select_client": "Zvolte klienta:",
|
| 83 |
+
"gen_input_client_name": "Zadejte jméno a příjmení nového klienta:",
|
| 84 |
+
"gen_client_placeholder": "Např. František Novotný",
|
| 85 |
+
"gen_spec_label": "Detailní specifikace projektu a zadání pro AI",
|
| 86 |
+
"gen_spec_default": "Kompletní rekonstrukce rodinného domu, rozvody vody a finální zapojení sanitární techniky.",
|
| 87 |
+
"gen_team_alloc": "### 🕒 Personální alokace (Odpracované hodiny týmu)",
|
| 88 |
+
"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ě.",
|
| 89 |
+
"gen_ai_btn": "🪄 Spustit AI simulaci a předvyplnit položky i hodiny",
|
| 90 |
+
"gen_ai_toast": "AI úspěšně zanalyzovala text a navrhla optimální strukturu nákladů!",
|
| 91 |
+
"gen_direct_costs": "### 🚚 Přímé materiálové a vedlejší náklady",
|
| 92 |
+
"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.",
|
| 93 |
+
"gen_csv_title": "### 📂 Hromadný import externích nákladů přes CSV",
|
| 94 |
+
"gen_csv_uploader": "Nahrajte soubor .csv (Struktura sloupců: Sekce nákladu, Název položky / Popis, Částka)",
|
| 95 |
+
"gen_csv_success": "CSV data byla úspěšně nahrána do tabulky výše!",
|
| 96 |
+
"gen_csv_err_headers": "Chyba: CSV neodpovídá požadované struktuře záhlaví.",
|
| 97 |
+
"gen_csv_err_read": "Soubor nelze přečíst:",
|
| 98 |
+
"gen_total_calc": "### 📈 Celková kalkulace rozpočtu:",
|
| 99 |
+
"gen_save_btn": "💾 Schválit a uložit rozpočet do systému",
|
| 100 |
+
"gen_err_no_client": "Chyba: Před uložením musíte uvést nebo vybrat jméno klienta!",
|
| 101 |
+
"gen_save_success": "Rozpočet byl úspěšně zanesen do firemní databáze.",
|
| 102 |
+
|
| 103 |
+
# Správa týmu
|
| 104 |
+
"team_title": "👥 Správa firemního týmu a mzdových sazeb",
|
| 105 |
+
"team_desc": "Editujte profily a interní hodinové sazby svých pracovníků.",
|
| 106 |
+
"team_sub_list": "Evidovaní pracovníci a specialisté",
|
| 107 |
+
"team_col_id": "Interní ID",
|
| 108 |
+
"team_col_role": "Jméno člena / Profesní role",
|
| 109 |
+
"team_col_rate": "Hodinová mzdová sazba",
|
| 110 |
+
"team_empty": "V týmu se aktuálně nenachází žádný pracovník. Použijte formulář vpravo nebo načtěte Demo data.",
|
| 111 |
+
"team_remove_section": "**❌ Odstranění pracovníka z týmu**",
|
| 112 |
+
"team_remove_select": "Vyberte osobu k odebrání:",
|
| 113 |
+
"team_remove_btn": "Definitivně odebrat člena",
|
| 114 |
+
"team_remove_toast": "Pracovník byl vyřazen z interní databáze.",
|
| 115 |
+
"team_add_section": "### ➕ Přidat / Upravit kartu pracovníka",
|
| 116 |
+
"team_input_name": "Jméno příjmení / Pracovní zařazení",
|
| 117 |
+
"team_input_placeholder": "Např. Kamil (Zedník)",
|
| 118 |
+
"team_input_rate": "Hodinová sazba v aktivní měně",
|
| 119 |
+
"team_submit_btn": "Uložit profil",
|
| 120 |
+
"team_update_toast": "Sazba u pracovníka byla zaktualizována.",
|
| 121 |
+
"team_add_toast": "Nový člen byl úspěšně zaevidován.",
|
| 122 |
+
|
| 123 |
+
# CRM Adresář
|
| 124 |
+
"crm_title": "👥 Adresář zákazníků (CRM)",
|
| 125 |
+
"crm_empty": "Adresář klientů je momentálně prázdný.",
|
| 126 |
+
"crm_add_section": "### ➕ Rychlé ruční založení klienta",
|
| 127 |
+
"cust_name": "Jméno a příjmení",
|
| 128 |
+
"company": "Firma (Volitelné)",
|
| 129 |
+
"phone": "Telefonní číslo",
|
| 130 |
+
"save_cust": "Uložit klienta",
|
| 131 |
+
"crm_err_name": "Jméno klienta je povinné pole.",
|
| 132 |
+
"crm_success_toast": "Klient byl bezpečně uložen do CRM.",
|
| 133 |
+
|
| 134 |
+
# Fakturace
|
| 135 |
+
"inv_title": "🧾 Fakturace jedním klikem",
|
| 136 |
+
"inv_select_quote": "Zvolte schválený pevný rozpočet k okamžité fakturaci:",
|
| 137 |
+
"invoice_btn": "🧾 VYSTAVIT FAKTURU JEDNÍM KLIKNUTÍM",
|
| 138 |
+
"inv_success_toast": "Faktura byla vystavena a uložena do knihy!",
|
| 139 |
+
"inv_no_fixed_quotes": "V systému se nenachází žádná schválená pevná nabídka (Orientační odhady nelze fakturovat).",
|
| 140 |
+
"inv_book_title": "Kompletní přehled vydaných faktur a stavu plateb",
|
| 141 |
+
"inv_status_paid": "🟢 Uhrazeno",
|
| 142 |
+
"inv_status_sent": "🔵 Odesláno (Po splatnosti)",
|
| 143 |
+
"inv_mark_paid": "Označit jako ZAPLACENO",
|
| 144 |
+
"inv_empty": "Kniha faktur je prázdná.",
|
| 145 |
+
|
| 146 |
+
# PRO Funkce
|
| 147 |
"paywall_msg": "🔒 Dosáhli jste limitu 3 bezplatných nabídek. Pro neomezený přístup aktivujte tarif PRO.",
|
| 148 |
+
"pro_router": "📍 AI Plánovač tras a logistiky",
|
| 149 |
+
"pro_sms": "📲 Inteligentní SMS Centrum upomínek",
|
| 150 |
+
"pro_locked_warn": "🔒 Tento panel obsahuje pokročilé moduly TradeFlow PRO. Aktivujte zkušebn�� verzi v Dev Admin Panelu na boční liště.",
|
| 151 |
+
"pro_route_title": "📍 AI Geografický optimalizátor tras výjezdů",
|
| 152 |
+
"pro_route_label": "Seznam adres plánovaných zásahů",
|
| 153 |
+
"pro_route_btn": "🗺️ Spočítat matematicky nejkratší trasu",
|
| 154 |
+
"pro_route_result": "🚀 AI Výsledek kalkulace: Výjezd z Plzně -> Beroun -> Praha. Celková úspora paliva a logistických nákladů činí: 42 km.",
|
| 155 |
+
"pro_sms_title": "📱 Inteligentní manažerské SMS centrum pohledávek",
|
| 156 |
+
"pro_sms_step1": "### 🔍 1. Vyhledání dlužníka v systému",
|
| 157 |
+
"pro_sms_filter_name": "Filtrovat neplatiče dle jména:",
|
| 158 |
+
"pro_sms_sort_label": "Seřadit dlužníky dle kritéria:",
|
| 159 |
+
"pro_sms_sort_high": "Od nejvyšší dlužné částky",
|
| 160 |
+
"pro_sms_sort_low": "Od nejnižší dlužné částky",
|
| 161 |
+
"pro_sms_no_debts": "V systému aktuálně neevidujeme žádné neuhrazené faktury odpovídající filtrům.",
|
| 162 |
+
"pro_sms_select_debtor": "🎯 Vyberte cílového dlužníka k obeslání:",
|
| 163 |
+
"pro_sms_step2": "### 📋 2. Výběr textové šablony",
|
| 164 |
+
"pro_sms_template_label": "Zvolte aktivní komunikační šablonu:",
|
| 165 |
+
"pro_sms_expander": "⚙️ Pokročilý editor šablon & Generátor nových textů přes AI",
|
| 166 |
+
"pro_sms_ai_assistant": "🤖 **AI Asistent pro tvorbu tónu textu**",
|
| 167 |
+
"pro_sms_ai_prompt": "Zadejte zadání pro AI (např. 'Napiš velmi naléhavou předžalobní výzvu')",
|
| 168 |
+
"pro_sms_ai_btn": "🪄 Sestavit novou šablonu pomocí AI",
|
| 169 |
+
"pro_sms_ai_success": "AI text úspěšně vygenerován do pole níže!",
|
| 170 |
+
"pro_sms_new_title": "Název nové šablony:",
|
| 171 |
+
"pro_sms_new_body": "Text těla zprávy (používejte značky {jmeno}, {cislo}, {castka})",
|
| 172 |
+
"pro_sms_save_template_btn": "Uložit šablonu do trvalého seznamu",
|
| 173 |
+
"pro_sms_template_success": "Šablona úspěšně uložena!",
|
| 174 |
+
"pro_sms_step3": "### 📱 3. Finální vygenerovaná zpráva pro klienta",
|
| 175 |
+
"pro_sms_copy_btn": "📋 Kopírovat text zprávy",
|
| 176 |
+
"pro_sms_copy_ok": "✅ Zkopírováno do schránky!",
|
| 177 |
+
"pro_sms_select_info": "Vyberte dlužníka pro zobrazení a zkopírování SMS zprávy."
|
| 178 |
},
|
| 179 |
"SK": {
|
| 180 |
+
# Obecné a záložky
|
| 181 |
+
"title": "🏆 TradeFlow AI",
|
| 182 |
+
"caption": "Chytrý operačný systém pre novú generáciu remeselníkov.",
|
| 183 |
+
"tab_dash": "📈 Prehľad & Metriky",
|
| 184 |
+
"tab_gen": "📝 AI Generátor Ponúk",
|
| 185 |
+
"tab_team": "👥 Správa Tímu",
|
| 186 |
+
"tab_crm": "👥 Adresár (CRM)",
|
| 187 |
+
"tab_inv": "🧾 Fakturácia jedným klikom",
|
| 188 |
+
"tab_pro": "🚀 PRO Exkluzívne Funkcie",
|
| 189 |
+
|
| 190 |
+
# Sidebar & Admin Panel
|
| 191 |
+
"sb_settings": "🌐 Nastavenia",
|
| 192 |
+
"sb_lang": "Jazyk aplikácie",
|
| 193 |
+
"sb_curr": "Aktívna mena",
|
| 194 |
+
"sb_dev_panel": "🛡️ Dev Admin Panel",
|
| 195 |
+
"sb_pwd_label": "Zadajte administrátorské heslo",
|
| 196 |
+
"sb_pwd_hint": "Heslo pre testovanie je: admin123",
|
| 197 |
+
"sb_unlock_btn": "Odomknúť Admin Panel",
|
| 198 |
+
"sb_auth_ok": "Prístup povolený!",
|
| 199 |
+
"sb_auth_err": "Nesprávne heslo!",
|
| 200 |
+
"sb_auth_status": "🔓 Vývojárska autorizácia",
|
| 201 |
+
"sb_pro_toggle": "Užívateľ má aktívny PRO tarif",
|
| 202 |
+
"sb_quotes_count": "Počet vytvorených ponúk",
|
| 203 |
+
"sb_demo_section": "🧪 Správa ukážkových dát",
|
| 204 |
+
"sb_import_demo": "📥 Importovať komplexné Demo Dáta",
|
| 205 |
+
"sb_clear_demo": "🗑️ Kompletné vyčistenie databáz",
|
| 206 |
+
"sb_logout": "Zamknúť panel & Odhlásiť sa",
|
| 207 |
+
"toast_demo_ok": "Systém bol úspešne naplnený cvičnými dátami!",
|
| 208 |
+
"toast_clear_ok": "Aplikácia bola kompletne vyčistená.",
|
| 209 |
+
|
| 210 |
+
# Dashboard Metriky
|
| 211 |
+
"profit": "Skutočný zisk",
|
| 212 |
+
"pending": "Čakajúce peniaze",
|
| 213 |
+
"potential": "Otvorené AI Ponuky",
|
| 214 |
+
"limit": "Využitie Free limitu",
|
| 215 |
+
"dash_help_potential": "Počíta iba Presné fixné ponuky.",
|
| 216 |
+
"dash_limit_warn": "⚠️ LIMIT UKONČENÝ",
|
| 217 |
+
|
| 218 |
+
# Dashboard Analýza
|
| 219 |
+
"dash_analysis_title": "📊 Pokročilá analýza a finančné filtrovanie sekcií",
|
| 220 |
+
"dash_analysis_desc": "Sledujte finančné toky zoskupené podľa konkrétnych nákladových sekcií naprieč celou firmou.",
|
| 221 |
+
"dash_filter_cat": "Zvoľte nákladovú kategóriu na odfiltrovanie:",
|
| 222 |
+
"dash_filter_type": "Filtrovať typ zákazky:",
|
| 223 |
+
"dash_filter_all_cats": "Zobraziť všetko",
|
| 224 |
+
"dash_type_all": "Všetky rozpočty",
|
| 225 |
+
"dash_type_fixed": "Iba záväzné presné ponuky",
|
| 226 |
+
"dash_type_estimated": "Iba orientačné odhady",
|
| 227 |
+
"dash_aggregated_spend": "💰 Agregovaná útrata vo zvolenom filtri:",
|
| 228 |
+
"dash_no_records": "Pre zadané kritériá a filtre neboli nájdené žiadne záznamy.",
|
| 229 |
+
"dash_no_costs_yet": "V systéme zatiaľ nie sú evidované žiadne podrobné položky nákladov.",
|
| 230 |
+
"dash_book_title": "Kniha vytvorených kalkulácií a ponúk",
|
| 231 |
+
"dash_book_empty": "Kniha ponúk je prázdna.",
|
| 232 |
+
|
| 233 |
+
# Sloupce tabulek v Dashboardu
|
| 234 |
+
"col_project": "Projekt / Klient",
|
| 235 |
+
"col_section": "Sekcia nákladu",
|
| 236 |
+
"col_item_name": "Názov položky / Popis",
|
| 237 |
+
"col_amount": "Čiastka",
|
| 238 |
+
"col_type": "Typ rozpočtu",
|
| 239 |
+
"col_date": "Dátum",
|
| 240 |
+
|
| 241 |
+
# AI Generátor Nabídek
|
| 242 |
+
"gen_type_label": "Vyberte povahu výstupu a nacenenie:",
|
| 243 |
+
"gen_type_est": "🔮 Orientačný odhad od AI (Čisto orientačná kalkulácia, neprepisuje sa do dlžníkov)",
|
| 244 |
+
"gen_type_fix": "🎯 Presná fixná ponuka (Záväzný finančný plán, slúži ako podklad pre faktúru)",
|
| 245 |
+
"gen_client_section": "### 👤 Priradenie klienta",
|
| 246 |
+
"gen_client_mode": "Spôsob zadania zákazníka:",
|
| 247 |
+
"gen_client_crm": "Vybrať z adresára CRM",
|
| 248 |
+
"gen_client_manual": "✍️ Zadať úplne nového klienta ručne",
|
| 249 |
+
"gen_select_client": "Zvoľte klienta:",
|
| 250 |
+
"gen_input_client_name": "Zadajte meno a priezvisko nového klienta:",
|
| 251 |
+
"gen_client_placeholder": "Napr. František Novotný",
|
| 252 |
+
"gen_spec_label": "Detailná špecifikácia projektu a zadanie pre AI",
|
| 253 |
+
"gen_spec_default": "Kompletná rekonštrukcia rodinného domu, rozvody vody a finálne zapojenie sanitárnej techniky.",
|
| 254 |
+
"gen_team_alloc": "### 🕒 Personálna alokácia (Odpracované hodiny tímu)",
|
| 255 |
+
"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.",
|
| 256 |
+
"gen_ai_btn": "🪄 Spustiť AI simuláciu a predvyplniť položky aj hodiny",
|
| 257 |
+
"gen_ai_toast": "AI úspešne zanalyzovala text a navrhla optimálnu štruktúru nákladov!",
|
| 258 |
+
"gen_direct_costs": "### 🚚 Priame materiálové a vedľajšie náklady",
|
| 259 |
+
"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.",
|
| 260 |
+
"gen_csv_title": "### 📂 Hromadný import externých nákladov cez CSV",
|
| 261 |
+
"gen_csv_uploader": "Nahrajte súbor .csv (Štruktúra stĺpcov: Sekcia nákladu, Název položky / Popis, Částka)",
|
| 262 |
+
"gen_csv_success": "CSV dáta boli úspešne nahrané do tabuľky vyššie!",
|
| 263 |
+
"gen_csv_err_headers": "Chyba: CSV neodpovedá požadovanej štruktúre záhlavia.",
|
| 264 |
+
"gen_csv_err_read": "Súbor nie je možné prečítať:",
|
| 265 |
+
"gen_total_calc": "### 📈 Celková kalkulácia rozpočtu:",
|
| 266 |
+
"gen_save_btn": "💾 Schváliť a uložiť rozpočet do systému",
|
| 267 |
+
"gen_err_no_client": "Chyba: Pred uložením musíte uviesť alebo vybrať meno klienta!",
|
| 268 |
+
"gen_save_success": "Rozpočet bol úspešne zanesený do firemnej databázy.",
|
| 269 |
+
|
| 270 |
+
# Správa týmu
|
| 271 |
+
"team_title": "👥 Správa firemného tímu a mzdových sadzieb",
|
| 272 |
+
"team_desc": "Editujte profily a interné hodinové sadzby svojich pracovníkov.",
|
| 273 |
+
"team_sub_list": "Evidovaní pracovníci a špecialisti",
|
| 274 |
+
"team_col_id": "Interné ID",
|
| 275 |
+
"team_col_role": "Meno člena / Profesijná rola",
|
| 276 |
+
"team_col_rate": "Hodinová mzdová sadzba",
|
| 277 |
+
"team_empty": "V tíme sa aktuálne nenachádza žiadny pracovník. Použite formulár vpravo alebo načítajte Demo dáta.",
|
| 278 |
+
"team_remove_section": "**❌ Odstránenie pracovníka z tímu**",
|
| 279 |
+
"team_remove_select": "Vyberte osobu na odobratie:",
|
| 280 |
+
"team_remove_btn": "Definitívne odobrať člena",
|
| 281 |
+
"team_remove_toast": "Pracovník bol vyradený z internej databázy.",
|
| 282 |
+
"team_add_section": "### ➕ Pridať / Upraviť kartu pracovníka",
|
| 283 |
+
"team_input_name": "Meno priezvisko / Pracovné zaradenie",
|
| 284 |
+
"team_input_placeholder": "Napr. Kamil (Murár)",
|
| 285 |
+
"team_input_rate": "Hodinová sadzba v aktívnej mene",
|
| 286 |
+
"team_submit_btn": "Uložiť profil",
|
| 287 |
+
"team_update_toast": "Sadzba u pracovníka bola zaktualizovaná.",
|
| 288 |
+
"team_add_toast": "Nový člen bol úspešne zaevidovaný.",
|
| 289 |
+
|
| 290 |
+
# CRM Adresář
|
| 291 |
+
"crm_title": "👥 Adresár zákazníkov (CRM)",
|
| 292 |
+
"crm_empty": "Adresár klientov je momentálne prázdny.",
|
| 293 |
+
"crm_add_section": "### ➕ Rýchle ručné založenie klienta",
|
| 294 |
+
"cust_name": "Meno a priezvisko",
|
| 295 |
+
"company": "Firma (Voliteľné)",
|
| 296 |
+
"phone": "Telefónne číslo",
|
| 297 |
+
"save_cust": "Uložiť klienta",
|
| 298 |
+
"crm_err_name": "Meno klienta je povinné pole.",
|
| 299 |
+
"crm_success_toast": "Klient bol bezpečne uložený do CRM.",
|
| 300 |
+
|
| 301 |
+
# Fakturace
|
| 302 |
+
"inv_title": "🧾 Fakturácia jedným klikom",
|
| 303 |
+
"inv_select_quote": "Zvoľte schválený pevný rozpočet k okamžitej fakturácii:",
|
| 304 |
+
"invoice_btn": "🧾 VYSTAVIŤ FAKTÚRU JEDNÝM KLIKNUTÍM",
|
| 305 |
+
"inv_success_toast": "Faktúra bola vystavená a uložená do knihy!",
|
| 306 |
+
"inv_no_fixed_quotes": "V systéme sa nenachádza žiadna schválená pevná ponuka (Orientačné odhady nie je možné fakturovať).",
|
| 307 |
+
"inv_book_title": "Kompletný prehľad vydaných faktúr a stavu platieb",
|
| 308 |
+
"inv_status_paid": "🟢 Uhradené",
|
| 309 |
+
"inv_status_sent": "🔵 Odoslané (Po splatnosti)",
|
| 310 |
+
"inv_mark_paid": "Označiť ako ZAPLATENÉ",
|
| 311 |
+
"inv_empty": "Kniha faktúr je prázdna.",
|
| 312 |
+
|
| 313 |
+
# PRO Funkce
|
| 314 |
"paywall_msg": "🔒 Dosiahli ste limitu 3 bezplatných ponúk. Pre neobmedzený prístup aktivujte tarif PRO.",
|
| 315 |
+
"pro_router": "📍 AI Plánovač trás a logistiky",
|
| 316 |
+
"pro_sms": "📲 Inteligentné SMS Centrum upomienok",
|
| 317 |
+
"pro_locked_warn": "🔒 Tento panel obsahuje pokročilé moduly TradeFlow PRO. Aktivujte skúšobnú verziu v Dev Admin Panelu na bočnej lište.",
|
| 318 |
+
"pro_route_title": "📍 AI Geografický optimalizátor trás výjazdov",
|
| 319 |
+
"pro_route_label": "Zoznam adries plánovaných zásahov",
|
| 320 |
+
"pro_route_btn": "🗺️ Spočítať matematicky najkratšiu trasu",
|
| 321 |
+
"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.",
|
| 322 |
+
"pro_sms_title": "📱 Inteligentné manažérske SMS centrum pohľadávok",
|
| 323 |
+
"pro_sms_step1": "### 🔍 1. Vyhľadanie dlžníka v systéme",
|
| 324 |
+
"pro_sms_filter_name": "Filtrovať neplatičov podľa mena:",
|
| 325 |
+
"pro_sms_sort_label": "Zoradiť dlžníkov podľa kritéria:",
|
| 326 |
+
"pro_sms_sort_high": "Od najvyššej dlžnej sumy",
|
| 327 |
+
"pro_sms_sort_low": "Od najnižšej dlžnej sumy",
|
| 328 |
+
"pro_sms_no_debts": "V systéme aktuálne neevidujeme žiadne neuhradené faktúry zodpovedajúce filtrom.",
|
| 329 |
+
"pro_sms_select_debtor": "🎯 Vyberte cieľového dlžníka na obeslanie:",
|
| 330 |
+
"pro_sms_step2": "### 📋 2. Výber textovej šablóny",
|
| 331 |
+
"pro_sms_template_label": "Zvoľte aktívnu komunikačnú šablónu:",
|
| 332 |
+
"pro_sms_expander": "⚙️ Pokročilý editor šablón & Generátor nových textov cez AI",
|
| 333 |
+
"pro_sms_ai_assistant": "🤖 **AI Asistent pre tvorbu tónu textu**",
|
| 334 |
+
"pro_sms_ai_prompt": "Zadajte zadanie pre AI (napr. 'Napíš veľmi naliehavú predžalobnú výzvu')",
|
| 335 |
+
"pro_sms_ai_btn": "🪄 Zostaviť novú šablónu pomocou AI",
|
| 336 |
+
"pro_sms_ai_success": "AI text úspešne vygenerovaný do poľa nižšie!",
|
| 337 |
+
"pro_sms_new_title": "Názov novej šablóny:",
|
| 338 |
+
"pro_sms_new_body": "Text tela správy (používajte značky {jmeno}, {cislo}, {castka})",
|
| 339 |
+
"pro_sms_save_template_btn": "Uložiť šablónu do trvalého zoznamu",
|
| 340 |
+
"pro_sms_template_success": "Šablóna úspešne uložená!",
|
| 341 |
+
"pro_sms_step3": "### 📱 3. Finálna vygenerovaná správa pre klienta",
|
| 342 |
+
"pro_sms_copy_btn": "📋 Kopírovať text správy",
|
| 343 |
+
"pro_sms_copy_ok": "✅ Skopírované do schránky!",
|
| 344 |
+
"pro_sms_select_info": "Vyberte dlžníka pre zobrazenie a skopírovanie SMS správy."
|
| 345 |
},
|
| 346 |
"EN": {
|
| 347 |
+
# Obecné a záložky
|
| 348 |
+
"title": "🏆 TradeFlow AI",
|
| 349 |
+
"caption": "Smart operating system for the next-gen skilled trades.",
|
| 350 |
+
"tab_dash": "📈 Dashboard & Metrics",
|
| 351 |
+
"tab_gen": "📝 AI Quote Generator",
|
| 352 |
+
"tab_team": "👥 Team Management",
|
| 353 |
+
"tab_crm": "👥 Contacts (CRM)",
|
| 354 |
+
"tab_inv": "🧾 1-Click Invoicing",
|
| 355 |
+
"tab_pro": "🚀 PRO Features",
|
| 356 |
+
|
| 357 |
+
# Sidebar & Admin Panel
|
| 358 |
+
"sb_settings": "🌐 Settings",
|
| 359 |
+
"sb_lang": "App Language",
|
| 360 |
+
"sb_curr": "Active Currency",
|
| 361 |
+
"sb_dev_panel": "🛡️ Dev Admin Panel",
|
| 362 |
+
"sb_pwd_label": "Enter Admin Password",
|
| 363 |
+
"sb_pwd_hint": "Testing password is: admin123",
|
| 364 |
+
"sb_unlock_btn": "Unlock Admin Panel",
|
| 365 |
+
"sb_auth_ok": "Access Granted!",
|
| 366 |
+
"sb_auth_err": "Incorrect Password!",
|
| 367 |
+
"sb_auth_status": "🔓 Developer Authorization",
|
| 368 |
+
"sb_pro_toggle": "User has active PRO tier",
|
| 369 |
+
"sb_quotes_count": "Created quotes counter",
|
| 370 |
+
"sb_demo_section": "🧪 Sample Data Management",
|
| 371 |
+
"sb_import_demo": "📥 Import Complex Demo Data",
|
| 372 |
+
"sb_clear_demo": "🗑️ Complete Databases Wipe",
|
| 373 |
+
"sb_logout": "Lock Panel & Log Out",
|
| 374 |
+
"toast_demo_ok": "System filled with sample data successfully!",
|
| 375 |
+
"toast_clear_ok": "Application databases wiped completely.",
|
| 376 |
+
|
| 377 |
+
# Dashboard Metriky
|
| 378 |
+
"profit": "Actual Profit",
|
| 379 |
+
"pending": "Pending Revenue",
|
| 380 |
+
"potential": "Open AI Quotes",
|
| 381 |
+
"limit": "Free Tier Usage",
|
| 382 |
+
"dash_help_potential": "Counts strict Fixed Price quotes only.",
|
| 383 |
+
"dash_limit_warn": "⚠️ LIMIT EXCEEDED",
|
| 384 |
+
|
| 385 |
+
# Dashboard Analýza
|
| 386 |
+
"dash_analysis_title": "📊 Advanced Section Analysis & Financial Filtering",
|
| 387 |
+
"dash_analysis_desc": "Monitor financial flows grouped by specific cost sections across the entire company.",
|
| 388 |
+
"dash_filter_cat": "Select cost category to filter by:",
|
| 389 |
+
"dash_filter_type": "Filter project type:",
|
| 390 |
+
"dash_filter_all_cats": "Show all",
|
| 391 |
+
"dash_type_all": "All budgets",
|
| 392 |
+
"dash_type_fixed": "Binding fixed quotes only",
|
| 393 |
+
"dash_type_estimated": "Rough estimates only",
|
| 394 |
+
"dash_aggregated_spend": "💰 Aggregated spend in selected filter:",
|
| 395 |
+
"dash_no_records": "No records found matching the given criteria and filters.",
|
| 396 |
+
"dash_no_costs_yet": "No itemized cost entries are registered in the system yet.",
|
| 397 |
+
"dash_book_title": "Log of Created Calculations & Quotes",
|
| 398 |
+
"dash_book_empty": "The quotes logbook is currently empty.",
|
| 399 |
+
|
| 400 |
+
# Sloupce tabulek v Dashboardu
|
| 401 |
+
"col_project": "Project / Client",
|
| 402 |
+
"col_section": "Cost Section",
|
| 403 |
+
"col_item_name": "Item Name / Description",
|
| 404 |
+
"col_amount": "Amount",
|
| 405 |
+
"col_type": "Budget Type",
|
| 406 |
+
"col_date": "Date",
|
| 407 |
+
|
| 408 |
+
# AI Generátor Nabídek
|
| 409 |
+
"gen_type_label": "Select output scope and pricing strategy:",
|
| 410 |
+
"gen_type_est": "🔮 Rough AI Estimate (Purely indicative calculation, does not map into debtors ledger)",
|
| 411 |
+
"gen_type_fix": "🎯 Binding Fixed Quote (Strict financial plan, serves as an invoice baseline)",
|
| 412 |
+
"gen_client_section": "### 👤 Client Assignment",
|
| 413 |
+
"gen_client_mode": "Customer input method:",
|
| 414 |
+
"gen_client_crm": "Select from CRM address book",
|
| 415 |
+
"gen_client_manual": "✍️ Enter brand new client manually",
|
| 416 |
+
"gen_select_client": "Choose client:",
|
| 417 |
+
"gen_input_client_name": "Enter full name of the new client:",
|
| 418 |
+
"gen_client_placeholder": "E.g., John Smith",
|
| 419 |
+
"gen_spec_label": "Detailed project specifications and instructions for AI",
|
| 420 |
+
"gen_spec_default": "Complete residential house reconstruction, plumbing pipelines layout and final sanitary alignment.",
|
| 421 |
+
"gen_team_alloc": "### 🕒 Workforce Allocation (Team hours worked)",
|
| 422 |
+
"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.",
|
| 423 |
+
"gen_ai_btn": "🪄 Execute AI Simulation and pre-fill rows & hours",
|
| 424 |
+
"gen_ai_toast": "AI successfully analyzed text and proposed optimal cost structure!",
|
| 425 |
+
"gen_direct_costs": "### 🚚 Direct Material & Secondary Expenses",
|
| 426 |
+
"gen_table_hint": "💡 Add new rows using the `+` icon at the bottom. Delete rows by selecting them and pressing `Delete` on your keyboard.",
|
| 427 |
+
"gen_csv_title": "### 📂 Bulk External Costs CSV Import",
|
| 428 |
+
"gen_csv_uploader": "Upload a .csv file (Required column layout: Sekce nákladu, Název položky / Popis, Částka)",
|
| 429 |
+
"gen_csv_success": "CSV data successfully mapped into the table above!",
|
| 430 |
+
"gen_csv_err_headers": "Error: CSV does not match the required headers layout.",
|
| 431 |
+
"gen_csv_err_read": "File cannot be parsed:",
|
| 432 |
+
"gen_total_calc": "### 📈 Total Budget Calculation:",
|
| 433 |
+
"gen_save_btn": "💾 Approve and record budget into the system",
|
| 434 |
+
"gen_err_no_client": "Error: You must state or select a client name before saving!",
|
| 435 |
+
"gen_save_success": "Budget was successfully recorded into company database.",
|
| 436 |
+
|
| 437 |
+
# Správa týmu
|
| 438 |
+
"team_title": "👥 Internal Team & Hourly Sazby Management",
|
| 439 |
+
"team_desc": "Edit profiles and corporate hourly rates of your specialists.",
|
| 440 |
+
"team_sub_list": "Registered Workers & Specialists",
|
| 441 |
+
"team_col_id": "Internal ID",
|
| 442 |
+
"team_col_role": "Member Name / Professional Role",
|
| 443 |
+
"team_col_rate": "Hourly Wage Rate",
|
| 444 |
+
"team_empty": "No workers inside the team database. Use the form on the right or turn on Demo Data.",
|
| 445 |
+
"team_remove_section": "**❌ Exclude worker from team**",
|
| 446 |
+
"team_remove_select": "Select person to remove:",
|
| 447 |
+
"team_remove_btn": "Permanently remove member",
|
| 448 |
+
"team_remove_toast": "Worker was removed from internal database.",
|
| 449 |
+
"team_add_section": "### ➕ Add / Modify Worker Card",
|
| 450 |
+
"team_input_name": "Full Name / Job Position",
|
| 451 |
+
"team_input_placeholder": "E.g., Carl (Mason)",
|
| 452 |
+
"team_input_rate": "Hourly rate in active currency",
|
| 453 |
+
"team_submit_btn": "Save Profile",
|
| 454 |
+
"team_update_toast": "Worker wage rate updated.",
|
| 455 |
+
"team_add_toast": "New member successfully registered.",
|
| 456 |
+
|
| 457 |
+
# CRM Adresář
|
| 458 |
+
"crm_title": "👥 Client Address Book (CRM)",
|
| 459 |
+
"crm_empty": "Client book is empty at the moment.",
|
| 460 |
+
"crm_add_section": "### ➕ Quick Manual Client Entry",
|
| 461 |
+
"cust_name": "Full Name",
|
| 462 |
+
"company": "Company (Optional)",
|
| 463 |
+
"phone": "Phone Number",
|
| 464 |
+
"save_cust": "Save Client",
|
| 465 |
+
"crm_err_name": "Client name is a mandatory field.",
|
| 466 |
+
"crm_success_toast": "Client records safely saved to CRM.",
|
| 467 |
+
|
| 468 |
+
# Fakturace
|
| 469 |
+
"inv_title": "🧾 1-Click Invoicing System",
|
| 470 |
+
"inv_select_quote": "Choose an approved fixed budget for immediate invoicing:",
|
| 471 |
+
"invoice_btn": "🧾 ISSUE 1-CLICK INVOICE NOW",
|
| 472 |
+
"inv_success_toast": "Invoice issued and stored into ledger successfully!",
|
| 473 |
+
"inv_no_fixed_quotes": "No approved fixed quotes available (Rough estimations cannot be converted into an invoice).",
|
| 474 |
+
"inv_book_title": "Complete Issued Invoices Overview & Payments Status",
|
| 475 |
+
"inv_status_paid": "🟢 Settled & Paid",
|
| 476 |
+
"inv_status_sent": "🔵 Sent (Overdue)",
|
| 477 |
+
"inv_mark_paid": "Mark as PAID / SETTLED",
|
| 478 |
+
"inv_empty": "The invoice ledger is empty.",
|
| 479 |
+
|
| 480 |
+
# PRO Funkce
|
| 481 |
"paywall_msg": "🔒 You have reached the 3 free quotes limit. Unlock PRO tier for unlimited access.",
|
| 482 |
+
"pro_router": "📍 AI Route & Logistics Planner",
|
| 483 |
+
"pro_sms": "📲 Intelligent SMS Reminder Center",
|
| 484 |
+
"pro_locked_warn": "🔒 This panel contains advanced TradeFlow PRO modules. Activate evaluation license in Dev Admin Panel via sidebar.",
|
| 485 |
+
"pro_route_title": "📍 AI Geographical Route Optimization Engine",
|
| 486 |
+
"pro_route_label": "Target list of planned on-site addresses",
|
| 487 |
+
"pro_route_btn": "🗺️ Compute Mathematical Shortest Path",
|
| 488 |
+
"pro_route_result": "🚀 AI Optimization outcome: Departure Plzeň -> Beroun -> Praha. Total fuel and logistics distance saved: 42 km.",
|
| 489 |
+
"pro_sms_title": "📱 Smart Corporate Debt Collection SMS Center",
|
| 490 |
+
"pro_sms_step1": "### 🔍 1. Locating Delinquent Accounts in System",
|
| 491 |
+
"pro_sms_filter_name": "Filter debtors by name lookup:",
|
| 492 |
+
"pro_sms_sort_label": "Sort debtors by criteria order:",
|
| 493 |
+
"pro_sms_sort_high": "Highest outstanding debt first",
|
| 494 |
+
"pro_sms_sort_low": "Lowest outstanding debt first",
|
| 495 |
+
"pro_sms_no_debts": "No outstanding overdue invoices matching current filters discovered.",
|
| 496 |
+
"pro_sms_select_debtor": "🎯 Choose target account to contact:",
|
| 497 |
+
"pro_sms_step2": "### 📋 2. Template Preset Assignment",
|
| 498 |
+
"pro_sms_template_label": "Choose active communications preset:",
|
| 499 |
+
"pro_sms_expander": "⚙️ Advanced Presets Editor & AI Content Generator Engine",
|
| 500 |
+
"pro_sms_ai_assistant": "🤖 **AI Tone Modulation Assistant**",
|
| 501 |
+
"pro_sms_ai_prompt": "State assignment for AI (e.g., 'Write highly urgent pre-legal warning letter')",
|
| 502 |
+
"pro_sms_ai_btn": "🪄 Compose New Template via AI",
|
| 503 |
+
"pro_sms_ai_success": "AI copy successfully synthesized into the area below!",
|
| 504 |
+
"pro_sms_new_title": "New template label name:",
|
| 505 |
+
"pro_sms_new_body": "Message body code (use placeholder tags {jmeno}, {cislo}, {castka})",
|
| 506 |
+
"pro_sms_save_template_btn": "Save Preset into permanent storage",
|
| 507 |
+
"pro_sms_template_success": "Template preset recorded!",
|
| 508 |
+
"pro_sms_step3": "### 📱 3. Final Compiled Client Text Copy",
|
| 509 |
+
"pro_sms_copy_btn": "📋 Copy Message Text",
|
| 510 |
+
"pro_sms_copy_ok": "✅ Copy to clipboard successful!",
|
| 511 |
+
"pro_sms_select_info": "Select an overdue account to preview and extract SMS dispatch copy."
|
| 512 |
}
|
| 513 |
}
|
| 514 |
|
|
|
|
| 521 |
}
|
| 522 |
|
| 523 |
# -----------------------------------------------------------------------------
|
| 524 |
+
# INICIALIZACE STAVU
|
| 525 |
# -----------------------------------------------------------------------------
|
| 526 |
+
VERSION = "2.1.2-ENTERPRISE"
|
| 527 |
|
| 528 |
st.set_page_config(page_title="TradeFlow AI", page_icon="🏆", layout="wide", initial_sidebar_state="expanded")
|
| 529 |
|
|
|
|
| 533 |
if "user_subscribed" not in st.session_state: st.session_state.user_subscribed = False
|
| 534 |
if "quotes_count" not in st.session_state: st.session_state.quotes_count = 0
|
| 535 |
|
|
|
|
| 536 |
if "db_team" not in st.session_state: st.session_state.db_team = []
|
| 537 |
if "db_customers" not in st.session_state: st.session_state.db_customers = []
|
| 538 |
if "db_quotes" not in st.session_state: st.session_state.db_quotes = []
|
|
|
|
| 545 |
{"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!"}
|
| 546 |
]
|
| 547 |
|
| 548 |
+
t = TRANSLATIONS[st.session_state.lang if st.session_state.lang in TRANSLATIONS else "CZ"]
|
| 549 |
+
|
| 550 |
if "cost_items_df" not in st.session_state:
|
| 551 |
st.session_state.cost_items_df = pd.DataFrame([
|
| 552 |
{"Sekce nákladu": "Materiál", "Název položky / Popis": "", "Částka": 0.0}
|
|
|
|
| 559 |
return f"{converted:,.2f} {c_info['symbol']}"
|
| 560 |
return f"{c_info['symbol']}{converted:,.2f}"
|
| 561 |
|
|
|
|
|
|
|
| 562 |
# -----------------------------------------------------------------------------
|
| 563 |
+
# SIDEBAR: PANEL S NASTAVENÍM & VÝVOJÁŘSKÝM PUSH SEEDINGEM
|
| 564 |
# -----------------------------------------------------------------------------
|
| 565 |
with st.sidebar:
|
| 566 |
+
st.header(t["sb_settings"])
|
| 567 |
+
st.session_state.lang = st.selectbox(t["sb_lang"], list(TRANSLATIONS.keys()), index=list(TRANSLATIONS.keys()).index(st.session_state.lang))
|
| 568 |
+
|
| 569 |
+
# Po změně jazyka musíme hned aktualizovat překladový slovník, aby zbytek kódu běžel v novém kontextu
|
| 570 |
+
t = TRANSLATIONS[st.session_state.lang]
|
| 571 |
+
|
| 572 |
+
st.session_state.curr = st.selectbox(t["sb_curr"], list(CURRENCIES.keys()), index=list(CURRENCIES.keys()).index(st.session_state.curr))
|
| 573 |
|
| 574 |
st.markdown("---")
|
| 575 |
+
st.header(t["sb_dev_panel"])
|
| 576 |
|
| 577 |
if not st.session_state.admin_authenticated:
|
| 578 |
+
pwd_input = st.text_input(t["sb_pwd_label"], type="password", help=t["sb_pwd_hint"])
|
| 579 |
+
if st.button(t["sb_unlock_btn"]):
|
| 580 |
if pwd_input == "admin123":
|
| 581 |
st.session_state.admin_authenticated = True
|
| 582 |
+
st.success(t["sb_auth_ok"])
|
| 583 |
st.rerun()
|
| 584 |
else:
|
| 585 |
+
st.error(t["sb_auth_err"])
|
| 586 |
else:
|
| 587 |
+
st.info(t["sb_auth_status"])
|
| 588 |
+
st.session_state.user_subscribed = st.toggle(t["sb_pro_toggle"], value=st.session_state.user_subscribed)
|
| 589 |
+
st.session_state.quotes_count = st.number_input(t["sb_quotes_count"], min_value=0, max_value=20, value=st.session_state.quotes_count)
|
| 590 |
|
| 591 |
+
st.markdown(f"### {t['sb_demo_section']}")
|
| 592 |
+
if st.button(t["sb_import_demo"], use_container_width=True):
|
|
|
|
| 593 |
st.session_state.db_team = [
|
| 594 |
{"id": 1, "jmeno": "Šimon (Director)", "sazba": 45.0},
|
| 595 |
{"id": 2, "jmeno": "Nikola (Prezentace & Podpora)", "sazba": 30.0},
|
|
|
|
| 622 |
{"id": 4, "cislo": "20260004", "zakaznik": "Jan Novák", "castka": 5010.00, "stav": "Odesláno", "datum": "2026-06-15"}
|
| 623 |
]
|
| 624 |
st.session_state.quotes_count = 1
|
| 625 |
+
st.toast(t["toast_demo_ok"], icon="📥")
|
| 626 |
time.sleep(0.3)
|
| 627 |
st.rerun()
|
| 628 |
|
| 629 |
+
if st.button(t["sb_clear_demo"], use_container_width=True):
|
| 630 |
st.session_state.db_team = []
|
| 631 |
st.session_state.db_customers = []
|
| 632 |
st.session_state.db_quotes = []
|
| 633 |
st.session_state.db_invoices = []
|
| 634 |
st.session_state.quotes_count = 0
|
| 635 |
st.session_state.cost_items_df = pd.DataFrame([{"Sekce nákladu": "Materiál", "Název položky / Popis": "", "Částka": 0.0}])
|
| 636 |
+
st.toast(t["toast_clear_ok"], icon="🗑️")
|
| 637 |
time.sleep(0.3)
|
| 638 |
st.rerun()
|
| 639 |
|
| 640 |
st.markdown("---")
|
| 641 |
+
if st.button(t["sb_logout"], use_container_width=True):
|
| 642 |
st.session_state.admin_authenticated = False
|
| 643 |
st.rerun()
|
| 644 |
|
|
|
|
| 652 |
])
|
| 653 |
|
| 654 |
# -----------------------------------------------------------------------------
|
| 655 |
+
# TAB 1: DASHBOARD & METRIKY
|
| 656 |
# -----------------------------------------------------------------------------
|
| 657 |
with tab_dashboard:
|
| 658 |
st.header(t["tab_dash"])
|
|
|
|
| 663 |
c1, c2, c3, c4 = st.columns(4)
|
| 664 |
c1.metric(t["profit"], format_price(total_revenue))
|
| 665 |
c2.metric(t["pending"], format_price(pending_revenue))
|
| 666 |
+
c3.metric(t["potential"], format_price(potencial), help=t["dash_help_potential"])
|
| 667 |
+
c4.metric(t["limit"], f"{st.session_state.quotes_count} / 3", delta=t["dash_limit_warn"] if is_locked else None)
|
| 668 |
|
| 669 |
st.markdown("---")
|
| 670 |
+
st.subheader(t["dash_analysis_title"])
|
| 671 |
+
st.write(t["dash_analysis_desc"])
|
| 672 |
|
| 673 |
flat_costs = []
|
| 674 |
for q in st.session_state.db_quotes:
|
| 675 |
if q.get("labor_eur", 0) > 0:
|
| 676 |
flat_costs.append({
|
| 677 |
+
t["col_project"]: f"{q['zakaznik']} ({q['datum']})",
|
| 678 |
+
t["col_section"]: "Výplaty zaměstnancům" if st.session_state.lang != "EN" else "Employee Wages",
|
| 679 |
+
t["col_item_name"]: "Mzda týmu na projektu (odpracované hodiny)" if st.session_state.lang != "EN" else "Team payroll on project (hours worked)",
|
| 680 |
"Částka (Základ v EUR)": q["labor_eur"],
|
| 681 |
+
t["col_type"]: "🎯 Přesná fixní" if q["typ"] == "fixni" else "🔮 Orientační odhad"
|
| 682 |
})
|
| 683 |
for ec in q.get("extra_costs", []):
|
| 684 |
flat_costs.append({
|
| 685 |
+
t["col_project"]: f"{q['zakaznik']} ({q['datum']})",
|
| 686 |
+
t["col_section"]: ec["kategorie"],
|
| 687 |
+
t["col_item_name"]: ec["nazev"] if ec["nazev"] else "Přímý výdaj",
|
| 688 |
"Částka (Základ v EUR)": ec["eur"],
|
| 689 |
+
t["col_type"]: "🎯 Přesná fixní" if q["typ"] == "fixni" else "🔮 Orientační odhad"
|
| 690 |
})
|
| 691 |
|
| 692 |
if flat_costs:
|
|
|
|
| 694 |
|
| 695 |
f_col1, f_col2 = st.columns(2)
|
| 696 |
with f_col1:
|
| 697 |
+
available_cats = [t["dash_filter_all_cats"]] + list(df_costs[t["col_section"]].unique())
|
| 698 |
+
selected_cat_filter = st.selectbox(t["dash_filter_cat"], available_cats)
|
| 699 |
with f_col2:
|
| 700 |
+
selected_type_filter = st.radio(t["dash_filter_type"], [t["dash_type_all"], t["dash_type_fixed"], t["dash_type_estimated"]], horizontal=True)
|
| 701 |
|
| 702 |
+
if selected_cat_filter != t["dash_filter_all_cats"]:
|
| 703 |
+
df_costs = df_costs[df_costs[t["col_section"]] == selected_cat_filter]
|
| 704 |
|
| 705 |
+
if selected_type_filter == t["dash_type_fixed"]:
|
| 706 |
+
df_costs = df_costs[df_costs[t["col_type"]] == "🎯 Přesná fixní"]
|
| 707 |
+
elif selected_type_filter == t["dash_type_estimated"]:
|
| 708 |
+
df_costs = df_costs[df_costs[t["col_type"]] == "🔮 Orientační odhad"]
|
| 709 |
|
| 710 |
if not df_costs.empty:
|
| 711 |
total_filtered_eur = df_costs["Částka (Základ v EUR)"].sum()
|
| 712 |
+
st.markdown(f"#### {t['dash_aggregated_spend']} **{format_price(total_filtered_eur)}**")
|
| 713 |
|
| 714 |
df_costs_display = df_costs.copy()
|
| 715 |
+
df_costs_display[t["col_amount"]] = df_costs_display["Částka (Základ v EUR)"].apply(format_price)
|
| 716 |
df_costs_display = df_costs_display.drop(columns=["Částka (Základ v EUR)"])
|
| 717 |
st.dataframe(df_costs_display, use_container_width=True, hide_index=True)
|
| 718 |
else:
|
| 719 |
+
st.info(t["dash_no_records"])
|
| 720 |
else:
|
| 721 |
+
st.info(t["dash_no_costs_yet"])
|
| 722 |
|
| 723 |
st.markdown("---")
|
| 724 |
+
st.subheader(t["dash_book_title"])
|
| 725 |
if st.session_state.db_quotes:
|
| 726 |
df_raw = pd.DataFrame(st.session_state.db_quotes)
|
| 727 |
df_display = df_raw.copy()
|
| 728 |
+
|
| 729 |
+
# Přejmenování klíčů a lokalizace pro tabulku
|
| 730 |
+
df_display[t["col_date"]] = df_display["datum"]
|
| 731 |
+
df_display[t["col_project"]] = df_display["zakaznik"]
|
| 732 |
+
df_display[t["col_item_name"]] = df_display["popis"]
|
| 733 |
+
df_display[t["col_amount"]] = df_display["celkem"].apply(format_price)
|
| 734 |
+
df_display[t["col_type"]] = df_display["typ"].map({"odhad": "🔮 " + t["dash_type_estimated"], "fixni": "🎯 " + t["dash_type_fixed"]})
|
| 735 |
+
|
| 736 |
+
cols_to_show = [t["col_date"], t["col_project"], t["col_type"], t["col_item_name"], t["col_amount"]]
|
| 737 |
+
st.dataframe(df_display[cols_to_show], use_container_width=True, hide_index=True)
|
| 738 |
else:
|
| 739 |
+
st.info(t["dash_book_empty"])
|
| 740 |
|
| 741 |
# -----------------------------------------------------------------------------
|
| 742 |
+
# TAB 2: AI GENERÁTOR NABÍDEK (OPRAVENÝ & S PŘELOŽENÝM SOUBOREM NÁKLADŮ)
|
| 743 |
# -----------------------------------------------------------------------------
|
| 744 |
with tab_generator:
|
| 745 |
st.header(t["tab_gen"])
|
| 746 |
if is_locked:
|
| 747 |
st.error(t["paywall_msg"])
|
| 748 |
else:
|
| 749 |
+
quote_type = st.radio(t["gen_type_label"], [t["gen_type_est"], t["gen_type_fix"]])
|
| 750 |
+
is_fixed = t["gen_type_fix"] in quote_type
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
|
| 752 |
+
st.markdown(t["gen_client_section"])
|
| 753 |
cust_names = [c["jmeno"] for c in st.session_state.db_customers]
|
| 754 |
|
| 755 |
+
client_mode = st.radio(t["gen_client_mode"], [t["gen_client_crm"], t["gen_client_manual"]], horizontal=True)
|
| 756 |
+
if client_mode == t["gen_client_crm"] and cust_names:
|
| 757 |
+
selected_cust = st.selectbox(t["gen_select_client"], cust_names)
|
| 758 |
else:
|
| 759 |
+
selected_cust = st.text_input(t["gen_input_client_name"], value="", placeholder=t["gen_client_placeholder"])
|
| 760 |
|
| 761 |
+
input_task = st.text_area(t["gen_spec_label"], value=t["gen_spec_default"])
|
| 762 |
|
| 763 |
+
st.markdown(t["gen_team_alloc"])
|
| 764 |
|
| 765 |
if not st.session_state.db_team:
|
| 766 |
+
st.info(t["gen_no_team_warn"])
|
| 767 |
team_hours = {}
|
| 768 |
else:
|
| 769 |
+
if st.button(t["gen_ai_btn"]):
|
| 770 |
st.session_state["ai_mock_active"] = True
|
| 771 |
st.session_state.cost_items_df = pd.DataFrame([
|
| 772 |
{"Sekce nákladu": "Materiál", "Název položky / Popis": "AI Odhad: Potrubí, ventily a fitinky", "Částka": 480.0},
|
| 773 |
+
{"Sekce nákladu": "Benzín / Doprava", "Název položky / Popis": "AI Odhad: Nákladní závoz materiálu", "Částka": 65.0},
|
| 774 |
{"Sekce nákladu": "Pronájem vybavení", "Název položky / Popis": "AI Odhad: Zapůjčení tlakové zkoušečky", "Částka": 110.0}
|
| 775 |
])
|
| 776 |
+
st.toast(t["gen_ai_toast"], icon="🪄")
|
| 777 |
|
| 778 |
team_hours = {}
|
| 779 |
cols_team = st.columns(len(st.session_state.db_team))
|
|
|
|
| 792 |
)
|
| 793 |
team_hours[member["id"]] = hours_input
|
| 794 |
|
| 795 |
+
st.markdown(t["gen_direct_costs"])
|
| 796 |
+
st.caption(t["gen_table_hint"])
|
| 797 |
|
|
|
|
| 798 |
curr_rate = CURRENCIES[st.session_state.curr]["rate"]
|
| 799 |
|
| 800 |
+
# Úprava konfigurace tabulky - odstraněna měna ze záhlaví sloupce (pouze t["col_amount"])
|
| 801 |
column_configuration = {
|
| 802 |
"Sekce nákladu": st.column_config.SelectboxColumn(
|
| 803 |
+
t["col_section"],
|
| 804 |
+
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"],
|
| 805 |
required=True
|
| 806 |
),
|
| 807 |
+
"Název položky / Popis": st.column_config.TextColumn(t["col_item_name"], required=True),
|
| 808 |
+
"Částka": st.column_config.NumberColumn(t["col_amount"], min_value=0.0, required=True, format="%.2f")
|
| 809 |
}
|
| 810 |
|
| 811 |
edited_df = st.data_editor(
|
|
|
|
| 817 |
)
|
| 818 |
st.session_state.cost_items_df = edited_df
|
| 819 |
|
| 820 |
+
st.markdown(t["gen_csv_title"])
|
|
|
|
| 821 |
|
| 822 |
def zpracuj_csv_import():
|
| 823 |
if st.session_state.csv_uploader_key is not None:
|
|
|
|
| 828 |
sanitized_df = uploaded_df[needed_headers].copy()
|
| 829 |
sanitized_df["Částka"] = pd.to_numeric(sanitized_df["Částka"], errors='coerce').fillna(0.0)
|
| 830 |
|
|
|
|
| 831 |
st.session_state.cost_items_df = pd.concat([st.session_state.cost_items_df, sanitized_df], ignore_index=True)
|
| 832 |
+
st.toast(t["gen_csv_success"], icon="📥")
|
| 833 |
else:
|
| 834 |
+
st.error(t["gen_csv_err_headers"])
|
| 835 |
except Exception as e:
|
| 836 |
+
st.error(f"{t['gen_csv_err_read']} {e}")
|
| 837 |
|
| 838 |
st.file_uploader(
|
| 839 |
+
t["gen_csv_uploader"],
|
| 840 |
type=["csv"],
|
| 841 |
key="csv_uploader_key",
|
| 842 |
+
on_change=zprocessed_csv_import if 'zprocessed_csv_import' in locals() else zpracuj_csv_import
|
| 843 |
)
|
|
|
|
| 844 |
|
| 845 |
st.markdown("---")
|
| 846 |
|
|
|
|
| 852 |
total_extra_eur = total_extra_local / curr_rate
|
| 853 |
total_project_eur = total_labor_eur + total_extra_eur
|
| 854 |
|
| 855 |
+
st.markdown(f"{t['gen_total_calc']} **{format_price(total_project_eur)}**")
|
| 856 |
|
| 857 |
+
if st.button(t["gen_save_btn"], type="primary"):
|
| 858 |
if not selected_cust or selected_cust.strip() == "":
|
| 859 |
+
st.error(t["gen_err_no_client"])
|
| 860 |
else:
|
| 861 |
if selected_cust not in cust_names:
|
| 862 |
st.session_state.db_customers.append({
|
| 863 |
"id": len(st.session_state.db_customers) + 1,
|
| 864 |
"jmeno": selected_cust.strip(),
|
| 865 |
+
"firma": "Fyzická osoba" if st.session_state.lang != "EN" else "Individual Client",
|
| 866 |
+
"telefon": "Nezadáno" if st.session_state.lang != "EN" else "N/A"
|
| 867 |
})
|
| 868 |
|
| 869 |
saved_extra_costs = []
|
|
|
|
| 890 |
st.session_state["ai_mock_active"] = False
|
| 891 |
|
| 892 |
st.session_state.cost_items_df = pd.DataFrame([{"Sekce nákladu": "Materiál", "Název položky / Popis": "", "Částka": 0.0}])
|
| 893 |
+
st.success(t["gen_save_success"])
|
| 894 |
time.sleep(0.4)
|
| 895 |
st.rerun()
|
| 896 |
|
|
|
|
| 898 |
# TAB 3: SPRÁVA TÝMU
|
| 899 |
# -----------------------------------------------------------------------------
|
| 900 |
with tab_team:
|
| 901 |
+
st.header(t["team_title"])
|
| 902 |
+
st.write(t["team_desc"])
|
| 903 |
|
| 904 |
t_col1, t_col2 = st.columns([2, 1])
|
| 905 |
|
| 906 |
with t_col1:
|
| 907 |
+
st.markdown(f"### {t['team_sub_list']}")
|
| 908 |
team_display_list = []
|
| 909 |
for m in st.session_state.db_team:
|
| 910 |
team_display_list.append({
|
| 911 |
+
t["team_col_id"]: m["id"],
|
| 912 |
+
t["team_col_role"]: m["jmeno"],
|
| 913 |
+
t["team_col_rate"]: format_price(m["sazba"])
|
| 914 |
})
|
| 915 |
if team_display_list:
|
| 916 |
st.dataframe(pd.DataFrame(team_display_list), use_container_width=True, hide_index=True)
|
| 917 |
else:
|
| 918 |
+
st.info(t["team_empty"])
|
| 919 |
|
| 920 |
+
st.markdown(t["team_remove_section"])
|
| 921 |
if st.session_state.db_team:
|
| 922 |
+
member_to_remove = st.selectbox(t["team_remove_select"], [m["jmeno"] for m in st.session_state.db_team])
|
| 923 |
+
if st.button(t["team_remove_btn"]):
|
| 924 |
st.session_state.db_team = [m for m in st.session_state.db_team if m["jmeno"] != member_to_remove]
|
| 925 |
+
st.success(f"{t['team_remove_toast']} ({member_to_remove})")
|
| 926 |
time.sleep(0.3)
|
| 927 |
st.rerun()
|
| 928 |
|
| 929 |
with t_col2:
|
| 930 |
+
st.markdown(t["team_add_section"])
|
| 931 |
with st.form("team_management_form"):
|
| 932 |
+
new_tm_name = st.text_input(t["team_input_name"], placeholder=t["team_input_placeholder"])
|
| 933 |
+
new_tm_rate = st.number_input(t["team_input_rate"], min_value=1, value=400 if st.session_state.curr == "CZK" else 20)
|
|
|
|
| 934 |
|
| 935 |
+
if st.form_submit_button(t["team_submit_btn"]):
|
| 936 |
if new_tm_name:
|
| 937 |
rate_in_eur = new_tm_rate / CURRENCIES[st.session_state.curr]["rate"]
|
| 938 |
matched_member = next((m for m in st.session_state.db_team if m["jmeno"].lower() == new_tm_name.lower()), None)
|
| 939 |
if matched_member:
|
| 940 |
matched_member["sazba"] = rate_in_eur
|
| 941 |
+
st.success(t["team_update_toast"])
|
| 942 |
else:
|
| 943 |
next_id = max([m["id"] for m in st.session_state.db_team]) + 1 if st.session_state.db_team else 1
|
| 944 |
st.session_state.db_team.append({"id": next_id, "jmeno": new_tm_name, "sazba": rate_in_eur})
|
| 945 |
+
st.success(t["team_add_toast"])
|
| 946 |
time.sleep(0.3)
|
| 947 |
st.rerun()
|
| 948 |
|
|
|
|
| 950 |
# TAB 4: CRM ADRESÁŘ
|
| 951 |
# -----------------------------------------------------------------------------
|
| 952 |
with tab_crm:
|
| 953 |
+
st.header(t["crm_title"])
|
| 954 |
col_crm1, col_crm2 = st.columns([2, 1])
|
| 955 |
with col_crm1:
|
| 956 |
if st.session_state.db_customers:
|
| 957 |
+
df_crm = pd.DataFrame(st.session_state.db_customers)
|
| 958 |
+
df_crm_display = pd.DataFrame({
|
| 959 |
+
t["cust_name"]: df_crm["jmeno"],
|
| 960 |
+
t["company"]: df_crm["firma"],
|
| 961 |
+
t["phone"]: df_crm["telefon"]
|
| 962 |
+
})
|
| 963 |
+
st.dataframe(df_crm_display, use_container_width=True, hide_index=True)
|
| 964 |
else:
|
| 965 |
+
st.info(t["crm_empty"])
|
| 966 |
with col_crm2:
|
| 967 |
+
st.markdown(t["crm_add_section"])
|
| 968 |
with st.form("manual_crm_form"):
|
| 969 |
c_n = st.text_input(t["cust_name"], placeholder="Jan Novák")
|
| 970 |
c_f = st.text_input(t["company"], placeholder="Stavebniny s.r.o.")
|
|
|
|
| 974 |
st.session_state.db_customers.append({
|
| 975 |
"id": len(st.session_state.db_customers) + 1,
|
| 976 |
"jmeno": c_n,
|
| 977 |
+
"firma": c_f if c_f else ("Fyzická osoba" if st.session_state.lang != "EN" else "Individual"),
|
| 978 |
"telefon": c_p
|
| 979 |
})
|
| 980 |
+
st.success(t["crm_success_toast"])
|
| 981 |
+
time.sleep(0.3)
|
| 982 |
+
st.rerun()
|
| 983 |
else:
|
| 984 |
+
st.error(t["crm_err_name"])
|
| 985 |
|
| 986 |
# -----------------------------------------------------------------------------
|
| 987 |
# TAB 5: FAKTURACE
|
| 988 |
# -----------------------------------------------------------------------------
|
| 989 |
with tab_fakturace:
|
| 990 |
+
st.header(t["inv_title"])
|
| 991 |
only_fixni_quotes = [q for q in st.session_state.db_quotes if q.get("typ") == "fixni"]
|
| 992 |
|
| 993 |
if only_fixni_quotes:
|
| 994 |
+
invoice_options = {f"ID: {q['id']} | {q['zakaznik']} — {format_price(q['celkem'])}": q for q in only_fixni_quotes}
|
| 995 |
+
selected_quote_to_inv = st.selectbox(t["inv_select_quote"], list(invoice_options.keys()))
|
| 996 |
targeted_quote = invoice_options[selected_quote_to_inv]
|
| 997 |
|
| 998 |
if st.button(t["invoice_btn"], type="primary", use_container_width=True):
|
|
|
|
| 1004 |
"stav": "Odesláno",
|
| 1005 |
"datum": time.strftime("%Y-%m-%d")
|
| 1006 |
})
|
| 1007 |
+
st.success(t["inv_success_toast"])
|
| 1008 |
time.sleep(0.3)
|
| 1009 |
st.rerun()
|
| 1010 |
else:
|
| 1011 |
+
st.info(t["inv_no_fixed_quotes"])
|
| 1012 |
|
| 1013 |
st.markdown("---")
|
| 1014 |
+
st.subheader(t["inv_book_title"])
|
| 1015 |
|
| 1016 |
if st.session_state.db_invoices:
|
| 1017 |
for index_inv, invoice_obj in enumerate(st.session_state.db_invoices):
|
| 1018 |
div_c1, div_c2, div_c3 = st.columns([2, 1, 1])
|
| 1019 |
+
div_c1.write(f"🧾 **{t['col_amount']} č. {invoice_obj['cislo']}** — {invoice_obj['zakaznik']} [**{format_price(invoice_obj['castka'])}**]")
|
| 1020 |
|
| 1021 |
if invoice_obj["stav"] == "Zaplaceno":
|
| 1022 |
+
div_c2.markdown(f"<span style='color:green; font-weight:bold;'>{t['inv_status_paid']}</span>", unsafe_allow_html=True)
|
| 1023 |
div_c3.write("")
|
| 1024 |
else:
|
| 1025 |
+
div_c2.markdown(f"<span style='color:orange; font-weight:bold;'>{t['inv_status_sent']}</span>", unsafe_allow_html=True)
|
| 1026 |
+
if div_c3.button(t["inv_mark_paid"], key=f"payment_trigger_key_{index_inv}", use_container_width=True):
|
| 1027 |
invoice_obj["stav"] = "Zaplaceno"
|
| 1028 |
st.rerun()
|
| 1029 |
else:
|
| 1030 |
+
st.info(t["inv_empty"])
|
| 1031 |
|
| 1032 |
# -----------------------------------------------------------------------------
|
| 1033 |
# TAB 6: PRO BONUSY
|
|
|
|
| 1035 |
with tab_pro_features:
|
| 1036 |
st.header(t["tab_pro"])
|
| 1037 |
if not st.session_state.user_subscribed:
|
| 1038 |
+
st.warning(t["pro_locked_warn"])
|
| 1039 |
else:
|
| 1040 |
pro_sub1, pro_sub2 = st.tabs([t["pro_router"], t["pro_sms"]])
|
| 1041 |
|
| 1042 |
with pro_sub1:
|
| 1043 |
+
st.subheader(t["pro_route_title"])
|
| 1044 |
+
st.text_area(t["pro_route_label"], value="Plzeňská 15, Praha\nPrůmyslová 4, Beroun\nSukova 12, Plzeň")
|
| 1045 |
+
if st.button(t["pro_route_btn"], use_container_width=True):
|
| 1046 |
+
st.info(t["pro_route_result"])
|
| 1047 |
|
| 1048 |
with pro_sub2:
|
| 1049 |
+
st.subheader(t["pro_sms_title"])
|
| 1050 |
+
st.markdown(t["pro_sms_step1"])
|
|
|
|
| 1051 |
open_debts = [invoice for invoice in st.session_state.db_invoices if invoice["stav"] == "Odesláno"]
|
| 1052 |
|
| 1053 |
search_col1, search_col2 = st.columns([1, 1])
|
| 1054 |
with search_col1:
|
| 1055 |
+
query_name = st.text_input(t["pro_sms_filter_name"], value="")
|
| 1056 |
with search_col2:
|
| 1057 |
+
sorting_order = st.selectbox(t["pro_sms_sort_label"], [t["pro_sms_sort_high"], t["pro_sms_sort_low"]])
|
| 1058 |
|
| 1059 |
if query_name:
|
| 1060 |
open_debts = [d for d in open_debts if query_name.lower() in d["zakaznik"].lower()]
|
| 1061 |
|
| 1062 |
+
if sorting_order == t["pro_sms_sort_high"]:
|
| 1063 |
open_debts.sort(key=lambda x: x["castka"], reverse=True)
|
| 1064 |
else:
|
| 1065 |
open_debts.sort(key=lambda x: x["castka"])
|
| 1066 |
|
| 1067 |
if not open_debts:
|
| 1068 |
+
st.info(t["pro_sms_no_debts"])
|
| 1069 |
active_debtor = None
|
| 1070 |
else:
|
| 1071 |
debtor_mapping = {f"👤 {d['zakaznik']} (Faktura č. {d['cislo']} | Dluh: {format_price(d['castka'])})": d for d in open_debts}
|
| 1072 |
+
selected_debtor_string = st.selectbox(t["pro_sms_select_debtor"], list(debtor_mapping.keys()))
|
| 1073 |
active_debtor = debtor_mapping[selected_debtor_string]
|
| 1074 |
|
| 1075 |
st.markdown("---")
|
| 1076 |
+
st.markdown(t["pro_sms_step2"])
|
| 1077 |
|
| 1078 |
preset_labels = [p["název"] for p in st.session_state.sms_presets]
|
| 1079 |
+
chosen_preset_label = st.selectbox(t["pro_sms_template_label"], preset_labels)
|
| 1080 |
active_preset = next(p for p in st.session_state.sms_presets if p["název"] == chosen_preset_label)
|
| 1081 |
|
| 1082 |
+
with st.expander(t["pro_sms_expander"]):
|
| 1083 |
+
st.markdown(t["pro_sms_ai_assistant"])
|
| 1084 |
+
prompt_text = st.text_input(t["pro_sms_ai_prompt"], placeholder="...")
|
| 1085 |
|
| 1086 |
+
if st.button(t["pro_sms_ai_btn"]):
|
| 1087 |
if prompt_text:
|
| 1088 |
p_low = prompt_text.lower()
|
| 1089 |
if "předžal" in p_low or "naléh" in p_low or "soud" in p_low:
|
| 1090 |
+
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."
|
| 1091 |
else:
|
| 1092 |
+
result_sms = "Dobrý den {jmeno}, připomínáme zapomenutou fakturu č. {cislo} na částku {castka}. Prosíme o co nejrychlejší proplacení."
|
| 1093 |
st.session_state["ai_sms_output_buffer"] = result_sms
|
| 1094 |
+
st.success(t["pro_sms_ai_success"])
|
| 1095 |
|
| 1096 |
+
new_preset_title = st.text_input(t["pro_sms_new_title"], value="")
|
| 1097 |
default_body = st.session_state.get("ai_sms_output_buffer", "Vážený pane/paní {jmeno}, ...")
|
| 1098 |
+
new_preset_body = st.text_area(t["pro_sms_new_body"], value=default_body)
|
| 1099 |
|
| 1100 |
+
if st.button(t["pro_sms_save_template_btn"]):
|
| 1101 |
if new_preset_title and new_preset_body:
|
| 1102 |
st.session_state.sms_presets.append({"název": new_preset_title, "text": new_preset_body})
|
| 1103 |
+
st.success(t["pro_sms_template_success"])
|
| 1104 |
time.sleep(0.3)
|
| 1105 |
st.rerun()
|
| 1106 |
|
| 1107 |
st.markdown("---")
|
| 1108 |
+
st.markdown(t["pro_sms_step3"])
|
| 1109 |
|
| 1110 |
if active_debtor:
|
| 1111 |
raw_template = active_preset["text"]
|
|
|
|
| 1119 |
border: 1px solid #dee2e6;
|
| 1120 |
padding: 18px;
|
| 1121 |
border-radius: 8px;
|
| 1122 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 1123 |
font-size: 15px;
|
| 1124 |
color: #212529;
|
| 1125 |
white-space: pre-wrap;
|
| 1126 |
word-wrap: break-word;
|
| 1127 |
line-height: 1.6;
|
| 1128 |
margin-bottom: 12px;
|
| 1129 |
+
;">{final_compiled_sms}</div>
|
| 1130 |
""", unsafe_allow_html=True)
|
| 1131 |
|
| 1132 |
safe_encoded_sms = urllib.parse.quote(final_compiled_sms)
|
|
|
|
| 1135 |
<button onclick="
|
| 1136 |
navigator.clipboard.writeText(decodeURIComponent('{safe_encoded_sms}'));
|
| 1137 |
let btn = document.getElementById('js_copy_btn');
|
| 1138 |
+
btn.innerText = '{t['pro_sms_copy_ok']}';
|
| 1139 |
btn.style.backgroundColor = '#28a745';
|
| 1140 |
+
setTimeout(() => {{ btn.innerText = '{t['pro_sms_copy_btn']}'; btn.style.backgroundColor = '#007bff'; }}, 2000);
|
| 1141 |
" id="js_copy_btn" style="
|
| 1142 |
background-color: #007bff;
|
| 1143 |
color: white;
|
|
|
|
| 1146 |
font-size: 14px;
|
| 1147 |
border-radius: 6px;
|
| 1148 |
cursor: pointer;
|
|
|
|
| 1149 |
font-weight: bold;
|
| 1150 |
width: 100%;
|
| 1151 |
+
">{t['pro_sms_copy_btn']}</button>
|
|
|
|
|
|
|
| 1152 |
"""
|
| 1153 |
components.html(copy_button_html, height=50)
|
| 1154 |
else:
|
| 1155 |
+
st.info(t["pro_sms_select_info"])
|