DotNet_Capstone_Project/.Net Capstone Project/patientsv2.html

505 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Patients — Clinical Insight Pro</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css">
<style>
/* The Add/Edit Patient form is long — scroll inside the dialog. */
#pt_modal .modal-content { max-height: 90vh; }
#pt_modal .modal-body { max-height: 70vh; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #1a1a1a #e3e9e9; }
#pt_modal .modal-body::-webkit-scrollbar { width: 12px; }
#pt_modal .modal-body::-webkit-scrollbar-track { background: #e3e9e9; border-radius: 6px; }
#pt_modal .modal-body::-webkit-scrollbar-thumb { background: #1a1a1a; border-radius: 6px; border: 2px solid #e3e9e9; }
/* Patient cards — inlined so the layout never depends on css/styles.css
being fresh (the avatars previously rendered as full-width bars because
.rp-avatar had no size). */
.rp-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
align-items: start;
}
.rp-card {
background: #fff; border: 1px solid #e3e9e9; border-radius: 14px;
padding: 1.4rem 1rem 1.2rem;
display: flex; flex-direction: column; align-items: center;
text-align: center; gap: .7rem;
box-shadow: 0 1px 3px rgba(16,40,40,.06);
transition: box-shadow .12s ease, transform .12s ease;
}
.rp-card:hover { box-shadow: 0 6px 18px rgba(16,40,40,.12); transform: translateY(-2px); }
.rp-avatar {
width: 64px; height: 64px; min-width: 64px; min-height: 64px; flex: 0 0 64px;
border-radius: 50%; display: flex; align-items: center; justify-content: center;
color: #fff; font-weight: 700; font-size: 1.15rem; letter-spacing: .5px; line-height: 1;
}
.rp-card-name { font-weight: 600; font-size: 1rem; color: #1f2d2d; line-height: 1.25; word-break: break-word; }
.rp-card-badge {
font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .4px;
border-radius: 999px; padding: .2rem .7rem; background: #eef3f3; color: #5f7373;
max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rp-card-sub { font-size: .78rem; color: #5f7373; }
.rp-card-actions { display: flex; gap: .35rem; flex-wrap: wrap; justify-content: center; }
.rp-card-view {
display: inline-block; font-size: .85rem; padding: .3rem 1rem;
border: 1px solid #0f6e6e; color: #0f6e6e; background: #fff;
border-radius: 8px; cursor: pointer; text-decoration: none; line-height: 1.4;
}
.rp-card-view:hover { background: #0f6e6e; color: #fff !important; }
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-ci">
<div class="container-fluid px-4">
<a class="navbar-brand" href="dashboard.html">🩺 Clinical Insight Pro</a>
<div class="ms-auto d-flex align-items-center gap-3">
<span class="navbar-text" id="clinicianLabel">Clinician</span>
<button class="btn btn-outline-light btn-sm" id="logoutBtn">Sign out</button>
</div>
</div>
</nav>
<div class="app-shell">
<aside class="app-sidebar" id="appSidebar"></aside>
<main class="app-main">
<div class="resource-page-header">
<h4>🧑‍⚕️ Patients</h4>
<span class="text-muted-ci small" id="pt_countLabel"></span>
</div>
<div class="card-ci p-3 mb-3 d-flex flex-row align-items-center gap-3 flex-wrap">
<input type="text" class="form-control resource-search-bar" id="pt_searchInput" placeholder="Search by name or patient ID…" autocomplete="off">
<button class="btn btn-ci-primary ms-auto" id="pt_addBtn">+ Add Patient</button>
</div>
<div class="card-ci p-3">
<div class="rp-card-grid" id="pt_cardGrid"></div>
<div class="table-responsive d-none">
<table class="table table-sm resource-grid-table align-middle mb-0">
<thead>
<tr><th>Name</th><th>Age</th><th>Gender</th><th>MRN</th><th>Patient ID</th><th></th></tr>
</thead>
<tbody id="pt_gridBody"></tbody>
</table>
</div>
<div id="pt_gridEmpty" class="text-muted-ci text-center py-4 d-none">No data found.</div>
</div>
</main>
</div>
<div class="toast-container position-fixed bottom-0 end-0 p-3" id="toastContainer"></div>
<div class="modal fade" id="pt_modal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="pt_modalTitle">
Add Patient
</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal">
</button>
</div>
<form id="pt_form">
<div class="modal-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label">
Medical Record Number
</label>
<input type="text"
class="form-control"
id="pt_mrn"
placeholder="Leave blank — continues MRN-IN-100006, 100007, …">
</div>
<div class="col-md-6">
<label class="form-label">Blood Group</label>
<select class="form-select" id="pt_bloodGroup">
<option value=""></option>
<option>A+</option><option>A-</option>
<option>B+</option><option>B-</option>
<option>AB+</option><option>AB-</option>
<option>O+</option><option>O-</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Email</label>
<input type="email"
class="form-control"
id="pt_email"
placeholder="name@example.com">
</div>
<div class="col-md-6">
<label class="form-label">
Given Name
</label>
<input type="text"
class="form-control"
id="pt_givenName"
required>
</div>
<div class="col-md-6">
<label class="form-label">
Family Name
</label>
<input type="text"
class="form-control"
id="pt_familyName"
required>
</div>
<div class="col-md-6">
<label class="form-label">
Birth Date
</label>
<input type="date"
class="form-control"
id="pt_birthDate">
</div>
<div class="col-md-6">
<label class="form-label">
Gender
</label>
<select class="form-select"
id="pt_gender">
<option value="female">Female</option>
<option value="male">Male</option>
<option value="other">Other</option>
<option value="unknown">Unknown</option>
</select>
</div>
<div class="col-12">
<label class="form-label">
Phone Number
</label>
<input type="text"
class="form-control"
id="pt_phone">
</div>
<div class="col-md-6">
<label class="form-label">
City
</label>
<input type="text"
class="form-control"
id="pt_city">
</div>
<div class="col-md-6">
<label class="form-label">
State
</label>
<input type="text"
class="form-control"
id="pt_state">
</div>
<div class="col-12">
<label class="form-label">
Postal Code
</label>
<input type="text"
class="form-control"
id="pt_postalCode">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">
Cancel
</button>
<button type="submit"
class="btn btn-ci-primary">
Save
</button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/config.js"></script>
<script src="js/sidebar.js"></script>
<script src="js/api.js"></script>
<script>
let ptCache = [];
let ptSearchTerm = '';
let ptModal, ptEditingId = null;
function ptEscapeHtml(s) {
if (s === null || s === undefined) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}
function ptShowToast(message, variant = 'success') {
const container = document.getElementById('toastContainer');
const el = document.createElement('div');
el.className = `toast align-items-center text-bg-${variant === 'danger' ? 'danger' : 'success'} border-0 show mb-2`;
el.innerHTML = `<div class="d-flex"><div class="toast-body">${ptEscapeHtml(message)}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" onclick="this.closest('.toast').remove()"></button></div>`;
container.appendChild(el);
setTimeout(() => el.remove(), 5000);
}
function ptFiltered() {
const term = ptSearchTerm.trim().toLowerCase();
if (!term) return ptCache;
return ptCache.filter(p => {
const fullName = (p.fullName || '').toLowerCase();
const parts = fullName.split(' ').filter(Boolean);
const nameMatch = fullName.startsWith(term) || parts.some(x => x.startsWith(term));
const idTerm = term.replace(/^pt-/, '');
const mrnTerm = term.replace(/^mrn-/, '');
const idMatch = (p.id || '').toLowerCase().replace(/^pt-/, '').includes(idTerm);
const mrnMatch = (p.medicalRecordNumber || '').toLowerCase().replace(/^mrn-/, '').includes(mrnTerm);
return nameMatch || idMatch || mrnMatch;
});
}
async function ptLoadGrid() {
try {
ptCache = await Api.getPatients();
} catch (err) {
ptShowToast(err.message, 'danger');
return;
}
const rows = ptFiltered();
document.getElementById('pt_countLabel').textContent = `${rows.length} of ${ptCache.length} patient${ptCache.length === 1 ? '' : 's'}`;
const body = document.getElementById('pt_gridBody');
const empty = document.getElementById('pt_gridEmpty');
if (rows.length === 0) {
body.innerHTML = '';
empty.classList.remove('d-none');
return;
}
empty.classList.add('d-none');
// Avatar card per patient, matching the other resource pages.
const grid = document.getElementById('pt_cardGrid');
grid.innerHTML = rows.map(p => {
const colour = ptAvatarColour(p.fullName);
return `
<div class="rp-card">
<div class="rp-avatar" style="background:${colour}">${ptEscapeHtml(ptInitials(p.fullName))}</div>
<div class="rp-card-name" title="${ptEscapeHtml(p.fullName)}">${ptEscapeHtml(p.fullName)}</div>
<div class="rp-card-badge">${p.age} YRS · ${ptEscapeHtml(p.gender).toUpperCase()}</div>
<div class="rp-card-sub">${ptEscapeHtml(p.medicalRecordNumber)}</div>
<div class="rp-card-sub">${ptEscapeHtml(p.id)}</div>
<a class="rp-card-view" style="color:${colour};border-color:${colour}"
href="dashboard.html?patient=${encodeURIComponent(p.id)}">View chart</a>
<div class="rp-card-actions">
<button class="btn btn-sm btn-outline-secondary" data-edit="${p.id}">Edit</button>
<button class="btn btn-sm btn-outline-danger" data-delete="${p.id}">Delete</button>
</div>
</div>`;
}).join('');
grid._ptRows = rows;
body.innerHTML = '';
}
const PT_AVATAR_COLOURS = ['#2e86ab','#1e8449','#d68910','#7d3c98','#c0392b','#117864','#a04000','#5499c7','#b7950b','#34495e'];
function ptInitials(name) {
return (name || '').split(' ').filter(Boolean)
.filter(part => !/^(dr|mr|mrs|ms|miss)\.?$/i.test(part))
.slice(0, 2).map(p => p[0].toUpperCase()).join('') || '?';
}
function ptAvatarColour(name) {
let hash = 0;
for (const ch of (name || '')) hash = (hash * 31 + ch.charCodeAt(0)) % 100000;
return PT_AVATAR_COLOURS[hash % PT_AVATAR_COLOURS.length];
}
function ptOpenAdd() {
ptEditingId = null;
document.getElementById('pt_modalTitle').textContent = 'Add Patient';
document.getElementById('pt_form').reset();
ptModal.show();
}
async function ptOpenEdit(row) {
try {
const p = await Api.getPatientById(row.id);
ptEditingId = p.id;
document.getElementById('pt_modalTitle').textContent =
`Edit - ${p.givenName || ''} ${p.familyName || ''}`;
document.getElementById('pt_mrn').value =
p.medicalRecordNumber || '';
document.getElementById('pt_givenName').value =
p.givenName || '';
document.getElementById('pt_familyName').value =
p.familyName || '';
document.getElementById('pt_birthDate').value =
p.birthDate
? new Date(p.birthDate).toISOString().split('T')[0]
: '';
document.getElementById('pt_gender').value =
p.gender || 'unknown';
document.getElementById('pt_phone').value =
p.telecomValue || '';
document.getElementById('pt_city').value =
p.city || '';
document.getElementById('pt_state').value =
p.state || '';
document.getElementById('pt_bloodGroup').value =
p.bloodGroup || '';
document.getElementById('pt_email').value =
p.email || '';
document.getElementById('pt_postalCode').value =
p.postalCode || '';
ptModal.show();
}
catch (err) {
ptShowToast(err.message, 'danger');
}
}
async function ptSubmit(e) {
e.preventDefault();
const payload = {
medicalRecordNumber:
document.getElementById('pt_mrn').value,
givenName:
document.getElementById('pt_givenName').value,
familyName:
document.getElementById('pt_familyName').value,
birthDate:
document.getElementById('pt_birthDate').value,
gender:
document.getElementById('pt_gender').value,
telecomValue:
document.getElementById('pt_phone').value,
city:
document.getElementById('pt_city').value,
state:
document.getElementById('pt_state').value,
postalCode:
document.getElementById('pt_postalCode').value,
bloodGroup:
document.getElementById('pt_bloodGroup').value || null,
email:
document.getElementById('pt_email').value || null
};
try {
if (ptEditingId) {
if (!payload.birthDate) {
ptShowToast('Date of birth is required to save changes.', 'danger');
return;
}
await Api.updatePatient(ptEditingId, payload);
ptShowToast('Patient updated.');
} else {
await Api.createPatient(payload);
ptShowToast('Patient created.');
}
ptModal.hide();
await ptLoadGrid();
} catch (err) {
ptShowToast(err.message, 'danger');
}
}
async function ptDelete(p) {
if (!confirm(`Delete patient ${p.fullName} and all their chart data?`)) return;
try {
await Api.deletePatient(p.id);
ptShowToast('Patient deleted.');
await ptLoadGrid();
} catch (err) {
ptShowToast(err.message, 'danger');
}
}
document.addEventListener('DOMContentLoaded', () => {
if (!sessionStorage.getItem('cip_token')) { window.location.href = 'login.html'; return; }
renderSidebar('patients');
document.getElementById('clinicianLabel').textContent = `👤 ${sessionStorage.getItem('cip_clinician') || 'Clinician'}`;
document.getElementById('logoutBtn').addEventListener('click', () => { sessionStorage.clear(); window.location.href = 'login.html'; });
ptModal = new bootstrap.Modal(document.getElementById('pt_modal'));
document.getElementById('pt_addBtn').addEventListener('click', ptOpenAdd);
document.getElementById('pt_form').addEventListener('submit', ptSubmit);
document.getElementById('pt_searchInput').addEventListener('input', (e) => {
ptSearchTerm = e.target.value;
ptLoadGrid();
});
document.getElementById('pt_cardGrid').addEventListener('click', (e) => {
const editBtn = e.target.closest('[data-edit]');
const delBtn = e.target.closest('[data-delete]');
// Rows live on the card grid now; the old table body is left empty by
// the card renderer, so reading _ptRows from it always gave [].
const rows = document.getElementById('pt_cardGrid')._ptRows || [];
if (editBtn) {
const p = rows.find(r => r.id === editBtn.dataset.edit);
if (p) ptOpenEdit(p);
} else if (delBtn) {
const p = rows.find(r => r.id === delBtn.dataset.delete);
if (p) ptDelete(p);
}
});
ptLoadGrid();
});
</script>
</body>
</html>