Diff -- /var/www/vhosts/3dshawn.com/crm.3dshawn.com/TEMPLATES/cf_admin_chat.html

O Operator
Diff

/var/www/vhosts/3dshawn.com/crm.3dshawn.com/TEMPLATES/cf_admin_chat.html

added on local at 2026-07-01 15:03:07

Added
+0
lines
Removed
-0
lines
Context
516
unchanged
Blobs
from 93bde25bc08b
to 93bde25bc08b
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
11<!-- =====================================================================
22 Admin Chat -- 3-column layout: convo list / thread / visitor info.
33===================================================================== -->
44
55<style>
66 .ch-grid { display:grid; grid-template-columns:300px 1fr 260px; gap:14px; height:calc(100vh - 220px); min-height:520px; }
77 @media (max-width: 1100px) { .ch-grid { grid-template-columns:240px 1fr; } .ch-side { display:none; } }
88 @media (max-width: 800px) { .ch-grid { grid-template-columns:1fr; height:auto; } .ch-list { max-height:360px; overflow-y:auto; } }
99
1010 .ch-panel { background: var(--col-surface-1); border:1px solid var(--col-border); border-radius:12px; overflow:hidden; display:flex; flex-direction:column; }
1111
1212 /* Convo list */
1313 .ch-list-head { padding:10px 12px; border-bottom:1px solid var(--col-border); display:flex; flex-direction:column; gap:8px; }
1414 .ch-list-head .ch-list-title { font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--col-text-3);font-weight:700; }
1515 .ch-tabs { display:flex; gap:4px; flex-wrap:wrap; }
1616 .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; }
1717 .ch-tab:hover { color:var(--col-text); background: rgba(59,130,246,0.06); }
1818 .ch-tab.is-current { color:#fff; background:#1d4ed8; }
1919 .ch-tab .cnt { font-size:10px; padding:1px 5px; border-radius:4px; background:rgba(255,255,255,0.1); }
2020 .ch-tab.is-current .cnt { background:rgba(255,255,255,0.22); }
2121 .ch-search { position:relative; }
2222 .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; }
2323 .ch-search input:focus { border-color:#3b82f6; }
2424 .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; }
2525 .ch-search .clear:hover { color:#fff; }
2626 .ch-list { overflow-y:auto; flex:1; }
2727 .ch-item { display:block; padding:12px 14px; border-bottom:1px solid var(--col-border); text-decoration:none; color:inherit; transition: background 0.12s; }
2828 .ch-item:hover, .ch-item.is-selected { background: rgba(59,130,246,0.08); }
2929 .ch-item.is-selected { border-left:3px solid #3b82f6; padding-left:11px; }
3030 .ch-item.is-closed, .ch-item.is-archived { opacity:0.6; }
3131 .ch-item.is-closed:hover, .ch-item.is-archived:hover, .ch-item.is-closed.is-selected, .ch-item.is-archived.is-selected { opacity:1; }
3232 .ch-item .top { display:flex; justify-content:space-between; gap:8px; align-items:center; margin-bottom:4px; }
3333 .ch-item .name { font-weight:700; font-size:13px; color:var(--col-text); display:inline-flex; align-items:center; gap:6px; }
3434 .ch-item .name .live-dot { width:7px; height:7px; border-radius:50%; background:#22c55e; box-shadow:0 0 5px #22c55e; }
3535 .ch-item .badge { font-size:10px; padding:1px 6px; border-radius:6px; background:#ef4444; color:#fff; font-weight:700; }
3636 .ch-state-pill { font-size:9px; padding:1px 6px; border-radius:5px; font-weight:700; letter-spacing:0.6px; text-transform:uppercase; }
3737 .ch-state-pill.closed { background:rgba(100,116,139,0.25); color:#94a3b8; }
3838 .ch-state-pill.archived { background:rgba(168,85,247,0.20); color:#c084fc; }
3939 .ch-state-pill.waiting { background:rgba(245,158,11,0.20); color:#fbbf24; }
4040
4141 /* Thread action buttons in head */
4242 .ch-thread-actions { display:flex; gap:6px; margin-left:auto; }
4343 .ch-thread-actions form { display:inline; }
4444 .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; }
4545 .ch-thread-actions button:hover { color:#fff; border-color:#3b82f6; }
4646 .ch-thread-actions button.danger:hover { color:#fff; border-color:#ef4444; background:rgba(239,68,68,0.1); }
4747 .ch-thread-actions button.reopen:hover { color:#fff; border-color:#22c55e; background:rgba(34,197,94,0.1); }
4848 .ch-thread-state { font-size:10px; font-weight:700; letter-spacing:0.6px; text-transform:uppercase; padding:2px 8px; border-radius:5px; }
4949 .ch-thread-state.closed { background:rgba(100,116,139,0.25); color:#cbd5e1; }
5050 .ch-thread-state.archived { background:rgba(168,85,247,0.20); color:#c084fc; }
5151 .ch-pill { font-size:9px; font-weight:700; padding:2px 6px; border-radius:5px; letter-spacing:0.8px; text-transform:uppercase; }
5252 .ch-pill.guest { background: rgba(100,116,139,0.20); color:#cbd5e1; }
5353 .ch-pill.lead { background: rgba(245,158,11,0.20); color:#fbbf24; }
5454 .ch-pill.customer { background: rgba(34,197,94,0.20); color:#4ade80; }
5555 .ch-item .preview { font-size:11px; color:var(--col-text-3); display:-webkit-box; -webkit-line-clamp:1; -webkit-box-orient:vertical; overflow:hidden; }
5656 .ch-item .meta { font-size:10px; color:var(--col-text-3); margin-top:2px; }
5757 .ch-item .meta.from-admin { color:#60a5fa; }
5858
5959 /* Thread */
6060 .ch-thread { display:flex; flex-direction:column; }
6161 .ch-thread-head { padding:12px 16px; border-bottom:1px solid var(--col-border); display:flex; align-items:center; gap:10px; }
6262 .ch-thread-body { flex:1; overflow-y:auto; padding:18px; display:flex; flex-direction:column; gap:10px; }
6363 .ch-msg { padding:10px 14px; border-radius:14px; line-height:1.5; font-size:13px; align-self:flex-start; box-sizing:border-box; }
6464 .ch-msg.visitor, .ch-msg.admin { width:100%; }
6565 .ch-msg.visitor { background:#1e293b; color:#e2e8f0; border-bottom-left-radius:4px; }
6666 .ch-msg.admin { background:#1e3a8a; color:#fff; border-bottom-left-radius:4px; }
6767 .ch-msg.system { background:rgba(124,58,237,0.18); color:#c4b5fd; align-self:center; font-size:11px; padding:4px 10px; }
6868 .ch-msg-head { display:flex; justify-content:space-between; align-items:center; gap:10px; margin-bottom:4px; }
6969 .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; }
7070 .ch-msg-author svg { width:13px; height:13px; flex-shrink:0; }
7171 .ch-msg time { font-size:10px; opacity:0.75; white-space:nowrap; font-weight:700; }
7272 .ch-thread-foot { border-top:1px solid var(--col-border); padding:10px 12px; display:flex; flex-direction:column; gap:8px; }
7373 .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; }
7474 .ch-thread-foot textarea:focus { border-color:#3b82f6; }
7575 .ch-thread-foot .ch-actions { display:flex; gap:8px; justify-content:flex-end; }
7676 .ch-thread-foot button { background:#2563eb; border:0; color:#fff; padding:8px 18px; border-radius:8px; cursor:pointer; font-weight:700; font-size:13px; font-family:inherit; }
7777 .ch-thread-foot button:hover { background:#3b82f6; }
7878 .ch-thread-foot button.push-btn { background: transparent; border:1px solid var(--col-border); color:var(--col-text-2); }
7979 .ch-thread-foot button.push-btn:hover { color:#fff; border-color:#3b82f6; }
8080 .ch-msg { white-space:pre-wrap; }
8181
8282 /* Side info */
8383 .ch-side { padding:14px; }
8484 .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; }
8585 .ch-side .row { display:flex; justify-content:space-between; gap:8px; padding:6px 0; border-bottom:1px dashed var(--col-border); font-size:12px; }
8686 .ch-side .row:last-child { border-bottom:none; }
8787 .ch-side .k { color:var(--col-text-3); }
8888 .ch-side .v { color:var(--col-text); font-weight:600; text-align:right; word-break:break-all; }
8989
9090 /* Online toggle */
9191 .ch-toggle { display:inline-flex; align-items:center; gap:8px; cursor:pointer; font-size:12px; color:var(--col-text-2); }
9292 .ch-toggle .ind { width:10px; height:10px; border-radius:50%; background:#475569; }
9393 .ch-toggle.is-on .ind { background:#22c55e; box-shadow:0 0 6px #22c55e; }
9494</style>
9595
9696<div class="page-head">
9797 <div>
9898 <span class="page-eyebrow"><span class="dot"></span> Admin &middot; Live chat</span>
9999 <h1 class="page-title">Visitor chat console</h1>
100100 <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 dashboard automatically.</p>
101101 </div>
102102 <div class="page-actions">
103103 <form method="POST" action="/admin_tracking_action.cgi" style="display:inline">
104104 <input type="hidden" name="act" value="toggle_online">
105105 <input type="hidden" name="value" value="[if:$admin_online]0[/if][if:!$admin_online]1[/if]">
106106 <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">
107107 <span class="ind"></span>
108108 [if:$admin_online]You're online &mdash; click to go offline[/if][if:!$admin_online]You're offline &mdash; click to go online[/if]
109109 </button>
110110 </form>
111111 [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]
112112 [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]
113113 </div>
114114</div>
115115
116116[if:$schema_missing]
117117<div class="an-banner" style="background:rgba(245,158,11,0.10);border-color:rgba(245,158,11,0.30)">
118118 <strong>Chat schema not yet installed.</strong> Run the latest <code>db_schema.sql</code>.
119119</div>
120120[/if]
121121
122122[if:$tutorial_mode]
123123<div class="tutorial-banner">
124124 <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>
125125 <div class="tutorial-banner-body">
126126 <div class="tutorial-banner-title">Tutorial &middot; Sample data only</div>
127127 <div class="tutorial-banner-text">3 example conversations with realistic visitor messages. None of this is real chat traffic.</div>
128128 </div>
129129 <a href="/admin_chat.cgi" class="btn btn-secondary btn-sm">Exit sample view</a>
130130</div>
131131[/if]
132132
133133[if:$schema_ready]
134134
135135<!-- Concluded chats per day (last 30 days) -->
136136[if:$has_resolved_months]
137137<style>
138138.ccd-graph { display:grid;grid-template-columns:repeat(30,1fr);gap:3px;align-items:end;height:110px;position:relative; }
139139.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; }
140140.ccd-bar-wrap:hover { background:rgba(59,130,246,0.10); }
141141.ccd-bar { width:100%;background:var(--col-accent-deep,#1d4ed8);border-radius:3px 3px 0 0;min-height:2px;transition:filter 0.12s,transform 0.12s;transform-origin:bottom; }
142142.ccd-bar-wrap:hover .ccd-bar { filter:brightness(1.30);transform:scaleY(1.04); }
143143.ccd-bar-wrap.is-current .ccd-bar { box-shadow:0 0 0 2px rgba(59,130,246,0.55); }
144144.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); }
145145.ccd-axis-cell { text-align:center;letter-spacing:0;line-height:1; }
146146.ccd-axis-cell.is-week { color:var(--col-text-2);font-weight:700; }
147147.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); }
148148.ccd-tip strong { color:var(--col-accent-bright,#60a5fa);font-weight:700; }
149149.ccd-bar-wrap:hover .ccd-tip { opacity:1; }
150150
151151/* Drilldown overlay: dimmed full-page backdrop with a centered card
152152 listing every chat closed on the clicked day. Linked rows jump
153153 straight back into the conversation thread in the console. */
154154.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; }
155155.ccd-overlay.is-open { display:flex; }
156156.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); }
157157.ccd-card-head { display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px; }
158158.ccd-card-head h3 { margin:0;font-family:var(--font-display);font-size:18px;font-weight:700;color:#fff; }
159159.ccd-card-close { background:transparent;border:none;color:var(--col-text-3);font-size:22px;cursor:pointer;line-height:1;padding:0 4px; }
160160.ccd-card-close:hover { color:#fff; }
161161.ccd-card-empty { font-size:13px;color:var(--col-text-3);padding:18px 0; }
162162.ccd-card-list { display:flex;flex-direction:column;gap:6px;max-height:60vh;overflow:auto; }
163163.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; }
164164.ccd-card-row:hover { border-color:var(--col-accent);background:var(--col-surface-3); }
165165.ccd-card-time { font-family:var(--font-mono,monospace);font-size:11px;color:var(--col-text-3); }
166166</style>
167167<div style="display:grid;grid-template-columns:240px 1fr;gap:14px;margin:0 0 18px;align-items:stretch">
168168 <div style="background:var(--col-surface-1);border:1px solid var(--col-border);border-radius:12px;padding:16px">
169169 <div class="text-xs text-dim" style="letter-spacing:1.5px;text-transform:uppercase;font-weight:700">Resolved today</div>
170170 <div style="font-family:var(--font-display);font-size:30px;font-weight:700;color:#fff;margin:6px 0 2px">$resolved_mtd</div>
171171 <div class="text-xs text-dim">$resolved_total_6mo closed + archived in the last 30 days</div>
172172 </div>
173173 <div style="background:var(--col-surface-1);border:1px solid var(--col-border);border-radius:12px;padding:16px">
174174 <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>
175175 <div class="ccd-graph" id="ccdGraph">
176176 [loop:@resolved_months]
177177 <div class="ccd-bar-wrap[if:$loop1.is_current] is-current[/if]"
178178 data-d-iso="$loop1.d_iso"
179179 data-d-label="$loop1.d_label"
180180 data-n="$loop1.n"
181181 data-chats='$loop1.chats_json'>
182182 <div class="ccd-tip"><strong>$loop1.n</strong> on $loop1.d_label</div>
183183 <div class="ccd-bar" style="height:$loop1.bar_pct%"></div>
184184 </div>
185185 [/loop]
186186 </div>
187187 <div class="ccd-axis">
188188 [loop:@resolved_months]
189189 <div class="ccd-axis-cell[if:$loop1.is_current] is-week[/if]">$loop1.d_short</div>
190190 [/loop]
191191 </div>
192192 </div>
193193</div>
194194
195195<!-- Drilldown overlay (one shared modal; populated on bar click) -->
196196<div class="ccd-overlay" id="ccdOverlay" role="dialog" aria-modal="true" aria-labelledby="ccdOverlayTitle">
197197 <div class="ccd-card">
198198 <div class="ccd-card-head">
199199 <h3 id="ccdOverlayTitle">&mdash;</h3>
200200 <button type="button" class="ccd-card-close" aria-label="Close" data-ccd-close>&times;</button>
201201 </div>
202202 <div id="ccdOverlayBody"></div>
203203 </div>
204204</div>
205205
206206<script>
207207(function(){
208208 var graph = document.getElementById('ccdGraph');
209209 var ov = document.getElementById('ccdOverlay');
210210 if (!graph || !ov) return;
211211 var title = document.getElementById('ccdOverlayTitle');
212212 var body = document.getElementById('ccdOverlayBody');
213213
214214 function openOverlay(wrap) {
215215 var n = parseInt(wrap.getAttribute('data-n') || '0', 10);
216216 var lbl = wrap.getAttribute('data-d-label') || '';
217217 var chats = [];
218218 try { chats = JSON.parse(wrap.getAttribute('data-chats') || '[]'); } catch(e) { chats = []; }
219219 title.textContent = lbl + ' - ' + n + (n === 1 ? ' chat' : ' chats') + ' resolved';
220220 if (!chats.length) {
221221 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>';
222222 } else {
223223 var rows = chats.map(function(c){
224224 var label = (c.label == null ? '' : ('' + c.label));
225225 var hm = c.hm || '';
226226 var href = '/admin_chat.cgi?chat=' + encodeURIComponent(c.id);
227227 var safeLabel = label.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
228228 return '<a class="ccd-card-row" href="' + href + '">'
229229 + '<span>' + safeLabel + '</span>'
230230 + '<span class="ccd-card-time">' + hm + '</span>'
231231 + '</a>';
232232 }).join('');
233233 body.innerHTML = '<div class="ccd-card-list">' + rows + '</div>';
234234 }
235235 ov.classList.add('is-open');
236236 }
237237 function closeOverlay() { ov.classList.remove('is-open'); }
238238
239239 graph.addEventListener('click', function(e){
240240 var wrap = e.target.closest('.ccd-bar-wrap');
241241 if (!wrap || !graph.contains(wrap)) return;
242242 openOverlay(wrap);
243243 });
244244 ov.addEventListener('click', function(e){
245245 if (e.target === ov || e.target.hasAttribute('data-ccd-close')) closeOverlay();
246246 });
247247 document.addEventListener('keydown', function(e){
248248 if (e.key === 'Escape' && ov.classList.contains('is-open')) closeOverlay();
249249 });
250250})();
251251</script>
252252[/if]
253253
254254<div class="ch-grid">
255255 <!-- Conversation list -->
256256 <div class="ch-panel">
257257 <div class="ch-list-head">
258258 <span class="ch-list-title">Conversations &middot; $chat_count[if:$has_search] match[/if]</span>
259259 <form method="GET" action="/admin_chat.cgi" class="ch-search">
260260 [if:$tutorial_mode]<input type="hidden" name="tutorial" value="1">[/if]
261261 <input type="hidden" name="view" value="$view">
262262 <input type="text" name="q" value="$search_q" placeholder="Search by name, email, or message..." autocomplete="off">
263263 [if:$has_search]<a href="/admin_chat.cgi?view=$view[if:$tutorial_mode]&tutorial=1[/if]" class="clear" title="Clear search">&times;</a>[/if]
264264 </form>
265265 <div class="ch-tabs">
266266 <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>
267267 <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>
268268 <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>
269269 <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>
270270 </div>
271271 </div>
272272 <div class="ch-list">
273273 [if:$has_chats]
274274 [loop:@chats]
275275 <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]">
276276 <div class="top">
277277 <span class="name">[if:$loop1.visitor_online]<span class="live-dot" title="Visitor on site now"></span>[/if]$loop1.label</span>
278278 [if:$loop1.unread]<span class="badge">$loop1.unread</span>[/if]
279279 </div>
280280 <div style="display:flex;gap:6px;align-items:center;margin:2px 0 4px;flex-wrap:wrap">
281281 [if:$loop1.is_customer]<span class="ch-pill customer">Customer</span><span class="meta">$loop1.total_orders orders &middot; $loop1.ltv_display</span>[/if]
282282 [if:$loop1.is_lead]<span class="ch-pill lead">Lead</span>[/if]
283283 [if:$loop1.is_guest]<span class="ch-pill guest">Guest</span>[/if]
284284 [if:$loop1.is_closed]<span class="ch-state-pill closed">Closed</span>[/if]
285285 [if:$loop1.is_archived]<span class="ch-state-pill archived">Archived</span>[/if]
286286 [if:$loop1.awaiting_reply]<span class="ch-state-pill waiting">Awaiting</span>[/if]
287287 </div>
288288 [if:$loop1.has_email]<div class="meta">$loop1.email</div>[/if]
289289 <div class="preview"><strong>[if:$loop1.last_from][if:$loop1.last_from eq 'admin']You: [/if][/if]</strong>$loop1.last_body</div>
290290 <div class="meta">$loop1.platform_meta &middot; $loop1.last_message_at</div>
291291 </a>
292292 [/loop]
293293 [/if]
294294 [if:!$has_chats]
295295 <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>
296296 [/if]
297297 </div>
298298 </div>
299299
300300 <!-- Thread + reply -->
301301 <div class="ch-panel ch-thread">
302302 [if:$has_selected]
303303 <div class="ch-thread-head">
304304 <span style="font-weight:700;color:var(--col-text);font-size:14px">$thread.label</span>
305305 [if:$thread.is_closed]<span class="ch-thread-state closed">Closed</span>[/if]
306306 [if:$thread.is_archived]<span class="ch-thread-state archived">Archived</span>[/if]
307307 <span style="font-size:11px;color:var(--col-text-3);margin-left:12px">$thread.os &middot; $thread.browser &middot; $thread.device</span>
308308 <div class="ch-thread-actions">
309309 [if:$thread.is_open]
310310 <form method="POST" action="/admin_tracking_action.cgi"
311311 data-confirm-title="Close this conversation?"
312312 data-confirm-message="The visitor will see the closure in their chat widget."
313313 data-confirm-label="Close conversation"
314314 data-confirm-style="warning">
315315 <input type="hidden" name="act" value="close_chat">
316316 <input type="hidden" name="chat_id" value="$selected_chat_id">
317317 <button type="submit" class="danger" title="Mark closed -- still visible in Closed view">Close</button>
318318 </form>
319319 <form method="POST" action="/admin_tracking_action.cgi">
320320 <input type="hidden" name="act" value="archive_chat">
321321 <input type="hidden" name="chat_id" value="$selected_chat_id">
322322 <button type="submit" title="Move to Archived view -- comes back if visitor messages again">Archive</button>
323323 </form>
324324 [/if]
325325 [if:$thread.is_closed]
326326 <form method="POST" action="/admin_tracking_action.cgi">
327327 <input type="hidden" name="act" value="reopen_chat">
328328 <input type="hidden" name="chat_id" value="$selected_chat_id">
329329 <button type="submit" class="reopen">Reopen</button>
330330 </form>
331331 <form method="POST" action="/admin_tracking_action.cgi">
332332 <input type="hidden" name="act" value="archive_chat">
333333 <input type="hidden" name="chat_id" value="$selected_chat_id">
334334 <button type="submit">Archive</button>
335335 </form>
336336 [/if]
337337 [if:$thread.is_archived]
338338 <form method="POST" action="/admin_tracking_action.cgi">
339339 <input type="hidden" name="act" value="reopen_chat">
340340 <input type="hidden" name="chat_id" value="$selected_chat_id">
341341 <button type="submit" class="reopen">Reopen</button>
342342 </form>
343343 [/if]
344344 </div>
345345 </div>
346346 <div class="ch-thread-body" id="ch-thread-body">
347347 [loop:@thread_rows]
348348 [if:$loop1.is_system]<div class="ch-msg system">$loop1.body</div>[/if]
349349 [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]
350350 [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]
351351 [/loop]
352352 </div>
353353 <form method="POST" action="/admin_tracking_action.cgi" class="ch-thread-foot">
354354 <input type="hidden" name="act" value="send_message">
355355 <input type="hidden" name="chat_id" value="$selected_chat_id">
356356 <input type="hidden" name="back_to" value="chat">
357357 <textarea name="body" placeholder="Type your reply... (Enter to send, Shift+Enter for new line)" maxlength="2000" required autofocus rows="3"></textarea>
358358 <div class="ch-actions">
359359 <button type="button" class="push-btn js-push-link" title="Push a link/window to this visitor">+ Push link</button>
360360 <button type="submit">Send</button>
361361 </div>
362362 </form>
363363 <form id="ch-push-form" method="POST" action="/admin_tracking_action.cgi" style="display:none">
364364 <input type="hidden" name="act" value="push_link">
365365 <input type="hidden" name="session_id" value="$thread.session_id">
366366 <input type="hidden" name="back_to" value="chat">
367367 <input type="hidden" name="chat_id" value="$selected_chat_id">
368368 <input type="hidden" name="url" value="">
369369 <input type="hidden" name="label" value="">
370370 </form>
371371 <script>
372372 // Push-link flow: URL prompt -> optional label prompt -> submit.
373373 document.addEventListener('click', function (e) {
374374 var btn = e.target.closest('.js-push-link');
375375 if (!btn) return;
376376 appPrompt({
377377 title: 'Push a link to this visitor',
378378 message: 'They will see a notification with this URL in their chat widget.',
379379 placeholder: 'https://',
380380 default_value: 'https://',
381381 input_type: 'url',
382382 label: 'Next',
383383 required: true,
384384 validate: function (v) {
385385 if (!/^https?:\/\//i.test(v)) return 'Must start with http:// or https://';
386386 return '';
387387 }
388388 }).then(function (url) {
389389 if (url === null) return;
390390 appPrompt({
391391 title: 'Label for the link (optional)',
392392 message: 'Short text shown alongside the URL. Leave blank to use the URL itself.',
393393 placeholder: 'Try this',
394394 label: 'Push link',
395395 required: false
396396 }).then(function (label) {
397397 if (label === null) return;
398398 var f = document.getElementById('ch-push-form');
399399 f.url.value = url;
400400 f.label.value = label || '';
401401 f.submit();
402402 });
403403 });
404404 });
405405 </script>
406406 [/if]
407407 [if:!$has_selected]
408408 <div style="flex:1;display:grid;place-items:center;text-align:center;padding:32px;color:var(--col-text-3)">
409409 <div>
410410 <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>
411411 <div style="font-size:14px;color:var(--col-text-2);font-weight:600;margin-bottom:4px">Pick a conversation</div>
412412 <div style="font-size:12px;color:var(--col-text-3)">Or wait for a visitor to open the chat widget.</div>
413413 </div>
414414 </div>
415415 [/if]
416416 </div>
417417
418418 <!-- Visitor side panel -->
419419 <div class="ch-panel ch-side">
420420 [if:$has_thread]
421421 <h3>Contact status</h3>
422422 <div style="margin-bottom:12px;text-align:center">
423423 [if:$thread.is_customer]<span class="ch-pill customer" style="font-size:11px;padding:4px 10px">Customer</span>
424424 <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]
425425 [if:$thread.is_lead]<span class="ch-pill lead" style="font-size:11px;padding:4px 10px">Lead</span>
426426 <div style="margin-top:6px;font-size:11px;color:var(--col-text-3)">Signed up, no orders yet</div>[/if]
427427 [if:$thread.is_guest]<span class="ch-pill guest" style="font-size:11px;padding:4px 10px">Guest</span>
428428 <div style="margin-top:6px;font-size:11px;color:var(--col-text-3)">No account &mdash; could become a lead</div>[/if]
429429 </div>
430430 [if:$thread.has_email]
431431 <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>
432432 [/if]
433433 <h3 style="margin-top:14px">Client info</h3>
434434 <div class="row"><span class="k">OS</span><span class="v">$thread.os</span></div>
435435 <div class="row"><span class="k">Browser</span><span class="v">$thread.browser</span></div>
436436 <div class="row"><span class="k">Device</span><span class="v">$thread.device</span></div>
437437 <div class="row"><span class="k">Screen</span><span class="v">$thread.screen</span></div>
438438 <div class="row"><span class="k">Language</span><span class="v">$thread.language</span></div>
439439 <div class="row"><span class="k">Time zone</span><span class="v">$thread.timezone</span></div>
440440 <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>
441441
442442 <div style="margin-top:16px;display:flex;gap:8px">
443443 <a href="/admin_visitors.cgi?id=$thread.session_id" class="btn btn-secondary btn-sm" style="flex:1;text-align:center">All sessions</a>
444444 </div>
445445 [/if]
446446 </div>
447447</div>
448448
449449<script>
450450 // Auto-scroll the thread body to bottom on load
451451 (function () {
452452 var el = document.getElementById('ch-thread-body');
453453 if (el) el.scrollTop = el.scrollHeight;
454454 })();
455455
456456 // Preserve any in-progress reply across the auto-refresh. We stash
457457 // the textbox value in sessionStorage keyed by chat_id, restore it
458458 // on load, and clear it on submit. Enter sends, Shift+Enter inserts
459459 // a newline -- standard chat ergonomics.
460460 (function () {
461461 var input = document.querySelector('.ch-thread-foot [name="body"]');
462462 var chatId = document.querySelector('.ch-thread-foot input[name="chat_id"]');
463463 if (!input || !chatId) return;
464464 var key = 'ws_chat_draft_' + chatId.value;
465465 var saved = sessionStorage.getItem(key);
466466 if (saved) {
467467 input.value = saved;
468468 try { input.setSelectionRange(saved.length, saved.length); } catch (e) {}
469469 }
470470 input.addEventListener('input', function () {
471471 sessionStorage.setItem(key, input.value);
472472 });
473473 input.addEventListener('keydown', function (e) {
474474 if (e.key === 'Enter' && !e.shiftKey) {
475475 e.preventDefault();
476476 // Programmatic form.submit() does NOT fire the 'submit' event,
477477 // so the form-level cleanup listener below never runs on Enter.
478478 // Clear the draft here too, otherwise the next page load
479479 // restores the text we just sent.
480480 sessionStorage.removeItem(key);
481481 if (input.form) input.form.submit();
482482 }
483483 });
484484 if (input.form) {
485485 input.form.addEventListener('submit', function () {
486486 sessionStorage.removeItem(key);
487487 });
488488 }
489489 })();
490490
491491 // Auto-refresh every 6s to pick up new visitor messages (skipped in
492492 // sample view). If the admin is actively focused on the reply input
493493 // we defer the reload 3s at a time so we never yank the cursor mid-
494494 // sentence -- their draft is saved either way, but reloading while
495495 // typing is jarring. Same deferral when the daily-trend drilldown
496496 // overlay is open: yanking that mid-read is just as bad.
497497 if (location.search.indexOf('tutorial=1') < 0) {
498498 function chRefreshTick() {
499499 var input = document.querySelector('.ch-thread-foot [name="body"]');
500500 if (input && document.activeElement === input) {
501501 setTimeout(chRefreshTick, 3000);
502502 return;
503503 }
504504 var drill = document.getElementById('ccdOverlay');
505505 if (drill && drill.classList.contains('is-open')) {
506506 setTimeout(chRefreshTick, 3000);
507507 return;
508508 }
509509 location.reload();
510510 }
511511 setTimeout(chRefreshTick, 6000);
512512 }
513513</script>
514514[/if]
515515
516516<div class="adm-foot-spacer" data-end="admin-chat">&nbsp;</div>