Spaces:
Running
Running
電話 (Phone):「寬鬆驗證」+ Email 檢查
Browse files
app.py
CHANGED
|
@@ -25,7 +25,6 @@ SPECIAL_DAYS = {
|
|
| 25 |
# --- 3. 輔助函式 ---
|
| 26 |
def get_date_options():
|
| 27 |
options = []
|
| 28 |
-
# 這裡的 TAIPEI_TZ 會在函式被呼叫時重新取得當下時間
|
| 29 |
today = datetime.now(TAIPEI_TZ)
|
| 30 |
weekdays = ["(一)", "(二)", "(三)", "(四)", "(五)", "(六)", "(日)"]
|
| 31 |
for i in range(30):
|
|
@@ -43,31 +42,22 @@ def update_time_slots(date_str):
|
|
| 43 |
weekday = date_obj.weekday()
|
| 44 |
except: return gr.update(choices=[]), "日期格式錯誤"
|
| 45 |
|
| 46 |
-
# 預設的所有時段
|
| 47 |
slots = ["18:30", "19:00", "19:30", "20:00", "20:30",
|
| 48 |
"21:00", "21:30", "22:00", "22:30", "23:00", "23:30", "00:00", "00:30", "01:00", "01:30"]
|
| 49 |
if weekday == 4 or weekday == 5: slots.extend(["02:00", "02:30"])
|
| 50 |
|
| 51 |
-
# 🔥🔥🔥 新增:時間過濾邏輯 🔥🔥🔥
|
| 52 |
now = datetime.now(TAIPEI_TZ)
|
| 53 |
-
# 只有當選擇的日期是「今天」時,才需要過濾過去的時間
|
| 54 |
if date_obj.date() == now.date():
|
| 55 |
-
current_time_str = now.strftime("%H:%M")
|
| 56 |
valid_slots = []
|
| 57 |
for s in slots:
|
| 58 |
-
h = int(s.split(":")[0])
|
| 59 |
-
|
| 60 |
-
# 判斷邏輯:
|
| 61 |
-
# 1. 凌晨時段 (00:00 - 05:00):這屬於「跨日」,相對於今天的晚餐時段來說是未來,所以保留。
|
| 62 |
-
# 2. 晚間時段 (18:00+):必須比「現在時間」晚,才保留。
|
| 63 |
if h < 5:
|
| 64 |
valid_slots.append(s)
|
| 65 |
elif s > current_time_str:
|
| 66 |
valid_slots.append(s)
|
| 67 |
-
|
| 68 |
-
slots = valid_slots # 更新清單
|
| 69 |
|
| 70 |
-
# 計算剩餘座位 (維持原樣)
|
| 71 |
clean_date = date_str.split(" ")[0]
|
| 72 |
daily_limit = SPECIAL_DAYS.get(clean_date, DEFAULT_LIMIT)
|
| 73 |
|
|
@@ -78,26 +68,13 @@ def update_time_slots(date_str):
|
|
| 78 |
status_msg = f"✨ {date_str} (剩餘座位: {remaining} 位)"
|
| 79 |
except: status_msg = f"✨ {date_str}"
|
| 80 |
|
| 81 |
-
# 如果所有時段都過期了 (slots 為空),value 給 None
|
| 82 |
return gr.update(choices=slots, value=slots[0] if slots else None), status_msg
|
| 83 |
|
| 84 |
-
# --- [新增] 初始化 UI 的函式 (解決日期過期問題) ---
|
| 85 |
def init_ui():
|
| 86 |
-
"""
|
| 87 |
-
當網頁載入時執行:
|
| 88 |
-
1. 重新計算日期列表 (確保今天是真的今天)
|
| 89 |
-
2. 自動選擇第一天 (今天)
|
| 90 |
-
3. 根據今天,自動更新時段和剩餘座位 (此時會自動觸發上方的時間過濾)
|
| 91 |
-
"""
|
| 92 |
fresh_dates = get_date_options()
|
| 93 |
default_date = fresh_dates[0]
|
| 94 |
time_update, status_msg = update_time_slots(default_date)
|
| 95 |
-
|
| 96 |
-
return (
|
| 97 |
-
gr.update(choices=fresh_dates, value=default_date),
|
| 98 |
-
time_update,
|
| 99 |
-
status_msg
|
| 100 |
-
)
|
| 101 |
|
| 102 |
# --- 4. 核心邏輯 (抓 ID 與 處理訂位) ---
|
| 103 |
def get_line_id_from_url(request: gr.Request):
|
|
@@ -106,8 +83,25 @@ def get_line_id_from_url(request: gr.Request):
|
|
| 106 |
return ""
|
| 107 |
|
| 108 |
def handle_booking(name, tel, email, date_str, time, pax, remarks, line_id):
|
|
|
|
| 109 |
if not name or not tel or not date_str or not time:
|
| 110 |
-
return "⚠️ 請完整填寫必填欄位"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
clean_date = date_str.split(" ")[0]
|
| 113 |
daily_limit = SPECIAL_DAYS.get(clean_date, DEFAULT_LIMIT)
|
|
@@ -132,6 +126,7 @@ def handle_booking(name, tel, email, date_str, time, pax, remarks, line_id):
|
|
| 132 |
try:
|
| 133 |
supabase.table("bookings").insert(data).execute()
|
| 134 |
|
|
|
|
| 135 |
if LINE_ACCESS_TOKEN and LINE_ADMIN_ID:
|
| 136 |
src = "🟢 LINE用戶" if line_id else "⚪ 訪客"
|
| 137 |
note = remarks if remarks else "無"
|
|
@@ -157,18 +152,18 @@ def check_confirmation(request: gr.Request):
|
|
| 157 |
|
| 158 |
OFFICIAL_SITE = "https://ciecietaipei.github.io/index.html"
|
| 159 |
|
| 160 |
-
notify_msg = "" #
|
| 161 |
|
| 162 |
if action == 'confirm' and bid:
|
| 163 |
try:
|
| 164 |
-
#
|
| 165 |
supabase.table("bookings").update({"status": "顧客已確認"}).eq("id", bid).execute()
|
| 166 |
|
| 167 |
-
# 🔥
|
| 168 |
res = supabase.table("bookings").select("name, date, time, pax").eq("id", bid).execute()
|
| 169 |
if res.data:
|
| 170 |
b = res.data[0]
|
| 171 |
-
notify_msg = f"🎉
|
| 172 |
|
| 173 |
final_url = f"{OFFICIAL_SITE}?status=confirmed"
|
| 174 |
except:
|
|
@@ -176,14 +171,14 @@ def check_confirmation(request: gr.Request):
|
|
| 176 |
|
| 177 |
elif action == 'cancel' and bid:
|
| 178 |
try:
|
| 179 |
-
#
|
| 180 |
supabase.table("bookings").update({"status": "顧客已取消"}).eq("id", bid).execute()
|
| 181 |
|
| 182 |
-
# 🔥
|
| 183 |
res = supabase.table("bookings").select("name, date, time").eq("id", bid).execute()
|
| 184 |
if res.data:
|
| 185 |
b = res.data[0]
|
| 186 |
-
notify_msg = f"⚠️
|
| 187 |
|
| 188 |
final_url = f"{OFFICIAL_SITE}?status=canceled"
|
| 189 |
except:
|
|
@@ -191,19 +186,19 @@ def check_confirmation(request: gr.Request):
|
|
| 191 |
else:
|
| 192 |
final_url = ""
|
| 193 |
|
| 194 |
-
# 🔥🔥🔥
|
| 195 |
if notify_msg and LINE_ACCESS_TOKEN and LINE_ADMIN_ID:
|
| 196 |
try:
|
| 197 |
requests.post(
|
| 198 |
"https://api.line.me/v2/bot/message/push",
|
| 199 |
headers={"Authorization": f"Bearer {LINE_ACCESS_TOKEN}", "Content-Type": "application/json"},
|
| 200 |
json={
|
| 201 |
-
"to": LINE_ADMIN_ID,
|
| 202 |
"messages": [{"type": "text", "text": notify_msg}]
|
| 203 |
}
|
| 204 |
)
|
| 205 |
except Exception as e:
|
| 206 |
-
print(f"
|
| 207 |
|
| 208 |
return final_url
|
| 209 |
|
|
@@ -236,7 +231,6 @@ with gr.Blocks(theme=theme, css=custom_css, title="Booking") as demo:
|
|
| 236 |
status_box = gr.Markdown("請先選擇日期...", visible=True)
|
| 237 |
time_slot = gr.Dropdown(choices=[], label="可用時段 Available Time", interactive=True)
|
| 238 |
|
| 239 |
-
# 載入時初始化 UI (包含日期與時間過濾)
|
| 240 |
demo.load(init_ui, None, [booking_date, time_slot, status_box])
|
| 241 |
|
| 242 |
gr.HTML("<div style='height: 10px'></div>")
|
|
|
|
| 25 |
# --- 3. 輔助函式 ---
|
| 26 |
def get_date_options():
|
| 27 |
options = []
|
|
|
|
| 28 |
today = datetime.now(TAIPEI_TZ)
|
| 29 |
weekdays = ["(一)", "(二)", "(三)", "(四)", "(五)", "(六)", "(日)"]
|
| 30 |
for i in range(30):
|
|
|
|
| 42 |
weekday = date_obj.weekday()
|
| 43 |
except: return gr.update(choices=[]), "日期格式錯誤"
|
| 44 |
|
|
|
|
| 45 |
slots = ["18:30", "19:00", "19:30", "20:00", "20:30",
|
| 46 |
"21:00", "21:30", "22:00", "22:30", "23:00", "23:30", "00:00", "00:30", "01:00", "01:30"]
|
| 47 |
if weekday == 4 or weekday == 5: slots.extend(["02:00", "02:30"])
|
| 48 |
|
|
|
|
| 49 |
now = datetime.now(TAIPEI_TZ)
|
|
|
|
| 50 |
if date_obj.date() == now.date():
|
| 51 |
+
current_time_str = now.strftime("%H:%M")
|
| 52 |
valid_slots = []
|
| 53 |
for s in slots:
|
| 54 |
+
h = int(s.split(":")[0])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
if h < 5:
|
| 56 |
valid_slots.append(s)
|
| 57 |
elif s > current_time_str:
|
| 58 |
valid_slots.append(s)
|
| 59 |
+
slots = valid_slots
|
|
|
|
| 60 |
|
|
|
|
| 61 |
clean_date = date_str.split(" ")[0]
|
| 62 |
daily_limit = SPECIAL_DAYS.get(clean_date, DEFAULT_LIMIT)
|
| 63 |
|
|
|
|
| 68 |
status_msg = f"✨ {date_str} (剩餘座位: {remaining} 位)"
|
| 69 |
except: status_msg = f"✨ {date_str}"
|
| 70 |
|
|
|
|
| 71 |
return gr.update(choices=slots, value=slots[0] if slots else None), status_msg
|
| 72 |
|
|
|
|
| 73 |
def init_ui():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
fresh_dates = get_date_options()
|
| 75 |
default_date = fresh_dates[0]
|
| 76 |
time_update, status_msg = update_time_slots(default_date)
|
| 77 |
+
return (gr.update(choices=fresh_dates, value=default_date), time_update, status_msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
# --- 4. 核心邏輯 (抓 ID 與 處理訂位) ---
|
| 80 |
def get_line_id_from_url(request: gr.Request):
|
|
|
|
| 83 |
return ""
|
| 84 |
|
| 85 |
def handle_booking(name, tel, email, date_str, time, pax, remarks, line_id):
|
| 86 |
+
# 1. 基本必填檢查
|
| 87 |
if not name or not tel or not date_str or not time:
|
| 88 |
+
return "⚠️ 請完整填寫必填欄位 (姓名、電話、日期、時間)"
|
| 89 |
+
|
| 90 |
+
# 🔥🔥🔥 [新增] 格式驗證邏輯 🔥🔥🔥
|
| 91 |
+
|
| 92 |
+
# 2. Email 驗證 (如果用戶有填寫 Email,就必須包含 @)
|
| 93 |
+
if email and "@" not in email:
|
| 94 |
+
return "⚠️ Email 格式錯誤 (請確認包含 @ 符號)"
|
| 95 |
+
|
| 96 |
+
# 3. 電話驗證 (寬鬆模式:相容外國人)
|
| 97 |
+
# 邏輯:不限制符號 (+, -, 空白, 括號都可),但「數字」的總數至少要有 6 碼
|
| 98 |
+
# 例如: "+886 912-345-678" (數字12碼) -> 通過
|
| 99 |
+
# 例如: "Test Phone" (數字0碼) -> 失敗
|
| 100 |
+
digit_count = sum(c.isdigit() for c in tel)
|
| 101 |
+
if digit_count < 6:
|
| 102 |
+
return "⚠️ 電話號碼格式錯誤 (請填寫有效的電話號碼)"
|
| 103 |
+
|
| 104 |
+
# 🔥🔥🔥 驗證結束 🔥🔥🔥
|
| 105 |
|
| 106 |
clean_date = date_str.split(" ")[0]
|
| 107 |
daily_limit = SPECIAL_DAYS.get(clean_date, DEFAULT_LIMIT)
|
|
|
|
| 126 |
try:
|
| 127 |
supabase.table("bookings").insert(data).execute()
|
| 128 |
|
| 129 |
+
# [LINE 通知老闆 - 新訂位]
|
| 130 |
if LINE_ACCESS_TOKEN and LINE_ADMIN_ID:
|
| 131 |
src = "🟢 LINE用戶" if line_id else "⚪ 訪客"
|
| 132 |
note = remarks if remarks else "無"
|
|
|
|
| 152 |
|
| 153 |
OFFICIAL_SITE = "https://ciecietaipei.github.io/index.html"
|
| 154 |
|
| 155 |
+
notify_msg = "" # 準備要發給老闆的訊息
|
| 156 |
|
| 157 |
if action == 'confirm' and bid:
|
| 158 |
try:
|
| 159 |
+
# 1. 更新資料庫
|
| 160 |
supabase.table("bookings").update({"status": "顧客已確認"}).eq("id", bid).execute()
|
| 161 |
|
| 162 |
+
# 2. 🔥 抓訂位資料 (為了通知老闆是誰按了確認)
|
| 163 |
res = supabase.table("bookings").select("name, date, time, pax").eq("id", bid).execute()
|
| 164 |
if res.data:
|
| 165 |
b = res.data[0]
|
| 166 |
+
notify_msg = f"🎉 顧客已確認出席!\n{b['date']} {b['time']}\n{b['name']} ({b['pax']}人)"
|
| 167 |
|
| 168 |
final_url = f"{OFFICIAL_SITE}?status=confirmed"
|
| 169 |
except:
|
|
|
|
| 171 |
|
| 172 |
elif action == 'cancel' and bid:
|
| 173 |
try:
|
| 174 |
+
# 1. 更新資料庫
|
| 175 |
supabase.table("bookings").update({"status": "顧客已取消"}).eq("id", bid).execute()
|
| 176 |
|
| 177 |
+
# 2. 🔥 抓訂位資料
|
| 178 |
res = supabase.table("bookings").select("name, date, time").eq("id", bid).execute()
|
| 179 |
if res.data:
|
| 180 |
b = res.data[0]
|
| 181 |
+
notify_msg = f"⚠️ 顧客已取消...\n{b['date']} {b['time']}\n{b['name']}"
|
| 182 |
|
| 183 |
final_url = f"{OFFICIAL_SITE}?status=canceled"
|
| 184 |
except:
|
|
|
|
| 186 |
else:
|
| 187 |
final_url = ""
|
| 188 |
|
| 189 |
+
# 🔥🔥🔥 3. 這裡執行發送通知給老闆 (使用 Messaging API) 🔥🔥🔥
|
| 190 |
if notify_msg and LINE_ACCESS_TOKEN and LINE_ADMIN_ID:
|
| 191 |
try:
|
| 192 |
requests.post(
|
| 193 |
"https://api.line.me/v2/bot/message/push",
|
| 194 |
headers={"Authorization": f"Bearer {LINE_ACCESS_TOKEN}", "Content-Type": "application/json"},
|
| 195 |
json={
|
| 196 |
+
"to": LINE_ADMIN_ID, # 發給老闆
|
| 197 |
"messages": [{"type": "text", "text": notify_msg}]
|
| 198 |
}
|
| 199 |
)
|
| 200 |
except Exception as e:
|
| 201 |
+
print(f"通知老闆失敗: {e}")
|
| 202 |
|
| 203 |
return final_url
|
| 204 |
|
|
|
|
| 231 |
status_box = gr.Markdown("請先選擇日期...", visible=True)
|
| 232 |
time_slot = gr.Dropdown(choices=[], label="可用時段 Available Time", interactive=True)
|
| 233 |
|
|
|
|
| 234 |
demo.load(init_ui, None, [booking_date, time_slot, status_box])
|
| 235 |
|
| 236 |
gr.HTML("<div style='height: 10px'></div>")
|