DeepLearning101 commited on
Commit
4a4796d
·
verified ·
1 Parent(s): e80ff0c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +100 -108
app.py CHANGED
@@ -20,7 +20,7 @@ REAL_ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD") or "2016-11-11"
20
  supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
21
 
22
  # ==========================================
23
- # 模組 1:訂位與通知管理
24
  # ==========================================
25
 
26
  def get_bookings():
@@ -36,112 +36,73 @@ def send_confirmation_hybrid(booking_id):
36
  if not booking_id: return "❌ 請輸入訂單 ID"
37
 
38
  try:
39
- # 1. 撈取訂位資料
40
  res = supabase.table("bookings").select("*").eq("id", booking_id).execute()
41
  if not res.data: return f"❌ 找不到 ID: {booking_id}"
42
  booking = res.data[0]
43
 
44
  email = booking.get('email')
45
- user_id = booking.get('user_id') # 這個就是 LINE ID
46
  current_status = booking.get('status', '')
47
 
48
- # 準備回傳網址
49
  confirm_link = f"{PUBLIC_SPACE_URL}?id={booking_id}&action=confirm"
50
  cancel_link = f"{PUBLIC_SPACE_URL}?id={booking_id}&action=cancel"
51
-
52
  log_msg = f"🆔 {booking_id} 處理結果: "
53
-
54
- # 2. 智慧判斷:是發送「提醒」還是「確認」?
55
  is_reminder = "確認" in current_status
56
 
57
  if is_reminder:
58
- # --- 🔔 提醒模式 (Reminder) ---
59
- action_label = "提醒"
60
  mail_subject = f"🔔 行前提醒: {booking['date']} - Cié Cié Taipei"
61
- line_text = (
62
- f"🔔 行前提醒\n\n"
63
- f"{booking['name']} 您好,期待今晚與您相見!\n\n"
64
- f"📅 日期:{booking['date']}\n"
65
- f"⏰ 時間:{booking['time']}\n"
66
- f"👥 人數:{booking['pax']} 位\n\n"
67
- f"座位已為您準備好,若需變更請聯繫我們。"
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="color:#aaa; 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
- line_text = (
91
- f" 確認\n\n"
92
- f"{booking['name']} 您好已收到您的預約。\n\n"
93
- f"📅 日期:{booking['date']}\n"
94
- f"⏰ 時間:{booking['time']}\n"
95
- f"👥 人數:{booking['pax']} 位\n\n"
96
- f"👉 請務必點擊下方連結「確認出席」,謝謝!\n"
97
- f"{confirm_link}"
98
- )
99
- mail_html = f"""
100
- <div style="background:#1a1a1a; color:#d4af37; padding:20px; border-radius:8px; font-family:sans-serif;">
101
  <h2 style="text-align:center; border-bottom:1px solid #444; padding-bottom:15px;">Cié Cié Taipei</h2>
102
  <p style="color:#eee; font-size:16px;"><strong>{booking['name']}</strong> 您好,請確認您的訂位:</p>
103
- <div style="background:#2a2a2a; padding:15px; border-radius:8px; margin:20px 0; border-left:4px solid #2ecc71;">
104
- <ul style="color:#ddd; padding-left:20px; line-height:1.8;">
105
- <li>📅 日期:<strong style="color:#fff;">{booking['date']}</strong></li>
106
- <li>⏰ 時間:<strong style="color:#fff;">{booking['time']}</strong></li>
107
- <li>👥 人數:<strong style="color:#fff;">{booking['pax']} 位</strong></li>
108
- </ul>
109
- </div>
110
- <div style="text-align:center; margin-top:30px;">
111
- <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>
112
- <a href="{cancel_link}" style="display:inline-block; border:1px solid #555; color:#aaa; padding:11px 29px; text-decoration:none; border-radius:50px;">🚫 取消</a>
113
- </div>
114
- </div>
115
- """
116
 
117
- # 3. 執行發送 (自動判斷有無 Email/LINE)
118
  if email and "@" in email and GAS_MAIL_URL:
119
  try:
120
  requests.post(GAS_MAIL_URL, json={"to": email, "subject": mail_subject, "htmlBody": mail_html, "name": "Cié Cié Taipei"})
121
  log_msg += f"✅ Mail送出 "
122
- except Exception as e: log_msg += f"❌ Mail失敗 "
123
- else:
124
- log_msg += "⚠️ 無Mail "
125
 
126
  if user_id and len(str(user_id)) > 5 and LINE_ACCESS_TOKEN:
127
  try:
128
- r = requests.post("https://api.line.me/v2/bot/message/push",
129
- headers={"Authorization": f"Bearer {LINE_ACCESS_TOKEN}"},
130
- json={"to": user_id, "messages": [{"type": "text", "text": line_text}]})
131
  if r.status_code == 200: log_msg += f"✅ LINE送出 "
132
  else: log_msg += f"❌ LINE失敗 "
133
- except Exception as e: log_msg += f"❌ LINE錯誤 "
134
- else:
135
- log_msg += "⚠️ 無LINE ID "
136
 
137
- # 4. 更新狀態
138
  if not is_reminder:
139
  supabase.table("bookings").update({"status": "已發確認信"}).eq("id", booking_id).execute()
140
-
141
  return log_msg
142
-
143
  except Exception as e: return f"嚴重錯誤: {str(e)}"
144
 
 
 
 
 
 
 
 
 
 
145
  def render_booking_cards():
146
  df = get_bookings()
147
  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>"
@@ -157,6 +118,7 @@ def render_booking_cards():
157
  if '確認' in status: status_bg = "#2ecc71"; status_tx = "#000"; border_color = "#2ecc71"
158
  elif '取消' in status: status_bg = "#e74c3c"; status_tx = "#fff"; border_color = "#e74c3c"
159
  elif '已發' in status: status_bg = "#f1c40f"; status_tx = "#000"; border_color = "#f1c40f"
 
160
 
161
  line_badge = "<span style='background:#00B900; color:white; padding:2px 6px; border-radius:4px; font-size:0.7em; margin-left:8px;'>LINE綁定</span>" if has_line else ""
162
 
@@ -168,7 +130,7 @@ def render_booking_cards():
168
  <span style="font-size:2.5em; color:#fff; font-weight:900; line-height:1; font-family:monospace;">{row['id']}</span>
169
  </div>
170
  <div style="text-align:right;">
171
- <div style="background:{status_bg}; color:{status_tx}; padding:6px 12px; border-radius:4px; font-weight:bold; font-size:0.9em; display:inline-block;">{status}</div>
172
  <div style="margin-top:5px;">{line_badge}</div>
173
  </div>
174
  </div>
@@ -193,57 +155,77 @@ def render_booking_cards():
193
 
194
 
195
  # ==========================================
196
- # 模組 2:菜單動態管理
197
  # ==========================================
198
 
199
  def get_menu_items():
200
  try:
201
  res = supabase.table("menu_items").select("*").order("category").order("created_at").execute()
202
  if not res.data:
203
- return pd.DataFrame(columns=['ID', '餐點名稱', '價格', '分類', '供應時段', '可外帶', '預付規則', '狀態'])
204
 
205
  df = pd.DataFrame(res.data)
206
 
207
- # 處理陣列或空值,讓畫面顯示更漂亮
208
  df['available_times'] = df['available_times'].apply(lambda x: "、".join(x) if isinstance(x, list) else "全時段")
209
  df['allow_takeout'] = df['allow_takeout'].apply(lambda x: "✅" if x else "❌")
210
  df['require_prepay'] = df['require_prepay'].apply(lambda x: "🔥 需預付" if x else "一般")
211
  df['is_active'] = df['is_active'].apply(lambda x: "🟢 販售中" if x else "🔴 已下架")
 
 
212
 
213
- # 整理給畫面顯示用的欄位
214
- display_df = df[['id', 'name', 'price', 'category', 'available_times', 'allow_takeout', 'require_prepay', 'is_active']]
215
- display_df.columns = ['ID', '餐點名稱', '價格', '分類', '供應時段', '可外帶', '預付規則', '狀態']
216
  return display_df
217
  except Exception as e:
218
  print("Fetch menu error:", e)
219
- return pd.DataFrame(columns=['ID', '餐點名稱', '價格', '分類', '供應時段', '可外帶', '預付規則', '狀態'])
 
 
 
 
 
 
 
 
 
 
 
220
 
221
- def add_menu_item(name, desc, price, category, available_times, require_prepay, allow_takeout):
222
  if not name or not price: return "⚠️ 名稱與價格為必填", get_menu_items()
223
  if not available_times: return "⚠️ 請至少選擇一個供應時段", get_menu_items()
224
 
 
 
 
225
  try:
226
  data = {
227
  "name": name,
228
  "description": desc,
229
  "price": int(price),
230
  "category": category,
231
- "available_times": available_times, # 儲存為字串陣列
232
- "allow_takeout": allow_takeout,
233
  "require_prepay": require_prepay,
234
- "is_active": True
 
235
  }
236
  supabase.table("menu_items").insert(data).execute()
237
- return f"✅ 成功新增餐點:{name}", get_menu_items()
 
 
 
238
  except Exception as e:
239
  return f"❌ 錯誤: {str(e)}", get_menu_items()
240
 
241
- def toggle_menu_item(item_id, is_active):
242
- if not item_id: return "⚠️ 請輸入餐點 ID", get_menu_items()
243
  try:
244
- supabase.table("menu_items").update({"is_active": is_active}).eq("id", item_id.strip()).execute()
 
 
245
  status_text = "上架" if is_active else "下架"
246
- return f"✅ 已將餐點 {item_id} 設 {status_text}", get_menu_items()
247
  except Exception as e:
248
  return f"❌ 錯誤: {str(e)}", get_menu_items()
249
 
@@ -278,35 +260,41 @@ with gr.Blocks(title="Cié Cié Admin", css=custom_css, theme=gr.themes.Monochro
278
  # --- 分頁 1:訂位與通知管理 ---
279
  with gr.TabItem("🍷 訂位與通知管理"):
280
  with gr.Column(elem_id="op-panel"):
281
- gr.Markdown("### 🚀 發送確認信 / 行前提醒 (系統自動偵測 LINE Email)")
282
  with gr.Row():
283
- id_input = gr.Number(label="輸入訂單 ID 發送通知", precision=0, scale=2)
284
- send_btn = gr.Button("🚀 一鍵發送通知", variant="primary", scale=1)
 
285
  refresh_btn = gr.Button("🔄 刷新列表", scale=1)
286
  log_output = gr.Textbox(label="執行結果日誌", lines=1)
287
 
288
  booking_display = gr.HTML(elem_id="booking_display")
289
 
 
290
  refresh_btn.click(render_booking_cards, outputs=booking_display)
291
- send_btn.click(
292
- send_confirmation_hybrid, inputs=id_input, outputs=log_output
293
- ).then(render_booking_cards, outputs=booking_display)
294
 
295
  # --- 分頁 2:菜單動態管理 ---
296
  with gr.TabItem("🍽️ 菜單動態管理"):
297
- gr.Markdown("### ✨ 上架新餐點 (將立即同步至客人的點餐前台)")
298
  with gr.Row():
 
299
  with gr.Column(scale=1):
300
- m_name = gr.Textbox(label="餐點名稱 *", placeholder="ex. 炭烤肋眼牛排 12oz")
301
- m_desc = gr.Textbox(label="餐點描述 (選填)", placeholder="ex. 附烤大蒜與特製紅酒醬")
302
  m_price = gr.Number(label="價格 (TWD) *", precision=0)
303
  m_cat = gr.Dropdown(choices=["main", "snack", "drink", "other"], label="分類", value="main")
304
 
305
- # --- 時段與外帶選項 ---
 
 
 
 
 
306
  m_times = gr.CheckboxGroup(
307
  choices=["白天 (11:00-18:30)", "晚餐 (18:30-21:30)", "宵夜 (21:30後)"],
308
- value=["晚餐 (18:30-21:30)", "宵夜 (21:30後)"], # 預設勾選
309
- label="🕒 適用時段 (可多選) *"
310
  )
311
  with gr.Row():
312
  m_takeout = gr.Checkbox(label="🛍️ 開放外帶", value=True)
@@ -315,14 +303,15 @@ with gr.Blocks(title="Cié Cié Admin", css=custom_css, theme=gr.themes.Monochro
315
  m_add_btn = gr.Button("➕ ��增上架", variant="primary")
316
  m_add_log = gr.Textbox(label="新增結果", interactive=False)
317
 
 
318
  with gr.Column(scale=2):
319
  gr.Markdown("### 📋 目前線上菜單")
320
  menu_df = gr.Dataframe(interactive=False, wrap=True)
321
  m_refresh_btn = gr.Button("🔄 刷新菜單")
322
 
323
- gr.Markdown("#### ⚙️ 快速上下架操作")
324
  with gr.Row():
325
- m_toggle_id = gr.Textbox(label="輸入餐點 ID (UUID)", scale=2)
326
  m_set_active = gr.Button("🟢 重新上架", scale=1)
327
  m_set_inactive = gr.Button("🔴 暫時下架", scale=1)
328
  m_toggle_log = gr.Textbox(label="操作結果", interactive=False)
@@ -330,21 +319,24 @@ with gr.Blocks(title="Cié Cié Admin", css=custom_css, theme=gr.themes.Monochro
330
  # 事件綁定
331
  m_add_btn.click(
332
  add_menu_item,
333
- inputs=[m_name, m_desc, m_price, m_cat, m_times, m_prepay, m_takeout],
334
  outputs=[m_add_log, menu_df]
335
- )
336
- m_refresh_btn.click(get_menu_items, outputs=menu_df)
337
 
338
- m_set_active.click(toggle_menu_item, inputs=[m_toggle_id, gr.State(True)], outputs=[m_toggle_log, menu_df])
339
- m_set_inactive.click(toggle_menu_item, inputs=[m_toggle_id, gr.State(False)], outputs=[m_toggle_log, menu_df])
 
 
340
 
341
- # 登入事件 (登入成功後,同時載入訂位卡片與菜單)
342
  login_btn.click(
343
  check_login, inputs=[username_input, password_input], outputs=[login_row, admin_tabs, error_msg]
344
  ).then(
345
  render_booking_cards, outputs=booking_display
346
  ).then(
347
  get_menu_items, outputs=menu_df
 
 
348
  )
349
 
350
  if __name__ == "__main__":
 
20
  supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
21
 
22
  # ==========================================
23
+ # 模組 1:訂位與通知管理 (包含 No-Show)
24
  # ==========================================
25
 
26
  def get_bookings():
 
36
  if not booking_id: return "❌ 請輸入訂單 ID"
37
 
38
  try:
 
39
  res = supabase.table("bookings").select("*").eq("id", booking_id).execute()
40
  if not res.data: return f"❌ 找不到 ID: {booking_id}"
41
  booking = res.data[0]
42
 
43
  email = booking.get('email')
44
+ user_id = booking.get('user_id')
45
  current_status = booking.get('status', '')
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
  log_msg = f"🆔 {booking_id} 處理結果: "
 
 
50
  is_reminder = "確認" in current_status
51
 
52
  if is_reminder:
53
+ # --- 🔔 提醒模式 ---
 
54
  mail_subject = f"🔔 行前提醒: {booking['date']} - Cié Cié Taipei"
55
+ line_text = (f"🔔 行前提醒\n\n{booking['name']} 您好,期待今晚與您相見!\n\n"
56
+ f"📅 日期:{booking['date']}\n⏰ 時間:{booking['time']}\n👥 人數:{booking['pax']} 位\n\n"
57
+ f"座位已為準備好,若需變更請聯繫我們。")
58
+ mail_html = f"""<div style="background:#1a1a1a; color:#d4af37; padding:20px; border-radius:8px; font-family:sans-serif;">
 
 
 
 
 
 
59
  <h2 style="text-align:center; border-bottom:1px solid #444; padding-bottom:15px;">Cié Cié Taipei</h2>
60
  <p style="color:#eee; font-size:16px;"><strong>{booking['name']}</strong> 您好,行前提醒:</p>
61
+ <ul style="color:#ddd; line-height:1.8;"><li>📅 日期:{booking['date']}</li><li>⏰ 時間:{booking['time']}</li><li>👥 人數:{booking['pax']} 位</li></ul>
62
+ <div style="text-align:center; margin-top:30px;"><span style="color:#888;">無需再次確認。</span><br><br><a href="{cancel_link}" style="color:#aaa; font-size:12px;">若無法前來,請點此取消</a></div></div>"""
 
 
 
 
 
 
 
 
 
 
 
63
  else:
64
+ # --- 🚀 確認模式 ---
 
65
  mail_subject = f"訂位確認: {booking['date']} - Cié Cié Taipei"
66
+ line_text = (f"✅ 訂位確認\n\n{booking['name']} 您好,已收到您的預約。\n\n"
67
+ f"📅 日期:{booking['date']}\n⏰ 時間:{booking['time']}\n👥 人數:{booking['pax']} 位\n\n"
68
+ f"👉 請務必點擊下方連結「確認出席」謝謝!\n{confirm_link}")
69
+ mail_html = f"""<div style="background:#1a1a1a; color:#d4af37; padding:20px; border-radius:8px; font-family:sans-serif;">
 
 
 
 
 
 
 
70
  <h2 style="text-align:center; border-bottom:1px solid #444; padding-bottom:15px;">Cié Cié Taipei</h2>
71
  <p style="color:#eee; font-size:16px;"><strong>{booking['name']}</strong> 您好,請確認您的訂位:</p>
72
+ <ul style="color:#ddd; line-height:1.8;"><li>📅 日期:{booking['date']}</li><li>⏰ 時間:{booking['time']}</li><li>👥 人數:{booking['pax']} 位</li></ul>
73
+ <div style="text-align:center; margin-top:30px;"><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>
74
+ <a href="{cancel_link}" style="display:inline-block; border:1px solid #555; color:#aaa; padding:11px 29px; text-decoration:none; border-radius:50px;">🚫 取消</a></div></div>"""
 
 
 
 
 
 
 
 
 
 
75
 
76
+ # 執行發送
77
  if email and "@" in email and GAS_MAIL_URL:
78
  try:
79
  requests.post(GAS_MAIL_URL, json={"to": email, "subject": mail_subject, "htmlBody": mail_html, "name": "Cié Cié Taipei"})
80
  log_msg += f"✅ Mail送出 "
81
+ except: log_msg += f"❌ Mail失敗 "
82
+ else: log_msg += "⚠️ 無Mail "
 
83
 
84
  if user_id and len(str(user_id)) > 5 and LINE_ACCESS_TOKEN:
85
  try:
86
+ r = requests.post("https://api.line.me/v2/bot/message/push", headers={"Authorization": f"Bearer {LINE_ACCESS_TOKEN}"}, json={"to": user_id, "messages": [{"type": "text", "text": line_text}]})
 
 
87
  if r.status_code == 200: log_msg += f"✅ LINE送出 "
88
  else: log_msg += f"❌ LINE失敗 "
89
+ except: log_msg += f"❌ LINE錯誤 "
90
+ else: log_msg += "⚠️ 無LINE ID "
 
91
 
 
92
  if not is_reminder:
93
  supabase.table("bookings").update({"status": "已發確認信"}).eq("id", booking_id).execute()
 
94
  return log_msg
 
95
  except Exception as e: return f"嚴重錯誤: {str(e)}"
96
 
97
+ def mark_no_show(booking_id):
98
+ """將訂位標記為 No-Show 黑名單"""
99
+ if not booking_id: return "❌ 請輸入訂單 ID"
100
+ try:
101
+ supabase.table("bookings").update({"status": "No-Show"}).eq("id", booking_id).execute()
102
+ return f"🚫 訂單 {booking_id} 已成功標記為 No-Show!(未來該號碼或LINE將被收取訂金)"
103
+ except Exception as e:
104
+ return f"❌ 錯誤: {str(e)}"
105
+
106
  def render_booking_cards():
107
  df = get_bookings()
108
  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>"
 
118
  if '確認' in status: status_bg = "#2ecc71"; status_tx = "#000"; border_color = "#2ecc71"
119
  elif '取消' in status: status_bg = "#e74c3c"; status_tx = "#fff"; border_color = "#e74c3c"
120
  elif '已發' in status: status_bg = "#f1c40f"; status_tx = "#000"; border_color = "#f1c40f"
121
+ elif 'No-Show' in status: status_bg = "#000"; status_tx = "#e74c3c"; border_color = "#e74c3c"
122
 
123
  line_badge = "<span style='background:#00B900; color:white; padding:2px 6px; border-radius:4px; font-size:0.7em; margin-left:8px;'>LINE綁定</span>" if has_line else ""
124
 
 
130
  <span style="font-size:2.5em; color:#fff; font-weight:900; line-height:1; font-family:monospace;">{row['id']}</span>
131
  </div>
132
  <div style="text-align:right;">
133
+ <div style="background:{status_bg}; color:{status_tx}; padding:6px 12px; border-radius:4px; font-weight:bold; font-size:0.9em; display:inline-block; border: 1px solid {border_color};">{status}</div>
134
  <div style="margin-top:5px;">{line_badge}</div>
135
  </div>
136
  </div>
 
155
 
156
 
157
  # ==========================================
158
+ # 模組 2:菜單動態管理 (改為手動輸入圖片網址)
159
  # ==========================================
160
 
161
  def get_menu_items():
162
  try:
163
  res = supabase.table("menu_items").select("*").order("category").order("created_at").execute()
164
  if not res.data:
165
+ return pd.DataFrame(columns=['餐點名稱', '價格', '分類', '供應時段', '可外帶', '預付規則', '狀態', '照片連結'])
166
 
167
  df = pd.DataFrame(res.data)
168
 
 
169
  df['available_times'] = df['available_times'].apply(lambda x: "、".join(x) if isinstance(x, list) else "全時段")
170
  df['allow_takeout'] = df['allow_takeout'].apply(lambda x: "✅" if x else "❌")
171
  df['require_prepay'] = df['require_prepay'].apply(lambda x: "🔥 需預付" if x else "一般")
172
  df['is_active'] = df['is_active'].apply(lambda x: "🟢 販售中" if x else "🔴 已下架")
173
+ # 簡單標示有沒有填網址
174
+ df['has_image'] = df.get('image_url', pd.Series()).apply(lambda x: "🔗 有" if pd.notnull(x) and str(x).strip() else "無")
175
 
176
+ display_df = df[['name', 'price', 'category', 'available_times', 'allow_takeout', 'require_prepay', 'is_active', 'has_image']]
177
+ display_df.columns = ['餐點名稱', '價格', '分類', '供應時段', '可外帶', '預付規則', '狀態', '照片連結']
 
178
  return display_df
179
  except Exception as e:
180
  print("Fetch menu error:", e)
181
+ return pd.DataFrame(columns=['餐點名稱', '價格', '分類', '供應時段', '可外帶', '預付規則', '狀態', '照片連結'])
182
+
183
+ def update_menu_dropdown():
184
+ """更新上下架操作用的下拉選單,隱藏難記的 UUID"""
185
+ try:
186
+ res = supabase.table("menu_items").select("id, name, is_active").execute()
187
+ if not res.data: return gr.update(choices=[])
188
+ # 格式:[🟢販售中] 炭烤肋眼牛排 | uuid
189
+ choices = [f"[{'🟢販售中' if item['is_active'] else '🔴已下架'}] {item['name']} | {item['id']}" for item in res.data]
190
+ return gr.update(choices=choices)
191
+ except:
192
+ return gr.update(choices=[])
193
 
194
+ def add_menu_item(name, desc, price, category, available_times, require_prepay, allow_takeout, image_url_input):
195
  if not name or not price: return "⚠️ 名稱與價格為必填", get_menu_items()
196
  if not available_times: return "⚠️ 請至少選擇一個供應時段", get_menu_items()
197
 
198
+ # 處理圖片網址,確保空字串轉為 None 存入資料庫
199
+ final_image_url = image_url_input.strip() if image_url_input and str(image_url_input).strip() else None
200
+
201
  try:
202
  data = {
203
  "name": name,
204
  "description": desc,
205
  "price": int(price),
206
  "category": category,
207
+ "available_times": available_times,
208
+ "allow_takeout": allow_takeout,
209
  "require_prepay": require_prepay,
210
+ "is_active": True,
211
+ "image_url": final_image_url # 寫入直接填���的網址
212
  }
213
  supabase.table("menu_items").insert(data).execute()
214
+ log_msg = f"✅ 成功新增餐點:{name}"
215
+ if final_image_url:
216
+ log_msg += "\n🔗 圖片網址已連結"
217
+ return log_msg, get_menu_items()
218
  except Exception as e:
219
  return f"❌ 錯誤: {str(e)}", get_menu_items()
220
 
221
+ def toggle_menu_item(selected_string, is_active):
222
+ if not selected_string: return "⚠️ 請選擇餐點", get_menu_items()
223
  try:
224
+ # 從選單字串中提取出藏在最後面的 UUID
225
+ item_id = selected_string.split(" | ")[-1].strip()
226
+ supabase.table("menu_items").update({"is_active": is_active}).eq("id", item_id).execute()
227
  status_text = "上架" if is_active else "下架"
228
+ return f"✅ 餐點狀態已更新{status_text}", get_menu_items()
229
  except Exception as e:
230
  return f"❌ 錯誤: {str(e)}", get_menu_items()
231
 
 
260
  # --- 分頁 1:訂位與通知管理 ---
261
  with gr.TabItem("🍷 訂位與通知管理"):
262
  with gr.Column(elem_id="op-panel"):
263
+ gr.Markdown("### 🚀 發送通知防鴿子標記")
264
  with gr.Row():
265
+ id_input = gr.Number(label="輸入訂單 ID", precision=0, scale=2)
266
+ send_btn = gr.Button("🚀 發送提醒/確認", variant="primary", scale=1)
267
+ noshow_btn = gr.Button("🚫 標記 No-Show", variant="stop", scale=1) # No-Show 按鈕
268
  refresh_btn = gr.Button("🔄 刷新列表", scale=1)
269
  log_output = gr.Textbox(label="執行結果日誌", lines=1)
270
 
271
  booking_display = gr.HTML(elem_id="booking_display")
272
 
273
+ # 按鈕事件
274
  refresh_btn.click(render_booking_cards, outputs=booking_display)
275
+ send_btn.click(send_confirmation_hybrid, inputs=id_input, outputs=log_output).then(render_booking_cards, outputs=booking_display)
276
+ noshow_btn.click(mark_no_show, inputs=id_input, outputs=log_output).then(render_booking_cards, outputs=booking_display)
 
277
 
278
  # --- 分頁 2:菜單動態管理 ---
279
  with gr.TabItem("🍽️ 菜單動態管理"):
280
+ gr.Markdown("### ✨ 上架新餐點")
281
  with gr.Row():
282
+ # 左側:新增餐點表單
283
  with gr.Column(scale=1):
284
+ m_name = gr.Textbox(label="餐點名稱 *")
285
+ m_desc = gr.Textbox(label="餐點描述 (選填)")
286
  m_price = gr.Number(label="價格 (TWD) *", precision=0)
287
  m_cat = gr.Dropdown(choices=["main", "snack", "drink", "other"], label="分類", value="main")
288
 
289
+ # --- 改良:手動填寫圖片網址 ---
290
+ m_image_url = gr.Textbox(
291
+ label="餐點照片網址 (選填)",
292
+ placeholder="例如: https://ciecietaipei.github.io/assets/steak.jpg"
293
+ )
294
+
295
  m_times = gr.CheckboxGroup(
296
  choices=["白天 (11:00-18:30)", "晚餐 (18:30-21:30)", "宵夜 (21:30後)"],
297
+ value=["晚餐 (18:30-21:30)", "宵夜 (21:30後)"], label="🕒 適用時段 *"
 
298
  )
299
  with gr.Row():
300
  m_takeout = gr.Checkbox(label="🛍️ 開放外帶", value=True)
 
303
  m_add_btn = gr.Button("➕ ��增上架", variant="primary")
304
  m_add_log = gr.Textbox(label="新增結果", interactive=False)
305
 
306
+ # 右側:清單與防呆上下架
307
  with gr.Column(scale=2):
308
  gr.Markdown("### 📋 目前線上菜單")
309
  menu_df = gr.Dataframe(interactive=False, wrap=True)
310
  m_refresh_btn = gr.Button("🔄 刷新菜單")
311
 
312
+ gr.Markdown("#### ⚙️ 快速上下架操作 (免記ID)")
313
  with gr.Row():
314
+ m_toggle_dropdown = gr.Dropdown(label="選擇要操作的餐點", choices=[], scale=2)
315
  m_set_active = gr.Button("🟢 重新上架", scale=1)
316
  m_set_inactive = gr.Button("🔴 暫時下架", scale=1)
317
  m_toggle_log = gr.Textbox(label="操作結果", interactive=False)
 
319
  # 事件綁定
320
  m_add_btn.click(
321
  add_menu_item,
322
+ inputs=[m_name, m_desc, m_price, m_cat, m_times, m_prepay, m_takeout, m_image_url],
323
  outputs=[m_add_log, menu_df]
324
+ ).then(update_menu_dropdown, outputs=m_toggle_dropdown)
 
325
 
326
+ m_refresh_btn.click(get_menu_items, outputs=menu_df).then(update_menu_dropdown, outputs=m_toggle_dropdown)
327
+
328
+ m_set_active.click(toggle_menu_item, inputs=[m_toggle_dropdown, gr.State(True)], outputs=[m_toggle_log, menu_df]).then(update_menu_dropdown, outputs=m_toggle_dropdown)
329
+ m_set_inactive.click(toggle_menu_item, inputs=[m_toggle_dropdown, gr.State(False)], outputs=[m_toggle_log, menu_df]).then(update_menu_dropdown, outputs=m_toggle_dropdown)
330
 
331
+ # 登入事件
332
  login_btn.click(
333
  check_login, inputs=[username_input, password_input], outputs=[login_row, admin_tabs, error_msg]
334
  ).then(
335
  render_booking_cards, outputs=booking_display
336
  ).then(
337
  get_menu_items, outputs=menu_df
338
+ ).then(
339
+ update_menu_dropdown, outputs=m_toggle_dropdown
340
  )
341
 
342
  if __name__ == "__main__":