DeepLearning101 commited on
Commit
e80ff0c
·
verified ·
1 Parent(s): 927501e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +182 -131
app.py CHANGED
@@ -5,69 +5,67 @@ import requests
5
  from supabase import create_client, Client
6
  from datetime import datetime, timedelta, timezone
7
 
8
- # 設定台北時區
9
  TAIPEI_TZ = timezone(timedelta(hours=8))
10
 
11
- # --- 設定 ---
12
  SUPABASE_URL = os.getenv("SUPABASE_URL")
13
  SUPABASE_KEY = os.getenv("SUPABASE_KEY")
14
  GAS_MAIL_URL = os.getenv("GAS_MAIL_URL")
15
  LINE_ACCESS_TOKEN = os.getenv("LINE_ACCESS_TOKEN")
16
- PUBLIC_SPACE_URL = "https://deeplearning101-ciecietaipei.hf.space"
17
 
18
- # 取得帳密
19
  REAL_ADMIN_USER = os.getenv("ADMIN_USER") or "Deep Learning 101"
20
  REAL_ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD") or "2016-11-11"
21
 
22
  supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
23
 
 
 
 
 
24
  def get_bookings():
25
  try:
26
  res = supabase.table("bookings").select("*").order("created_at", desc=True).execute()
27
  if not res.data: return pd.DataFrame()
28
  return pd.DataFrame(res.data)
29
- except:
 
30
  return pd.DataFrame()
31
 
32
- # 🔥🔥🔥 核心後端:智慧判斷發送邏輯 (修正 LINE 連結) 🔥🔥🔥
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
 
50
- log_msg = f"🆔 {booking_id}: "
51
 
52
- # 2. 判斷模式
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 修正:加入取消連結,方便客人臨時取消
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"座位已為您準備好。\n"
68
- f"若無法前來,請點擊下方連結取消:\n{cancel_link}"
69
  )
70
-
71
  mail_html = f"""
72
  <div style="background:#1a1a1a; color:#d4af37; padding:20px; border-radius:8px; font-family:sans-serif;">
73
  <h2 style="text-align:center; border-bottom:1px solid #444; padding-bottom:15px;">Cié Cié Taipei</h2>
@@ -81,27 +79,23 @@ def send_confirmation_hybrid(booking_id):
81
  </div>
82
  <div style="text-align:center; margin-top:30px;">
83
  <span style="color:#888;">無需再次確認。</span><br><br>
84
- <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>
85
  </div>
86
  </div>
87
  """
88
  else:
89
  # --- 🚀 確認模式 (Confirmation) ---
90
- action_label = "確認"
91
  mail_subject = f"訂位確認: {booking['date']} - Cié Cié Taipei"
92
-
93
- # 🔥 LINE 修正:加入 確認 與 取消 的連結
94
  line_text = (
95
  f"✅ 訂位確認\n\n"
96
  f"{booking['name']} 您好,已收到您的預約。\n\n"
97
  f"📅 日期:{booking['date']}\n"
98
  f"⏰ 時間:{booking['time']}\n"
99
  f"👥 人數:{booking['pax']} 位\n\n"
100
- f"請點擊下方連結確認出席\n"
101
- f"👉 確認:{confirm_link}\n\n"
102
- f"🚫 取消:{cancel_link}"
103
  )
104
-
105
  mail_html = f"""
106
  <div style="background:#1a1a1a; color:#d4af37; padding:20px; border-radius:8px; font-family:sans-serif;">
107
  <h2 style="text-align:center; border-bottom:1px solid #444; padding-bottom:15px;">Cié Cié Taipei</h2>
@@ -120,31 +114,27 @@ def send_confirmation_hybrid(booking_id):
120
  </div>
121
  """
122
 
123
- # 3. 執行發送
124
- # Email
125
  if email and "@" in email and GAS_MAIL_URL:
126
  try:
127
  requests.post(GAS_MAIL_URL, json={"to": email, "subject": mail_subject, "htmlBody": mail_html, "name": "Cié Cié Taipei"})
128
- log_msg += f"✅ Mail({action_label}) "
129
- except Exception as e: log_msg += f"❌ MailErr({str(e)}) "
130
  else:
131
  log_msg += "⚠️ 無Mail "
132
 
133
- # LINE
134
  if user_id and len(str(user_id)) > 5 and LINE_ACCESS_TOKEN:
135
  try:
136
  r = requests.post("https://api.line.me/v2/bot/message/push",
137
  headers={"Authorization": f"Bearer {LINE_ACCESS_TOKEN}"},
138
  json={"to": user_id, "messages": [{"type": "text", "text": line_text}]})
139
- if r.status_code == 200:
140
- log_msg += f" LINE({action_label}) "
141
- else:
142
- log_msg += f"❌ LINE失敗({r.status_code}: {r.text}) "
143
- except Exception as e: log_msg += f"❌ LINEErr({str(e)}) "
144
  else:
145
  log_msg += "⚠️ 無LINE ID "
146
 
147
- # 4. 更新狀態 (僅在非提醒模式下更新)
148
  if not is_reminder:
149
  supabase.table("bookings").update({"status": "已發確認信"}).eq("id", booking_id).execute()
150
 
@@ -152,7 +142,6 @@ def send_confirmation_hybrid(booking_id):
152
 
153
  except Exception as e: return f"嚴重錯誤: {str(e)}"
154
 
155
- # 🔥🔥🔥 卡片渲染 (純顯示,無 JS 互動) 🔥🔥🔥
156
  def render_booking_cards():
157
  df = get_bookings()
158
  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>"
@@ -162,61 +151,38 @@ def render_booking_cards():
162
 
163
  for index, row in df.iterrows():
164
  status = row.get('status', '待處理')
 
165
 
166
- # 顏色邏輯
167
  status_bg = "#ccc"; status_tx = "#000"; border_color = "#444"
168
- if '確認' in status:
169
- status_bg = "#2ecc71"; status_tx = "#000"; border_color = "#2ecc71"
170
- elif '取消' in status:
171
- status_bg = "#e74c3c"; status_tx = "#fff"; border_color = "#e74c3c"
172
- elif '已發' in status:
173
- status_bg = "#f1c40f"; status_tx = "#000"; border_color = "#f1c40f"
174
 
175
  card = f"""
176
- <div class="booking-card" style="
177
- background: #1a1a1a;
178
- border-left: 6px solid {border_color};
179
- border-radius: 12px;
180
- padding: 20px;
181
- box-shadow: 0 4px 15px rgba(0,0,0,0.5);
182
- font-family: '微軟正黑體', sans-serif;
183
- position: relative;">
184
-
185
  <div style="display:flex; justify-content:space-between; align-items:start; margin-bottom:15px;">
186
  <div>
187
  <span style="font-size:0.9em; color:#666; font-weight:bold; display:block;">訂單 ID</span>
188
  <span style="font-size:2.5em; color:#fff; font-weight:900; line-height:1; font-family:monospace;">{row['id']}</span>
189
  </div>
190
- <div style="background:{status_bg}; color:{status_tx}; padding:6px 12px; border-radius:4px; font-weight:bold; font-size:0.9em;">
191
- {status}
 
192
  </div>
193
  </div>
194
-
195
  <div style="display:grid; grid-template-columns: 1fr 1fr; gap:10px; margin-bottom:15px; border-top:1px solid #333; padding-top:15px;">
196
- <div>
197
- <span style="color:#888; font-size:0.85em;">📅 日期 Date</span><br>
198
- <span style="color:#d4af37; font-size:1.2em; font-weight:bold;">{row['date']}</span>
199
- </div>
200
- <div>
201
- <span style="color:#888; font-size:0.85em;">⏰ 時間 Time</span><br>
202
- <span style="color:#d4af37; font-size:1.5em; font-weight:900;">{row['time']}</span>
203
- </div>
204
  </div>
205
-
206
  <div style="background:#222; padding:15px; border-radius:8px; margin-bottom:15px;">
207
- <div style="margin-bottom:8px;">
208
- <span style="color:#fff; font-size:1.4em; font-weight:bold;">{row['name']}</span>
209
- <span style="color:#aaa;">({row['pax']}位)</span>
210
- </div>
211
- <div style="font-size:1.1em; margin-bottom:5px;">
212
- 📞 <a href="tel:{row['tel']}" style="color:#69c0ff; text-decoration:none;">{row['tel']}</a>
213
- </div>
214
  <div style="font-size:0.9em; color:#888;">✉️ {row['email'] or '-'}</div>
215
  </div>
216
-
217
  <div style="background:#f1c40f11; padding:12px; border-radius:6px; border:1px solid #f1c40f33;">
218
- <span style="color:#aaa; font-size:0.8em;">📝 備註:</span>
219
- <span style="color:#f1c40f;">{row.get('remarks') or '無'}</span>
220
  </div>
221
  </div>
222
  """
@@ -225,76 +191,161 @@ def render_booking_cards():
225
  cards_html += "</div>"
226
  return cards_html
227
 
228
- # --- 登入邏輯 ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  def check_login(user, password):
230
  if user == REAL_ADMIN_USER and password == REAL_ADMIN_PASSWORD:
231
- return {
232
- login_row: gr.update(visible=False),
233
- admin_row: gr.update(visible=True),
234
- error_msg: ""
235
- }
236
- else: return {error_msg: "<span style='color: red'>❌ 帳號或密碼錯誤</span>"}
237
 
238
- # --- CSS ---
239
  custom_css = """
240
  body, .gradio-container { background-color: #0F0F0F; color: #fff; }
241
- #op-panel {
242
- position: sticky;
243
- top: 0;
244
- z-index: 100;
245
- background: #1a1a1a;
246
- border-bottom: 2px solid #d4af37;
247
- padding: 15px;
248
- margin-bottom: 20px;
249
- box-shadow: 0 4px 10px rgba(0,0,0,0.5);
250
- }
251
- #booking_display {
252
- height: auto !important;
253
- overflow: visible !important;
254
- }
255
  """
256
 
257
- # --- 介面 ---
258
- with gr.Blocks(title="Admin") as demo:
259
 
260
  with gr.Group(visible=True) as login_row:
261
- gr.Markdown("# 🔒 Login")
262
  with gr.Row():
263
- username_input = gr.Textbox(label="User")
264
- password_input = gr.Textbox(label="Pass", type="password")
265
- login_btn = gr.Button("Enter", variant="primary")
266
  error_msg = gr.Markdown("")
267
 
268
- with gr.Group(visible=False) as admin_row:
269
- # 🔥 操作區 (固定在頂部)
270
- with gr.Column(elem_id="op-panel"):
271
- gr.Markdown("### 🍷 Cié Cié 訂位管理")
272
- with gr.Row():
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
- refresh_btn.click(render_booking_cards, outputs=booking_display)
287
-
288
- send_btn.click(
289
- send_confirmation_hybrid,
290
- inputs=id_input,
291
- outputs=log_output
292
- ).then(
293
- render_booking_cards,
294
- outputs=booking_display
295
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
- demo.launch(css=custom_css)
 
 
 
 
 
 
 
298
 
299
  if __name__ == "__main__":
300
  demo.launch()
 
5
  from supabase import create_client, Client
6
  from datetime import datetime, timedelta, timezone
7
 
8
+ # --- 設定時區與環境變數 ---
9
  TAIPEI_TZ = timezone(timedelta(hours=8))
10
 
 
11
  SUPABASE_URL = os.getenv("SUPABASE_URL")
12
  SUPABASE_KEY = os.getenv("SUPABASE_KEY")
13
  GAS_MAIL_URL = os.getenv("GAS_MAIL_URL")
14
  LINE_ACCESS_TOKEN = os.getenv("LINE_ACCESS_TOKEN")
15
+ PUBLIC_SPACE_URL = "https://deeplearning101-ciecietaipei.hf.space" # 您的官網網址
16
 
 
17
  REAL_ADMIN_USER = os.getenv("ADMIN_USER") or "Deep Learning 101"
18
  REAL_ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD") or "2016-11-11"
19
 
20
  supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
21
 
22
+ # ==========================================
23
+ # 模組 1:訂位與通知管理
24
+ # ==========================================
25
+
26
  def get_bookings():
27
  try:
28
  res = supabase.table("bookings").select("*").order("created_at", desc=True).execute()
29
  if not res.data: return pd.DataFrame()
30
  return pd.DataFrame(res.data)
31
+ except Exception as e:
32
+ print("Fetch bookings error:", e)
33
  return pd.DataFrame()
34
 
 
35
  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>
 
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>
 
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
 
 
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>"
 
151
 
152
  for index, row in df.iterrows():
153
  status = row.get('status', '待處理')
154
+ has_line = bool(row.get('user_id') and len(str(row.get('user_id'))) > 5)
155
 
 
156
  status_bg = "#ccc"; status_tx = "#000"; border_color = "#444"
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
 
163
  card = f"""
164
+ <div class="booking-card" style="background:#1a1a1a; border-left:6px solid {border_color}; border-radius:12px; padding:20px; box-shadow:0 4px 15px rgba(0,0,0,0.5); position:relative;">
 
 
 
 
 
 
 
 
165
  <div style="display:flex; justify-content:space-between; align-items:start; margin-bottom:15px;">
166
  <div>
167
  <span style="font-size:0.9em; color:#666; font-weight:bold; display:block;">訂單 ID</span>
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>
 
175
  <div style="display:grid; grid-template-columns: 1fr 1fr; gap:10px; margin-bottom:15px; border-top:1px solid #333; padding-top:15px;">
176
+ <div><span style="color:#888; font-size:0.85em;">📅 日期</span><br><span style="color:#d4af37; font-size:1.2em; font-weight:bold;">{row['date']}</span></div>
177
+ <div><span style="color:#888; font-size:0.85em;"> 時間</span><br><span style="color:#d4af37; font-size:1.5em; font-weight:900;">{row['time']}</span></div>
 
 
 
 
 
 
178
  </div>
 
179
  <div style="background:#222; padding:15px; border-radius:8px; margin-bottom:15px;">
180
+ <div style="margin-bottom:8px;"><span style="color:#fff; font-size:1.4em; font-weight:bold;">{row['name']}</span> <span style="color:#aaa;">({row['pax']}位)</span></div>
181
+ <div style="font-size:1.1em; margin-bottom:5px;">📞 {row['tel']}</div>
 
 
 
 
 
182
  <div style="font-size:0.9em; color:#888;">✉️ {row['email'] or '-'}</div>
183
  </div>
 
184
  <div style="background:#f1c40f11; padding:12px; border-radius:6px; border:1px solid #f1c40f33;">
185
+ <span style="color:#aaa; font-size:0.8em;">📝 備註:</span> <span style="color:#f1c40f;">{row.get('remarks') or '無'}</span>
 
186
  </div>
187
  </div>
188
  """
 
191
  cards_html += "</div>"
192
  return cards_html
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
+
250
+
251
+ # ==========================================
252
+ # Gradio 介面建構
253
+ # ==========================================
254
+
255
  def check_login(user, password):
256
  if user == REAL_ADMIN_USER and password == REAL_ADMIN_PASSWORD:
257
+ return { login_row: gr.update(visible=False), admin_tabs: gr.update(visible=True), error_msg: "" }
258
+ return { error_msg: "<span style='color: red'>❌ 帳號或密碼錯誤</span>" }
 
 
 
 
259
 
 
260
  custom_css = """
261
  body, .gradio-container { background-color: #0F0F0F; color: #fff; }
262
+ #op-panel { position: sticky; top: 0; z-index: 100; background: #1a1a1a; border-bottom: 2px solid #d4af37; padding: 15px; margin-bottom: 20px; box-shadow: 0 4px 10px rgba(0,0,0,0.5); }
263
+ #booking_display { height: auto !important; overflow: visible !important; }
 
 
 
 
 
 
 
 
 
 
 
 
264
  """
265
 
266
+ with gr.Blocks(title="Cié Cié Admin", css=custom_css, theme=gr.themes.Monochrome()) as demo:
 
267
 
268
  with gr.Group(visible=True) as login_row:
269
+ gr.Markdown("# 🔒 老闆/管理員登入")
270
  with gr.Row():
271
+ username_input = gr.Textbox(label="使用者名稱")
272
+ password_input = gr.Textbox(label="密碼", type="password")
273
+ login_btn = gr.Button("登入系統", variant="primary")
274
  error_msg = gr.Markdown("")
275
 
276
+ with gr.Tabs(visible=False) as admin_tabs:
277
+
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)
313
+ m_prepay = gr.Checkbox(label="🔥 需全額預付", value=False)
314
+
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)
329
+
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__":
351
  demo.launch()