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']} 您好,行前提醒:
- 📅 日期:{booking['date']}
- ⏰ 時間:{booking['time']}
- 👥 人數:{booking['pax']} 位
"""
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']} 您好,請確認您的訂位:
- 📅 日期:{booking['date']}
- ⏰ 時間:{booking['time']}
- 👥 人數:{booking['pax']} 位
"""
# 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['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()