Diff -- /var/www/vhosts/3dshawn.com/repricer.3dshawn.com/admin_chat.cgi

O Operator
Diff

/var/www/vhosts/3dshawn.com/repricer.3dshawn.com/admin_chat.cgi

added on local at 2026-07-11 18:33:23

Added
+555
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 7928bc4ab136
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2#======================================================================
3# RePricer - Admin Chat
4#
5# Dedicated chat console: list of conversations on the left, thread on
6# the right, reply box at the bottom. Online/offline toggle controls
7# whether the chat widget appears on visitor-facing storefronts.
8#======================================================================
9use strict;
10use warnings;
11
12use lib '/var/www/vhosts/3dshawn.com/repricer.3dshawn.com';
13use CGI;
14use MODS::Template;
15use MODS::DBConnect;
16use MODS::Login;
17use MODS::RePricer::Config;
18use MODS::RePricer::Wrapper;
19use MODS::RePricer::Admin;
20use MODS::RePricer::Tracking;
21
22my $q = CGI->new;
23my $form = $q->Vars;
24my $auth = MODS::Login->new;
25my $wrap = MODS::RePricer::Wrapper->new;
26my $tfile = MODS::Template->new;
27my $db = MODS::DBConnect->new;
28my $cfg = MODS::RePricer::Config->new;
29my $admin = MODS::RePricer::Admin->new;
30my $tr = MODS::RePricer::Tracking->new;
31my $DB = $cfg->settings('database_name');
32
33$|=1;
34
35# SCRIPT_NAME-based dispatch: consolidated wrappers (e.g. admin_tracking_action.cgi)
36# do' this file, so entry varies. Default is the chat console itself.
37my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'admin_chat';
38
39if ($entry eq 'admin_tracking_action') {
40 # admin_tracking_action needs its own auth (stricter feature gate).
41 my $ui = $auth->login_verify();
42 if (!$ui) {
43 print "Status: 302 Found\nLocation: /login.cgi\n\n";
44 exit;
45 }
46 $admin->require_admin($ui);
47 require MODS::RePricer::Permissions;
48 MODS::RePricer::Permissions->new->require_feature($ui, 'admin_view_traffic');
49 _handle_tracking_action($q, $form, $ui);
50 exit;
51}
52
53my $userinfo = $auth->login_verify();
54if (!$userinfo) {
55 print "Status: 302 Found\nLocation: /login.cgi\n\n";
56 exit;
57}
58$admin->require_admin($userinfo);
59require MODS::RePricer::Permissions;
60MODS::RePricer::Permissions->new->require_feature($userinfo, 'admin_chat');
61
62my $tutorial_mode = ($form->{tutorial} && $form->{tutorial} eq '1') ? 1 : 0;
63
64my $dbh = $db->db_connect();
65my $schema_ready = $tr->schema_ready($db, $dbh, $DB);
66
67my $admin_online = $schema_ready ? $tr->admin_online($db, $dbh, $DB) : 0;
68
69# View filter + search
70my %ok_view = (active=>1, closed=>1, archived=>1, all=>1);
71my $view = $form->{view} || 'active';
72$view = 'active' unless $ok_view{$view};
73my $search = defined $form->{q} ? $form->{q} : '';
74$search =~ s/^\s+|\s+$//g;
75my $search_trim = substr($search, 0, 100);
76
77# Conversation list
78my @chats_raw = $schema_ready ? $tr->admin_chat_list($db, $dbh, $DB,
79 filter => $view, search => $search_trim, limit => 100) : ();
80my $counts = $schema_ready ? $tr->chat_counts_by_status($db, $dbh, $DB)
81 : { active=>0, closed=>0, archived=>0, all=>0 };
82
83sub _h { my $s = shift; $s //= ''; $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g; $s =~ s/"/&quot;/g; return $s; }
84sub _qs { my $s = shift; $s //= ''; $s =~ s/([^A-Za-z0-9\-_.~])/sprintf('%%%02X', ord($1))/eg; return $s; }
85sub _ac_dt_epoch {
86 my ($s) = @_;
87 return 0 unless defined $s && length $s;
88 return 0 unless $s =~ /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?/;
89 require Time::Local;
90 my $ep = eval { Time::Local::timelocal(($6//0)+0,($5//0)+0,($4//0)+0,$3+0,$2-1,$1-1900) };
91 return ($ep && $ep > 0) ? $ep : 0;
92}
93sub _ac_ts_wrap {
94 my ($raw, $fallback, $fmt) = @_;
95 my $ep = _ac_dt_epoch($raw);
96 my $shown = defined($fallback) && length($fallback) ? $fallback : (defined($raw) ? $raw : '');
97 return $shown unless $ep > 0 && length $shown;
98 $fmt ||= 'relative';
99 return qq~<span class="ts" data-ts="$ep" data-fmt="$fmt">$shown</span>~;
100}
101
102# Build the ?view=X&q=... fragment so list links keep filter state.
103my $view_qs = '&view=' . $view;
104$view_qs .= '&q=' . _qs($search_trim) if length $search_trim;
105
106my @chats;
107my $selected_chat_id = $form->{chat} || 0;
108$selected_chat_id =~ s/[^0-9]//g;
109
110foreach my $c (@chats_raw) {
111 my $is_selected = ($selected_chat_id && $c->{chat_id} eq $selected_chat_id) ? 1 : 0;
112 $selected_chat_id ||= $c->{chat_id} if !$selected_chat_id;
113
114 # Pull session fingerprint for classification.
115 my $full = $db->db_readwrite($dbh, qq~
116 SELECT id, buyer_account_id FROM `${DB}`.tracking_sessions WHERE id='$c->{session_id}' LIMIT 1
117 ~, $ENV{SCRIPT_NAME}, __LINE__);
118 my $cls = $tr->classify_session($db, $dbh, $DB, $full);
119
120 my $chat_status = $c->{status} || 'open';
121 my $idle = $c->{idle_seconds} || 0;
122 push @chats, {
123 chat_id => $c->{chat_id},
124 session_id => $c->{session_id},
125 label => _h($c->{visitor_label} || ('Visitor #' . $c->{session_id})),
126 platform_meta => _h(($c->{os_name} || '') . ' / ' . ($c->{browser_name} || '')),
127 device => _h($c->{device_type} || ''),
128 current_page => _h($c->{current_page_path} || ''),
129 last_message_at => _ac_ts_wrap($c->{last_message_at}, $c->{last_message_at} || '', 'relative'),
130 last_body => _h(substr($c->{last_body} || '', 0, 60)),
131 last_from => $c->{last_from} || '',
132 unread => $c->{unread_for_admin} || 0,
133 href => '/admin_chat.cgi?chat=' . $c->{chat_id} . $view_qs,
134 is_selected => $is_selected,
135 # Buyer classification (guest/lead/customer)
136 status => $cls->{status},
137 status_label => $cls->{status_label},
138 is_customer => $cls->{status} eq 'customer' ? 1 : 0,
139 is_lead => $cls->{status} eq 'lead' ? 1 : 0,
140 is_guest => $cls->{status} eq 'guest' ? 1 : 0,
141 email => _h($cls->{email} || ''),
142 has_email => $cls->{email} ? 1 : 0,
143 total_orders => $cls->{total_orders},
144 ltv_display => '$' . sprintf('%.2f', ($cls->{ltv_cents} || 0) / 100),
145 # Conversation status (open/closed/archived) for visual treatment
146 chat_status => $chat_status,
147 is_open => $chat_status eq 'open' ? 1 : 0,
148 is_closed => $chat_status eq 'closed' ? 1 : 0,
149 is_archived => $chat_status eq 'archived' ? 1 : 0,
150 visitor_online => ($c->{is_online} && $idle < 60) ? 1 : 0,
151 awaiting_reply => (($c->{last_from} || '') eq 'visitor' && $c->{unread_for_admin}) ? 1 : 0,
152 };
153}
154
155# Selected thread + visitor details
156my @thread_rows;
157my $thread = {};
158if ($schema_ready && $selected_chat_id) {
159 # Mark read on view
160 $tr->mark_chat_read($db, $dbh, $DB, $selected_chat_id, 'admin');
161
162 my @msgs = $tr->chat_messages($db, $dbh, $DB, $selected_chat_id, 200);
163 foreach my $m (@msgs) {
164 # HTML-escape first, then linkify URLs. Order matters: escaping
165 # an already-built <a> tag would mangle it; URLs themselves
166 # don't contain HTML-special chars normally, and any that do
167 # (& in querystrings) are correctly &amp;-escaped inside the
168 # href attribute, which is what HTML attribute parsing expects.
169 my $safe = _h($m->{body});
170 $safe =~ s{(https?://[^\s<]+)}{<a href="$1" target="_blank" rel="noopener" style="color:#10b981;text-decoration:underline">$1</a>}g;
171 push @thread_rows, {
172 id => $m->{id},
173 from_role => $m->{from_role},
174 body => $safe,
175 sent_at => _ac_ts_wrap($m->{sent_at}, $m->{sent_at} || '', 'datetime'),
176 is_admin => ($m->{from_role} eq 'admin') ? 1 : 0,
177 is_visitor => ($m->{from_role} eq 'visitor') ? 1 : 0,
178 is_system => ($m->{from_role} eq 'system') ? 1 : 0,
179 };
180 }
181
182 # Visitor info for the right rail + chat row for status-driven actions
183 my $sess = $db->db_readwrite($dbh, qq~
184 SELECT s.*, c.status AS chat_status FROM `${DB}`.support_chats c
185 JOIN `${DB}`.tracking_sessions s ON s.id = c.session_id
186 WHERE c.id='$selected_chat_id' LIMIT 1
187 ~, $ENV{SCRIPT_NAME}, __LINE__);
188 if ($sess && $sess->{id}) {
189 my $cls = $tr->classify_session($db, $dbh, $DB, $sess);
190 my $chat_status = $sess->{chat_status} || 'open';
191 $thread = {
192 session_id => $sess->{id},
193 label => _h($sess->{visitor_label} || ''),
194 os => _h(($sess->{os_name} || '') . ' ' . ($sess->{os_version} || '')),
195 browser => _h(($sess->{browser_name} || '') . ' ' . ($sess->{browser_version} || '')),
196 device => _h($sess->{device_type} || ''),
197 screen => ($sess->{screen_width} && $sess->{screen_height}) ? ($sess->{screen_width} . 'x' . $sess->{screen_height}) : '',
198 language => _h($sess->{language} || ''),
199 timezone => _h($sess->{timezone} || ''),
200 current_page => _h($sess->{current_page_path} || ''),
201 status => $cls->{status},
202 status_label => $cls->{status_label},
203 is_customer => $cls->{status} eq 'customer' ? 1 : 0,
204 is_lead => $cls->{status} eq 'lead' ? 1 : 0,
205 is_guest => $cls->{status} eq 'guest' ? 1 : 0,
206 email => _h($cls->{email} || ''),
207 has_email => $cls->{email} ? 1 : 0,
208 total_orders => $cls->{total_orders},
209 ltv_display => '$' . sprintf('%.2f', ($cls->{ltv_cents} || 0) / 100),
210 # Conversation status drives which header buttons render.
211 chat_status => $chat_status,
212 is_open => $chat_status eq 'open' ? 1 : 0,
213 is_closed => $chat_status eq 'closed' ? 1 : 0,
214 is_archived => $chat_status eq 'archived' ? 1 : 0,
215 };
216 }
217}
218
219# ---- Concluded-chats summary (last 30 days + today) -----------------
220# Counts every chat that hit status='closed' OR 'archived' (both count
221# as "resolved" -- archive is just an old close). Today's bar always
222# renders even when zero closures so the trend reads honestly. Also
223# pulls the individual chats so the bar drilldown overlay can list
224# them without a follow-up request.
225my @resolved_raw = $schema_ready ? $tr->chats_closed_by_day($db, $dbh, $DB, 30) : ();
226my @resolved_list = $schema_ready ? $tr->chats_closed_list($db, $dbh, $DB, 30) : ();
227my %resolved_by_d = map { $_->{d} => $_ } @resolved_raw;
228my %chats_by_d;
229foreach my $cc (@resolved_list) {
230 my $d = $cc->{d} || '';
231 push @{ $chats_by_d{$d} }, {
232 id => $cc->{id} + 0,
233 hm => $cc->{hm} || '',
234 label => $cc->{label} || ('Visitor #' . $cc->{id}),
235 };
236}
237
238# Build the trailing 30 days from oldest -> newest so the chart reads
239# left-to-right as time-moves-forward.
240my @resolved_days;
241my $resolved_max = 0;
242my $today_count = 0;
243my @short_months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
244# Today's date as a reference point. We walk back 29 days and stop at today.
245my ($t_sec,$t_min,$t_hour,$t_mday,$t_mon,$t_year) = localtime(time);
246require POSIX;
247my $today_epoch = POSIX::mktime(0,0,12,$t_mday,$t_mon,$t_year);
248for (my $i = 29; $i >= 0; $i--) {
249 my $epoch = $today_epoch - ($i * 86400);
250 my @t = localtime($epoch);
251 my $d_iso = sprintf('%04d-%02d-%02d', $t[5]+1900, $t[4]+1, $t[3]);
252 my $d_short = sprintf('%s %d', $short_months[$t[4]], $t[3]); # "May 17"
253 my $n = ($resolved_by_d{$d_iso} && $resolved_by_d{$d_iso}->{n}) || 0;
254 $resolved_max = $n if $n > $resolved_max;
255 $today_count = $n if $i == 0;
256 # Serialise the per-day chat list so the drilldown can read it
257 # from a data-* attribute. Embeds visitor label + close time +
258 # link to the conversation. Strict JSON (double-quoted keys/values
259 # with JSON-escaping) so the JS can JSON.parse it cleanly.
260 my $chats_for_day = $chats_by_d{$d_iso} || [];
261 my @items_json;
262 for my $cc (@$chats_for_day) {
263 my $lbl = $cc->{label};
264 # JSON-escape: backslash, double-quote, control chars
265 $lbl =~ s/\\/\\\\/g;
266 $lbl =~ s/"/\\"/g;
267 $lbl =~ s/\r/\\r/g;
268 $lbl =~ s/\n/\\n/g;
269 $lbl =~ s/\t/\\t/g;
270 my $hm = $cc->{hm} || '';
271 $hm =~ s/[^0-9:]//g;
272 my $cid = $cc->{id} + 0;
273 # Build with sprintf instead of qq~ so a literal '~' in the
274 # visitor label cannot accidentally terminate the heredoc.
275 push @items_json, sprintf('{"id":%d,"hm":"%s","label":"%s"}', $cid, $hm, $lbl);
276 }
277 my $chats_json = '[' . join(',', @items_json) . ']';
278 # HTML-attribute escape the JSON so it's safe inside data-chats='...'.
279 # Single-quote attribute means we only need to escape the apostrophe.
280 $chats_json =~ s/&/&amp;/g;
281 $chats_json =~ s/'/&#39;/g;
282 push @resolved_days, {
283 d_iso => $d_iso,
284 d_short => $d_short,
285 d_label => $d_short,
286 n => $n,
287 is_current => $i == 0 ? 1 : 0,
288 chats_json => $chats_json,
289 };
290}
291# Add a relative-height percent for the bar visual.
292foreach my $r (@resolved_days) {
293 $r->{bar_pct} = $resolved_max ? sprintf('%.1f', ($r->{n} / $resolved_max) * 100) : '0.0';
294}
295my $resolved_total_30d = 0;
296$resolved_total_30d += $_->{n} for @resolved_days;
297# Preserve the old var names so unrelated template branches keep working.
298my $mtd_resolved = $today_count;
299my @resolved_months = @resolved_days;
300my $resolved_total_6mo = $resolved_total_30d;
301
302$db->db_disconnect($dbh);
303
304my $tvars = {
305 user_id => $userinfo->{user_id},
306 schema_ready => $schema_ready ? 1 : 0,
307 schema_missing => $schema_ready ? 0 : 1,
308 admin_online => $admin_online,
309
310 # Monthly resolved-chats summary
311 resolved_months => \@resolved_months,
312 has_resolved_months => scalar(@resolved_months) ? 1 : 0,
313 resolved_total_6mo => $resolved_total_6mo,
314 resolved_mtd => $mtd_resolved,
315 chats => \@chats,
316 has_chats => scalar(@chats) ? 1 : 0,
317 chat_count => scalar(@chats),
318 selected_chat_id => $selected_chat_id || 0,
319 has_selected => $selected_chat_id ? 1 : 0,
320 thread => $thread,
321 has_thread => %$thread ? 1 : 0,
322 thread_rows => \@thread_rows,
323 has_thread_rows => scalar(@thread_rows) ? 1 : 0,
324 tutorial_mode => $tutorial_mode,
325 not_tutorial_mode=> $tutorial_mode ? 0 : 1,
326 # Filter + search state
327 view => $view,
328 is_view_active => $view eq 'active' ? 1 : 0,
329 is_view_closed => $view eq 'closed' ? 1 : 0,
330 is_view_archived => $view eq 'archived' ? 1 : 0,
331 is_view_all => $view eq 'all' ? 1 : 0,
332 search_q => _h($search_trim),
333 has_search => length $search_trim ? 1 : 0,
334 count_active => $counts->{active},
335 count_closed => $counts->{closed},
336 count_archived => $counts->{archived},
337 count_all => $counts->{all},
338};
339
340# Tutorial sample
341if ($tutorial_mode) {
342 $tvars->{admin_online} = 1;
343 $tvars->{count_active} = 3;
344 $tvars->{count_closed} = 12;
345 $tvars->{count_archived} = 48;
346 $tvars->{count_all} = 63;
347 # Sample daily resolved-chats trend (30 trailing days). Numbers
348 # picked to look organic (busier weekdays, quieter weekends) without
349 # spiking off-chart so the bar visual reads naturally in tutorial.
350 my @sample_pattern = (4,6,5,7,3,2,1, 5,8,9,7,6,2,3, 6,7,10,8,9,4,2, 5,9,11,8,7,3,1, 6,12);
351 my @sample_days;
352 my @short_months_tut = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
353 my ($s_sec,$s_min,$s_hour,$s_mday,$s_mon,$s_year) = localtime(time);
354 require POSIX;
355 my $s_today_epoch = POSIX::mktime(0,0,12,$s_mday,$s_mon,$s_year);
356 my $sample_total = 0;
357 my $sample_today = 0;
358 my $sample_max = 0;
359 for (my $i = 29; $i >= 0; $i--) {
360 my $n = $sample_pattern[29 - $i] || 0;
361 my @t = localtime($s_today_epoch - ($i * 86400));
362 my $d_iso = sprintf('%04d-%02d-%02d', $t[5]+1900, $t[4]+1, $t[3]);
363 my $d_short = sprintf('%s %d', $short_months_tut[$t[4]], $t[3]);
364 $sample_total += $n;
365 $sample_today = $n if $i == 0;
366 $sample_max = $n if $n > $sample_max;
367 push @sample_days, {
368 d_iso => $d_iso,
369 d_short => $d_short,
370 d_label => $d_short,
371 n => $n,
372 is_current => $i == 0 ? 1 : 0,
373 chats_json => '[]',
374 };
375 }
376 foreach my $m (@sample_days) {
377 $m->{bar_pct} = $sample_max ? sprintf('%.1f', ($m->{n} / $sample_max) * 100) : '0.0';
378 }
379 $tvars->{resolved_months} = \@sample_days;
380 $tvars->{has_resolved_months} = 1;
381 $tvars->{resolved_total_6mo} = $sample_total;
382 $tvars->{resolved_mtd} = $sample_today;
383 $tvars->{chats} = [
384 { chat_id=>1, session_id=>842, label=>'Visitor #842', platform_meta=>'macOS / Safari', device=>'desktop',
385 current_page=>'/dashboard.cgi', last_message_at=>'2 min ago',
386 last_body=>'Do these prints come with supports?', last_from=>'visitor',
387 unread=>2, href=>'/admin_chat.cgi?tutorial=1&chat=1', is_selected=>1,
388 status=>'customer', status_label=>'Customer', is_customer=>1, is_lead=>0, is_guest=>0,
389 email=>'rune.caster@example.com', has_email=>1, total_orders=>4, ltv_display=>'$96.00',
390 chat_status=>'open', is_open=>1, is_closed=>0, is_archived=>0, visitor_online=>1, awaiting_reply=>1 },
391 { chat_id=>2, session_id=>841, label=>'Visitor #841', platform_meta=>'Windows / Chrome', device=>'desktop',
392 current_page=>'/products.cgi?marketplace=amazon', last_message_at=>'18 min ago',
393 last_body=>'Thanks, that worked!', last_from=>'visitor',
394 unread=>0, href=>'/admin_chat.cgi?tutorial=1&chat=2', is_selected=>0,
395 status=>'guest', status_label=>'Guest', is_customer=>0, is_lead=>0, is_guest=>1,
396 email=>'', has_email=>0, total_orders=>0, ltv_display=>'$0.00',
397 chat_status=>'open', is_open=>1, is_closed=>0, is_archived=>0, visitor_online=>0, awaiting_reply=>0 },
398 { chat_id=>3, session_id=>840, label=>'Visitor #840', platform_meta=>'iOS / Safari', device=>'mobile',
399 current_page=>'/repricing_rules.cgi', last_message_at=>'1 h ago',
400 last_body=>'Glad you could help!', last_from=>'admin',
401 unread=>0, href=>'/admin_chat.cgi?tutorial=1&chat=3', is_selected=>0,
402 status=>'lead', status_label=>'Lead', is_customer=>0, is_lead=>1, is_guest=>0,
403 email=>'bench.hammer@example.com', has_email=>1, total_orders=>0, ltv_display=>'$0.00',
404 chat_status=>'open', is_open=>1, is_closed=>0, is_archived=>0, visitor_online=>0, awaiting_reply=>0 },
405 ];
406 $tvars->{has_chats} = 1;
407 $tvars->{chat_count} = 3;
408 $tvars->{selected_chat_id} = 1;
409 $tvars->{has_selected} = 1;
410 $tvars->{thread} = {
411 session_id=>842, label=>'Visitor #842',
412 os=>'macOS 14.4', browser=>'Safari 17.4', device=>'desktop',
413 screen=>'2560x1440', language=>'en-US', timezone=>'America/Denver',
414 current_page=>'/dashboard.cgi',
415 status=>'customer', status_label=>'Customer', is_customer=>1, is_lead=>0, is_guest=>0,
416 email=>'rune.caster@example.com', has_email=>1, total_orders=>4, ltv_display=>'$96.00',
417 chat_status=>'open', is_open=>1, is_closed=>0, is_archived=>0,
418 };
419 $tvars->{has_thread} = 1;
420 $tvars->{thread_rows} = [
421 { id=>1, from_role=>'visitor', body=>'Hi! Quick question about the Dragon Lord MK-II.', sent_at=>'14:18', is_admin=>0, is_visitor=>1, is_system=>0 },
422 { id=>2, from_role=>'admin', body=>'Hey! Happy to help -- what is the question?', sent_at=>'14:19', is_admin=>1, is_visitor=>0, is_system=>0 },
423 { id=>3, from_role=>'visitor', body=>'Do these prints come with supports?', sent_at=>'14:20', is_admin=>0, is_visitor=>1, is_system=>0 },
424 { id=>4, from_role=>'visitor', body=>'And what layer height did you test at?', sent_at=>'14:20', is_admin=>0, is_visitor=>1, is_system=>0 },
425 ];
426 $tvars->{has_thread_rows} = 1;
427 $tvars->{schema_ready} = 1;
428 $tvars->{schema_missing} = 0;
429}
430
431print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
432
433my $body = join('', $tfile->template('repricer_admin_chat.html', $tvars, $userinfo));
434
435$wrap->render({
436 userinfo => $userinfo,
437 page_key => 'admin_chat',
438 title => 'Admin &middot; Chat',
439 body => $body,
440});
441
442#======================================================================
443# Consolidated from admin_tracking_action.cgi
444# Handles: send_message, push_link, toggle_online, close_chat,
445# archive_chat, reopen_chat. Auth already verified by caller.
446#======================================================================
447sub _handle_tracking_action {
448 my ($q, $form, $userinfo) = @_;
449
450 my $admin_id = $userinfo->{user_id};
451 $admin_id =~ s/[^0-9]//g;
452
453 my $act = $form->{act} || '';
454 my $back_to = $form->{back_to} || '';
455
456 my $dbh = $db->db_connect();
457 unless ($tr->schema_ready($db, $dbh, $DB)) {
458 $db->db_disconnect($dbh);
459 _act_tracking_back($back_to, '?err=schema');
460 return;
461 }
462
463 if ($act eq 'send_message') {
464 my $chat_id = $form->{chat_id}; $chat_id =~ s/[^0-9]//g;
465 my $body = $form->{body} || '';
466 if ($chat_id && length $body) {
467 $tr->post_chat_message($db, $dbh, $DB,
468 chat_id => $chat_id,
469 from_role => 'admin',
470 admin_user_id => $admin_id,
471 body => $body,
472 );
473 }
474 $db->db_disconnect($dbh);
475 _act_tracking_back($back_to, '?chat=' . ($chat_id || ''));
476 return;
477 }
478
479 if ($act eq 'push_link') {
480 my $sid = $form->{session_id}; $sid =~ s/[^0-9]//g;
481 my $url = $form->{url} || '';
482 my $label = $form->{label} || '';
483 if ($sid && length $url) {
484 $tr->push_link($db, $dbh, $DB,
485 session_id => $sid,
486 admin_user_id => $admin_id,
487 url => $url,
488 label => $label,
489 open_mode => $form->{open_mode} || 'new_tab',
490 );
491 }
492 $db->db_disconnect($dbh);
493 if ($back_to eq 'chat') {
494 my $chat_id = $form->{chat_id} || '';
495 $chat_id =~ s/[^0-9]//g;
496 _act_tracking_back('chat', '?chat=' . $chat_id . '&ok=pushed');
497 } else {
498 _act_tracking_back('visitors', '?id=' . $sid . '&ok=pushed');
499 }
500 return;
501 }
502
503 if ($act eq 'toggle_online') {
504 my $value = $form->{value};
505 $value = ($value && $value eq '1') ? 1 : 0;
506 $tr->set_admin_online($db, $dbh, $DB, $value, $admin_id);
507 $db->db_disconnect($dbh);
508 _act_tracking_back('chat', '');
509 return;
510 }
511
512 if ($act eq 'close_chat') {
513 my $chat_id = $form->{chat_id}; $chat_id =~ s/[^0-9]//g;
514 if ($chat_id) {
515 $tr->close_chat_by_admin($db, $dbh, $DB, $chat_id, $admin_id);
516 }
517 $db->db_disconnect($dbh);
518 # Land on the Closed view with this chat selected -- otherwise it
519 # vanishes from the default Active list and looks broken.
520 _act_tracking_back('chat', '?view=closed&chat=' . ($chat_id || ''));
521 return;
522 }
523
524 if ($act eq 'archive_chat') {
525 my $chat_id = $form->{chat_id}; $chat_id =~ s/[^0-9]//g;
526 if ($chat_id) {
527 $tr->archive_chat_by_admin($db, $dbh, $DB, $chat_id, $admin_id);
528 }
529 $db->db_disconnect($dbh);
530 _act_tracking_back('chat', '?view=archived&chat=' . ($chat_id || ''));
531 return;
532 }
533
534 if ($act eq 'reopen_chat') {
535 my $chat_id = $form->{chat_id}; $chat_id =~ s/[^0-9]//g;
536 if ($chat_id) {
537 $tr->reopen_chat_by_admin($db, $dbh, $DB, $chat_id, $admin_id);
538 }
539 $db->db_disconnect($dbh);
540 _act_tracking_back('chat', '?view=active&chat=' . ($chat_id || ''));
541 return;
542 }
543
544 $db->db_disconnect($dbh);
545 _act_tracking_back($back_to, '?err=unknown_act');
546 return;
547}
548
549sub _act_tracking_back {
550 my ($where, $qs) = @_;
551 my $loc = ($where eq 'visitors') ? '/admin_visitors.cgi'
552 : ($where eq 'chat') ? '/admin_chat.cgi'
553 : '/admin_chat.cgi';
554 print "Status: 302 Found\nLocation: $loc$qs\n\n";
555}