import gradio as gr import os import pandas as pd import requests from supabase import create_client, Client from datetime import datetime, timedelta, timezone # 以下 import 供 LINE Pay 功能使用,停用中 # import base64 # import hashlib # import hmac # import uuid # import urllib.parse # --- 設定時區與環境變數 --- TAIPEI_TZ = timezone(timedelta(hours=8)) SUPABASE_URL = os.getenv("SUPABASE_URL") SUPABASE_KEY = os.getenv("SUPABASE_KEY") GAS_MAIL_URL = os.getenv("GAS_MAIL_URL") API_BASE_URL = os.getenv("API_BASE_URL", "https://deeplearning101-api-ciecietaipei.hf.space") LINE_ACCESS_TOKEN = os.getenv("LINE_ACCESS_TOKEN") # Resend / LINE Pay(停用中) # RESEND_API_KEY = os.getenv("RESEND_API_KEY", "") # LINE_PAY_CHANNEL_ID = os.getenv("LINE_PAY_CHANNEL_ID", "") # LINE_PAY_CHANNEL_SECRET = os.getenv("LINE_PAY_CHANNEL_SECRET", "") # LINE_PAY_BASE_URL = "https://sandbox-api-pay.line.me" PUBLIC_SPACE_URL = "https://ciecietaipei.github.io/booking.html" REAL_ADMIN_USER = os.getenv("ADMIN_USER") or "Deep Learning 101" REAL_ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD") or "2016-11-11" supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) # ========================================== # 模組 0:信件確認 Webhook(停用中) # # 原本用意:客人點信件連結會導向此 Gradio Space, # 由此攔截 URL 參數更新狀態。 # 現在改成導向 FastAPI /api/booking/action, # 再 redirect 回 booking.html,此函式不再被呼叫。 # ========================================== # def check_url_action(request: gr.Request): # if not request: return gr.update(visible=False), gr.update(visible=True) # action = request.query_params.get('action') # bid = request.query_params.get('id') # if action == 'confirm' and bid: # try: # supabase.table("bookings").update({"status": "顧客已確認"}).eq("id", bid).execute() # msg = "

✅ 訂位已成功確認!

感謝您的回覆,期待您的光臨。您現在可以關閉此視窗。

" # return gr.update(value=msg, visible=True), gr.update(visible=False) # except Exception as e: # return gr.update(value=f"❌ 處理失敗: {str(e)}", visible=True), gr.update(visible=False) # elif action == 'cancel' and bid: # try: # supabase.table("bookings").update({"status": "顧客已取消"}).eq("id", bid).execute() # msg = "

🚫 訂位已取消。

期待下次為您服務。您現在可以關閉此視窗。

" # return gr.update(value=msg, visible=True), gr.update(visible=False) # except Exception as e: # return gr.update(value=f"❌ 處理失敗: {str(e)}", visible=True), gr.update(visible=False) # return gr.update(visible=False), gr.update(visible=True) # ========================================== # 模組 1:訂位與通知管理 # ========================================== def get_bookings(): try: res = supabase.table("bookings").select("*").order("created_at", desc=True).execute() if not res.data: return pd.DataFrame() return pd.DataFrame(res.data) except Exception as e: print("Fetch bookings error:", e) return pd.DataFrame() def send_confirmation_hybrid(booking_id): if not booking_id: return "❌ 請輸入訂單 ID" try: res = supabase.table("bookings").select("*").eq("id", booking_id).execute() if not res.data: return f"❌ 找不到 ID: {booking_id}" booking = res.data[0] email = booking.get('email') user_id = booking.get('user_id') current_status = booking.get('status', '') # 禁止對 No-Show 或已取消的訂單誤發確認信 if current_status in ("No-Show", "顧客已取消"): return f"⛔ 狀態為【{current_status}】,不可發送確認信" confirm_link = f"{API_BASE_URL}/api/booking/action?action=confirm&id={booking_id}" cancel_link = f"{API_BASE_URL}/api/booking/action?action=cancel&id={booking_id}" log_msg = f"🆔 {booking_id} 處理結果: " # 原版 bug:is_reminder = "確認" in current_status # → "已發確認信" 也含「確認」字,導致第二次點還是發提醒而非首次確認 # 修正:精確比對已發過確認信的狀態 is_reminder = current_status in ("已發確認信", "顧客已確認") # ── 催付款流程(LINE Pay 停用,此區塊一併停用)──────────── # is_pending_payment = "待付款" in current_status # if is_pending_payment: # order_id = None # for line in booking.get('remarks', '').split('\n'): # if "訂單號:" in line: # order_id = line.split(":")[-1].strip() # payment_link = f"{PUBLIC_SPACE_URL}?action=repay&order_id={order_id}" if order_id else "" # mail_subject = f"💳 提醒付款: {booking['date']} 訂單 - Cié Cié Taipei" # line_text = (f"💳 付款提醒\n\n{booking['name']} 您好,您有一筆訂單尚未完成付款。\n\n" # f"📅 日期:{booking['date']}\n⏰ 時間:{booking['time']}\n\n" # f"👉 請點擊下方連結完成線上結帳:\n{payment_link}\n\n如已付款請忽略此訊息。") # mail_html = f"""
#

Cié Cié Taipei

#

{booking['name']} 您好,您的訂單尚未完成付款:

# #
# 💳 前往完成付款 #
""" # ── 催付款流程結束 ──────────────────────────────────────── if is_reminder: # 行前提醒模式 mail_subject = f"🔔 行前提醒: {booking['date']} - Cié Cié Taipei" line_text = (f"🔔 行前提醒\n\n{booking['name']} 您好,期待今晚與您相見!\n\n" f"📅 日期:{booking['date']}\n⏰ 時間:{booking['time']}\n👥 人數:{booking['pax']} 位\n\n" f"座位已為您準備好,若需變更請聯繫我們。") mail_html = f"""

Cié Cié Taipei

{booking['name']} 您好,行前提醒:

無需再次確認。

若無法前來,請點此取消
""" else: # 首次確認信(含確認出席 / 取消 按鈕) mail_subject = f"訂位確認: {booking['date']} - Cié Cié Taipei" line_text = (f"✅ 訂位確認\n\n{booking['name']} 您好,已收到您的預約。\n\n" f"📅 日期:{booking['date']}\n⏰ 時間:{booking['time']}\n👥 人數:{booking['pax']} 位\n\n" f"👉 請務必點擊下方連結「確認出席」,謝謝!\n{confirm_link}") mail_html = f"""

Cié Cié Taipei

{booking['name']} 您好,請確認您的訂位:

✅ 確認出席 🚫 取消
""" # ── Email 發送:GAS 唯一機制(原版先嘗試 Resend,失敗才 fallback GAS)── # 原版 Resend 段落(停用,Resend 無法使用): # if RESEND_API_KEY: # try: # response = requests.post( # "https://api.resend.com/emails", # headers={"Authorization": f"Bearer {RESEND_API_KEY}", "Content-Type": "application/json"}, # json={"from": "Cié Cié Taipei ", "to": [email], "subject": mail_subject, "html": mail_html}, # timeout=15 # ) # if response.status_code in (200, 201): # log_msg += "✅ Mail送出 "; mail_sent = True # else: # log_msg += f"⚠️ Resend失敗({response.status_code}) " # except Exception as e: # log_msg += "⚠️ Resend異常 " if email and "@" in email: if GAS_MAIL_URL: try: response = requests.post( GAS_MAIL_URL, json={"to": email, "subject": mail_subject, "htmlBody": mail_html, "name": "Cié Cié Taipei"}, timeout=15 ) if response.status_code == 200: log_msg += "✅ Mail送出 " else: log_msg += f"❌ Mail失敗(HTTP {response.status_code}) " print(f"❌ GAS Mail 錯誤: {response.status_code} - {response.text}") except Exception as e: log_msg += "❌ Mail異常 " print(f"❌ GAS Mail 異常: {str(e)}") else: log_msg += "⚠️ 未設定GAS_MAIL_URL " else: log_msg += "⚠️ 無Mail " # LINE 推播(輔助,失敗只 log) if user_id and len(str(user_id)) > 5 and LINE_ACCESS_TOKEN: try: r = requests.post( "https://api.line.me/v2/bot/message/push", headers={"Authorization": f"Bearer {LINE_ACCESS_TOKEN}"}, json={"to": user_id, "messages": [{"type": "text", "text": line_text}]}, timeout=10 ) if r.status_code == 200: log_msg += "✅ LINE送出 " else: log_msg += f"⚠️ LINE失敗({r.status_code}) " except Exception as e: log_msg += "⚠️ LINE異常 " print(f"❌ LINE 異常: {str(e)}") else: log_msg += "⚠️ 無LINE ID " # 只有首次確認才更新狀態(提醒不改狀態) if not is_reminder: supabase.table("bookings").update({"status": "已發確認信"}).eq("id", booking_id).execute() return log_msg except Exception as e: return f"嚴重錯誤: {str(e)}" def toggle_no_show(booking_id, is_noshow=True): if not booking_id: return "❌ 請輸入訂單 ID" status_text = "No-Show" if is_noshow else "待處理" try: supabase.table("bookings").update({"status": status_text}).eq("id", booking_id).execute() if is_noshow: return f"🚫 訂單 {booking_id} 已成功標記為 No-Show 黑名單!" else: return f"✅ 訂單 {booking_id} 已撤銷 No-Show,恢復為「待處理」。" except Exception as e: return f"❌ 錯誤: {str(e)}" def update_booking_status(booking_id, new_status): if not booking_id: return "❌ 請輸入訂單 ID" if not new_status: return "⚠️ 請選擇要更改的新狀態" try: res = supabase.table("bookings").update({"status": new_status}).eq("id", booking_id).execute() if not res.data: raise Exception("找不到該筆訂單或權限不足") return f"✅ 訂單 {booking_id} 狀態已成功更新為:【{new_status}】" except Exception as e: return f"❌ 狀態更新失敗: {str(e)}" # LINE Pay 付款狀態查詢(停用中) # def check_linepay_status(booking_id): # if not booking_id: return "❌ 請輸入訂單 ID" # if not LINE_PAY_CHANNEL_SECRET: return "⚠️ 系統未設定 LINE Pay 金鑰,無法查詢" # try: # res = supabase.table("bookings").select("*").eq("id", booking_id).execute() # if not res.data: return f"❌ 找不到訂單 ID: {booking_id}" # remarks = res.data[0].get('remarks', '') # order_id = None # for line in remarks.split('\n'): # if "訂單號:" in line: # order_id = line.split(":")[-1].strip() # if not order_id: # return "⚠️ 此筆訂單沒有 LINE Pay 訂單號 (可能為免訂金或舊訂單)" # uri = "/v3/payments" # query_string = urllib.parse.urlencode({"orderId": order_id}) # nonce = str(uuid.uuid4()) # message = LINE_PAY_CHANNEL_SECRET + uri + query_string + nonce # signature = base64.b64encode(hmac.new(LINE_PAY_CHANNEL_SECRET.encode(), message.encode(), hashlib.sha256).digest()).decode() # headers = {"Content-Type": "application/json", "X-LINE-ChannelId": LINE_PAY_CHANNEL_ID, "X-LINE-Authorization-Nonce": nonce, "X-LINE-Authorization": signature} # r = requests.get(f"{LINE_PAY_BASE_URL}{uri}?{query_string}", headers=headers) # res_data = r.json() # if res_data.get("returnCode") == "0000": # info = res_data.get("info", []) # if not info: return "ℹ️ LINE Pay 回報:此訂單尚未有交易紀錄" # tx = info[0] # status_map = {"AUTHORIZATION": "授權中", "PAYMENT": "✅ 付款成功", "CANCELED": "🚫 已取消", "FAIL": "❌ 付款失敗"} # status_text = status_map.get(tx.get("transactionType"), tx.get("transactionType")) # amount = tx.get("payInfo", [{}])[0].get("amount", 0) # return f"🔍 LINE Pay 查詢結果: [{status_text}] 金額: ${amount} (交易序號: {tx.get('transactionId')})" # else: # return f"❌ 查詢失敗: {res_data.get('returnMessage')}" # except Exception as e: # return f"❌ 系統錯誤: {str(e)}" def render_booking_cards(): df = get_bookings() count_html = f"
📊 共 {len(df)} 筆資料
" if df.empty: return f"{count_html}
📭 目前沒有訂位資料
" cards_html = f"{count_html}
" for index, row in df.iterrows(): status = row.get('status', '待處理') has_line = bool(row.get('user_id') and len(str(row.get('user_id'))) > 5) status_bg = "#ccc"; status_tx = "#000"; border_color = "#444" if '確認' in status: status_bg = "#2ecc71"; status_tx = "#000"; border_color = "#2ecc71" elif '取消' in status: status_bg = "#e74c3c"; status_tx = "#fff"; border_color = "#e74c3c" elif '已發' in status: status_bg = "#f1c40f"; status_tx = "#000"; border_color = "#f1c40f" elif 'No-Show' in status: status_bg = "#000"; status_tx = "#e74c3c"; border_color = "#e74c3c" # LINE Pay 付款狀態(停用中) # elif '付款' in status: status_bg = "#3498db"; status_tx = "#fff"; border_color = "#3498db" line_badge = "LINE綁定" if has_line else "" card = f"""
訂單 ID {row['id']}
{status}
{line_badge}
📅 日期
{row['date']}
⏰ 時間
{row['time']}
{row['name']} ({row['pax']}位)
📞 {row['tel']}
✉️ {row['email'] or '-'}
📝 備註: {row.get('remarks') or '無'}
""" cards_html += card cards_html += "
" return cards_html # ========================================== # 模組 2:菜單動態管理 # ========================================== def get_menu_items(): try: res = supabase.table("menu_items").select("*").order("category").order("created_at").execute() if not res.data: return pd.DataFrame(columns=['餐點名稱', '價格', '限量', '分類', '供應時段', '可外帶', '預付規則', '狀態', '照片連結']) df = pd.DataFrame(res.data) df['available_times'] = df['available_times'].apply(lambda x: "、".join(x) if isinstance(x, list) else "全時段") df['allow_takeout'] = df['allow_takeout'].apply(lambda x: "✅" if x else "❌") df['require_prepay'] = df['require_prepay'].apply(lambda x: "🔥 需預付" if x else "一般") df['is_active'] = df['is_active'].apply(lambda x: "🟢 販售中" if x else "🔴 已下架") df['has_image'] = df.get('image_url', pd.Series()).apply(lambda x: "🔗 有" if pd.notnull(x) and str(x).strip() else "無") df['daily_limit_display'] = df.get('daily_limit', pd.Series([999]*len(df))).apply( lambda x: "無限制" if pd.isna(x) or float(x) >= 999 else f"剩 {int(float(x))} 份" ) display_df = df[['name', 'price', 'daily_limit_display', 'category', 'available_times', 'allow_takeout', 'require_prepay', 'is_active', 'has_image']] display_df.columns = ['餐點名稱', '價格', '限量', '分類', '供應時段', '可外帶', '預付規則', '狀態', '照片連結'] return display_df except Exception as e: print("Fetch menu error:", e) return pd.DataFrame(columns=['餐點名稱', '價格', '限量', '分類', '供應時段', '可外帶', '預付規則', '狀態', '照片連結']) def update_menu_dropdown(): try: res = supabase.table("menu_items").select("id, name, is_active").execute() if not res.data: return gr.update(choices=[]) choices = [f"[{'🟢販售中' if item['is_active'] else '🔴已下架'}] {item['name']} | {item['id']}" for item in res.data] return gr.update(choices=choices) except: return gr.update(choices=[]) def load_menu_data(selected_string): if not selected_string: return "", "", 0, 999, "main", ["晚餐 (18:30-21:30)", "宵夜 (21:30後)"], False, True, "", "" item_id = selected_string.split(" | ")[-1].strip() try: res = supabase.table("menu_items").select("*").eq("id", item_id).execute() if not res.data: return "", "", 0, 999, "main", ["晚餐 (18:30-21:30)", "宵夜 (21:30後)"], False, True, "", "" item = res.data[0] times = item.get("available_times", []) if not times: times = ["晚餐 (18:30-21:30)", "宵夜 (21:30後)"] return ( item.get("name", ""), item.get("description", ""), item.get("price", 0), item.get("daily_limit") if item.get("daily_limit") is not None else 999, item.get("category", "main"), times, item.get("require_prepay", False), item.get("allow_takeout", True), item.get("image_url", "") or "", item_id ) except: return "", "", 0, 999, "main", ["晚餐 (18:30-21:30)", "宵夜 (21:30後)"], False, True, "", "" def add_menu_item(name, desc, price, limit, category, available_times, require_prepay, allow_takeout, image_url_input): if not name or price is None: return "⚠️ 名稱與價格為必填", get_menu_items() if not available_times: return "⚠️ 請至少選擇一個供應時段", get_menu_items() final_image_url = image_url_input.strip() if image_url_input and str(image_url_input).strip() else None try: data = { "name": name, "description": desc, "price": int(price), "daily_limit": int(limit) if limit is not None else 999, "category": category, "available_times": available_times, "allow_takeout": allow_takeout, "require_prepay": require_prepay, "is_active": True, "image_url": final_image_url } supabase.table("menu_items").insert(data).execute() return f"✅ 成功新增上架:{name}", get_menu_items() except Exception as e: return f"❌ 錯誤: {str(e)}", get_menu_items() def update_menu_item(item_id, name, desc, price, limit, category, available_times, require_prepay, allow_takeout, image_url_input): if not item_id: return "⚠️ 請先從右側選擇並「載入編輯」一項餐點", get_menu_items() if not name or price is None: return "⚠️ 名稱與價格為必填", get_menu_items() final_image_url = image_url_input.strip() if image_url_input and str(image_url_input).strip() else None try: data = { "name": name, "description": desc, "price": int(price), "daily_limit": int(limit) if limit is not None else 999, "category": category, "available_times": available_times, "allow_takeout": allow_takeout, "require_prepay": require_prepay, "image_url": final_image_url } supabase.table("menu_items").update(data).eq("id", item_id).execute() return f"💾 成功儲存修改:{name}", get_menu_items() except Exception as e: return f"❌ 錯誤: {str(e)}", get_menu_items() def toggle_menu_item(selected_string, is_active): if not selected_string: return "⚠️ 請選擇餐點", get_menu_items() try: item_id = selected_string.split(" | ")[-1].strip() supabase.table("menu_items").update({"is_active": is_active}).eq("id", item_id).execute() status_text = "上架" if is_active else "下架" return f"✅ 餐點狀態已更新為:{status_text}", get_menu_items() except Exception as e: return f"❌ 錯誤: {str(e)}", get_menu_items() # ========================================== # Gradio 介面 # ========================================== def check_login(user, password): if user == REAL_ADMIN_USER and password == REAL_ADMIN_PASSWORD: return {login_row: gr.update(visible=False), admin_tabs: gr.update(visible=True), error_msg: ""} return {error_msg: "❌ 帳號或密碼錯誤"} custom_css = """ body, .gradio-container { background-color: #0F0F0F; color: #fff; } #op-panel { position: sticky; top: 0; z-index: 100; background: #1a1a1a; border-bottom: 2px solid #d4af37; padding: 15px; margin-bottom: 20px; box-shadow: 0 4px 10px rgba(0,0,0,0.5); } #booking_display { height: auto !important; overflow: visible !important; } """ with gr.Blocks(title="Cié Cié Admin", css=custom_css, theme=gr.themes.Monochrome()) as demo: # URL 參數攔截(停用中 — 改由 FastAPI /api/booking/action 處理) # url_action_msg = gr.HTML(visible=False) with gr.Group(visible=True) as login_row: gr.Markdown("# 🔒 老闆/管理員登入") with gr.Row(): username_input = gr.Textbox(label="使用者名稱") password_input = gr.Textbox(label="密碼", type="password") login_btn = gr.Button("登入系統", variant="primary") error_msg = gr.Markdown("") with gr.Tabs(visible=False) as admin_tabs: # --- 分頁 1:訂位與通知管理 --- with gr.TabItem("🍷 訂位與通知管理"): with gr.Column(elem_id="op-panel"): gr.Markdown("### 🚀 訂單操作控制台") with gr.Row(): id_input = gr.Number(label="輸入訂單 ID", precision=0, scale=1) new_status_dropdown = gr.Dropdown( label="手動選擇新狀態", # 已移除「待付款」、「待處理 (已付訂金)」(LINE Pay 停用) choices=["待處理", "已發確認信", "顧客已確認", "顧客已取消", "No-Show", "已完成 (結案)"], scale=1 ) with gr.Row(): send_btn = gr.Button("🚀 發信/LINE", variant="primary", scale=1) update_status_btn = gr.Button("💾 變更狀態", variant="secondary", scale=1) # LINE Pay 查詢按鈕(停用中) # check_lp_btn = gr.Button("🔍 查 LINE Pay (自動救援)", variant="secondary", scale=1) noshow_btn = gr.Button("🚫 標記 No-Show", variant="stop", scale=1) revert_noshow_btn = gr.Button("✅ 撤銷 No-Show", scale=1) refresh_btn = gr.Button("🔄 刷新列表", scale=1) log_output = gr.Textbox(label="執行結果日誌", lines=1) booking_display = gr.HTML(elem_id="booking_display") update_status_btn.click(update_booking_status, inputs=[id_input, new_status_dropdown], outputs=log_output).then(render_booking_cards, outputs=booking_display) # check_lp_btn.click(check_linepay_status, inputs=[id_input], outputs=log_output) # LINE Pay 停用 refresh_btn.click(render_booking_cards, outputs=booking_display) send_btn.click(send_confirmation_hybrid, inputs=id_input, outputs=log_output).then(render_booking_cards, outputs=booking_display) noshow_btn.click(toggle_no_show, inputs=[id_input, gr.State(True)], outputs=log_output).then(render_booking_cards, outputs=booking_display) revert_noshow_btn.click(toggle_no_show, inputs=[id_input, gr.State(False)], outputs=log_output).then(render_booking_cards, outputs=booking_display) # --- 分頁 2:菜單動態管理 --- with gr.TabItem("🍽️ 菜單動態管理"): gr.Markdown("### ✨ 上架與編輯餐點") m_edit_id = gr.State("") with gr.Row(): with gr.Column(scale=1): m_name = gr.Textbox(label="餐點名稱 *") m_desc = gr.Textbox(label="餐點描述 (選填)") with gr.Row(): m_price = gr.Number(label="價格 (TWD) *", precision=0) m_limit = gr.Number(label="每日限量 (999代表無限制)", value=999, precision=0) m_cat = gr.Dropdown(choices=["main", "snack", "drink", "other"], label="分類", value="main") m_image_url = gr.Textbox(label="餐點照片網址 (選填)", placeholder="例如: https://ciecietaipei.github.io/assets/steak.jpg") m_times = gr.CheckboxGroup( choices=["白天 (11:00-18:30)", "晚餐 (18:30-21:30)", "宵夜 (21:30後)"], value=["晚餐 (18:30-21:30)", "宵夜 (21:30後)"], label="🕒 適用時段 *" ) with gr.Row(): m_takeout = gr.Checkbox(label="🛍️ 開放外帶", value=True) m_prepay = gr.Checkbox(label="🔥 需全額預付", value=False) with gr.Row(): m_add_btn = gr.Button("➕ 作為新餐點上架", variant="primary") m_update_btn = gr.Button("💾 儲存修改 (需先載入)", variant="secondary") m_form_log = gr.Textbox(label="執行結果", interactive=False) with gr.Column(scale=2): gr.Markdown("### 📋 目前線上菜單") menu_df = gr.Dataframe(interactive=False, wrap=True) m_refresh_btn = gr.Button("🔄 刷新菜單") gr.Markdown("#### ⚙️ 快速操作 (編輯與上下架)") with gr.Row(): m_toggle_dropdown = gr.Dropdown(label="選擇要操作的餐點", choices=[], scale=3) m_load_btn = gr.Button("✏️ 載入編輯", scale=1) m_set_active = gr.Button("🟢 上架", scale=1) m_set_inactive = gr.Button("🔴 下架", scale=1) m_toggle_log = gr.Textbox(label="操作狀態", interactive=False) m_load_btn.click( load_menu_data, inputs=[m_toggle_dropdown], outputs=[m_name, m_desc, m_price, m_limit, m_cat, m_times, m_prepay, m_takeout, m_image_url, m_edit_id] ).then(lambda: "✅ 已載入至左側表單,修改後請點擊「儲存修改」", outputs=m_toggle_log) m_add_btn.click( add_menu_item, inputs=[m_name, m_desc, m_price, m_limit, m_cat, m_times, m_prepay, m_takeout, m_image_url], outputs=[m_form_log, menu_df] ).then(update_menu_dropdown, outputs=m_toggle_dropdown) m_update_btn.click( update_menu_item, inputs=[m_edit_id, m_name, m_desc, m_price, m_limit, m_cat, m_times, m_prepay, m_takeout, m_image_url], outputs=[m_form_log, menu_df] ).then(update_menu_dropdown, outputs=m_toggle_dropdown) m_refresh_btn.click(get_menu_items, outputs=menu_df).then(update_menu_dropdown, outputs=m_toggle_dropdown) m_set_active.click(toggle_menu_item, inputs=[m_toggle_dropdown, gr.State(True)], outputs=[m_toggle_log, menu_df]).then(update_menu_dropdown, outputs=m_toggle_dropdown) m_set_inactive.click(toggle_menu_item, inputs=[m_toggle_dropdown, gr.State(False)], outputs=[m_toggle_log, menu_df]).then(update_menu_dropdown, outputs=m_toggle_dropdown) # URL 參數攔截(停用中) # demo.load(check_url_action, inputs=None, outputs=[url_action_msg, login_row]) login_btn.click( check_login, inputs=[username_input, password_input], outputs=[login_row, admin_tabs, error_msg] ).then( render_booking_cards, outputs=booking_display ).then( get_menu_items, outputs=menu_df ).then( update_menu_dropdown, outputs=m_toggle_dropdown ) if __name__ == "__main__": demo.launch()