simikkk commited on
Commit
12b0d91
·
verified ·
1 Parent(s): 5112b5b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +190 -154
app.py CHANGED
@@ -116,181 +116,217 @@ def process_invoice_file(file_path: str, user_id: int) -> dict:
116
  except Exception as e: return {"filename": os.path.basename(file_path), "status": "error"}
117
 
118
  # ==========================================
119
- # 3. UI HTML TEMPLATES (Design System)
120
  # ==========================================
121
  CUSTOM_CSS = """
122
- /* Obsidian Glassmorphism Theme */
123
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
124
  :root { --bg: #09090b; --card: rgba(255,255,255,0.03); --border: rgba(255,255,255,0.08); --text: #f4f4f5; --muted: #71717a; --accent: #10b981; --accent-glow: rgba(16,185,129,0.2); }
125
  * { box-sizing: border-box; font-family: 'Inter', sans-serif; }
126
- body { background: var(--bg); color: var(--text); margin: 0; overflow-x: hidden; }
127
  .gradio-container { max-width: 100% !important; padding: 0 !important; background: var(--bg) !important; min-height: 100vh; }
128
  .hide { display: none !important; }
129
 
130
- /* Custom Layouts */
131
- .app-container { padding: 40px; max-width: 1200px; margin: 0 auto; }
132
- .navbar { display: flex; justify-content: space-between; align-items: center; padding: 20px 40px; border-bottom: 1px solid var(--border); backdrop-filter: blur(10px); position: sticky; top: 0; background: rgba(9,9,11,0.8); z-index: 100; }
133
- .logo { font-weight: 700; font-size: 20px; display: flex; align-items: center; gap: 10px; }
134
- .logo svg { width: 24px; height: 24px; fill: var(--accent); }
135
-
136
- /* Components */
137
- .btn { background: var(--accent); color: #000; border: none; padding: 12px 24px; border-radius: 8px; font-weight: 600; cursor: pointer; transition: 0.2s; text-decoration: none; display: inline-block; }
138
- .btn:hover { box-shadow: 0 0 20px var(--accent-glow); transform: translateY(-1px); }
139
- .btn-ghost { background: transparent; border: 1px solid var(--border); color: var(--text); }
140
- .btn-ghost:hover { border-color: var(--accent); color: var(--accent); }
141
- .card { background: var(--card); border: 1px solid var(--border); border-radius: 16px; padding: 24px; backdrop-filter: blur(20px); margin-bottom: 20px; }
142
- .input { background: rgba(0,0,0,0.3) !important; border: 1px solid var(--border) !important; border-radius: 8px !important; padding: 12px !important; color: var(--text) !important; width: 100% !important; margin-bottom: 16px !important; }
143
- .input:focus { border-color: var(--accent) !important; outline: none !important; }
144
-
145
- /* Hero */
146
- .hero { text-align: center; padding: 100px 20px; }
147
- .hero h1 { font-size: 56px; font-weight: 700; margin-bottom: 24px; background: linear-gradient(180deg, #fff 0%, #a1a1aa 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; line-height: 1.1; }
148
- .hero p { font-size: 20px; color: var(--muted); max-width: 600px; margin: 0 auto 40px; }
149
-
150
- /* Skeleton Loader */
151
- .skeleton { background: linear-gradient(90deg, rgba(255,255,255,0.05) 25%, rgba(255,255,255,0.1) 50%, rgba(255,255,255,0.05) 75%); background-size: 200% 100%; animation: skeleton 1.5s infinite; border-radius: 4px; height: 20px; margin-bottom: 10px; }
152
- @keyframes skeleton { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
153
-
154
- /* Gradio Overrides */
155
- .gradio-container .form { background: transparent !important; border: none !important; }
156
  footer { display: none !important; }
157
  """
158
 
159
- ICON_BOLT = '<svg viewBox="0 0 24 24"><path d="M11 21h-1l1-7H7.5c-.58 0-.57-.32-.38-.66.19-.34.05-.08.07-.12C8.48 10.94 10.42 7.54 13 3h1l-1 7h3.5c.49 0 .56.33.47.51l-.07.15C12.96 17.55 11 21 11 21z"/></svg>'
160
-
161
- def render_landing():
162
- return f"""
163
- <div class="navbar">
164
- <div class="logo">{ICON_BOLT} OmniParse AI</div>
165
- <div style="display:flex; gap:12px;">
166
- <button class="btn btn-ghost" id="nav-auth">Log In</button>
167
- <button class="btn" id="nav-signup">Start Free</button>
168
- </div>
169
- </div>
170
- <div class="hero">
171
- <h1>Invoice processing in <span style="color: var(--accent);">seconds</span>, not hours.</h1>
172
- <p>AI extracts vendor, dates, amounts and line items from any PDF or image. Export to CSV, JSON or Excel. Connect via API.</p>
173
- <button class="btn" style="font-size: 18px; padding: 16px 32px;" id="hero-cta">Start Free — 20 invoices</button>
174
- <div style="margin-top: 40px; color: var(--muted); font-size: 14px;">
175
- 99.2% accuracy · &lt;4s per invoice · 40+ formats supported
176
- </div>
177
- </div>
178
- <div class="app-container">
179
- <div class="card" style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:20px;">
180
- <div><h3 style="color:var(--accent);">1. Upload</h3><p style="color:var(--muted)">Drag & drop PDF or Image</p></div>
181
- <div><h3 style="color:var(--accent);">2. AI Extract</h3><p style="color:var(--muted)">Groq + Tesseract OCR</p></div>
182
- <div><h3 style="color:var(--accent);">3. Export</h3><p style="color:var(--muted)">CSV, JSON, Excel</p></div>
183
- </div>
184
- </div>
185
- """
186
-
187
- def render_auth():
188
- return """
189
- <div class="app-container" style="max-width: 450px; margin-top: 80px;">
190
- <div class="card">
191
- <h2 style="text-align:center; margin-bottom: 24px;">Welcome to OmniParse</h2>
192
- <input type="text" class="input" id="auth-email" placeholder="Work Email">
193
- <input type="password" class="input" id="auth-pass" placeholder="Password (Min 12 chars)">
194
- <button class="btn" style="width:100%;" id="auth-submit">Log In / Sign Up</button>
195
- <p style="text-align:center; color:var(--muted); margin-top:16px; font-size:14px;" id="auth-msg"></p>
196
- <button class="btn btn-ghost" style="width:100%; margin-top:16px;" id="auth-back">Back to Home</button>
197
- </div>
198
- </div>
199
- """
200
-
201
- def render_dashboard(user, table_html, status_msg=""):
202
- plan = user.get('plan', 'free').upper() if user else 'FREE'
203
- msg_html = f"<div style='color:var(--accent); margin-bottom:20px;'>{status_msg}</div>" if status_msg else ""
204
-
205
- return f"""
206
- <div class="navbar">
207
- <div class="logo">{ICON_BOLT} OmniParse AI</div>
208
- <div style="display:flex; gap:12px; align-items:center;">
209
- <span style="color:var(--muted); font-size:14px;">Plan: <strong style="color:var(--accent);">{plan}</strong></span>
210
- <button class="btn btn-ghost" id="dash-logout">Log Out</button>
211
- </div>
212
- </div>
213
- <div class="app-container" style="display:grid; grid-template-columns: 250px 1fr; gap: 30px;">
214
- <div class="card" style="padding: 16px;">
215
- <button class="btn btn-ghost" style="width:100%; margin-bottom:10px; text-align:left;" id="nav-upload">Upload Invoice</button>
216
- <button class="btn btn-ghost" style="width:100%; margin-bottom:10px; text-align:left;" id="nav-invoices">My Invoices</button>
217
- <button class="btn btn-ghost" style="width:100%; text-align:left;" id="nav-upgrade">Upgrade</button>
218
- </div>
219
- <div id="dash-content">
220
- {msg_html}
221
- {table_html}
222
- </div>
223
- </div>
224
- """
225
 
226
  # ==========================================
227
- # 4. GRADIO APP (Invisible Bridge)
228
  # ==========================================
229
- with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Default(primary_hue="emerald", neutral_hue="zinc")) as app:
230
- user_state = gr.State(None)
231
- ui_container = gr.HTML(render_landing())
 
 
 
 
 
 
232
 
233
- # Neviditelné Gradio komponenty pro backend komunikaci
234
- hidden_action = gr.Textbox(elem_id="hidden-action", visible=False)
235
- hidden_email = gr.Textbox(elem_id="hidden-email", visible=False)
236
- hidden_pass = gr.Textbox(elem_id="hidden-pass", visible=False)
237
- hidden_files = gr.File(elem_id="hidden-files", visible=False, file_count="multiple")
238
 
239
- # JavaScript inject pro přepínanie view a sběr dat
240
- gr.HTML("""
241
- <script>
242
- function triggerAction(action, data={}) {
243
- document.getElementById('hidden-action').value = action;
244
- document.getElementById('hidden-action').dispatchEvent(new Event('input'));
245
- if(data.email) document.getElementById('hidden-email').value = data.email;
246
- if(data.pass) document.getElementById('hidden-pass').value = data.pass;
247
- }
248
 
249
- document.addEventListener('click', function(e) {
250
- if(e.target.id === 'nav-auth' || e.target.id === 'nav-signup' || e.target.id === 'hero-cta') triggerAction('show_auth');
251
- if(e.target.id === 'auth-back') triggerAction('show_landing');
252
- if(e.target.id === 'auth-submit') triggerAction('auth', {email: document.getElementById('auth-email').value, pass: document.getElementById('auth-pass').value});
253
- if(e.target.id === 'dash-logout') triggerAction('logout');
 
 
254
 
255
- if(e.target.id === 'nav-upload') document.getElementById('hidden-files').click();
256
- if(e.target.id === 'nav-invoices') triggerAction('show_invoices');
257
- if(e.target.id === 'nav-upgrade') triggerAction('show_upgrade');
258
- });
259
- </script>
260
- """)
261
-
262
- def router(action, email, pass_val, files, user):
263
- if not action: return render_landing(), user
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
- if action == "show_auth":
266
- return render_auth(), user
 
 
 
 
267
 
268
- elif action == "show_landing":
269
- return render_landing(), user
270
-
271
- elif action == "auth":
272
- if not supabase: return render_auth().replace('<p id="auth-msg"></p>', '<p id="auth-msg" style="color:var(--accent);">DB Error</p>'), user
273
- # Try Login
274
- res = supabase.table("users").select("*").eq("email", email).execute()
275
- if res.data and verify_password(pass_val, res.data[0]['password']):
276
- user = res.data[0]
277
- return render_dashboard(user, "<div class='card'>Welcome back!</div>"), user
278
- # Try Signup
279
- if not res.data:
280
- if not validate_password_strength(pass_val):
281
- return render_auth().replace('<p id="auth-msg"></p>', '<p id="auth-msg" style="color:#ef4444;">Weak password.</p>'), user
282
- hashed = hash_password(pass_val)
283
- new_user = supabase.table("users").insert({"email": email, "password": hashed, "plan": "free"}).execute()
284
- user = new_user.data[0]
285
- return render_dashboard(user, "<div class='card'>Account created!</div>"), user
286
- return render_auth().replace('<p id="auth-msg"></p>', '<p id="auth-msg" style="color:#ef4444;">Invalid credentials.</p>'), user
287
-
288
- elif action == "logout":
289
- return render_landing(), None
290
-
291
- return render_landing(), user
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
- hidden_action.change(fn=router, inputs=[hidden_action, hidden_email, hidden_pass, hidden_files, user_state], outputs=[ui_container, user_state])
 
 
 
294
 
295
  if __name__ == "__main__":
296
  app.launch(server_name="0.0.0.0", server_port=7860)
 
116
  except Exception as e: return {"filename": os.path.basename(file_path), "status": "error"}
117
 
118
  # ==========================================
119
+ # 3. OBSIDIAN DESIGN SYSTEM (CSS)
120
  # ==========================================
121
  CUSTOM_CSS = """
 
122
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
123
  :root { --bg: #09090b; --card: rgba(255,255,255,0.03); --border: rgba(255,255,255,0.08); --text: #f4f4f5; --muted: #71717a; --accent: #10b981; --accent-glow: rgba(16,185,129,0.2); }
124
  * { box-sizing: border-box; font-family: 'Inter', sans-serif; }
125
+ body { background: var(--bg); color: var(--text); margin: 0; }
126
  .gradio-container { max-width: 100% !important; padding: 0 !important; background: var(--bg) !important; min-height: 100vh; }
127
  .hide { display: none !important; }
128
 
129
+ /* Layout */
130
+ #navbar { display: flex; justify-content: space-between; align-items: center; padding: 20px 40px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: rgba(9,9,11,0.8); backdrop-filter: blur(10px); z-index: 100; }
131
+ #hero { text-align: center; padding: 100px 20px; }
132
+ #hero h1 { font-size: 56px; font-weight: 700; margin-bottom: 24px; background: linear-gradient(180deg, #fff 0%, #a1a1aa 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; line-height: 1.1; }
133
+ #hero p { font-size: 20px; color: var(--muted); max-width: 600px; margin: 0 auto 40px; }
134
+ .dash-container { display: flex; padding: 40px; max-width: 1400px; margin: 0 auto; gap: 30px; }
135
+ .dash-sidebar { width: 250px; display: flex; flex-direction: column; gap: 10px; }
136
+ .dash-main { flex: 1; background: var(--card); border: 1px solid var(--border); border-radius: 16px; padding: 24px; backdrop-filter: blur(20px); }
137
+
138
+ /* Gradio Component Overrides */
139
+ .gr-button { background: var(--accent) !important; color: #000 !important; border: none !important; border-radius: 8px !important; padding: 12px 24px !important; font-weight: 600 !important; transition: 0.2s !important; box-shadow: none !important; }
140
+ .gr-button:hover { box-shadow: 0 0 20px var(--accent-glow) !important; transform: translateY(-1px) !important; }
141
+ .btn-ghost { background: transparent !important; border: 1px solid var(--border) !important; color: var(--text) !important; }
142
+ .btn-ghost:hover { border-color: var(--accent) !important; color: var(--accent) !important; }
143
+ .btn-danger { background: #ef4444 !important; color: #fff !important; }
144
+
145
+ .gr-textbox, .gr-dropdown { background: rgba(0,0,0,0.3) !important; border: 1px solid var(--border) !important; border-radius: 8px !important; color: var(--text) !important; }
146
+ .gr-textbox:focus { border-color: var(--accent) !important; }
147
+ .gr-form { background: transparent !important; border: none !important; }
148
+ .gr-box { border: none !important; background: transparent !important; }
149
+ .gr-dataframe table { background: transparent !important; color: var(--text) !important; border-collapse: collapse; width: 100%; }
150
+ .gr-dataframe th { background: rgba(255,255,255,0.05) !important; border-bottom: 1px solid var(--border) !important; color: var(--accent) !important; text-align: left; padding: 12px; }
151
+ .gr-dataframe td { border-bottom: 1px solid var(--border) !important; padding: 12px; }
152
+ .gr-markdown { color: var(--text) !important; background: transparent !important; }
 
 
153
  footer { display: none !important; }
154
  """
155
 
156
+ ICON_BOLT = '<svg style="width:24px;height:24px;display:inline-block;vertical-align:middle;margin-right:10px;fill:var(--accent);" viewBox="0 0 24 24"><path d="M11 21h-1l1-7H7.5c-.58 0-.57-.32-.38-.66.19-.34.05-.08.07-.12C8.48 10.94 10.42 7.54 13 3h1l-1 7h3.5c.49 0 .56.33.47.51l-.07.15C12.96 17.55 11 21 11 21z"/></svg>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
  # ==========================================
159
+ # 4. BACKEND FUNCTIONS
160
  # ==========================================
161
+ def get_user_invoices(user_dict):
162
+ if not user_dict or not supabase: return pd.DataFrame()
163
+ res = supabase.table("invoices").select("*").eq("user_id", user_dict['id']).order("created_at", desc=True).limit(100).execute()
164
+ return pd.DataFrame(res.data)
165
+
166
+ def handle_auth(email, password, request: gr.Request):
167
+ ip = request.client.host if request else "127.0.0.1"
168
+ if rate_limiter.is_rate_limited(f"auth_{ip}", 5, 300):
169
+ return None, "🚨 Příliš mnoho pokusů.", gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
170
 
171
+ if not supabase: return None, "DB Error", gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
 
 
 
 
172
 
173
+ res = supabase.table("users").select("*").eq("email", email).execute()
174
+ if res.data and verify_password(password, res.data[0]['password']):
175
+ user = res.data[0]
176
+ return user, "Přihlášení úspěšné.", gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)
 
 
 
 
 
177
 
178
+ if not res.data:
179
+ if not validate_password_strength(password):
180
+ return None, "Slabé heslo (min 12 znaků, 1 velké, 1 číslo, 1 symbol).", gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
181
+ hashed = hash_password(password)
182
+ new_user = supabase.table("users").insert({"email": email, "password": hashed, "plan": "free"}).execute()
183
+ user = new_user.data[0]
184
+ return user, "Účet vytvořen!", gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)
185
 
186
+ return None, "Neplatné údaje.", gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
187
+
188
+ def handle_upload(files, user_dict):
189
+ if not user_dict: return "Nejste přihlášen.", get_user_invoices(user_dict)
190
+ if not files: return "Žádné soubory.", get_user_invoices(user_dict)
191
+ for file in files:
192
+ if os.path.getsize(file.name) > 20 * 1024 * 1024: continue
193
+ process_invoice_file(file.name, user_dict['id'])
194
+ return f"Zpracováno {len(files)} souborů.", get_user_invoices(user_dict)
195
+
196
+ def create_stripe_checkout(plan: str, user_dict):
197
+ if not user_dict or not STRIPE_SECRET_KEY: return "Platby nejsou nakonfigurovány."
198
+ prices = {"basic": STRIPE_PRICE_BASIC, "pro": STRIPE_PRICE_PRO, "enterprise": STRIPE_PRICE_ENTERPRISE}
199
+ try:
200
+ session = stripe.checkout.Session.create(
201
+ payment_method_types=['card'], line_items=[{"price": prices[plan], "quantity": 1}],
202
+ mode='subscription', success_url=f"{APP_URL}?checkout=success&session_id={{CHECKOUT_SESSION_ID}}",
203
+ cancel_url=f"{APP_URL}?checkout=cancel", customer_email=user_dict['email'],
204
+ metadata={"user_id": user_dict['id'], "plan": plan}
205
+ )
206
+ return f"[Zaplatit zde]({session.url})"
207
+ except Exception as e: return f"Chyba: {e}"
208
+
209
+ # ==========================================
210
+ # 5. GRADIO UI ARCHITECTURE
211
+ # ==========================================
212
+ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Default(primary_hue="emerald", neutral_hue="zinc")) as app:
213
+ user_state = gr.State(None)
214
+
215
+ # ---------------- VIEW 1: LANDING ----------------
216
+ with gr.Group(visible=True) as landing_view:
217
+ with gr.Row(elem_id="navbar"):
218
+ gr.HTML(f"<div style='font-weight:700;font-size:20px;'>{ICON_BOLT}OmniParse AI</div>")
219
+ landing_login_btn = gr.Button("Log In", elem_classes="btn-ghost", size="sm")
220
+ with gr.Column(elem_id="hero"):
221
+ gr.HTML("<h1>Invoice processing in <span style='color: var(--accent);'>seconds</span>, not hours.</h1>")
222
+ gr.HTML("<p>AI extracts vendor, dates, amounts and line items from any PDF or image. Export to CSV, JSON or Excel.</p>")
223
+ landing_signup_btn = gr.Button("Start Free — 20 invoices", size="lg")
224
+ gr.HTML("<div style='margin-top: 40px; color: var(--muted); font-size: 14px;'>99.2% accuracy · &lt;4s per invoice · 40+ formats</div>")
225
+
226
+ # ---------------- VIEW 2: AUTH ----------------
227
+ with gr.Group(visible=False) as auth_view:
228
+ with gr.Column(elem_id="hero", elem_classes="auth-container"):
229
+ gr.HTML("<h2 style='margin-bottom:24px;'>Welcome to OmniParse</h2>")
230
+ auth_email = gr.Textbox(label="Work Email", placeholder="you@company.com")
231
+ auth_pass = gr.Textbox(label="Password", type="password", placeholder="Min 12 chars, 1 upper, 1 symbol")
232
+ auth_submit = gr.Button("Log In / Sign Up", size="lg")
233
+ auth_msg = gr.Markdown("")
234
+ auth_back = gr.Button("Back to Home", elem_classes="btn-ghost", size="sm")
235
+
236
+ # ---------------- VIEW 3: DASHBOARD ----------------
237
+ with gr.Group(visible=False) as dash_view:
238
+ with gr.Row(elem_id="navbar"):
239
+ gr.HTML(f"<div style='font-weight:700;font-size:20px;'>{ICON_BOLT}Dashboard</div>")
240
+ dash_logout = gr.Button("Log Out", elem_classes="btn-danger", size="sm")
241
 
242
+ with gr.Row(elem_classes="dash-container"):
243
+ # Sidebar
244
+ with gr.Column(elem_classes="dash-sidebar"):
245
+ nav_upload = gr.Button("Upload Invoice", elem_classes="btn-ghost")
246
+ nav_invoices = gr.Button("My Invoices", elem_classes="btn-ghost")
247
+ nav_upgrade = gr.Button("Upgrade Plan", elem_classes="btn-ghost")
248
 
249
+ # Main Content Area
250
+ with gr.Column(elem_classes="dash-main"):
251
+ # Upload Panel
252
+ with gr.Group(visible=True) as panel_upload:
253
+ gr.HTML("<h3>Upload Invoices</h3>")
254
+ file_input = gr.File(file_count="multiple", file_types=[".pdf", ".png", ".jpg"])
255
+ process_btn = gr.Button("Process Invoices", variant="primary")
256
+ upload_status = gr.Markdown("")
257
+
258
+ # Invoices Panel
259
+ with gr.Group(visible=False) as panel_invoices:
260
+ gr.HTML("<h3>Processed Invoices</h3>")
261
+ invoices_table = gr.Dataframe(headers=["Vendor", "Invoice#", "Total", "Status"], wrap=True, interactive=False)
262
+ export_btn = gr.Button("Export to CSV", elem_classes="btn-ghost")
263
+ csv_file = gr.File(label="Download CSV", visible=False)
264
+
265
+ # Upgrade Panel
266
+ with gr.Group(visible=False) as panel_upgrade:
267
+ gr.HTML("<h3>Upgrade Your Plan</h3>")
268
+ with gr.Row():
269
+ gr.HTML("<div style='text-align:center;padding:20px;'><h4>Basic</h4><h2 style='color:var(--accent);'>$29/mo</h2></div>")
270
+ gr.HTML("<div style='text-align:center;padding:20px;'><h4>Pro</h4><h2 style='color:var(--accent);'>$129/mo</h2></div>")
271
+ gr.HTML("<div style='text-align:center;padding:20px;'><h4>Enterprise</h4><h2 style='color:var(--accent);'>$499/mo</h2></div>")
272
+ with gr.Row():
273
+ up_basic = gr.Button("Choose Basic")
274
+ up_pro = gr.Button("Choose Pro")
275
+ up_ent = gr.Button("Choose Enterprise")
276
+ checkout_link = gr.Markdown("")
277
+
278
+ # ==========================================
279
+ # 6. EVENT BINDINGS (FUNKČNÍ INTERAKCE)
280
+ # ==========================================
281
+ # Navigation Logic
282
+ def show_auth(): return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
283
+ def show_landing(): return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
284
+ def show_dashboard(user):
285
+ if not user: return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
286
+ return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)
287
+
288
+ landing_login_btn.click(fn=show_auth, outputs=[landing_view, auth_view, dash_view])
289
+ landing_signup_btn.click(fn=show_auth, outputs=[landing_view, auth_view, dash_view])
290
+ auth_back.click(fn=show_landing, outputs=[landing_view, auth_view, dash_view])
291
+
292
+ # Auth Logic
293
+ auth_submit.click(
294
+ fn=handle_auth,
295
+ inputs=[auth_email, auth_pass],
296
+ outputs=[user_state, auth_msg, landing_view, auth_view, dash_view]
297
+ ).then(
298
+ fn=get_user_invoices, inputs=[user_state], outputs=[invoices_table]
299
+ )
300
+
301
+ dash_logout.click(fn=lambda: (None, *show_landing()), outputs=[user_state, landing_view, auth_view, dash_view])
302
+
303
+ # Dashboard Navigation Logic
304
+ def switch_panel(panel_name):
305
+ u, i, p = gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
306
+ if panel_name == "upload": u = gr.update(visible=True)
307
+ elif panel_name == "invoices": i = gr.update(visible=True)
308
+ elif panel_name == "upgrade": p = gr.update(visible=True)
309
+ return u, i, p
310
+
311
+ nav_upload.click(fn=lambda: switch_panel("upload"), outputs=[panel_upload, panel_invoices, panel_upgrade])
312
+ nav_invoices.click(fn=lambda: switch_panel("invoices"), outputs=[panel_upload, panel_invoices, panel_upgrade])
313
+ nav_upgrade.click(fn=lambda: switch_panel("upgrade"), outputs=[panel_upload, panel_invoices, panel_upgrade])
314
+
315
+ # Processing Logic
316
+ process_btn.click(fn=handle_upload, inputs=[file_input, user_state], outputs=[upload_status, invoices_table])
317
+
318
+ def handle_export(user_dict):
319
+ df = get_user_invoices(user_dict)
320
+ if df.empty: return None
321
+ temp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
322
+ df.to_csv(temp.name, index=False)
323
+ return gr.update(value=temp.name, visible=True)
324
+ export_btn.click(fn=handle_export, inputs=[user_state], outputs=[csv_file])
325
 
326
+ # Stripe Logic
327
+ up_basic.click(fn=lambda u: create_stripe_checkout("basic", u), inputs=[user_state], outputs=[checkout_link])
328
+ up_pro.click(fn=lambda u: create_stripe_checkout("pro", u), inputs=[user_state], outputs=[checkout_link])
329
+ up_ent.click(fn=lambda u: create_stripe_checkout("enterprise", u), inputs=[user_state], outputs=[checkout_link])
330
 
331
  if __name__ == "__main__":
332
  app.launch(server_name="0.0.0.0", server_port=7860)