File size: 26,558 Bytes
21615f5 00f4ca2 21615f5 6539233 913a9b3 e80ff0c 6539233 21615f5 00f4ca2 b3670eb 7c22e44 f935367 21615f5 0ad419c 21615f5 8354a5a e80ff0c 7c22e44 ac7e28f e80ff0c 21615f5 5e4d3a2 e80ff0c 5e4d3a2 21615f5 00f4ca2 0130dd1 21615f5 0130dd1 21615f5 dbcfe96 16c25fa 4a4796d 5a645b7 21615f5 e80ff0c 5a645b7 4a4796d 0130dd1 4a4796d 0130dd1 4a4796d dbcfe96 4a4796d dbcfe96 4a4796d 0130dd1 4a4796d dbcfe96 4a4796d 3db40dc b634ffe dbcfe96 e80ff0c 4a4796d b3670eb 307d97e 00f4ca2 4a4796d e80ff0c 4a4796d 69435e5 5a645b7 dbcfe96 00f4ca2 0130dd1 307d97e 7c22e44 4a4796d 7c22e44 4a4796d 7c22e44 4a4796d ac7e28f 69435e5 0130dd1 307d97e 69435e5 0130dd1 69435e5 e80ff0c 307d97e 0130dd1 e80ff0c 4a4796d ac7e28f e80ff0c 69435e5 e80ff0c 0130dd1 3db40dc e80ff0c 4a4796d e80ff0c 3db40dc 69435e5 0130dd1 e80ff0c ec6b4c8 0130dd1 e80ff0c 0130dd1 69435e5 0130dd1 e80ff0c 69435e5 21615f5 e80ff0c 7c22e44 e80ff0c 4a4796d e80ff0c 4a4796d e80ff0c 4a4796d e80ff0c 4a4796d 7c22e44 4a4796d 7c22e44 e80ff0c 4a4796d e80ff0c 4a4796d e80ff0c 7c22e44 e80ff0c 7c22e44 e80ff0c 4a4796d e80ff0c 4a4796d e80ff0c 4a4796d 7c22e44 e80ff0c 0ad419c e80ff0c 0ad419c 0fdf933 69435e5 e80ff0c a2d9c5d e80ff0c f0da861 7c22e44 0ad419c e80ff0c 0ad419c e80ff0c e0e49dd 0ad419c e80ff0c ac7e28f e80ff0c ac7e28f 7c22e44 e80ff0c ac7e28f e80ff0c 64ee3c6 ac7e28f e80ff0c 4a4796d 7c22e44 0130dd1 e80ff0c 7c22e44 e80ff0c 7c22e44 e80ff0c 4a4796d e80ff0c 7c22e44 e80ff0c 4a4796d e80ff0c 7c22e44 e80ff0c 7c22e44 e80ff0c 7c22e44 e80ff0c 7c22e44 e80ff0c 4a4796d 7c22e44 4a4796d e80ff0c 7c22e44 4a4796d 21615f5 7c22e44 4a4796d e80ff0c 4a4796d e80ff0c 3db40dc 21615f5 5a5ae27 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 | import gradio as gr
import os
import pandas as pd
import requests
from supabase import create_client, Client
from datetime import datetime, timedelta, timezone
# --- 設定時區與環境變數 ---
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")
# 改回使用 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"<div style='text-align:center; padding:50px; color:#2ecc71;'><h1>✅ 訂位已成功確認!</h1><p>感謝您的回覆,期待您的光臨。您現在可以關閉此視窗。</p></div>"
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"<div style='text-align:center; padding:50px; color:#e74c3c;'><h1>🚫 訂位已取消。</h1><p>期待下次為您服務。您現在可以關閉此視窗。</p></div>"
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', '')
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
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"""<div style="background:#1a1a1a; color:#d4af37; padding:20px; border-radius:8px; font-family:sans-serif;">
<h2 style="text-align:center; border-bottom:1px solid #444; padding-bottom:15px;">Cié Cié Taipei</h2>
<p style="color:#eee; font-size:16px;"><strong>{booking['name']}</strong> 您好,行前提醒:</p>
<ul style="color:#ddd; line-height:1.8;"><li>📅 日期:{booking['date']}</li><li>⏰ 時間:{booking['time']}</li><li>👥 人數:{booking['pax']} 位</li></ul>
<div style="text-align:center; margin-top:30px;"><span style="color:#888;">無需再次確認。</span><br><br><a href="{cancel_link}" style="color:#aaa; font-size:12px;">若無法前來,請點此取消</a></div></div>"""
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"""<div style="background:#1a1a1a; color:#d4af37; padding:20px; border-radius:8px; font-family:sans-serif;">
<h2 style="text-align:center; border-bottom:1px solid #444; padding-bottom:15px;">Cié Cié Taipei</h2>
<p style="color:#eee; font-size:16px;"><strong>{booking['name']}</strong> 您好,請確認您的訂位:</p>
<ul style="color:#ddd; line-height:1.8;"><li>📅 日期:{booking['date']}</li><li>⏰ 時間:{booking['time']}</li><li>👥 人數:{booking['pax']} 位</li></ul>
<div style="text-align:center; margin-top:30px;"><a href="{confirm_link}" style="display:inline-block; background:#d4af37; color:#000; padding:12px 30px; text-decoration:none; border-radius:50px; font-weight:bold; margin-right:10px;">✅ 確認出席</a>
<a href="{cancel_link}" style="display:inline-block; border:1px solid #555; color:#aaa; padding:11px 29px; text-decoration:none; border-radius:50px;">🚫 取消</a></div></div>"""
# 執行發送
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:
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)}"
def render_booking_cards():
df = get_bookings()
count_html = f"<div style='color:#bbb; margin-bottom:10px; text-align:right; font-size:14px;'>📊 共 <span style='color:#fff; font-weight:bold;'>{len(df)}</span> 筆資料</div>"
if df.empty: return f"{count_html}<div style='text-align:center; padding:60px; color:#666; font-size:1.5em;'>📭 目前沒有訂位資料</div>"
cards_html = f"{count_html}<div style='display: flex; flex-direction: column; gap: 20px; padding-bottom: 20px;'>"
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 = "<span style='background:#00B900; color:white; padding:2px 6px; border-radius:4px; font-size:0.7em; margin-left:8px;'>LINE綁定</span>" if has_line else ""
card = f"""
<div class="booking-card" style="background:#1a1a1a; border-left:6px solid {border_color}; border-radius:12px; padding:20px; box-shadow:0 4px 15px rgba(0,0,0,0.5); position:relative;">
<div style="display:flex; justify-content:space-between; align-items:start; margin-bottom:15px;">
<div>
<span style="font-size:0.9em; color:#666; font-weight:bold; display:block;">訂單 ID</span>
<span style="font-size:2.5em; color:#fff; font-weight:900; line-height:1; font-family:monospace;">{row['id']}</span>
</div>
<div style="text-align:right;">
<div style="background:{status_bg}; color:{status_tx}; padding:6px 12px; border-radius:4px; font-weight:bold; font-size:0.9em; display:inline-block; border: 1px solid {border_color};">{status}</div>
<div style="margin-top:5px;">{line_badge}</div>
</div>
</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:10px; margin-bottom:15px; border-top:1px solid #333; padding-top:15px;">
<div><span style="color:#888; font-size:0.85em;">📅 日期</span><br><span style="color:#d4af37; font-size:1.2em; font-weight:bold;">{row['date']}</span></div>
<div><span style="color:#888; font-size:0.85em;">⏰ 時間</span><br><span style="color:#d4af37; font-size:1.5em; font-weight:900;">{row['time']}</span></div>
</div>
<div style="background:#222; padding:15px; border-radius:8px; margin-bottom:15px;">
<div style="margin-bottom:8px;"><span style="color:#fff; font-size:1.4em; font-weight:bold;">{row['name']}</span> <span style="color:#aaa;">({row['pax']}位)</span></div>
<div style="font-size:1.1em; margin-bottom:5px;">📞 {row['tel']}</div>
<div style="font-size:0.9em; color:#888;">✉️ {row['email'] or '-'}</div>
</div>
<div style="background:#f1c40f11; padding:12px; border-radius:6px; border:1px solid #f1c40f33;">
<span style="color:#aaa; font-size:0.8em;">📝 備註:</span> <span style="color:#f1c40f;">{row.get('remarks') or '無'}</span>
</div>
</div>
"""
cards_html += card
cards_html += "</div>"
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: "<span style='color: red'>❌ 帳號或密碼錯誤</span>" }
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)
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)
# 其餘按鈕事件
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() |