DeepLearning101's picture
把連結補進 LINE 的訊息內容
927501e verified
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"""
<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>
<div style="background:#2a2a2a; padding:15px; border-radius:8px; margin:20px 0; border-left:4px solid #f1c40f;">
<ul style="color:#ddd; padding-left:20px; line-height:1.8;">
<li>📅 日期:<strong style="color:#fff;">{booking['date']}</strong></li>
<li>⏰ 時間:<strong style="color:#fff;">{booking['time']}</strong></li>
<li>👥 人數:<strong style="color:#fff;">{booking['pax']} 位</strong></li>
</ul>
</div>
<div style="text-align:center; margin-top:30px;">
<span style="color:#888;">無需再次確認。</span><br><br>
<a href="{cancel_link}" style="display:inline-block; border:1px solid #555; color:#aaa; padding:10px 20px; text-decoration:none; border-radius:50px; font-size:12px;">若無法前來,請點此取消</a>
</div>
</div>
"""
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"""
<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>
<div style="background:#2a2a2a; padding:15px; border-radius:8px; margin:20px 0; border-left:4px solid #2ecc71;">
<ul style="color:#ddd; padding-left:20px; line-height:1.8;">
<li>📅 日期:<strong style="color:#fff;">{booking['date']}</strong></li>
<li>⏰ 時間:<strong style="color:#fff;">{booking['time']}</strong></li>
<li>👥 人數:<strong style="color:#fff;">{booking['pax']} 位</strong></li>
</ul>
</div>
<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>
"""
# 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"<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', '待處理')
# 顏色邏輯
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"""
<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);
font-family: '微軟正黑體', sans-serif;
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="background:{status_bg}; color:{status_tx}; padding:6px 12px; border-radius:4px; font-weight:bold; font-size:0.9em;">
{status}
</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;">📅 日期 Date</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;">⏰ 時間 Time</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;">
📞 <a href="tel:{row['tel']}" style="color:#69c0ff; text-decoration:none;">{row['tel']}</a>
</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
# --- 登入邏輯 ---
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: "<span style='color: red'>❌ 帳號或密碼錯誤</span>"}
# --- 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()