feat: modernize dashboard and client profile tabs

This commit is contained in:
Marco0300
2026-09-01 22:24:32 +02:00
parent 9ce08bc6f8
commit 0b1cb20578
2 changed files with 53 additions and 12 deletions
+46 -5
View File
@@ -1,10 +1,51 @@
body { background: #f5f7fb; }
.navbar-brand { letter-spacing: .02em; }
.metric-card { border: 0; box-shadow: 0 .25rem 1rem rgba(24, 39, 75, .06); }
.sidebar-link.active { background: rgba(13, 110, 253, .1); color: #0d6efd; font-weight: 600; }
.login-card { max-width: 430px; margin: 10vh auto; }
:root {
--jc-ink: #172033;
--jc-muted: #667085;
--jc-primary: #635bff;
--jc-primary-dark: #5148e5;
--jc-surface: #ffffff;
--jc-bg: #f6f7fb;
--jc-border: #e8eaf2;
--jc-shadow: 0 14px 38px rgba(20, 28, 55, .08);
}
* { box-sizing: border-box; }
body { background: var(--jc-bg); color: var(--jc-ink); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; letter-spacing: -.01em; }
a { color: var(--jc-primary); }
.navbar { background: linear-gradient(135deg, #1d2540 0%, #302c68 58%, #5148e5 100%) !important; min-height: 68px; box-shadow: 0 4px 18px rgba(31, 36, 76, .18); }
.navbar-brand { letter-spacing: -.035em; font-size: 1.2rem; }
.sidebar-link { color: #667085; border-radius: 10px; padding: .7rem .85rem; font-weight: 500; transition: .18s ease; }
.sidebar-link:hover { background: #f0efff; color: var(--jc-primary); transform: translateX(2px); }
.sidebar-link.active { background: #eeecff; color: var(--jc-primary); font-weight: 700; }
main { min-width: 0; }
.card { border: 1px solid var(--jc-border); border-radius: 16px; box-shadow: var(--jc-shadow); }
.card-header, .card-body { border-radius: inherit; }
.card h1, .card h2, .card h3 { letter-spacing: -.03em; }
.metric-card { border: 0; overflow: hidden; position: relative; box-shadow: var(--jc-shadow); }
.metric-card::after { content: ""; position: absolute; width: 92px; height: 92px; border-radius: 50%; right: -28px; top: -28px; background: rgba(99, 91, 255, .1); }
.metric-card .display-6 { color: var(--jc-ink); letter-spacing: -.06em; }
.btn-primary { background: var(--jc-primary); border-color: var(--jc-primary); box-shadow: 0 8px 18px rgba(99, 91, 255, .22); }
.btn-primary:hover, .btn-primary:focus { background: var(--jc-primary-dark); border-color: var(--jc-primary-dark); }
.btn-outline-primary { color: var(--jc-primary); border-color: #bdb8ff; }
.btn-outline-primary:hover { background: var(--jc-primary); border-color: var(--jc-primary); }
.form-control, .form-select { border-color: #dfe2ec; border-radius: 10px; padding: .68rem .8rem; }
.form-control:focus, .form-select:focus { border-color: #9b95ff; box-shadow: 0 0 0 .22rem rgba(99, 91, 255, .12); }
.table { --bs-table-bg: transparent; }
.table thead th { color: var(--jc-muted); font-size: .74rem; text-transform: uppercase; letter-spacing: .08em; font-weight: 700; border-bottom-color: var(--jc-border); }
.table tbody td { border-bottom-color: #f0f1f5; vertical-align: middle; }
.badge { border-radius: 999px; padding: .45em .72em; font-weight: 650; }
.login-card { max-width: 440px; margin: 10vh auto; border: 0; box-shadow: 0 24px 70px rgba(31, 36, 76, .16); }
.login-card::before { content: ""; display: block; height: 7px; border-radius: 16px 16px 0 0; background: linear-gradient(90deg, #635bff, #9b95ff); }
.client-tabs { display: flex; gap: .4rem; flex-wrap: wrap; padding: .45rem; margin-bottom: 1.35rem; background: #fff; border: 1px solid var(--jc-border); border-radius: 14px; box-shadow: 0 8px 24px rgba(20, 28, 55, .05); }
.client-tab { border: 0; background: transparent; border-radius: 10px; color: var(--jc-muted); padding: .68rem 1rem; font-weight: 650; transition: .18s ease; }
.client-tab:hover { background: #f2f1ff; color: var(--jc-primary); }
.client-tab.active { background: var(--jc-primary); color: #fff; box-shadow: 0 6px 14px rgba(99, 91, 255, .2); }
.client-section-hidden { display: none !important; }
.section-kicker { color: var(--jc-primary); font-size: .75rem; text-transform: uppercase; letter-spacing: .1em; font-weight: 800; }
@media (max-width: 767.98px) {
.desktop-table { display: none; }
.mobile-card { display: block; }
.sidebar-link { padding: .55rem .7rem; }
.client-tabs { overflow-x: auto; flex-wrap: nowrap; scrollbar-width: thin; }
.client-tab { white-space: nowrap; padding: .62rem .8rem; }
}
@media (min-width: 768px) { .mobile-card { display: none; } }
+7 -7
View File
@@ -529,20 +529,21 @@ if ($route === 'client') {
$credentialRows = $credentialStmt->fetchAll();
}
render_header('Client details');
echo '<div class="d-flex justify-content-between align-items-center mb-4"><div><a href="/?route=clients" class="text-decoration-none">← Back to clients</a> <a href="/?route=client_history&id=' . (int)$clientId . '" class="text-decoration-none">View history</a><h1 class="h3 mt-2 mb-1">' . e($client['name']) . '</h1><p class="text-muted mb-0">Client profile and support contacts.</p></div><span class="badge text-bg-' . ($client['status'] === 'active' ? 'success' : 'secondary') . '">' . e(ucfirst($client['status'])) . '</span></div>' . (isset($_GET['contact_created']) ? '<div class="alert alert-success">Contact added successfully.</div>' : '') . (isset($_GET['sla_updated']) ? '<div class="alert alert-success">SLA agreement updated.</div>' : '') . ($contactErrors ? '<div class="alert alert-danger">' . e(implode(' ', $contactErrors)) . '</div>' : '') . ($slaErrors ? '<div class="alert alert-danger">' . e(implode(' ', $slaErrors)) . '</div>' : '') . '<div class="row g-4"><div class="col-lg-6"><div class="card h-100"><div class="card-body"><h2 class="h5">Support information</h2><dl class="row mb-0"><dt class="col-sm-5">Email</dt><dd class="col-sm-7">' . e((string)($client['support_email'] ?? '—')) . '</dd><dt class="col-sm-5">Phone</dt><dd class="col-sm-7">' . e((string)($client['support_phone'] ?? '—')) . '</dd><dt class="col-sm-5">Preferred method</dt><dd class="col-sm-7">' . e((string)($client['preferred_contact_method'] ?? '—')) . '</dd><dt class="col-sm-5">Address</dt><dd class="col-sm-7">' . nl2br(e((string)($client['physical_address'] ?? '—'))) . '</dd></dl></div></div></div><div class="col-lg-6"><div class="card h-100"><div class="card-body"><h2 class="h5">Contacts</h2>';
echo '<div class="client-tabs" role="tablist"><button class="client-tab active" data-client-tab="overview">Overview</button><button class="client-tab" data-client-tab="contacts">Contacts</button><button class="client-tab" data-client-tab="sla">SLA</button><button class="client-tab" data-client-tab="technical">Client information</button><a class="client-tab" href="/?route=client_history&id=' . (int)$clientId . '">Jobcard history</a></div><script>document.addEventListener("DOMContentLoaded",function(){const tabs=[...document.querySelectorAll("[data-client-tab]")];const sections=[...document.querySelectorAll("[data-client-section]")];function activate(key){tabs.forEach(t=>t.classList.toggle("active",t.dataset.clientTab===key));sections.forEach(s=>s.classList.toggle("client-section-hidden",s.dataset.clientSection!==key));}tabs.forEach(t=>t.addEventListener("click",()=>activate(t.dataset.clientTab)));activate("overview");});</script>';
echo '<div class="d-flex justify-content-between align-items-center mb-4"><div><a href="/?route=clients" class="text-decoration-none">← Back to clients</a> <a href="/?route=client_history&id=' . (int)$clientId . '" class="text-decoration-none">View history</a><h1 class="h3 mt-2 mb-1">' . e($client['name']) . '</h1><p class="text-muted mb-0">Client profile and support contacts.</p></div><span class="badge text-bg-' . ($client['status'] === 'active' ? 'success' : 'secondary') . '">' . e(ucfirst($client['status'])) . '</span></div>' . (isset($_GET['contact_created']) ? '<div class="alert alert-success">Contact added successfully.</div>' : '') . (isset($_GET['sla_updated']) ? '<div class="alert alert-success">SLA agreement updated.</div>' : '') . ($contactErrors ? '<div class="alert alert-danger">' . e(implode(' ', $contactErrors)) . '</div>' : '') . ($slaErrors ? '<div class="alert alert-danger">' . e(implode(' ', $slaErrors)) . '</div>' : '') . '<div class="row g-4"><div class="col-lg-6"><div class="card h-100" data-client-section="overview"><div class="card-body"><h2 class="h5">Support information</h2><dl class="row mb-0"><dt class="col-sm-5">Email</dt><dd class="col-sm-7">' . e((string)($client['support_email'] ?? '—')) . '</dd><dt class="col-sm-5">Phone</dt><dd class="col-sm-7">' . e((string)($client['support_phone'] ?? '—')) . '</dd><dt class="col-sm-5">Preferred method</dt><dd class="col-sm-7">' . e((string)($client['preferred_contact_method'] ?? '—')) . '</dd><dt class="col-sm-5">Address</dt><dd class="col-sm-7">' . nl2br(e((string)($client['physical_address'] ?? '—'))) . '</dd></dl></div></div></div><div class="col-lg-6"><div class="card h-100" data-client-section="contacts"><div class="card-body"><h2 class="h5">Contacts</h2>';
if (!$contacts) echo '<p class="text-muted mb-0">No contacts recorded.</p>';
foreach ($contacts as $contact) { echo '<div class="border-bottom py-2"><div class="fw-semibold">' . e($contact['name']) . ($contact['is_primary'] ? ' <span class="badge text-bg-primary">Primary</span>' : '') . '</div><div class="small text-muted">' . e((string)($contact['email'] ?? '')) . ' ' . e((string)($contact['phone'] ?? '')) . '</div>'; if (can('clients.manage')) { echo '<form method="post" class="d-inline me-2"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="contact_primary"><input type="hidden" name="contact_id" value="' . (int)$contact['id'] . '"><button class="btn btn-sm btn-link p-0">Set primary</button></form><form method="post" class="d-inline"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="contact_delete"><input type="hidden" name="contact_id" value="' . (int)$contact['id'] . '"><button class="btn btn-sm btn-link text-danger p-0">Delete</button></form><form method="post" class="row g-1 mt-1"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="contact_edit"><input type="hidden" name="contact_id" value="' . (int)$contact['id'] . '"><div class="col-md-3"><input class="form-control form-control-sm" name="name" value="' . e($contact['name']) . '" required></div><div class="col-md-3"><input class="form-control form-control-sm" type="email" name="email" value="' . e((string)($contact['email'] ?? '')) . '"></div><div class="col-md-3"><input class="form-control form-control-sm" name="phone" value="' . e((string)($contact['phone'] ?? '')) . '"></div><div class="col-md-3"><button class="btn btn-sm btn-outline-secondary">Save edit</button></div></form>'; } echo '</div>'; }
if (can('clients.manage')) echo '<hr><h3 class="h6 mt-3">Add contact</h3><form method="post" class="row g-2"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="contact"><div class="col-12"><input class="form-control" name="name" placeholder="Full name" value="' . e((string)$contactOld['name']) . '" required></div><div class="col-md-6"><input class="form-control" type="email" name="email" placeholder="Email" value="' . e((string)($contactOld['email'] ?? '')) . '"></div><div class="col-md-6"><input class="form-control" name="phone" placeholder="Phone" value="' . e((string)($contactOld['phone'] ?? '')) . '"></div><div class="col-12 form-check ms-2"><input class="form-check-input" type="checkbox" name="is_primary" value="1" id="contact-primary"><label class="form-check-label" for="contact-primary">Primary contact</label></div><div class="col-12"><button class="btn btn-sm btn-outline-primary">Add contact</button></div></form>';
echo '</div></div></div></div>';
if (can('sla.view') || can('sla.manage')) {
echo '<div class="card mt-4"><div class="card-body"><h2 class="h5">SLA agreement</h2>';
echo '<div class="card mt-4" data-client-section="sla"><div class="card-body"><h2 class="h5">SLA agreement</h2>';
if (can('sla.manage')) { echo '<form method="post" class="row g-3"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="sla"><div class="col-md-3 form-check ms-2"><input class="form-check-input" type="checkbox" name="enabled" value="1" id="sla-enabled"' . (($slaAgreement['enabled'] ?? true) ? ' checked' : '') . '><label class="form-check-label" for="sla-enabled">Enabled</label></div><div class="col-md-4"><label class="form-label">Agreement type</label><input class="form-control" name="agreement_type" value="' . e((string)($slaAgreement['agreement_type'] ?? '')) . '"></div><div class="col-md-3"><label class="form-label">Allocated hours</label><input class="form-control" type="number" min="0" step="0.01" name="allocated_hours" value="' . e((string)($slaAgreement['allocated_hours'] ?? '0')) . '"></div><div class="col-md-3"><label class="form-label">Period</label><select class="form-select" name="period_type">'; foreach (['monthly','annual','custom'] as $period) echo '<option value="' . $period . '"' . (($slaAgreement['period_type'] ?? 'monthly') === $period ? ' selected' : '') . '>' . e(ucfirst($period)) . '</option>'; echo '</select></div><div class="col-md-3"><label class="form-label">Start date</label><input class="form-control" type="date" name="start_date" value="' . e((string)($slaAgreement['start_date'] ?? '')) . '"></div><div class="col-md-3"><label class="form-label">End date</label><input class="form-control" type="date" name="end_date" value="' . e((string)($slaAgreement['end_date'] ?? '')) . '"></div><div class="col-md-3 form-check pt-4"><input class="form-check-input" type="checkbox" name="rollover_enabled" value="1" id="sla-rollover"' . (($slaAgreement['rollover_enabled'] ?? false) ? ' checked' : '') . '><label class="form-check-label" for="sla-rollover">Rollover enabled</label></div><div class="col-12"><label class="form-label">SLA notes</label><textarea class="form-control" name="notes" rows="3">' . e((string)($slaAgreement['notes'] ?? '')) . '</textarea></div><div class="col-12"><button class="btn btn-primary">Save SLA</button></div></form>';
} elseif ($slaAgreement) echo '<dl class="row mb-0"><dt class="col-sm-3">Type</dt><dd class="col-sm-9">' . e((string)($slaAgreement['agreement_type'] ?? '—')) . '</dd><dt class="col-sm-3">Allocation</dt><dd class="col-sm-9">' . e(number_format((float)$slaAgreement['allocated_hours'], 2)) . ' hours / ' . e($slaAgreement['period_type']) . '</dd></dl>';
else echo '<p class="text-muted mb-0">No SLA agreement configured.</p>';
echo '</div></div>';
}
if (can('credentials.view') || can('credentials.manage')) {
echo '<div class="card mt-4"><div class="card-body"><h2 class="h5">Protected credentials</h2>';
echo '<div class="card mt-4" data-client-section="technical"><div class="card-body"><h2 class="h5">Saved client information</h2>';
if ($credentialErrors) echo '<div class="alert alert-danger">' . e(implode(' ', $credentialErrors)) . '</div>';
if (isset($_GET['credential_created'])) echo '<div class="alert alert-success">Credential saved securely.</div>';
foreach ($credentialRows as $credentialRow) {
@@ -550,17 +551,16 @@ if ($route === 'client') {
if (can('credentials.view') && !($revealedCredential && $revealedCredential['id'] === (int)$credentialRow['id'])) echo '<form method="post" class="d-inline"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="credential_reveal"><input type="hidden" name="credential_id" value="' . (int)$credentialRow['id'] . '"><button class="btn btn-sm btn-link p-0">Reveal once</button></form>';
echo '</div>';
}
if (can('credentials.manage')) { echo '<hr><h3 class="h6">Add credential</h3><form method="post" class="row g-2"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="credential"><div class="col-md-3"><select class="form-select" name="category">'; foreach (\App\Domain\Credential\TechnicalInformation::categories() as $category) echo '<option value="' . e($category) . '">' . e(ucfirst($category)) . '</option>'; echo '</select></div><div class="col-md-3"><input class="form-control" name="label" placeholder="Label" required></div><div class="col-md-3"><input class="form-control" name="username" placeholder="Username"></div><div class="col-md-3"><input class="form-control" type="password" name="secret" placeholder="Secret" required></div><div class="col-12"><textarea class="form-control" name="credential_notes" rows="2" placeholder="Notes"></textarea></div><div class="col-12"><button class="btn btn-primary">Encrypt and save</button></div></form>'; }
echo '</div></div>';
}
if (can('technical.view') || can('technical.manage')) {
$technicalRows = (new \App\Domain\Credential\TechnicalInformationRepository(db()))->forClient($clientId);
echo '<div class="card mt-4"><div class="card-body"><h2 class="h5">Technical information</h2>';
echo '<div class="card mt-4" data-client-section="technical"><div class="card-body"><h2 class="h5">Technical information</h2>';
foreach ($technicalRows as $technical) { $display = $technical['display']; echo '<div class="border-bottom py-2"><strong>' . e(ucfirst($technical['category'])) . '</strong><div>' . e((string)$display['label']) . ($display['username'] ? ' · ' . e((string)$display['username']) : '') . '</div><div class="small text-muted">' . nl2br(e((string)($display['notes'] ?? ''))) . '</div></div>'; }
if (can('technical.manage')) { echo '<hr><form method="post" class="row g-2"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="technical"><div class="col-md-3"><select class="form-select" name="category">'; foreach (\App\Domain\Credential\TechnicalInformationCommand::categories() as $category) echo '<option value="' . e($category) . '">' . e(ucfirst($category)) . '</option>'; echo '</select></div><div class="col-md-3"><input class="form-control" name="label" placeholder="Label" required></div><div class="col-md-3"><input class="form-control" name="username" placeholder="Username"></div><div class="col-md-3"><input class="form-control" name="notes" placeholder="Notes"></div><div class="col-12"><button class="btn btn-outline-primary">Save technical information</button></div></form>'; }
if (can('credentials.manage')) { echo '<hr><h3 class="h6">Add client information</h3><form method="post" class="row g-3"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="credential"><input type="hidden" name="category" value="other"><div class="col-md-6"><label class="form-label">Name</label><input class="form-control" name="label" placeholder="e.g. Main server" required></div><div class="col-md-6"><label class="form-label">Username</label><input class="form-control" name="username" placeholder="Username"></div><div class="col-md-6"><label class="form-label">Password</label><input class="form-control" type="password" name="secret" placeholder="Password" required></div><div class="col-md-6"><label class="form-label">Extra info</label><input class="form-control" name="credential_notes" placeholder="Extra information"></div><div class="col-12"><button class="btn btn-primary">Save client information securely</button></div></form>'; }
echo '</div></div>';
}
if (can('clients.manage')) echo '<div class="card mt-4"><div class="card-body"><h2 class="h5">Edit client</h2><form method="post" class="row g-3"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="client_update"><div class="col-md-6"><label class="form-label">Client name</label><input class="form-control" name="name" value="' . e($client['name']) . '" required></div><div class="col-md-3"><label class="form-label">Status</label><select class="form-select" name="status"><option value="active"' . ($client['status'] === 'active' ? ' selected' : '') . '>Active</option><option value="inactive"' . ($client['status'] === 'inactive' ? ' selected' : '') . '>Inactive</option></select></div><div class="col-md-3"><label class="form-label">Preferred contact</label><input class="form-control" name="preferred_contact_method" value="' . e((string)($client['preferred_contact_method'] ?? '')) . '"></div><div class="col-md-6"><label class="form-label">Support email</label><input class="form-control" type="email" name="support_email" value="' . e((string)($client['support_email'] ?? '')) . '"></div><div class="col-md-6"><label class="form-label">Support phone</label><input class="form-control" name="support_phone" value="' . e((string)($client['support_phone'] ?? '')) . '"></div><div class="col-md-6"><label class="form-label">Physical address</label><textarea class="form-control" name="physical_address" rows="3">' . e((string)($client['physical_address'] ?? '')) . '</textarea></div><div class="col-md-6"><label class="form-label">Postal address</label><textarea class="form-control" name="postal_address" rows="3">' . e((string)($client['postal_address'] ?? '')) . '</textarea></div><div class="col-12"><label class="form-label">General notes</label><textarea class="form-control" name="general_notes" rows="3">' . e((string)($client['general_notes'] ?? '')) . '</textarea></div><div class="col-12"><button class="btn btn-primary">Save client</button></div></form></div></div>';
if (can('clients.manage')) echo '<div class="card mt-4" data-client-section="overview"><div class="card-body"><h2 class="h5">Edit client</h2><form method="post" class="row g-3"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="client_update"><div class="col-md-6"><label class="form-label">Client name</label><input class="form-control" name="name" value="' . e($client['name']) . '" required></div><div class="col-md-3"><label class="form-label">Status</label><select class="form-select" name="status"><option value="active"' . ($client['status'] === 'active' ? ' selected' : '') . '>Active</option><option value="inactive"' . ($client['status'] === 'inactive' ? ' selected' : '') . '>Inactive</option></select></div><div class="col-md-3"><label class="form-label">Preferred contact</label><input class="form-control" name="preferred_contact_method" value="' . e((string)($client['preferred_contact_method'] ?? '')) . '"></div><div class="col-md-6"><label class="form-label">Support email</label><input class="form-control" type="email" name="support_email" value="' . e((string)($client['support_email'] ?? '')) . '"></div><div class="col-md-6"><label class="form-label">Support phone</label><input class="form-control" name="support_phone" value="' . e((string)($client['support_phone'] ?? '')) . '"></div><div class="col-md-6"><label class="form-label">Physical address</label><textarea class="form-control" name="physical_address" rows="3">' . e((string)($client['physical_address'] ?? '')) . '</textarea></div><div class="col-md-6"><label class="form-label">Postal address</label><textarea class="form-control" name="postal_address" rows="3">' . e((string)($client['postal_address'] ?? '')) . '</textarea></div><div class="col-12"><label class="form-label">General notes</label><textarea class="form-control" name="general_notes" rows="3">' . e((string)($client['general_notes'] ?? '')) . '</textarea></div><div class="col-12"><button class="btn btn-primary">Save client</button></div></form></div></div>';
render_footer();
exit;
}