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()