Diff -- /var/www/vhosts/3dshawn.com/admin.3dshawn.com/support.cgi

O Operator
Diff

/var/www/vhosts/3dshawn.com/admin.3dshawn.com/support.cgi

added on local at 2026-07-11 08:07:44

Added
+0
lines
Removed
-0
lines
Context
440
unchanged
Blobs
from cdbe845dc26e
to cdbe845dc26e
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
11#!/usr/bin/perl
22#======================================================================
33# Meta-Admin -- /support.cgi
44#
55# Cross-site customer support inbox. Reads real support_threads +
66# support_messages across the 6 unified sister sites (WebSTLs, AffSoft,
77# ShopCart, RePricer, ABForge, ContactForge). Replying POSTs to that
88# site's /support_meta_send.cgi with an HMAC-signed body -- the site's
99# own Support.pm handles the DB write, so unread flags, status flips
1010# and last_message_at bookkeeping all flow through the same code path
1111# as a native (per-site) admin reply.
1212#
1313# Modes (query params):
1414# /support.cgi -> thread list (default view)
1515# /support.cgi?thread=SLUG:ID -> single-thread detail + reply form
1616# POST /support.cgi with `thread` + `body` -> send reply, redirect back
1717#======================================================================
1818use strict;
1919use warnings;
2020use lib '/var/www/vhosts/3dshawn.com/admin.3dshawn.com';
2121use CGI ();
2222use MODS::Login;
2323use MODS::MetaAdmin::Config;
2424use MODS::MetaAdmin::Wrapper;
2525use MODS::MetaAdmin::SupportInbox;
2626use MODS::MetaAdmin::CannedReplies;
2727use JSON::PP ();
2828
2929my $auth = MODS::Login->new;
3030my $u = $auth->login_verify;
3131unless ($u) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; }
3232
3333my $cgi = CGI->new;
3434my $method = $ENV{REQUEST_METHOD} || 'GET';
3535my $inbox = MODS::MetaAdmin::SupportInbox->new;
3636
3737sub _esc { my $s=shift; $s//=''; $s=~s/&/&amp;/g; $s=~s/</&lt;/g; $s=~s/>/&gt;/g; $s=~s/"/&quot;/g; return $s; }
3838
3939# Format a MySQL DATETIME "YYYY-MM-DD HH:MM:SS" as a relative age.
4040sub _rel_age {
4141 my $ts = shift;
4242 return '-' unless $ts && $ts =~ /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})$/;
4343 require Time::Local;
4444 my $epoch = eval { Time::Local::timelocal($6,$5,$4,$3,$2-1,$1-1900) } || 0;
4545 return '-' unless $epoch;
4646 my $delta = time - $epoch;
4747 return 'just now' if $delta < 60;
4848 return int($delta/60).' m ago' if $delta < 3600;
4949 return int($delta/3600).' h ago' if $delta < 86400;
5050 return int($delta/86400).' d ago';
5151}
5252
5353#---------------------------------------------------------------------
5454# POST reply
5555#---------------------------------------------------------------------
5656if ($method eq 'POST') {
5757 my $thread = $cgi->param('thread') // '';
5858 my $body = $cgi->param('body') // '';
5959 $body =~ s/\r\n/\n/g;
6060 $body =~ s/\s+$//;
6161
6262 my $err = '';
6363 my ($slug, $tid);
6464 if ($thread =~ /^([a-z0-9_-]+):(\d+)$/i) {
6565 ($slug, $tid) = (lc($1), $2 + 0);
6666 } else {
6767 $err = 'bad_thread_param';
6868 }
6969 unless ($err) {
7070 unless (length $body) { $err = 'empty_body'; }
7171 }
7272 unless ($err) {
7373 my ($ok, $ret) = $inbox->post_reply($slug, $tid, $body);
7474 $err = $ret unless $ok;
7575 }
7676
7777 my $back = '/support.cgi?thread=' . ($slug // '') . ':' . ($tid // '');
7878 $back .= '&err=' . _esc($err) if $err;
7979 $back .= '&sent=1' unless $err;
8080 print "Status: 302 Found\nLocation: $back\n\n";
8181 exit;
8282}
8383
8484#---------------------------------------------------------------------
8585# Detail mode
8686#---------------------------------------------------------------------
8787my $thread_param = $cgi->param('thread') || '';
8888if ($thread_param =~ /^([a-z0-9_-]+):(\d+)$/i) {
8989 my ($slug, $tid) = (lc($1), $2 + 0);
9090 my $res = $inbox->get_thread($slug, $tid);
9191
9292 my $body_html = '';
9393 unless ($res) {
9494 $body_html .= q~
9595 <div class="page-head"><div class="left">
9696 <a href="/support.cgi" class="page-eyebrow" style="text-decoration:none"><span class="dot"></span> &larr; Back to inbox</a>
9797 <h1 class="page-title">Thread not found</h1>
9898 <p class="page-subtitle">That thread ID isn't valid for the selected site, or the site DB is unreachable.</p>
9999 </div></div>
100100 ~;
101101 } else {
102102 my $t = $res->{thread};
103103 my $msgs = $res->{messages};
104104 my $col = _esc($t->{brand_color});
105105 my $site = _esc($t->{display_name});
106106 my $subj = _esc($t->{subject} || '(no subject)');
107107 my $cust = _esc($t->{customer_name} || $t->{customer_email} || '(unknown customer)');
108108 my $cust_e= _esc($t->{customer_email}|| '');
109109 my $stat = _esc($t->{status});
110110 my $pri = _esc($t->{priority});
111111 my $created_raw = _esc($t->{created_at} || '');
112112 my $created_ep = int($t->{created_epoch} || 0);
113113 my $created = qq~<span class="ts" data-ts="$created_ep" data-fmt="datetime">$created_raw</span>~;
114114 my $back = "/support.cgi?site=" . _esc($slug);
115115
116116 my $sent = $cgi->param('sent') ? 1 : 0;
117117 my $err_p = _esc($cgi->param('err') || '');
118118
119119 $body_html .= qq~
120120 <div class="page-head"><div class="left">
121121 <a href="/support.cgi" class="page-eyebrow" style="text-decoration:none"><span class="dot" style="background:$col"></span> &larr; Back to inbox</a>
122122 <h1 class="page-title">$subj</h1>
123123 <p class="page-subtitle"><span style="color:$col;font-weight:600">$site</span> &middot; from <strong>$cust</strong>~;
124124 $body_html .= " &lt;$cust_e&gt;" if $cust_e;
125125 $body_html .= qq~ &middot; opened $created &middot; status <code>$stat</code> &middot; priority <code>$pri</code></p>
126126 </div></div>
127127 ~;
128128
129129 if ($sent) {
130130 $body_html .= q~<div class="banner banner-ok" style="margin:12px 0;padding:10px 14px;border-radius:10px;background:rgba(34,197,94,.10);border:1px solid rgba(34,197,94,.35);color:#a7f3d0;font-size:13px">Reply sent. The customer's unread flag has been bumped on that site.</div>~;
131131 }
132132 if ($err_p) {
133133 $body_html .= qq~<div class="banner banner-err" style="margin:12px 0;padding:10px 14px;border-radius:10px;background:rgba(239,68,68,.10);border:1px solid rgba(239,68,68,.35);color:#fca5a5;font-size:13px">Reply failed: <code>$err_p</code></div>~;
134134 }
135135
136136 $body_html .= q~
137137 <style>
138138 .msg-list { display:flex; flex-direction:column; gap:14px; }
139139 .msg-row { display:flex; gap:14px; align-items:flex-start; }
140140 .msg-avatar { flex:0 0 auto; width:36px; height:36px; border-radius:50%;
141141 display:flex; align-items:center; justify-content:center; font-weight:700;
142142 font-size:12px; letter-spacing:.5px; color:#fff; user-select:none;
143143 box-shadow:0 0 12px rgba(0,0,0,.2); }
144144 .msg-body { flex:1 1 auto; min-width:0; background:var(--col-surface);
145145 border:1px solid var(--col-border); border-radius:12px;
146146 padding:14px 16px; }
147147 .msg-head { display:flex; align-items:center; gap:10px; margin-bottom:8px; font-size:12px; }
148148 .msg-sender { font-weight:700; color:var(--col-text); }
149149 .msg-role { font-size:10.5px; letter-spacing:.6px; text-transform:uppercase;
150150 padding:2px 8px; border-radius:999px; font-weight:700; }
151151 .msg-role-admin { background:rgba(181,48,72,.15); color:#e5a1ac; border:1px solid rgba(181,48,72,.35); }
152152 .msg-role-customer { background:rgba(6,182,212,.12); color:#7dd3fc; border:1px solid rgba(6,182,212,.35); }
153153 .msg-role-system { background:rgba(255,255,255,.05); color:#a8a29e; border:1px solid rgba(255,255,255,.10); }
154154 .msg-time { color:var(--col-text-dim); font-size:11px; margin-left:auto; }
155155 .msg-txt { color:var(--col-text-2); font-size:13px; line-height:1.6; white-space:pre-wrap; word-wrap:break-word; }
156156 .reply-form { margin-top:24px; }
157157 .reply-form textarea { width:100%; box-sizing:border-box; background:var(--col-surface);
158158 color:var(--col-text); border:1px solid var(--col-border);
159159 border-radius:12px; padding:14px 16px; font-family:inherit; font-size:13px;
160160 line-height:1.6; resize:vertical; min-height:160px; outline:none;
161161 transition:border-color .15s ease; }
162162 .reply-form textarea:focus { border-color:var(--col-brand, var(--col-primary, #b53048)); }
163163 .reply-form .row { display:flex; align-items:center; gap:12px; margin-top:12px; }
164164 .reply-form button { background:var(--col-brand, var(--col-primary, #b53048)); color:#fff;
165165 border:none; padding:9px 18px; border-radius:999px; font-weight:600;
166166 font-size:13px; cursor:pointer; transition:transform .1s ease, box-shadow .1s ease; }
167167 .reply-form button:hover { box-shadow:0 4px 14px rgba(181,48,72,.35); transform:translateY(-1px); }
168168 .reply-form .hint { color:var(--col-text-dim); font-size:11.5px; }
169169 </style>
170170 ~;
171171
172172 my $msg_list = '<div class="msg-list">';
173173 foreach my $m (@$msgs) {
174174 my $role = $m->{sender_role} || 'system';
175175 my $init = uc(substr(($role eq 'admin') ? 'AD' : ($role eq 'customer') ? 'CU' : 'SY', 0, 2));
176176 my $bg = ($role eq 'admin') ? $t->{brand_color}
177177 : ($role eq 'customer') ? '#0891b2' : '#4b5563';
178178 my $sender = _esc($m->{sender_name} || $m->{sender_email} || ($role eq 'admin' ? 'Portfolio admin' : ucfirst($role)));
179179 my $when = _esc($m->{created_at} || '');
180180 my $age = _rel_age($m->{created_at});
181181 my $ep = int($m->{created_epoch} || 0);
182182 my $mbody = _esc($m->{body});
183183 $msg_list .= qq~
184184 <div class="msg-row">
185185 <div class="msg-avatar" style="background:$bg;box-shadow:0 0 12px $bg">$init</div>
186186 <div class="msg-body">
187187 <div class="msg-head">
188188 <span class="msg-sender">$sender</span>
189189 <span class="msg-role msg-role-$role">$role</span>
190190 <span class="msg-time"><span class="ts" data-ts="$ep" data-fmt="relative">$age</span> &middot; <span class="ts" data-ts="$ep" data-fmt="datetime">$when</span></span>
191191 </div>
192192 <div class="msg-txt">$mbody</div>
193193 </div>
194194 </div>~;
195195 }
196196 $msg_list .= '</div>';
197197
198198 my $thread_key = _esc($slug) . ':' . ($tid + 0);
199199
200200 # Canned reply library — server-side token expansion so the dropdown
201201 # can drop pre-expanded text straight into the textarea (no
202202 # round-trip needed).
203203 my $canned = MODS::MetaAdmin::CannedReplies->new;
204204 my $tpl_rows = $canned->list(active_only => 1);
205205 my $ctx = $canned->thread_context($t);
206206 my %tpl_expanded;
207207 my $tpl_dropdown_opts = '<option value="">-- pick a canned reply --</option>';
208208 foreach my $tpl (@$tpl_rows) {
209209 $tpl_expanded{$tpl->{id} + 0} = $canned->expand($tpl->{body}, $ctx);
210210 my $n = _esc($tpl->{name});
211211 my $i = _esc($tpl->{id});
212212 $tpl_dropdown_opts .= qq~<option value="$i">$n</option>~;
213213 }
214214 my $tpl_json = JSON::PP::encode_json(\%tpl_expanded);
215215 my $tpl_count = scalar @$tpl_rows;
216216 my $tpl_hint = $tpl_count
217217 ? qq~$tpl_count canned reply@{[ $tpl_count == 1 ? '' : 's' ]} available &middot; <a href="/canned.cgi" style="color:var(--col-text-muted)">manage</a>~
218218 : q~No canned replies yet &mdash; <a href="/canned.cgi?edit=NEW" style="color:var(--col-text-muted)">create one</a>~;
219219
220220 $body_html .= qq~
221221 <div class="module glow-magenta">
222222 <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><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></div><div class="module-title">Conversation</div><span class="module-subtitle">Most recent at the bottom. Your reply below is delivered through the site's own send API so the customer sees it exactly the same way.</span></div></div>
223223 <div class="module-body">
224224 $msg_list
225225
226226 <form method="post" action="/support.cgi" class="reply-form">
227227 <input type="hidden" name="thread" value="$thread_key">
228228 <label style="display:block;margin-bottom:8px;font-size:11.5px;letter-spacing:1.4px;text-transform:uppercase;color:var(--col-text-dim);font-weight:700">Reply as portfolio admin</label>
229229 <div class="canned-picker">
230230 <select id="cannedPicker">$tpl_dropdown_opts</select>
231231 <span class="canned-hint">$tpl_hint</span>
232232 </div>
233233 <textarea name="body" id="replyBody" placeholder="Type your reply, or pick a canned reply above..." required></textarea>
234234 <div class="row">
235235 <span class="hint">Delivered via <code>/support_meta_send.cgi</code> on $site. Customer's unread flag will be bumped.</span>
236236 <button type="submit" style="margin-left:auto">Send reply</button>
237237 </div>
238238 </form>
239239 </div>
240240 </div>
241241 <style>
242242 .canned-picker { display:flex; align-items:center; gap:12px; margin-bottom:10px; }
243243 .canned-picker select { background:var(--col-surface); color:var(--col-text); border:1px solid var(--col-border); border-radius:8px; padding:6px 10px; font-family:inherit; font-size:12.5px; min-width:280px; cursor:pointer; }
244244 .canned-picker select:focus { outline:none; border-color:var(--col-brand, var(--col-primary, #b53048)); }
245245 .canned-hint { color:var(--col-text-dim); font-size:11.5px; }
246246 .canned-hint a { color:inherit; text-decoration:underline; }
247247 </style>
248248 <script>
249249 (function(){
250250 var expanded = $tpl_json;
251251 var sel = document.getElementById('cannedPicker');
252252 var ta = document.getElementById('replyBody');
253253 if (!sel || !ta) return;
254254 sel.addEventListener('change', function(){
255255 var id = sel.value;
256256 if (!id) return;
257257 var txt = expanded[id];
258258 if (typeof txt !== 'string') return;
259259 // Insert (or replace if textarea empty). If user has typed
260260 // something, append with a blank line separator.
261261 if (ta.value.trim() === '') { ta.value = txt; }
262262 else { ta.value = ta.value.replace(/\\s+\$/, '') + '\\n\\n' + txt; }
263263 ta.focus();
264264 // Reset the picker so the same template can be re-picked.
265265 sel.value = '';
266266 });
267267 })();
268268 </script>
269269 ~;
270270 }
271271
272272 MODS::MetaAdmin::Wrapper->new->render(
273273 title => 'Support thread',
274274 page_key => 'support',
275275 body => $body_html,
276276 userinfo => $u,
277277 );
278278 exit;
279279}
280280
281281#---------------------------------------------------------------------
282282# List mode -- default
283283#---------------------------------------------------------------------
284284my $q_status = $cgi->param('status') // '';
285285my $q_site = $cgi->param('site') // '';
286286my $q_days = $cgi->param('days');
287287$q_days = 90 unless $q_days && $q_days =~ /^\d+$/;
288288$q_days = 730 if $q_days > 730;
289289
290290my $threads = $inbox->list_threads(
291291 status => $q_status,
292292 site => $q_site,
293293 days => $q_days,
294294 limit => 100,
295295);
296296
297297my $total = scalar @$threads;
298298my $unresolved = grep { ($_->{status} || '') =~ /^(open|waiting_admin)$/ } @$threads;
299299my $admin_unread_total = 0;
300300$admin_unread_total += ($_->{admin_unread} || 0) for @$threads;
301301
302302my $body = '';
303303$body .= q~
304304<div class="page-head"><div class="left">
305305 <span class="page-eyebrow"><span class="dot"></span> Inbox</span>
306306 <h1 class="page-title">Support</h1>
307307 <p class="page-subtitle">Every open customer-support thread across the portfolio, in one list. Click a thread to read the full conversation and reply -- your reply goes out through the site's own send pipeline so unread flags and status flips fire exactly the same way as if you'd replied from the site itself.</p>
308308</div></div>
309309~;
310310
311311# KPI row
312312$body .= qq~
313313<div class="dash-grid" style="grid-template-columns: repeat(3, 1fr)">
314314 <div class="module glow-magenta">
315315 <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><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></div><div class="module-title">Threads</div><span class="module-subtitle">Total support conversations touched in the selected window.</span></div></div>
316316 <div class="module-body kpi kpi-magenta"><div class="num">$total</div><div class="lbl">in the last $q_days days</div><div class="sub">across the portfolio</div></div>
317317 </div>
318318 <div class="module glow-lime">
319319 <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg></div><div class="module-title">Waiting on you</div><span class="module-subtitle">Threads whose last message came from the customer or that never got an admin reply.</span></div></div>
320320 <div class="module-body kpi kpi-lime"><div class="num">$unresolved</div><div class="lbl">open + waiting_admin</div><div class="sub">worth answering</div></div>
321321 </div>
322322 <div class="module glow-cyan">
323323 <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8a6 6 0 0 0-9.33-5"/><path d="M6 8a6 6 0 0 0 9.33 5"/><circle cx="12" cy="20" r="1"/></svg></div><div class="module-title">Unread on admin side</div><span class="module-subtitle">Sum of the <code>admin_unread</code> counter across every listed thread. Bumped by any customer message.</span></div></div>
324324 <div class="module-body kpi kpi-cyan"><div class="num">$admin_unread_total</div><div class="lbl">unread messages</div><div class="sub">across all threads</div></div>
325325 </div>
326326</div>
327327~;
328328
329329# Filter chips
330330my %sel_status = (
331331 '' => $q_status eq '' ? ' is-active' : '',
332332 'unresolved' => $q_status eq 'unresolved' ? ' is-active' : '',
333333 'waiting_admin' => $q_status eq 'waiting_admin' ? ' is-active' : '',
334334 'waiting_customer' => $q_status eq 'waiting_customer' ? ' is-active' : '',
335335 'closed' => $q_status eq 'closed' ? ' is-active' : '',
336336);
337337
338338my $q_site_e = _esc($q_site);
339339my $site_options = '<option value="">All sites</option>';
340340foreach my $slug (@{ $inbox->supported_slugs }) {
341341 my $sel = ($slug eq $q_site) ? ' selected' : '';
342342 my $slug_e = _esc($slug);
343343 $site_options .= qq~<option value="$slug_e"$sel>$slug_e</option>~;
344344}
345345
346346$body .= qq~
347347<form method="get" action="/support.cgi"
348348 style="display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:8px 0 14px;padding:10px 14px;background:var(--col-surface);border:1px solid var(--col-border);border-radius:12px">
349349 <span style="font-size:11.5px;letter-spacing:1px;text-transform:uppercase;color:var(--col-text-muted);font-weight:700">Filter</span>
350350 <a class="tr-chip$sel_status{''}" href="?days=$q_days">All</a>
351351 <a class="tr-chip$sel_status{'unresolved'}" href="?days=$q_days&status=unresolved">Unresolved</a>
352352 <a class="tr-chip$sel_status{'waiting_admin'}" href="?days=$q_days&status=waiting_admin">Waiting on admin</a>
353353 <a class="tr-chip$sel_status{'waiting_customer'}" href="?days=$q_days&status=waiting_customer">Waiting on customer</a>
354354 <a class="tr-chip$sel_status{'closed'}" href="?days=$q_days&status=closed">Closed</a>
355355 <span style="margin-left:16px;font-size:11.5px;letter-spacing:1px;text-transform:uppercase;color:var(--col-text-muted);font-weight:700">Site</span>
356356 <select name="site" onchange="this.form.submit()" style="background:var(--col-surface);color:var(--col-text);border:1px solid var(--col-border);border-radius:8px;padding:4px 8px;font-size:12px;font-family:inherit">
357357 $site_options
358358 </select>
359359 <input type="hidden" name="days" value="$q_days">
360360 <input type="hidden" name="status" value="~ . _esc($q_status) . qq~">
361361 <span style="margin-left:auto;font-size:11.5px;color:var(--col-text-muted)">Window: <strong style="color:var(--col-text)">Last $q_days days</strong></span>
362362</form>
363363<style>
364364.tr-chip{display:inline-flex;align-items:center;padding:5px 11px;border-radius:999px;font-size:12px;font-weight:600;color:var(--col-text-muted);text-decoration:none;background:transparent;border:1px solid var(--col-border);cursor:pointer;transition:all .15s ease;font-family:inherit}
365365.tr-chip:hover{color:var(--col-text);border-color:var(--col-brand, var(--col-primary, #b53048))}
366366.tr-chip.is-active{background:var(--col-brand, var(--col-primary, #b53048));color:#fff;border-color:var(--col-brand, var(--col-primary, #b53048))}
367367
368368.thr-table { display:grid; row-gap:2px; font-size:13px; }
369369.thr-row { display:grid; grid-template-columns: 40px 1.6fr 2.6fr 100px 90px; gap:14px; align-items:center;
370370 padding:12px 14px; border-bottom:1px solid rgba(255,255,255,.03); cursor:pointer;
371371 transition:background .12s ease; text-decoration:none; color:inherit; }
372372.thr-row:hover { background:rgba(255,255,255,.03); }
373373.thr-brand { width:26px; height:26px; border-radius:7px; display:flex; align-items:center; justify-content:center;
374374 font-size:10.5px; font-weight:700; color:#fff; letter-spacing:.5px; }
375375.thr-main { display:flex; flex-direction:column; gap:3px; min-width:0; }
376376.thr-subj { font-weight:600; color:var(--col-text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
377377.thr-meta { font-size:11px; color:var(--col-text-dim); }
378378.thr-prev { color:var(--col-text-dim); font-size:12.5px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
379379.thr-status { font-size:10.5px; letter-spacing:.6px; text-transform:uppercase; padding:3px 8px; border-radius:999px; font-weight:700; text-align:center; }
380380.st-open { background:rgba(6,182,212,.12); color:#7dd3fc; border:1px solid rgba(6,182,212,.35); }
381381.st-waiting_admin { background:rgba(239,68,68,.10); color:#fca5a5; border:1px solid rgba(239,68,68,.35); }
382382.st-waiting_customer { background:rgba(180,150,50,.12);color:#facc15; border:1px solid rgba(180,150,50,.35); }
383383.st-resolved,.st-closed { background:rgba(34,197,94,.10);color:#a7f3d0; border:1px solid rgba(34,197,94,.35); }
384384.thr-when { text-align:right; font-size:11.5px; color:var(--col-text-dim); }
385385.thr-unread { display:inline-flex; align-items:center; justify-content:center; min-width:20px; height:20px;
386386 padding:0 6px; border-radius:999px; background:#b53048; color:#fff; font-size:10.5px;
387387 font-weight:700; margin-left:8px; box-shadow:0 0 8px rgba(181,48,72,.5); }
388388</style>
389389~;
390390
391391my $inbox_html = '';
392392if (!@$threads) {
393393 $inbox_html = q~<div style="padding:40px 14px;text-align:center;color:var(--col-text-dim);font-size:13px">
394394 No support threads in this window. Try a wider date range or clearing filters.
395395 </div>~;
396396} else {
397397 my %letters = (webstls=>'WS',ptmatrix=>'PT',affsoft=>'AS',shopcart=>'SC',
398398 repricer=>'RP',abforge=>'AB',contactforge=>'CF');
399399 foreach my $t (@$threads) {
400400 my $slug = $t->{slug};
401401 my $tid = $t->{id} + 0;
402402 my $col = _esc($t->{brand_color} || '#807aa8');
403403 my $ltr = $letters{$slug} || uc(substr($t->{display_name}||'?',0,2));
404404 my $subj = _esc($t->{subject} || '(no subject)');
405405 my $site = _esc($t->{display_name} || $slug);
406406 my $cust = _esc($t->{customer_name} || $t->{customer_email} || 'unknown');
407407 my $prev = _esc($t->{preview} || '');
408408 my $stat = _esc($t->{status} || 'open');
409409 my $age = _rel_age($t->{last_message_at} || $t->{created_at});
410410 my $when_ep = int($t->{last_message_epoch} || $t->{created_epoch} || 0);
411411 my $unread = ($t->{admin_unread} || 0) + 0;
412412 my $ur_html = $unread > 0 ? qq~<span class="thr-unread">$unread</span>~ : '';
413413 my $href = "/support.cgi?thread=" . _esc($slug) . ":" . $tid;
414414 $inbox_html .= qq~
415415 <a class="thr-row" href="$href">
416416 <div class="thr-brand" style="background:$col;box-shadow:0 0 10px $col">$ltr</div>
417417 <div class="thr-main">
418418 <div class="thr-subj">$subj$ur_html</div>
419419 <div class="thr-meta">$site &middot; from $cust</div>
420420 </div>
421421 <div class="thr-prev">$prev</div>
422422 <div class="thr-status st-$stat">$stat</div>
423423 <div class="thr-when"><span class="ts" data-ts="$when_ep" data-fmt="relative">$age</span></div>
424424 </a>~;
425425 }
426426}
427427
428428$body .= qq~
429429<div class="module glow-magenta">
430430 <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg></div><div class="module-title">All threads</div><span class="module-subtitle">Newest activity first. Unread badge is the site's <code>admin_unread</code> counter -- resets on the site when a super-admin opens the thread there.</span></div></div>
431431 <div class="module-body" style="padding:0"><div class="thr-table">$inbox_html</div></div>
432432</div>
433433~;
434434
435435MODS::MetaAdmin::Wrapper->new->render(
436436 title => 'Support inbox',
437437 page_key => 'support',
438438 body => $body,
439439 userinfo => $u,
440440);