added on local at 2026-07-01 12:34:07
| 1 | #!/usr/bin/perl | |
| 2 | ||
| 3 | ################################################################################################################################## | |
| 4 | # ______ ____ ____ ______ ______ ____ ___ __ ___ ______ _ __ ____ ____ __ __ _ __ _____ ____ | |
| 5 | # / ____// __ \ / __ \ / ____/ / ____// __ \ / | / |/ // ____/| | / // __ \ / __ \ / //_/ | | / /|__ / / __ \ | |
| 6 | # / / / / / // / / // __/ / /_ / /_/ // /| | / /|_/ // __/ | | /| / // / / // /_/ // ,< | | / / /_ < / / / / | |
| 7 | # / /___ / /_/ // /_/ // /___ / __/ / _, _// ___ | / / / // /___ | |/ |/ // /_/ // _, _// /| | | |/ / ___/ /_ / /_/ / | |
| 8 | # \____/ \____//_____//_____/ /_/ /_/ |_|/_/ |_|/_/ /_//_____/ |__/|__/ \____//_/ |_|/_/ |_| |___/ /____/(_)\____/ | |
| 9 | # | |
| 10 | # - Written by: Shawn Pickering | |
| 11 | # - shawn@shawnpickering.com | |
| 12 | ################################################################################################################################## | |
| 13 | use strict; | |
| 14 | use lib '/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com'; | |
| 15 | ||
| 16 | ||
| 17 | ############################################################# | |
| 18 | ## Variables | |
| 19 | ############################################################# | |
| 20 | ||
| 21 | ||
| 22 | ||
| 23 | ############################################################ | |
| 24 | # Modules | |
| 25 | ############################################################ | |
| 26 | #Load the CGI module | |
| 27 | use CGI; | |
| 28 | my $query = new CGI; | |
| 29 | my $form = $query->Vars; | |
| 30 | ||
| 31 | #Load the login module | |
| 32 | use MODS::Login; | |
| 33 | my $login = new MODS::Login; | |
| 34 | ||
| 35 | #Load Urls | |
| 36 | use MODS::PTMatrix::Urls; | |
| 37 | my $getlist = new MODS::PTMatrix::Urls; | |
| 38 | my $url = $getlist->urls(); | |
| 39 | ||
| 40 | #Load the template module | |
| 41 | use MODS::Template; | |
| 42 | my $tfile = new MODS::Template; | |
| 43 | ||
| 44 | #Page Wrapper Module | |
| 45 | use MODS::PTMatrix::Wrapper; | |
| 46 | my $load = new MODS::PTMatrix::Wrapper; | |
| 47 | ||
| 48 | #Load the database connect module | |
| 49 | use MODS::DBConnect; | |
| 50 | my $db = new MODS::DBConnect; | |
| 51 | ||
| 52 | #Load the configuration file | |
| 53 | use MODS::PTMatrix::Config; | |
| 54 | my $config = new MODS::PTMatrix::Config; | |
| 55 | my $database_name = $config->settings('database_name'); | |
| 56 | ||
| 57 | #Shared PM helpers | |
| 58 | use MODS::PTMatrix::PM; | |
| 59 | ||
| 60 | #For grid generation | |
| 61 | use Time::Local; | |
| 62 | ||
| 63 | #For day-data payload sent to the click-to-overlay handler | |
| 64 | use JSON::PP qw(encode_json); | |
| 65 | ||
| 66 | ||
| 67 | ||
| 68 | ############################################################# | |
| 69 | ## Get form data | |
| 70 | ############################################################# | |
| 71 | #Escapes bad characters | |
| 72 | foreach my $line(keys %$form){ | |
| 73 | $form->{$line} =~ s/[^!-~\s]//g; #Remove non ascii chars | |
| 74 | $form->{$line} = quotemeta("$form->{$line}"); | |
| 75 | } | |
| 76 | ||
| 77 | ||
| 78 | ||
| 79 | ############################################################ | |
| 80 | # Program Controls | |
| 81 | ############################################################ | |
| 82 | $|=1; | |
| 83 | my $userinfo = $login->login_verify(); | |
| 84 | print qq~Content-type:text/html; Cache-Control:no-cache;\n\n~; | |
| 85 | if(!$userinfo){print qq~<script>window.location="$url->{login}";</script>~;} | |
| 86 | else{&calendar_view;} | |
| 87 | ||
| 88 | ||
| 89 | ############################################################ | |
| 90 | # Subroutines | |
| 91 | ############################################################ | |
| 92 | sub calendar_view{ | |
| 93 | my $uid = MODS::PTMatrix::PM::get_user_id($userinfo); | |
| 94 | my $cid = MODS::PTMatrix::PM::get_company_id($userinfo); | |
| 95 | ||
| 96 | #Current month / year - default to today, override via ?y= / ?m= | |
| 97 | my @now = localtime(time); | |
| 98 | my $y = $form->{y} || ($now[5] + 1900); $y += 0; $y = 2026 if $y < 1970 || $y > 9999; | |
| 99 | my $m = $form->{m} || ($now[4] + 1); $m += 0; $m = 1 if $m < 1 || $m > 12; | |
| 100 | ||
| 101 | #Scope = me / team / dept / all - coerced down to what this user actually has access to | |
| 102 | my $raw_scope = $form->{scope} || 'me'; | |
| 103 | my $st = MODS::PTMatrix::PM::user_scope_tier($userinfo); | |
| 104 | $raw_scope = 'me' unless $raw_scope =~ /^(me|team|dept|all)$/; | |
| 105 | $raw_scope = 'me' if $raw_scope eq 'team' && !$st->{has_team}; | |
| 106 | $raw_scope = 'team' if $raw_scope eq 'dept' && !$st->{has_department}; | |
| 107 | $raw_scope = 'me' if $raw_scope eq 'dept' && !$st->{has_team}; #safety net | |
| 108 | $raw_scope = 'me' if $raw_scope eq 'all' && !$st->{can_company}; | |
| 109 | my $scope = $raw_scope; | |
| 110 | my @user_dept_ids = @{ $st->{team_dept_ids} }; | |
| 111 | my @user_dept_ids_full = @{ $st->{dept_dept_ids} }; | |
| 112 | my $has_team = $st->{has_team}; | |
| 113 | my $has_department = $st->{has_department}; | |
| 114 | my $is_mgr = $st->{can_company}; | |
| 115 | ||
| 116 | #Walk one month forward / back for the prev / next links | |
| 117 | my $first = sprintf("%04d-%02d-01", $y, $m); | |
| 118 | my $next_t = timegm(0,0,12, 1, $m-1, $y-1900) + 32*86400; | |
| 119 | my @nx = gmtime($next_t); | |
| 120 | my ($ny, $nm) = ($nx[5]+1900, $nx[4]+1); | |
| 121 | my $next_first = sprintf("%04d-%02d-01", $ny, $nm); | |
| 122 | ||
| 123 | my $prev_t = timegm(0,0,12, 1, $m-1, $y-1900) - 2*86400; | |
| 124 | my @pv = gmtime($prev_t); | |
| 125 | my ($py, $pm) = ($pv[5]+1900, $pv[4]+1); | |
| 126 | ||
| 127 | #Pick the SQL WHERE clause to match the chosen scope | |
| 128 | my $where_me; | |
| 129 | if($scope eq 'me'){ | |
| 130 | $where_me = "AND (t.assigned_user_id='$uid' OR t.reporter_user_id='$uid')"; | |
| 131 | }elsif($scope eq 'team' || $scope eq 'dept'){ | |
| 132 | my @ids = ($scope eq 'dept') ? @user_dept_ids_full : @user_dept_ids; | |
| 133 | if(scalar @ids){ | |
| 134 | my $dept_ids = join(',', map { "'$_'" } @ids); | |
| 135 | $where_me = "AND (t.assigned_department_id IN ($dept_ids) OR t.assigned_user_id IN (SELECT user_id FROM `${database_name}`.department_members WHERE department_id IN ($dept_ids)))"; | |
| 136 | }else{ | |
| 137 | $where_me = "AND 1=0"; | |
| 138 | } | |
| 139 | }else{ | |
| 140 | $where_me = ''; #scope=all | |
| 141 | } | |
| 142 | ||
| 143 | my $dbh = $db->db_connect(); | |
| 144 | ||
| 145 | #------------------------------------------------------------------------------------------------------ | |
| 146 | #Tasks due within the month window | |
| 147 | my $sql_t = qq~SELECT t.id, t.title, t.priority, t.status, t.due_date, t.start_date, p.id AS project_id, p.name AS project_name, p.key_prefix, p.color AS project_color FROM `${database_name}`.tasks t JOIN `${database_name}`.projects p ON p.id=t.project_id WHERE t.company_id='$cid' AND t.due_date BETWEEN '$first' AND '$next_first' $where_me ORDER BY t.due_date, FIELD(t.priority,'critical','urgent','high','medium','low')~; | |
| 148 | my @tasks = $db->db_readwrite_multiple($dbh, $sql_t, $ENV{SCRIPT_NAME}, __LINE__); | |
| 149 | #------------------------------------------------------------------------------------------------------ | |
| 150 | ||
| 151 | #------------------------------------------------------------------------------------------------------ | |
| 152 | #Projects for the "add task" dropdown (only ones still active) | |
| 153 | my $sql_p = qq~SELECT id, name FROM `${database_name}`.projects WHERE company_id='$cid' AND deleted_at IS NULL AND status NOT IN ('archived','cancelled','complete') ORDER BY name ASC~; | |
| 154 | my @projects = $db->db_readwrite_multiple($dbh, $sql_p, $ENV{SCRIPT_NAME}, __LINE__); | |
| 155 | foreach my $p(@projects){$p->{name} = MODS::PTMatrix::PM::h($p->{name});} | |
| 156 | #------------------------------------------------------------------------------------------------------ | |
| 157 | ||
| 158 | $db->db_disconnect($dbh); | |
| 159 | ||
| 160 | #Bucket tasks by their due_date | |
| 161 | my %tasks_by_day; | |
| 162 | foreach my $t(@tasks){ | |
| 163 | $t->{title} = MODS::PTMatrix::PM::h($t->{title}); | |
| 164 | $t->{project_name} = MODS::PTMatrix::PM::h($t->{project_name} || ''); | |
| 165 | $t->{project_color} ||= '#5aa9ff'; | |
| 166 | push @{$tasks_by_day{$t->{due_date}}}, $t; | |
| 167 | } | |
| 168 | ||
| 169 | #Build the 42-cell 6-row grid starting at the previous Sunday | |
| 170 | my $first_t = timegm(0,0,12, 1, $m-1, $y-1900); | |
| 171 | my @ft = gmtime($first_t); | |
| 172 | my $dow_first = $ft[6]; | |
| 173 | my $grid_start = $first_t - $dow_first * 86400; | |
| 174 | ||
| 175 | my @cells; | |
| 176 | my %cells_data; | |
| 177 | my $today = sprintf("%04d-%02d-%02d", $now[5]+1900, $now[4]+1, $now[3]); | |
| 178 | my $month_label = ('January','February','March','April','May','June','July','August','September','October','November','December')[$m-1]; | |
| 179 | my $cur_d = $grid_start; | |
| 180 | for(my $i=0; $i<42; $i++){ | |
| 181 | my @x = gmtime($cur_d); | |
| 182 | my $d_str = sprintf("%04d-%02d-%02d", $x[5]+1900, $x[4]+1, $x[3]); | |
| 183 | my $in_month = ($x[4]+1 == $m) ? 1 : 0; | |
| 184 | my $is_today = ($d_str eq $today) ? 1 : 0; | |
| 185 | my $is_weekend = ($x[6] == 0 || $x[6] == 6) ? 1 : 0; | |
| 186 | my $tlist = $tasks_by_day{$d_str} || []; | |
| 187 | my @top = @$tlist > 2 ? @{$tlist}[0..1] : @$tlist; | |
| 188 | my $more = @$tlist > 2 ? scalar(@$tlist) - 2 : 0; | |
| 189 | push @cells, { | |
| 190 | d_str => $d_str, | |
| 191 | day_num => $x[3], | |
| 192 | in_month => $in_month, | |
| 193 | is_today => $is_today, | |
| 194 | is_weekend => $is_weekend, | |
| 195 | tasks_top => \@top, | |
| 196 | more_count => $more, | |
| 197 | has_tasks => scalar(@$tlist) ? 1 : 0, | |
| 198 | }; | |
| 199 | $cells_data{$d_str} = { tasks => $tlist }; | |
| 200 | $cur_d += 86400; | |
| 201 | } | |
| 202 | ||
| 203 | my $cells_json = encode_json(\%cells_data); | |
| 204 | ||
| 205 | #Create all template variables | |
| 206 | my $tvars; | |
| 207 | $tvars->{y} = $y; | |
| 208 | $tvars->{m} = $m; | |
| 209 | $tvars->{month_label} = $month_label; | |
| 210 | $tvars->{py} = $py; | |
| 211 | $tvars->{pm} = $pm; | |
| 212 | $tvars->{ny} = $ny; | |
| 213 | $tvars->{nm} = $nm; | |
| 214 | $tvars->{cells} = \@cells; | |
| 215 | $tvars->{scope_me} = ($scope eq 'me') ? 1 : 0; | |
| 216 | $tvars->{scope_team} = ($scope eq 'team') ? 1 : 0; | |
| 217 | $tvars->{scope_dept} = ($scope eq 'dept') ? 1 : 0; | |
| 218 | $tvars->{scope_all} = ($scope eq 'all') ? 1 : 0; | |
| 219 | $tvars->{has_department} = $has_department; | |
| 220 | $tvars->{has_team} = $has_team; | |
| 221 | $tvars->{is_mgr} = $is_mgr; | |
| 222 | $tvars->{projects} = \@projects; | |
| 223 | $tvars->{cells_json} = $cells_json; | |
| 224 | ||
| 225 | my $body = join('', $tfile->template('tf_calendar.html', $tvars, $userinfo)); | |
| 226 | ||
| 227 | $load->render({ | |
| 228 | userinfo => $userinfo, | |
| 229 | page_key => 'calendar', | |
| 230 | title => 'Calendar', | |
| 231 | body => $body, | |
| 232 | extra_js => [ | |
| 233 | '/assets/javascript/kanban.js?v=20260610d', | |
| 234 | '/assets/javascript/kanban-drawer.js?v=20260607a', | |
| 235 | ], | |
| 236 | }); | |
| 237 | } |