Upload files to ".Net Capstone Project"
This commit is contained in:
parent
54d93213ed
commit
6c0b37042a
504
.Net Capstone Project/patientsv2.html
Normal file
504
.Net Capstone Project/patientsv2.html
Normal file
@ -0,0 +1,504 @@
|
||||
<!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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||
}
|
||||
|
||||
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>
|
||||
Loading…
Reference in New Issue
Block a user