diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 82428ad..5def21d 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -1583,6 +1583,8 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static funct $routes->post('doctor/auth/verify-pin', 'DoctorApi::verifyPin'); $routes->post('doctor/auth/set-pin', 'DoctorApi::setPin'); $routes->get('doctor/opd/list/(:num)', 'DoctorApi::opdList/$1'); + $routes->get('doctor/opd/prescription/(:num)', 'DoctorApi::getOpdPrescription/$1'); + $routes->post('doctor/opd/prescription/save/(:num)', 'DoctorApi::saveOpdPrescription/$1'); $routes->get('doctor/ipd/list/(:num)', 'DoctorApi::ipdList/$1'); $routes->get('doctor/ipd/workspace/(:num)', 'DoctorApi::patientWorkspace/$1'); $routes->post('doctor/ipd/treatment/save/(:num)', 'DoctorApi::saveTreatmentNote/$1'); diff --git a/app/Controllers/Api/v1/DoctorApi.php b/app/Controllers/Api/v1/DoctorApi.php index 64bee39..da50fa7 100644 --- a/app/Controllers/Api/v1/DoctorApi.php +++ b/app/Controllers/Api/v1/DoctorApi.php @@ -165,22 +165,176 @@ class DoctorApi extends BaseController public function opdList(int $docId) { $db = db_connect(); - $builder = $db->table('opd_master opd') - ->select('opd.*, p.fname, p.mname, p.lname, p.uhid, p.mphone1, p.gender, p.age') - ->join('patient_master p', 'p.id = opd.patient_id', 'left') - ->orderBy('opd.id', 'DESC') - ->limit(50); - if ($docId > 0 && $db->fieldExists('doctor_id', 'opd_master')) { - $builder->where('opd.doctor_id', $docId); + $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.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"; + + if ($docId > 0) { + $sql .= " WHERE o.doc_id = " . (int) $docId; } - $records = $builder->get()->getResultArray(); + $sql .= " ORDER BY o.opd_id DESC LIMIT 100"; + + $query = $db->query($sql); + $allRows = $query->getResultArray(); + + $appointments = []; + $counts = [ + 'all' => count($allRows), + 'booked' => 0, + 'waiting' => 0, + 'visited' => 0, + 'cancelled' => 0, + ]; + + foreach ($allRows as $r) { + $statusInt = (int) ($r['opd_status'] ?? 0); + $statusText = 'waiting'; + if ($statusInt === 0) { + $statusText = 'booked'; + $counts['booked']++; + } elseif ($statusInt === 1) { + $statusText = 'waiting'; + $counts['waiting']++; + } elseif ($statusInt === 2) { + $statusText = 'visited'; + $counts['visited']++; + } elseif ($statusInt === 3) { + $statusText = 'cancelled'; + $counts['cancelled']++; + } else { + $counts['waiting']++; + } + + $r['status_label'] = ucfirst($statusText); + $r['status_key'] = $statusText; + $r['gender_label'] = ((int)($r['gender'] ?? 1) === 1) ? 'Male' : 'Female'; + $r['patient_display_name'] = $r['P_name'] ?? 'Patient'; + $r['uhid'] = $r['p_code'] ?? ''; + $appointments[] = $r; + } return $this->response->setJSON([ 'status' => 1, 'doctor_id' => $docId, - 'appointments' => $records, + 'counts' => $counts, + 'appointments' => $appointments, + ]); + } + + /** + * GET api/v1/doctor/opd/prescription/(:num) + */ + public function getOpdPrescription(int $opdId) + { + $db = db_connect(); + + $opdRow = $db->table('opd_master o') + ->select('o.*, p.p_code, p.p_fname, p.gender, p.age, p.mphone1, d.p_fname as doc_name') + ->join('patient_master p', 'p.id = o.p_id', 'left') + ->join('doctor_master d', 'd.id = o.doc_id', 'left') + ->where('o.opd_id', $opdId) + ->get() + ->getRowArray(); + + if (! $opdRow) { + return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'OPD record not found']); + } + + $prescRow = $db->table('opd_prescription') + ->where('opd_id', $opdId) + ->get() + ->getRowArray() ?? []; + + return $this->response->setJSON([ + 'status' => 1, + 'opd' => [ + 'opd_id' => (int) $opdRow['opd_id'], + 'opd_code' => $opdRow['opd_code'], + 'patient_name' => $opdRow['p_fname'], + 'uhid' => $opdRow['p_code'], + 'gender' => ((int)$opdRow['gender'] === 1) ? 'Male' : 'Female', + 'age' => $opdRow['age'], + 'doctor_name' => $opdRow['doc_name'], + 'opd_status' => (int) $opdRow['opd_status'], + ], + 'prescription' => $prescRow, + ]); + } + + /** + * POST api/v1/doctor/opd/prescription/save/(:num) + */ + public function saveOpdPrescription(int $opdId) + { + $db = db_connect(); + $post = $this->request->getPost() ?: ($this->request->getJSON(true) ?? []); + + $opdRow = $db->table('opd_master')->where('opd_id', $opdId)->get()->getRowArray(); + if (! $opdRow) { + return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'OPD record not found']); + } + + $docId = (int) ($post['doctor_id'] ?? $opdRow['doc_id']); + $pId = (int) ($opdRow['p_id'] ?? 0); + + $vitals = [ + 'temp' => trim((string) ($post['temp'] ?? '')), + 'pulse' => trim((string) ($post['pulse'] ?? '')), + 'bp' => trim((string) ($post['bp_systolic'] ?? '')), + 'diastolic' => trim((string) ($post['bp_diastolic'] ?? '')), + 'spo2' => trim((string) ($post['spo2'] ?? '')), + 'weight' => trim((string) ($post['weight'] ?? '')), + ]; + + $chiefComplaints = trim((string) ($post['chief_complaints'] ?? '')); + $provisionalDiagnosis = trim((string) ($post['provisional_diagnosis'] ?? '')); + $advice = trim((string) ($post['advice'] ?? '')); + $investigation = trim((string) ($post['investigation'] ?? '')); + $nextVisit = trim((string) ($post['next_visit'] ?? '')); + $jsonMedicines = is_array($post['medicines'] ?? null) ? json_encode($post['medicines']) : (string) ($post['medicines'] ?? '[]'); + + $prescData = array_merge($vitals, [ + 'opd_id' => $opdId, + 'doc_id' => $docId, + 'p_id' => $pId, + 'date' => date('Y-m-d'), + 'chief_complaints' => $chiefComplaints, + 'provisional_diagnosis' => $provisionalDiagnosis, + 'advice' => $advice, + 'investigation' => $investigation, + 'next_visit' => $nextVisit, + 'json_medicines' => $jsonMedicines, + ]); + + $existing = $db->table('opd_prescription')->where('opd_id', $opdId)->get()->getRowArray(); + if ($existing) { + $db->table('opd_prescription')->where('opd_id', $opdId)->update($prescData); + } else { + $db->table('opd_prescription')->insert($prescData); + } + + // Update OPD status to Visited (2) + $db->table('opd_master')->where('opd_id', $opdId)->update(['opd_status' => 2]); + + return $this->response->setJSON([ + 'status' => 1, + 'message' => 'Prescription saved & OPD Visit marked completed successfully', ]); } @@ -191,15 +345,15 @@ class DoctorApi extends BaseController { $db = db_connect(); $builder = $db->table('ipd_master ipd') - ->select('ipd.*, p.fname, p.mname, p.lname, p.uhid, p.mphone1, p.gender, p.age, b.bed_code, b.bed_number, w.ward_name') - ->join('patient_master p', 'p.id = ipd.patient_id', 'left') + ->select('ipd.*, p.p_fname as fname, p.p_code as uhid, p.mphone1, p.gender, p.age, b.bed_code, b.bed_number, w.ward_name') + ->join('patient_master p', 'p.id = ipd.p_id', 'left') ->join('bed_master b', 'b.id = ipd.bed_id', 'left') ->join('ward_master w', 'w.id = b.ward_id', 'left') ->where('ipd.status', 'admitted') ->orderBy('ipd.id', 'DESC'); - if ($docId > 0 && $db->fieldExists('doctor_id', 'ipd_master')) { - $builder->where('ipd.doctor_id', $docId); + if ($docId > 0 && $db->fieldExists('doc_id', 'ipd_master')) { + $builder->where('ipd.doc_id', $docId); } $patients = $builder->get()->getResultArray(); diff --git a/public/App/Doctor/index.html b/public/App/Doctor/index.html index f5c02f0..a6025cd 100644 --- a/public/App/Doctor/index.html +++ b/public/App/Doctor/index.html @@ -24,7 +24,6 @@ --primary-bg: #0f172a; --card-bg: #1e293b; --accent-blue: #0d6efd; - --accent-indigo: #6610f2; --accent-green: #198754; --accent-amber: #ffc107; --accent-red: #dc3545; @@ -97,13 +96,21 @@ overflow: hidden; } - .vitals-pill { - font-size: 12px; - padding: 4px 10px; - border-radius: 20px; - background: #f1f5f9; + .chip-btn { + font-size: 11px; + padding: 3px 8px; + border-radius: 14px; + border: 1px solid #cbd5e1; + background: #f8fafc; color: #334155; - font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; + } + + .chip-btn:hover, .chip-btn.active { + background: #0d6efd; + color: #ffffff; + border-color: #0d6efd; } @@ -120,12 +127,17 @@ const [doctors, setDoctors] = useState([]); const [ipdPatients, setIpdPatients] = useState([]); const [opdAppointments, setOpdAppointments] = useState([]); + const [opdCounts, setOpdCounts] = useState({ all: 0, waiting: 0, visited: 0, booked: 0, cancelled: 0 }); + const [opdFilter, setOpdFilter] = useState('all'); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); - const [mainTab, setMainTab] = useState('ipd'); // 'ipd', 'opd', 'discharge' + const [mainTab, setMainTab] = useState('opd'); // Default to OPD Appointments as requested! + + // Active IPD / OPD Workspace state const [activePatient, setActivePatient] = useState(null); + const [activeOpd, setActiveOpd] = useState(null); const [workspaceData, setWorkspaceData] = useState([]); - const [emrTab, setEmrTab] = useState('vitals'); // 'vitals', 'progress', 'rx', 'lab' + const [emrTab, setEmrTab] = useState('vitals'); const [installPrompt, setInstallPrompt] = useState(null); // Auth state @@ -137,15 +149,31 @@ const [newPin, setNewPin] = useState(''); const [confirmPin, setConfirmPin] = useState(''); - // Clinical Progress Note Form - const [doctorNote, setDoctorNote] = useState(''); - const [doctorAdvice, setDoctorAdvice] = useState(''); + // OPD Prescription Consultation Form State (Optimized for Mobile Screens) + const [prescForm, setPrescForm] = useState({ + chief_complaints: '', + provisional_diagnosis: '', + investigation: '', + advice: '', + next_visit: '', + temp: '', + pulse: '', + bp_systolic: '', + bp_diastolic: '', + spo2: '', + weight: '' + }); - // Quick E-Rx Form - const [rxForm, setRxForm] = useState({ drug_name: '', dosage: '', frequency: '1-0-1', duration: '5 days', instructions: '' }); + // Medicines Repeater Array + const [medicines, setMedicines] = useState([ + { drug_name: '', dosage: '1 Tab', frequency: '1-0-1', duration: '5 Days', instructions: 'After Food' } + ]); - // Lab Order Form - const [labOrderText, setLabOrderText] = useState(''); + // Quick Chips presets for Mobile Typing + const COMPLAINT_CHIPS = ['Fever', 'Cough', 'Swelling', 'Joint Pain', 'Headache', 'Back Ache', 'Nausea / Vomiting', 'Weakness']; + const DIAGNOSIS_CHIPS = ['Acute Rheumatic Fever', 'Viral Infection', 'Hypertension', 'Type 2 Diabetes', 'GERD', 'Bronchitis', 'Osteoarthritis']; + const DRUG_CHIPS = ['Paracetamol 500mg', 'Amoxicillin 500mg', 'Pantoprazole 40mg', 'Cetirizine 10mg', 'Ibuprofen 400mg', 'Azithromycin 500mg', 'Multivitamin']; + const LAB_CHIPS = ['CBC', 'Serum Creatinine', 'LFT', 'KFT', 'Chest X-Ray PA', 'ECG', 'Urine RE']; // Fetch doctors list const fetchDoctors = async () => { @@ -155,7 +183,6 @@ if (json.status === 1 && json.data) { setDoctors(json.data); - // Check URL query parameters for doctor_id (QR scan or link) const params = new URLSearchParams(window.location.search); const docIdParam = params.get('doctor_id'); if (docIdParam) { @@ -190,25 +217,15 @@ const json = await res.json(); if (json.status === 1) { setOpdAppointments(json.appointments || []); + if (json.counts) { + setOpdCounts(json.counts); + } } } catch (err) { console.error('Failed to load OPD appointments', err); } }; - // Fetch patient bedside workspace - const fetchWorkspace = async (ipdId) => { - try { - const res = await fetch(`/api/v1/doctor/ipd/workspace/${ipdId}`); - const json = await res.json(); - if (json.status === 1) { - setWorkspaceData(json.entries || []); - } - } catch (err) { - console.error('Failed to load patient workspace', err); - } - }; - // Restore 30-Day Device Authentication useEffect(() => { const savedAuth = localStorage.getItem(DOC_DEVICE_AUTH_KEY); @@ -265,7 +282,6 @@ if (json.status === 1 && json.doctor) { setAuthenticatedDoctor(json.doctor); - // Save 30-day device authentication const deviceAuthObj = { doctor: json.doctor, expires_at: Date.now() + THIRTY_DAYS_MS @@ -284,7 +300,6 @@ } }; - // Set Doctor PIN const handleSaveNewPin = async () => { if (!newPin || newPin.length < 4) { setPinError('PIN must be 4 to 6 numeric digits.'); @@ -308,7 +323,7 @@ const json = await res.json(); if (json.status === 1) { - alert('Security PIN set successfully! You can now login with your PIN.'); + alert('Security PIN set successfully! Enter your PIN to unlock.'); setIsSettingNewPin(false); setNewPin(''); setConfirmPin(''); @@ -326,118 +341,116 @@ localStorage.removeItem(DOC_DEVICE_AUTH_KEY); setPinInput(''); setActivePatient(null); + setActiveOpd(null); }; - const openPatientWorkspace = (patient) => { - setActivePatient(patient); - fetchWorkspace(patient.id); - }; - - // Save Doctor Daily Rounds / Treatment Note - const handleSaveDoctorTreatmentNote = async () => { - if (!activePatient) return; - if (!doctorNote.trim()) { - alert('Please enter doctor treatment / daily progress note.'); - return; - } - + // Open OPD Consultation / Prescription View + const openOpdConsultation = async (app) => { + setActiveOpd(app); try { - const body = new URLSearchParams(); - body.append('doctor_id', authenticatedDoctor ? authenticatedDoctor.id : 0); - body.append('doctor_name', authenticatedDoctor ? authenticatedDoctor.name : 'Doctor'); - body.append('treatment_text', doctorNote); - body.append('general_note', doctorAdvice); - - const res = await fetch(`/api/v1/doctor/ipd/treatment/save/${activePatient.id}`, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: body.toString() - }); + const res = await fetch(`/api/v1/doctor/opd/prescription/${app.opd_id}`); const json = await res.json(); - if (json.status === 1) { - alert(json.message || 'Doctor note saved'); - setDoctorNote(''); - setDoctorAdvice(''); - fetchWorkspace(activePatient.id); + if (json.status === 1 && json.prescription) { + const pr = json.prescription; + setPrescForm({ + chief_complaints: pr.chief_complaints || '', + provisional_diagnosis: pr.provisional_diagnosis || '', + investigation: pr.investigation || '', + advice: pr.advice || '', + next_visit: pr.next_visit || '', + temp: pr.temp || '', + pulse: pr.pulse || '', + bp_systolic: pr.bp || '', + bp_diastolic: pr.diastolic || '', + spo2: pr.spo2 || '', + weight: pr.weight || '' + }); + if (pr.json_medicines) { + try { + const parsedMeds = JSON.parse(pr.json_medicines); + if (Array.isArray(parsedMeds) && parsedMeds.length > 0) { + setMedicines(parsedMeds); + } + } catch (e) {} + } } else { - alert(json.message || 'Failed to save note'); + setPrescForm({ + chief_complaints: '', provisional_diagnosis: '', investigation: '', advice: '', next_visit: '', + temp: '', pulse: '', bp_systolic: '', bp_diastolic: '', spo2: '', weight: '' + }); + setMedicines([{ drug_name: '', dosage: '1 Tab', frequency: '1-0-1', duration: '5 Days', instructions: 'After Food' }]); } } catch (err) { - alert('Error submitting note'); + console.error('Failed to load OPD prescription', err); } }; - // Save Quick E-Prescription Order - const handleSaveQuickRx = async () => { - if (!activePatient) return; - if (!rxForm.drug_name.trim()) { - alert('Please enter Drug / Medicine Name.'); - return; - } - - const rxText = `Rx: ${rxForm.drug_name} | Dosage: ${rxForm.dosage} | Freq: ${rxForm.frequency} | Duration: ${rxForm.duration} ${rxForm.instructions ? '(' + rxForm.instructions + ')' : ''}`; + // Save OPD Prescription & Mark Visited + const handleSaveOpdPrescription = async () => { + if (!activeOpd) return; try { const body = new URLSearchParams(); body.append('doctor_id', authenticatedDoctor ? authenticatedDoctor.id : 0); - body.append('doctor_name', authenticatedDoctor ? authenticatedDoctor.name : 'Doctor'); - body.append('treatment_text', rxText); + Object.keys(prescForm).forEach(k => body.append(k, prescForm[k])); + body.append('medicines', JSON.stringify(medicines)); - const res = await fetch(`/api/v1/doctor/ipd/treatment/save/${activePatient.id}`, { + const res = await fetch(`/api/v1/doctor/opd/prescription/save/${activeOpd.opd_id}`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() }); const json = await res.json(); if (json.status === 1) { - alert('E-Prescription order added to patient record!'); - setRxForm({ drug_name: '', dosage: '', frequency: '1-0-1', duration: '5 days', instructions: '' }); - fetchWorkspace(activePatient.id); + alert(json.message || 'Prescription saved & OPD Visit completed!'); + setActiveOpd(null); + fetchOpdAppointments(authenticatedDoctor ? authenticatedDoctor.id : 0); + } else { + alert(json.message || 'Failed to save prescription'); } } catch (err) { - alert('Error submitting prescription'); + alert('Error saving OPD consultation'); } }; - // Save Lab Test Order - const handleSaveLabOrder = async () => { - if (!activePatient || !labOrderText.trim()) { - alert('Please enter Lab / Pathology / Radiology test orders.'); - return; - } + // Add/Remove Medicine Repeater Rows + const addMedicineRow = (drugName = '') => { + setMedicines([...medicines, { drug_name: drugName, dosage: '1 Tab', frequency: '1-0-1', duration: '5 Days', instructions: 'After Food' }]); + }; - const labText = `Lab & Diagnostics Order: ${labOrderText}`; + const removeMedicineRow = (idx) => { + if (medicines.length === 1) return; + setMedicines(medicines.filter((_, i) => i !== idx)); + }; - try { - const body = new URLSearchParams(); - body.append('doctor_id', authenticatedDoctor ? authenticatedDoctor.id : 0); - body.append('doctor_name', authenticatedDoctor ? authenticatedDoctor.name : 'Doctor'); - body.append('treatment_text', labText); + const updateMedicineRow = (idx, field, val) => { + const updated = [...medicines]; + updated[idx][field] = val; + setMedicines(updated); + }; - const res = await fetch(`/api/v1/doctor/ipd/treatment/save/${activePatient.id}`, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: body.toString() - }); - const json = await res.json(); - if (json.status === 1) { - alert('Lab & Diagnostic order submitted!'); - setLabOrderText(''); - fetchWorkspace(activePatient.id); - } - } catch (err) { - alert('Error submitting lab order'); + // Quick Chip Appenders + const appendChipText = (field, text) => { + const current = prescForm[field]; + if (!current) { + setPrescForm({ ...prescForm, [field]: text }); + } else if (!current.includes(text)) { + setPrescForm({ ...prescForm, [field]: `${current}, ${text}` }); } }; - const filteredIpdPatients = ipdPatients.filter(p => { - const name = `${p.fname || ''} ${p.lname || ''}`.toLowerCase(); + const filteredOpdAppointments = opdAppointments.filter(app => { + const matchesFilter = opdFilter === 'all' || app.status_key === opdFilter; const q = searchQuery.toLowerCase(); - return name.includes(q) || (p.uhid && p.uhid.toLowerCase().includes(q)) || (p.bed_code && p.bed_code.toLowerCase().includes(q)); + const name = (app.patient_display_name || '').toLowerCase(); + const uhid = (app.uhid || '').toLowerCase(); + const opdCode = (app.opd_code || '').toLowerCase(); + const matchesSearch = searchQuery === '' || name.includes(q) || uhid.includes(q) || opdCode.includes(q); + return matchesFilter && matchesSearch; }); // ------------------------------------------------------------- - // RENDER: LOCK SCREEN (DOCTOR PIN AUTHENTICATION) + // RENDER: LOCK SCREEN (PIN AUTHENTICATION) // ------------------------------------------------------------- if (!authenticatedDoctor) { return ( @@ -531,14 +544,6 @@ // ------------------------------------------------------------- return (