Diff -- /var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/TEMPLATES/tf_project.html
Diff
/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/TEMPLATES/tf_project.html
added on local at 2026-07-01 12:35:05
Added
+255
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 13a6e0480acb
to 13a6e0480acb
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.| 1 | <div class="crumbs"><a href="/projects.cgi">Projects</a><span class="sep">/</span>$proj_name</div> | |
| 2 | <div class="page-header"> | |
| 3 | <div> | |
| 4 | <h1 style="display:flex;align-items:center;gap:12px"><span style="width:14px;height:14px;border-radius:4px;background:$proj_color"></span>$proj_name <span class="badge" style="font-size:12px">$proj_key</span></h1> | |
| 5 | <p class="subtitle">$proj_description</p> | |
| 6 | </div> | |
| 7 | <div class="flex-end"> | |
| 8 | <a href="/kanban.cgi?project=$proj_id" class="btn" data-tip="Open the **drag-and-drop Kanban board** for this project.">Kanban</a> | |
| 9 | <a href="/gantt.cgi?project=$proj_id" class="btn" data-tip="Open the **Gantt timeline**. Drag a bar to reschedule a task, drag its edges to resize.">Gantt</a> | |
| 10 | <a href="/decisions.cgi?project=$proj_id" class="btn btn-ghost" data-tip="First-class decision log: why you chose what you chose.">Decisions</a> | |
| 11 | <a href="/risks.cgi?project=$proj_id" class="btn btn-ghost" data-tip="Risk register with severity × likelihood heat matrix.">Risks</a> | |
| 12 | <a href="/tasks_export.cgi?project=$proj_id" class="btn btn-ghost" data-tip="Download every task in this project as CSV.">Export CSV</a> | |
| 13 | [if:$can_edit]<a href="/tasks_import.cgi?project=$proj_id" class="btn btn-ghost" data-tip="Bulk-create tasks from a CSV. Required column: title.">Import CSV</a>[/if] | |
| 14 | [if:$can_edit]<button onclick="document.getElementById('editProjectModal').classList.add('open')" class="btn btn-ghost" data-tip="Edit this project's name, description, color, status, priority, dates, and visibility.">Edit project</button>[/if] | |
| 15 | <button onclick="document.getElementById('newTaskModal').classList.add('open')" class="btn btn-primary" data-tip="Add a new task to this project. Auto-gets the next ticket number (e.g. `MOV-7`).">+ New task</button> | |
| 16 | </div> | |
| 17 | </div> | |
| 18 | ||
| 19 | [if:$show_created]<div class="flash-msg">Project created. Add your first task to get going.</div>[/if] | |
| 20 | ||
| 21 | <div class="kpi-grid"> | |
| 22 | <div class="kpi"><div class="kpi-label">Total tasks</div><div class="kpi-value">$counts.total</div></div> | |
| 23 | <div class="kpi success"><div class="kpi-label">Done</div><div class="kpi-value">$counts.done_n</div><div class="kpi-delta">$progress_pct% complete</div></div> | |
| 24 | <div class="kpi"><div class="kpi-label">In progress</div><div class="kpi-value">$counts.prog_n</div></div> | |
| 25 | <div class="kpi danger"><div class="kpi-label">Overdue</div><div class="kpi-value">$counts.overdue_n</div></div> | |
| 26 | </div> | |
| 27 | ||
| 28 | [if:$burndown_ok] | |
| 29 | <div class="card mb-3"> | |
| 30 | <div class="card-header" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px"> | |
| 31 | <div> | |
| 32 | <h3 style="margin:0">Burndown & predicted completion</h3> | |
| 33 | <div style="font-size:13px;margin-top:4px"> | |
| 34 | [if:$bd_status_good]<span style="color:#22c55e;font-weight:600">✓ $bd_status_label</span>[/if] | |
| 35 | [if:$bd_status_bad]<span style="color:#ef4444;font-weight:600">⚠ $bd_status_label</span>[/if] | |
| 36 | [if:$bd_status_warn]<span style="color:#f59e0b;font-weight:600">$bd_status_label</span>[/if] | |
| 37 | [if:$bd_predicted_date] · <span class="text-muted">predicted finish $bd_predicted_date (target $proj_target_iso)</span>[/if] | |
| 38 | </div> | |
| 39 | </div> | |
| 40 | <div style="display:flex;gap:14px;font-size:12px;flex-wrap:wrap"> | |
| 41 | <span><span style="display:inline-block;width:18px;height:2px;background:rgba(255,255,255,.35);vertical-align:3px"></span> Ideal</span> | |
| 42 | <span><span style="display:inline-block;width:18px;height:2px;background:#5aa9ff;vertical-align:3px"></span> Actual</span> | |
| 43 | [if:$bd_has_prediction]<span><span style="display:inline-block;width:18px;height:2px;background:#f59e0b;vertical-align:3px;border-top:1px dashed #f59e0b"></span> Predicted</span>[/if] | |
| 44 | <span class="text-muted">$bd_remaining of $bd_total left · $bd_velocity tasks/day</span> | |
| 45 | </div> | |
| 46 | </div> | |
| 47 | <svg viewBox="0 0 $bd_chart_w $bd_chart_h" style="width:100%;height:auto;display:block"> | |
| 48 | [loop:@bd_y_labels] | |
| 49 | <line x1="50" x2="$bd_chart_w" y1="$loop1.y" y2="$loop1.y" stroke="rgba(255,255,255,.06)" stroke-width="1"/> | |
| 50 | <text x="44" y="$loop1.y" dy="4" text-anchor="end" fill="var(--col-text-dim)" font-size="11">$loop1.text</text> | |
| 51 | [/loop] | |
| 52 | <line x1="$bd_target_x" x2="$bd_target_x" y1="14" y2="212" stroke="#ef4444" stroke-width="1" stroke-dasharray="3,3" opacity="0.6"/> | |
| 53 | <text x="$bd_target_x" y="13" text-anchor="middle" fill="#ef4444" font-size="10" font-weight="600">target</text> | |
| 54 | <line x1="$bd_today_x" x2="$bd_today_x" y1="14" y2="212" stroke="rgba(255,255,255,.25)" stroke-width="1"/> | |
| 55 | <polyline points="$bd_path_ideal" fill="none" stroke="rgba(255,255,255,.35)" stroke-width="1.5" stroke-dasharray="5,4"/> | |
| 56 | <polyline points="$bd_path_actual" fill="none" stroke="#5aa9ff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> | |
| 57 | [if:$bd_has_prediction] | |
| 58 | <polyline points="$bd_path_pred" fill="none" stroke="#f59e0b" stroke-width="2" stroke-dasharray="5,4" stroke-linecap="round"/> | |
| 59 | <circle cx="$bd_pred_x" cy="$bd_pred_y" r="3" fill="#f59e0b"/> | |
| 60 | [/if] | |
| 61 | [loop:@bd_x_labels] | |
| 62 | <text x="$loop1.x" y="$loop1.y" text-anchor="middle" fill="var(--col-text-dim)" font-size="11">$loop1.text</text> | |
| 63 | [/loop] | |
| 64 | </svg> | |
| 65 | </div> | |
| 66 | [/if] | |
| 67 | [if:!$burndown_ok] | |
| 68 | <div class="card mb-3" style="padding:18px;border-left:3px solid var(--col-warn)"> | |
| 69 | <strong>Burndown chart unavailable.</strong> Set both a <em>start date</em> and a <em>target due date</em> on this project, and add at least one task, and the predicted-completion chart will appear here. | |
| 70 | </div> | |
| 71 | [/if] | |
| 72 | ||
| 73 | <div class="row" style="gap:24px;align-items:stretch;flex-wrap:wrap"> | |
| 74 | <div class="col" style="min-width:300px;flex:2"> | |
| 75 | <div class="card"> | |
| 76 | <div class="card-header"><h3>Recent tasks</h3><a href="/kanban.cgi?project=$proj_id" class="text-muted" style="font-size:13px">Open Kanban →</a></div> | |
| 77 | [if:$has_recent] | |
| 78 | <table class="table"> | |
| 79 | <thead><tr><th>Ticket</th><th>Title</th><th>Status</th><th>Priority</th><th>Assignee</th><th>Due</th></tr></thead> | |
| 80 | <tbody> | |
| 81 | [loop:@recent] | |
| 82 | <tr class="clickable" onclick="location.href='/task.cgi?id=$loop1.id'"> | |
| 83 | <td><code>$loop1.display_ticket</code></td> | |
| 84 | <td>$loop1.title</td> | |
| 85 | <td><span class="badge">$loop1.status</span></td> | |
| 86 | <td><span class="priority-pill priority-$loop1.priority">$loop1.priority</span></td> | |
| 87 | <td>[if:$loop1.assignee_name]<div style="display:inline-flex;align-items:center;gap:6px"><div class="avatar avatar-sm" style="background:$loop1.assignee_color">$loop1.assignee_initials</div><span style="font-size:12px">$loop1.assignee_name</span></div>[/if][if:!$loop1.assignee_name]<span class="text-dim">Unassigned</span>[/if]</td> | |
| 88 | <td>$loop1.due_date</td> | |
| 89 | </tr> | |
| 90 | [/loop] | |
| 91 | </tbody> | |
| 92 | </table> | |
| 93 | [/if] | |
| 94 | [if:!$has_recent] | |
| 95 | <div class="muted-empty"> | |
| 96 | <h3>No tasks yet</h3> | |
| 97 | <p>Add your first task to start tracking work.</p> | |
| 98 | <button onclick="document.getElementById('newTaskModal').classList.add('open')" class="btn btn-primary mt-2">Add first task</button> | |
| 99 | </div> | |
| 100 | [/if] | |
| 101 | </div> | |
| 102 | </div> | |
| 103 | <div class="col" style="min-width:260px;flex:1"> | |
| 104 | <div class="card mb-3"> | |
| 105 | <div class="card-header"><h3>Details</h3>[if:$can_edit]<a href="/project_edit.cgi?id=$proj_id" class="text-muted" style="font-size:12px">Edit</a>[/if]</div> | |
| 106 | <table class="task-meta-table"> | |
| 107 | <tr><td class="lbl">Status</td><td><span class="badge">$proj_status</span></td></tr> | |
| 108 | <tr><td class="lbl">Priority</td><td><span class="priority-pill priority-$proj_priority">$proj_priority</span></td></tr> | |
| 109 | <tr><td class="lbl">Owner</td><td>$owner_name</td></tr> | |
| 110 | [if:$has_dept]<tr><td class="lbl">Department</td><td><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:$dept_color;margin-right:6px"></span>$dept_name</td></tr>[/if] | |
| 111 | [if:$proj_start]<tr><td class="lbl">Start</td><td>$proj_start</td></tr>[/if] | |
| 112 | [if:$proj_due]<tr><td class="lbl">Due</td><td>$proj_due</td></tr>[/if] | |
| 113 | </table> | |
| 114 | </div> | |
| 115 | <div class="card"> | |
| 116 | <div class="card-header"><h3>Team</h3><a href="/project_members.cgi?id=$proj_id" class="text-muted" style="font-size:12px">Manage</a></div> | |
| 117 | [if:$has_members] | |
| 118 | [loop:@members] | |
| 119 | <div style="display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--col-border)"> | |
| 120 | <div class="avatar avatar-sm" style="background:$loop1.avatar_color">$loop1.initials</div> | |
| 121 | <div style="flex:1;min-width:0"><div style="font-size:13px">$loop1.display_name</div><div style="font-size:11px;color:var(--col-text-dim)">$loop1.role</div></div> | |
| 122 | </div> | |
| 123 | [/loop] | |
| 124 | [/if] | |
| 125 | [if:!$has_members]<p class="text-dim" style="font-size:13px">No members yet.</p>[/if] | |
| 126 | </div> | |
| 127 | </div> | |
| 128 | </div> | |
| 129 | ||
| 130 | <div class="modal-backdrop" id="newTaskModal" onclick="if(event.target===this)this.classList.remove('open')"> | |
| 131 | <div class="modal"> | |
| 132 | <div class="modal-header"><h2>New task</h2><button class="modal-close" onclick="document.getElementById('newTaskModal').classList.remove('open')">×</button></div> | |
| 133 | <form method="POST" action="/task_action.cgi"> | |
| 134 | <input type="hidden" name="act" value="create"> | |
| 135 | <input type="hidden" name="project_id" value="$proj_id"> | |
| 136 | <div class="modal-body"> | |
| 137 | <div class="field"><label data-tip="One-line summary your team will scan on Kanban cards and lists. Keep it under 80 characters.">Title *</label><input class="input" name="title" required autofocus placeholder="Short summary"></div> | |
| 138 | <div class="field"><label data-tip="Full details — acceptance criteria, links, screenshots, anything a teammate needs to start. Plain text for now.">Description</label><textarea class="textarea" name="description" placeholder="Details, acceptance criteria, links"></textarea></div> | |
| 139 | <div class="row"> | |
| 140 | <div class="col"><div class="field"><label data-tip="**Low**: nice to have. **Medium**: default. **High**: needs attention this week. **Urgent**: today. **Critical**: stop-the-world.">Priority</label> | |
| 141 | <select class="select" name="priority"><option value="low">Low</option><option value="medium" selected>Medium</option><option value="high">High</option><option value="urgent">Urgent</option><option value="critical">Critical</option></select> | |
| 142 | </div></div> | |
| 143 | <div class="col"><div class="field"><label data-tip="When this task should be finished by. Drives Gantt bars, calendar pins, and 'due this week' / overdue counters.">Due date</label><input class="input" type="date" name="due_date"></div></div> | |
| 144 | </div> | |
| 145 | </div> | |
| 146 | <div class="modal-footer"><button type="button" class="btn btn-ghost" onclick="document.getElementById('newTaskModal').classList.remove('open')">Cancel</button><button type="submit" class="btn btn-primary">Create task</button></div> | |
| 147 | </form> | |
| 148 | </div> | |
| 149 | </div> | |
| 150 | ||
| 151 | [if:$show_updated]<div class="flash-msg" style="background:rgba(34,197,94,.12);border-left:3px solid #22c55e;color:#22c55e;padding:10px 14px;border-radius:6px;margin:-12px 0 14px">✓ Project updated.</div>[/if] | |
| 152 | ||
| 153 | [if:$can_edit] | |
| 154 | <div class="modal-backdrop" id="editProjectModal" onclick="if(event.target===this)this.classList.remove('open')"> | |
| 155 | <div class="modal" style="max-width:640px"> | |
| 156 | <div class="modal-header"><h2>Edit project</h2><button class="modal-close" onclick="document.getElementById('editProjectModal').classList.remove('open')">×</button></div> | |
| 157 | <form method="POST" action="/project_action.cgi"> | |
| 158 | <input type="hidden" name="act" value="update"> | |
| 159 | <input type="hidden" name="id" value="$proj_id"> | |
| 160 | <div class="modal-body"> | |
| 161 | <div class="field"><label>Name *</label><input class="input" name="name" required value="$proj_name"></div> | |
| 162 | <div class="field" style="margin-top:10px"><label>Description</label><textarea class="textarea" name="description" rows="3">$proj_description</textarea></div> | |
| 163 | <div class="row" style="margin-top:10px"> | |
| 164 | <div class="col"><div class="field"><label>Status</label> | |
| 165 | <select class="select" name="status"> | |
| 166 | <option value="planning" $sel_status_planning>Planning</option> | |
| 167 | <option value="active" $sel_status_active>Active</option> | |
| 168 | <option value="on_hold" $sel_status_on_hold>On hold — paused, tasks intact</option> | |
| 169 | <option value="cancelled" $sel_status_cancelled>Cancelled — abandoned, tasks intact</option> | |
| 170 | <option value="complete" $sel_status_complete>Complete</option> | |
| 171 | <option value="archived" $sel_status_archived>Archived</option> | |
| 172 | </select> | |
| 173 | </div></div> | |
| 174 | <div class="col"><div class="field"><label>Priority</label> | |
| 175 | <select class="select" name="priority"> | |
| 176 | <option value="low" $sel_priority_low>Low</option> | |
| 177 | <option value="medium" $sel_priority_medium>Medium</option> | |
| 178 | <option value="high" $sel_priority_high>High</option> | |
| 179 | <option value="urgent" $sel_priority_urgent>Urgent</option> | |
| 180 | </select> | |
| 181 | </div></div> | |
| 182 | <div class="col"><div class="field"><label>Color</label><input class="input" type="color" name="color" value="$proj_color" style="height:38px;padding:2px"></div></div> | |
| 183 | </div> | |
| 184 | <div class="row" style="margin-top:10px"> | |
| 185 | <div class="col"><div class="field"><label>Start date</label><input class="input" type="date" name="start_date" value="$proj_start_iso"></div></div> | |
| 186 | <div class="col"><div class="field"><label>Target due date</label><input class="input" type="date" name="target_due_date" value="$proj_due_iso"></div></div> | |
| 187 | </div> | |
| 188 | <div class="field" style="margin-top:14px"> | |
| 189 | <label class="checkbox"><input type="checkbox" name="is_public_in_company" value="1" $proj_is_public> Visible to all members of the company (otherwise only project members see it)</label> | |
| 190 | </div> | |
| 191 | </div> | |
| 192 | <div class="modal-footer"> | |
| 193 | <span class="text-muted" style="font-size:12px;margin-right:auto">Use the status to pause / cancel. Use the danger zone below to remove the project entirely.</span> | |
| 194 | <button type="button" class="btn btn-ghost" onclick="document.getElementById('editProjectModal').classList.remove('open')">Cancel</button> | |
| 195 | <button type="submit" class="btn btn-primary">Save changes</button> | |
| 196 | </div> | |
| 197 | </form> | |
| 198 | ||
| 199 | <!-- ============ Danger zone ============ --> | |
| 200 | <div class="pl-danger-zone"> | |
| 201 | <div class="pl-danger-head"> | |
| 202 | <div class="pl-danger-title">Danger zone</div> | |
| 203 | <div class="pl-danger-sub">Two options for removing this project. Soft-delete is reversible; permanent delete is not.</div> | |
| 204 | </div> | |
| 205 | <div class="pl-danger-row"> | |
| 206 | <div class="pl-danger-info"> | |
| 207 | <div class="pl-danger-label">Mark as deleted</div> | |
| 208 | <div class="pl-danger-desc">Hides the project and its tasks from listings, dashboards, and reports. All data is preserved — you (or any admin) can restore it from the deleted-projects list later.</div> | |
| 209 | </div> | |
| 210 | <form method="POST" action="/project_action.cgi" onsubmit="return confirm('Mark this project as deleted? It will be hidden from reports but recoverable.')"> | |
| 211 | <input type="hidden" name="act" value="mark_deleted"> | |
| 212 | <input type="hidden" name="id" value="$proj_id"> | |
| 213 | <button type="submit" class="btn pl-btn-soft">Mark deleted</button> | |
| 214 | </form> | |
| 215 | </div> | |
| 216 | <div class="pl-danger-row pl-danger-row-hard"> | |
| 217 | <div class="pl-danger-info"> | |
| 218 | <div class="pl-danger-label">Delete permanently</div> | |
| 219 | <div class="pl-danger-desc">Physically removes the project and ALL of its tasks, comments, attachments, kanban columns, and dependencies. <strong>This cannot be undone.</strong></div> | |
| 220 | </div> | |
| 221 | <form method="POST" action="/project_action.cgi" onsubmit="return plConfirmHardDelete(this)"> | |
| 222 | <input type="hidden" name="act" value="delete_permanently"> | |
| 223 | <input type="hidden" name="id" value="$proj_id"> | |
| 224 | <input type="hidden" name="confirm" value=""> | |
| 225 | <button type="submit" class="btn pl-btn-hard">Delete permanently</button> | |
| 226 | </form> | |
| 227 | </div> | |
| 228 | </div> | |
| 229 | ||
| 230 | </div> | |
| 231 | </div> | |
| 232 | <script> | |
| 233 | function plConfirmHardDelete(form){ | |
| 234 | var c = prompt("Type DELETE in caps to permanently delete this project and all of its tasks. This cannot be undone."); | |
| 235 | if (c !== "DELETE") return false; | |
| 236 | form.confirm.value = "YES_DELETE_PERMANENTLY"; | |
| 237 | return true; | |
| 238 | } | |
| 239 | </script> | |
| 240 | <style> | |
| 241 | .pl-danger-zone { margin: 18px 22px 22px; padding: 16px 18px; background: linear-gradient(160deg, rgba(239,68,68,0.06), rgba(239,68,68,0.02)); border: 1px solid rgba(239,68,68,0.28); border-radius: 12px; } | |
| 242 | .pl-danger-head { padding-bottom: 12px; margin-bottom: 14px; border-bottom: 1px solid rgba(239,68,68,0.18); } | |
| 243 | .pl-danger-title { color: #f87171; font-weight: 700; font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase; } | |
| 244 | .pl-danger-sub { color: #8893a4; font-size: 12px; margin-top: 4px; } | |
| 245 | .pl-danger-row { display: flex; gap: 16px; align-items: center; padding: 12px 0; border-bottom: 1px dashed rgba(255,255,255,0.05); } | |
| 246 | .pl-danger-row:last-child { border-bottom: 0; } | |
| 247 | .pl-danger-info { flex: 1; min-width: 0; } | |
| 248 | .pl-danger-label { color: #fff; font-weight: 700; font-size: 13px; } | |
| 249 | .pl-danger-desc { color: #8893a4; font-size: 12px; margin-top: 4px; line-height: 1.5; } | |
| 250 | .pl-btn-soft { background: rgba(239,68,68,0.16); color: #f87171; border: 1px solid rgba(239,68,68,0.42); font-weight: 600; padding: 7px 14px; border-radius: 7px; cursor: pointer; flex-shrink: 0; } | |
| 251 | .pl-btn-soft:hover { background: rgba(239,68,68,0.28); } | |
| 252 | .pl-btn-hard { background: #b91c1c; color: #fff; border: 1px solid #ef4444; font-weight: 700; padding: 7px 14px; border-radius: 7px; cursor: pointer; flex-shrink: 0; } | |
| 253 | .pl-btn-hard:hover { background: #dc2626; } | |
| 254 | </style> | |
| 255 | [/if] |