Diff -- /var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/preferences.cgi
Diff

/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/preferences.cgi

added on local at 2026-07-01 12:34:34

Added
+309
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to c754b7241fa8
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##################################################################################################################################
4# ______ ____ ____ ______ ______ ____ ___ __ ___ ______ _ __ ____ ____ __ __ _ __ _____ ____
5# / ____// __ \ / __ \ / ____/ / ____// __ \ / | / |/ // ____/| | / // __ \ / __ \ / //_/ | | / /|__ / / __ \
6# / / / / / // / / // __/ / /_ / /_/ // /| | / /|_/ // __/ | | /| / // / / // /_/ // ,< | | / / /_ < / / / /
7# / /___ / /_/ // /_/ // /___ / __/ / _, _// ___ | / / / // /___ | |/ |/ // /_/ // _, _// /| | | |/ / ___/ /_ / /_/ /
8# \____/ \____//_____//_____/ /_/ /_/ |_|/_/ |_|/_/ /_//_____/ |__/|__/ \____//_/ |_|/_/ |_| |___/ /____/(_)\____/
9#
10# - Written by: Shawn Pickering
11# - shawn@shawnpickering.com
12##################################################################################################################################
13use strict;
14use lib '/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com';
15
16
17#############################################################
18## Variables
19#############################################################
20my @EP_CATS = (
21 { key => 'lifecycle', label => 'Lifecycle emails', description => 'Trial reminders, plan changes, downgrade notices, winback. We recommend keeping these on.', default_on => 1 },
22 { key => 'product', label => 'Product updates', description => 'New features, release notes, what shipped this month. Once or twice a month.', default_on => 0 },
23 { key => 'marketing', label => 'Tips & marketing', description => 'Onboarding tips, customer stories, occasional promotions.', default_on => 0 },
24);
25
26
27
28############################################################
29# Modules
30############################################################
31#Load the CGI module
32use CGI;
33my $query = new CGI;
34my $form = $query->Vars;
35
36#Load the login module
37use MODS::Login;
38my $login = new MODS::Login;
39
40#Load Urls
41use MODS::PTMatrix::Urls;
42my $getlist = new MODS::PTMatrix::Urls;
43my $url = $getlist->urls();
44
45#Load the template module
46use MODS::Template;
47my $tfile = new MODS::Template;
48
49#Page Wrapper Module
50use MODS::PTMatrix::Wrapper;
51my $load = new MODS::PTMatrix::Wrapper;
52
53#Load the database connect module
54use MODS::DBConnect;
55my $db = new MODS::DBConnect;
56
57#Load the configuration file
58use MODS::PTMatrix::Config;
59my $config = new MODS::PTMatrix::Config;
60my $database_name = $config->settings('database_name');
61
62#Shared PM helpers
63use MODS::PTMatrix::PM;
64
65
66
67#############################################################
68## Get form data
69#############################################################
70#Escapes bad characters
71foreach my $line(keys %$form){
72 $form->{$line} =~ s/[^!-~\s]//g; #Remove non ascii chars
73 $form->{$line} = quotemeta("$form->{$line}");
74}
75
76
77
78############################################################
79# Program Controls
80############################################################
81$|=1;
82my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'preferences';
83my $userinfo = $login->login_verify();
84
85if($entry eq 'save_pref'){
86 print qq~Content-Type: application/json\n\n~;
87 if(!$userinfo){print '{"ok":false,"err":"not_auth"}'; exit;}
88 &save_ui_pref;
89 exit;
90}
91
92print qq~Content-type:text/html; Cache-Control:no-cache;\n\n~;
93if(!$userinfo){print qq~<script>window.location="$url->{login}";</script>~; exit;}
94
95if($entry eq 'email_preferences'){
96 if(($ENV{REQUEST_METHOD} || '') eq 'POST' && ($form->{act} || '') eq 'save'){&ep_save_prefs;}
97 else{&ep_prefs_page;}
98}else{
99 if(($ENV{REQUEST_METHOD}||'') eq 'POST'){&save_prefs;}
100 else{&prefs_page;}
101}
102
103
104############################################################
105# Subroutines
106############################################################
107sub save_prefs{
108 my $uid = MODS::PTMatrix::PM::get_user_id($userinfo);
109
110 #Validate everything against the allowlists - drop bad values to safe defaults
111 my $theme = ($form->{theme} =~ /^(dark|light|auto)$/) ? $form->{theme} : 'dark';
112 my $view = ($form->{default_view} =~ /^(kanban|gantt|list|calendar)$/)? $form->{default_view} : 'kanban';
113 my $n_assigned = $form->{email_notif_assigned} ? 1 : 0;
114 my $n_mentioned = $form->{email_notif_mentioned} ? 1 : 0;
115 my $n_status = $form->{email_notif_status} ? 1 : 0;
116 my $n_due_soon = $form->{email_notif_due_soon} ? 1 : 0;
117 my $n_weekly = $form->{email_notif_weekly} ? 1 : 0;
118 my $show_tips = $form->{show_tooltips} ? 1 : 0;
119 my $grad_mode = ($form->{card_gradient_mode} =~ /^(off|priority|assignee|custom)$/) ? $form->{card_gradient_mode} : 'priority';
120 my $grad_from = MODS::PTMatrix::PM::safe_color($form->{card_gradient_from} || '#5aa9ff');
121 my $grad_to = MODS::PTMatrix::PM::safe_color($form->{card_gradient_to} || '#1a2230');
122 my $density = ($form->{board_density} =~ /^(compact|comfortable|spacious)$/) ? $form->{board_density} : 'comfortable';
123
124 my $dbh = $db->db_connect();
125 my $sql = qq~INSERT INTO `${database_name}`.user_settings (user_id, theme, default_view, show_tooltips, email_notif_assigned, email_notif_mentioned, email_notif_status, email_notif_due_soon, email_notif_weekly, card_gradient_mode, card_gradient_from, card_gradient_to, board_density) VALUES ('$uid', '$theme', '$view', '$show_tips', '$n_assigned', '$n_mentioned', '$n_status', '$n_due_soon', '$n_weekly', '$grad_mode', '$grad_from', '$grad_to', '$density') ON DUPLICATE KEY UPDATE theme=VALUES(theme), default_view=VALUES(default_view), show_tooltips=VALUES(show_tooltips), email_notif_assigned=VALUES(email_notif_assigned), email_notif_mentioned=VALUES(email_notif_mentioned), email_notif_status=VALUES(email_notif_status), email_notif_due_soon=VALUES(email_notif_due_soon), email_notif_weekly=VALUES(email_notif_weekly), card_gradient_mode=VALUES(card_gradient_mode), card_gradient_from=VALUES(card_gradient_from), card_gradient_to=VALUES(card_gradient_to), board_density=VALUES(board_density)~;
126 $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__);
127 $db->db_disconnect($dbh);
128
129 &prefs_page('Preferences saved.');
130}
131
132sub prefs_page{
133 my($flash) = @_;
134 $flash ||= '';
135
136 my $uid = MODS::PTMatrix::PM::get_user_id($userinfo);
137 my $dbh = $db->db_connect();
138
139 #------------------------------------------------------------------------------------------------------
140 #Read the user's current settings row
141 my $sql = qq~SELECT * FROM `${database_name}`.user_settings WHERE user_id='$uid' LIMIT 1~;
142 my $s = $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__);
143 $s ||= {};
144 #------------------------------------------------------------------------------------------------------
145
146 $db->db_disconnect($dbh);
147
148 my $mode = $s->{card_gradient_mode} || 'priority';
149
150 #Create all template variables
151 my $tvars;
152 $tvars->{flash} = $flash;
153 $tvars->{has_flash} = length($flash) ? 1 : 0;
154 $tvars->{theme_dark} = ($s->{theme} || 'dark') eq 'dark' ? 1 : 0;
155 $tvars->{theme_light} = ($s->{theme} || '') eq 'light' ? 1 : 0;
156 $tvars->{theme_auto} = ($s->{theme} || '') eq 'auto' ? 1 : 0;
157 $tvars->{view_kanban} = ($s->{default_view} || 'kanban') eq 'kanban' ? 1 : 0;
158 $tvars->{view_gantt} = ($s->{default_view} || '') eq 'gantt' ? 1 : 0;
159 $tvars->{view_list} = ($s->{default_view} || '') eq 'list' ? 1 : 0;
160 $tvars->{view_cal} = ($s->{default_view} || '') eq 'calendar' ? 1 : 0;
161 $tvars->{n_assigned} = defined $s->{email_notif_assigned} ? ($s->{email_notif_assigned}+0) : 1;
162 $tvars->{n_mentioned} = defined $s->{email_notif_mentioned} ? ($s->{email_notif_mentioned}+0) : 1;
163 $tvars->{n_status} = defined $s->{email_notif_status} ? ($s->{email_notif_status}+0) : 1;
164 $tvars->{n_due_soon} = defined $s->{email_notif_due_soon} ? ($s->{email_notif_due_soon}+0) : 1;
165 $tvars->{n_weekly} = defined $s->{email_notif_weekly} ? ($s->{email_notif_weekly}+0) : 0;
166 $tvars->{show_tips} = defined $s->{show_tooltips} ? ($s->{show_tooltips}+0) : 1;
167 $tvars->{grad_off} = $mode eq 'off' ? 1 : 0;
168 $tvars->{grad_priority} = $mode eq 'priority' ? 1 : 0;
169 $tvars->{grad_assignee} = $mode eq 'assignee' ? 1 : 0;
170 $tvars->{grad_custom} = $mode eq 'custom' ? 1 : 0;
171 $tvars->{grad_from} = $s->{card_gradient_from} || '#5aa9ff';
172 $tvars->{grad_to} = $s->{card_gradient_to} || '#1a2230';
173 $tvars->{den_compact} = ($s->{board_density}||'comfortable') eq 'compact' ? 1 : 0;
174 $tvars->{den_comfortable} = ($s->{board_density}||'comfortable') eq 'comfortable' ? 1 : 0;
175 $tvars->{den_spacious} = ($s->{board_density}||'comfortable') eq 'spacious' ? 1 : 0;
176
177 my $body = join('', $tfile->template('tf_preferences.html', $tvars, $userinfo));
178
179 $load->render({
180 userinfo => $userinfo,
181 page_key => 'preferences',
182 title => 'Preferences',
183 body => $body,
184 });
185}
186
187#============================================================
188# email_preferences entry: per-category email opt-outs
189#============================================================
190sub ep_save_prefs{
191 my $uid = MODS::PTMatrix::PM::get_user_id($userinfo);
192 my $dbh = $db->db_connect();
193
194 foreach my $c(@EP_CATS){
195 my $sub = $form->{"pref_$c->{key}"} ? 1 : 0;
196 my $sql = qq~INSERT INTO `${database_name}`.user_email_preferences (user_id, category, is_subscribed) VALUES ('$uid', '$c->{key}', '$sub') ON DUPLICATE KEY UPDATE is_subscribed='$sub'~;
197 $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__);
198 }
199
200 $db->db_disconnect($dbh);
201 &ep_prefs_page('Preferences saved.');
202}
203
204sub ep_prefs_page{
205 my($flash) = @_;
206 $flash ||= '';
207
208 my $uid = MODS::PTMatrix::PM::get_user_id($userinfo);
209 my $dbh = $db->db_connect();
210
211 my $sql_prefs = qq~SELECT category, is_subscribed FROM `${database_name}`.user_email_preferences WHERE user_id='$uid'~;
212 my @rows = $db->db_readwrite_multiple($dbh, $sql_prefs, $ENV{SCRIPT_NAME}, __LINE__);
213 my %prefs;
214 foreach my $r(@rows){$prefs{$r->{category}} = $r->{is_subscribed} ? 1 : 0;}
215
216 my $sql_recent = qq~SELECT template_slug, subject, status, created_at FROM `${database_name}`.email_sends_log WHERE user_id='$uid' ORDER BY id DESC LIMIT 10~;
217 my @recent = $db->db_readwrite_multiple($dbh, $sql_recent, $ENV{SCRIPT_NAME}, __LINE__);
218
219 $db->db_disconnect($dbh);
220
221 my $flash_html = $flash ? qq~<div style="padding:10px 14px;background:rgba(34,197,94,.12);border:1px solid rgba(34,197,94,.3);border-radius:8px;color:#86efac;font-size:13px;margin-bottom:18px">$flash</div>~ : '';
222
223 my $rows_html = '';
224 foreach my $c(@EP_CATS){
225 my $sub = defined($prefs{$c->{key}}) ? $prefs{$c->{key}} : $c->{default_on};
226 my $checked = $sub ? 'checked' : '';
227 $rows_html .= qq~
228 <label style="display:flex;align-items:flex-start;gap:14px;padding:16px;border:1px solid rgba(255,255,255,.08);border-radius:10px;margin-bottom:10px;cursor:pointer;background:rgba(20,20,30,.4)">
229 <input type="checkbox" name="pref_$c->{key}" value="1" $checked style="margin-top:3px;width:18px;height:18px">
230 <div>
231 <div style="font-weight:600;color:#e2e8f0;font-size:14px">$c->{label}</div>
232 <div style="color:#94a3b8;font-size:13px;line-height:1.5;margin-top:4px">$c->{description}</div>
233 </div>
234 </label>~;
235 }
236
237 my $recent_html = '';
238 foreach my $r(@recent){
239 my $color = $r->{status} eq 'sent' ? '#86efac' : ($r->{status} =~ /^suppressed/ ? '#fbbf24' : '#fca5a5');
240 my $sj = MODS::PTMatrix::PM::h($r->{subject});
241 $recent_html .= qq~
242 <div style="display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.05);font-size:12px">
243 <div><span style="color:#cbd5e1">$sj</span><div style="color:#64748b;font-size:11px;font-family:ui-monospace,Consolas,Menlo,monospace">$r->{template_slug}</div></div>
244 <div><span style="color:$color;font-size:11px;text-transform:uppercase;letter-spacing:.05em">$r->{status}</span><div style="color:#94a3b8;font-size:11px;text-align:right">$r->{created_at}</div></div>
245 </div>~;
246 }
247
248 my $body = qq~
249<div class="page-pad" style="max-width:720px">
250 <h1 style="margin:0 0 6px;font-size:28px;color:#fff">Email preferences</h1>
251 <p style="color:#94a3b8;margin:0 0 24px;font-size:14px">Choose which emails you'd like to receive from PTMatrix. Transactional emails (password resets, invite acceptance, billing receipts) are always sent regardless.</p>
252 $flash_html
253
254 <form method="POST" action="$url->{email_preferences}">
255 <input type="hidden" name="act" value="save">
256 $rows_html
257 <div style="margin-top:18px"><button type="submit" class="btn btn-primary btn-lg">Save preferences</button></div>
258 </form>
259
260 <h2 style="font-size:16px;color:#fff;margin:32px 0 12px">Recent emails sent to you</h2>
261 <div style="border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:8px 16px;background:rgba(20,20,30,.4)">$recent_html</div>
262</div>~;
263
264 $load->render({
265 userinfo => $userinfo,
266 page_key => 'email_preferences',
267 title => 'Email preferences',
268 body => $body,
269 });
270}
271
272#============================================================
273# save_pref entry: AJAX UI-pref JSON endpoint
274#============================================================
275sub save_ui_pref{
276 my $uid = ($userinfo->{user_id} || 0) + 0;
277 my $dbh = $db->db_connect();
278 if(!$dbh){print '{"ok":false,"err":"db"}'; return;}
279
280 my $has_mob_col = $db->db_readwrite($dbh, qq~SELECT COUNT(*) AS n FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '${database_name}' AND TABLE_NAME = 'user_settings' AND COLUMN_NAME = 'sidebar_mobile_open'~, $ENV{SCRIPT_NAME}, __LINE__);
281 my $mobile_col_ready = ($has_mob_col && $has_mob_col->{n}) ? 1 : 0;
282
283 my @insert_cols = ('user_id');
284 my @insert_vals = ("'$uid'");
285 my @update_clause;
286
287 if(exists $form->{sidebar_collapsed}){
288 my $v = ($form->{sidebar_collapsed} || 0) ? 1 : 0;
289 push @insert_cols, 'sidebar_collapsed';
290 push @insert_vals, "'$v'";
291 push @update_clause, "sidebar_collapsed='$v'";
292 }
293 if($mobile_col_ready && exists $form->{sidebar_mobile_open}){
294 my $v = ($form->{sidebar_mobile_open} || 0) ? 1 : 0;
295 push @insert_cols, 'sidebar_mobile_open';
296 push @insert_vals, "'$v'";
297 push @update_clause, "sidebar_mobile_open='$v'";
298 }
299
300 if(@update_clause){
301 my $cols_sql = join(',', @insert_cols);
302 my $vals_sql = join(',', @insert_vals);
303 my $update_sql = join(',', @update_clause);
304 my $sql = qq~INSERT INTO `${database_name}`.user_settings ($cols_sql) VALUES ($vals_sql) ON DUPLICATE KEY UPDATE $update_sql~;
305 $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__);
306 }
307 $db->db_disconnect($dbh);
308 print '{"ok":true}';
309}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help