Spaces:
Sleeping
Sleeping
File size: 13,849 Bytes
21615f5 00f4ca2 21615f5 6539233 913a9b3 e0e49dd 6539233 21615f5 00f4ca2 b3670eb 21615f5 041ae95 0ad419c 21615f5 8354a5a 21615f5 5e4d3a2 21615f5 927501e 00f4ca2 0130dd1 307d97e 21615f5 64ee3c6 21615f5 0130dd1 21615f5 dbcfe96 16c25fa 5a645b7 21615f5 0130dd1 21615f5 913a9b3 16c25fa 21615f5 64ee3c6 5a645b7 0130dd1 5a645b7 0130dd1 927501e 16c25fa 0130dd1 16c25fa 927501e 16c25fa 0130dd1 dbcfe96 0130dd1 dbcfe96 0130dd1 dbcfe96 0130dd1 5a645b7 dbcfe96 0130dd1 927501e 16c25fa 927501e 16c25fa 0130dd1 dbcfe96 0130dd1 dbcfe96 0130dd1 dbcfe96 0130dd1 3db40dc b634ffe dbcfe96 5a645b7 307d97e 64ee3c6 b3670eb 927501e 307d97e 00f4ca2 0130dd1 69435e5 dbcfe96 0130dd1 307d97e 69435e5 0130dd1 5a645b7 dbcfe96 00f4ca2 69435e5 0130dd1 307d97e 0130dd1 69435e5 0130dd1 307d97e 69435e5 0130dd1 69435e5 307d97e 0130dd1 69435e5 3db40dc 0130dd1 69435e5 0130dd1 3db40dc 0130dd1 69435e5 3db40dc 69435e5 ec6b4c8 0130dd1 ec6b4c8 0130dd1 ec6b4c8 0130dd1 ec6b4c8 69435e5 0130dd1 ec6b4c8 0130dd1 3db40dc 0130dd1 69435e5 0130dd1 69435e5 21615f5 041ae95 0ad419c 307d97e 0ad419c 0130dd1 0fdf933 69435e5 0130dd1 a2d9c5d 0130dd1 5e4d3a2 f0da861 0ad419c 3db40dc 0ad419c 3db40dc e0e49dd 0ad419c 0130dd1 64ee3c6 0130dd1 3db40dc 69435e5 0130dd1 69435e5 0130dd1 69435e5 0130dd1 69435e5 21615f5 f0da861 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 |
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() |