DeepLearning101 commited on
Commit
0130dd1
·
verified ·
1 Parent(s): 64ee3c6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +115 -158
app.py CHANGED
@@ -29,24 +29,21 @@ def get_bookings():
29
  except:
30
  return pd.DataFrame()
31
 
32
- # 🔥🔥🔥 後端:暴力發送邏輯 (加上 Print Debug) 🔥🔥🔥
33
  def send_confirmation_hybrid(booking_id):
34
- # Debug: 這裡會印在 Hugging Face 的 Logs 裡
35
- print(f"🔥 [Backend] 收到發送請求,ID: {booking_id}")
36
-
37
- if not booking_id:
38
- return "❌ 錯誤:後端收到的 ID 為空!"
39
 
40
  try:
41
  # 1. 撈資料
42
  res = supabase.table("bookings").select("*").eq("id", booking_id).execute()
43
- if not res.data: return f"❌ 找不到 ID {booking_id} 的訂單"
44
  booking = res.data[0]
45
 
46
  email = booking.get('email')
47
  user_id = booking.get('user_id')
48
  current_status = booking.get('status', '')
49
 
 
50
  confirm_link = f"{PUBLIC_SPACE_URL}/?id={booking_id}&action=confirm"
51
  cancel_link = f"{PUBLIC_SPACE_URL}/?id={booking_id}&action=cancel"
52
 
@@ -56,37 +53,41 @@ def send_confirmation_hybrid(booking_id):
56
  is_reminder = "確認" in current_status
57
 
58
  if is_reminder:
59
- # 提醒模式
60
  action_label = "提醒"
61
- mail_subject = f"🔔 訂位提醒: {booking['date']} - Cié Cié Taipei"
 
62
  line_text = (
63
- f"🔔 訂位提醒\n\n"
64
  f"{booking['name']} 您好,期待今晚與您相見!\n\n"
65
  f"📅 日期:{booking['date']}\n"
66
  f"⏰ 時間:{booking['time']}\n"
67
  f"👥 人數:{booking['pax']} 位\n\n"
68
  f"座位已為您準備好,若需變更請聯繫我們。"
69
  )
 
70
  mail_html = f"""
71
- <div style="background:#1a1a1a; color:#d4af37; padding:20px; border-radius:8px;">
72
- <h2 style="text-align:center; border-bottom:1px solid #444;">Cié Cié Taipei</h2>
73
- <p style="color:#eee;"><strong>{booking['name']}</strong> 您好,行前提醒:</p>
74
- <div style="background:#2a2a2a; padding:15px; margin:20px 0; border-left:4px solid #f1c40f;">
75
- <ul style="color:#ddd;">
76
- <li>📅 日期:{booking['date']}</li>
77
- <li>⏰ 時間:{booking['time']}</li>
78
- <li>👥 人數:{booking['pax']} 位</li>
79
  </ul>
80
  </div>
81
  <div style="text-align:center; margin-top:30px;">
82
- <a href="{cancel_link}" style="color:#aaa;">若無法前來,請點此取消</a>
 
83
  </div>
84
  </div>
85
  """
86
  else:
87
- # 確認模式
88
  action_label = "確認"
89
  mail_subject = f"訂位確認: {booking['date']} - Cié Cié Taipei"
 
90
  line_text = (
91
  f"✅ 訂位確認\n\n"
92
  f"{booking['name']} 您好,已收到您的預約。\n\n"
@@ -95,25 +96,27 @@ def send_confirmation_hybrid(booking_id):
95
  f"👥 人數:{booking['pax']} 位\n\n"
96
  f"請務必查收 Email 並點擊「確認出席」,謝謝!"
97
  )
 
98
  mail_html = f"""
99
- <div style="background:#1a1a1a; color:#d4af37; padding:20px; border-radius:8px;">
100
- <h2 style="text-align:center; border-bottom:1px solid #444;">Cié Cié Taipei</h2>
101
- <p style="color:#eee;"><strong>{booking['name']}</strong> 您好,請確認您的訂位:</p>
102
- <div style="background:#2a2a2a; padding:15px; margin:20px 0; border-left:4px solid #2ecc71;">
103
- <ul style="color:#ddd;">
104
- <li>📅 日期:{booking['date']}</li>
105
- <li>⏰ 時間:{booking['time']}</li>
106
- <li>👥 人數:{booking['pax']} 位</li>
107
  </ul>
108
  </div>
109
  <div style="text-align:center; margin-top:30px;">
110
- <a href="{confirm_link}" style="background:#d4af37; color:#000; padding:12px 30px; text-decoration:none; border-radius:50px; font-weight:bold;">✅ 確認出席</a>
111
- <a href="{cancel_link}" style="color:#aaa; margin-left:20px;">取消</a>
112
  </div>
113
  </div>
114
  """
115
 
116
- # 3. 發送
 
117
  if email and "@" in email and GAS_MAIL_URL:
118
  try:
119
  requests.post(GAS_MAIL_URL, json={"to": email, "subject": mail_subject, "htmlBody": mail_html, "name": "Cié Cié Taipei"})
@@ -122,117 +125,94 @@ def send_confirmation_hybrid(booking_id):
122
  else:
123
  log_msg += "⚠️ 無Mail "
124
 
 
125
  if user_id and len(str(user_id)) > 5 and LINE_ACCESS_TOKEN:
126
  try:
127
- requests.post("https://api.line.me/v2/bot/message/push",
128
  headers={"Authorization": f"Bearer {LINE_ACCESS_TOKEN}"},
129
  json={"to": user_id, "messages": [{"type": "text", "text": line_text}]})
130
- log_msg += f"✅ LINE({action_label}) "
 
 
 
131
  except Exception as e: log_msg += f"❌ LINEErr({str(e)}) "
132
  else:
133
  log_msg += "⚠️ 無LINE ID "
134
 
 
135
  if not is_reminder:
136
  supabase.table("bookings").update({"status": "已發確認信"}).eq("id", booking_id).execute()
137
 
138
- print(f"🔥 [Backend] 處理完成: {log_msg}")
139
  return log_msg
140
 
141
- except Exception as e:
142
- print(f"🔥 [Backend] 嚴重錯誤: {str(e)}")
143
- return f"嚴重錯誤: {str(e)}"
144
 
145
- # --- 卡片渲染 ( CSS 驅動點擊) ---
146
  def render_booking_cards():
147
  df = get_bookings()
148
- count_html = f"<div style='color:#bbb; margin-bottom:20px; text-align:right; font-size:16px; padding: 0 10px;'>📊 共找到 <span style='color:#fff; font-weight:bold;'>{len(df)}</span> 筆資料</div>"
149
  if df.empty: return f"{count_html}<div style='text-align:center; padding:60px; color:#666; font-size:1.5em;'>📭 目前沒有訂位資料</div>"
150
 
151
- cards_html = f"{count_html}<div style='display: flex; flex-direction: column; gap: 24px; padding-bottom: 50px;'>"
152
 
153
  for index, row in df.iterrows():
154
  status = row.get('status', '待處理')
155
 
156
- status_color = "#ccc"; border_color = "#444"
157
- if '確認' in status: status_color = "#2ecc71"; border_color = "#2ecc71"
158
- elif '取消' in status: status_color = "#e74c3c"; border_color = "#e74c3c"
159
- elif '已發' in status: status_color = "#f1c40f"; border_color = "#f1c40f"
160
-
161
- is_canceled = '取消' in status
162
- is_confirmed = '確認' in status
163
-
164
- # JS Class 標記
165
- btn_class = "op-btn"
166
- btn_data = f"data-id='{row['id']}'"
167
-
168
- if is_canceled:
169
- btn_style = "background: #333; color: #666; cursor: not-allowed;"
170
- btn_text = "🚫 已取消"
171
- btn_class = "" # 取消狀態不給按
172
- elif is_confirmed:
173
- btn_style = "background: #2c3e50; color: #fff; border: 1px solid #555; box-shadow: 0 0 8px rgba(46, 204, 113, 0.3);"
174
- btn_text = "🔔 發送提醒"
175
- elif '已發' in status:
176
- btn_style = "background: #2c3e50; color: #ddd; border: 1px solid #555;"
177
- btn_text = "🔄 重發確認"
178
- else:
179
- btn_style = "background: #d4af37; color: #000; font-weight:800; box-shadow: 0 4px 12px rgba(212, 175, 55, 0.5);"
180
- btn_text = "🚀 發送確認"
181
 
 
182
  card = f"""
183
  <div class="booking-card" style="
184
- background: #1a1a1a; border-left: 8px solid {border_color}; border-radius: 12px; padding: 24px;
185
- box-shadow: 0 8px 20px rgba(0,0,0,0.6); font-family: '微軟正黑體', sans-serif; position: relative; margin-bottom: 10px;">
 
 
 
 
 
186
 
187
- <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; border-bottom:1px solid #333; padding-bottom:15px;">
188
- <div style="font-size:1.3em; color:#fff; font-weight:bold;">
189
- <span style="font-size:0.8em; color:#aaa; margin-right:8px; font-weight:normal;">📅 日期</span>{row['date']}
 
190
  </div>
191
- <div style="color: {status_color}; background: {status_color}22; padding: 8px 16px; border-radius: 30px; font-size: 1em; font-weight: bold; border: 1px solid {status_color}44;">
192
  {status}
193
  </div>
194
  </div>
195
 
196
- <div style="display:grid; grid-template-columns: 1fr 1fr; gap:20px; margin-bottom:25px;">
197
  <div>
198
- <div style="font-size:1em; color:#aaa; margin-bottom:5px;">⏰ 訂位時間</div>
199
- <div style="font-size:2.2em; color:#d4af37; font-weight:900; letter-spacing:-1px; line-height:1;">{row['time']}</div>
200
  </div>
201
  <div>
202
- <div style="font-size:1em; color:#aaa; margin-bottom:5px;">👥 人數</div>
203
- <div style="font-size:2.2em; color:#fff; font-weight:900; line-height:1;">{row['pax']} <span style="font-size:0.5em; font-weight:400; color:#888;">位</span></div>
204
  </div>
205
  </div>
206
 
207
- <div style="background:#222; padding:20px; border-radius:10px; margin-bottom:20px;">
208
- <div style="margin-bottom:15px;">
209
- <span style="color:#bbb; font-size:1em; display:block; margin-bottom:4px; font-weight:bold;">👤 姓名 Name</span>
210
- <span style="color:#fff; font-size:1.5em; font-weight:bold;">{row['name']}</span>
211
- </div>
212
- <div style="margin-bottom:15px;">
213
- <span style="color:#bbb; font-size:1em; display:block; margin-bottom:4px; font-weight:bold;">📞 電話 Phone</span>
214
- <a href="tel:{row['tel']}" style="color:#69c0ff; text-decoration:none; font-size:1.4em; letter-spacing:1px; font-weight:600; border-bottom: 1px dotted #69c0ff;">{row['tel']}</a>
215
  </div>
216
- <div>
217
- <span style="color:#bbb; font-size:1em; display:block; margin-bottom:4px; font-weight:bold;">✉️ 信箱 Email</span>
218
- <span style="color:#ddd; font-size:1.1em; word-break:break-all;">{row['email'] or '未提供'}</span>
219
- </div>
220
- </div>
221
-
222
- <div style="margin-bottom:25px;">
223
- <div style="font-size:1em; color:#aaa; margin-bottom:8px;">📝 備註 Note</div>
224
- <div style="color:#f1c40f; background:#f1c40f11; padding:15px; border-radius:8px; font-size:1.1em; line-height:1.6; border: 1px solid #f1c40f33;">
225
- {row.get('remarks') or '無'}
226
  </div>
 
227
  </div>
228
 
229
- <div style="display:flex; justify-content:space-between; align-items:center; border-top:1px solid #333; padding-top:20px;">
230
- <div style="font-size: 1.1em; color: #000; font-weight: 900; background: #e0e0e0; padding: 8px 12px; border-radius: 6px; font-family: monospace;">
231
- ID: {row['id']}
232
- </div>
233
- <button class="{btn_class}" {btn_data} style="border: none; padding: 14px 30px; border-radius: 8px; font-size: 1.1em; transition: all 0.2s; min-width: 150px; cursor: pointer; {btn_style}">
234
- {btn_text}
235
- </button>
236
  </div>
237
  </div>
238
  """
@@ -251,56 +231,30 @@ def check_login(user, password):
251
  }
252
  else: return {error_msg: "<span style='color: red'>❌ 帳號或密碼錯誤</span>"}
253
 
254
- # 🔥🔥🔥 Debug 版 JS:有彈窗,按鈕看得到 🔥🔥🔥
255
- GLOBAL_JS = """
256
- <script>
257
- document.addEventListener('DOMContentLoaded', function() {
258
- console.log("🟢 Debug JS Loaded");
259
- });
260
-
261
- document.addEventListener('click', function(e) {
262
- if (e.target && e.target.classList.contains('op-btn')) {
263
- const id = e.target.getAttribute('data-id');
264
-
265
- // 1. 彈窗確認 JS 有活著
266
- // alert("JS 偵測到點擊!準備發送 ID: " + id);
267
-
268
- const idInput = document.querySelector('#debug_id_input input');
269
-
270
- if (idInput) {
271
- idInput.value = id;
272
- idInput.dispatchEvent(new Event('input', { bubbles: true }));
273
-
274
- setTimeout(() => {
275
- const sendBtn = document.querySelector('#debug_send_btn');
276
- if (sendBtn) {
277
- console.log("🚀 Clicking send button...");
278
- sendBtn.click();
279
- } else {
280
- alert("❌ 找不到發送按鈕 (#debug_send_btn)");
281
- }
282
- }, 100);
283
- } else {
284
- alert("❌ 找不到輸入框 (#debug_id_input)");
285
- }
286
- }
287
- });
288
- </script>
289
- """
290
-
291
  custom_css = """
292
  body, .gradio-container { background-color: #0F0F0F; color: #fff; }
293
- #booking_display { height: auto !important; max-height: none !important; overflow: visible !important; margin-bottom: 50px; }
294
- button:active { transform: scale(0.96); }
295
- #header-panel { background: #1a1a1a; padding: 15px; margin-bottom: 20px; border-radius: 10px; }
296
- .op-btn { pointer-events: auto !important; }
297
 
298
- /* 暫時不隱藏,方便 Debug */
299
- /* #debug_ops { display: none; } */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  """
301
 
 
302
  with gr.Blocks(title="Admin") as demo:
303
- gr.HTML(GLOBAL_JS)
304
 
305
  with gr.Group(visible=True) as login_row:
306
  gr.Markdown("# 🔒 Login")
@@ -311,28 +265,31 @@ with gr.Blocks(title="Admin") as demo:
311
  error_msg = gr.Markdown("")
312
 
313
  with gr.Group(visible=False) as admin_row:
314
- with gr.Row(variant="panel", elem_id="header-panel"):
315
- gr.Markdown("### 🍷 Cié Cié Dashboard")
316
- refresh_btn = gr.Button("🔄 刷新列表", size="sm", variant="secondary")
317
-
318
- # ⚠️ 這裡我把隱藏區改名為 debug_ops 並且顯示出來
319
- # 你會看到一個數字框和一個按鈕,這是正常的
320
- with gr.Row(visible=True, elem_id="debug_ops"):
321
- debug_id_input = gr.Number(label="Debug ID Input (Auto Fill)", elem_id="debug_id_input", precision=0)
322
- debug_send_btn = gr.Button("Debug Send Button (Auto Click)", elem_id="debug_send_btn")
323
 
 
 
 
324
  booking_display = gr.HTML(elem_id="booking_display")
325
- log_output = gr.Textbox(label="系統日誌 (System Log)", lines=1)
326
 
327
  login_btn.click(check_login, inputs=[username_input, password_input], outputs=[login_row, admin_row, error_msg]).then(
328
  render_booking_cards, outputs=booking_display
329
  )
 
 
330
  refresh_btn.click(render_booking_cards, outputs=booking_display)
331
 
332
- # 綁定
333
- debug_send_btn.click(
334
  send_confirmation_hybrid,
335
- inputs=debug_id_input,
336
  outputs=log_output
337
  ).then(
338
  render_booking_cards,
 
29
  except:
30
  return pd.DataFrame()
31
 
32
+ # 🔥🔥🔥 核心後端:智慧判斷發送邏輯 ( LINE Debug) 🔥🔥🔥
33
  def send_confirmation_hybrid(booking_id):
34
+ if not booking_id: return "❌ 請輸入訂單 ID"
 
 
 
 
35
 
36
  try:
37
  # 1. 撈資料
38
  res = supabase.table("bookings").select("*").eq("id", booking_id).execute()
39
+ if not res.data: return f"❌ 找不到 ID: {booking_id}"
40
  booking = res.data[0]
41
 
42
  email = booking.get('email')
43
  user_id = booking.get('user_id')
44
  current_status = booking.get('status', '')
45
 
46
+ # 連結
47
  confirm_link = f"{PUBLIC_SPACE_URL}/?id={booking_id}&action=confirm"
48
  cancel_link = f"{PUBLIC_SPACE_URL}/?id={booking_id}&action=cancel"
49
 
 
53
  is_reminder = "確認" in current_status
54
 
55
  if is_reminder:
56
+ # --- 🔔 提醒模式 (Reminder) ---
57
  action_label = "提醒"
58
+ mail_subject = f"🔔 行前提醒: {booking['date']} - Cié Cié Taipei"
59
+
60
  line_text = (
61
+ f"🔔 行前提醒\n\n"
62
  f"{booking['name']} 您好,期待今晚與您相見!\n\n"
63
  f"📅 日期:{booking['date']}\n"
64
  f"⏰ 時間:{booking['time']}\n"
65
  f"👥 人數:{booking['pax']} 位\n\n"
66
  f"座位已為您準備好,若需變更請聯繫我們。"
67
  )
68
+
69
  mail_html = f"""
70
+ <div style="background:#1a1a1a; color:#d4af37; padding:20px; border-radius:8px; font-family:sans-serif;">
71
+ <h2 style="text-align:center; border-bottom:1px solid #444; padding-bottom:15px;">Cié Cié Taipei</h2>
72
+ <p style="color:#eee; font-size:16px;"><strong>{booking['name']}</strong> 您好,行前提醒:</p>
73
+ <div style="background:#2a2a2a; padding:15px; border-radius:8px; margin:20px 0; border-left:4px solid #f1c40f;">
74
+ <ul style="color:#ddd; padding-left:20px; line-height:1.8;">
75
+ <li>📅 日期:<strong style="color:#fff;">{booking['date']}</strong></li>
76
+ <li>⏰ 時間:<strong style="color:#fff;">{booking['time']}</strong></li>
77
+ <li>👥 人數:<strong style="color:#fff;">{booking['pax']} 位</strong></li>
78
  </ul>
79
  </div>
80
  <div style="text-align:center; margin-top:30px;">
81
+ <span style="color:#888;">無需再次確認。</span><br><br>
82
+ <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>
83
  </div>
84
  </div>
85
  """
86
  else:
87
+ # --- 🚀 確認模式 (Confirmation) ---
88
  action_label = "確認"
89
  mail_subject = f"訂位確認: {booking['date']} - Cié Cié Taipei"
90
+
91
  line_text = (
92
  f"✅ 訂位確認\n\n"
93
  f"{booking['name']} 您好,已收到您的預約。\n\n"
 
96
  f"👥 人數:{booking['pax']} 位\n\n"
97
  f"請務必查收 Email 並點擊「確認出席」,謝謝!"
98
  )
99
+
100
  mail_html = f"""
101
+ <div style="background:#1a1a1a; color:#d4af37; padding:20px; border-radius:8px; font-family:sans-serif;">
102
+ <h2 style="text-align:center; border-bottom:1px solid #444; padding-bottom:15px;">Cié Cié Taipei</h2>
103
+ <p style="color:#eee; font-size:16px;"><strong>{booking['name']}</strong> 您好,請確認您的訂位:</p>
104
+ <div style="background:#2a2a2a; padding:15px; border-radius:8px; margin:20px 0; border-left:4px solid #2ecc71;">
105
+ <ul style="color:#ddd; padding-left:20px; line-height:1.8;">
106
+ <li>📅 日期:<strong style="color:#fff;">{booking['date']}</strong></li>
107
+ <li>⏰ 時間:<strong style="color:#fff;">{booking['time']}</strong></li>
108
+ <li>👥 人數:<strong style="color:#fff;">{booking['pax']} 位</strong></li>
109
  </ul>
110
  </div>
111
  <div style="text-align:center; margin-top:30px;">
112
+ <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>
113
+ <a href="{cancel_link}" style="display:inline-block; border:1px solid #555; color:#aaa; padding:11px 29px; text-decoration:none; border-radius:50px;">🚫 取消</a>
114
  </div>
115
  </div>
116
  """
117
 
118
+ # 3. 執行發送
119
+ # Email
120
  if email and "@" in email and GAS_MAIL_URL:
121
  try:
122
  requests.post(GAS_MAIL_URL, json={"to": email, "subject": mail_subject, "htmlBody": mail_html, "name": "Cié Cié Taipei"})
 
125
  else:
126
  log_msg += "⚠️ 無Mail "
127
 
128
+ # LINE (增加錯誤代碼顯示,幫您 Debug 為什麼偶爾失敗)
129
  if user_id and len(str(user_id)) > 5 and LINE_ACCESS_TOKEN:
130
  try:
131
+ r = requests.post("https://api.line.me/v2/bot/message/push",
132
  headers={"Authorization": f"Bearer {LINE_ACCESS_TOKEN}"},
133
  json={"to": user_id, "messages": [{"type": "text", "text": line_text}]})
134
+ if r.status_code == 200:
135
+ log_msg += f"✅ LINE({action_label}) "
136
+ else:
137
+ log_msg += f"❌ LINE失敗({r.status_code}: {r.text}) "
138
  except Exception as e: log_msg += f"❌ LINEErr({str(e)}) "
139
  else:
140
  log_msg += "⚠️ 無LINE ID "
141
 
142
+ # 4. 更新狀態 (僅在非提醒模式下更新)
143
  if not is_reminder:
144
  supabase.table("bookings").update({"status": "已發確認信"}).eq("id", booking_id).execute()
145
 
 
146
  return log_msg
147
 
148
+ except Exception as e: return f"嚴重錯誤: {str(e)}"
 
 
149
 
150
+ # 🔥🔥🔥 卡片渲染 (純顯示,無 JS 互動) 🔥🔥🔥
151
  def render_booking_cards():
152
  df = get_bookings()
153
+ 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>"
154
  if df.empty: return f"{count_html}<div style='text-align:center; padding:60px; color:#666; font-size:1.5em;'>📭 目前沒有訂位資料</div>"
155
 
156
+ cards_html = f"{count_html}<div style='display: flex; flex-direction: column; gap: 20px; padding-bottom: 20px;'>"
157
 
158
  for index, row in df.iterrows():
159
  status = row.get('status', '待處理')
160
 
161
+ # 顏色邏輯
162
+ status_bg = "#ccc"; status_tx = "#000"; border_color = "#444"
163
+ if '確認' in status:
164
+ status_bg = "#2ecc71"; status_tx = "#000"; border_color = "#2ecc71"
165
+ elif '取消' in status:
166
+ status_bg = "#e74c3c"; status_tx = "#fff"; border_color = "#e74c3c"
167
+ elif '已發' in status:
168
+ status_bg = "#f1c40f"; status_tx = "#000"; border_color = "#f1c40f"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
+ # 這裡不放按鈕了,改放一個超大的 ID 顯示
171
  card = f"""
172
  <div class="booking-card" style="
173
+ background: #1a1a1a;
174
+ border-left: 6px solid {border_color};
175
+ border-radius: 12px;
176
+ padding: 20px;
177
+ box-shadow: 0 4px 15px rgba(0,0,0,0.5);
178
+ font-family: '微軟正黑體', sans-serif;
179
+ position: relative;">
180
 
181
+ <div style="display:flex; justify-content:space-between; align-items:start; margin-bottom:15px;">
182
+ <div>
183
+ <span style="font-size:0.9em; color:#666; font-weight:bold; display:block;">訂單 ID</span>
184
+ <span style="font-size:2.5em; color:#fff; font-weight:900; line-height:1; font-family:monospace;">{row['id']}</span>
185
  </div>
186
+ <div style="background:{status_bg}; color:{status_tx}; padding:6px 12px; border-radius:4px; font-weight:bold; font-size:0.9em;">
187
  {status}
188
  </div>
189
  </div>
190
 
191
+ <div style="display:grid; grid-template-columns: 1fr 1fr; gap:10px; margin-bottom:15px; border-top:1px solid #333; padding-top:15px;">
192
  <div>
193
+ <span style="color:#888; font-size:0.85em;">📅 日期 Date</span><br>
194
+ <span style="color:#d4af37; font-size:1.2em; font-weight:bold;">{row['date']}</span>
195
  </div>
196
  <div>
197
+ <span style="color:#888; font-size:0.85em;">⏰ 時間 Time</span><br>
198
+ <span style="color:#d4af37; font-size:1.5em; font-weight:900;">{row['time']}</span>
199
  </div>
200
  </div>
201
 
202
+ <div style="background:#222; padding:15px; border-radius:8px; margin-bottom:15px;">
203
+ <div style="margin-bottom:8px;">
204
+ <span style="color:#fff; font-size:1.4em; font-weight:bold;">{row['name']}</span>
205
+ <span style="color:#aaa;">({row['pax']}位)</span>
 
 
 
 
206
  </div>
207
+ <div style="font-size:1.1em; margin-bottom:5px;">
208
+ 📞 <a href="tel:{row['tel']}" style="color:#69c0ff; text-decoration:none;">{row['tel']}</a>
 
 
 
 
 
 
 
 
209
  </div>
210
+ <div style="font-size:0.9em; color:#888;">✉️ {row['email'] or '-'}</div>
211
  </div>
212
 
213
+ <div style="background:#f1c40f11; padding:12px; border-radius:6px; border:1px solid #f1c40f33;">
214
+ <span style="color:#aaa; font-size:0.8em;">📝 備註:</span>
215
+ <span style="color:#f1c40f;">{row.get('remarks') or '無'}</span>
 
 
 
 
216
  </div>
217
  </div>
218
  """
 
231
  }
232
  else: return {error_msg: "<span style='color: red'>❌ 帳號或密碼錯誤</span>"}
233
 
234
+ # --- CSS ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  custom_css = """
236
  body, .gradio-container { background-color: #0F0F0F; color: #fff; }
 
 
 
 
237
 
238
+ /* 讓操作區塊固定在頂部 (Sticky) */
239
+ #op-panel {
240
+ position: sticky;
241
+ top: 0;
242
+ z-index: 100;
243
+ background: #1a1a1a;
244
+ border-bottom: 2px solid #d4af37;
245
+ padding: 15px;
246
+ margin-bottom: 20px;
247
+ box-shadow: 0 4px 10px rgba(0,0,0,0.5);
248
+ }
249
+
250
+ #booking_display {
251
+ height: auto !important;
252
+ overflow: visible !important;
253
+ }
254
  """
255
 
256
+ # --- 介面 ---
257
  with gr.Blocks(title="Admin") as demo:
 
258
 
259
  with gr.Group(visible=True) as login_row:
260
  gr.Markdown("# 🔒 Login")
 
265
  error_msg = gr.Markdown("")
266
 
267
  with gr.Group(visible=False) as admin_row:
268
+ # 🔥 操作區 (固定在頂部)
269
+ with gr.Column(elem_id="op-panel"):
270
+ gr.Markdown("### 🍷 Cié Cié 訂位管理")
271
+ with gr.Row():
272
+ # 這裡就是最原始、最不會壞的輸入框 + 按鈕
273
+ id_input = gr.Number(label="輸入 ID 發送通知", precision=0, scale=2)
274
+ send_btn = gr.Button("🚀 發送通知 / 提醒 (Hybrid)", variant="primary", scale=1)
275
+ refresh_btn = gr.Button("🔄 刷新列表", scale=1)
 
276
 
277
+ log_output = gr.Textbox(label="執行結果", lines=1)
278
+
279
+ # 卡片顯示區
280
  booking_display = gr.HTML(elem_id="booking_display")
 
281
 
282
  login_btn.click(check_login, inputs=[username_input, password_input], outputs=[login_row, admin_row, error_msg]).then(
283
  render_booking_cards, outputs=booking_display
284
  )
285
+
286
+ # 綁定刷新
287
  refresh_btn.click(render_booking_cards, outputs=booking_display)
288
 
289
+ # 綁定發送 (發送完自動刷新列表)
290
+ send_btn.click(
291
  send_confirmation_hybrid,
292
+ inputs=id_input,
293
  outputs=log_output
294
  ).then(
295
  render_booking_cards,