diff --git a/app/Controllers/Api/v1/DoctorApi.php b/app/Controllers/Api/v1/DoctorApi.php index a94fef8..7ca5353 100644 --- a/app/Controllers/Api/v1/DoctorApi.php +++ b/app/Controllers/Api/v1/DoctorApi.php @@ -347,6 +347,31 @@ class DoctorApi extends BaseController $docId = (int) ($post['doctor_id'] ?? $opdRow['doc_id']); $pId = (int) ($opdRow['p_id'] ?? 0); + $imageBase64 = $post['image_base64'] ?? null; + $imageUrl = ''; + if (! empty($imageBase64)) { + if (preg_match('/^data:image\/(\w+);base64,/', $imageBase64, $type)) { + $imgData = substr($imageBase64, strpos($imageBase64, ',') + 1); + $ext = strtolower($type[1]); + if ($ext === 'jpeg') $ext = 'jpg'; + $binary = base64_decode($imgData); + if ($binary !== false) { + $uploadDir = FCPATH . 'uploads/doctor_notes/'; + if (! is_dir($uploadDir)) { + @mkdir($uploadDir, 0777, true); + } + $filename = 'opd_rx_' . $opdId . '_' . time() . '_' . rand(100, 999) . '.' . $ext; + file_put_contents($uploadDir . $filename, $binary); + $imageUrl = '/uploads/doctor_notes/' . $filename; + } + } + } + + $adviceText = trim((string) ($post['advice'] ?? '')); + if (! empty($imageUrl)) { + $adviceText .= ($adviceText ? "\n" : '') . '[Handwritten Rx Photo: ' . $imageUrl . ']'; + } + $prescData = [ 'opd_id' => $opdId, 'doc_id' => $docId, @@ -363,7 +388,7 @@ class DoctorApi extends BaseController 'complaints' => trim((string) ($post['chief_complaints'] ?? '')), 'Finding_Examinations' => trim((string) ($post['finding_examinations'] ?? '')), 'diagnosis' => trim((string) ($post['provisional_diagnosis'] ?? '')), - 'advice' => trim((string) ($post['advice'] ?? '')), + 'advice' => $adviceText, 'investigation' => trim((string) ($post['investigation'] ?? '')), 'next_visit' => trim((string) ($post['next_visit'] ?? '')), ]; @@ -500,20 +525,45 @@ class DoctorApi extends BaseController { $post = $this->request->getPost() ?: ($this->request->getJSON(true) ?? []); $treatmentText = trim((string) ($post['treatment_text'] ?? '')); + $imageBase64 = $post['image_base64'] ?? null; - if ($treatmentText === '') { - return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Doctor treatment note / order is required']); + $imageUrl = ''; + if (! empty($imageBase64)) { + if (preg_match('/^data:image\/(\w+);base64,/', $imageBase64, $type)) { + $imgData = substr($imageBase64, strpos($imageBase64, ',') + 1); + $ext = strtolower($type[1]); + if ($ext === 'jpeg') $ext = 'jpg'; + $binary = base64_decode($imgData); + if ($binary !== false) { + $uploadDir = FCPATH . 'uploads/doctor_notes/'; + if (! is_dir($uploadDir)) { + @mkdir($uploadDir, 0777, true); + } + $filename = 'note_ipd_' . $ipdId . '_' . time() . '_' . rand(100, 999) . '.' . $ext; + file_put_contents($uploadDir . $filename, $binary); + $imageUrl = '/uploads/doctor_notes/' . $filename; + } + } + } + + if ($treatmentText === '' && empty($imageUrl)) { + return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Please enter clinical notes or attach a handwritten note photo']); } $docId = (int) ($post['doctor_id'] ?? 0); $doctorName = trim((string) ($post['doctor_name'] ?? 'Doctor')); + $fullNoteText = '[Doctor Note] ' . $treatmentText; + if (! empty($imageUrl)) { + $fullNoteText .= ' [IMAGE_ATTACHMENT:' . $imageUrl . ']'; + } + $data = [ 'ipd_id' => $ipdId, 'entry_type' => 'treatment', 'recorded_at' => date('Y-m-d H:i:s'), - 'treatment_text' => '[Doctor Note] ' . $treatmentText, - 'general_note' => (string) ($post['general_note'] ?? ''), + 'treatment_text' => $fullNoteText, + 'general_note' => $imageUrl ? ('Attachment: ' . $imageUrl) : (string) ($post['general_note'] ?? ''), 'recorded_by' => 'Dr. ' . $doctorName, 'recorded_by_id' => $docId, 'created_at' => date('Y-m-d H:i:s'), @@ -524,7 +574,8 @@ class DoctorApi extends BaseController return $this->response->setJSON([ 'status' => 1, - 'message' => 'Doctor clinical note / treatment saved successfully', + 'message' => 'Doctor clinical note / handwritten photo saved successfully', + 'image_url' => $imageUrl, ]); } } diff --git a/public/App/Doctor/index.html b/public/App/Doctor/index.html index cc67004..de83338 100644 --- a/public/App/Doctor/index.html +++ b/public/App/Doctor/index.html @@ -302,6 +302,9 @@ const [activeOpd, setActiveOpd] = useState(null); const [workspaceEntries, setWorkspaceEntries] = useState([]); const [newTreatmentNote, setNewTreatmentNote] = useState(''); + const [noteImageBase64, setNoteImageBase64] = useState(''); + const [noteImagePreview, setNoteImagePreview] = useState(''); + const [installPrompt, setInstallPrompt] = useState(null); // Auth state @@ -511,6 +514,7 @@ const openIpdWorkspace = async (patient) => { setActivePatient(patient); + clearCapturedImage(); try { const res = await fetch(`/api/v1/doctor/ipd/workspace/${patient.ipd_id}`); const json = await res.json(); @@ -522,14 +526,36 @@ } }; + const handleImageCapture = (e) => { + const file = e.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (event) => { + setNoteImageBase64(event.target.result); + setNoteImagePreview(event.target.result); + }; + reader.readAsDataURL(file); + }; + + const clearCapturedImage = () => { + setNoteImageBase64(''); + setNoteImagePreview(''); + }; + const handleSaveTreatmentNote = async () => { - if (!activePatient || !newTreatmentNote.trim()) return; + if (!activePatient || (!newTreatmentNote.trim() && !noteImageBase64)) { + alert('Please enter clinical observations or take a picture of your handwritten note.'); + 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', newTreatmentNote); + if (noteImageBase64) { + body.append('image_base64', noteImageBase64); + } const res = await fetch(`/api/v1/doctor/ipd/treatment/save/${activePatient.ipd_id}`, { method: 'POST', @@ -538,8 +564,9 @@ }); const json = await res.json(); if (json.status === 1) { - alert(json.message || 'Clinical treatment note saved successfully!'); + alert(json.message || 'Clinical note / handwritten note saved successfully!'); setNewTreatmentNote(''); + clearCapturedImage(); openIpdWorkspace(activePatient); } else { alert(json.message || 'Failed to save treatment note'); @@ -551,6 +578,7 @@ const openOpdConsultation = async (app) => { setActiveOpd(app); + clearCapturedImage(); try { const res = await fetch(`/api/v1/doctor/opd/prescription/${app.opd_id}`); const json = await res.json(); @@ -602,6 +630,9 @@ body.append('doctor_id', authenticatedDoctor ? authenticatedDoctor.id : 0); Object.keys(prescForm).forEach(k => body.append(k, prescForm[k])); body.append('medicines', JSON.stringify(medicines)); + if (noteImageBase64) { + body.append('image_base64', noteImageBase64); + } const res = await fetch(`/api/v1/doctor/opd/prescription/save/${activeOpd.opd_id}`, { method: 'POST', @@ -612,6 +643,7 @@ if (json.status === 1) { alert(json.message || 'Prescription saved & OPD Visit completed!'); setActiveOpd(null); + clearCapturedImage(); fetchOpdAppointments(authenticatedDoctor ? authenticatedDoctor.id : 0, opdDateMode); } else { alert(json.message || 'Failed to save prescription'); @@ -672,6 +704,28 @@ return nameStr.substring(0, 2).toUpperCase(); }; + const renderNoteContent = (entry) => { + const text = entry.treatment_text || entry.general_note || ''; + const match = text.match(/\[IMAGE_ATTACHMENT:(.*?)\]/); + const cleanText = text.replace(/\[IMAGE_ATTACHMENT:.*?\]/, '').replace('[Doctor Note] ', ''); + + return ( +
+ {cleanText &&
{cleanText}
} + {match && match[1] && ( +
+ + Doctor Handwritten Note Photo + + + Doctor Handwritten Note + +
+ )} +
+ ); + }; + // ------------------------------------------------------------- // RENDER: LOCK SCREEN (PIN AUTHENTICATION) // ------------------------------------------------------------- @@ -784,8 +838,8 @@ - @@ -1179,12 +1233,30 @@ - {/* Write Doctor Round Note / Treatment Order */} + {/* Write Doctor Round Note / Take Picture of Handwritten Note */}
- +
+ + +
+ + {/* Image Preview Box if picture taken */} + {noteImagePreview && ( +
+ Handwritten Note Attached + Handwritten Note Preview + +
+ )} +
@@ -1200,7 +1272,7 @@ {e.entry_type} {e.recorded_at} - by {e.recorded_by} -
{e.treatment_text || e.general_note}
+ {renderNoteContent(e)} )) )} @@ -1208,7 +1280,7 @@ ) : activeOpd ? ( /* ------------------------------------------------------------- - OPD CONSULTATION & PRESCRIPTION FORM (NATIVE CONTROLS COPY) + OPD CONSULTATION & PRESCRIPTION FORM ------------------------------------------------------------- */
@@ -1242,6 +1314,31 @@
+ {/* Camera / Photo Attach Handwritten Prescription Option */} +
+
+
+
Attach Photo of Handwritten Rx Note
+ Snap a picture of your paper prescription to send to Pharmacy / Patient +
+ +
+ + {/* Preview if image captured */} + {noteImagePreview && ( +
+ Rx Photo Attached + Handwritten Rx Preview + +
+ )} +
+ {/* Vitals Entry Bar */}
Patient Vitals (BP, Pulse, Temp, SpO2, Weight)