feat(doctor): add current date filter for OPD appointments and my assigned vs all hospital admissions toggle for IPDs

This commit is contained in:
DevSoftTech Server 2026-08-29 03:41:09 +05:30
parent cd0b78d5fd
commit 7a0631979a
2 changed files with 102 additions and 35 deletions

View File

@ -167,43 +167,54 @@ class DoctorApi extends BaseController
$db = db_connect();
helper('common');
$sql = "SELECT o.opd_id, o.opd_code, o.opd_no, o.opd_status, o.apointment_date,
o.opd_fee_desc, o.opd_fee_amount, o.payment_status, o.opd_fee_type,
coalesce(o.running_opd, 0) as running_opd,
p.id as p_id, p.p_code, p.p_fname as P_name, p.gender, p.age, p.age_in_month, p.estimate_dob, p.dob, p.mphone1,
(CASE
WHEN (coalesce(o.running_opd,0)=1 OR o.opd_fee_type=3 OR UPPER(coalesce(o.opd_fee_desc,'')) LIKE '%RUNNING%') THEN 'Running'
WHEN o.payment_mode=4 THEN 'Credit to ECHS'
WHEN o.payment_status=1 AND o.payment_mode=0 THEN 'No Cost'
WHEN o.payment_mode=1 THEN 'Cash'
WHEN o.payment_mode=2 THEN 'Bank Card'
ELSE 'Pending'
END) as opd_type,
IF(pr.id IS NULL, 0, 1) as has_prescription,
coalesce(pr.queue_no, 0) as queue_no
FROM opd_master o
JOIN patient_master p ON o.p_id = p.id
LEFT JOIN opd_prescription pr ON o.opd_id = pr.opd_id";
$dateMode = $this->request->getGet('date_mode') ?? 'today'; // 'today' or 'all'
$baseSql = "SELECT o.opd_id, o.opd_code, o.opd_no, o.opd_status, o.apointment_date,
o.opd_fee_desc, o.opd_fee_amount, o.payment_status, o.opd_fee_type,
coalesce(o.running_opd, 0) as running_opd,
p.id as p_id, p.p_code, p.p_fname as P_name, p.gender, p.age, p.age_in_month, p.estimate_dob, p.dob, p.mphone1,
(CASE
WHEN (coalesce(o.running_opd,0)=1 OR o.opd_fee_type=3 OR UPPER(coalesce(o.opd_fee_desc,'')) LIKE '%RUNNING%') THEN 'Running'
WHEN o.payment_mode=4 THEN 'Credit to ECHS'
WHEN o.payment_status=1 AND o.payment_mode=0 THEN 'No Cost'
WHEN o.payment_mode=1 THEN 'Cash'
WHEN o.payment_mode=2 THEN 'Bank Card'
ELSE 'Pending'
END) as opd_type,
IF(pr.id IS NULL, 0, 1) as has_prescription,
coalesce(pr.queue_no, 0) as queue_no
FROM opd_master o
JOIN patient_master p ON o.p_id = p.id
LEFT JOIN opd_prescription pr ON o.opd_id = pr.opd_id
WHERE 1=1";
if ($docId > 0) {
$sql .= " WHERE o.doc_id = " . (int) $docId;
$baseSql .= " AND o.doc_id = " . (int) $docId;
}
$sql .= " ORDER BY o.opd_id DESC LIMIT 100";
$sqlToday = $baseSql . " AND DATE(o.apointment_date) = CURDATE() ORDER BY o.opd_id DESC LIMIT 100";
$sqlAll = $baseSql . " ORDER BY o.opd_id DESC LIMIT 100";
$query = $db->query($sql);
$allRows = $query->getResultArray();
$queryToday = $db->query($sqlToday);
$todayRows = $queryToday->getResultArray();
$queryAll = $db->query($sqlAll);
$allRows = $queryAll->getResultArray();
$targetRows = ($dateMode === 'today') ? $todayRows : $allRows;
$appointments = [];
$counts = [
'all' => count($allRows),
'all' => count($targetRows),
'booked' => 0,
'waiting' => 0,
'visited' => 0,
'cancelled' => 0,
'today_total' => count($todayRows),
'all_total' => count($allRows),
];
foreach ($allRows as $r) {
foreach ($targetRows as $r) {
$statusInt = (int) ($r['opd_status'] ?? 0);
$statusText = 'waiting';
if ($statusInt === 0) {
@ -245,6 +256,7 @@ class DoctorApi extends BaseController
return $this->response->setJSON([
'status' => 1,
'doctor_id' => $docId,
'date_mode' => $dateMode,
'counts' => $counts,
'appointments' => $appointments,
]);

View File

@ -284,10 +284,15 @@
function DoctorApp() {
const [doctors, setDoctors] = useState([]);
const [ipdPatients, setIpdPatients] = useState([]);
const [myIpdPatients, setMyIpdPatients] = useState([]);
const [allIpdPatients, setAllIpdPatients] = useState([]);
const [ipdViewMode, setIpdViewMode] = useState('mine'); // 'mine' or 'all'
const [opdAppointments, setOpdAppointments] = useState([]);
const [opdCounts, setOpdCounts] = useState({ all: 0, waiting: 0, visited: 0, booked: 0, cancelled: 0 });
const [opdFilter, setOpdFilter] = useState('all');
const [opdDateMode, setOpdDateMode] = useState('today'); // 'today' or 'all'
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [mainTab, setMainTab] = useState('opd');
@ -362,7 +367,8 @@
const res = await fetch(`/api/v1/doctor/ipd/list/${docId || 0}`);
const json = await res.json();
if (json.status === 1) {
setIpdPatients(json.patients || []);
setAllIpdPatients(json.all_patients || json.patients || []);
setMyIpdPatients(json.patients || []);
}
} catch (err) {
console.error('Failed to load IPD patients', err);
@ -371,9 +377,10 @@
}
};
const fetchOpdAppointments = async (docId) => {
const fetchOpdAppointments = async (docId, dateModeOverride) => {
try {
const res = await fetch(`/api/v1/doctor/opd/list/${docId || 0}`);
const mode = dateModeOverride || opdDateMode;
const res = await fetch(`/api/v1/doctor/opd/list/${docId || 0}?date_mode=${mode}`);
const json = await res.json();
if (json.status === 1) {
setOpdAppointments(json.appointments || []);
@ -394,7 +401,7 @@
if (parsed && parsed.doctor && parsed.expires_at > Date.now()) {
setAuthenticatedDoctor(parsed.doctor);
fetchIpdPatients(parsed.doctor.id);
fetchOpdAppointments(parsed.doctor.id);
fetchOpdAppointments(parsed.doctor.id, opdDateMode);
} else {
localStorage.removeItem(DOC_DEVICE_AUTH_KEY);
}
@ -447,7 +454,7 @@
localStorage.setItem(DOC_DEVICE_AUTH_KEY, JSON.stringify(deviceAuthObj));
setPinInput('');
fetchIpdPatients(json.doctor.id);
fetchOpdAppointments(json.doctor.id);
fetchOpdAppointments(json.doctor.id, opdDateMode);
} else if (json.status === 2) {
setIsSettingNewPin(true);
} else {
@ -605,7 +612,7 @@
if (json.status === 1) {
alert(json.message || 'Prescription saved & OPD Visit completed!');
setActiveOpd(null);
fetchOpdAppointments(authenticatedDoctor ? authenticatedDoctor.id : 0);
fetchOpdAppointments(authenticatedDoctor ? authenticatedDoctor.id : 0, opdDateMode);
} else {
alert(json.message || 'Failed to save prescription');
}
@ -648,7 +655,8 @@
return matchesFilter && matchesSearch;
});
const filteredIpdPatients = ipdPatients.filter(pat => {
const activeIpdSource = (ipdViewMode === 'mine') ? myIpdPatients : allIpdPatients;
const filteredIpdPatients = activeIpdSource.filter(pat => {
const q = searchQuery.toLowerCase();
const name = (pat.patient_display_name || pat.fname || '').toLowerCase();
const uhid = (pat.uhid || '').toLowerCase();
@ -773,7 +781,7 @@
</div>
</div>
<div className="d-flex align-items-center gap-2">
<button className="btn btn-sm btn-outline-light rounded-circle p-2" style={{width: '36px', height: '36px'}} onClick={() => { fetchOpdAppointments(authenticatedDoctor.id); fetchIpdPatients(authenticatedDoctor.id); }} title="Refresh Queue">
<button className="btn btn-sm btn-outline-light rounded-circle p-2" style={{width: '36px', height: '36px'}} onClick={() => { fetchOpdAppointments(authenticatedDoctor.id, opdDateMode); fetchIpdPatients(authenticatedDoctor.id); }} title="Refresh Queue">
<i className="bi bi-arrow-clockwise"></i>
</button>
<button className="btn btn-sm btn-danger px-3 py-1 fw-bold rounded-pill" onClick={handleLogout} title="Lock Device Access">
@ -792,12 +800,33 @@
<i className="bi bi-calendar-check-fill me-2"></i> OPD Appointments ({opdAppointments.length})
</button>
<button className={`segmented-tab ${mainTab === 'ipd' ? 'active' : ''}`} onClick={() => setMainTab('ipd')}>
<i className="bi bi-hospital-fill me-2"></i> Admitted IPDs ({ipdPatients.length})
<i className="bi bi-hospital-fill me-2"></i> Admitted IPDs ({activeIpdSource.length})
</button>
</div>
{mainTab === 'opd' ? (
<div>
{/* Date Filter Segmented Bar (Current Date vs All Dates) */}
<div className="d-flex align-items-center justify-content-between mb-3 bg-white p-2 rounded-4 border shadow-sm flex-wrap gap-2">
<div className="d-flex gap-1 bg-light p-1 rounded-pill border">
<button
className={`btn btn-sm rounded-pill px-3 py-1 fw-bold ${opdDateMode === 'today' ? 'btn-primary shadow-sm' : 'btn-light text-secondary'}`}
onClick={() => { setOpdDateMode('today'); fetchOpdAppointments(authenticatedDoctor.id, 'today'); }}
>
<i className="bi bi-calendar-event me-1"></i> Today's OPDs ({opdCounts.today_total || 0})
</button>
<button
className={`btn btn-sm rounded-pill px-3 py-1 fw-bold ${opdDateMode === 'all' ? 'btn-primary shadow-sm' : 'btn-light text-secondary'}`}
onClick={() => { setOpdDateMode('all'); fetchOpdAppointments(authenticatedDoctor.id, 'all'); }}
>
<i className="bi bi-calendar3 me-1"></i> All Dates ({opdCounts.all_total || 0})
</button>
</div>
<span className="badge bg-primary-subtle text-primary border border-primary-subtle px-2 py-1 rounded-pill small" style={{fontSize: '11px'}}>
{opdDateMode === 'today' ? '● Current Date Only' : '● All Appointments'}
</span>
</div>
{/* Status Filter Pills Bar */}
<div className="d-flex gap-2 mb-3 overflow-auto no-scrollbar pb-1">
<button
@ -848,7 +877,9 @@
{filteredOpdAppointments.length === 0 ? (
<div className="text-center text-muted py-5 bg-white rounded-4 border shadow-sm">
<i className="bi bi-person-x fs-1 text-secondary opacity-50 d-block mb-2"></i>
No OPD appointments found for selected filter.
{opdDateMode === 'today'
? "No OPD appointments registered for today's date."
: "No OPD appointments found for selected filter."}
</div>
) : (
filteredOpdAppointments.map(app => {
@ -969,9 +1000,30 @@
</div>
) : (
/* -------------------------------------------------------------
ADMITTED IPD PATIENTS LIST (ASSIGNED TO DOCTOR)
ADMITTED IPD PATIENTS LIST (ASSIGNED TO DOCTOR / ALL ADMISSIONS)
------------------------------------------------------------- */
<div>
{/* IPD Admission View Mode Segmented Bar */}
<div className="d-flex align-items-center justify-content-between mb-3 bg-white p-2 rounded-4 border shadow-sm flex-wrap gap-2">
<div className="d-flex gap-1 bg-light p-1 rounded-pill border">
<button
className={`btn btn-sm rounded-pill px-3 py-1 fw-bold ${ipdViewMode === 'mine' ? 'btn-success text-white shadow-sm' : 'btn-light text-secondary'}`}
onClick={() => setIpdViewMode('mine')}
>
<i className="bi bi-person-check-fill me-1"></i> My Assigned Patients ({myIpdPatients.length})
</button>
<button
className={`btn btn-sm rounded-pill px-3 py-1 fw-bold ${ipdViewMode === 'all' ? 'btn-success text-white shadow-sm' : 'btn-light text-secondary'}`}
onClick={() => setIpdViewMode('all')}
>
<i className="bi bi-hospital-fill me-1"></i> All Hospital Admissions ({allIpdPatients.length})
</button>
</div>
<span className="badge bg-success-subtle text-success border border-success-subtle px-2 py-1 rounded-pill small" style={{fontSize: '11px'}}>
{ipdViewMode === 'mine' ? '● Doctor Assignments' : '● Hospital Wide IPDs'}
</span>
</div>
{/* Search Input Bar */}
<div className="input-group mb-3 shadow-sm rounded-pill overflow-hidden bg-white border">
<span className="input-group-text bg-white border-0 ps-3 text-secondary"><i className="bi bi-search"></i></span>
@ -1020,6 +1072,9 @@
<span className="badge bg-rose-50 text-danger border border-danger-subtle px-2 py-1" style={{fontSize: '11px'}}>
<i className="bi bi-card-text me-1"></i>{pat.uhid}
</span>
<span className="badge bg-light text-dark border px-2 py-1" style={{fontSize: '11px'}}>
<i className="bi bi-person-badge me-1"></i>{pat.assigned_doctors || pat.r_doc_name || `Dr. ${authenticatedDoctor.name}`}
</span>
</div>
</div>
</div>