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") PUBLIC_SPACE_URL = "https://deeplearning101-ciecietaipei.hf.space" # 取得帳密 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) 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: return pd.DataFrame() # 🔥🔥🔥 核心後端:智慧判斷發送邏輯 (修正 LINE 連結) 🔥🔥🔥 def send_confirmation_hybrid(booking_id): if not booking_id: return "❌ 請輸入訂單 ID" try: # 1. 撈資料 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}: " # 2. 判斷模式 is_reminder = "確認" in current_status if is_reminder: # --- 🔔 提醒模式 (Reminder) --- action_label = "提醒" mail_subject = f"🔔 行前提醒: {booking['date']} - Cié Cié Taipei" # 🔥 LINE 修正:加入取消連結,方便客人臨時取消 line_text = ( f"🔔 行前提醒\n\n" f"{booking['name']} 您好,期待今晚與您相見!\n\n" f"📅 日期:{booking['date']}\n" f"⏰ 時間:{booking['time']}\n" f"👥 人數:{booking['pax']} 位\n\n" f"座位已為您準備好。\n" f"若無法前來,請點擊下方連結取消:\n{cancel_link}" ) mail_html = f"""

Cié Cié Taipei

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

無需再次確認。

若無法前來,請點此取消
""" else: # --- 🚀 確認模式 (Confirmation) --- action_label = "確認" mail_subject = f"訂位確認: {booking['date']} - Cié Cié Taipei" # 🔥 LINE 修正:加入 確認 與 取消 的連結 line_text = ( f"✅ 訂位確認\n\n" f"{booking['name']} 您好,已收到您的預約。\n\n" f"📅 日期:{booking['date']}\n" f"⏰ 時間:{booking['time']}\n" f"👥 人數:{booking['pax']} 位\n\n" f"請點擊下方連結確認出席:\n" f"👉 確認:{confirm_link}\n\n" f"🚫 取消:{cancel_link}" ) mail_html = f"""

Cié Cié Taipei

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

✅ 確認出席 🚫 取消
""" # 3. 執行發送 # Email 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({action_label}) " except Exception as e: log_msg += f"❌ MailErr({str(e)}) " else: log_msg += "⚠️ 無Mail " # LINE 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({action_label}) " else: log_msg += f"❌ LINE失敗({r.status_code}: {r.text}) " except Exception as e: log_msg += f"❌ LINEErr({str(e)}) " else: log_msg += "⚠️ 無LINE ID " # 4. 更新狀態 (僅在非提醒模式下更新) 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)}" # 🔥🔥🔥 卡片渲染 (純顯示,無 JS 互動) 🔥🔥🔥 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', '待處理') # 顏色邏輯 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" card = f"""
訂單 ID {row['id']}
{status}
📅 日期 Date
{row['date']}
⏰ 時間 Time
{row['time']}
{row['name']} ({row['pax']}位)
📞 {row['tel']}
✉️ {row['email'] or '-'}
📝 備註: {row.get('remarks') or '無'}
""" cards_html += card cards_html += "
" return cards_html # --- 登入邏輯 --- def check_login(user, password): if user == REAL_ADMIN_USER and password == REAL_ADMIN_PASSWORD: return { login_row: gr.update(visible=False), admin_row: gr.update(visible=True), error_msg: "" } else: return {error_msg: "❌ 帳號或密碼錯誤"} # --- CSS --- 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="Admin") as demo: with gr.Group(visible=True) as login_row: gr.Markdown("# 🔒 Login") with gr.Row(): username_input = gr.Textbox(label="User") password_input = gr.Textbox(label="Pass", type="password") login_btn = gr.Button("Enter", variant="primary") error_msg = gr.Markdown("") with gr.Group(visible=False) as admin_row: # 🔥 操作區 (固定在頂部) with gr.Column(elem_id="op-panel"): gr.Markdown("### 🍷 Cié Cié 訂位管理") with gr.Row(): id_input = gr.Number(label="輸入 ID 發送通知", precision=0, scale=2) send_btn = gr.Button("🚀 發送通知 / 提醒 (Hybrid)", variant="primary", scale=1) refresh_btn = gr.Button("🔄 刷新列表", scale=1) log_output = gr.Textbox(label="執行結果", lines=1) # 卡片顯示區 booking_display = gr.HTML(elem_id="booking_display") login_btn.click(check_login, inputs=[username_input, password_input], outputs=[login_row, admin_row, error_msg]).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 ) demo.launch(css=custom_css) if __name__ == "__main__": demo.launch()