added on local at 2026-07-01 12:35:04
| 1 | <div class="page-header"><div><h1>Preferences</h1><p class="subtitle">Tune PTMatrix to your workflow.</p></div></div> | |
| 2 | [if:$has_flash]<div class="flash-msg">$flash</div>[/if] | |
| 3 | <form method="POST"> | |
| 4 | <div class="row" style="gap:24px;flex-wrap:wrap"> | |
| 5 | <div class="col" style="min-width:280px;flex:1"> | |
| 6 | <div class="card"> | |
| 7 | <div class="card-header"><h3>Appearance</h3></div> | |
| 8 | <div class="field"><label>Theme</label> | |
| 9 | <select class="select" name="theme"> | |
| 10 | <option value="dark"[if:$theme_dark] selected[/if]>Dark</option> | |
| 11 | <option value="light"[if:$theme_light] selected[/if]>Light (coming soon)</option> | |
| 12 | <option value="auto"[if:$theme_auto] selected[/if]>Match system</option> | |
| 13 | </select> | |
| 14 | </div> | |
| 15 | <div class="field"><label>Default project view</label> | |
| 16 | <select class="select" name="default_view"> | |
| 17 | <option value="kanban"[if:$view_kanban] selected[/if]>Kanban board</option> | |
| 18 | <option value="gantt"[if:$view_gantt] selected[/if]>Gantt chart</option> | |
| 19 | <option value="list"[if:$view_list] selected[/if]>List</option> | |
| 20 | <option value="calendar"[if:$view_cal] selected[/if]>Calendar</option> | |
| 21 | </select> | |
| 22 | </div> | |
| 23 | <label class="checkbox" style="display:flex;padding:8px 0" data-tip="When ON, hovering field labels and icons reveals styled hint popovers. Once you know your way around, turn this off for a calmer interface."> | |
| 24 | <input type="checkbox" name="show_tooltips" value="1"[if:$show_tips] checked[/if]> | |
| 25 | Show interface helpers (hover tooltips) | |
| 26 | </label> | |
| 27 | </div> | |
| 28 | </div> | |
| 29 | ||
| 30 | <div class="col" style="min-width:300px;flex:2"> | |
| 31 | <div class="card"> | |
| 32 | <div class="card-header"><h3>Board appearance</h3></div> | |
| 33 | <div class="field"> | |
| 34 | <label data-tip="Pick how the background of Kanban cards is tinted. Priority lets each card's gradient reflect urgency. Assignee uses each owner's avatar color. Custom lets you pick a fixed pair of colors.">Card background</label> | |
| 35 | <div class="mode-radios"> | |
| 36 | <label> | |
| 37 | <input type="radio" name="card_gradient_mode" value="off"[if:$grad_off] checked[/if]> | |
| 38 | <span class="mode-swatch" style="background:var(--col-surface)"></span> | |
| 39 | <span>Flat</span> | |
| 40 | </label> | |
| 41 | <label> | |
| 42 | <input type="radio" name="card_gradient_mode" value="priority"[if:$grad_priority] checked[/if]> | |
| 43 | <span class="mode-swatch" style="background:linear-gradient(135deg,rgba(245,158,11,.4),var(--col-surface))"></span> | |
| 44 | <span>By priority</span> | |
| 45 | </label> | |
| 46 | <label> | |
| 47 | <input type="radio" name="card_gradient_mode" value="assignee"[if:$grad_assignee] checked[/if]> | |
| 48 | <span class="mode-swatch" style="background:linear-gradient(135deg,rgba(139,92,246,.4),var(--col-surface))"></span> | |
| 49 | <span>By assignee</span> | |
| 50 | </label> | |
| 51 | <label> | |
| 52 | <input type="radio" name="card_gradient_mode" value="custom"[if:$grad_custom] checked[/if]> | |
| 53 | <span class="mode-swatch" id="customSwatch" style="background:linear-gradient(135deg,$grad_from,$grad_to)"></span> | |
| 54 | <span>Custom pair</span> | |
| 55 | </label> | |
| 56 | </div> | |
| 57 | </div> | |
| 58 | ||
| 59 | <div class="row" style="gap:14px;align-items:end"> | |
| 60 | <div class="col" style="max-width:160px"><div class="field"><label>Custom from</label><input class="input" type="color" name="card_gradient_from" id="cgFrom" value="$grad_from" style="height:38px;padding:2px"></div></div> | |
| 61 | <div class="col" style="max-width:160px"><div class="field"><label>Custom to</label><input class="input" type="color" name="card_gradient_to" id="cgTo" value="$grad_to" style="height:38px;padding:2px"></div></div> | |
| 62 | <div class="col"><div class="grad-preview" id="gradPreview" style="background:linear-gradient(135deg,$grad_from,$grad_to)"></div></div> | |
| 63 | </div> | |
| 64 | ||
| 65 | <div class="field" style="margin-top:16px"> | |
| 66 | <label>Board density</label> | |
| 67 | <select class="select" name="board_density"> | |
| 68 | <option value="compact"[if:$den_compact] selected[/if]>Compact (more on screen)</option> | |
| 69 | <option value="comfortable"[if:$den_comfortable] selected[/if]>Comfortable (default)</option> | |
| 70 | <option value="spacious"[if:$den_spacious] selected[/if]>Spacious (larger cards)</option> | |
| 71 | </select> | |
| 72 | </div> | |
| 73 | </div> | |
| 74 | </div> | |
| 75 | ||
| 76 | <div class="col" style="min-width:280px;flex:2"> | |
| 77 | <div class="card"> | |
| 78 | <div class="card-header"><h3>Email notifications</h3></div> | |
| 79 | <p class="text-muted" style="font-size:13px">Send me an email when…</p> | |
| 80 | <div class="mt-1"> | |
| 81 | <label class="checkbox" style="display:flex;padding:8px 0"><input type="checkbox" name="email_notif_assigned" value="1"[if:$n_assigned] checked[/if]> A task is assigned to me</label> | |
| 82 | <label class="checkbox" style="display:flex;padding:8px 0"><input type="checkbox" name="email_notif_mentioned" value="1"[if:$n_mentioned] checked[/if]> Someone @-mentions me</label> | |
| 83 | <label class="checkbox" style="display:flex;padding:8px 0"><input type="checkbox" name="email_notif_status" value="1"[if:$n_status] checked[/if]> Status changes on a task I'm watching</label> | |
| 84 | <label class="checkbox" style="display:flex;padding:8px 0"><input type="checkbox" name="email_notif_due_soon" value="1"[if:$n_due_soon] checked[/if]> A task assigned to me is due in 24h</label> | |
| 85 | <label class="checkbox" style="display:flex;padding:8px 0"><input type="checkbox" name="email_notif_weekly" value="1"[if:$n_weekly] checked[/if]> Weekly digest (Mondays)</label> | |
| 86 | </div> | |
| 87 | </div> | |
| 88 | ||
| 89 | <div class="card mt-3" id="tf-push-card"> | |
| 90 | <div class="card-header"><h3>Browser push notifications</h3></div> | |
| 91 | <p class="text-muted" style="font-size:13px;margin-bottom:14px">Get a system notification when you’re mentioned, assigned a task, or have an SLA-affected ticket update — even when PTMatrix isn’t open. Works on desktop and Android. On iOS, add PTMatrix to your home screen first.</p> | |
| 92 | <div id="tf-push-status" style="font-size:13px;color:var(--col-text-dim);margin-bottom:12px">Checking browser support…</div> | |
| 93 | <div style="display:flex;gap:8px;flex-wrap:wrap"> | |
| 94 | <button type="button" class="btn btn-primary" id="tf-push-enable" style="display:none">Enable on this device</button> | |
| 95 | <button type="button" class="btn" id="tf-push-disable" style="display:none">Disable on this device</button> | |
| 96 | <button type="button" class="btn" id="tf-push-test" style="display:none">Send test notification</button> | |
| 97 | </div> | |
| 98 | <div id="tf-push-err" style="display:none;color:#ef4444;font-size:12.5px;margin-top:10px"></div> | |
| 99 | </div> | |
| 100 | </div> | |
| 101 | </div> | |
| 102 | <div class="flex-end mt-3"><button type="submit" class="btn btn-primary">Save preferences</button></div> | |
| 103 | </form> | |
| 104 | ||
| 105 | <script> | |
| 106 | (function(){ | |
| 107 | var card = document.getElementById('tf-push-card'); | |
| 108 | if (!card) return; | |
| 109 | var statusEl = document.getElementById('tf-push-status'); | |
| 110 | var btnOn = document.getElementById('tf-push-enable'); | |
| 111 | var btnOff = document.getElementById('tf-push-disable'); | |
| 112 | var btnTest = document.getElementById('tf-push-test'); | |
| 113 | var errEl = document.getElementById('tf-push-err'); | |
| 114 | function showErr(m){ errEl.style.display = 'block'; errEl.textContent = m; } | |
| 115 | function clearErr(){ errEl.style.display = 'none'; errEl.textContent = ''; } | |
| 116 | ||
| 117 | // tfPush is defined by the wrapper's inline script which loads after this | |
| 118 | // body section. Wait up to ~2s for it before deciding. | |
| 119 | var tries = 0; | |
| 120 | (function waitForTfPush(){ | |
| 121 | if (window.tfPush) { initPush(); return; } | |
| 122 | if (++tries > 40) { | |
| 123 | statusEl.textContent = 'This browser does not support web push notifications.'; | |
| 124 | return; | |
| 125 | } | |
| 126 | setTimeout(waitForTfPush, 50); | |
| 127 | })(); | |
| 128 | ||
| 129 | function initPush(){ | |
| 130 | if (!window.tfPush.supported) { | |
| 131 | statusEl.textContent = 'This browser does not support web push notifications.'; | |
| 132 | return; | |
| 133 | } | |
| 134 | function refresh(){ | |
| 135 | var perm = tfPush.permission(); | |
| 136 | tfPush.isSubscribed().then(function(sub){ | |
| 137 | if (perm === 'denied') { | |
| 138 | statusEl.textContent = 'Blocked: re-allow notifications in your browser site settings.'; | |
| 139 | btnOn.style.display = btnOff.style.display = btnTest.style.display = 'none'; | |
| 140 | } else if (sub) { | |
| 141 | statusEl.innerHTML = '<span style="color:#22c55e;font-weight:600">✓ Subscribed on this device.</span>'; | |
| 142 | btnOn.style.display = 'none'; | |
| 143 | btnOff.style.display = 'inline-flex'; | |
| 144 | btnTest.style.display = 'inline-flex'; | |
| 145 | } else { | |
| 146 | statusEl.textContent = 'Not subscribed on this device.'; | |
| 147 | btnOn.style.display = 'inline-flex'; | |
| 148 | btnOff.style.display = btnTest.style.display = 'none'; | |
| 149 | } | |
| 150 | }); | |
| 151 | } | |
| 152 | refresh(); | |
| 153 | ||
| 154 | btnOn.addEventListener('click', function(){ | |
| 155 | clearErr(); | |
| 156 | btnOn.disabled = true; btnOn.textContent = 'Asking…'; | |
| 157 | tfPush.enable().then(function(){ refresh(); }, function(err){ | |
| 158 | showErr(err && err.message ? err.message : 'Could not enable.'); | |
| 159 | }).then(function(){ btnOn.disabled = false; btnOn.textContent = 'Enable on this device'; }); | |
| 160 | }); | |
| 161 | btnOff.addEventListener('click', function(){ | |
| 162 | clearErr(); | |
| 163 | btnOff.disabled = true; | |
| 164 | tfPush.disable().then(refresh).then(function(){ btnOff.disabled = false; }, function(err){ | |
| 165 | showErr(err && err.message ? err.message : 'Could not disable.'); | |
| 166 | btnOff.disabled = false; | |
| 167 | }); | |
| 168 | }); | |
| 169 | btnTest.addEventListener('click', function(){ | |
| 170 | clearErr(); | |
| 171 | btnTest.disabled = true; | |
| 172 | tfPush.test().then(function(){ | |
| 173 | btnTest.disabled = false; | |
| 174 | statusEl.innerHTML = '<span style="color:#22c55e;font-weight:600">✓ Test push sent. Check your system notifications.</span>'; | |
| 175 | }, function(err){ | |
| 176 | btnTest.disabled = false; | |
| 177 | showErr(err && err.message ? err.message : 'Test send failed.'); | |
| 178 | }); | |
| 179 | }); | |
| 180 | } // end initPush | |
| 181 | })(); | |
| 182 | </script> | |
| 183 | ||
| 184 | <script> | |
| 185 | (function(){ | |
| 186 | var f = document.getElementById('cgFrom'); | |
| 187 | var t = document.getElementById('cgTo'); | |
| 188 | var p = document.getElementById('gradPreview'); | |
| 189 | var s = document.getElementById('customSwatch'); | |
| 190 | function paint(){ | |
| 191 | var grad = 'linear-gradient(135deg,' + f.value + ',' + t.value + ')'; | |
| 192 | if (p) p.style.background = grad; | |
| 193 | if (s) s.style.background = grad; | |
| 194 | } | |
| 195 | if (f && t) { f.addEventListener('input', paint); t.addEventListener('input', paint); } | |
| 196 | })(); | |
| 197 | </script> |