DeepLearning101 commited on
Commit
3db40dc
·
verified ·
1 Parent(s): 69435e5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +146 -83
app.py CHANGED
@@ -27,107 +27,141 @@ def get_bookings():
27
  return pd.DataFrame(res.data)
28
 
29
  def send_confirmation_hybrid(booking_id):
 
30
  try:
31
  # 1. 抓取資料
32
  res = supabase.table("bookings").select("*").eq("id", booking_id).execute()
33
  if not res.data: return "❌ 找不到訂單"
34
  booking = res.data[0]
35
  email, user_id = booking.get('email'), booking.get('user_id')
36
- log_msg = ""
37
 
38
  confirm_link = f"{PUBLIC_SPACE_URL}/?id={booking_id}&action=confirm"
39
  cancel_link = f"{PUBLIC_SPACE_URL}/?id={booking_id}&action=cancel"
40
 
41
- # 2. 發送 Email (偵錯版)
42
- if not GAS_MAIL_URL:
43
- log_msg += "⚠️ 未設GAS_URL "
44
- elif email and "@" in email:
45
  try:
46
- # 簡單的 HTML 信件
47
  html = f"""
48
- <div style="padding: 20px; background: #111; color: #d4af37; border-radius: 8px;">
49
- <h2>Cié Cié Taipei 訂位確認</h2>
50
- <p>{booking['name']} 您好,已為您保留:</p>
51
- <ul style="color: #eee;">
52
- <li>日期:{booking['date']}</li>
53
- <li>時間:{booking['time']}</li>
54
- <li>人數:{booking['pax']} 位</li>
55
- </ul>
56
- <a href="{confirm_link}" style="background:#d4af37; color:#000; padding:10px 20px; text-decoration:none; border-radius:5px;">確認出席</a>
57
- <a href="{cancel_link}" style="color:#ff5252; padding:10px 20px;">取消</a>
 
 
 
 
58
  </div>
59
  """
60
- r = requests.post(GAS_MAIL_URL, json={"to": email, "subject": "訂位確認", "htmlBody": html, "name": "Cié Cié Taipei"})
61
- log_msg += f"✅ Mail({r.status_code}) "
62
- except Exception as e:
63
- log_msg += f"❌ MailErr "
64
 
65
- # 3. 發送 LINE (維持原樣)
66
  if user_id and len(str(user_id)) > 10 and LINE_ACCESS_TOKEN:
67
  try:
68
- # 這裡省略詳細 Flex Message 代碼以節省篇幅,維持您原本的邏輯即可
69
- # 簡單發個文字測試
70
  requests.post("https://api.line.me/v2/bot/message/push",
71
  headers={"Authorization": f"Bearer {LINE_ACCESS_TOKEN}"},
72
  json={"to": user_id, "messages": [{"type": "text", "text": f"✅ 訂位確認\n{booking['date']} {booking['time']}\n請回覆確認,謝謝!"}]})
73
  log_msg += "✅ LINE "
74
- except:
75
- log_msg += "❌ LINEErr"
76
 
77
  # 4. 更新狀態
78
  supabase.table("bookings").update({"status": "已發確認信"}).eq("id", booking_id).execute()
79
  return log_msg
80
  except Exception as e: return f"Error: {e}"
81
 
82
- # 🔥🔥🔥 核心改變:將 Dataframe 轉為 HTML 卡片 🔥🔥🔥
83
  def render_booking_cards():
84
  df = get_bookings()
85
  if df.empty:
86
- return "<div style='text-align:center; padding:20px; color:#888;'>📅 目前沒有訂位資料</div>"
87
 
88
- cards_html = "<div style='display: flex; flex-direction: column; gap: 15px; padding-bottom: 50px;'>"
89
 
90
  for index, row in df.iterrows():
91
- # 定義狀態顏色
92
  status = row.get('status', '待處理')
93
- if '確認' in status: border_color = '#2ecc71' # 綠色
94
- elif '取消' in status: border_color = '#e74c3c' # 紅色
95
- elif '已發' in status: border_color = '#f1c40f' # 黃色
96
- else: border_color = '#95a5a6' # 灰色
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- # 卡片 HTML 結構
99
  card = f"""
100
- <div style="
101
- background: #262626;
102
- border-left: 5px solid {border_color};
103
- border-radius: 8px;
104
- padding: 15px;
105
- color: #eee;
106
- box-shadow: 0 2px 5px rgba(0,0,0,0.3);
107
- font-family: sans-serif;">
 
108
 
109
- <div style="display:flex; justify-content:space-between; margin-bottom:8px;">
110
- <span style="font-size:1.2em; font-weight:bold; color:#d4af37;">
111
- {row['time']} <span style="color:#fff; font-size:0.9em;">{row['name']}</span>
112
- </span>
113
- <span style="font-size:0.8em; padding:2px 6px; border-radius:4px; background:#333; color:{border_color};">
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  {status}
115
- </span>
116
  </div>
117
 
118
- <div style="display:flex; gap:15px; color:#aaa; font-size:0.9em; margin-bottom:8px;">
119
- <span>📅 {row['date']}</span>
120
- <span>👤 {row['pax']} 位</span>
121
- <span>🆔 {row['id']}</span>
122
- </div>
123
-
124
- <div style="background:#1a1a1a; padding:8px; border-radius:4px; font-size:0.9em; color:#ccc; margin-bottom:10px;">
125
- 📝 {row.get('remarks') or '無備註'}
126
  </div>
127
 
128
- <div style="display:flex; justify-content:space-between; align-items:center; font-size:0.9em;">
129
- <a href="tel:{row['tel']}" style="color:#3498db; text-decoration:none;">📞 {row['tel']}</a>
130
- <span style="color:#666; font-size:0.8em;">{row['email']}</span>
 
 
 
 
 
 
 
 
 
 
 
 
131
  </div>
132
  </div>
133
  """
@@ -149,59 +183,88 @@ def check_login(user, password):
149
  error_msg: "<span style='color: red'>❌ 帳號或密碼錯誤</span>"
150
  }
151
 
152
- # --- CSS (極簡化,因為現在用卡片了,不需要處理表格捲軸) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  custom_css = """
154
  body, .gradio-container { background-color: #0F0F0F; color: #fff; }
155
- #booking_display { max-height: 80vh; overflow-y: auto; } /* 讓卡片區可以垂直捲動 */
 
 
 
 
 
 
 
156
  """
157
 
158
  # --- 介面 ---
159
  with gr.Blocks(title="Admin", css=custom_css) as demo:
160
 
161
- # 1. 登入
162
  with gr.Group(visible=True) as login_row:
163
- gr.Markdown("# 🔒 請登入後台")
164
  with gr.Row():
165
- username_input = gr.Textbox(label="帳號", placeholder="Enter username")
166
- password_input = gr.Textbox(label="密碼", type="password", placeholder="Enter password")
167
- login_btn = gr.Button("登入", variant="primary")
168
  error_msg = gr.Markdown("")
169
 
170
- # 2. 後台
171
  with gr.Group(visible=False) as admin_row:
172
- gr.Markdown("# 🍷 訂位管理 (Card View)")
 
 
173
 
174
- with gr.Row():
175
- refresh_btn = gr.Button("🔄 重新整理列表")
176
-
177
- # 🔥 顯示卡片的地方 (用 HTML 渲染)
178
- booking_display = gr.HTML(label="訂位列表", elem_id="booking_display")
179
 
180
- gr.Markdown("### 🛠️ 操作區")
181
- with gr.Row():
182
- id_input = gr.Number(label="輸入訂單 ID", precision=0)
183
- action_btn = gr.Button("📧 發送確認信 (Hybrid)", variant="primary")
184
 
185
- log_output = gr.Textbox(label="執行結果")
186
 
187
  # 事件綁定
188
- # 1. 登入後自動載入列表
189
  login_btn.click(check_login, inputs=[username_input, password_input], outputs=[login_row, admin_row, error_msg]).then(
190
  render_booking_cards, outputs=booking_display
191
  )
192
 
193
- # 2. 重新整理
194
  refresh_btn.click(render_booking_cards, outputs=booking_display)
195
 
196
- # 3. 發送確認信 -> 發送後自動更新列表 (變色)
197
- action_btn.click(
198
  send_confirmation_hybrid,
199
- inputs=id_input,
200
  outputs=log_output
201
  ).then(
202
  render_booking_cards,
203
  outputs=booking_display
204
  )
205
 
 
 
 
206
  if __name__ == "__main__":
207
  demo.launch()
 
27
  return pd.DataFrame(res.data)
28
 
29
  def send_confirmation_hybrid(booking_id):
30
+ if not booking_id: return "❌ 無效 ID"
31
  try:
32
  # 1. 抓取資料
33
  res = supabase.table("bookings").select("*").eq("id", booking_id).execute()
34
  if not res.data: return "❌ 找不到訂單"
35
  booking = res.data[0]
36
  email, user_id = booking.get('email'), booking.get('user_id')
37
+ log_msg = f"🆔 {booking_id}: "
38
 
39
  confirm_link = f"{PUBLIC_SPACE_URL}/?id={booking_id}&action=confirm"
40
  cancel_link = f"{PUBLIC_SPACE_URL}/?id={booking_id}&action=cancel"
41
 
42
+ # 2. 發送 Email
43
+ if email and "@" in email and GAS_MAIL_URL:
 
 
44
  try:
 
45
  html = f"""
46
+ <div style="background:#111; color:#d4af37; padding:20px; border-radius:8px; font-family:sans-serif;">
47
+ <h2 style="border-bottom:1px solid #333; padding-bottom:10px;">Cié Cié Taipei 訂位確認</h2>
48
+ <p style="color:#eee; font-size:16px;">{booking['name']} 您好,已為您保留座位:</p>
49
+ <div style="background:#222; padding:15px; border-radius:6px; margin:15px 0;">
50
+ <ul style="color:#ddd; padding-left:20px; line-height:1.8;">
51
+ <li>📅 日期:<strong style="color:#fff;">{booking['date']}</strong></li>
52
+ <li>⏰ 時間:<strong style="color:#fff;">{booking['time']}</strong></li>
53
+ <li>👥 人數:<strong style="color:#fff;">{booking['pax']} 位</strong></li>
54
+ </ul>
55
+ </div>
56
+ <div style="margin-top:20px;">
57
+ <a href="{confirm_link}" style="background:#d4af37; color:#000; padding:10px 20px; text-decoration:none; border-radius:4px; font-weight:bold; margin-right:10px;">✅ 確認出席</a>
58
+ <a href="{cancel_link}" style="border:1px solid #ff5252; color:#ff5252; padding:9px 19px; text-decoration:none; border-radius:4px;">🚫 取消</a>
59
+ </div>
60
  </div>
61
  """
62
+ requests.post(GAS_MAIL_URL, json={"to": email, "subject": "訂位確認 - Cié Cié Taipei", "htmlBody": html, "name": "Cié Cié Taipei"})
63
+ log_msg += "✅ Mail "
64
+ except: log_msg += "❌ MailErr "
 
65
 
66
+ # 3. 發送 LINE
67
  if user_id and len(str(user_id)) > 10 and LINE_ACCESS_TOKEN:
68
  try:
 
 
69
  requests.post("https://api.line.me/v2/bot/message/push",
70
  headers={"Authorization": f"Bearer {LINE_ACCESS_TOKEN}"},
71
  json={"to": user_id, "messages": [{"type": "text", "text": f"✅ 訂位確認\n{booking['date']} {booking['time']}\n請回覆確認,謝謝!"}]})
72
  log_msg += "✅ LINE "
73
+ except: log_msg += "❌ LINEErr"
 
74
 
75
  # 4. 更新狀態
76
  supabase.table("bookings").update({"status": "已發確認信"}).eq("id", booking_id).execute()
77
  return log_msg
78
  except Exception as e: return f"Error: {e}"
79
 
80
+ # 🔥🔥🔥 2.0 版卡片渲染:設計感 + 操作按鈕 🔥🔥🔥
81
  def render_booking_cards():
82
  df = get_bookings()
83
  if df.empty:
84
+ return "<div style='text-align:center; padding:40px; color:#666; font-size:1.2em;'>📭 目前沒有訂位資料</div>"
85
 
86
+ cards_html = "<div style='display: flex; flex-direction: column; gap: 16px; padding-bottom: 60px;'>"
87
 
88
  for index, row in df.iterrows():
89
+ # 狀態樣式邏輯
90
  status = row.get('status', '待處理')
91
+ status_bg = "#333"
92
+ status_color = "#ccc"
93
+
94
+ if '確認' in status:
95
+ status_bg = "rgba(46, 204, 113, 0.2)"; status_color = "#2ecc71" # 綠
96
+ elif '取消' in status:
97
+ status_bg = "rgba(231, 76, 60, 0.2)"; status_color = "#e74c3c" # 紅
98
+ elif '已發' in status:
99
+ status_bg = "rgba(241, 196, 15, 0.2)"; status_color = "#f1c40f" # 黃
100
+
101
+ # 動作按鈕:如果是「已取消」或「已確認」,按鈕就反灰,避免誤按
102
+ is_disabled = '取消' in status or '確認' in status
103
+ btn_style = "opacity: 0.5; cursor: not-allowed;" if is_disabled else "cursor: pointer; hover:brightness(1.2);"
104
+ btn_onclick = "" if is_disabled else f"cardAction({row['id']})"
105
+ btn_text = "已處理" if is_disabled else "🚀 發送確認"
106
 
 
107
  card = f"""
108
+ <div class="booking-card" style="
109
+ background: linear-gradient(145deg, #2a2a2a, #222);
110
+ border: 1px solid #333;
111
+ border-radius: 12px;
112
+ padding: 16px;
113
+ position: relative;
114
+ box-shadow: 0 4px 15px rgba(0,0,0,0.3);
115
+ transition: transform 0.2s;
116
+ font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">
117
 
118
+ <div style="display:flex; justify-content:space-between; align-items:start; margin-bottom:12px;">
119
+ <div>
120
+ <div style="color:#d4af37; font-size:1.4em; font-weight:800; letter-spacing:0.5px; line-height:1.2;">
121
+ {row['time']} <span style="font-size:0.6em; color:#888; font-weight:400;">{row['date'][5:]}</span>
122
+ </div>
123
+ <div style="color:#fff; font-size:1.1em; font-weight:600; margin-top:4px;">
124
+ {row['name']} <span style="font-size:0.8em; color:#aaa; font-weight:400;">({row['pax']}位)</span>
125
+ </div>
126
+ </div>
127
+
128
+ <div style="
129
+ background: {status_bg};
130
+ color: {status_color};
131
+ padding: 4px 10px;
132
+ border-radius: 20px;
133
+ font-size: 0.75em;
134
+ font-weight: 600;
135
+ border: 1px solid {status_color}44;">
136
  {status}
137
+ </div>
138
  </div>
139
 
140
+ <div style="background:rgba(0,0,0,0.2); border-radius:8px; padding:10px; font-size:0.85em; color:#aaa; line-height:1.6;">
141
+ <div style="display:flex; gap:10px; margin-bottom:4px;">
142
+ <span>📞 <a href="tel:{row['tel']}" style="color:#aaa; text-decoration:none; border-bottom:1px dotted #666;">{row['tel']}</a></span>
143
+ </div>
144
+ <div style="color:#666; font-size:0.8em; margin-bottom:4px;">✉️ {row['email'] or '無 Email'}</div>
145
+ <div style="color:#ccc; border-top:1px solid #333; margin-top:6px; padding-top:6px;">
146
+ 📝 {row.get('remarks') or '<span style="color:#555">無備註</span>'}
147
+ </div>
148
  </div>
149
 
150
+ <div style="margin-top:15px; display:flex; justify-content:space-between; align-items:center;">
151
+ <span style="font-size:0.7em; color:#444; font-family:monospace;">ID: {row['id']}</span>
152
+
153
+ <button onclick="{btn_onclick}" style="
154
+ background: #d4af37;
155
+ color: #000;
156
+ border: none;
157
+ padding: 8px 16px;
158
+ border-radius: 6px;
159
+ font-weight: bold;
160
+ font-size: 0.85em;
161
+ transition: all 0.2s;
162
+ {btn_style}">
163
+ {btn_text}
164
+ </button>
165
  </div>
166
  </div>
167
  """
 
183
  error_msg: "<span style='color: red'>❌ 帳號或密碼錯誤</span>"
184
  }
185
 
186
+ # --- 🟢 [JS] 讓卡片按鈕能觸發 Python 事件 ---
187
+ js_logic = """
188
+ function() {
189
+ // 定義全局函數,讓 HTML 中的 onclick 可以呼叫
190
+ window.cardAction = function(id) {
191
+ // 1. 找到隱藏的 ID 輸入框
192
+ const idInput = document.querySelector('#hidden_id_input input');
193
+ if (idInput) {
194
+ // 設定值並觸發 input 事件 (讓 Gradio 知道值變了)
195
+ idInput.value = id;
196
+ idInput.dispatchEvent(new Event('input', { bubbles: true }));
197
+ }
198
+
199
+ // 2. 找到隱藏的發送按鈕並點擊
200
+ setTimeout(() => {
201
+ const sendBtn = document.querySelector('#hidden_send_btn');
202
+ if (sendBtn) sendBtn.click();
203
+ }, 100);
204
+ }
205
+ }
206
+ """
207
+
208
+ # --- CSS 優化 ---
209
  custom_css = """
210
  body, .gradio-container { background-color: #0F0F0F; color: #fff; }
211
+ /* 讓卡片容器可以滾動但隱藏捲軸 */
212
+ #booking_display {
213
+ max-height: 85vh;
214
+ overflow-y: auto;
215
+ padding-right: 5px;
216
+ }
217
+ #booking_display::-webkit-scrollbar { width: 5px; }
218
+ #booking_display::-webkit-scrollbar-thumb { background: #333; border-radius: 5px; }
219
  """
220
 
221
  # --- 介面 ---
222
  with gr.Blocks(title="Admin", css=custom_css) as demo:
223
 
224
+ # 1. 登入層
225
  with gr.Group(visible=True) as login_row:
226
+ gr.Markdown("# 🔒 Login")
227
  with gr.Row():
228
+ username_input = gr.Textbox(label="User")
229
+ password_input = gr.Textbox(label="Pass", type="password")
230
+ login_btn = gr.Button("Enter", variant="primary")
231
  error_msg = gr.Markdown("")
232
 
233
+ # 2. 後台層
234
  with gr.Group(visible=False) as admin_row:
235
+ with gr.Row(variant="panel"):
236
+ gr.Markdown("## 🍷 Dashboard")
237
+ refresh_btn = gr.Button("🔄", size="sm") # 簡化按鈕
238
 
239
+ # 🔥 卡片顯示區
240
+ booking_display = gr.HTML(elem_id="booking_display")
 
 
 
241
 
242
+ # 🟢 隱藏的操作區 ( JS 自動控制)
243
+ with gr.Column(visible=False):
244
+ hidden_id_input = gr.Number(elem_id="hidden_id_input", precision=0)
245
+ hidden_send_btn = gr.Button("Send", elem_id="hidden_send_btn")
246
 
247
+ log_output = gr.Textbox(label="最近操作記錄", lines=1)
248
 
249
  # 事件綁定
 
250
  login_btn.click(check_login, inputs=[username_input, password_input], outputs=[login_row, admin_row, error_msg]).then(
251
  render_booking_cards, outputs=booking_display
252
  )
253
 
 
254
  refresh_btn.click(render_booking_cards, outputs=booking_display)
255
 
256
+ # 🔥 隱藏按鈕被 JS 點擊後 -> ���行發送 -> 更新 Log -> 重新渲染卡片
257
+ hidden_send_btn.click(
258
  send_confirmation_hybrid,
259
+ inputs=hidden_id_input,
260
  outputs=log_output
261
  ).then(
262
  render_booking_cards,
263
  outputs=booking_display
264
  )
265
 
266
+ # 載入 JS
267
+ demo.load(None, js=js_logic)
268
+
269
  if __name__ == "__main__":
270
  demo.launch()