Diff -- /var/www/vhosts/3dshawn.com/repricer.3dshawn.com/TEMPLATES/repricer_admin_chat.html
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
to 97e93724abea
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 · 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 — 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 — click to go offline[/if][if:!$admin_online]You're offline — 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 · 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 · 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">—</h3> | |
| 200 | <button type="button" class="ccd-card-close" aria-label="Close" data-ccd-close>×</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,'&').replace(/</g,'<').replace(/>/g,'>'); | |
| 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 · $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">×</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 · $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 · $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 "$search_q".[/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 · $thread.browser · $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 · <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 — 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"> </div> |