DeepLearning101 commited on
Commit
1147dda
·
verified ·
1 Parent(s): 79e6794

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +78 -90
app.py CHANGED
@@ -4,8 +4,10 @@ import requests
4
  from supabase import create_client, Client
5
  from datetime import datetime, timedelta, timezone
6
 
7
- # --- 設定 ---
8
  TAIPEI_TZ = timezone(timedelta(hours=8))
 
 
9
  LINE_ACCESS_TOKEN = os.getenv("LINE_ACCESS_TOKEN")
10
  LINE_ADMIN_ID = os.getenv("LINE_ADMIN_ID")
11
  SUPABASE_URL = os.getenv("SUPABASE_URL")
@@ -13,15 +15,10 @@ SUPABASE_KEY = os.getenv("SUPABASE_KEY")
13
 
14
  if SUPABASE_URL and SUPABASE_KEY:
15
  supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
 
 
16
 
17
- # --- 📅 座位控管設定 (在此修改特殊節日上限) ---
18
- DEFAULT_LIMIT = 30
19
- SPECIAL_DAYS = {
20
- "2026-12-31": 10, # 跨年夜
21
- "2026-02-14": 15 # 情人節
22
- }
23
-
24
- # --- 輔助函式 ---
25
  def get_date_options():
26
  options = []
27
  today = datetime.now(TAIPEI_TZ)
@@ -41,154 +38,147 @@ def update_time_slots(date_str):
41
  except: return gr.update(choices=[]), "日期格式錯誤"
42
 
43
  slots = ["18:00", "18:30", "19:00", "19:30", "20:00", "20:30",
44
- "21:00", "21:30", "22:00", "22:30", "23:00", "23:30", "00:00", "00:30", "01:00"]
45
- if weekday == 4 or weekday == 5: slots.extend(["01:30", "02:00", "02:30"])
46
-
47
- # 檢查剩餘座位
48
- clean_date = date_str.split(" ")[0]
49
- daily_limit = SPECIAL_DAYS.get(clean_date, DEFAULT_LIMIT)
50
 
51
- try:
52
- res = supabase.table("bookings").select("pax").eq("date", date_str).neq("status", "顧客已取消").execute()
53
- current_total = sum([item['pax'] for item in res.data])
54
- remaining = daily_limit - current_total
55
- status_msg = f"✨ {date_str} (剩餘座位: {remaining} 位)"
56
- except: status_msg = f" {date_str}"
57
-
58
  return gr.update(choices=slots, value=slots[0] if slots else None), status_msg
59
 
60
- # --- 核心邏輯 ---
61
  def get_line_id_from_url(request: gr.Request):
62
- if request: return request.query_params.get("line_id", "")
 
63
  return ""
64
 
65
- def handle_booking(name, tel, email, date_str, time, pax, remarks, line_id):
 
66
  if not name or not tel or not date_str or not time:
67
- return "⚠️ 請完整填寫必填欄位"
68
-
69
- # 座位檢查
70
- clean_date = date_str.split(" ")[0]
71
- daily_limit = SPECIAL_DAYS.get(clean_date, DEFAULT_LIMIT)
72
- try:
73
- res = supabase.table("bookings").select("pax").eq("date", date_str).neq("status", "顧客已取消").execute()
74
- if sum([i['pax'] for i in res.data]) + pax > daily_limit:
75
- return "⚠️ 抱歉,該時段剩餘座位不足,請調整人數或日期。"
76
- except: pass
77
-
78
- # 防重複
79
  try:
80
- existing = supabase.table("bookings").select("id").eq("tel", tel).eq("date", date_str).eq("time", time).neq("status", "顧客已取消").execute()
81
- if existing.data: return "⚠️ 您已預約過此時段,請勿重複提交。"
 
 
 
82
  except: pass
83
 
84
- data = {"name": name, "tel": tel, "email": email, "date": date_str, "time": time, "pax": pax, "remarks": remarks, "status": "待處理", "user_id": line_id}
 
 
 
 
 
85
 
86
  try:
 
87
  supabase.table("bookings").insert(data).execute()
 
 
88
  if LINE_ACCESS_TOKEN and LINE_ADMIN_ID:
89
- src = "🟢 LINE用戶" if line_id else "⚪ 訪客"
90
- msg = f"🔥 新訂位 ({src})\n{name} / {tel}\n{date_str} {time} ({pax}人)"
91
- 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}]})
 
 
92
 
93
- 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 確認信。</p></div>"""
94
- except Exception as e: return f"❌ 系統錯誤: {str(e)}"
95
-
96
- # --- Webhook (確認/取消) ---
 
 
 
 
 
 
 
 
 
97
  def check_confirmation(request: gr.Request):
98
  if not request: return ""
99
- action = request.query_params.get('action')
100
- bid = request.query_params.get('id')
 
101
 
102
  if action == 'confirm' and bid:
103
  try:
104
  supabase.table("bookings").update({"status": "顧客已確認"}).eq("id", bid).execute()
105
- return f"""<script>alert('✅ 訂位已確認 (編號 {bid})');</script><div style='padding:20px; background:#d4af37; color:black; text-align:center; border-radius:8px;'>🎉 感謝確認!</div>"""
106
  except: return ""
 
107
  elif action == 'cancel' and bid:
108
  try:
109
  supabase.table("bookings").update({"status": "顧客已取消"}).eq("id", bid).execute()
110
- return f"""<script>alert('已取消訂位 (編號 {bid})');</script><div style='padding:20px; background:#333; color:#ff5252; text-align:center; border:1px solid #ff5252; border-radius:8px;'>🚫 訂位已取消。</div>"""
111
  except: return ""
112
  return ""
113
 
114
- # --- 6. 介面設定 (Theme & CSS) ---
115
  theme = gr.themes.Soft(
116
- primary_hue="amber",
117
- neutral_hue="zinc",
118
  font=[gr.themes.GoogleFont("Playfair Display"), "ui-sans-serif", "sans-serif"],
119
  ).set(
120
- body_background_fill="#0F0F0F",
121
- block_background_fill="#1a1a1a",
122
- block_border_width="1px",
123
- block_border_color="#333",
124
- input_background_fill="#262626",
125
- input_border_color="#444",
126
- body_text_color="#E0E0E0",
127
- block_title_text_color="#d4af37",
128
- button_primary_background_fill="#d4af37",
129
- button_primary_text_color="#000000",
130
  )
131
 
132
  custom_css = """
133
  footer {display: none !important;}
134
  .gradio-container, .block, .row, .column { overflow: visible !important; }
135
- .options, .wrap .options {
136
- background-color: #262626 !important;
137
- border: 1px solid #d4af37 !important;
138
- z-index: 10000 !important;
139
- box-shadow: 0 5px 15px rgba(0,0,0,0.5);
140
- }
141
  .item, .options .item { color: #E0E0E0 !important; padding: 8px 12px !important; }
142
  .item:hover, .item.selected, .options .item:hover { background-color: #d4af37 !important; color: black !important; }
143
  input:focus, .dropdown-trigger:focus-within { border-color: #d4af37 !important; box-shadow: 0 0 8px rgba(212, 175, 55, 0.4) !important; }
144
  h3 { border-bottom: 1px solid #444; padding-bottom: 5px; margin-bottom: 10px; }
145
- .legal-footer {
146
- text-align: center; margin-top: 15px; padding-top: 15px; border-top: 1px solid #333; color: #666; font-size: 0.75rem; line-height: 1.5; font-family: sans-serif;
147
- }
148
  .legal-footer strong { color: #888; }
149
  """
150
 
151
- # --- 7. 介面佈局 (完整版) ---
152
  with gr.Blocks(theme=theme, css=custom_css, title="Booking") as demo:
153
 
154
- # [隱藏] 用來接收 URL 確認參數的區塊
 
155
  confirm_msg_box = gr.HTML()
156
 
157
- # 頁面載入時,執行 check_confirmation
158
- demo.load(check_confirmation, inputs=None, outputs=confirm_msg_box)
 
159
 
160
  with gr.Row():
161
  with gr.Column():
162
  gr.Markdown("### 📅 預約資訊 Booking Info")
163
-
164
- date_options = get_date_options()
165
- booking_date = gr.Dropdown(choices=date_options, label="選擇日期 Select Date", interactive=True)
166
  pax_count = gr.Slider(minimum=1, maximum=10, value=2, step=1, label="用餐人數 Guest Count")
167
-
168
  with gr.Column():
169
  gr.Markdown("### 🕰️ 選擇時段 Time Slot")
170
  status_box = gr.Markdown("請先選擇日期...", visible=True)
171
  time_slot = gr.Dropdown(choices=[], label="可用時段 Available Time", interactive=True)
172
 
173
  gr.HTML("<div style='height: 10px'></div>")
174
-
175
  gr.Markdown("### 👤 聯絡人資料 Contact,收到確認 E-Mail 並點擊 確認出席 才算訂位成功")
176
  with gr.Group():
177
  with gr.Row():
178
  cust_name = gr.Textbox(label="訂位姓名 Name *", placeholder="ex. 王小明")
179
  cust_tel = gr.Textbox(label="手機號碼 Phone *", placeholder="ex. 0912-xxx-xxx")
180
-
181
  with gr.Row():
182
  cust_email = gr.Textbox(label="電子信箱 E-mail (接收確認信用,請記得檢查垃圾信件匣。)", placeholder="example@gmail.com")
183
  with gr.Row():
184
  cust_remarks = gr.Textbox(label="備註 Remarks (過敏/慶生/特殊需求)", lines=2)
185
 
186
  gr.HTML("<div style='height: 15px'></div>")
187
-
188
  submit_btn = gr.Button("確認預約 Request Booking (系統會記錄是否曾 No Show)", size="lg", variant="primary")
189
  output_msg = gr.HTML()
190
-
191
- # 頁尾警語
192
  gr.HTML("""
193
  <div class="legal-footer">
194
  <p style="margin-bottom: 5px;">© 2026 CIE CIE TAIPEI. All Rights Reserved.</p>
@@ -197,14 +187,12 @@ with gr.Blocks(theme=theme, css=custom_css, title="Booking") as demo:
197
  </div>
198
  """)
199
 
200
- # --- 互動事件綁定 ---
201
- # 1. 日期改變 -> 更新時段
202
  booking_date.change(update_time_slots, inputs=booking_date, outputs=[time_slot, status_box])
203
 
204
- # 2. 送出按鈕 -> 處理訂位
205
  submit_btn.click(
206
  handle_booking,
207
- inputs=[cust_name, cust_tel, cust_email, booking_date, time_slot, pax_count, cust_remarks],
208
  outputs=output_msg
209
  )
210
 
 
4
  from supabase import create_client, Client
5
  from datetime import datetime, timedelta, timezone
6
 
7
+ # 設定台北時區
8
  TAIPEI_TZ = timezone(timedelta(hours=8))
9
+
10
+ # --- 1. 連線設定 ---
11
  LINE_ACCESS_TOKEN = os.getenv("LINE_ACCESS_TOKEN")
12
  LINE_ADMIN_ID = os.getenv("LINE_ADMIN_ID")
13
  SUPABASE_URL = os.getenv("SUPABASE_URL")
 
15
 
16
  if SUPABASE_URL and SUPABASE_KEY:
17
  supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
18
+ else:
19
+ print("⚠️ 警告:SUPABASE 環境變數未設定")
20
 
21
+ # --- 2. 輔助函式 (日期與時間) ---
 
 
 
 
 
 
 
22
  def get_date_options():
23
  options = []
24
  today = datetime.now(TAIPEI_TZ)
 
38
  except: return gr.update(choices=[]), "日期格式錯誤"
39
 
40
  slots = ["18:00", "18:30", "19:00", "19:30", "20:00", "20:30",
41
+ "21:00", "21:30", "22:00", "22:30", "23:00", "23:30",
42
+ "00:00", "00:30", "01:00"]
 
 
 
 
43
 
44
+ if weekday == 4 or weekday == 5:
45
+ slots.extend(["01:30", "02:00", "02:30"])
46
+ status_msg = f"✨ 已選擇 {date_str} (週末營業至 03:00)"
47
+ else:
48
+ slots.extend(["01:30"])
49
+ status_msg = f"🌙 已選擇 {date_str} (平日營業至 02:00)"
50
+
51
  return gr.update(choices=slots, value=slots[0] if slots else None), status_msg
52
 
53
+ # --- [新功能] 抓取網址中的 LINE ID ---
54
  def get_line_id_from_url(request: gr.Request):
55
+ if request:
56
+ return request.query_params.get("line_id", "")
57
  return ""
58
 
59
+ # --- 3. 核心邏輯:處理訂位 ---
60
+ def handle_booking(name, tel, email, date_str, time, pax, remarks, line_id): # 新增 line_id
61
  if not name or not tel or not date_str or not time:
62
+ return "⚠️ 請完整填寫必填欄位 (姓名、電話、日期、時間)"
63
+
64
+ # 防呆機制:檢查是否重複提交
 
 
 
 
 
 
 
 
 
65
  try:
66
+ existing = supabase.table("bookings").select("id")\
67
+ .eq("tel", tel).eq("date", date_str).eq("time", time)\
68
+ .neq("status", "顧客已取消").execute()
69
+ if existing.data and len(existing.data) > 0:
70
+ return "⚠️ 系統偵測到您已預約過此時段,請勿重複提交。"
71
  except: pass
72
 
73
+ # 準備資料 (新增 user_id)
74
+ data = {
75
+ "name": name, "tel": tel, "email": email, "date": date_str, "time": time,
76
+ "pax": pax, "remarks": remarks, "status": "待處理",
77
+ "user_id": line_id # 存入 LINE ID
78
+ }
79
 
80
  try:
81
+ # 寫入資料庫
82
  supabase.table("bookings").insert(data).execute()
83
+
84
+ # 發送 LINE Notify 給老闆 (顯示來源)
85
  if LINE_ACCESS_TOKEN and LINE_ADMIN_ID:
86
+ source = "🟢 LINE用戶" if line_id else "⚪ 一般訪客"
87
+ msg = f"🔥 新訂位 ({source})\n姓名:{name}\n電話:{tel}\n時間:{date_str} {time}\n人數:{pax}"
88
+ requests.post("https://api.line.me/v2/bot/message/push",
89
+ headers={"Authorization": f"Bearer {LINE_ACCESS_TOKEN}", "Content-Type": "application/json"},
90
+ json={"to": LINE_ADMIN_ID, "messages": [{"type": "text", "text": msg}]})
91
 
92
+ return """
93
+ <div style='text-align: center; color: #fff; padding: 20px; border: 1px solid #d4af37; border-radius: 8px; background: #222;'>
94
+ <h2 style='color: #d4af37; margin: 0;'>Request Received</h2>
95
+ <p style='margin: 10px 0;'>🥂 預約申請已提交</p>
96
+ <p style='font-size: 0.9em; color: #aaa;'>系統將發送確認信至您的 Email。</p>
97
+ <p style='font-size: 0.85em; color: #ff5252; margin-top: 5px;'>(若未收到,請檢查您的<b>垃圾信件匣</b>)</p>
98
+ </div>
99
+ """
100
+ except Exception as e:
101
+ print(f"Error: {e}")
102
+ return f"❌ 系統錯誤: {str(e)}"
103
+
104
+ # --- 4. 核心邏輯:處理確認連結 (Webhook) ---
105
  def check_confirmation(request: gr.Request):
106
  if not request: return ""
107
+ params = request.query_params
108
+ action = params.get('action')
109
+ bid = params.get('id')
110
 
111
  if action == 'confirm' and bid:
112
  try:
113
  supabase.table("bookings").update({"status": "顧客已確認"}).eq("id", bid).execute()
114
+ return f"""<script>document.addEventListener('DOMContentLoaded', function() {{alert('✅ 感謝您!訂位已確認 (編號 {bid})');}});</script><div style='padding:20px; background:#d4af37; color:black; text-align:center; margin-bottom:20px; border-radius:8px; font-weight:bold;'>🎉 感謝您的確認!我們期待您的光臨。 (訂單編號: {bid})</div>"""
115
  except: return ""
116
+
117
  elif action == 'cancel' and bid:
118
  try:
119
  supabase.table("bookings").update({"status": "顧客已取消"}).eq("id", bid).execute()
120
+ return f"""<script>document.addEventListener('DOMContentLoaded', function() {{alert('已為您取消訂位 (編號 {bid})');}});</script><div style='padding:20px; background:#333; border: 1px solid #ff5252; color:#ff5252; text-align:center; margin-bottom:20px; border-radius:8px;'>🚫 訂位已取消。<br>期待您下次有機會再來訪。</div>"""
121
  except: return ""
122
  return ""
123
 
124
+ # --- 5. 介面設定 (保留您的樣式) ---
125
  theme = gr.themes.Soft(
126
+ primary_hue="amber", neutral_hue="zinc",
 
127
  font=[gr.themes.GoogleFont("Playfair Display"), "ui-sans-serif", "sans-serif"],
128
  ).set(
129
+ body_background_fill="#0F0F0F", block_background_fill="#1a1a1a", block_border_width="1px", block_border_color="#333",
130
+ input_background_fill="#262626", input_border_color="#444", body_text_color="#E0E0E0", block_title_text_color="#d4af37",
131
+ button_primary_background_fill="#d4af37", button_primary_text_color="#000000",
 
 
 
 
 
 
 
132
  )
133
 
134
  custom_css = """
135
  footer {display: none !important;}
136
  .gradio-container, .block, .row, .column { overflow: visible !important; }
137
+ .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); }
 
 
 
 
 
138
  .item, .options .item { color: #E0E0E0 !important; padding: 8px 12px !important; }
139
  .item:hover, .item.selected, .options .item:hover { background-color: #d4af37 !important; color: black !important; }
140
  input:focus, .dropdown-trigger:focus-within { border-color: #d4af37 !important; box-shadow: 0 0 8px rgba(212, 175, 55, 0.4) !important; }
141
  h3 { border-bottom: 1px solid #444; padding-bottom: 5px; margin-bottom: 10px; }
142
+ .legal-footer { text-align: center; margin-top: 15px; padding-top: 15px; border-top: 1px solid #333; color: #666; font-size: 0.75rem; line-height: 1.5; font-family: sans-serif; }
 
 
143
  .legal-footer strong { color: #888; }
144
  """
145
 
146
+ # --- 6. 介面佈局 ---
147
  with gr.Blocks(theme=theme, css=custom_css, title="Booking") as demo:
148
 
149
+ # [新功能] 隱藏欄位接收 LINE ID
150
+ line_id_state = gr.Textbox(visible=False)
151
  confirm_msg_box = gr.HTML()
152
 
153
+ # 載入時執行 JS 抓取參數
154
+ demo.load(get_line_id_from_url, None, line_id_state)
155
+ demo.load(check_confirmation, None, confirm_msg_box)
156
 
157
  with gr.Row():
158
  with gr.Column():
159
  gr.Markdown("### 📅 預約資訊 Booking Info")
160
+ booking_date = gr.Dropdown(choices=get_date_options(), label="選擇日期 Select Date", interactive=True)
 
 
161
  pax_count = gr.Slider(minimum=1, maximum=10, value=2, step=1, label="用餐人數 Guest Count")
 
162
  with gr.Column():
163
  gr.Markdown("### 🕰️ 選擇時段 Time Slot")
164
  status_box = gr.Markdown("請先選擇日期...", visible=True)
165
  time_slot = gr.Dropdown(choices=[], label="可用時段 Available Time", interactive=True)
166
 
167
  gr.HTML("<div style='height: 10px'></div>")
 
168
  gr.Markdown("### 👤 聯絡人資料 Contact,收到確認 E-Mail 並點擊 確認出席 才算訂位成功")
169
  with gr.Group():
170
  with gr.Row():
171
  cust_name = gr.Textbox(label="訂位姓名 Name *", placeholder="ex. 王小明")
172
  cust_tel = gr.Textbox(label="手機號碼 Phone *", placeholder="ex. 0912-xxx-xxx")
 
173
  with gr.Row():
174
  cust_email = gr.Textbox(label="電子信箱 E-mail (接收確認信用,請記得檢查垃圾信件匣。)", placeholder="example@gmail.com")
175
  with gr.Row():
176
  cust_remarks = gr.Textbox(label="備註 Remarks (過敏/慶生/特殊需求)", lines=2)
177
 
178
  gr.HTML("<div style='height: 15px'></div>")
 
179
  submit_btn = gr.Button("確認預約 Request Booking (系統會記錄是否曾 No Show)", size="lg", variant="primary")
180
  output_msg = gr.HTML()
181
+
 
182
  gr.HTML("""
183
  <div class="legal-footer">
184
  <p style="margin-bottom: 5px;">© 2026 CIE CIE TAIPEI. All Rights Reserved.</p>
 
187
  </div>
188
  """)
189
 
 
 
190
  booking_date.change(update_time_slots, inputs=booking_date, outputs=[time_slot, status_box])
191
 
192
+ # [關鍵] 送出時把 line_id_state 也傳進去
193
  submit_btn.click(
194
  handle_booking,
195
+ inputs=[cust_name, cust_tel, cust_email, booking_date, time_slot, pax_count, cust_remarks, line_id_state],
196
  outputs=output_msg
197
  )
198