added on local at 2026-07-01 13:47:17
| 1 | #!/usr/bin/perl | |
| 2 | #====================================================================== | |
| 3 | # AffSoft - User calendar (personal scheduling) | |
| 4 | #====================================================================== | |
| 5 | use strict; use warnings; | |
| 6 | use lib '/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com'; | |
| 7 | use CGI; | |
| 8 | use Time::Local; | |
| 9 | use JSON::PP qw(encode_json); | |
| 10 | use MODS::Template; | |
| 11 | use MODS::DBConnect; | |
| 12 | use MODS::Login; | |
| 13 | use MODS::AffSoft::Config; | |
| 14 | use MODS::AffSoft::Wrapper; | |
| 15 | ||
| 16 | my $q = CGI->new; | |
| 17 | my $form = $q->Vars; | |
| 18 | my $auth = MODS::Login->new; | |
| 19 | my $wrap = MODS::AffSoft::Wrapper->new; | |
| 20 | my $tfile = MODS::Template->new; | |
| 21 | my $db = MODS::DBConnect->new; | |
| 22 | my $cfg = MODS::AffSoft::Config->new; | |
| 23 | my $DB = $cfg->settings('database_name'); | |
| 24 | ||
| 25 | my $userinfo = $auth->login_verify(); | |
| 26 | if (!$userinfo) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; } | |
| 27 | my $uid = $userinfo->{user_id} || 0; | |
| 28 | $uid =~ s/[^0-9]//g; | |
| 29 | ||
| 30 | my $dbh = $db->db_connect(); | |
| 31 | ||
| 32 | # ----- POST: create or delete events ----- | |
| 33 | if (($ENV{REQUEST_METHOD} || '') eq 'POST') { | |
| 34 | my $act = $form->{act} || ''; | |
| 35 | if ($act eq 'create') { | |
| 36 | my $title = $form->{title} || ''; | |
| 37 | $title =~ s/^\s+|\s+$//g; | |
| 38 | $title = substr($title, 0, 280); | |
| 39 | my $kind = $form->{kind} || ''; | |
| 40 | $kind = '' unless $kind =~ /^[a-z_]{1,40}$/; | |
| 41 | my $date = $form->{event_date} || ''; | |
| 42 | $date = '' unless $date =~ /^\d{4}-\d{2}-\d{2}$/; | |
| 43 | my $notes = $form->{notes} || ''; | |
| 44 | $notes = substr($notes, 0, 1000); | |
| 45 | if (length($title) && length($date)) { | |
| 46 | my $t_sql = $title; $t_sql =~ s/'/\\'/g; | |
| 47 | my $n_sql = $notes; $n_sql =~ s/'/\\'/g; | |
| 48 | $db->db_readwrite($dbh, qq~ | |
| 49 | INSERT INTO `${DB}`.calendar_events | |
| 50 | SET user_id='$uid', event_date='$date', | |
| 51 | title='$t_sql', kind='$kind', notes='$n_sql' | |
| 52 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 53 | } | |
| 54 | } elsif ($act eq 'delete') { | |
| 55 | my $eid = $form->{event_id} || 0; | |
| 56 | $eid =~ s/[^0-9]//g; | |
| 57 | if ($eid) { | |
| 58 | $db->db_readwrite($dbh, qq~ | |
| 59 | DELETE FROM `${DB}`.calendar_events WHERE id='$eid' AND user_id='$uid' | |
| 60 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 61 | } | |
| 62 | } | |
| 63 | my $back = $form->{back} || '/calendar.cgi'; | |
| 64 | print "Status: 302 Found\nLocation: $back\n\n"; exit; | |
| 65 | } | |
| 66 | ||
| 67 | # ----- Month nav ----- | |
| 68 | my @now = localtime(time); | |
| 69 | my $y = $form->{y} || ($now[5] + 1900); $y += 0; $y = 2026 if $y < 1970 || $y > 9999; | |
| 70 | my $m = $form->{m} || ($now[4] + 1); $m += 0; $m = 1 if $m < 1 || $m > 12; | |
| 71 | ||
| 72 | my $first = sprintf("%04d-%02d-01", $y, $m); | |
| 73 | my $next_t = timegm(0,0,12, 1, $m-1, $y-1900) + 32*86400; | |
| 74 | my @nx = gmtime($next_t); | |
| 75 | my ($ny, $nm) = ($nx[5]+1900, $nx[4]+1); | |
| 76 | my $next_first = sprintf("%04d-%02d-01", $ny, $nm); | |
| 77 | ||
| 78 | my $prev_t = timegm(0,0,12, 1, $m-1, $y-1900) - 2*86400; | |
| 79 | my @pv = gmtime($prev_t); | |
| 80 | my ($py, $pm) = ($pv[5]+1900, $pv[4]+1); | |
| 81 | ||
| 82 | my $raw_scope = $form->{scope} || 'me'; | |
| 83 | $raw_scope = 'me' unless $raw_scope eq 'me' || $raw_scope eq 'my_team' || $raw_scope eq 'division' || $raw_scope eq 'team'; | |
| 84 | my $is_admin = ($userinfo->{is_admin} || $userinfo->{is_super_admin}) ? 1 : 0; | |
| 85 | $raw_scope = 'me' if $raw_scope eq 'team' && !$is_admin; | |
| 86 | my $user_team_id = 0; | |
| 87 | { | |
| 88 | my $dbh_t = $db->db_connect(); | |
| 89 | my $r = $db->db_readwrite($dbh_t, qq~SELECT team_id FROM `${DB}`.users WHERE id='$uid' LIMIT 1~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 90 | $user_team_id = $r && $r->{team_id} ? $r->{team_id} + 0 : 0; | |
| 91 | $db->db_disconnect($dbh_t); | |
| 92 | } | |
| 93 | my $has_my_team = $user_team_id ? 1 : 0; | |
| 94 | $raw_scope = 'me' if $raw_scope eq 'my_team' && !$has_my_team; | |
| 95 | my @div_team_ids = ($user_team_id); | |
| 96 | my $has_division = 0; | |
| 97 | if ($user_team_id) { | |
| 98 | my $dbh_div = $db->db_connect(); | |
| 99 | my @frontier = ($user_team_id); | |
| 100 | my %seen; $seen{$user_team_id} = 1; | |
| 101 | for (my $i=0; $i<5; $i++) { | |
| 102 | last unless @frontier; | |
| 103 | my $in = join(',', map { "'$_'" } @frontier); | |
| 104 | my @kids = $db->db_readwrite_multiple($dbh_div, qq~SELECT id FROM `${DB}`.teams WHERE parent_team_id IN ($in)~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 105 | @frontier = (); | |
| 106 | foreach my $k (@kids) { next if $seen{$k->{id}}; $seen{$k->{id}}=1; push @frontier, $k->{id}; } | |
| 107 | } | |
| 108 | @div_team_ids = keys %seen; | |
| 109 | $has_division = (scalar(@div_team_ids) > 1) ? 1 : 0; # only show chip if cascade adds something | |
| 110 | $db->db_disconnect($dbh_div); | |
| 111 | } | |
| 112 | $raw_scope = 'me' if $raw_scope eq 'division' && !$has_division; | |
| 113 | my $scope = $raw_scope; | |
| 114 | ||
| 115 | my $div_in = scalar(@div_team_ids) ? join(',', map { "'$_'" } @div_team_ids) : "''"; | |
| 116 | # ----- Fetch events ----- | |
| 117 | my @events = $db->db_readwrite_multiple($dbh, qq~ | |
| 118 | SELECT id, event_date, title, kind, notes, color | |
| 119 | FROM `${DB}`.calendar_events | |
| 120 | WHERE ( | |
| 121 | CASE WHEN '$scope'='team' THEN | |
| 122 | user_id IN (SELECT id FROM `${DB}`.users WHERE is_admin=1) | |
| 123 | WHEN '$scope'='my_team' THEN | |
| 124 | user_id IN (SELECT id FROM `${DB}`.users WHERE team_id='$user_team_id') | |
| 125 | WHEN '$scope'='division' THEN | |
| 126 | user_id IN (SELECT id FROM `${DB}`.users WHERE team_id IN ($div_in)) | |
| 127 | ELSE user_id='$uid' END | |
| 128 | ) AND event_date BETWEEN '$first' AND '$next_first' | |
| 129 | ORDER BY event_date ASC, id ASC | |
| 130 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 131 | ||
| 132 | $db->db_disconnect($dbh); | |
| 133 | ||
| 134 | my %events_by_day; | |
| 135 | foreach my $e (@events) { | |
| 136 | $e->{title} = _h($e->{title}); | |
| 137 | $e->{kind} = _h($e->{kind} || ''); | |
| 138 | $e->{color} ||= '#5aa9ff'; | |
| 139 | push @{$events_by_day{$e->{event_date}}}, $e; | |
| 140 | } | |
| 141 | ||
| 142 | # ----- Build grid ----- | |
| 143 | my $first_t = timegm(0,0,12, 1, $m-1, $y-1900); | |
| 144 | my @ft = gmtime($first_t); | |
| 145 | my $dow_first = $ft[6]; | |
| 146 | my $grid_start = $first_t - $dow_first * 86400; | |
| 147 | ||
| 148 | my @cells; | |
| 149 | my %cells_data; | |
| 150 | my $today = sprintf("%04d-%02d-%02d", $now[5]+1900, $now[4]+1, $now[3]); | |
| 151 | my $month_label = ('January','February','March','April','May','June','July','August','September','October','November','December')[$m-1]; | |
| 152 | my $cur_d = $grid_start; | |
| 153 | for (my $i=0; $i<42; $i++) { | |
| 154 | my @x = gmtime($cur_d); | |
| 155 | my $d_str = sprintf("%04d-%02d-%02d", $x[5]+1900, $x[4]+1, $x[3]); | |
| 156 | my $in_month = ($x[4]+1 == $m) ? 1 : 0; | |
| 157 | my $is_today = ($d_str eq $today) ? 1 : 0; | |
| 158 | my $is_weekend = ($x[6] == 0 || $x[6] == 6) ? 1 : 0; | |
| 159 | my $evts = $events_by_day{$d_str} || []; | |
| 160 | my @top = @$evts > 2 ? @{$evts}[0..1] : @$evts; | |
| 161 | my $more = @$evts > 2 ? scalar(@$evts) - 2 : 0; | |
| 162 | push @cells, { | |
| 163 | d_str => $d_str, | |
| 164 | day_num => $x[3], | |
| 165 | in_month => $in_month, | |
| 166 | is_today => $is_today, | |
| 167 | is_weekend => $is_weekend, | |
| 168 | events_top => \@top, | |
| 169 | more_count => $more, | |
| 170 | has_events => scalar(@$evts) ? 1 : 0, | |
| 171 | }; | |
| 172 | $cells_data{$d_str} = { events => $evts }; | |
| 173 | $cur_d += 86400; | |
| 174 | } | |
| 175 | ||
| 176 | my $cells_json = encode_json(\%cells_data); | |
| 177 | ||
| 178 | print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store\n\n"; | |
| 179 | my $tvars = { | |
| 180 | scope => $scope, | |
| 181 | scope_me => ($scope eq 'me') ? 1 : 0, | |
| 182 | scope_team => ($scope eq 'team') ? 1 : 0, | |
| 183 | scope_my_team => ($scope eq 'my_team') ? 1 : 0, | |
| 184 | has_my_team => $has_my_team, | |
| 185 | scope_division => ($scope eq 'division') ? 1 : 0, | |
| 186 | has_division => $has_division, | |
| 187 | is_admin => $is_admin, | |
| 188 | y => $y, m => $m, month_label => $month_label, | |
| 189 | py => $py, pm => $pm, ny => $ny, nm => $nm, | |
| 190 | cells => \@cells, | |
| 191 | cells_json => $cells_json, | |
| 192 | }; | |
| 193 | my $body = join('', $tfile->template('affsoft_calendar.html', $tvars, $userinfo)); | |
| 194 | $wrap->render({ | |
| 195 | userinfo => $userinfo, | |
| 196 | page_key => 'calendar', | |
| 197 | title => 'Calendar', | |
| 198 | body => $body, | |
| 199 | }); | |
| 200 | ||
| 201 | sub _h { | |
| 202 | my $s = shift; | |
| 203 | return '' unless defined $s; | |
| 204 | $s =~ s/&/&/g; | |
| 205 | $s =~ s/</</g; | |
| 206 | $s =~ s/>/>/g; | |
| 207 | $s =~ s/"/"/g; | |
| 208 | return $s; | |
| 209 | } |