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

View File

@ -284,10 +284,15 @@
function DoctorApp() { function DoctorApp() {
const [doctors, setDoctors] = useState([]); 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 [opdAppointments, setOpdAppointments] = useState([]);
const [opdCounts, setOpdCounts] = useState({ all: 0, waiting: 0, visited: 0, booked: 0, cancelled: 0 }); const [opdCounts, setOpdCounts] = useState({ all: 0, waiting: 0, visited: 0, booked: 0, cancelled: 0 });
const [opdFilter, setOpdFilter] = useState('all'); const [opdFilter, setOpdFilter] = useState('all');
const [opdDateMode, setOpdDateMode] = useState('today'); // 'today' or 'all'
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [mainTab, setMainTab] = useState('opd'); const [mainTab, setMainTab] = useState('opd');
@ -362,7 +367,8 @@
const res = await fetch(`/api/v1/doctor/ipd/list/${docId || 0}`); const res = await fetch(`/api/v1/doctor/ipd/list/${docId || 0}`);
const json = await res.json(); const json = await res.json();
if (json.status === 1) { if (json.status === 1) {
setIpdPatients(json.patients || []); setAllIpdPatients(json.all_patients || json.patients || []);
setMyIpdPatients(json.patients || []);
} }
} catch (err) { } catch (err) {
console.error('Failed to load IPD patients', err); console.error('Failed to load IPD patients', err);
@ -371,9 +377,10 @@
} }
}; };
const fetchOpdAppointments = async (docId) => { const fetchOpdAppointments = async (docId, dateModeOverride) => {
try { 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(); const json = await res.json();
if (json.status === 1) { if (json.status === 1) {
setOpdAppointments(json.appointments || []); setOpdAppointments(json.appointments || []);
@ -394,7 +401,7 @@
if (parsed && parsed.doctor && parsed.expires_at > Date.now()) { if (parsed && parsed.doctor && parsed.expires_at > Date.now()) {
setAuthenticatedDoctor(parsed.doctor); setAuthenticatedDoctor(parsed.doctor);
fetchIpdPatients(parsed.doctor.id); fetchIpdPatients(parsed.doctor.id);
fetchOpdAppointments(parsed.doctor.id); fetchOpdAppointments(parsed.doctor.id, opdDateMode);
} else { } else {
localStorage.removeItem(DOC_DEVICE_AUTH_KEY); localStorage.removeItem(DOC_DEVICE_AUTH_KEY);
} }
@ -447,7 +454,7 @@
localStorage.setItem(DOC_DEVICE_AUTH_KEY, JSON.stringify(deviceAuthObj)); localStorage.setItem(DOC_DEVICE_AUTH_KEY, JSON.stringify(deviceAuthObj));
setPinInput(''); setPinInput('');
fetchIpdPatients(json.doctor.id); fetchIpdPatients(json.doctor.id);
fetchOpdAppointments(json.doctor.id); fetchOpdAppointments(json.doctor.id, opdDateMode);
} else if (json.status === 2) { } else if (json.status === 2) {
setIsSettingNewPin(true); setIsSettingNewPin(true);
} else { } else {
@ -605,7 +612,7 @@
if (json.status === 1) { if (json.status === 1) {
alert(json.message || 'Prescription saved & OPD Visit completed!'); alert(json.message || 'Prescription saved & OPD Visit completed!');
setActiveOpd(null); setActiveOpd(null);
fetchOpdAppointments(authenticatedDoctor ? authenticatedDoctor.id : 0); fetchOpdAppointments(authenticatedDoctor ? authenticatedDoctor.id : 0, opdDateMode);
} else { } else {
alert(json.message || 'Failed to save prescription'); alert(json.message || 'Failed to save prescription');
} }
@ -648,7 +655,8 @@
return matchesFilter && matchesSearch; return matchesFilter && matchesSearch;
}); });
const filteredIpdPatients = ipdPatients.filter(pat => { const activeIpdSource = (ipdViewMode === 'mine') ? myIpdPatients : allIpdPatients;
const filteredIpdPatients = activeIpdSource.filter(pat => {
const q = searchQuery.toLowerCase(); const q = searchQuery.toLowerCase();
const name = (pat.patient_display_name || pat.fname || '').toLowerCase(); const name = (pat.patient_display_name || pat.fname || '').toLowerCase();
const uhid = (pat.uhid || '').toLowerCase(); const uhid = (pat.uhid || '').toLowerCase();
@ -773,7 +781,7 @@
</div> </div>
</div> </div>
<div className="d-flex align-items-center gap-2"> <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> <i className="bi bi-arrow-clockwise"></i>
</button> </button>
<button className="btn btn-sm btn-danger px-3 py-1 fw-bold rounded-pill" onClick={handleLogout} title="Lock Device Access"> <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}) <i className="bi bi-calendar-check-fill me-2"></i> OPD Appointments ({opdAppointments.length})
</button> </button>
<button className={`segmented-tab ${mainTab === 'ipd' ? 'active' : ''}`} onClick={() => setMainTab('ipd')}> <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> </button>
</div> </div>
{mainTab === 'opd' ? ( {mainTab === 'opd' ? (
<div> <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 */} {/* Status Filter Pills Bar */}
<div className="d-flex gap-2 mb-3 overflow-auto no-scrollbar pb-1"> <div className="d-flex gap-2 mb-3 overflow-auto no-scrollbar pb-1">
<button <button
@ -848,7 +877,9 @@
{filteredOpdAppointments.length === 0 ? ( {filteredOpdAppointments.length === 0 ? (
<div className="text-center text-muted py-5 bg-white rounded-4 border shadow-sm"> <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> <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> </div>
) : ( ) : (
filteredOpdAppointments.map(app => { filteredOpdAppointments.map(app => {
@ -969,9 +1000,30 @@
</div> </div>
) : ( ) : (
/* ------------------------------------------------------------- /* -------------------------------------------------------------
ADMITTED IPD PATIENTS LIST (ASSIGNED TO DOCTOR) ADMITTED IPD PATIENTS LIST (ASSIGNED TO DOCTOR / ALL ADMISSIONS)
------------------------------------------------------------- */ ------------------------------------------------------------- */
<div> <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 */} {/* Search Input Bar */}
<div className="input-group mb-3 shadow-sm rounded-pill overflow-hidden bg-white border"> <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> <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'}}> <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} <i className="bi bi-card-text me-1"></i>{pat.uhid}
</span> </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> </div>
</div> </div>