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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +113 -81
app.py CHANGED
@@ -43,23 +43,32 @@ def send_confirmation_hybrid(booking_id):
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
 
@@ -68,7 +77,7 @@ def send_confirmation_hybrid(booking_id):
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
 
@@ -77,88 +86,115 @@ def send_confirmation_hybrid(booking_id):
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>
@@ -183,20 +219,15 @@ def check_login(user, password):
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();
@@ -208,20 +239,21 @@ function() {
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():
@@ -230,30 +262,30 @@ with gr.Blocks(title="Admin", css=custom_css) as demo:
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,
 
43
  if email and "@" in email and GAS_MAIL_URL:
44
  try:
45
  html = f"""
46
+ <div style="background:#1a1a1a; color:#d4af37; padding:20px; border-radius:8px; font-family:sans-serif; border:1px solid #333;">
47
+ <div style="text-align:center; border-bottom:1px solid #333; padding-bottom:15px; margin-bottom:15px;">
48
+ <h2 style="margin:0; font-size:24px; letter-spacing:2px;">Cié Cié Taipei</h2>
49
+ <p style="color:#888; margin:5px 0 0 0; font-size:14px;">Reservation Confirmation</p>
 
 
 
 
 
50
  </div>
51
+
52
+ <p style="color:#eee; font-size:16px;"><strong>{booking['name']}</strong> 您好,我們期待您的光臨!<br>已為您保留座位資訊如下:</p>
53
+
54
+ <div style="background:#2a2a2a; padding:20px; border-radius:8px; margin:20px 0; border-left:4px solid #d4af37;">
55
+ <table style="width:100%; border-collapse:collapse; color:#ddd;">
56
+ <tr><td style="padding:5px 0; color:#888;">日期 Date</td><td style="padding:5px 0; font-weight:bold; color:#fff;">{booking['date']}</td></tr>
57
+ <tr><td style="padding:5px 0; color:#888;">時間 Time</td><td style="padding:5px 0; font-weight:bold; color:#fff;">{booking['time']}</td></tr>
58
+ <tr><td style="padding:5px 0; color:#888;">人數 Guest</td><td style="padding:5px 0; font-weight:bold; color:#fff;">{booking['pax']} 位</td></tr>
59
+ <tr><td style="padding:5px 0; color:#888;">備註 Note</td><td style="padding:5px 0;">{booking.get('remarks') or '無'}</td></tr>
60
+ </table>
61
  </div>
62
+
63
+ <div style="text-align:center; margin-top:30px;">
64
+ <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; box-shadow:0 4px 10px rgba(212, 175, 55, 0.3);">✅ 確認出席</a>
65
+ <a href="{cancel_link}" style="display:inline-block; border:1px solid #555; color:#aaa; padding:11px 29px; text-decoration:none; border-radius:50px; font-size:14px;">🚫 取消訂位</a>
66
+ </div>
67
+
68
+ <p style="text-align:center; color:#666; font-size:12px; margin-top:30px;">若按鈕無法點擊,請直接回覆此信件。</p>
69
  </div>
70
  """
71
+ requests.post(GAS_MAIL_URL, json={"to": email, "subject": f"訂位確認: {booking['date']} - Cié Cié Taipei", "htmlBody": html, "name": "Cié Cié Taipei"})
72
  log_msg += "✅ Mail "
73
  except: log_msg += "❌ MailErr "
74
 
 
77
  try:
78
  requests.post("https://api.line.me/v2/bot/message/push",
79
  headers={"Authorization": f"Bearer {LINE_ACCESS_TOKEN}"},
80
+ json={"to": user_id, "messages": [{"type": "text", "text": f"✅ 訂位確認\n日期:{booking['date']}\n時間:{booking['time']}\n人數:{booking['pax']}位\n\n請查收 Email 確認信並點擊確認��鈕,謝謝!"}]})
81
  log_msg += "✅ LINE "
82
  except: log_msg += "❌ LINEErr"
83
 
 
86
  return log_msg
87
  except Exception as e: return f"Error: {e}"
88
 
89
+ # 🔥🔥🔥 3.0 版卡片:標籤化 + 大字體 + 重發功能 🔥🔥🔥
90
  def render_booking_cards():
91
  df = get_bookings()
92
  if df.empty:
93
+ return "<div style='text-align:center; padding:60px; color:#666; font-size:1.2em;'>📭 目前沒有訂位資料</div>"
94
 
95
+ cards_html = "<div style='display: flex; flex-direction: column; gap: 20px; padding-bottom: 80px;'>"
96
 
97
  for index, row in df.iterrows():
98
+ # 狀態邏輯
99
  status = row.get('status', '待處理')
 
100
  status_color = "#ccc"
101
+ border_color = "#444"
102
 
103
  if '確認' in status:
104
+ status_color = "#2ecc71" # 綠
105
+ border_color = "#2ecc71"
106
  elif '取消' in status:
107
+ status_color = "#e74c3c" # 紅
108
+ border_color = "#e74c3c"
109
  elif '已發' in status:
110
+ status_color = "#f1c40f" # 黃
111
+ border_color = "#f1c40f"
112
 
113
+ # 按鈕邏輯
114
+ # 只有「顧客已取消」才鎖定,其他狀態 (包含已確認、已發送) 都允許重發
115
+ is_canceled = '取消' in status
116
+ btn_onclick = "" if is_canceled else f"cardAction({row['id']})"
117
+
118
+ # 按鈕樣式與文字
119
+ if is_canceled:
120
+ btn_style = "background: #333; color: #666; cursor: not-allowed;"
121
+ btn_text = "🚫 已取消"
122
+ elif '已發' in status or '確認' in status:
123
+ btn_style = "background: #2c3e50; color: #fff; border: 1px solid #555; hover:background:#34495e;"
124
+ btn_text = "🔄 重發確認" # 允許重發
125
+ else:
126
+ btn_style = "background: #d4af37; color: #000; font-weight:bold; box-shadow: 0 4px 10px rgba(212, 175, 55, 0.4);"
127
+ btn_text = "🚀 發送確認"
128
 
129
  card = f"""
130
  <div class="booking-card" style="
131
+ background: #1e1e1e;
132
+ border-left: 6px solid {border_color};
133
+ border-radius: 10px;
134
+ padding: 20px;
135
+ box-shadow: 0 6px 16px rgba(0,0,0,0.4);
136
+ font-family: 'Segoe UI', Roboto, sans-serif;
137
+ position: relative;">
 
138
 
139
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; border-bottom:1px solid #333; padding-bottom:10px;">
140
+ <div style="font-size:1.1em; color:#d4af37; font-weight:600;">
141
+ <span style="font-size:0.8em; color:#888; margin-right:5px;">📅 日期</span> {row['date']}
 
 
 
 
 
142
  </div>
 
143
  <div style="
 
144
  color: {status_color};
145
+ background: {status_color}1a;
146
+ padding: 4px 12px;
147
  border-radius: 20px;
148
+ font-size: 0.85em;
149
+ font-weight: bold;
150
+ letter-spacing: 1px;">
151
  {status}
152
  </div>
153
  </div>
154
+
155
+ <div style="display:grid; grid-template-columns: 1fr 1fr; gap:15px; margin-bottom:15px;">
156
+ <div>
157
+ <div style="font-size:0.8em; color:#666; margin-bottom:2px;">⏰ 訂位時間</div>
158
+ <div style="font-size:1.6em; color:#fff; font-weight:800; font-family:monospace;">{row['time']}</div>
159
+ </div>
160
+ <div>
161
+ <div style="font-size:0.8em; color:#666; margin-bottom:2px;">👥 人數</div>
162
+ <div style="font-size:1.6em; color:#fff; font-weight:800;">{row['pax']} <span style="font-size:0.5em; font-weight:400; color:#888;">位</span></div>
163
+ </div>
164
+ </div>
165
 
166
+ <div style="background:#262626; padding:15px; border-radius:8px; margin-bottom:15px;">
167
+ <div style="margin-bottom:8px;">
168
+ <span style="color:#888; font-size:0.85em; display:inline-block; width:40px;">姓名</span>
169
+ <span style="color:#eee; font-size:1.1em; font-weight:600;">{row['name']}</span>
170
+ </div>
171
+ <div style="margin-bottom:8px;">
172
+ <span style="color:#888; font-size:0.85em; display:inline-block; width:40px;">電話</span>
173
+ <a href="tel:{row['tel']}" style="color:#4dabf7; text-decoration:none; font-size:1.05em; letter-spacing:0.5px;">{row['tel']}</a>
174
+ </div>
175
+ <div>
176
+ <span style="color:#888; font-size:0.85em; display:inline-block; width:40px;">信箱</span>
177
+ <span style="color:#aaa; font-size:0.95em;">{row['email'] or '未提供'}</span>
178
  </div>
179
+ </div>
180
+
181
+ <div style="margin-bottom:20px;">
182
+ <div style="font-size:0.8em; color:#888; margin-bottom:4px;">📝 備註事項</div>
183
+ <div style="color:#d4af37; background:#d4af371a; padding:10px; border-radius:6px; font-size:0.95em; line-height:1.5;">
184
+ {row.get('remarks') or '無特別備註'}
185
  </div>
186
  </div>
187
 
188
+ <div style="display:flex; justify-content:space-between; align-items:center; border-top:1px solid #333; padding-top:15px;">
189
+ <span style="font-size:0.75em; color:#444; font-family:monospace;">ID: {row['id']}</span>
190
 
191
  <button onclick="{btn_onclick}" style="
 
 
192
  border: none;
193
+ padding: 10px 24px;
194
  border-radius: 6px;
195
+ font-size: 0.95em;
 
196
  transition: all 0.2s;
197
+ width: 140px;
198
  {btn_style}">
199
  {btn_text}
200
  </button>
 
219
  error_msg: "<span style='color: red'>❌ 帳號或密碼錯誤</span>"
220
  }
221
 
222
+ # --- JS 邏輯 (負責點擊卡片按鈕) ---
223
  js_logic = """
224
  function() {
 
225
  window.cardAction = function(id) {
 
226
  const idInput = document.querySelector('#hidden_id_input input');
227
  if (idInput) {
 
228
  idInput.value = id;
229
  idInput.dispatchEvent(new Event('input', { bubbles: true }));
230
  }
 
 
231
  setTimeout(() => {
232
  const sendBtn = document.querySelector('#hidden_send_btn');
233
  if (sendBtn) sendBtn.click();
 
239
  # --- CSS 優化 ---
240
  custom_css = """
241
  body, .gradio-container { background-color: #0F0F0F; color: #fff; }
242
+ /* 隱藏捲軸但保留捲動功能 */
243
  #booking_display {
244
  max-height: 85vh;
245
  overflow-y: auto;
246
  padding-right: 5px;
247
  }
248
+ #booking_display::-webkit-scrollbar { width: 4px; }
249
+ #booking_display::-webkit-scrollbar-thumb { background: #444; border-radius: 4px; }
250
+ button:active { transform: scale(0.98); }
251
  """
252
 
253
  # --- 介面 ---
254
  with gr.Blocks(title="Admin", css=custom_css) as demo:
255
 
256
+ # 1. 登入
257
  with gr.Group(visible=True) as login_row:
258
  gr.Markdown("# 🔒 Login")
259
  with gr.Row():
 
262
  login_btn = gr.Button("Enter", variant="primary")
263
  error_msg = gr.Markdown("")
264
 
265
+ # 2. 後台
266
  with gr.Group(visible=False) as admin_row:
267
+ with gr.Row(variant="panel", elem_style="background:#1a1a1a; border:none; padding:10px; margin-bottom:20px;"):
268
+ gr.Markdown("### 🍷 Cié Cié Dashboard")
269
+ refresh_btn = gr.Button("🔄 刷新列表", size="sm", variant="secondary")
270
 
271
+ # 卡片顯示區
272
  booking_display = gr.HTML(elem_id="booking_display")
273
 
274
+ # 隱藏的操作區
275
  with gr.Column(visible=False):
276
  hidden_id_input = gr.Number(elem_id="hidden_id_input", precision=0)
277
  hidden_send_btn = gr.Button("Send", elem_id="hidden_send_btn")
278
 
279
+ log_output = gr.Textbox(label="系統日誌", lines=1, interactive=False)
280
 
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
+ # 點擊卡片 -> 發送 -> 刷新卡片
289
  hidden_send_btn.click(
290
  send_confirmation_hybrid,
291
  inputs=hidden_id_input,