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']} 您好,您的訂單尚未完成付款:
#
- 📅 日期:{booking['date']}
- ⏰ 時間:{booking['time']}
#
"""
# ── 催付款流程結束 ────────────────────────────────────────
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']} 您好,行前提醒:
- 📅 日期:{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']} 位
"""
# ── 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"""
📅 日期
{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()