diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 21cfe17..3bdc9cc 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -712,6 +712,17 @@ $routes->get('Opd_prescription/opd_invest_profile_master_get/(:num)', 'Opd_presc
$routes->post('Opd_prescription/opd_invest_profile_master_save', 'Opd_prescription::opd_invest_profile_master_save');
$routes->post('Opd_prescription/opd_invest_profile_master_remove/(:num)', 'Opd_prescription::opd_invest_profile_master_remove/$1');
+// Clinical Master (Complaints + Diagnosis)
+$routes->get('Opd_prescription/clinical_master_workspace', 'Opd_prescription::clinical_master_workspace', ['filter' => 'permission:doctor_work.medicine.manage,doctor_work.access']);
+$routes->get('Opd_prescription/complaints_master_data', 'Opd_prescription::complaints_master_data');
+$routes->get('Opd_prescription/complaints_master_get/(:num)', 'Opd_prescription::complaints_master_get/$1');
+$routes->post('Opd_prescription/complaints_master_save', 'Opd_prescription::complaints_master_save');
+$routes->post('Opd_prescription/complaints_master_remove/(:num)', 'Opd_prescription::complaints_master_remove/$1');
+$routes->get('Opd_prescription/disease_master_data', 'Opd_prescription::disease_master_data');
+$routes->get('Opd_prescription/disease_master_get/(:num)', 'Opd_prescription::disease_master_get/$1');
+$routes->post('Opd_prescription/disease_master_save', 'Opd_prescription::disease_master_save');
+$routes->post('Opd_prescription/disease_master_remove/(:num)', 'Opd_prescription::disease_master_remove/$1');
+
$routes->get('Opd_prescription/medicine_search', 'Opd_prescription::medicine_search');
$routes->get('Opd_prescription/medicine_substitutes', 'Opd_prescription::medicine_substitutes');
$routes->get('Opd_prescription/medicine_list/(:num)', 'Opd_prescription::medicine_list/$1');
diff --git a/app/Controllers/Opd_prescription.php b/app/Controllers/Opd_prescription.php
index 0e6c582..fcea750 100644
--- a/app/Controllers/Opd_prescription.php
+++ b/app/Controllers/Opd_prescription.php
@@ -4052,6 +4052,282 @@ class Opd_prescription extends BaseController
// ─────────────────────────────────────────────────────────────────────────
+ // ─── Clinical Master Workspace (Complaints + Diagnosis/Disease) ────────
+
+ public function clinical_master_workspace(): string
+ {
+ return view('billing/opd_clinical_master_workspace');
+ }
+
+ // --- Complaints Master -------------------------------------------------
+
+ public function complaints_master_data(): \CodeIgniter\HTTP\ResponseInterface
+ {
+ if (! $this->db->tableExists('complaints_master')) {
+ return $this->response->setJSON(['data' => [], 'recordsTotal' => 0, 'recordsFiltered' => 0]);
+ }
+
+ $start = (int) ($this->request->getGet('start') ?? 0);
+ $length = (int) ($this->request->getGet('length') ?? 25);
+ $filter = trim((string) ($this->request->getGet('filter') ?? ''));
+ $snomed = (int) ($this->request->getGet('snomed_only') ?? 0);
+
+ $qb = $this->db->table('complaints_master');
+
+ if ($filter !== '') {
+ $qb->groupStart()
+ ->like('Name', $filter)
+ ->orLike('name_hinglish', $filter)
+ ->orLike('keywords', $filter)
+ ->groupEnd();
+ }
+ if ($snomed === 1) {
+ $qb->where('snomed_concept_id IS NOT NULL')->where("snomed_concept_id != ''");
+ }
+
+ $total = (clone $qb)->countAllResults(false);
+ $rows = $qb->select('Code, Name, show_in_short, name_hinglish, ai_hint, is_active, snomed_concept_id, snomed_term')
+ ->orderBy('show_in_short', 'DESC')
+ ->orderBy('Name', 'ASC')
+ ->limit($length, $start)
+ ->get()
+ ->getResultArray();
+
+ return $this->response->setJSON([
+ 'recordsTotal' => $total,
+ 'recordsFiltered' => $total,
+ 'data' => $rows,
+ ]);
+ }
+
+ public function complaints_master_get(int $code): \CodeIgniter\HTTP\ResponseInterface
+ {
+ if (! $this->db->tableExists('complaints_master')) {
+ return $this->response->setStatusCode(404)->setJSON(['error' => 'Table not found']);
+ }
+ $row = $this->db->table('complaints_master')
+ ->where('Code', $code)
+ ->get(1)
+ ->getRowArray();
+ if (! $row) {
+ return $this->response->setStatusCode(404)->setJSON(['error' => 'Not found']);
+ }
+ return $this->response->setJSON(['row' => $row]);
+ }
+
+ public function complaints_master_save(): \CodeIgniter\HTTP\ResponseInterface
+ {
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
+ }
+ if (! $this->db->tableExists('complaints_master')) {
+ return $this->response->setJSON(['update' => 0, 'error_text' => 'Table not found']);
+ }
+
+ $code = (int) ($this->request->getPost('Code') ?? 0);
+ $name = strtoupper(trim((string) ($this->request->getPost('Name') ?? '')));
+ $nameHinglish = trim((string) ($this->request->getPost('name_hinglish') ?? ''));
+ $keywords = trim((string) ($this->request->getPost('keywords') ?? ''));
+ $aiHint = trim((string) ($this->request->getPost('ai_hint') ?? ''));
+ $showInShort = (int) ($this->request->getPost('show_in_short') ?? 0);
+ $isActive = (int) ($this->request->getPost('is_active') ?? 1);
+ $snomedConceptId = trim((string) ($this->request->getPost('snomed_concept_id') ?? ''));
+ $snomedTerm = trim((string) ($this->request->getPost('snomed_term') ?? ''));
+
+ if ($name === '') {
+ return $this->response->setJSON(['update' => 0, 'error_text' => 'Name is required']);
+ }
+
+ $payload = [
+ 'Name' => $name,
+ 'name_hinglish' => $nameHinglish !== '' ? $nameHinglish : null,
+ 'keywords' => $keywords !== '' ? $keywords : null,
+ 'ai_hint' => $aiHint !== '' ? $aiHint : null,
+ 'show_in_short' => $showInShort ? 1 : 0,
+ 'is_active' => $isActive ? 1 : 0,
+ 'snomed_concept_id'=> $snomedConceptId !== '' ? $snomedConceptId : null,
+ 'snomed_term' => $snomedTerm !== '' ? $snomedTerm : null,
+ 'updated_at' => date('Y-m-d H:i:s'),
+ ];
+
+ if ($code > 0) {
+ $this->db->table('complaints_master')->where('Code', $code)->update($payload);
+ $newCode = $code;
+ $msg = 'Complaint updated';
+ } else {
+ // auto Code
+ $maxCode = (int) ($this->db->table('complaints_master')->selectMax('Code')->get()->getRowArray()['Code'] ?? 0);
+ $newCode = $maxCode + 1;
+ $payload['Code'] = $newCode;
+ $payload['created_at'] = date('Y-m-d H:i:s');
+ $this->db->table('complaints_master')->insert($payload);
+ $msg = 'Complaint added';
+ }
+
+ return $this->response->setJSON([
+ 'update' => 1,
+ 'error_text' => $msg,
+ 'code' => $newCode,
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ public function complaints_master_remove(int $code): \CodeIgniter\HTTP\ResponseInterface
+ {
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
+ }
+ if (! $this->db->tableExists('complaints_master')) {
+ return $this->response->setJSON(['update' => 0, 'error_text' => 'Table not found']);
+ }
+
+ // Soft-delete: set is_active = 0
+ $this->db->table('complaints_master')
+ ->where('Code', $code)
+ ->update(['is_active' => 0, 'updated_at' => date('Y-m-d H:i:s')]);
+
+ return $this->response->setJSON([
+ 'update' => 1,
+ 'error_text' => 'Complaint deactivated',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ // --- Disease/Diagnosis Master ------------------------------------------
+
+ public function disease_master_data(): \CodeIgniter\HTTP\ResponseInterface
+ {
+ if (! $this->db->tableExists('disease_master')) {
+ return $this->response->setJSON(['data' => [], 'recordsTotal' => 0, 'recordsFiltered' => 0]);
+ }
+
+ $start = (int) ($this->request->getGet('start') ?? 0);
+ $length = (int) ($this->request->getGet('length') ?? 25);
+ $filter = trim((string) ($this->request->getGet('filter') ?? ''));
+ $snomed = (int) ($this->request->getGet('snomed_only') ?? 0);
+
+ $fields = $this->db->getFieldNames('disease_master') ?? [];
+ $hasIsActive = in_array('is_active', $fields, true);
+
+ $qb = $this->db->table('disease_master');
+
+ if ($filter !== '') {
+ $qb->groupStart()->like('Name', $filter)->groupEnd();
+ }
+ if ($snomed === 1) {
+ $qb->where('snomed_concept_id IS NOT NULL')->where("snomed_concept_id != ''");
+ }
+
+ $total = (clone $qb)->countAllResults(false);
+ $select = 'Code, Name' . ($hasIsActive ? ', is_active' : '') . ', snomed_concept_id, snomed_term';
+ $rows = $qb->select($select)
+ ->orderBy('Name', 'ASC')
+ ->limit($length, $start)
+ ->get()
+ ->getResultArray();
+
+ return $this->response->setJSON([
+ 'recordsTotal' => $total,
+ 'recordsFiltered' => $total,
+ 'data' => $rows,
+ ]);
+ }
+
+ public function disease_master_get(int $code): \CodeIgniter\HTTP\ResponseInterface
+ {
+ if (! $this->db->tableExists('disease_master')) {
+ return $this->response->setStatusCode(404)->setJSON(['error' => 'Table not found']);
+ }
+ $row = $this->db->table('disease_master')
+ ->where('Code', $code)
+ ->get(1)
+ ->getRowArray();
+ if (! $row) {
+ return $this->response->setStatusCode(404)->setJSON(['error' => 'Not found']);
+ }
+ return $this->response->setJSON(['row' => $row]);
+ }
+
+ public function disease_master_save(): \CodeIgniter\HTTP\ResponseInterface
+ {
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
+ }
+ if (! $this->db->tableExists('disease_master')) {
+ return $this->response->setJSON(['update' => 0, 'error_text' => 'Table not found']);
+ }
+
+ $code = (int) ($this->request->getPost('Code') ?? 0);
+ $name = strtoupper(trim((string) ($this->request->getPost('Name') ?? '')));
+ $snomedConceptId = trim((string) ($this->request->getPost('snomed_concept_id') ?? ''));
+ $snomedTerm = trim((string) ($this->request->getPost('snomed_term') ?? ''));
+ $isActive = (int) ($this->request->getPost('is_active') ?? 1);
+
+ if ($name === '') {
+ return $this->response->setJSON(['update' => 0, 'error_text' => 'Name is required']);
+ }
+
+ $fields = $this->db->getFieldNames('disease_master') ?? [];
+ $hasIsActive = in_array('is_active', $fields, true);
+
+ $payload = [
+ 'Name' => $name,
+ 'snomed_concept_id' => $snomedConceptId !== '' ? $snomedConceptId : null,
+ 'snomed_term' => $snomedTerm !== '' ? $snomedTerm : null,
+ ];
+ if ($hasIsActive) {
+ $payload['is_active'] = $isActive ? 1 : 0;
+ }
+
+ if ($code > 0) {
+ $this->db->table('disease_master')->where('Code', $code)->update($payload);
+ $newCode = $code;
+ $msg = 'Diagnosis updated';
+ } else {
+ $maxCode = (int) ($this->db->table('disease_master')->selectMax('Code')->get()->getRowArray()['Code'] ?? 0);
+ $newCode = $maxCode + 1;
+ $payload['Code'] = $newCode;
+ $this->db->table('disease_master')->insert($payload);
+ $msg = 'Diagnosis added';
+ }
+
+ return $this->response->setJSON([
+ 'update' => 1,
+ 'error_text' => $msg,
+ 'code' => $newCode,
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ public function disease_master_remove(int $code): \CodeIgniter\HTTP\ResponseInterface
+ {
+ if (! $this->request->isAJAX()) {
+ return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
+ }
+ if (! $this->db->tableExists('disease_master')) {
+ return $this->response->setJSON(['update' => 0, 'error_text' => 'Table not found']);
+ }
+
+ $fields = $this->db->getFieldNames('disease_master') ?? [];
+ if (in_array('is_active', $fields, true)) {
+ $this->db->table('disease_master')->where('Code', $code)->update(['is_active' => 0]);
+ } else {
+ $this->db->table('disease_master')->where('Code', $code)->delete();
+ }
+
+ return $this->response->setJSON([
+ 'update' => 1,
+ 'error_text' => 'Diagnosis deactivated',
+ 'csrfName' => csrf_token(),
+ 'csrfHash' => csrf_hash(),
+ ]);
+ }
+
+ // ─────────────────────────────────────────────────────────────────────────
+
public function opd_medicince_duplicate_report()
{
$medicineModel = new OpdMedicineModel($this->db);
diff --git a/app/Database/Migrations/2026-05-20-000001_AddSnomedColumnsToClinicalMasters.php b/app/Database/Migrations/2026-05-20-000001_AddSnomedColumnsToClinicalMasters.php
new file mode 100644
index 0000000..03e7c19
--- /dev/null
+++ b/app/Database/Migrations/2026-05-20-000001_AddSnomedColumnsToClinicalMasters.php
@@ -0,0 +1,104 @@
+db->tableExists('complaints_master')) {
+ $fields = $this->db->getFieldNames('complaints_master') ?? [];
+
+ if (! in_array('snomed_concept_id', $fields, true)) {
+ $this->forge->addColumn('complaints_master', [
+ 'snomed_concept_id' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 20,
+ 'null' => true,
+ 'default' => null,
+ 'after' => 'is_active',
+ ],
+ ]);
+ }
+
+ if (! in_array('snomed_term', $fields, true)) {
+ $this->forge->addColumn('complaints_master', [
+ 'snomed_term' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 255,
+ 'null' => true,
+ 'default' => null,
+ 'after' => 'snomed_concept_id',
+ ],
+ ]);
+ }
+ }
+
+ // disease_master
+ if ($this->db->tableExists('disease_master')) {
+ $fields = $this->db->getFieldNames('disease_master') ?? [];
+
+ if (! in_array('snomed_concept_id', $fields, true)) {
+ $this->forge->addColumn('disease_master', [
+ 'snomed_concept_id' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 20,
+ 'null' => true,
+ 'default' => null,
+ ],
+ ]);
+ }
+
+ if (! in_array('snomed_term', $fields, true)) {
+ $this->forge->addColumn('disease_master', [
+ 'snomed_term' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 255,
+ 'null' => true,
+ 'default' => null,
+ ],
+ ]);
+ }
+
+ if (! in_array('is_active', $fields, true)) {
+ $this->forge->addColumn('disease_master', [
+ 'is_active' => [
+ 'type' => 'TINYINT',
+ 'constraint' => 1,
+ 'default' => 1,
+ 'after' => 'snomed_term',
+ ],
+ ]);
+ }
+ }
+ }
+
+ public function down(): void
+ {
+ if ($this->db->tableExists('complaints_master')) {
+ $fields = $this->db->getFieldNames('complaints_master') ?? [];
+ if (in_array('snomed_concept_id', $fields, true)) {
+ $this->forge->dropColumn('complaints_master', 'snomed_concept_id');
+ }
+ if (in_array('snomed_term', $fields, true)) {
+ $this->forge->dropColumn('complaints_master', 'snomed_term');
+ }
+ }
+
+ if ($this->db->tableExists('disease_master')) {
+ $fields = $this->db->getFieldNames('disease_master') ?? [];
+ if (in_array('snomed_concept_id', $fields, true)) {
+ $this->forge->dropColumn('disease_master', 'snomed_concept_id');
+ }
+ if (in_array('snomed_term', $fields, true)) {
+ $this->forge->dropColumn('disease_master', 'snomed_term');
+ }
+ if (in_array('is_active', $fields, true)) {
+ $this->forge->dropColumn('disease_master', 'is_active');
+ }
+ }
+ }
+}
diff --git a/app/Views/billing/opd_clinical_master_workspace.php b/app/Views/billing/opd_clinical_master_workspace.php
new file mode 100644
index 0000000..72ba247
--- /dev/null
+++ b/app/Views/billing/opd_clinical_master_workspace.php
@@ -0,0 +1,661 @@
+Clinical Master
+ Manage Chief Complaints & Diagnosis options with SNOMED CT coding
+
+
+
+
+
+
+
+ Name
+ SNOMED CT
+ Short
+ Active
+ Action
+
+
+ Loading…