Spaces:
Running
Running
File size: 12,063 Bytes
91b069f dbf3645 f401a2c 311693d f401a2c 1147dda dbf3645 259f188 9c20d88 259f188 bba7a48 311693d bba7a48 311693d 9c20d88 f401a2c 9c20d88 259f188 9c20d88 ec81d8b d8955c7 9c20d88 ec81d8b 9c20d88 9f3eb4c 9c20d88 d8955c7 053a647 d8955c7 053a647 d8955c7 053a647 d8955c7 311693d ec81d8b bc34808 053a647 bc34808 bba7a48 ec81d8b 1147dda ec81d8b dbf3645 311693d 053a647 4bfed4c 053a647 311693d ec81d8b 311693d ec81d8b f401a2c 1147dda 311693d bba7a48 1147dda dbf3645 1147dda 053a647 ec81d8b 311693d d4d6acc 311693d 53f4233 946dd5a 311693d ef50b1a dbf3645 311693d dbf3645 52cffdf 053a647 ef50b1a dbf3645 053a647 dbf3645 ef50b1a 053a647 ef50b1a 053a647 ef50b1a 52cffdf f401a2c 053a647 f401a2c 52cffdf 053a647 ef50b1a 053a647 ef50b1a 053a647 ef50b1a 053a647 ef50b1a 053a647 ef50b1a 91b069f 311693d 946dd5a 9c20d88 bba7a48 311693d bba7a48 e20d451 9c20d88 53f4233 bc34808 53f4233 9c20d88 53f4233 9c20d88 53f4233 311693d bc34808 9c20d88 53f4233 91b069f 53f4233 dbf3645 53f4233 dbf3645 53f4233 e20d451 9c20d88 53f4233 7bb2e5d 311693d 53f4233 9c20d88 311693d dbf3645 9c20d88 |
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 |
import gradio as gr
import os
import requests
from supabase import create_client, Client
from datetime import datetime, timedelta, timezone
# --- 1. 設定與初始化 ---
TAIPEI_TZ = timezone(timedelta(hours=8))
LINE_ACCESS_TOKEN = os.getenv("LINE_ACCESS_TOKEN")
LINE_ADMIN_ID = os.getenv("LINE_ADMIN_ID")
SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
if SUPABASE_URL and SUPABASE_KEY:
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
# --- 2. 座位控管設定 ---
DEFAULT_LIMIT = 30
SPECIAL_DAYS = {
"2026-12-31": 10,
"2026-02-14": 15
}
# --- 3. 輔助函式 ---
def get_date_options():
options = []
today = datetime.now(TAIPEI_TZ)
weekdays = ["(一)", "(二)", "(三)", "(四)", "(五)", "(六)", "(日)"]
for i in range(30):
current_date = today + timedelta(days=i)
date_str = f"{current_date.strftime('%Y-%m-%d')} {weekdays[current_date.weekday()]}"
options.append(date_str)
return options
def update_time_slots(date_str):
if not date_str: return gr.update(choices=[]), "請先選擇日期"
try:
clean_date_str = date_str.split(" ")[0]
date_obj = datetime.strptime(clean_date_str, "%Y-%m-%d")
weekday = date_obj.weekday()
except: return gr.update(choices=[]), "日期格式錯誤"
slots = ["18:30", "19:00", "19:30", "20:00", "20:30",
"21:00", "21:30", "22:00", "22:30", "23:00", "23:30", "00:00", "00:30", "01:00", "01:30"]
if weekday == 4 or weekday == 5: slots.extend(["02:00", "02:30"])
now = datetime.now(TAIPEI_TZ)
if date_obj.date() == now.date():
current_time_str = now.strftime("%H:%M")
valid_slots = []
for s in slots:
h = int(s.split(":")[0])
if h < 5:
valid_slots.append(s)
elif s > current_time_str:
valid_slots.append(s)
slots = valid_slots
clean_date = date_str.split(" ")[0]
daily_limit = SPECIAL_DAYS.get(clean_date, DEFAULT_LIMIT)
try:
res = supabase.table("bookings").select("pax").eq("date", date_str).neq("status", "顧客已取消").execute()
current_total = sum([item['pax'] for item in res.data])
remaining = daily_limit - current_total
status_msg = f"✨ {date_str} (剩餘座位: {remaining} 位)"
except: status_msg = f"✨ {date_str}"
return gr.update(choices=slots, value=slots[0] if slots else None), status_msg
def init_ui():
fresh_dates = get_date_options()
default_date = fresh_dates[0]
time_update, status_msg = update_time_slots(default_date)
return (gr.update(choices=fresh_dates, value=default_date), time_update, status_msg)
# --- 4. 核心邏輯 (抓 ID 與 處理訂位) ---
def get_line_id_from_url(request: gr.Request):
if request:
return request.query_params.get("line_id", "")
return ""
def handle_booking(name, tel, email, date_str, time, pax, remarks, line_id):
# 1. 基本必填檢查
if not name or not tel or not date_str or not time:
return "⚠️ 請完整填寫必填欄位 (姓名、電話、日期、時間)"
# 🔥🔥🔥 [新增] 格式驗證邏輯 🔥🔥🔥
# 2. Email 驗證 (如果用戶有填寫 Email,就必須包含 @)
if email and "@" not in email:
return "⚠️ Email 格式錯誤 (請確認包含 @ 符號)"
# 3. 電話驗證 (寬鬆模式:相容外國人)
# 邏輯:不限制符號 (+, -, 空白, 括號都可),但「數字」的總數至少要有 6 碼
# 例如: "+886 912-345-678" (數字12碼) -> 通過
# 例如: "Test Phone" (數字0碼) -> 失敗
digit_count = sum(c.isdigit() for c in tel)
if digit_count < 6:
return "⚠️ 電話號碼格式錯誤 (請填寫有效的電話號碼)"
# 🔥🔥🔥 驗證結束 🔥🔥🔥
clean_date = date_str.split(" ")[0]
daily_limit = SPECIAL_DAYS.get(clean_date, DEFAULT_LIMIT)
try:
res = supabase.table("bookings").select("pax").eq("date", date_str).neq("status", "顧客已取消").execute()
current_total = sum([item['pax'] for item in res.data])
if current_total + pax > daily_limit:
return "⚠️ 抱歉,該時段剩餘座位不足,請調整人數或日期。"
except: pass
try:
existing = supabase.table("bookings").select("id").eq("tel", tel).eq("date", date_str).eq("time", time).neq("status", "顧客已取消").execute()
if existing.data: return "⚠️ 您已預約過此時段,請勿重複提交。"
except: pass
data = {
"name": name, "tel": tel, "email": email, "date": date_str, "time": time,
"pax": pax, "remarks": remarks, "status": "待處理",
"user_id": line_id
}
try:
supabase.table("bookings").insert(data).execute()
# [LINE 通知老闆 - 新訂位]
if LINE_ACCESS_TOKEN and LINE_ADMIN_ID:
src = "🟢 LINE用戶" if line_id else "⚪ 訪客"
note = remarks if remarks else "無"
msg = (
f"🔥 新訂位 ({src})\n"
f"姓名:{name}\n"
f"電話:{tel}\n"
f"日期:{date_str}\n"
f"時間:{time}\n"
f"人數:{pax}人\n"
f"備註:{note}"
)
requests.post("https://api.line.me/v2/bot/message/push", headers={"Authorization": f"Bearer {LINE_ACCESS_TOKEN}", "Content-Type": "application/json"}, json={"to": LINE_ADMIN_ID, "messages": [{"type": "text", "text": msg}]})
return """<div style='text-align: center; color: #fff; padding: 20px; border: 1px solid #d4af37; border-radius: 8px; background: #222;'><h2 style='color: #d4af37; margin: 0;'>Request Received</h2><p style='margin: 10px 0;'>🥂 預約申請已提交</p><p style='font-size: 0.9em; color: #aaa;'>請留意 Email 確認信 或 Line 訊息。</p></div>"""
except Exception as e: return f"❌ 系統錯誤: {str(e)}"
# --- 5. Webhook (確認/取消 + 回傳轉址 URL + 🔥通知老闆) ---
def check_confirmation(request: gr.Request):
if not request: return ""
action = request.query_params.get('action')
bid = request.query_params.get('id')
OFFICIAL_SITE = "https://ciecietaipei.github.io/index.html"
notify_msg = "" # 準備要發給老闆的訊息
if action == 'confirm' and bid:
try:
# 1. 更新資料庫
supabase.table("bookings").update({"status": "顧客已確認"}).eq("id", bid).execute()
# 2. 🔥 抓訂位資料 (為了通知老闆是誰按了確認)
res = supabase.table("bookings").select("name, date, time, pax").eq("id", bid).execute()
if res.data:
b = res.data[0]
notify_msg = f"🎉 顧客已確認出席!\n{b['date']} {b['time']}\n{b['name']} ({b['pax']}人)"
final_url = f"{OFFICIAL_SITE}?status=confirmed"
except:
final_url = f"{OFFICIAL_SITE}"
elif action == 'cancel' and bid:
try:
# 1. 更新資料庫
supabase.table("bookings").update({"status": "顧客已取消"}).eq("id", bid).execute()
# 2. 🔥 抓訂位資料
res = supabase.table("bookings").select("name, date, time").eq("id", bid).execute()
if res.data:
b = res.data[0]
notify_msg = f"⚠️ 顧客已取消...\n{b['date']} {b['time']}\n{b['name']}"
final_url = f"{OFFICIAL_SITE}?status=canceled"
except:
final_url = f"{OFFICIAL_SITE}"
else:
final_url = ""
# 🔥🔥🔥 3. 這裡執行發送通知給老闆 (使用 Messaging API) 🔥🔥🔥
if notify_msg and LINE_ACCESS_TOKEN and LINE_ADMIN_ID:
try:
requests.post(
"https://api.line.me/v2/bot/message/push",
headers={"Authorization": f"Bearer {LINE_ACCESS_TOKEN}", "Content-Type": "application/json"},
json={
"to": LINE_ADMIN_ID, # 發給老闆
"messages": [{"type": "text", "text": notify_msg}]
}
)
except Exception as e:
print(f"通知老闆失敗: {e}")
return final_url
# --- 6. 介面 ---
theme = gr.themes.Soft(primary_hue="amber", neutral_hue="zinc").set(body_background_fill="#0F0F0F", block_background_fill="#1a1a1a", block_border_width="1px", block_border_color="#333", input_background_fill="#262626", input_border_color="#444", body_text_color="#E0E0E0", block_title_text_color="#d4af37", button_primary_background_fill="#d4af37", button_primary_text_color="#000000")
custom_css = "footer {display: none !important;} .gradio-container, .block, .row, .column { overflow: visible !important; } .options, .wrap .options { background-color: #262626 !important; border: 1px solid #d4af37 !important; z-index: 10000 !important; box-shadow: 0 5px 15px rgba(0,0,0,0.5); } .item:hover, .options .item:hover { background-color: #d4af37 !important; color: black !important; } .legal-footer { text-align: center; margin-top: 15px; padding-top: 15px; border-top: 1px solid #333; color: #666; font-size: 0.75rem; } #hidden_box { display: none !important; }"
with gr.Blocks(theme=theme, css=custom_css, title="Booking") as demo:
line_id_box = gr.Textbox(visible=True, elem_id="hidden_box", label="LINE ID")
redirect_url_box = gr.Textbox(visible=False)
demo.load(get_line_id_from_url, None, line_id_box)
demo.load(check_confirmation, None, redirect_url_box)
redirect_url_box.change(
fn=None,
inputs=redirect_url_box,
outputs=None,
js="(url) => { if(url) window.location.href = url; }"
)
with gr.Row():
with gr.Column():
gr.Markdown("### 📅 預約資訊 Booking Info")
booking_date = gr.Dropdown(label="選擇日期 Select Date", interactive=True)
pax_count = gr.Slider(minimum=1, maximum=10, value=2, step=1, label="用餐人數 Guest Count")
with gr.Column():
gr.Markdown("### 🕰️ 選擇時段 Time Slot")
status_box = gr.Markdown("請先選擇日期...", visible=True)
time_slot = gr.Dropdown(choices=[], label="可用時段 Available Time", interactive=True)
demo.load(init_ui, None, [booking_date, time_slot, status_box])
gr.HTML("<div style='height: 10px'></div>")
gr.Markdown("### 👤 聯絡人資料 Contact,收到確認 E-Mail 並點擊 確認出席 才算訂位成功")
with gr.Group():
with gr.Row():
cust_name = gr.Textbox(label="訂位姓名 Name *", placeholder="ex. 王小明")
cust_tel = gr.Textbox(label="手機號碼 Phone *", placeholder="ex. 0912-xxx-xxx")
with gr.Row():
cust_email = gr.Textbox(label="電子信箱 E-mail (接收確認信用,請記得檢查垃圾信件匣。)", placeholder="example@gmail.com")
with gr.Row():
cust_remarks = gr.Textbox(label="備註 Remarks (過敏/慶生/特殊需求)", lines=2)
gr.HTML("<div style='height: 15px'></div>")
submit_btn = gr.Button("確認預約 Request Booking (系統會記錄是否曾 No Show)", size="lg", variant="primary")
output_msg = gr.HTML()
gr.HTML("""<div class="legal-footer"><p style="margin-bottom: 5px;">© 2026 CIE CIE TAIPEI. All Rights Reserved.</p><p>內容涉及酒類產品訊息,請勿轉發分享給未達法定購買年齡者;未滿十八歲請勿飲酒。<br><strong>喝酒不開車,開車不喝酒。</strong></p></div>""")
booking_date.change(update_time_slots, inputs=booking_date, outputs=[time_slot, status_box])
submit_btn.click(handle_booking, inputs=[cust_name, cust_tel, cust_email, booking_date, time_slot, pax_count, cust_remarks, line_id_box], outputs=output_msg)
if __name__ == "__main__":
demo.launch() |