feat: show sla tracking only for active sla clients

This commit is contained in:
Marco0300
2026-09-02 00:53:02 +02:00
parent 9f149c61be
commit 82ccbc4d54
2 changed files with 6 additions and 2 deletions
+4 -2
View File
@@ -244,6 +244,8 @@ if ($route === 'jobcard') {
$jobcard = $jobcardStmt->fetch(); $jobcard = $jobcardStmt->fetch();
if (!$jobcard) { http_response_code(404); exit('Jobcard not found'); } if (!$jobcard) { http_response_code(404); exit('Jobcard not found'); }
if (!can_access_jobcard($jobcardId)) { http_response_code(404); exit('Jobcard not found'); } if (!can_access_jobcard($jobcardId)) { http_response_code(404); exit('Jobcard not found'); }
$clientSlaStmt = db()->prepare('SELECT 1 FROM sla_agreements WHERE client_id = :client AND enabled = 1 AND CURDATE() BETWEEN COALESCE(start_date, \'1000-01-01\') AND COALESCE(end_date, \'9999-12-31\') LIMIT 1');
$hasClientSla = (bool)$clientSlaStmt->execute(['client' => $jobcard['client_id']]) && (bool)$clientSlaStmt->fetchColumn();
$actionErrors = []; $actionErrors = [];
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
verify_csrf(); verify_csrf();
@@ -313,7 +315,7 @@ if ($route === 'jobcard') {
$technicianId = $user['role_name'] === 'Technician' ? (int)$user['id'] : filter_var(scalar_input($_POST['technician_id'] ?? null), FILTER_VALIDATE_INT); $technicianId = $user['role_name'] === 'Technician' ? (int)$user['id'] : filter_var(scalar_input($_POST['technician_id'] ?? null), FILTER_VALIDATE_INT);
$technicianCheck = db()->prepare("SELECT u.id FROM users u JOIN roles r ON r.id = u.role_id WHERE u.id = :id AND u.is_active = 1 AND r.name = 'Technician'"); $technicianCheck = db()->prepare("SELECT u.id FROM users u JOIN roles r ON r.id = u.role_id WHERE u.id = :id AND u.is_active = 1 AND r.name = 'Technician'");
$technicianCheck->execute(['id' => $technicianId]); $technicianCheck->execute(['id' => $technicianId]);
$timeInput = [...$_POST, 'jobcard_id' => $jobcardId, 'technician_id' => $technicianId, 'counts_toward_sla' => isset($_POST['counts_toward_sla']) ? '1' : '0']; $timeInput = [...$_POST, 'jobcard_id' => $jobcardId, 'technician_id' => $technicianId, 'counts_toward_sla' => $hasClientSla && isset($_POST['counts_toward_sla']) ? '1' : '0'];
$time = (new \App\Domain\Jobcard\TimeEntryCommand())->validate($timeInput); $time = (new \App\Domain\Jobcard\TimeEntryCommand())->validate($timeInput);
$actionErrors = array_values($time['errors']); $actionErrors = array_values($time['errors']);
if (!$technicianCheck->fetchColumn()) $actionErrors[] = 'Time must be attributed to an active technician.'; if (!$technicianCheck->fetchColumn()) $actionErrors[] = 'Time must be attributed to an active technician.';
@@ -380,7 +382,7 @@ if ($route === 'jobcard') {
if (can('jobcards.internal_notes')) echo '<div class="card mb-4"><div class="card-body"><h2 class="h5">Internal notes</h2><form method="post"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="notes"><textarea class="form-control mb-3" name="internal_notes" rows="4">' . e((string)($jobcard['internal_notes'] ?? '')) . '</textarea><button class="btn btn-primary">Save internal notes</button></form></div></div>'; if (can('jobcards.internal_notes')) echo '<div class="card mb-4"><div class="card-body"><h2 class="h5">Internal notes</h2><form method="post"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="notes"><textarea class="form-control mb-3" name="internal_notes" rows="4">' . e((string)($jobcard['internal_notes'] ?? '')) . '</textarea><button class="btn btn-primary">Save internal notes</button></form></div></div>';
echo '<div class="card"><div class="card-body"><div class="d-flex justify-content-between"><h2 class="h5">Time entries</h2><strong>' . e(number_format($totalHours, 2)) . ' hours</strong></div>'; echo '<div class="card"><div class="card-body"><div class="d-flex justify-content-between"><h2 class="h5">Time entries</h2><strong>' . e(number_format($totalHours, 2)) . ' hours</strong></div>';
foreach ($timeEntries as $entry) echo '<div class="border-bottom py-2"><strong>' . e($entry['technician_name']) . '</strong> · ' . e($entry['work_date']) . ' · ' . e(number_format((float)$entry['hours'], 2)) . 'h<div class="small text-muted">' . e((string)($entry['notes'] ?? '')) . '</div>' . (can('time_entries.record') ? '<a class="small" href="/?route=time_entry&id=' . (int)$entry['id'] . '">Correct or void</a>' : '') . '</div>'; foreach ($timeEntries as $entry) echo '<div class="border-bottom py-2"><strong>' . e($entry['technician_name']) . '</strong> · ' . e($entry['work_date']) . ' · ' . e(number_format((float)$entry['hours'], 2)) . 'h<div class="small text-muted">' . e((string)($entry['notes'] ?? '')) . '</div>' . (can('time_entries.record') ? '<a class="small" href="/?route=time_entry&id=' . (int)$entry['id'] . '">Correct or void</a>' : '') . '</div>';
if (can('time_entries.record')) { echo '<hr><form method="post" class="row g-2"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="time">'; if ($user['role_name'] !== 'Technician') { echo '<div class="col-md-4"><select class="form-select" name="technician_id" required><option value="">Technician</option>'; foreach ($technicians as $technician) echo '<option value="' . (int)$technician['id'] . '">' . e($technician['name']) . '</option>'; echo '</select></div>'; } echo '<div class="col-md-4"><label class="form-label">Date performed</label><input class="form-control" type="date" name="work_date" value="' . e(date('Y-m-d')) . '" required></div><div class="col-md-4"><label class="form-label">Hours</label><input class="form-control" type="number" step="0.01" min="0.01" name="hours" placeholder="0.00" required></div><div class="col-md-4 form-check pt-2"><input class="form-check-input" type="checkbox" name="counts_toward_sla" value="1" id="sla-time" checked><label class="form-check-label" for="sla-time">Counts toward SLA</label></div><div class="col-12"><label class="form-label">Technician notes / Work performed</label><textarea class="form-control" name="notes" rows="4" placeholder="Describe the work performed"></textarea></div><div class="col-12"><button class="btn btn-outline-primary">Add time</button></div></form>'; } if (can('time_entries.record')) { echo '<hr><form method="post" class="row g-2"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="time">'; if ($user['role_name'] !== 'Technician') { echo '<div class="col-md-4"><select class="form-select" name="technician_id" required><option value="">Technician</option>'; foreach ($technicians as $technician) echo '<option value="' . (int)$technician['id'] . '">' . e($technician['name']) . '</option>'; echo '</select></div>'; } echo '<div class="col-md-4"><label class="form-label">Date performed</label><input class="form-control" type="date" name="work_date" value="' . e(date('Y-m-d')) . '" required></div><div class="col-md-4"><label class="form-label">Hours</label><input class="form-control" type="number" step="0.01" min="0.01" name="hours" placeholder="0.00" required></div>' . ($hasClientSla ? '<div class="col-md-4 form-check pt-2"><input class="form-check-input" type="checkbox" name="counts_toward_sla" value="1" id="sla-time" checked><label class="form-check-label" for="sla-time">Counts toward SLA</label></div>' : '') . '<div class="col-12"><label class="form-label">Technician notes / Work performed</label><textarea class="form-control" name="notes" rows="4" placeholder="Describe the work performed"></textarea></div><div class="col-12"><button class="btn btn-outline-primary">Add time</button></div></form>'; }
echo '</div></div></div><div class="col-lg-4"><div class="card mb-4"><div class="card-body"><h2 class="h5">Status</h2><form method="post"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="status"><select class="form-select mb-2" name="status">'; echo '</div></div></div><div class="col-lg-4"><div class="card mb-4"><div class="card-body"><h2 class="h5">Status</h2><form method="post"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="status"><select class="form-select mb-2" name="status">';
foreach ((new \App\Domain\Jobcard\StatusTransitionValidator())->allowedFrom($jobcard['status']) as $status) if (($status !== 'closed' || can('jobcards.close')) && ($jobcard['status'] !== 'closed' || $status === 'closed' || can('jobcards.reopen'))) echo '<option value="' . e($status) . '"' . ($status === $jobcard['status'] ? ' selected' : '') . '>' . e(ucwords(str_replace('_', ' ', $status))) . '</option>'; foreach ((new \App\Domain\Jobcard\StatusTransitionValidator())->allowedFrom($jobcard['status']) as $status) if (($status !== 'closed' || can('jobcards.close')) && ($jobcard['status'] !== 'closed' || $status === 'closed' || can('jobcards.reopen'))) echo '<option value="' . e($status) . '"' . ($status === $jobcard['status'] ? ' selected' : '') . '>' . e(ucwords(str_replace('_', ' ', $status))) . '</option>';
echo '</select><button class="btn btn-outline-primary w-100">Update status</button></form></div></div><div class="card"><div class="card-body"><h2 class="h5">Assigned technicians</h2>'; echo '</select><button class="btn btn-outline-primary w-100">Update status</button></form></div></div><div class="card"><div class="card-body"><h2 class="h5">Assigned technicians</h2>';
+2
View File
@@ -24,6 +24,8 @@ jobcard_feature_assert(str_contains($front, '$reportClientsStmt->execute([(int)$
foreach (['Technician notes / Work performed', 'totalReportHours'] as $heading) jobcard_feature_assert(str_contains($front, $heading), "Detailed report must include {$heading}."); foreach (['Technician notes / Work performed', 'totalReportHours'] as $heading) jobcard_feature_assert(str_contains($front, $heading), "Detailed report must include {$heading}.");
jobcard_feature_assert(!str_contains($front, 'name=\"technician_notes\"') && !str_contains($front, 'Hours notes'), 'Jobcards must use time-entry technician notes only.'); jobcard_feature_assert(!str_contains($front, 'name=\"technician_notes\"') && !str_contains($front, 'Hours notes'), 'Jobcards must use time-entry technician notes only.');
jobcard_feature_assert(str_contains($front, 'remaining_hours') && str_contains($front, 'SLA hours remaining'), 'Client pages must show remaining SLA hours.'); jobcard_feature_assert(str_contains($front, 'remaining_hours') && str_contains($front, 'SLA hours remaining'), 'Client pages must show remaining SLA hours.');
jobcard_feature_assert(str_contains($front, '$hasClientSla ?') && str_contains($front, "'counts_toward_sla' => \$hasClientSla &&"), 'SLA checkbox and submitted flag must be conditional on an active client SLA.');
jobcard_feature_assert(str_contains($front, 'j.client_id = :client') && str_contains($front, 'te.counts_toward_sla = 1'), 'Client SLA usage must aggregate active-period SLA time entries regardless of jobcard status.'); jobcard_feature_assert(str_contains($front, 'j.client_id = :client') && str_contains($front, 'te.counts_toward_sla = 1'), 'Client SLA usage must aggregate active-period SLA time entries regardless of jobcard status.');
jobcard_feature_assert(str_contains($front, "['TOTAL HOURS'") && str_contains($front, 'totalReportHours'), 'Detailed report downloads must include total hours.'); jobcard_feature_assert(str_contains($front, "['TOTAL HOURS'") && str_contains($front, 'totalReportHours'), 'Detailed report downloads must include total hours.');
jobcard_feature_assert(str_contains($front, "if (\$route === 'settings')") && str_contains($front, "role_name'] ?? '') !== 'Administrator'"), 'Branding settings must be Administrator-only.'); jobcard_feature_assert(str_contains($front, "if (\$route === 'settings')") && str_contains($front, "role_name'] ?? '') !== 'Administrator'"), 'Branding settings must be Administrator-only.');