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 json
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")
LINE_ACCESS_TOKEN = os.getenv("LINE_ACCESS_TOKEN")
# --- 新增:LINE Pay 查詢用金鑰 ---
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"
# 改回使用 GitHub Pages 網址,確保客人點擊信件能回到充滿設計感的前台
PUBLIC_SPACE_URL = os.getenv("HF_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 (網址參數攔截)
# ==========================================
def check_url_action(request: gr.Request):
"""當客人點擊信件裡的連結來到此頁面時,攔截 action 參數並處理,隱藏登入畫面"""
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 = f"
✅ 訂位已成功確認!
感謝您的回覆,期待您的光臨。您現在可以關閉此視窗。
"
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 = f"🚫 訂位已取消。
期待下次為您服務。您現在可以關閉此視窗。
"
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:訂位與通知管理 (包含 No-Show 切換與手動狀態)
# ==========================================
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', '')
remarks = booking.get('remarks', '')
confirm_link = f"{PUBLIC_SPACE_URL}?id={booking_id}&action=confirm"
cancel_link = f"{PUBLIC_SPACE_URL}?id={booking_id}&action=cancel"
log_msg = f"🆔 {booking_id} 處理結果: "
is_reminder = "確認" in current_status
is_pending_payment = "待付款" in current_status
# 🌟 處理補付款/催繳情境 🌟
if is_pending_payment:
# 從備註中萃取原本的訂單號
order_id = None
for line in remarks.split('\n'):
if "訂單號:" in line:
order_id = line.split(":")[-1].strip()
payment_link = ""
if order_id:
# 這裡是一個簡化的重製付款連結邏輯
# 實際應用中,您可以呼叫 FastAPI 的一個專屬端點來重新產生有效的 LINE Pay 連結
# 為了演示,我們假設這個連結可以直接導向您前台的特定補付款頁面
payment_link = f"{PUBLIC_SPACE_URL}?action=repay&order_id={order_id}"
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"
f"如已付款請忽略此訊息。")
mail_html = f"""
Cié Cié Taipei
{booking['name']} 您好,您的訂單尚未完成付款:
- 📅 日期:{booking['date']}
- ⏰ 時間:{booking['time']}
"""
elif 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']} 您好,行前提醒:
- 📅 日期:{booking['date']}
- ⏰ 時間:{booking['time']}
- 👥 人數:{booking['pax']} 位
"""
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']} 您好,請確認您的訂位:
- 📅 日期:{booking['date']}
- ⏰ 時間:{booking['time']}
- 👥 人數:{booking['pax']} 位
"""
# 執行發送
if email and "@" in email and GAS_MAIL_URL:
try:
requests.post(GAS_MAIL_URL, json={"to": email, "subject": mail_subject, "htmlBody": mail_html, "name": "Cié Cié Taipei"})
log_msg += f"✅ Mail送出 "
except: log_msg += f"❌ Mail失敗 "
else: log_msg += "⚠️ 無Mail "
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}]})
if r.status_code == 200: log_msg += f"✅ LINE送出 "
else: log_msg += f"❌ LINE失敗 "
except: log_msg += f"❌ LINE錯誤 "
else: log_msg += "⚠️ 無LINE ID "
if not is_reminder and not is_pending_payment:
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):
"""標記或撤銷 No-Show 狀態"""
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:
# 1. 從資料庫抓取這筆訂單的備註,找出專屬訂單號 (ORDER-XXXX)
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 訂單號 (可能為免訂金或舊訂單)"
# 2. 生成 LINE Pay V3 查詢專用加密簽章
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
}
# 3. 發送查詢請求
r = requests.get(f"{LINE_PAY_BASE_URL}{uri}?{query_string}", headers=headers)
res_data = r.json()
# 4. 解析 LINE Pay 回傳的結果
if res_data.get("returnCode") == "0000":
info = res_data.get("info", [])
if not info:
return f"ℹ️ 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"
elif '付款' in status: status_bg = "#3498db"; status_tx = "#fff"; border_color = "#3498db"
line_badge = "
LINE綁定" if has_line else ""
card = f"""
📅 日期
{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 "無")
display_df = df[['name', 'price', '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, "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, "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("category", "main"),
times,
item.get("require_prepay", False),
item.get("allow_takeout", True),
item.get("image_url", "") or "",
item_id # 將 ID 存入隱藏狀態中
)
except:
return "", "", 0, "main", ["晚餐 (18:30-21:30)", "宵夜 (21:30後)"], False, True, "", ""
def add_menu_item(name, desc, price, category, available_times, require_prepay, allow_takeout, image_url_input):
if not name or not price: 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), "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, category, available_times, require_prepay, allow_takeout, image_url_input):
if not item_id: return "⚠️ 請先從右側選擇並「載入編輯」一項餐點", get_menu_items()
if not name or not price: 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), "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:
# 網址參數處理 (信件確認 Webhook)
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("### 🚀 訂單操作控制台")
# 第一排:ID 輸入與狀態變更選單
with gr.Row():
id_input = gr.Number(label="輸入訂單 ID", precision=0, scale=1)
new_status_dropdown = gr.Dropdown(
label="手動選擇新狀態",
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)
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)
# 其餘按鈕事件
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("### ✨ 上架與編輯餐點")
# 隱藏狀態,用來紀錄正在編輯哪一筆餐點的 ID
m_edit_id = gr.State("")
with gr.Row():
# 左側:餐點表單
with gr.Column(scale=1):
m_name = gr.Textbox(label="餐點名稱 *")
m_desc = gr.Textbox(label="餐點描述 (選填)")
m_price = gr.Number(label="價格 (TWD) *", 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_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_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_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()