Diff -- /var/www/vhosts/3dshawn.com/repricer.3dshawn.com/TEMPLATES/repricer_admin_chat.html

O Operator
Diff

/var/www/vhosts/3dshawn.com/repricer.3dshawn.com/TEMPLATES/repricer_admin_chat.html

added on local at 2026-07-01 21:47:21

Added
+516
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 97e93724abea
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1<!-- =====================================================================
2 Admin Chat -- 3-column layout: convo list / thread / visitor info.
3===================================================================== -->
4
5<style>
6 .ch-grid { display:grid; grid-template-columns:300px 1fr 260px; gap:14px; height:calc(100vh - 220px); min-height:520px; }
7 @media (max-width: 1100px) { .ch-grid { grid-template-columns:240px 1fr; } .ch-side { display:none; } }
8 @media (max-width: 800px) { .ch-grid { grid-template-columns:1fr; height:auto; } .ch-list { max-height:360px; overflow-y:auto; } }
9
10 .ch-panel { background: var(--col-surface-1); border:1px solid var(--col-border); border-radius:12px; overflow:hidden; display:flex; flex-direction:column; }
11
12 /* Convo list */
13 .ch-list-head { padding:10px 12px; border-bottom:1px solid var(--col-border); display:flex; flex-direction:column; gap:8px; }
14 .ch-list-head .ch-list-title { font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--col-text-3);font-weight:700; }
15 .ch-tabs { display:flex; gap:4px; flex-wrap:wrap; }
16 .ch-tab { display:inline-flex; align-items:center; gap:6px; padding:4px 9px; border-radius:6px; font-size:11px; font-weight:700; letter-spacing:0.4px; text-transform:uppercase; color:var(--col-text-3); text-decoration:none; background:transparent; border:1px solid transparent; transition: background 0.12s, color 0.12s; }
17 .ch-tab:hover { color:var(--col-text); background: rgba(5,150,105,0.06); }
18 .ch-tab.is-current { color:#fff; background:#047857; }
19 .ch-tab .cnt { font-size:10px; padding:1px 5px; border-radius:4px; background:rgba(255,255,255,0.1); }
20 .ch-tab.is-current .cnt { background:rgba(255,255,255,0.22); }
21 .ch-search { position:relative; }
22 .ch-search input { width:100%; box-sizing:border-box; background: var(--col-surface-2); border:1px solid var(--col-border); color:var(--col-text); padding:7px 28px 7px 10px; border-radius:7px; font-size:12px; font-family:inherit; outline:none; }
23 .ch-search input:focus { border-color:#059669; }
24 .ch-search .clear { position:absolute; right:6px; top:50%; transform:translateY(-50%); background:transparent; border:0; color:var(--col-text-3); cursor:pointer; padding:2px 4px; font-size:14px; line-height:1; }
25 .ch-search .clear:hover { color:#fff; }
26 .ch-list { overflow-y:auto; flex:1; }
27 .ch-item { display:block; padding:12px 14px; border-bottom:1px solid var(--col-border); text-decoration:none; color:inherit; transition: background 0.12s; }
28 .ch-item:hover, .ch-item.is-selected { background: rgba(5,150,105,0.08); }
29 .ch-item.is-selected { border-left:3px solid #059669; padding-left:11px; }
30 .ch-item.is-closed, .ch-item.is-archived { opacity:0.6; }
31 .ch-item.is-closed:hover, .ch-item.is-archived:hover, .ch-item.is-closed.is-selected, .ch-item.is-archived.is-selected { opacity:1; }
32 .ch-item .top { display:flex; justify-content:space-between; gap:8px; align-items:center; margin-bottom:4px; }
33 .ch-item .name { font-weight:700; font-size:13px; color:var(--col-text); display:inline-flex; align-items:center; gap:6px; }
34 .ch-item .name .live-dot { width:7px; height:7px; border-radius:50%; background:#22c55e; box-shadow:0 0 5px #22c55e; }
35 .ch-item .badge { font-size:10px; padding:1px 6px; border-radius:6px; background:#ef4444; color:#fff; font-weight:700; }
36 .ch-state-pill { font-size:9px; padding:1px 6px; border-radius:5px; font-weight:700; letter-spacing:0.6px; text-transform:uppercase; }
37 .ch-state-pill.closed { background:rgba(100,116,139,0.25); color:#94a3b8; }
38 .ch-state-pill.archived { background:rgba(168,85,247,0.20); color:#10b981; }
39 .ch-state-pill.waiting { background:rgba(245,158,11,0.20); color:#fbbf24; }
40
41 /* Thread action buttons in head */
42 .ch-thread-actions { display:flex; gap:6px; margin-left:auto; }
43 .ch-thread-actions form { display:inline; }
44 .ch-thread-actions button { background:transparent; border:1px solid var(--col-border); color:var(--col-text-2); padding:5px 10px; border-radius:6px; font-size:11px; font-weight:700; letter-spacing:0.4px; text-transform:uppercase; cursor:pointer; font-family:inherit; }
45 .ch-thread-actions button:hover { color:#fff; border-color:#059669; }
46 .ch-thread-actions button.danger:hover { color:#fff; border-color:#ef4444; background:rgba(239,68,68,0.1); }
47 .ch-thread-actions button.reopen:hover { color:#fff; border-color:#22c55e; background:rgba(34,197,94,0.1); }
48 .ch-thread-state { font-size:10px; font-weight:700; letter-spacing:0.6px; text-transform:uppercase; padding:2px 8px; border-radius:5px; }
49 .ch-thread-state.closed { background:rgba(100,116,139,0.25); color:#cbd5e1; }
50 .ch-thread-state.archived { background:rgba(168,85,247,0.20); color:#10b981; }
51 .ch-pill { font-size:9px; font-weight:700; padding:2px 6px; border-radius:5px; letter-spacing:0.8px; text-transform:uppercase; }
52 .ch-pill.guest { background: rgba(100,116,139,0.20); color:#cbd5e1; }
53 .ch-pill.lead { background: rgba(245,158,11,0.20); color:#fbbf24; }
54 .ch-pill.customer { background: rgba(34,197,94,0.20); color:#4ade80; }
55 .ch-item .preview { font-size:11px; color:var(--col-text-3); display:-webkit-box; -webkit-line-clamp:1; -webkit-box-orient:vertical; overflow:hidden; }
56 .ch-item .meta { font-size:10px; color:var(--col-text-3); margin-top:2px; }
57 .ch-item .meta.from-admin { color:#10b981; }
58
59 /* Thread */
60 .ch-thread { display:flex; flex-direction:column; }
61 .ch-thread-head { padding:12px 16px; border-bottom:1px solid var(--col-border); display:flex; align-items:center; gap:10px; }
62 .ch-thread-body { flex:1; overflow-y:auto; padding:18px; display:flex; flex-direction:column; gap:10px; }
63 .ch-msg { padding:10px 14px; border-radius:14px; line-height:1.5; font-size:13px; align-self:flex-start; box-sizing:border-box; }
64 .ch-msg.visitor, .ch-msg.admin { width:100%; }
65 .ch-msg.visitor { background:#1e293b; color:#e2e8f0; border-bottom-left-radius:4px; }
66 .ch-msg.admin { background:#1e3a8a; color:#fff; border-bottom-left-radius:4px; }
67 .ch-msg.system { background:rgba(15,118,110,0.18); color:#6ee7b7; align-self:center; font-size:11px; padding:4px 10px; }
68 .ch-msg-head { display:flex; justify-content:space-between; align-items:center; gap:10px; margin-bottom:4px; }
69 .ch-msg-author { display:flex; align-items:center; gap:6px; font-size:10px; font-weight:700; letter-spacing:0.6px; text-transform:uppercase; opacity:0.85; }
70 .ch-msg-author svg { width:13px; height:13px; flex-shrink:0; }
71 .ch-msg time { font-size:10px; opacity:0.75; white-space:nowrap; font-weight:700; }
72 .ch-thread-foot { border-top:1px solid var(--col-border); padding:10px 12px; display:flex; flex-direction:column; gap:8px; }
73 .ch-thread-foot textarea { width:100%; min-height:64px; max-height:200px; resize:vertical; background: var(--col-surface-2); border:1px solid var(--col-border); color:var(--col-text); padding:10px 12px; border-radius:8px; font-size:13px; font-family:inherit; outline:none; box-sizing:border-box; }
74 .ch-thread-foot textarea:focus { border-color:#059669; }
75 .ch-thread-foot .ch-actions { display:flex; gap:8px; justify-content:flex-end; }
76 .ch-thread-foot button { background:#059669; border:0; color:#fff; padding:8px 18px; border-radius:8px; cursor:pointer; font-weight:700; font-size:13px; font-family:inherit; }
77 .ch-thread-foot button:hover { background:#059669; }
78 .ch-thread-foot button.push-btn { background: transparent; border:1px solid var(--col-border); color:var(--col-text-2); }
79 .ch-thread-foot button.push-btn:hover { color:#fff; border-color:#059669; }
80 .ch-msg { white-space:pre-wrap; }
81
82 /* Side info */
83 .ch-side { padding:14px; }
84 .ch-side h3 { font-size:11px; letter-spacing:1.5px; text-transform:uppercase; color:var(--col-text-3); font-weight:700; margin:0 0 10px; }
85 .ch-side .row { display:flex; justify-content:space-between; gap:8px; padding:6px 0; border-bottom:1px dashed var(--col-border); font-size:12px; }
86 .ch-side .row:last-child { border-bottom:none; }
87 .ch-side .k { color:var(--col-text-3); }
88 .ch-side .v { color:var(--col-text); font-weight:600; text-align:right; word-break:break-all; }
89
90 /* Online toggle */
91 .ch-toggle { display:inline-flex; align-items:center; gap:8px; cursor:pointer; font-size:12px; color:var(--col-text-2); }
92 .ch-toggle .ind { width:10px; height:10px; border-radius:50%; background:#475569; }
93 .ch-toggle.is-on .ind { background:#22c55e; box-shadow:0 0 6px #22c55e; }
94</style>
95
96<div class="page-head">
97 <div>
98 <span class="page-eyebrow"><span class="dot"></span> Admin &middot; Live chat</span>
99 <h1 class="page-title">Visitor chat console</h1>
100 <p class="page-subtitle">Chat with visitors in real time. Toggle yourself offline when you don't want to take messages &mdash; the chat bubble hides on every storefront automatically.</p>
101 </div>
102 <div class="page-actions">
103 <form method="POST" action="/admin_tracking_action.cgi" style="display:inline">
104 <input type="hidden" name="act" value="toggle_online">
105 <input type="hidden" name="value" value="[if:$admin_online]0[/if][if:!$admin_online]1[/if]">
106 <button type="submit" class="ch-toggle[if:$admin_online] is-on[/if]" style="background:var(--col-surface-2);border:1px solid var(--col-border);padding:8px 14px;border-radius:10px;font-family:inherit">
107 <span class="ind"></span>
108 [if:$admin_online]You're online &mdash; click to go offline[/if][if:!$admin_online]You're offline &mdash; click to go online[/if]
109 </button>
110 </form>
111 [if:$not_tutorial_mode]<a href="/admin_chat.cgi?tutorial=1" class="btn btn-secondary"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>Sample Data View</a>[/if]
112 [if:$tutorial_mode]<a href="/admin_chat.cgi" class="btn btn-secondary"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>Exit sample view</a>[/if]
113 </div>
114</div>
115
116[if:$schema_missing]
117<div class="an-banner" style="background:rgba(245,158,11,0.10);border-color:rgba(245,158,11,0.30)">
118 <strong>Chat schema not yet installed.</strong> Run the latest <code>db_schema.sql</code>.
119</div>
120[/if]
121
122[if:$tutorial_mode]
123<div class="tutorial-banner">
124 <div class="tutorial-banner-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
125 <div class="tutorial-banner-body">
126 <div class="tutorial-banner-title">Tutorial &middot; Sample data only</div>
127 <div class="tutorial-banner-text">3 example conversations with realistic visitor messages. None of this is real chat traffic.</div>
128 </div>
129 <a href="/admin_chat.cgi" class="btn btn-secondary btn-sm">Exit sample view</a>
130</div>
131[/if]
132
133[if:$schema_ready]
134
135<!-- Concluded chats per day (last 30 days) -->
136[if:$has_resolved_months]
137<style>
138.ccd-graph { display:grid;grid-template-columns:repeat(30,1fr);gap:3px;align-items:end;height:110px;position:relative; }
139.ccd-bar-wrap { display:flex;flex-direction:column;align-items:stretch;height:100%;justify-content:flex-end;cursor:pointer;position:relative;border-radius:4px;transition:background 0.12s; }
140.ccd-bar-wrap:hover { background:rgba(5,150,105,0.10); }
141.ccd-bar { width:100%;background:var(--col-accent-deep,#047857);border-radius:3px 3px 0 0;min-height:2px;transition:filter 0.12s,transform 0.12s;transform-origin:bottom; }
142.ccd-bar-wrap:hover .ccd-bar { filter:brightness(1.30);transform:scaleY(1.04); }
143.ccd-bar-wrap.is-current .ccd-bar { box-shadow:0 0 0 2px rgba(5,150,105,0.55); }
144.ccd-axis { display:grid;grid-template-columns:repeat(30,1fr);gap:3px;margin-top:6px;font-size:9px;color:var(--col-text-3);font-family:var(--font-mono,monospace); }
145.ccd-axis-cell { text-align:center;letter-spacing:0;line-height:1; }
146.ccd-axis-cell.is-week { color:var(--col-text-2);font-weight:700; }
147.ccd-tip { position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);background:rgba(8,12,26,0.96);border:1px solid var(--col-border);color:#fff;font-size:11px;padding:6px 10px;border-radius:7px;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity 0.12s;z-index:30;box-shadow:0 6px 18px rgba(0,0,0,0.4); }
148.ccd-tip strong { color:var(--col-accent-bright,#10b981);font-weight:700; }
149.ccd-bar-wrap:hover .ccd-tip { opacity:1; }
150
151/* Drilldown overlay: dimmed full-page backdrop with a centered card
152 listing every chat closed on the clicked day. Linked rows jump
153 straight back into the conversation thread in the console. */
154.ccd-overlay { position:fixed;inset:0;background:rgba(2,6,18,0.72);backdrop-filter:blur(6px);z-index:9999;display:none;align-items:flex-start;justify-content:center;padding:60px 20px;overflow:auto; }
155.ccd-overlay.is-open { display:flex; }
156.ccd-card { background:var(--col-surface-1);border:1px solid var(--col-border);border-radius:14px;width:100%;max-width:560px;padding:22px 24px;box-shadow:0 24px 60px rgba(0,0,0,0.55); }
157.ccd-card-head { display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px; }
158.ccd-card-head h3 { margin:0;font-family:var(--font-display);font-size:18px;font-weight:700;color:#fff; }
159.ccd-card-close { background:transparent;border:none;color:var(--col-text-3);font-size:22px;cursor:pointer;line-height:1;padding:0 4px; }
160.ccd-card-close:hover { color:#fff; }
161.ccd-card-empty { font-size:13px;color:var(--col-text-3);padding:18px 0; }
162.ccd-card-list { display:flex;flex-direction:column;gap:6px;max-height:60vh;overflow:auto; }
163.ccd-card-row { display:flex;align-items:center;justify-content:space-between;gap:10px;background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:8px;padding:10px 12px;color:var(--col-text);text-decoration:none;transition:border-color 0.12s,background 0.12s; }
164.ccd-card-row:hover { border-color:var(--col-accent);background:var(--col-surface-3); }
165.ccd-card-time { font-family:var(--font-mono,monospace);font-size:11px;color:var(--col-text-3); }
166</style>
167<div style="display:grid;grid-template-columns:240px 1fr;gap:14px;margin:0 0 18px;align-items:stretch">
168 <div style="background:var(--col-surface-1);border:1px solid var(--col-border);border-radius:12px;padding:16px">
169 <div class="text-xs text-dim" style="letter-spacing:1.5px;text-transform:uppercase;font-weight:700">Resolved today</div>
170 <div style="font-family:var(--font-display);font-size:30px;font-weight:700;color:#fff;margin:6px 0 2px">$resolved_mtd</div>
171 <div class="text-xs text-dim">$resolved_total_6mo closed + archived in the last 30 days</div>
172 </div>
173 <div style="background:var(--col-surface-1);border:1px solid var(--col-border);border-radius:12px;padding:16px">
174 <div class="text-xs text-dim" style="letter-spacing:1.5px;text-transform:uppercase;font-weight:700;margin-bottom:10px">Concluded chats &middot; last 30 days</div>
175 <div class="ccd-graph" id="ccdGraph">
176 [loop:@resolved_months]
177 <div class="ccd-bar-wrap[if:$loop1.is_current] is-current[/if]"
178 data-d-iso="$loop1.d_iso"
179 data-d-label="$loop1.d_label"
180 data-n="$loop1.n"
181 data-chats='$loop1.chats_json'>
182 <div class="ccd-tip"><strong>$loop1.n</strong> on $loop1.d_label</div>
183 <div class="ccd-bar" style="height:$loop1.bar_pct%"></div>
184 </div>
185 [/loop]
186 </div>
187 <div class="ccd-axis">
188 [loop:@resolved_months]
189 <div class="ccd-axis-cell[if:$loop1.is_current] is-week[/if]">$loop1.d_short</div>
190 [/loop]
191 </div>
192 </div>
193</div>
194
195<!-- Drilldown overlay (one shared modal; populated on bar click) -->
196<div class="ccd-overlay" id="ccdOverlay" role="dialog" aria-modal="true" aria-labelledby="ccdOverlayTitle">
197 <div class="ccd-card">
198 <div class="ccd-card-head">
199 <h3 id="ccdOverlayTitle">&mdash;</h3>
200 <button type="button" class="ccd-card-close" aria-label="Close" data-ccd-close>&times;</button>
201 </div>
202 <div id="ccdOverlayBody"></div>
203 </div>
204</div>
205
206<script>
207(function(){
208 var graph = document.getElementById('ccdGraph');
209 var ov = document.getElementById('ccdOverlay');
210 if (!graph || !ov) return;
211 var title = document.getElementById('ccdOverlayTitle');
212 var body = document.getElementById('ccdOverlayBody');
213
214 function openOverlay(wrap) {
215 var n = parseInt(wrap.getAttribute('data-n') || '0', 10);
216 var lbl = wrap.getAttribute('data-d-label') || '';
217 var chats = [];
218 try { chats = JSON.parse(wrap.getAttribute('data-chats') || '[]'); } catch(e) { chats = []; }
219 title.textContent = lbl + ' - ' + n + (n === 1 ? ' chat' : ' chats') + ' resolved';
220 if (!chats.length) {
221 body.innerHTML = '<div class="ccd-card-empty">No closures on this day. The bar shows 0 because the data is up to date, not because we missed anything.</div>';
222 } else {
223 var rows = chats.map(function(c){
224 var label = (c.label == null ? '' : ('' + c.label));
225 var hm = c.hm || '';
226 var href = '/admin_chat.cgi?chat=' + encodeURIComponent(c.id);
227 var safeLabel = label.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
228 return '<a class="ccd-card-row" href="' + href + '">'
229 + '<span>' + safeLabel + '</span>'
230 + '<span class="ccd-card-time">' + hm + '</span>'
231 + '</a>';
232 }).join('');
233 body.innerHTML = '<div class="ccd-card-list">' + rows + '</div>';
234 }
235 ov.classList.add('is-open');
236 }
237 function closeOverlay() { ov.classList.remove('is-open'); }
238
239 graph.addEventListener('click', function(e){
240 var wrap = e.target.closest('.ccd-bar-wrap');
241 if (!wrap || !graph.contains(wrap)) return;
242 openOverlay(wrap);
243 });
244 ov.addEventListener('click', function(e){
245 if (e.target === ov || e.target.hasAttribute('data-ccd-close')) closeOverlay();
246 });
247 document.addEventListener('keydown', function(e){
248 if (e.key === 'Escape' && ov.classList.contains('is-open')) closeOverlay();
249 });
250})();
251</script>
252[/if]
253
254<div class="ch-grid">
255 <!-- Conversation list -->
256 <div class="ch-panel">
257 <div class="ch-list-head">
258 <span class="ch-list-title">Conversations &middot; $chat_count[if:$has_search] match[/if]</span>
259 <form method="GET" action="/admin_chat.cgi" class="ch-search">
260 [if:$tutorial_mode]<input type="hidden" name="tutorial" value="1">[/if]
261 <input type="hidden" name="view" value="$view">
262 <input type="text" name="q" value="$search_q" placeholder="Search by name, email, or message..." autocomplete="off">
263 [if:$has_search]<a href="/admin_chat.cgi?view=$view[if:$tutorial_mode]&tutorial=1[/if]" class="clear" title="Clear search">&times;</a>[/if]
264 </form>
265 <div class="ch-tabs">
266 <a href="/admin_chat.cgi?view=active[if:$tutorial_mode]&tutorial=1[/if]" class="ch-tab[if:$is_view_active] is-current[/if]">Active <span class="cnt">$count_active</span></a>
267 <a href="/admin_chat.cgi?view=closed[if:$tutorial_mode]&tutorial=1[/if]" class="ch-tab[if:$is_view_closed] is-current[/if]">Closed <span class="cnt">$count_closed</span></a>
268 <a href="/admin_chat.cgi?view=archived[if:$tutorial_mode]&tutorial=1[/if]" class="ch-tab[if:$is_view_archived] is-current[/if]">Archived <span class="cnt">$count_archived</span></a>
269 <a href="/admin_chat.cgi?view=all[if:$tutorial_mode]&tutorial=1[/if]" class="ch-tab[if:$is_view_all] is-current[/if]">All <span class="cnt">$count_all</span></a>
270 </div>
271 </div>
272 <div class="ch-list">
273 [if:$has_chats]
274 [loop:@chats]
275 <a href="$loop1.href" class="ch-item[if:$loop1.is_selected] is-selected[/if][if:$loop1.is_closed] is-closed[/if][if:$loop1.is_archived] is-archived[/if]">
276 <div class="top">
277 <span class="name">[if:$loop1.visitor_online]<span class="live-dot" title="Visitor on site now"></span>[/if]$loop1.label</span>
278 [if:$loop1.unread]<span class="badge">$loop1.unread</span>[/if]
279 </div>
280 <div style="display:flex;gap:6px;align-items:center;margin:2px 0 4px;flex-wrap:wrap">
281 [if:$loop1.is_customer]<span class="ch-pill customer">Customer</span><span class="meta">$loop1.total_orders orders &middot; $loop1.ltv_display</span>[/if]
282 [if:$loop1.is_lead]<span class="ch-pill lead">Lead</span>[/if]
283 [if:$loop1.is_guest]<span class="ch-pill guest">Guest</span>[/if]
284 [if:$loop1.is_closed]<span class="ch-state-pill closed">Closed</span>[/if]
285 [if:$loop1.is_archived]<span class="ch-state-pill archived">Archived</span>[/if]
286 [if:$loop1.awaiting_reply]<span class="ch-state-pill waiting">Awaiting</span>[/if]
287 </div>
288 [if:$loop1.has_email]<div class="meta">$loop1.email</div>[/if]
289 <div class="preview"><strong>[if:$loop1.last_from][if:$loop1.last_from eq 'admin']You: [/if][/if]</strong>$loop1.last_body</div>
290 <div class="meta">$loop1.platform_meta &middot; $loop1.last_message_at</div>
291 </a>
292 [/loop]
293 [/if]
294 [if:!$has_chats]
295 <div style="padding:24px;text-align:center;color:var(--col-text-3);font-size:12px">[if:$has_search]No conversations match &quot;$search_q&quot;.[/if][if:!$has_search]No conversations in this view yet.[/if]</div>
296 [/if]
297 </div>
298 </div>
299
300 <!-- Thread + reply -->
301 <div class="ch-panel ch-thread">
302 [if:$has_selected]
303 <div class="ch-thread-head">
304 <span style="font-weight:700;color:var(--col-text);font-size:14px">$thread.label</span>
305 [if:$thread.is_closed]<span class="ch-thread-state closed">Closed</span>[/if]
306 [if:$thread.is_archived]<span class="ch-thread-state archived">Archived</span>[/if]
307 <span style="font-size:11px;color:var(--col-text-3);margin-left:12px">$thread.os &middot; $thread.browser &middot; $thread.device</span>
308 <div class="ch-thread-actions">
309 [if:$thread.is_open]
310 <form method="POST" action="/admin_tracking_action.cgi"
311 data-confirm-title="Close this conversation?"
312 data-confirm-message="The visitor will see the closure in their chat widget."
313 data-confirm-label="Close conversation"
314 data-confirm-style="warning">
315 <input type="hidden" name="act" value="close_chat">
316 <input type="hidden" name="chat_id" value="$selected_chat_id">
317 <button type="submit" class="danger" title="Mark closed -- still visible in Closed view">Close</button>
318 </form>
319 <form method="POST" action="/admin_tracking_action.cgi">
320 <input type="hidden" name="act" value="archive_chat">
321 <input type="hidden" name="chat_id" value="$selected_chat_id">
322 <button type="submit" title="Move to Archived view -- comes back if visitor messages again">Archive</button>
323 </form>
324 [/if]
325 [if:$thread.is_closed]
326 <form method="POST" action="/admin_tracking_action.cgi">
327 <input type="hidden" name="act" value="reopen_chat">
328 <input type="hidden" name="chat_id" value="$selected_chat_id">
329 <button type="submit" class="reopen">Reopen</button>
330 </form>
331 <form method="POST" action="/admin_tracking_action.cgi">
332 <input type="hidden" name="act" value="archive_chat">
333 <input type="hidden" name="chat_id" value="$selected_chat_id">
334 <button type="submit">Archive</button>
335 </form>
336 [/if]
337 [if:$thread.is_archived]
338 <form method="POST" action="/admin_tracking_action.cgi">
339 <input type="hidden" name="act" value="reopen_chat">
340 <input type="hidden" name="chat_id" value="$selected_chat_id">
341 <button type="submit" class="reopen">Reopen</button>
342 </form>
343 [/if]
344 </div>
345 </div>
346 <div class="ch-thread-body" id="ch-thread-body">
347 [loop:@thread_rows]
348 [if:$loop1.is_system]<div class="ch-msg system">$loop1.body</div>[/if]
349 [if:$loop1.is_visitor]<div class="ch-msg visitor"><div class="ch-msg-head"><div class="ch-msg-author"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/></svg>Visitor</div><time>$loop1.sent_at</time></div>$loop1.body</div>[/if]
350 [if:$loop1.is_admin]<div class="ch-msg admin"><div class="ch-msg-head"><div class="ch-msg-author"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="4" cy="9" r="2"/><path d="M1 19a3 3 0 0 1 3-3"/><circle cx="12" cy="7" r="3"/><path d="M7 19a5 5 0 0 1 10 0"/><circle cx="20" cy="9" r="2"/><path d="M23 19a3 3 0 0 0-3-3"/></svg>Admin</div><time>$loop1.sent_at</time></div>$loop1.body</div>[/if]
351 [/loop]
352 </div>
353 <form method="POST" action="/admin_tracking_action.cgi" class="ch-thread-foot">
354 <input type="hidden" name="act" value="send_message">
355 <input type="hidden" name="chat_id" value="$selected_chat_id">
356 <input type="hidden" name="back_to" value="chat">
357 <textarea name="body" placeholder="Type your reply... (Enter to send, Shift+Enter for new line)" maxlength="2000" required autofocus rows="3"></textarea>
358 <div class="ch-actions">
359 <button type="button" class="push-btn js-push-link" title="Push a link/window to this visitor">+ Push link</button>
360 <button type="submit">Send</button>
361 </div>
362 </form>
363 <form id="ch-push-form" method="POST" action="/admin_tracking_action.cgi" style="display:none">
364 <input type="hidden" name="act" value="push_link">
365 <input type="hidden" name="session_id" value="$thread.session_id">
366 <input type="hidden" name="back_to" value="chat">
367 <input type="hidden" name="chat_id" value="$selected_chat_id">
368 <input type="hidden" name="url" value="">
369 <input type="hidden" name="label" value="">
370 </form>
371 <script>
372 // Push-link flow: URL prompt -> optional label prompt -> submit.
373 document.addEventListener('click', function (e) {
374 var btn = e.target.closest('.js-push-link');
375 if (!btn) return;
376 appPrompt({
377 title: 'Push a link to this visitor',
378 message: 'They will see a notification with this URL in their chat widget.',
379 placeholder: 'https://',
380 default_value: 'https://',
381 input_type: 'url',
382 label: 'Next',
383 required: true,
384 validate: function (v) {
385 if (!/^https?:\/\//i.test(v)) return 'Must start with http:// or https://';
386 return '';
387 }
388 }).then(function (url) {
389 if (url === null) return;
390 appPrompt({
391 title: 'Label for the link (optional)',
392 message: 'Short text shown alongside the URL. Leave blank to use the URL itself.',
393 placeholder: 'Try this',
394 label: 'Push link',
395 required: false
396 }).then(function (label) {
397 if (label === null) return;
398 var f = document.getElementById('ch-push-form');
399 f.url.value = url;
400 f.label.value = label || '';
401 f.submit();
402 });
403 });
404 });
405 </script>
406 [/if]
407 [if:!$has_selected]
408 <div style="flex:1;display:grid;place-items:center;text-align:center;padding:32px;color:var(--col-text-3)">
409 <div>
410 <svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" style="margin:0 auto 12px;display:block;color:var(--col-text-3)"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z"/></svg>
411 <div style="font-size:14px;color:var(--col-text-2);font-weight:600;margin-bottom:4px">Pick a conversation</div>
412 <div style="font-size:12px;color:var(--col-text-3)">Or wait for a visitor to open the chat widget.</div>
413 </div>
414 </div>
415 [/if]
416 </div>
417
418 <!-- Visitor side panel -->
419 <div class="ch-panel ch-side">
420 [if:$has_thread]
421 <h3>Buyer status</h3>
422 <div style="margin-bottom:12px;text-align:center">
423 [if:$thread.is_customer]<span class="ch-pill customer" style="font-size:11px;padding:4px 10px">Customer</span>
424 <div style="margin-top:6px;font-size:12px;color:var(--col-text-2)"><strong style="color:#fff">$thread.total_orders</strong> orders &middot; <strong style="color:#4ade80">$thread.ltv_display</strong> lifetime</div>[/if]
425 [if:$thread.is_lead]<span class="ch-pill lead" style="font-size:11px;padding:4px 10px">Lead</span>
426 <div style="margin-top:6px;font-size:11px;color:var(--col-text-3)">Signed up, no orders yet</div>[/if]
427 [if:$thread.is_guest]<span class="ch-pill guest" style="font-size:11px;padding:4px 10px">Guest</span>
428 <div style="margin-top:6px;font-size:11px;color:var(--col-text-3)">No account &mdash; could become a lead</div>[/if]
429 </div>
430 [if:$thread.has_email]
431 <div class="row"><span class="k">Email</span><span class="v" style="font-family:var(--font-mono,monospace);font-size:11px">$thread.email</span></div>
432 [/if]
433 <h3 style="margin-top:14px">Client info</h3>
434 <div class="row"><span class="k">OS</span><span class="v">$thread.os</span></div>
435 <div class="row"><span class="k">Browser</span><span class="v">$thread.browser</span></div>
436 <div class="row"><span class="k">Device</span><span class="v">$thread.device</span></div>
437 <div class="row"><span class="k">Screen</span><span class="v">$thread.screen</span></div>
438 <div class="row"><span class="k">Language</span><span class="v">$thread.language</span></div>
439 <div class="row"><span class="k">Time zone</span><span class="v">$thread.timezone</span></div>
440 <div class="row"><span class="k">Current page</span><span class="v" style="font-family:var(--font-mono,monospace);font-size:10px">$thread.current_page</span></div>
441
442 <div style="margin-top:16px;display:flex;gap:8px">
443 <a href="/admin_visitors.cgi?id=$thread.session_id" class="btn btn-secondary btn-sm" style="flex:1;text-align:center">All sessions</a>
444 </div>
445 [/if]
446 </div>
447</div>
448
449<script>
450 // Auto-scroll the thread body to bottom on load
451 (function () {
452 var el = document.getElementById('ch-thread-body');
453 if (el) el.scrollTop = el.scrollHeight;
454 })();
455
456 // Preserve any in-progress reply across the auto-refresh. We stash
457 // the textbox value in sessionStorage keyed by chat_id, restore it
458 // on load, and clear it on submit. Enter sends, Shift+Enter inserts
459 // a newline -- standard chat ergonomics.
460 (function () {
461 var input = document.querySelector('.ch-thread-foot [name="body"]');
462 var chatId = document.querySelector('.ch-thread-foot input[name="chat_id"]');
463 if (!input || !chatId) return;
464 var key = 'ws_chat_draft_' + chatId.value;
465 var saved = sessionStorage.getItem(key);
466 if (saved) {
467 input.value = saved;
468 try { input.setSelectionRange(saved.length, saved.length); } catch (e) {}
469 }
470 input.addEventListener('input', function () {
471 sessionStorage.setItem(key, input.value);
472 });
473 input.addEventListener('keydown', function (e) {
474 if (e.key === 'Enter' && !e.shiftKey) {
475 e.preventDefault();
476 // Programmatic form.submit() does NOT fire the 'submit' event,
477 // so the form-level cleanup listener below never runs on Enter.
478 // Clear the draft here too, otherwise the next page load
479 // restores the text we just sent.
480 sessionStorage.removeItem(key);
481 if (input.form) input.form.submit();
482 }
483 });
484 if (input.form) {
485 input.form.addEventListener('submit', function () {
486 sessionStorage.removeItem(key);
487 });
488 }
489 })();
490
491 // Auto-refresh every 6s to pick up new visitor messages (skipped in
492 // sample view). If the admin is actively focused on the reply input
493 // we defer the reload 3s at a time so we never yank the cursor mid-
494 // sentence -- their draft is saved either way, but reloading while
495 // typing is jarring. Same deferral when the daily-trend drilldown
496 // overlay is open: yanking that mid-read is just as bad.
497 if (location.search.indexOf('tutorial=1') < 0) {
498 function chRefreshTick() {
499 var input = document.querySelector('.ch-thread-foot [name="body"]');
500 if (input && document.activeElement === input) {
501 setTimeout(chRefreshTick, 3000);
502 return;
503 }
504 var drill = document.getElementById('ccdOverlay');
505 if (drill && drill.classList.contains('is-open')) {
506 setTimeout(chRefreshTick, 3000);
507 return;
508 }
509 location.reload();
510 }
511 setTimeout(chRefreshTick, 6000);
512 }
513</script>
514[/if]
515
516<div class="adm-foot-spacer" data-end="admin-chat">&nbsp;</div>