Diff -- /var/www/vhosts/3dshawn.com/crm.3dshawn.com/activities.cgi

O Operator
Diff

/var/www/vhosts/3dshawn.com/crm.3dshawn.com/activities.cgi

added on local at 2026-07-01 15:02:14

Added
+301
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 9935d393dfa4
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# ContactForge - Activities list (with filters) or calendar view.
4#
5# Consolidates the former /activity_action.cgi (CRUD handler) via
6# SCRIPT_NAME dispatch. The _action.cgi file is a wrapper that
7# `do`s this file.
8#======================================================================
9use strict;
10use warnings;
11
12use lib '/var/www/vhosts/3dshawn.com/crm.3dshawn.com';
13use CGI;
14use MODS::Template;
15use MODS::DBConnect;
16use MODS::Login;
17use MODS::ContactForge::Config;
18use MODS::ContactForge::Wrapper;
19
20my $q = CGI->new;
21my $form = $q->Vars;
22my $auth = MODS::Login->new;
23my $wrap = MODS::ContactForge::Wrapper->new;
24my $db = MODS::DBConnect->new;
25my $cfg = MODS::ContactForge::Config->new;
26my $DB = $cfg->settings('database_name');
27
28$|=1;
29my $userinfo = $auth->login_verify();
30if (!$userinfo) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; }
31my $uid = $userinfo->{user_id}; $uid =~ s/[^0-9]//g;
32
33my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'activities';
34
35if ($entry eq 'activity_action') {
36 _handle_action($q, $form);
37 exit;
38}
39
40eval {
41 require MODS::ContactForge::Permissions;
42 MODS::ContactForge::Permissions->new->require_feature($userinfo, 'activities_log');
43};
44
45sub _esc { my $s = shift; return '' unless defined $s;
46 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g; $s =~ s/"/&quot;/g; return $s; }
47sub _sql { my $s = shift // ''; $s =~ s/'/''/g; return $s; }
48sub _clean { my $s = shift // ''; $s =~ s/[^A-Za-z0-9_]//g; return $s; }
49sub _date { my $s = shift // ''; $s =~ s/[^0-9\-]//g; return $s; }
50
51my $kind = _clean($form->{kind});
52my $owner = _clean($form->{owner}) || $uid;
53my $from = _date($form->{from});
54my $to = _date($form->{to});
55my $cid = _clean($form->{contact_id});
56my $dlid = _clean($form->{deal_id});
57my $view = $form->{view} || 'list';
58
59my @w = ("a.owner_user_id='$owner'");
60push @w, "a.kind='" . _sql($kind) . "'" if $kind;
61push @w, "a.contact_id='$cid'" if $cid;
62push @w, "a.deal_id='$dlid'" if $dlid;
63push @w, "DATE(a.happened_at) >= '$from'" if $from;
64push @w, "DATE(a.happened_at) <= '$to'" if $to;
65my $where = 'WHERE ' . join(' AND ', @w);
66
67my $dbh = $db->db_connect();
68
69my @rows = $db->db_readwrite_multiple($dbh, qq~
70 SELECT a.id, a.kind, a.subject, a.outcome, a.happened_at, a.duration_seconds,
71 c.id AS c_id, c.first_name, c.last_name,
72 d.id AS d_id, d.title AS deal_title
73 FROM `${DB}`.activities a
74 LEFT JOIN `${DB}`.contacts c ON c.id=a.contact_id
75 LEFT JOIN `${DB}`.deals d ON d.id=a.deal_id
76 $where
77 ORDER BY COALESCE(a.happened_at, a.created_at) DESC
78 LIMIT 200
79~, $ENV{SCRIPT_NAME}, __LINE__);
80
81$db->db_disconnect($dbh);
82
83my %icon = (call=>'&#9742;', meeting=>'&#128197;', email=>'&#9993;',
84 sms=>'&#128241;', demo=>'&#127908;', note=>'&#9998;', other=>'&#8226;');
85
86my @kinds = ('', qw(call meeting email sms demo note other));
87my $kind_opts = join('', map {
88 my $sel = ($_ eq $kind) ? ' selected' : '';
89 my $lbl = $_ eq '' ? 'All kinds' : $_;
90 qq~<option value="$_"$sel>$lbl</option>~;
91} @kinds);
92
93my $rows_html;
94if ($view eq 'calendar') {
95 # Minimal week-style calendar: group by date.
96 my %by_date;
97 foreach my $r (@rows) {
98 my $d = substr($r->{happened_at}||'', 0, 10) || 'undated';
99 push @{$by_date{$d}}, $r;
100 }
101 $rows_html = '';
102 foreach my $d (sort { $b cmp $a } keys %by_date) {
103 $rows_html .= qq~<h3 style="margin:18px 0 8px 0;color:#9ca3af;font-size:14px;border-bottom:1px solid #1f2937;padding-bottom:4px">$d</h3>~;
104 foreach my $r (@{$by_date{$d}}) {
105 my $ico = $icon{$r->{kind}} || '&#8226;';
106 my $who = join(' ', grep {$_} ($r->{first_name}, $r->{last_name}));
107 $rows_html .= qq~<div style="padding:8px;background:#0f172a;border:1px solid #1f2937;border-radius:6px;margin-bottom:6px">
108 <span style="font-size:16px;margin-right:6px">$ico</span>
109 <strong>~ . _esc($r->{kind}) . '</strong> &middot; '
110 . _esc($r->{subject}||'(no subject)')
111 . ($who ? ' <span style="color:#9ca3af">with ' . _esc($who) . '</span>' : '')
112 . qq~</div>~;
113 }
114 }
115 $rows_html ||= '<p style="color:#9ca3af">No activities in range.</p>';
116} else {
117 if (@rows) {
118 $rows_html = '<table class="table"><thead><tr><th></th><th>Kind</th><th>Subject</th><th>Contact</th><th>Deal</th><th>When</th><th>Outcome</th></tr></thead><tbody>';
119 foreach my $r (@rows) {
120 my $ico = $icon{$r->{kind}} || '&#8226;';
121 my $who = join(' ', grep {$_} ($r->{first_name}, $r->{last_name}));
122 my $clink = $r->{c_id} ? qq~<a href="/contact_detail.cgi?id=$r->{c_id}">~ . _esc($who||'view') . '</a>' : '-';
123 my $dlink = $r->{d_id} ? qq~<a href="/deal_detail.cgi?id=$r->{d_id}">~ . _esc($r->{deal_title}||'view') . '</a>' : '-';
124 $rows_html .= '<tr>'
125 . qq~<td style="font-size:16px">$ico</td>~
126 . '<td>' . _esc($r->{kind}) . '</td>'
127 . '<td>' . _esc($r->{subject}||'-') . '</td>'
128 . "<td>$clink</td><td>$dlink</td>"
129 . '<td>' . _esc($r->{happened_at}||'-') . '</td>'
130 . '<td>' . _esc($r->{outcome}||'-') . '</td></tr>';
131 }
132 $rows_html .= '</tbody></table>';
133 } else {
134 $rows_html = '<p style="color:#9ca3af;padding:24px;text-align:center">No activities match. <a href="/activity_action.cgi?op=newform" style="color:#2dd4bf">Log one now</a>.</p>';
135 }
136}
137
138my $body = qq~
139<div class="page-header" style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
140 <div><h1 style="margin:0">Activities</h1>
141 <div style="color:#9ca3af;margin-top:4px">~ . scalar(@rows) . qq~ shown (latest 200)</div></div>
142 <div>
143 <a href="?view=list" style="padding:6px 12px;border-radius:6px;text-decoration:none;~
144 . ($view eq 'list' ? 'background:#0d9488;color:#fff' : 'background:#1f2937;color:#9ca3af') . qq~">List</a>
145 <a href="?view=calendar" style="padding:6px 12px;border-radius:6px;text-decoration:none;~
146 . ($view eq 'calendar' ? 'background:#0d9488;color:#fff' : 'background:#1f2937;color:#9ca3af') . qq~">Calendar</a>
147 <a href="/activity_action.cgi?op=newform" style="margin-left:8px;background:#0d9488;color:#fff;padding:8px 14px;border-radius:6px;text-decoration:none">+ Log activity</a>
148 </div>
149</div>
150
151<form class="card" method="get" style="background:#111827;border:1px solid #1f2937;border-radius:8px;padding:12px;margin-bottom:16px;display:flex;flex-wrap:wrap;gap:8px;align-items:center">
152 <input type="hidden" name="view" value="$view">
153 <select name="kind" style="background:#0f172a;border:1px solid #1f2937;color:#fff;padding:8px;border-radius:6px">$kind_opts</select>
154 <label style="color:#9ca3af;display:flex;align-items:center;gap:4px">From <input type="date" name="from" value="~ . _esc($from) . qq~" style="background:#0f172a;border:1px solid #1f2937;color:#fff;padding:6px;border-radius:6px"></label>
155 <label style="color:#9ca3af;display:flex;align-items:center;gap:4px">To <input type="date" name="to" value="~ . _esc($to) . qq~" style="background:#0f172a;border:1px solid #1f2937;color:#fff;padding:6px;border-radius:6px"></label>
156 <button style="background:#0d9488;color:#fff;border:none;padding:8px 14px;border-radius:6px;cursor:pointer">Apply</button>
157 <a href="/activities.cgi" style="color:#9ca3af;text-decoration:none">Reset</a>
158</form>
159
160<div class="card" style="background:#111827;border:1px solid #1f2937;border-radius:8px;padding:16px">
161 $rows_html
162</div>
163
164<style>
165.table{width:100%;border-collapse:collapse;font-size:13px}
166.table th{text-align:left;color:#9ca3af;font-weight:500;border-bottom:1px solid #1f2937;padding:6px 8px}
167.table td{padding:6px 8px;border-bottom:1px solid #1f2937}
168.table a{color:#2dd4bf;text-decoration:none}
169</style>
170~;
171
172print "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";
173$wrap->render({ userinfo => $userinfo, page_key => 'activities',
174 title => 'Activities &middot; ContactForge', body => $body });
175
176#======================================================================
177# activity_action entry: Activity CRUD handler.
178# On insert, bumps contact.last_activity_at + deal.last_activity_at.
179#======================================================================
180sub _act_sql { my $s = shift // ''; $s =~ s/'/''/g; return $s; }
181sub _act_num { my $s = shift // ''; $s =~ s/[^0-9]//g; return $s eq '' ? 'NULL' : "'$s'"; }
182sub _act_redirect { my $u = shift || '/activities.cgi'; print "Status: 302 Found\nLocation: $u\n\n"; }
183
184sub _handle_action {
185 my ($q, $form) = @_;
186
187 my $op = $form->{op} || ($form->{id} ? 'update' : 'newform');
188 my $id = $form->{id} || 0; $id =~ s/[^0-9]//g;
189
190 # ---- newform: render form ------------------------------------------
191 if ($op eq 'newform') {
192 my $cid_pre = $form->{contact_id} || ''; $cid_pre =~ s/[^0-9]//g;
193 my $did_pre = $form->{deal_id} || ''; $did_pre =~ s/[^0-9]//g;
194 my $coid_pre = $form->{company_id} || ''; $coid_pre =~ s/[^0-9]//g;
195
196 my $body = qq~
197 <h1>Log activity</h1>
198 <form method="post" action="/activity_action.cgi" style="background:#111827;border:1px solid #1f2937;border-radius:8px;padding:20px;max-width:560px">
199 <input type="hidden" name="op" value="create">
200 <input type="hidden" name="contact_id" value="$cid_pre">
201 <input type="hidden" name="deal_id" value="$did_pre">
202 <input type="hidden" name="company_id" value="$coid_pre">
203 <label>Kind<select name="kind" style="width:100%;background:#0f172a;border:1px solid #1f2937;color:#fff;padding:8px;border-radius:6px;margin-bottom:8px">
204 <option value="call">Call</option><option value="meeting">Meeting</option>
205 <option value="email">Email</option><option value="sms">SMS</option>
206 <option value="demo">Demo</option><option value="note">Note</option>
207 <option value="other">Other</option></select></label>
208 <label>Subject<input name="subject" style="width:100%;background:#0f172a;border:1px solid #1f2937;color:#fff;padding:8px;border-radius:6px;margin-bottom:8px"></label>
209 <label>Outcome<input name="outcome" style="width:100%;background:#0f172a;border:1px solid #1f2937;color:#fff;padding:8px;border-radius:6px;margin-bottom:8px"></label>
210 <label>When<input name="happened_at" type="datetime-local" style="width:100%;background:#0f172a;border:1px solid #1f2937;color:#fff;padding:8px;border-radius:6px;margin-bottom:8px"></label>
211 <label>Duration (minutes)<input name="duration_min" type="number" min="0" style="width:100%;background:#0f172a;border:1px solid #1f2937;color:#fff;padding:8px;border-radius:6px;margin-bottom:8px"></label>
212 <label>Description<textarea name="description" rows="3" style="width:100%;background:#0f172a;border:1px solid #1f2937;color:#fff;padding:8px;border-radius:6px;margin-bottom:8px"></textarea></label>
213 <div style="display:flex;gap:8px;justify-content:flex-end">
214 <a href="/activities.cgi" style="background:#1f2937;color:#fff;padding:8px 14px;border-radius:6px;text-decoration:none">Cancel</a>
215 <button style="background:#0d9488;color:#fff;border:none;padding:8px 14px;border-radius:6px;cursor:pointer">Log</button>
216 </div>
217 </form>
218 ~;
219 print "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";
220 $wrap->render({ userinfo => $userinfo, page_key => 'activities',
221 title => 'Log activity', body => $body });
222 return;
223 }
224
225 my $dbh = $db->db_connect();
226
227 if ($op eq 'create') {
228 my $kind = _act_sql($form->{kind} || 'other');
229 my $subj = _act_sql($form->{subject});
230 my $outc = _act_sql($form->{outcome});
231 my $desc = _act_sql($form->{description});
232 my $contact = _act_num($form->{contact_id});
233 my $company = _act_num($form->{company_id});
234 my $deal = _act_num($form->{deal_id});
235 my $when = $form->{happened_at}; $when =~ s/T/ /; $when =~ s/[^0-9\- :]//g;
236 my $when_sql = $when eq '' ? 'NOW()' : "'$when'";
237 my $dur_min = $form->{duration_min}; $dur_min =~ s/[^0-9]//g; $dur_min = 0 if $dur_min eq '';
238 my $dur_sec = $dur_min * 60;
239
240 $db->db_readwrite($dbh, qq~
241 INSERT INTO `${DB}`.activities
242 (owner_user_id, kind, subject, outcome, description, contact_id, company_id, deal_id,
243 happened_at, duration_seconds, created_at)
244 VALUES
245 ('$uid', '$kind', '$subj', '$outc', '$desc', $contact, $company, $deal,
246 $when_sql, '$dur_sec', NOW())
247 ~, $ENV{SCRIPT_NAME}, __LINE__);
248
249 # Bump last_activity_at on linked contact / deal / company.
250 my $cid_n = $form->{contact_id}; $cid_n =~ s/[^0-9]//g;
251 if ($cid_n) {
252 $db->db_readwrite($dbh,
253 "UPDATE `${DB}`.contacts SET last_activity_at=NOW(), last_contacted_at=NOW() WHERE id='$cid_n'",
254 $ENV{SCRIPT_NAME}, __LINE__);
255 }
256 my $did_n = $form->{deal_id}; $did_n =~ s/[^0-9]//g;
257 if ($did_n) {
258 $db->db_readwrite($dbh,
259 "UPDATE `${DB}`.deals SET last_activity_at=NOW() WHERE id='$did_n'",
260 $ENV{SCRIPT_NAME}, __LINE__);
261 }
262
263 $db->db_disconnect($dbh);
264 if ($did_n) { _act_redirect("/deal_detail.cgi?id=$did_n&tab=activities"); return; }
265 elsif ($cid_n) { _act_redirect("/contact_detail.cgi?id=$cid_n&tab=activities"); return; }
266 else { _act_redirect("/activities.cgi"); return; }
267 }
268 elsif ($op eq 'update' && $id) {
269 my @set;
270 foreach my $f (qw(kind subject outcome description)) {
271 next unless exists $form->{$f};
272 my $v = _act_sql($form->{$f});
273 push @set, "$f='$v'";
274 }
275 if (exists $form->{happened_at}) {
276 my $w = $form->{happened_at}; $w =~ s/T/ /; $w =~ s/[^0-9\- :]//g;
277 push @set, "happened_at=" . ($w eq '' ? 'NULL' : "'$w'");
278 }
279 if (@set) {
280 my $set = join(',', @set);
281 $db->db_readwrite($dbh,
282 "UPDATE `${DB}`.activities SET $set WHERE id='$id' AND owner_user_id='$uid'",
283 $ENV{SCRIPT_NAME}, __LINE__);
284 }
285 $db->db_disconnect($dbh);
286 _act_redirect("/activities.cgi");
287 return;
288 }
289 elsif ($op eq 'delete' && $id) {
290 $db->db_readwrite($dbh,
291 "DELETE FROM `${DB}`.activities WHERE id='$id' AND owner_user_id='$uid'",
292 $ENV{SCRIPT_NAME}, __LINE__);
293 $db->db_disconnect($dbh);
294 _act_redirect("/activities.cgi");
295 return;
296 }
297
298 $db->db_disconnect($dbh);
299 _act_redirect("/activities.cgi");
300 return;
301}