File size: 13,849 Bytes
21615f5
 
 
00f4ca2
21615f5
6539233
913a9b3
e0e49dd
6539233
 
 
21615f5
 
00f4ca2
b3670eb
 
21615f5
041ae95
0ad419c
 
 
21615f5
8354a5a
21615f5
5e4d3a2
 
 
 
 
 
21615f5
927501e
00f4ca2
0130dd1
307d97e
21615f5
64ee3c6
21615f5
0130dd1
21615f5
dbcfe96
16c25fa
 
5a645b7
21615f5
0130dd1
21615f5
913a9b3
16c25fa
 
21615f5
64ee3c6
5a645b7
 
 
0130dd1
5a645b7
0130dd1
 
927501e
16c25fa
0130dd1
16c25fa
 
 
 
927501e
 
16c25fa
0130dd1
dbcfe96
0130dd1
 
 
 
 
 
 
 
dbcfe96
 
 
0130dd1
 
dbcfe96
 
 
 
0130dd1
5a645b7
dbcfe96
0130dd1
927501e
16c25fa
 
 
 
 
 
927501e
 
 
16c25fa
0130dd1
dbcfe96
0130dd1
 
 
 
 
 
 
 
dbcfe96
 
 
0130dd1
 
dbcfe96
 
 
 
0130dd1
 
3db40dc
b634ffe
dbcfe96
5a645b7
307d97e
 
64ee3c6
b3670eb
927501e
307d97e
00f4ca2
0130dd1
69435e5
dbcfe96
0130dd1
 
 
 
307d97e
 
 
69435e5
0130dd1
5a645b7
dbcfe96
 
00f4ca2
69435e5
0130dd1
307d97e
0130dd1
69435e5
 
0130dd1
307d97e
69435e5
0130dd1
69435e5
 
 
307d97e
0130dd1
 
 
 
 
 
 
 
69435e5
 
3db40dc
0130dd1
 
 
 
 
 
 
69435e5
0130dd1
 
 
 
3db40dc
0130dd1
69435e5
3db40dc
69435e5
ec6b4c8
0130dd1
ec6b4c8
0130dd1
 
ec6b4c8
 
0130dd1
 
ec6b4c8
 
69435e5
0130dd1
 
 
 
ec6b4c8
0130dd1
 
3db40dc
0130dd1
69435e5
 
0130dd1
 
 
69435e5
 
 
 
 
 
 
21615f5
041ae95
0ad419c
 
 
 
 
 
 
307d97e
0ad419c
0130dd1
0fdf933
69435e5
0130dd1
 
 
 
 
 
 
 
 
 
 
 
 
 
a2d9c5d
 
0130dd1
5e4d3a2
f0da861
0ad419c
3db40dc
0ad419c
3db40dc
 
 
e0e49dd
0ad419c
 
0130dd1
 
 
 
 
 
 
64ee3c6
0130dd1
 
 
3db40dc
69435e5
 
 
 
0130dd1
69435e5
 
0130dd1
69435e5
0130dd1
69435e5
 
 
 
 
21615f5
f0da861
3db40dc
21615f5
5a5ae27
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
import gradio as gr
import os
import pandas as pd
import requests
from supabase import create_client, Client
from datetime import datetime, timedelta, timezone

# 設定台北時區
TAIPEI_TZ = timezone(timedelta(hours=8))

# --- 設定 ---
SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
GAS_MAIL_URL = os.getenv("GAS_MAIL_URL")
LINE_ACCESS_TOKEN = os.getenv("LINE_ACCESS_TOKEN")
PUBLIC_SPACE_URL = "https://deeplearning101-ciecietaipei.hf.space" 

# 取得帳密
REAL_ADMIN_USER = os.getenv("ADMIN_USER") or "Deep Learning 101"
REAL_ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD") or "2016-11-11"

supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)

def get_bookings():
    try:
        res = supabase.table("bookings").select("*").order("created_at", desc=True).execute()
        if not res.data: return pd.DataFrame()
        return pd.DataFrame(res.data)
    except:
        return pd.DataFrame()

# 🔥🔥🔥 核心後端:智慧判斷發送邏輯 (修正 LINE 連結) 🔥🔥🔥
def send_confirmation_hybrid(booking_id):
    if not booking_id: return "❌ 請輸入訂單 ID"
    
    try:
        # 1. 撈資料
        res = supabase.table("bookings").select("*").eq("id", booking_id).execute()
        if not res.data: return f"❌ 找不到 ID: {booking_id}"
        booking = res.data[0]
        
        email = booking.get('email')
        user_id = booking.get('user_id')
        current_status = booking.get('status', '')
        
        # 連結
        confirm_link = f"{PUBLIC_SPACE_URL}/?id={booking_id}&action=confirm"
        cancel_link = f"{PUBLIC_SPACE_URL}/?id={booking_id}&action=cancel"
        
        log_msg = f"🆔 {booking_id}: "

        # 2. 判斷模式
        is_reminder = "確認" in current_status

        if is_reminder:
            # --- 🔔 提醒模式 (Reminder) ---
            action_label = "提醒"
            mail_subject = f"🔔 行前提醒: {booking['date']} - Cié Cié Taipei"
            
            # 🔥 LINE 修正:加入取消連結,方便客人臨時取消
            line_text = (
                f"🔔 行前提醒\n\n"
                f"{booking['name']} 您好,期待今晚與您相見!\n\n"
                f"📅 日期:{booking['date']}\n"
                f"⏰ 時間:{booking['time']}\n"
                f"👥 人數:{booking['pax']} 位\n\n"
                f"座位已為您準備好。\n"
                f"若無法前來,請點擊下方連結取消:\n{cancel_link}"
            )
            
            mail_html = f"""
            <div style="background:#1a1a1a; color:#d4af37; padding:20px; border-radius:8px; font-family:sans-serif;">
                <h2 style="text-align:center; border-bottom:1px solid #444; padding-bottom:15px;">Cié Cié Taipei</h2>
                <p style="color:#eee; font-size:16px;"><strong>{booking['name']}</strong> 您好,行前提醒:</p>
                <div style="background:#2a2a2a; padding:15px; border-radius:8px; margin:20px 0; border-left:4px solid #f1c40f;">
                    <ul style="color:#ddd; padding-left:20px; line-height:1.8;">
                        <li>📅 日期:<strong style="color:#fff;">{booking['date']}</strong></li>
                        <li>⏰ 時間:<strong style="color:#fff;">{booking['time']}</strong></li>
                        <li>👥 人數:<strong style="color:#fff;">{booking['pax']} 位</strong></li>
                    </ul>
                </div>
                <div style="text-align:center; margin-top:30px;">
                    <span style="color:#888;">無需再次確認。</span><br><br>
                    <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>
                </div>
            </div>
            """
        else:
            # --- 🚀 確認模式 (Confirmation) ---
            action_label = "確認"
            mail_subject = f"訂位確認: {booking['date']} - Cié Cié Taipei"
            
            # 🔥 LINE 修正:加入 確認 與 取消 的連結
            line_text = (
                f"✅ 訂位確認\n\n"
                f"{booking['name']} 您好,已收到您的預約。\n\n"
                f"📅 日期:{booking['date']}\n"
                f"⏰ 時間:{booking['time']}\n"
                f"👥 人數:{booking['pax']} 位\n\n"
                f"請點擊下方連結確認出席:\n"
                f"👉 確認:{confirm_link}\n\n"
                f"🚫 取消:{cancel_link}"
            )
            
            mail_html = f"""
            <div style="background:#1a1a1a; color:#d4af37; padding:20px; border-radius:8px; font-family:sans-serif;">
                <h2 style="text-align:center; border-bottom:1px solid #444; padding-bottom:15px;">Cié Cié Taipei</h2>
                <p style="color:#eee; font-size:16px;"><strong>{booking['name']}</strong> 您好,請確認您的訂位:</p>
                <div style="background:#2a2a2a; padding:15px; border-radius:8px; margin:20px 0; border-left:4px solid #2ecc71;">
                    <ul style="color:#ddd; padding-left:20px; line-height:1.8;">
                        <li>📅 日期:<strong style="color:#fff;">{booking['date']}</strong></li>
                        <li>⏰ 時間:<strong style="color:#fff;">{booking['time']}</strong></li>
                        <li>👥 人數:<strong style="color:#fff;">{booking['pax']} 位</strong></li>
                    </ul>
                </div>
                <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>
                    <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>
            """

        # 3. 執行發送
        # Email
        if email and "@" in email and GAS_MAIL_URL:
            try:
                requests.post(GAS_MAIL_URL, json={"to": email, "subject": mail_subject, "htmlBody": mail_html, "name": "Cié Cié Taipei"})
                log_msg += f"✅ Mail({action_label}) "
            except Exception as e: log_msg += f"❌ MailErr({str(e)}) "
        else:
            log_msg += "⚠️ 無Mail "
        
        # LINE
        if user_id and len(str(user_id)) > 5 and LINE_ACCESS_TOKEN:
            try:
                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}]})
                if r.status_code == 200:
                    log_msg += f"✅ LINE({action_label}) "
                else:
                    log_msg += f"❌ LINE失敗({r.status_code}: {r.text}) "
            except Exception as e: log_msg += f"❌ LINEErr({str(e)}) "
        else:
            log_msg += "⚠️ 無LINE ID "

        # 4. 更新狀態 (僅在非提醒模式下更新)
        if not is_reminder:
            supabase.table("bookings").update({"status": "已發確認信"}).eq("id", booking_id).execute()
        
        return log_msg

    except Exception as e: return f"嚴重錯誤: {str(e)}"

# 🔥🔥🔥 卡片渲染 (純顯示,無 JS 互動) 🔥🔥🔥
def render_booking_cards():
    df = get_bookings()
    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>"
    if df.empty: return f"{count_html}<div style='text-align:center; padding:60px; color:#666; font-size:1.5em;'>📭 目前沒有訂位資料</div>"
    
    cards_html = f"{count_html}<div style='display: flex; flex-direction: column; gap: 20px; padding-bottom: 20px;'>"
    
    for index, row in df.iterrows():
        status = row.get('status', '待處理')
        
        # 顏色邏輯
        status_bg = "#ccc"; status_tx = "#000"; border_color = "#444"
        if '確認' in status: 
            status_bg = "#2ecc71"; status_tx = "#000"; border_color = "#2ecc71"
        elif '取消' in status: 
            status_bg = "#e74c3c"; status_tx = "#fff"; border_color = "#e74c3c"
        elif '已發' in status: 
            status_bg = "#f1c40f"; status_tx = "#000"; border_color = "#f1c40f"

        card = f"""
        <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); 
            font-family: '微軟正黑體', sans-serif; 
            position: relative;">
            
            <div style="display:flex; justify-content:space-between; align-items:start; margin-bottom:15px;">
                <div>
                    <span style="font-size:0.9em; color:#666; font-weight:bold; display:block;">訂單 ID</span>
                    <span style="font-size:2.5em; color:#fff; font-weight:900; line-height:1; font-family:monospace;">{row['id']}</span>
                </div>
                <div style="background:{status_bg}; color:{status_tx}; padding:6px 12px; border-radius:4px; font-weight:bold; font-size:0.9em;">
                    {status}
                </div>
            </div>

            <div style="display:grid; grid-template-columns: 1fr 1fr; gap:10px; margin-bottom:15px; border-top:1px solid #333; padding-top:15px;">
                <div>
                    <span style="color:#888; font-size:0.85em;">📅 日期 Date</span><br>
                    <span style="color:#d4af37; font-size:1.2em; font-weight:bold;">{row['date']}</span>
                </div>
                <div>
                    <span style="color:#888; font-size:0.85em;">⏰ 時間 Time</span><br>
                    <span style="color:#d4af37; font-size:1.5em; font-weight:900;">{row['time']}</span>
                </div>
            </div>
            
            <div style="background:#222; padding:15px; border-radius:8px; margin-bottom:15px;">
                <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>
                <div style="font-size:1.1em; margin-bottom:5px;">
                    📞 <a href="tel:{row['tel']}" style="color:#69c0ff; text-decoration:none;">{row['tel']}</a>
                </div>
                <div style="font-size:0.9em; color:#888;">✉️ {row['email'] or '-'}</div>
            </div>

            <div style="background:#f1c40f11; padding:12px; border-radius:6px; border:1px solid #f1c40f33;">
                <span style="color:#aaa; font-size:0.8em;">📝 備註:</span>
                <span style="color:#f1c40f;">{row.get('remarks') or '無'}</span>
            </div>
        </div>
        """
        cards_html += card
    
    cards_html += "</div>"
    return cards_html

# --- 登入邏輯 ---
def check_login(user, password):
    if user == REAL_ADMIN_USER and password == REAL_ADMIN_PASSWORD:
        return {
            login_row: gr.update(visible=False),
            admin_row: gr.update(visible=True),
            error_msg: ""
        }
    else: return {error_msg: "<span style='color: red'>❌ 帳號或密碼錯誤</span>"}

# --- CSS ---
custom_css = """
body, .gradio-container { background-color: #0F0F0F; color: #fff; }
#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);
}
#booking_display { 
    height: auto !important; 
    overflow: visible !important; 
}
"""

# --- 介面 ---
with gr.Blocks(title="Admin") as demo:
    
    with gr.Group(visible=True) as login_row:
        gr.Markdown("# 🔒 Login")
        with gr.Row():
            username_input = gr.Textbox(label="User")
            password_input = gr.Textbox(label="Pass", type="password")
        login_btn = gr.Button("Enter", variant="primary")
        error_msg = gr.Markdown("")
        
    with gr.Group(visible=False) as admin_row:
        # 🔥 操作區 (固定在頂部)
        with gr.Column(elem_id="op-panel"):
            gr.Markdown("### 🍷 Cié Cié 訂位管理")
            with gr.Row():
                id_input = gr.Number(label="輸入 ID 發送通知", precision=0, scale=2)
                send_btn = gr.Button("🚀 發送通知 / 提醒 (Hybrid)", variant="primary", scale=1)
                refresh_btn = gr.Button("🔄 刷新列表", scale=1)
            
            log_output = gr.Textbox(label="執行結果", lines=1)

        # 卡片顯示區
        booking_display = gr.HTML(elem_id="booking_display")
        
        login_btn.click(check_login, inputs=[username_input, password_input], outputs=[login_row, admin_row, error_msg]).then(
            render_booking_cards, outputs=booking_display
        )
        
        refresh_btn.click(render_booking_cards, outputs=booking_display)
        
        send_btn.click(
            send_confirmation_hybrid, 
            inputs=id_input, 
            outputs=log_output
        ).then(
            render_booking_cards, 
            outputs=booking_display
        )

    demo.launch(css=custom_css)

if __name__ == "__main__":
    demo.launch()