feat: add HealthPlix OPD sync flow with settings, diagnostics, and sync ID persistence

This commit is contained in:
DevSoftTech Server 2026-04-27 08:32:51 +05:30
parent f610adf2fe
commit 6ae9a3516f
16 changed files with 1858 additions and 3 deletions

View File

@ -502,6 +502,7 @@ $routes->post('Opd/confirm_payment', 'Opd::confirm_payment');
$routes->post('Opd/update_doc_date/(:num)', 'Opd::update_doc_date/$1');
$routes->post('Opd/opd_discount_update/(:num)', 'Opd::opd_discount_update/$1');
$routes->post('Opd/opd_cancel/(:num)', 'Opd::opd_cancel/$1');
$routes->post('Opd/healthplix_push/(:num)', 'Opd::healthplix_push/$1');
$routes->match(['get', 'post'], 'Opd/opd_load_doc/(:num)', 'Opd::opd_load_doc/$1');
$routes->match(['get', 'post'], 'Opd/opd_file_list/(:num)', 'Opd::opd_file_list/$1');
$routes->match(['get', 'post'], 'Opd/opd_file_last_list', 'Opd::opd_file_last_list');
@ -548,6 +549,10 @@ $routes->get('AbdmTaskBoard', 'AbdmTaskBoard::index', ['filter' => $abdmPermFilt
$routes->get('AbdmTaskBoard/list', 'AbdmTaskBoard::list', ['filter' => $abdmPermFilter]);
$routes->post('AbdmTaskBoard/perform_action', 'AbdmTaskBoard::performAction', ['filter' => $abdmPermFilter]);
$routes->post('AbdmTaskBoard/mark_status', 'AbdmTaskBoard::markStatus', ['filter' => $abdmPermFilter]);
$healthplixPermFilter = 'permission:admin.access,admin.settings';
$routes->post('HealthplixGateway/generate_token', 'HealthplixGateway::generateToken', ['filter' => $healthplixPermFilter]);
$routes->post('HealthplixGateway/register_patient', 'HealthplixGateway::registerPatient', ['filter' => $healthplixPermFilter]);
$routes->post('HealthplixGateway/book_appointment', 'HealthplixGateway::bookAppointment', ['filter' => $healthplixPermFilter]);
$routes->get('Opd_prescription/fhir_bundle/(:num)', 'Opd_prescription::fhir_bundle/$1');
$routes->get('Opd_prescription/fhir_bundle/(:num)/(:num)', 'Opd_prescription::fhir_bundle/$1/$2');
$routes->get('Opd_prescription/fhir_bundle_history/(:num)', 'Opd_prescription::fhir_bundle_history/$1');
@ -957,6 +962,9 @@ $routes->group('setting', static function($routes) {
$routes->post('admin/ai-settings/save', 'Setting\\AiSettings::save');
$routes->post('admin/ai-settings/test', 'Setting\\AiSettings::test');
$routes->post('admin/ai-settings/usage', 'Setting\\AiSettings::usage');
$routes->get('admin/healthplix-settings', 'Setting\\HealthplixSettings::index');
$routes->post('admin/healthplix-settings/save', 'Setting\\HealthplixSettings::save');
$routes->post('admin/healthplix-settings/test', 'Setting\\HealthplixSettings::test');
$routes->get('admin/hospital-profile', 'Setting\\HospitalProfile::index');
$routes->post('admin/hospital-profile/save', 'Setting\\HospitalProfile::save');
$routes->post('admin/hospital-profile/delete-logo', 'Setting\\HospitalProfile::deleteLogo');

View File

@ -0,0 +1,113 @@
<?php
namespace App\Controllers;
use App\Libraries\HealthplixService;
class HealthplixGateway extends BaseController
{
public function generateToken()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
}
$service = new HealthplixService();
$result = $service->generateToken();
if (($result['ok'] ?? false) !== true) {
return $this->response->setJSON([
'ok' => 0,
'error_text' => (string) ($result['error'] ?? 'HealthPlix token generation failed'),
]);
}
return $this->response->setJSON([
'ok' => 1,
'status' => (int) ($result['status'] ?? 200),
'message' => 'HealthPlix token generated',
]);
}
public function registerPatient()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
}
$payload = $this->resolvePayload();
if (empty($payload)) {
return $this->response->setJSON([
'ok' => 0,
'error_text' => 'Patient payload is required',
]);
}
$service = new HealthplixService();
$result = $service->registerPatient($payload);
if (($result['ok'] ?? false) !== true) {
return $this->response->setJSON([
'ok' => 0,
'error_text' => (string) ($result['error'] ?? 'HealthPlix patient registration failed'),
'status' => (int) ($result['status'] ?? 0),
]);
}
return $this->response->setJSON([
'ok' => 1,
'status' => (int) ($result['status'] ?? 200),
'data' => $result['data'] ?? [],
]);
}
public function bookAppointment()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
}
$payload = $this->resolvePayload();
if (empty($payload)) {
return $this->response->setJSON([
'ok' => 0,
'error_text' => 'Appointment payload is required',
]);
}
$service = new HealthplixService();
$result = $service->bookAppointment($payload);
if (($result['ok'] ?? false) !== true) {
return $this->response->setJSON([
'ok' => 0,
'error_text' => (string) ($result['error'] ?? 'HealthPlix appointment booking failed'),
'status' => (int) ($result['status'] ?? 0),
]);
}
return $this->response->setJSON([
'ok' => 1,
'status' => (int) ($result['status'] ?? 200),
'data' => $result['data'] ?? [],
]);
}
/**
* @return array<string, mixed>
*/
private function resolvePayload(): array
{
$json = $this->request->getJSON(true);
if (is_array($json) && ! empty($json)) {
return $json;
}
$payloadJson = trim((string) $this->request->getPost('payload_json'));
if ($payloadJson !== '') {
$decoded = json_decode($payloadJson, true);
if (is_array($decoded)) {
return $decoded;
}
}
return [];
}
}

View File

@ -3,6 +3,7 @@
namespace App\Controllers;
use App\Controllers\BaseController;
use App\Libraries\HealthplixService;
use App\Models\OpdModel;
use App\Models\PaymentModel;
use CodeIgniter\I18n\Time;
@ -1692,9 +1693,173 @@ class Opd extends BaseController
$query = $this->db->query($sql);
$data['bank_data'] = $query->getResult();
// ── HealthPlix sync status for invoice button ─────────────────────────
$data['healthplix_sync_applicable'] = false;
$data['healthplix_sync_done'] = false;
$data['healthplix_log'] = null;
$healthplixEnabled = $this->readHealthplixSetting('HEALTHPLIX_ENABLED') === '1';
if ($healthplixEnabled && (int) ($opdRow->payment_status ?? 0) === 1) {
$docId = (int) ($opdRow->doc_id ?? 0);
$doctorIdentifier = '';
if ($docId > 0 && $this->db->tableExists('doctor_master')) {
$identifierField = $this->resolveDoctorHealthplixIdentifierField();
if ($identifierField !== null) {
$docRow2 = $this->db->table('doctor_master')
->select($identifierField)
->where('id', $docId)
->get(1)
->getRowArray();
$doctorIdentifier = trim((string) ($docRow2[$identifierField] ?? ''));
}
}
if ($doctorIdentifier !== '') {
$data['healthplix_sync_applicable'] = true;
if ($this->db->tableExists('abdm_api_logs')) {
$hpLog = $this->db->table('abdm_api_logs')
->where('channel', 'healthplix')
->where('event_type', 'healthplix.appointment.book')
->where('entity_type', 'opd')
->where('entity_id', (string) $opdId)
->where('status', 'success')
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
if (! empty($hpLog)) {
$data['healthplix_sync_done'] = true;
$data['healthplix_log'] = $hpLog;
}
}
}
}
// OPD-level sync ID stored in table after successful appointment push.
$data['healthplix_opd_sync_id'] = trim((string) ($opdRow->healthplix_sync_id ?? ''));
// ─────────────────────────────────────────────────────────────────────
return view('billing/opd_invoice_V', $data);
}
public function healthplix_push(int $opdId)
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'message' => 'Invalid request']);
}
$opdId = (int) $opdId;
if ($opdId <= 0) {
return $this->response->setJSON([
'ok' => 0,
'message' => 'Invalid OPD ID',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
// If a success log already exists, just return it.
if ($this->db->tableExists('abdm_api_logs')) {
$existing = $this->db->table('abdm_api_logs')
->where('channel', 'healthplix')
->where('event_type', 'healthplix.appointment.book')
->where('entity_type', 'opd')
->where('entity_id', (string) $opdId)
->where('status', 'success')
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
if (! empty($existing)) {
return $this->response->setJSON([
'ok' => 1,
'already_synced' => true,
'message' => 'Already synced to HealthPlix.',
'log' => $existing,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
}
$this->syncPaidOpdToHealthplix($opdId);
// Check result
$synced = false;
$log = null;
$errorMsg = 'Sync failed. Check HealthPlix settings or logs.';
if ($this->db->tableExists('abdm_api_logs')) {
$log = $this->db->table('abdm_api_logs')
->where('channel', 'healthplix')
->where('event_type', 'healthplix.appointment.book')
->where('entity_type', 'opd')
->where('entity_id', (string) $opdId)
->where('status', 'success')
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
$synced = ! empty($log);
if (! $synced) {
$errLog = $this->db->table('abdm_api_logs')
->where('channel', 'healthplix')
->where('status', 'error')
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
// If no OPD-level error, look for any recent healthplix error (token/patient)
if (empty($errLog)) {
$errLog = $this->db->table('abdm_api_logs')
->where('channel', 'healthplix')
->where('status', 'error')
->where('created_at >=', date('Y-m-d H:i:s', time() - 30))
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
}
if (! empty($errLog)) {
$errDetail = trim((string) ($errLog['error_message'] ?? ''));
$errCode = (int) ($errLog['response_code'] ?? 0);
$errEvent = trim((string) ($errLog['event_type'] ?? ''));
if ($errCode > 0 && $errDetail !== '') {
$errorMsg = '[' . $errEvent . '] HTTP ' . $errCode . ': ' . $errDetail;
} elseif ($errCode > 0) {
$errorMsg = '[' . $errEvent . '] HealthPlix API returned HTTP ' . $errCode . '.';
} elseif ($errDetail !== '') {
$errorMsg = '[' . $errEvent . '] ' . $errDetail;
}
} else {
// No API log at all — likely a gate/config issue.
$healthplixEnabled = $this->readHealthplixSetting('HEALTHPLIX_ENABLED') === '1';
if (! $healthplixEnabled) {
$errorMsg = 'HealthPlix integration is disabled in settings.';
} else {
$identifierField = $this->resolveDoctorHealthplixIdentifierField();
$hasDoctorId = false;
if ($identifierField !== null) {
$opdRow2 = $this->db->table('opd_master')->select('doc_id')->where('opd_id', $opdId)->get(1)->getRowArray();
$docId2 = (int) ($opdRow2['doc_id'] ?? 0);
if ($docId2 > 0) {
$docRow2 = $this->db->table('doctor_master')->select($identifierField)->where('id', $docId2)->get(1)->getRowArray();
$hasDoctorId = trim((string) ($docRow2[$identifierField] ?? '')) !== '';
}
}
if (! $hasDoctorId) {
$errorMsg = 'Doctor does not have a HealthPlix Doctor Identifier set. Add it in Doctor Master.';
}
}
}
}
}
return $this->response->setJSON([
'ok' => $synced ? 1 : 0,
'synced' => $synced,
'message' => $synced
? 'Appointment registered in HealthPlix.'
: $errorMsg,
'log' => $log,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function invoice_print(int $opdId)
{
$data = $this->buildOpdInvoicePrintData($opdId);
@ -4229,6 +4394,7 @@ class Opd extends BaseController
$this->db->table('opd_master')->where('opd_id', $oid)->update($data);
$this->autoCreateOpdQueue($oid);
$this->syncPaidOpdToHealthplix($oid);
return $this->response->setJSON([
'update' => 1,
@ -4267,6 +4433,7 @@ class Opd extends BaseController
$this->db->table('opd_master')->where('opd_id', $oid)->update($data);
$this->autoCreateOpdQueue($oid);
$this->syncPaidOpdToHealthplix($oid);
return $this->response->setJSON([
'update' => 1,
@ -4316,6 +4483,7 @@ class Opd extends BaseController
$this->db->table('opd_master')->where('opd_id', $oid)->update($data);
$this->autoCreateOpdQueue($oid);
$this->syncPaidOpdToHealthplix($oid);
return $this->response->setJSON([
'update' => 1,
@ -4338,6 +4506,7 @@ class Opd extends BaseController
$this->db->table('opd_master')->where('opd_id', $oid)->update($data);
$this->autoCreateOpdQueue($oid);
$this->syncPaidOpdToHealthplix($oid);
return $this->response->setJSON([
'update' => 1,
@ -4355,6 +4524,250 @@ class Opd extends BaseController
]);
}
private function syncPaidOpdToHealthplix(int $opdId): void
{
if ($opdId <= 0 || ! $this->db->tableExists('opd_master')) {
return;
}
// Skip duplicate push if one success log already exists for this OPD.
if ($this->db->tableExists('abdm_api_logs')) {
$existing = $this->db->table('abdm_api_logs')
->where('channel', 'healthplix')
->where('event_type', 'healthplix.appointment.book')
->where('entity_type', 'opd')
->where('entity_id', (string) $opdId)
->where('status', 'success')
->get(1)
->getRowArray();
if (! empty($existing)) {
return;
}
}
try {
$opd = $this->db->table('opd_master')
->where('opd_id', $opdId)
->get(1)
->getRowArray();
if (! is_array($opd) || empty($opd)) {
return;
}
$patientId = (int) ($opd['p_id'] ?? 0);
$patient = [];
if ($patientId > 0 && $this->db->tableExists('patient_master')) {
$patient = $this->db->table('patient_master')
->where('id', $patientId)
->get(1)
->getRowArray() ?? [];
}
$doctorRow = [];
$doctorId = (int) ($opd['doc_id'] ?? 0);
if ($doctorId > 0 && $this->db->tableExists('doctor_master')) {
$doctorRow = $this->db->table('doctor_master')
->where('id', $doctorId)
->get(1)
->getRowArray() ?? [];
}
$doctorIdentifier = $this->readHealthplixSetting('HEALTHPLIX_DOCTOR_IDENTIFIER');
$doctorEmail = $this->readHealthplixSetting('HEALTHPLIX_DOCTOR_EMAIL');
$serviceIdentifier = $this->readHealthplixSetting('HEALTHPLIX_SERVICE_IDENTIFIER');
$serviceName = $this->readHealthplixSetting('HEALTHPLIX_SERVICE_NAME');
if (! empty($doctorRow)) {
$doctorIdentifierField = $this->resolveDoctorHealthplixIdentifierField();
if ($doctorIdentifierField !== null) {
$rowDoctorIdentifier = trim((string) ($doctorRow[$doctorIdentifierField] ?? ''));
if ($rowDoctorIdentifier !== '') {
$doctorIdentifier = $rowDoctorIdentifier;
}
}
$rowDoctorEmail = trim((string) ($doctorRow['email1'] ?? ''));
if ($rowDoctorEmail !== '') {
$doctorEmail = $rowDoctorEmail;
}
}
// Appointment booking should use a valid HealthPlix doctor UUID, not local doctor id.
if ($doctorIdentifier === '') {
return;
}
$service = new HealthplixService();
$patientPayload = [
'external_patient_id' => (string) $patientId,
'patient_code' => trim((string) ($patient['p_code'] ?? $patientId)),
'full_name' => trim((string) ($patient['p_fname'] ?? ($opd['P_name'] ?? ''))),
'mobile' => trim((string) ($patient['mphone1'] ?? '')),
'email' => trim((string) ($patient['email1'] ?? '')),
'gender' => $this->mapPatientGenderToHealthplix((string) ($patient['gender'] ?? '')),
'dob' => trim((string) ($patient['dob'] ?? '')),
'estimate_dob' => trim((string) ($patient['estimate_dob'] ?? '')),
'age_years' => trim((string) ($patient['age'] ?? '')),
'age_months' => trim((string) ($patient['age_in_month'] ?? '')),
'address_line1' => trim((string) ($patient['add1'] ?? '')),
'city' => trim((string) ($patient['city'] ?? '')),
'district' => trim((string) ($patient['district'] ?? '')),
'state' => trim((string) ($patient['state'] ?? '')),
'zip' => trim((string) ($patient['zip'] ?? '')),
'aadhar' => trim((string) ($patient['udai'] ?? '')),
'abha_id' => '',
'trigger' => 'opd.payment.confirmed',
];
$patientResult = $service->registerPatient($patientPayload);
$patientStatus = (int) ($patientResult['status'] ?? 0);
$patientAlreadyExists = $patientStatus === 409;
if (($patientResult['ok'] ?? false) !== true && ! $patientAlreadyExists) {
return;
}
// Persist patient sync id when HealthPlix accepted/recognized this patient.
$patientSyncFallback = trim((string) ($patient['p_code'] ?? $patientId));
$patientSyncId = $this->extractHealthplixSyncId($patientResult, $patientSyncFallback);
$this->persistHealthplixSyncId('patient_master', 'id', $patientId, $patientSyncId);
$payload = [
'external_appointment_id' => (string) $opdId,
'appointment_id' => (string) ($opd['opd_code'] ?? ''),
'appointment_datetime' => trim((string) ($opd['apointment_date'] ?? '')),
'patient_identifier' => (string) ($patient['p_code'] ?? $patientId),
'patient_name' => trim((string) ($patient['p_fname'] ?? ($opd['P_name'] ?? ''))),
'patient_mobile' => trim((string) ($patient['mphone1'] ?? '')),
'doctor_identifier' => $doctorIdentifier,
'doctor_name' => trim((string) ($opd['doc_name'] ?? '')),
'doctor_email' => $doctorEmail,
'service_identifier' => $serviceIdentifier,
'service_name' => $serviceName !== '' ? $serviceName : trim((string) ($opd['opd_fee_desc'] ?? 'Consultation')),
'price' => (float) ($opd['opd_fee_amount'] ?? 0),
'local_trigger' => 'opd.payment.confirmed',
];
$appointmentResult = $service->bookAppointment($payload);
if (($appointmentResult['ok'] ?? false) === true) {
$appointmentSyncFallback = trim((string) ($opd['opd_code'] ?? $opdId));
$appointmentSyncId = $this->extractHealthplixSyncId($appointmentResult, $appointmentSyncFallback);
$this->persistHealthplixSyncId('opd_master', 'opd_id', $opdId, $appointmentSyncId);
}
} catch (\Throwable $e) {
// Never block OPD payment flow due to external integration errors.
}
}
/**
* @param array<string, mixed> $result
*/
private function extractHealthplixSyncId(array $result, string $fallback = ''): string
{
$data = (array) ($result['data'] ?? []);
$candidates = [
$data['sync_id'] ?? null,
$data['reference_id'] ?? null,
$data['referenceId'] ?? null,
$data['request_id'] ?? null,
$data['appointment_id'] ?? null,
$data['appointment_identifier'] ?? null,
$data['patient_id'] ?? null,
$data['patient_identifier'] ?? null,
$data['id'] ?? null,
$data['uuid'] ?? null,
$result['sync_id'] ?? null,
];
foreach ($candidates as $candidate) {
$value = trim((string) $candidate);
if ($value !== '') {
return $value;
}
}
return trim($fallback);
}
private function persistHealthplixSyncId(string $table, string $idField, int $entityId, string $syncId): void
{
if ($entityId <= 0 || $syncId === '') {
return;
}
if (! $this->db->tableExists($table)) {
return;
}
$fields = array_map(
static fn (object $field): string => (string) ($field->name ?? ''),
$this->db->getFieldData($table)
);
if (! in_array($idField, $fields, true) || ! in_array('healthplix_sync_id', $fields, true)) {
return;
}
$this->db->table($table)
->where($idField, $entityId)
->update(['healthplix_sync_id' => $syncId]);
}
private function mapPatientGenderToHealthplix(string $gender): string
{
$gender = trim(strtolower($gender));
if ($gender === '1' || $gender === 'male' || $gender === 'm') {
return 'male';
}
if ($gender === '2' || $gender === 'female' || $gender === 'f') {
return 'female';
}
return 'other';
}
private function readHealthplixSetting(string $name): string
{
$value = trim((string) env($name, ''));
if ($value !== '') {
return $value;
}
if (defined($name)) {
$defined = trim((string) constant($name));
if ($defined !== '') {
return $defined;
}
}
if (! $this->db->tableExists('hospital_setting')) {
return '';
}
$row = $this->db->table('hospital_setting')
->select('s_value')
->where('s_name', $name)
->get(1)
->getRowArray();
return trim((string) ($row['s_value'] ?? ''));
}
private function resolveDoctorHealthplixIdentifierField(): ?string
{
if (! $this->db->tableExists('doctor_master')) {
return null;
}
$fields = $this->db->getFieldNames('doctor_master') ?? [];
foreach (['healthplix_doctor_identifier', 'healthplix_identifier'] as $field) {
if (in_array($field, $fields, true)) {
return $field;
}
}
return null;
}
private function autoCreateOpdQueue(int $opdId): void
{
if ($opdId <= 0 || ! $this->db->tableExists('opd_prescription')) {

View File

@ -5,6 +5,7 @@ namespace App\Controllers;
use App\Controllers\BaseController;
use App\Libraries\AbdmWorkTaskService;
use App\Libraries\BridgeSyncService;
use App\Libraries\HealthplixService;
use App\Models\BloodGroupModel;
use App\Models\PatientModel;
@ -1331,5 +1332,64 @@ class Patient extends BaseController
);
}
private function syncPatientToHealthplix(int $patientId, string $trigger): void
{
if ($patientId <= 0 || ! $this->db->tableExists('patient_master')) {
return;
}
try {
$row = $this->db->table('patient_master')
->where('id', $patientId)
->get(1)
->getRowArray();
if (! is_array($row) || empty($row)) {
return;
}
$abhaField = $this->resolvePatientAbhaIdField();
$abhaId = $abhaField !== null ? trim((string) ($row[$abhaField] ?? '')) : '';
$payload = [
'external_patient_id' => (string) $patientId,
'patient_code' => trim((string) ($row['p_code'] ?? '')),
'full_name' => trim((string) ($row['p_fname'] ?? '')),
'mobile' => trim((string) ($row['mphone1'] ?? '')),
'email' => trim((string) ($row['email1'] ?? '')),
'gender' => $this->mapPatientGenderToHealthplix((string) ($row['gender'] ?? '')),
'dob' => trim((string) ($row['dob'] ?? '')),
'age_years' => trim((string) ($row['age'] ?? '')),
'age_months' => trim((string) ($row['age_in_month'] ?? '')),
'address_line1' => trim((string) ($row['add1'] ?? '')),
'city' => trim((string) ($row['city'] ?? '')),
'district' => trim((string) ($row['district'] ?? '')),
'state' => trim((string) ($row['state'] ?? '')),
'zip' => trim((string) ($row['zip'] ?? '')),
'aadhar' => trim((string) ($row['udai'] ?? '')),
'abha_id' => $abhaId,
'trigger' => $trigger,
];
$service = new HealthplixService();
$service->registerPatient($payload);
} catch (\Throwable $e) {
// Never block patient workflows due to external integration failures.
}
}
private function mapPatientGenderToHealthplix(string $gender): string
{
$gender = trim(strtolower($gender));
if ($gender === '1' || $gender === 'male' || $gender === 'm') {
return 'male';
}
if ($gender === '2' || $gender === 'female' || $gender === 'f') {
return 'female';
}
return 'other';
}
}

View File

@ -28,6 +28,7 @@ class Doctor extends BaseController
'opd_print_template_options' => $templateContext['opd_print_options'],
'template_fields' => $templateContext['fields'],
'hospital_default_opd_valid_no_days' => $this->getHospitalDefaultOpdValidityDays(),
'healthplix_enabled' => $this->isHealthplixEnabled(),
]);
}
@ -49,6 +50,7 @@ class Doctor extends BaseController
'opd_print_template_options' => $templateContext['opd_print_options'],
'template_fields' => $templateContext['fields'],
'hospital_default_opd_valid_no_days' => $this->getHospitalDefaultOpdValidityDays(),
'healthplix_enabled' => $this->isHealthplixEnabled(),
]);
}
@ -58,6 +60,8 @@ class Doctor extends BaseController
$doctorModel = new DoctorModel();
$regField = $this->resolveDoctorRegField();
$hprField = $this->resolveDoctorHprField();
$healthplixDoctorIdentifier = trim((string) $this->request->getPost('input_healthplix_doctor_identifier'));
$doctorFields = $this->db->tableExists('doctor_master') ? ($this->db->getFieldNames('doctor_master') ?? []) : [];
$shortDescription = trim((string) $this->request->getPost('txt_doc_sign'));
$regInput = trim((string) $this->request->getPost('input_doc_reg_no'));
$hprInput = strtoupper(trim((string) $this->request->getPost('input_hpr_id')));
@ -75,6 +79,27 @@ class Doctor extends BaseController
'template_options' => $templateContext['options'],
'opd_print_template_options' => $templateContext['opd_print_options'],
'template_fields' => $templateContext['fields'],
'healthplix_enabled' => $this->isHealthplixEnabled(),
]);
}
return redirect()->back()->withInput()->with('errors', $errors);
}
if ($this->isHealthplixEnabled()
&& in_array('healthplix_doctor_identifier', $doctorFields, true)
&& $healthplixDoctorIdentifier === '') {
$errors = ['HealthPlix Doctor Identifier is required when HealthPlix integration is enabled.'];
if ($this->request->isAJAX()) {
$templateContext = $this->getDoctorTemplateContext();
return view('Setting/Doctor/Doctor_V', [
'errors' => $errors,
'formData' => $this->request->getPost(),
'template_options' => $templateContext['options'],
'opd_print_template_options' => $templateContext['opd_print_options'],
'template_fields' => $templateContext['fields'],
'hospital_default_opd_valid_no_days' => $this->getHospitalDefaultOpdValidityDays(),
'healthplix_enabled' => $this->isHealthplixEnabled(),
]);
}
@ -92,7 +117,6 @@ class Doctor extends BaseController
'doc_sign' => $shortDescription,
];
$doctorFields = $this->db->tableExists('doctor_master') ? ($this->db->getFieldNames('doctor_master') ?? []) : [];
if (in_array('opd_valid_no_days', $doctorFields, true)) {
$data['opd_valid_no_days'] = $this->resolveOpdValidityInput((string) $this->request->getPost('input_opd_valid_no_days'));
}
@ -120,6 +144,9 @@ class Doctor extends BaseController
if ($hprField !== null) {
$data[$hprField] = $hprInput;
}
if (in_array('healthplix_doctor_identifier', $doctorFields, true)) {
$data['healthplix_doctor_identifier'] = $healthplixDoctorIdentifier;
}
$insertId = $doctorModel->insert($data);
@ -134,6 +161,7 @@ class Doctor extends BaseController
'opd_print_template_options' => $templateContext['opd_print_options'],
'template_fields' => $templateContext['fields'],
'hospital_default_opd_valid_no_days' => $this->getHospitalDefaultOpdValidityDays(),
'healthplix_enabled' => $this->isHealthplixEnabled(),
]);
}
@ -163,6 +191,7 @@ class Doctor extends BaseController
'template_fields' => $templateContext['fields'],
'hospital_default_opd_valid_no_days' => $this->getHospitalDefaultOpdValidityDays(),
'doctor_opd_valid_no_days' => $this->resolveDoctorOpdValidityDaysForView($doctorRow),
'healthplix_enabled' => $this->isHealthplixEnabled(),
]);
}
@ -176,6 +205,8 @@ class Doctor extends BaseController
$docId = (int) $this->request->getPost('doc_id');
$regField = $this->resolveDoctorRegField();
$hprField = $this->resolveDoctorHprField();
$healthplixDoctorIdentifier = trim((string) $this->request->getPost('input_healthplix_doctor_identifier'));
$doctorFields = $this->db->tableExists('doctor_master') ? ($this->db->getFieldNames('doctor_master') ?? []) : [];
$shortDescription = trim((string) $this->request->getPost('txt_doc_sign'));
$regInput = trim((string) $this->request->getPost('input_doc_reg_no'));
$hprInput = strtoupper(trim((string) $this->request->getPost('input_hpr_id')));
@ -191,6 +222,16 @@ class Doctor extends BaseController
]);
}
if ($this->isHealthplixEnabled()
&& in_array('healthplix_doctor_identifier', $doctorFields, true)
&& $healthplixDoctorIdentifier === '') {
return $this->response->setJSON([
'update' => 0,
'showcontent' => '',
'error_text' => 'HealthPlix Doctor Identifier is required when HealthPlix integration is enabled.',
]);
}
$data = [
'p_title' => $this->request->getPost('select_title'),
'mphone1' => $this->request->getPost('input_mphone1'),
@ -202,7 +243,6 @@ class Doctor extends BaseController
'email1' => $this->request->getPost('input_email'),
];
$doctorFields = $this->db->tableExists('doctor_master') ? ($this->db->getFieldNames('doctor_master') ?? []) : [];
if (in_array('opd_valid_no_days', $doctorFields, true)) {
$data['opd_valid_no_days'] = $this->resolveOpdValidityInput((string) $this->request->getPost('input_opd_valid_no_days'));
}
@ -230,6 +270,9 @@ class Doctor extends BaseController
if ($hprField !== null) {
$data[$hprField] = $hprInput;
}
if (in_array('healthplix_doctor_identifier', $doctorFields, true)) {
$data['healthplix_doctor_identifier'] = $healthplixDoctorIdentifier;
}
$updated = $doctorModel->updateDoctor($data, $docId);
@ -781,4 +824,24 @@ class Doctor extends BaseController
return 5;
}
private function isHealthplixEnabled(): bool
{
$envValue = trim((string) env('HEALTHPLIX_ENABLED', ''));
if ($envValue !== '') {
return $envValue === '1';
}
if (! $this->db->tableExists('hospital_setting')) {
return false;
}
$row = $this->db->table('hospital_setting')
->select('s_value')
->where('s_name', 'HEALTHPLIX_ENABLED')
->get(1)
->getRowArray();
return trim((string) ($row['s_value'] ?? '0')) === '1';
}
}

View File

@ -0,0 +1,232 @@
<?php
namespace App\Controllers\Setting;
use App\Controllers\BaseController;
use App\Libraries\HealthplixService;
class HealthplixSettings extends BaseController
{
public function index()
{
if (! $this->canManageSettings()) {
return $this->response->setStatusCode(403)->setBody('Access denied');
}
return view('Setting/Admin/healthplix_settings', [
'healthplix_enabled' => $this->readSettingValue('HEALTHPLIX_ENABLED') === '1' ? '1' : '0',
'healthplix_base_url' => $this->readSettingValue('HEALTHPLIX_BASE_URL') ?: 'https://consultation-edge-dev.healthplix.com',
'healthplix_tenant_id' => $this->readSettingValue('HEALTHPLIX_TENANT_ID'),
'healthplix_tenant_key_masked' => $this->maskKey($this->readSettingValue('HEALTHPLIX_TENANT_KEY')),
'healthplix_tenant_key_exists' => $this->readSettingValue('HEALTHPLIX_TENANT_KEY') !== '',
'healthplix_doctor_identifier' => $this->readSettingValue('HEALTHPLIX_DOCTOR_IDENTIFIER'),
'healthplix_doctor_email' => $this->readSettingValue('HEALTHPLIX_DOCTOR_EMAIL'),
'healthplix_service_identifier' => $this->readSettingValue('HEALTHPLIX_SERVICE_IDENTIFIER'),
'healthplix_service_name' => $this->readSettingValue('HEALTHPLIX_SERVICE_NAME'),
'healthplix_tenant_header_name' => $this->readSettingValue('HEALTHPLIX_TENANT_HEADER_NAME') ?: 'tenant_id',
'healthplix_token_path' => $this->readSettingValue('HEALTHPLIX_TOKEN_PATH') ?: 'v1/generate/token',
'healthplix_patient_path' => $this->readSettingValue('HEALTHPLIX_PATIENT_PATH') ?: 'v1/patient/register',
'healthplix_appointment_path' => $this->readSettingValue('HEALTHPLIX_APPOINTMENT_PATH') ?: 'v1/appointment/register',
]);
}
public function save()
{
if (! $this->request->isAJAX()) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
if (! $this->canManageSettings()) {
return $this->response->setStatusCode(403)->setJSON(['update' => 0, 'error_text' => 'Access denied']);
}
if (! $this->db->tableExists('hospital_setting')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'hospital_setting table not found']);
}
$fields = [
'HEALTHPLIX_ENABLED' => $this->request->getPost('healthplix_enabled') === '1' ? '1' : '0',
'HEALTHPLIX_BASE_URL' => trim((string) $this->request->getPost('healthplix_base_url')),
'HEALTHPLIX_TENANT_ID' => trim((string) $this->request->getPost('healthplix_tenant_id')),
'HEALTHPLIX_TENANT_KEY' => trim((string) $this->request->getPost('healthplix_tenant_key')),
'HEALTHPLIX_DOCTOR_IDENTIFIER' => trim((string) $this->request->getPost('healthplix_doctor_identifier')),
'HEALTHPLIX_DOCTOR_EMAIL' => trim((string) $this->request->getPost('healthplix_doctor_email')),
'HEALTHPLIX_SERVICE_IDENTIFIER' => trim((string) $this->request->getPost('healthplix_service_identifier')),
'HEALTHPLIX_SERVICE_NAME' => trim((string) $this->request->getPost('healthplix_service_name')),
'HEALTHPLIX_TENANT_HEADER_NAME' => trim((string) $this->request->getPost('healthplix_tenant_header_name')),
'HEALTHPLIX_TOKEN_PATH' => trim((string) $this->request->getPost('healthplix_token_path')),
'HEALTHPLIX_PATIENT_PATH' => trim((string) $this->request->getPost('healthplix_patient_path')),
'HEALTHPLIX_APPOINTMENT_PATH' => trim((string) $this->request->getPost('healthplix_appointment_path')),
];
$savedCount = 0;
foreach ($fields as $name => $value) {
if ($value === '' && $name !== 'HEALTHPLIX_ENABLED') {
continue;
}
if ($this->upsertSettingValue($name, $value)) {
$savedCount++;
}
}
if ($savedCount === 0) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Provide at least one setting value to save',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
return $this->response->setJSON([
'update' => 1,
'error_text' => 'Saved ' . $savedCount . ' setting(s)',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function test()
{
if (! $this->request->isAJAX()) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
if (! $this->canManageSettings()) {
return $this->response->setStatusCode(403)->setJSON(['update' => 0, 'error_text' => 'Access denied']);
}
$overrides = [
'HEALTHPLIX_ENABLED' => $this->request->getPost('healthplix_enabled') === '1' ? '1' : '0',
];
$overrideFields = [
'HEALTHPLIX_BASE_URL' => 'healthplix_base_url',
'HEALTHPLIX_TENANT_HEADER_NAME' => 'healthplix_tenant_header_name',
'HEALTHPLIX_TENANT_ID' => 'healthplix_tenant_id',
'HEALTHPLIX_TENANT_KEY' => 'healthplix_tenant_key',
'HEALTHPLIX_TOKEN_PATH' => 'healthplix_token_path',
'HEALTHPLIX_PATIENT_PATH' => 'healthplix_patient_path',
'HEALTHPLIX_APPOINTMENT_PATH' => 'healthplix_appointment_path',
];
foreach ($overrideFields as $settingKey => $postKey) {
$value = trim((string) $this->request->getPost($postKey));
if ($value !== '') {
$overrides[$settingKey] = $value;
}
}
$service = new HealthplixService();
$result = $service->generateToken($overrides);
if (($result['ok'] ?? false) !== true) {
return $this->response->setJSON([
'update' => 0,
'error_text' => (string) ($result['error'] ?? 'HealthPlix token test failed'),
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
return $this->response->setJSON([
'update' => 1,
'error_text' => 'HealthPlix token generated successfully',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
private function canManageSettings(): bool
{
if (! function_exists('auth') || ! auth()->loggedIn()) {
return false;
}
return true;
}
private function readSettingValue(string $name): string
{
if (defined($name)) {
$definedValue = trim((string) constant($name));
if ($definedValue !== '') {
return $definedValue;
}
}
if (! $this->db->tableExists('hospital_setting')) {
return '';
}
$row = $this->db->table('hospital_setting')
->select('s_value')
->where('s_name', $name)
->get(1)
->getRowArray();
$value = trim((string) ($row['s_value'] ?? ''));
if ($value !== '') {
return $value;
}
$rows = $this->db->table('hospital_setting')
->select('s_name, s_value')
->get()
->getResultArray();
$target = strtoupper(trim($name));
foreach ($rows as $item) {
$itemName = strtoupper(trim((string) ($item['s_name'] ?? '')));
if ($itemName === $target) {
$itemValue = trim((string) ($item['s_value'] ?? ''));
if ($itemValue !== '') {
return $itemValue;
}
}
}
return '';
}
private function upsertSettingValue(string $name, string $value): bool
{
$existing = $this->db->table('hospital_setting')
->select('id, s_value')
->where('s_name', $name)
->get(1)
->getRowArray();
$oldValue = trim((string) ($existing['s_value'] ?? ''));
if ($existing) {
$ok = $this->db->table('hospital_setting')
->where('id', (int) ($existing['id'] ?? 0))
->update(['s_value' => $value]);
} else {
$ok = $this->db->table('hospital_setting')->insert([
's_name' => $name,
's_value' => $value,
]);
}
if ($ok) {
$this->auditClinicalUpdate('hospital_setting', $name, $name, $oldValue, $value);
}
return (bool) $ok;
}
private function maskKey(string $key): string
{
$key = trim($key);
if ($key === '') {
return '';
}
if (mb_strlen($key) <= 8) {
return str_repeat('*', mb_strlen($key));
}
return mb_substr($key, 0, 4) . str_repeat('*', mb_strlen($key) - 8) . mb_substr($key, -4);
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddHealthplixDoctorIdentifierToDoctorMaster extends Migration
{
public function up(): void
{
if (! $this->tableExists('doctor_master')) {
return;
}
if (! $this->fieldExists('doctor_master', 'healthplix_doctor_identifier')) {
$this->forge->addColumn('doctor_master', [
'healthplix_doctor_identifier' => [
'type' => 'VARCHAR',
'constraint' => 120,
'null' => true,
'after' => 'email1',
],
]);
}
}
public function down(): void
{
if ($this->tableExists('doctor_master')
&& $this->fieldExists('doctor_master', 'healthplix_doctor_identifier')) {
$this->forge->dropColumn('doctor_master', 'healthplix_doctor_identifier');
}
}
private function tableExists(string $table): bool
{
$dbName = (string) ($this->db->database ?? '');
if ($dbName === '' || $table === '') {
return false;
}
$row = $this->db->query(
'SELECT COUNT(*) AS total FROM information_schema.tables WHERE table_schema = ? AND table_name = ?',
[$dbName, $table]
)->getRowArray();
return ((int) ($row['total'] ?? 0)) > 0;
}
private function fieldExists(string $table, string $field): bool
{
$dbName = (string) ($this->db->database ?? '');
if ($dbName === '' || $table === '' || $field === '') {
return false;
}
$row = $this->db->query(
'SELECT COUNT(*) AS total FROM information_schema.columns WHERE table_schema = ? AND table_name = ? AND column_name = ?',
[$dbName, $table, $field]
)->getRowArray();
return ((int) ($row['total'] ?? 0)) > 0;
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddHealthplixSyncIdFields extends Migration
{
public function up(): void
{
$this->addPatientSyncField();
$this->addOpdSyncField();
}
public function down(): void
{
if ($this->tableExists('patient_master')
&& $this->fieldExists('patient_master', 'healthplix_sync_id')) {
$this->forge->dropColumn('patient_master', 'healthplix_sync_id');
}
if ($this->tableExists('opd_master')
&& $this->fieldExists('opd_master', 'healthplix_sync_id')) {
$this->forge->dropColumn('opd_master', 'healthplix_sync_id');
}
}
private function addPatientSyncField(): void
{
if (! $this->tableExists('patient_master')) {
return;
}
if (! $this->fieldExists('patient_master', 'healthplix_sync_id')) {
$this->forge->addColumn('patient_master', [
'healthplix_sync_id' => [
'type' => 'VARCHAR',
'constraint' => 120,
'null' => true,
],
]);
}
}
private function addOpdSyncField(): void
{
if (! $this->tableExists('opd_master')) {
return;
}
if (! $this->fieldExists('opd_master', 'healthplix_sync_id')) {
$this->forge->addColumn('opd_master', [
'healthplix_sync_id' => [
'type' => 'VARCHAR',
'constraint' => 120,
'null' => true,
],
]);
}
}
private function tableExists(string $table): bool
{
$dbName = (string) ($this->db->database ?? '');
if ($dbName === '' || $table === '') {
return false;
}
$row = $this->db->query(
'SELECT COUNT(*) AS total FROM information_schema.tables WHERE table_schema = ? AND table_name = ?',
[$dbName, $table]
)->getRowArray();
return ((int) ($row['total'] ?? 0)) > 0;
}
private function fieldExists(string $table, string $field): bool
{
$dbName = (string) ($this->db->database ?? '');
if ($dbName === '' || $table === '' || $field === '') {
return false;
}
$row = $this->db->query(
'SELECT COUNT(*) AS total FROM information_schema.columns WHERE table_schema = ? AND table_name = ? AND column_name = ?',
[$dbName, $table, $field]
)->getRowArray();
return ((int) ($row['total'] ?? 0)) > 0;
}
}

View File

@ -0,0 +1,497 @@
<?php
namespace App\Libraries;
class HealthplixService
{
/**
* @param array<string, string> $overrides
* @return array<string, mixed>
*/
public function generateToken(array $overrides = []): array
{
if (! $this->isEnabled($overrides)) {
return ['ok' => false, 'error' => 'HealthPlix integration is disabled for this hospital.'];
}
$baseUrl = rtrim($this->readSetting('HEALTHPLIX_BASE_URL', $overrides), '/');
$tokenPath = trim($this->readSetting('HEALTHPLIX_TOKEN_PATH', $overrides));
$tenantId = trim($this->readSetting('HEALTHPLIX_TENANT_ID', $overrides));
$tenantKey = trim($this->readSetting('HEALTHPLIX_TENANT_KEY', $overrides));
if ($baseUrl === '' || $tokenPath === '') {
return ['ok' => false, 'error' => 'HEALTHPLIX_BASE_URL and HEALTHPLIX_TOKEN_PATH are required.'];
}
if ($tenantId === '' || $tenantKey === '') {
return ['ok' => false, 'error' => 'HEALTHPLIX_TENANT_ID and HEALTHPLIX_TENANT_KEY are required.'];
}
$endpoint = $this->buildUrl($baseUrl, $tokenPath);
// Token endpoint uses Basic Auth only — no request body per HealthPlix API doc v1.7
$headers = [
'Authorization' => 'Basic ' . base64_encode($tenantId . ':' . $tenantKey),
'Accept' => 'application/json',
];
$result = $this->request('POST', $endpoint, $headers, [], 'healthplix.token.generate', 'tenant', $tenantId);
if (($result['ok'] ?? false) !== true) {
return $result;
}
$data = (array) ($result['data'] ?? []);
$token = trim((string) ($data['token'] ?? ($data['access_token'] ?? '')));
if ($token === '') {
return ['ok' => false, 'error' => 'HealthPlix token response did not include token/access_token.'];
}
return [
'ok' => true,
'token' => $token,
'status' => (int) ($result['status'] ?? 200),
'data' => $data,
];
}
/**
* @param array<string, mixed> $payload
* @param array<string, string> $overrides
* @return array<string, mixed>
*/
public function registerPatient(array $payload, array $overrides = []): array
{
if (! $this->isEnabled($overrides)) {
return ['ok' => false, 'error' => 'HealthPlix integration is disabled for this hospital.'];
}
$baseUrl = rtrim($this->readSetting('HEALTHPLIX_BASE_URL', $overrides), '/');
$path = trim($this->readSetting('HEALTHPLIX_PATIENT_PATH', $overrides));
if ($baseUrl === '' || $path === '') {
return ['ok' => false, 'error' => 'HEALTHPLIX_BASE_URL and HEALTHPLIX_PATIENT_PATH are required.'];
}
$tokenResult = $this->generateToken($overrides);
if (($tokenResult['ok'] ?? false) !== true) {
return $tokenResult;
}
$token = (string) ($tokenResult['token'] ?? '');
$endpoint = $this->buildUrl($baseUrl, $path);
$tenantHeaderName = trim($this->readSetting('HEALTHPLIX_TENANT_HEADER_NAME', $overrides));
if ($tenantHeaderName === '') {
$tenantHeaderName = 'tenant_id';
}
$tenantId = trim($this->readSetting('HEALTHPLIX_TENANT_ID', $overrides));
$headers = [
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
'Content-Type' => 'application/json',
$tenantHeaderName => $tenantId,
];
$entityId = trim((string) ($payload['external_patient_id'] ?? ($payload['patient_id'] ?? '')));
$apiPayload = $this->buildPatientApiPayload($payload);
return $this->request('POST', $endpoint, $headers, $apiPayload, 'healthplix.patient.register', 'patient', $entityId);
}
/**
* @param array<string, mixed> $payload
* @param array<string, string> $overrides
* @return array<string, mixed>
*/
public function bookAppointment(array $payload, array $overrides = []): array
{
if (! $this->isEnabled($overrides)) {
return ['ok' => false, 'error' => 'HealthPlix integration is disabled for this hospital.'];
}
$baseUrl = rtrim($this->readSetting('HEALTHPLIX_BASE_URL', $overrides), '/');
$path = trim($this->readSetting('HEALTHPLIX_APPOINTMENT_PATH', $overrides));
if ($baseUrl === '' || $path === '') {
return ['ok' => false, 'error' => 'HEALTHPLIX_BASE_URL and HEALTHPLIX_APPOINTMENT_PATH are required.'];
}
$tokenResult = $this->generateToken($overrides);
if (($tokenResult['ok'] ?? false) !== true) {
return $tokenResult;
}
$token = (string) ($tokenResult['token'] ?? '');
$endpoint = $this->buildUrl($baseUrl, $path);
$tenantHeaderName = trim($this->readSetting('HEALTHPLIX_TENANT_HEADER_NAME', $overrides));
if ($tenantHeaderName === '') {
$tenantHeaderName = 'tenant_id';
}
$tenantId = trim($this->readSetting('HEALTHPLIX_TENANT_ID', $overrides));
$headers = [
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
'Content-Type' => 'application/json',
$tenantHeaderName => $tenantId,
];
$entityId = trim((string) ($payload['external_appointment_id'] ?? ($payload['appointment_id'] ?? '')));
$apiPayload = $this->buildAppointmentApiPayload($payload);
return $this->request('POST', $endpoint, $headers, $apiPayload, 'healthplix.appointment.book', 'opd', $entityId);
}
/**
* @param array<string, string> $overrides
*/
private function isEnabled(array $overrides = []): bool
{
return $this->readSetting('HEALTHPLIX_ENABLED', $overrides) === '1';
}
/**
* Map internal patient payload to HealthPlix API v1 field names.
* Ref: API Spec Doc v1.7 - Register Patient (POST v1/patient/register)
* @param array<string, mixed> $payload
* @return array<string, mixed>
*/
private function buildPatientApiPayload(array $payload): array
{
$genderRaw = strtolower(trim((string) ($payload['gender'] ?? '')));
$genderMap = [
'male' => 'Male', 'm' => 'Male', '1' => 'Male',
'female' => 'Female', 'f' => 'Female', '2' => 'Female',
'other' => 'Others', 'o' => 'Others',
];
$gender = $genderMap[$genderRaw] ?? 'Others';
$honorific = trim((string) ($payload['honorific'] ?? ''));
if ($honorific === '') {
$honorific = ($gender === 'Female') ? 'Mrs.' : 'Mr.';
}
$patientCode = trim((string) ($payload['patient_code'] ?? ($payload['external_patient_id'] ?? '')));
$patientIdentifier = $patientCode !== '' ? $patientCode : (string) ($payload['external_patient_id'] ?? '');
// Minimum 3 characters required by API
if (mb_strlen($patientIdentifier) < 3) {
$patientIdentifier = str_pad($patientIdentifier, 3, '0', STR_PAD_LEFT);
}
$api = [
'patient_identifier' => $patientIdentifier,
'name' => trim((string) ($payload['full_name'] ?? '')),
'phone' => trim((string) ($payload['mobile'] ?? '')),
'gender' => $gender,
'honorific' => $honorific,
];
// HealthPlix expects dob; derive from estimate_dob/age when explicit DOB is missing.
$resolvedDob = $this->resolveDobForApi($payload);
if ($resolvedDob !== '') {
$api['dob'] = $resolvedDob;
}
// Optional fields — only send if non-empty
foreach ([
'email' => 'email',
'address_line1' => 'address',
'city' => 'city',
'zip' => 'pincode',
'phone_secondary' => 'phone_secondary',
'occupation' => 'occupation',
] as $internalKey => $apiKey) {
$val = trim((string) ($payload[$internalKey] ?? ''));
if ($val !== '') {
$api[$apiKey] = $val;
}
}
return $api;
}
/**
* Resolve DOB in YYYY-MM-DD from explicit dob, estimate_dob, or age values.
*
* @param array<string, mixed> $payload
*/
private function resolveDobForApi(array $payload): string
{
$dob = $this->normalizeDateForApi((string) ($payload['dob'] ?? ''));
if ($dob !== '') {
return $dob;
}
$estimateDob = $this->normalizeDateForApi((string) ($payload['estimate_dob'] ?? ''));
if ($estimateDob !== '') {
return $estimateDob;
}
$ageYears = $this->parseNonNegativeInt($payload['age_years'] ?? ($payload['age'] ?? null));
$ageMonths = $this->parseNonNegativeInt($payload['age_months'] ?? ($payload['age_in_month'] ?? null));
if ($ageYears === null && $ageMonths === null) {
return '';
}
try {
$dobFromAge = new \DateTime('today');
if ($ageYears !== null && $ageYears > 0) {
$dobFromAge->sub(new \DateInterval('P' . $ageYears . 'Y'));
}
if ($ageMonths !== null && $ageMonths > 0) {
$dobFromAge->sub(new \DateInterval('P' . $ageMonths . 'M'));
}
return $dobFromAge->format('Y-m-d');
} catch (\Throwable $e) {
return '';
}
}
private function normalizeDateForApi(string $value): string
{
$value = trim($value);
if ($value === '' || $value === '0000-00-00') {
return '';
}
try {
return (new \DateTime($value))->format('Y-m-d');
} catch (\Throwable $e) {
return '';
}
}
/**
* @param mixed $value
*/
private function parseNonNegativeInt($value): ?int
{
if (is_int($value)) {
return $value >= 0 ? $value : null;
}
$text = trim((string) $value);
if ($text === '') {
return null;
}
if (preg_match('/\d+/', $text, $m) !== 1) {
return null;
}
return max(0, (int) $m[0]);
}
/**
* Map internal appointment payload to HealthPlix API v1 field names.
* Ref: API Spec Doc v1.7 - Register Appointment (POST v1/appointment/register)
* @param array<string, mixed> $payload
* @return array<string, mixed>
*/
private function buildAppointmentApiPayload(array $payload): array
{
$patientCode = trim((string) ($payload['patient_identifier'] ?? ''));
if (mb_strlen($patientCode) < 3) {
$patientCode = str_pad($patientCode, 3, '0', STR_PAD_LEFT);
}
// Parse appointment date/time from stored datetime string
$datetimeRaw = trim((string) ($payload['appointment_datetime'] ?? ''));
$appntDate = '';
$appntTime = '';
if ($datetimeRaw !== '') {
try {
$dt = new \DateTime($datetimeRaw);
$appntDate = $dt->format('Y-m-d');
$appntTime = $dt->format('H:i:s');
} catch (\Throwable $e) {
// fallback to today below
}
}
if ($appntDate === '') {
$appntDate = date('Y-m-d');
}
if ($appntTime === '') {
$appntTime = date('H:i:s');
}
return [
'patient_identifier' => $patientCode,
'doctor_identifier' => trim((string) ($payload['doctor_identifier'] ?? '')),
'appnt_date' => $appntDate,
'appnt_time' => $appntTime,
'appnt_duration' => (int) ($payload['appnt_duration'] ?? 30),
'service_identifier' => trim((string) ($payload['service_identifier'] ?? '')),
];
}
/**
* @param array<string, string> $overrides
*/
private function readSetting(string $name, array $overrides = []): string
{
if (array_key_exists($name, $overrides)) {
return trim((string) $overrides[$name]);
}
$envValue = trim((string) env($name, ''));
if ($envValue !== '') {
return $envValue;
}
if (defined($name)) {
$defined = trim((string) constant($name));
if ($defined !== '') {
return $defined;
}
}
try {
$db = db_connect();
if ($db && method_exists($db, 'tableExists') && $db->tableExists('hospital_setting')) {
$row = $db->table('hospital_setting')
->select('s_value')
->where('s_name', $name)
->get(1)
->getRowArray();
$value = trim((string) ($row['s_value'] ?? ''));
if ($value !== '') {
return $value;
}
}
} catch (\Throwable $e) {
}
return '';
}
private function buildUrl(string $baseUrl, string $path): string
{
if (preg_match('~^https?://~i', $path) === 1) {
return $path;
}
return $baseUrl . '/' . ltrim($path, '/');
}
/**
* @param array<string, mixed> $json
* @return array<string, mixed>
*/
private function request(
string $method,
string $url,
array $headers,
array $json,
string $eventType = '',
string $entityType = '',
string $entityId = ''
): array
{
$options = [
'timeout' => 20,
'http_errors' => false,
];
if (defined('ENVIRONMENT') && in_array((string) ENVIRONMENT, ['development', 'testing'], true)) {
$options['verify'] = false;
}
try {
$client = service('curlrequest', $options);
$requestOptions = ['headers' => $headers];
if (! empty($json)) {
$requestOptions['json'] = $json;
}
$response = $client->request($method, $url, $requestOptions);
$status = $response->getStatusCode();
$body = (string) $response->getBody();
$decoded = json_decode($body, true);
if (! is_array($decoded)) {
$decoded = [];
}
if ($status < 200 || $status >= 300) {
$this->logApiCall($eventType, $url, $method, $entityType, $entityId, $json, $status, $body, 'error', 'HTTP ' . $status);
return [
'ok' => false,
'status' => $status,
'error' => 'HealthPlix API request failed (HTTP ' . $status . ')',
'body' => mb_substr(trim($body), 0, 600),
'data' => $decoded,
];
}
$this->logApiCall($eventType, $url, $method, $entityType, $entityId, $json, $status, $body, 'success', '');
return [
'ok' => true,
'status' => $status,
'data' => $decoded,
'body' => mb_substr(trim($body), 0, 600),
];
} catch (\Throwable $e) {
$this->logApiCall($eventType, $url, $method, $entityType, $entityId, $json, 0, '', 'error', $e->getMessage());
return [
'ok' => false,
'status' => 0,
'error' => 'HealthPlix API request failed: ' . $e->getMessage(),
];
}
}
/**
* @param array<string, mixed> $requestPayload
*/
private function logApiCall(
string $eventType,
string $endpoint,
string $method,
string $entityType,
string $entityId,
array $requestPayload,
int $responseCode,
string $responseBody,
string $status,
string $errorMessage
): void {
if ($eventType === '') {
return;
}
try {
$db = db_connect();
if (! $db || ! method_exists($db, 'tableExists') || ! $db->tableExists('abdm_api_logs')) {
return;
}
$requestJson = (string) json_encode($requestPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$decodedBody = json_decode($responseBody, true);
$responseJson = is_array($decodedBody)
? (string) json_encode($decodedBody, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
: trim($responseBody);
$db->table('abdm_api_logs')->insert([
'channel' => 'healthplix',
'event_type' => $eventType,
'endpoint' => $endpoint,
'http_method' => strtoupper($method),
'entity_type' => $entityType !== '' ? $entityType : null,
'entity_id' => $entityId !== '' ? $entityId : null,
'request_json' => $requestJson,
'response_code' => $responseCode > 0 ? $responseCode : null,
'response_json' => $responseJson !== '' ? mb_substr($responseJson, 0, 4000) : null,
'status' => $status,
'error_message' => $errorMessage !== '' ? mb_substr($errorMessage, 0, 1000) : null,
'created_at' => date('Y-m-d H:i:s'),
]);
} catch (\Throwable $e) {
// Logging must never block business flow.
}
}
}

View File

@ -0,0 +1,195 @@
<div class="card">
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
<h3 class="card-title mb-0">HealthPlix Integration</h3>
<span class="badge bg-primary fs-6">API Version: V1</span>
</div>
<div class="card-body">
<?= csrf_field() ?>
<div class="alert alert-info py-2 mb-3" style="font-size:13px">
<strong>Base URL (QA/Dev):</strong> <code>https://consultation-edge-dev.healthplix.com</code> &nbsp;|&nbsp;
<strong>Doc version:</strong> 1.7 (26-Apr-2025) &nbsp;|&nbsp;
All paths below are pre-filled as per official API Specification Doc.
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="healthplix_enabled" <?= (($healthplix_enabled ?? '0') === '1') ? 'checked' : '' ?>>
<label class="form-check-label" for="healthplix_enabled">Enable HealthPlix for this hospital</label>
<div class="form-text">Keep disabled for hospitals that do not use HealthPlix.</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">HealthPlix API Base URL</label>
<input type="text" class="form-control" id="healthplix_base_url" placeholder="https://consultation-edge-dev.healthplix.com" value="<?= esc($healthplix_base_url ?? '') ?>">
<div class="form-text">QA/Dev URL from HealthPlix doc. Production URL is provided by HealthPlix team.</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Tenant Header Name <span class="badge bg-secondary">Fixed by HealthPlix</span></label>
<input type="text" class="form-control" id="healthplix_tenant_header_name" placeholder="tenant_id" value="<?= esc($healthplix_tenant_header_name ?? 'tenant_id') ?>">
<div class="form-text">Header name for tenant ID in API requests. Default: <code>tenant_id</code> (do NOT put your Tenant ID value here).</div>
</div>
</div>
<hr class="my-2">
<h6 class="mb-2">API Endpoints <span class="badge bg-secondary">V1</span></h6>
<div class="form-text mb-2">Paths are relative to the Base URL above. Pre-filled from HealthPlix API Specification Doc v1.7.</div>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label">Token Path</label>
<input type="text" class="form-control" id="healthplix_token_path" placeholder="v1/generate/token" value="<?= esc($healthplix_token_path ?? 'v1/generate/token') ?>">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Patient Registration Path</label>
<input type="text" class="form-control" id="healthplix_patient_path" placeholder="v1/patient/register" value="<?= esc($healthplix_patient_path ?? 'v1/patient/register') ?>">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Appointment Booking Path</label>
<input type="text" class="form-control" id="healthplix_appointment_path" placeholder="v1/appointment/register" value="<?= esc($healthplix_appointment_path ?? 'v1/appointment/register') ?>">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Tenant ID (Basic Auth Username)</label>
<input type="text" class="form-control" id="healthplix_tenant_id" value="<?= esc($healthplix_tenant_id ?? '') ?>">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Tenant Key (Basic Auth Password)</label>
<input type="password" class="form-control" id="healthplix_tenant_key" placeholder="Enter to set/update key">
<div class="form-text" id="healthplix_tenant_key_hint">
<?php if (!empty($healthplix_tenant_key_exists)) : ?>
<span class="text-success">&#10003; Key saved (<?= esc($healthplix_tenant_key_masked ?? '') ?>). Leave blank to keep existing.</span>
<?php else : ?>
No tenant key configured yet.
<?php endif; ?>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Default Doctor Identifier</label>
<input type="text" class="form-control" id="healthplix_doctor_identifier" value="<?= esc($healthplix_doctor_identifier ?? '') ?>">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Doctor Email</label>
<input type="email" class="form-control" id="healthplix_doctor_email" value="<?= esc($healthplix_doctor_email ?? '') ?>">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Service Identifier</label>
<input type="text" class="form-control" id="healthplix_service_identifier" value="<?= esc($healthplix_service_identifier ?? '') ?>">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Service Name</label>
<input type="text" class="form-control" id="healthplix_service_name" value="<?= esc($healthplix_service_name ?? '') ?>">
</div>
</div>
<div class="d-flex gap-2 flex-wrap">
<button type="button" class="btn btn-primary" id="btn_save_healthplix">Save Settings</button>
<button type="button" class="btn btn-outline-success" id="btn_test_healthplix">Test Token</button>
</div>
<div id="healthplix_settings_msg" class="mt-3"></div>
</div>
</div>
<script>
(function() {
function getCsrfPair() {
var input = document.querySelector('input[name="<?= csrf_token() ?>"]');
if (!input) {
return { name: '<?= csrf_token() ?>', value: '<?= csrf_hash() ?>' };
}
return { name: input.getAttribute('name'), value: input.value };
}
function updateCsrf(data) {
if (!data || !data.csrfName || !data.csrfHash) {
return;
}
var input = document.querySelector('input[name="' + data.csrfName + '"]');
if (input) {
input.value = data.csrfHash;
}
}
function showMessage(type, text) {
var cls = type === 'success' ? 'alert alert-success' : 'alert alert-danger';
$('#healthplix_settings_msg').html('<div class="' + cls + '">' + $('<div>').text(text || '').html() + '</div>');
}
function postJson(url, payload, cb) {
var csrf = getCsrfPair();
payload = payload || {};
payload[csrf.name] = csrf.value;
$.post(url, payload, function(data) {
updateCsrf(data);
cb(data || {});
}, 'json').fail(function(xhr) {
var response = (xhr && xhr.responseJSON) ? xhr.responseJSON : {};
cb(response || {});
});
}
function payload() {
return {
healthplix_enabled: $('#healthplix_enabled').is(':checked') ? '1' : '0',
healthplix_base_url: ($('#healthplix_base_url').val() || '').trim(),
healthplix_tenant_header_name: ($('#healthplix_tenant_header_name').val() || '').trim(),
healthplix_tenant_id: ($('#healthplix_tenant_id').val() || '').trim(),
healthplix_tenant_key: ($('#healthplix_tenant_key').val() || '').trim(),
healthplix_doctor_identifier: ($('#healthplix_doctor_identifier').val() || '').trim(),
healthplix_doctor_email: ($('#healthplix_doctor_email').val() || '').trim(),
healthplix_service_identifier: ($('#healthplix_service_identifier').val() || '').trim(),
healthplix_service_name: ($('#healthplix_service_name').val() || '').trim(),
healthplix_token_path: ($('#healthplix_token_path').val() || '').trim(),
healthplix_patient_path: ($('#healthplix_patient_path').val() || '').trim(),
healthplix_appointment_path: ($('#healthplix_appointment_path').val() || '').trim()
};
}
// Update hint live as user types
$('#healthplix_tenant_key').on('input', function() {
var val = $(this).val().trim();
if (val.length > 0) {
$('#healthplix_tenant_key_hint').html('<span class="text-warning">&#9998; New key will be saved on Save Settings.</span>');
} else {
$('#healthplix_tenant_key_hint').html('<span class="text-muted">Leave blank to keep existing key.</span>');
}
});
$('#btn_save_healthplix').on('click', function() {
postJson('<?= base_url('setting/admin/healthplix-settings/save') ?>', payload(), function(data) {
if ((data.update || 0) !== 1) {
showMessage('error', data.error_text || 'Unable to save settings');
return;
}
showMessage('success', data.error_text || 'Settings saved');
var savedKey = ($('#healthplix_tenant_key').val() || '').trim();
if (savedKey.length > 0) {
var masked = savedKey.substring(0, 4) + '****' + savedKey.slice(-4);
$('#healthplix_tenant_key_hint').html('<span class="text-success">&#10003; Key saved (' + masked + '). Leave blank to keep existing.</span>');
} else {
$('#healthplix_tenant_key_hint').html('<span class="text-muted">Leave blank to keep existing key.</span>');
}
$('#healthplix_tenant_key').val('');
});
});
$('#btn_test_healthplix').on('click', function() {
postJson('<?= base_url('setting/admin/healthplix-settings/test') ?>', payload(), function(data) {
if ((data.update || 0) !== 1) {
showMessage('error', data.error_text || 'HealthPlix token test failed');
return;
}
showMessage('success', data.error_text || 'HealthPlix token test successful');
});
});
})();
</script>

View File

@ -68,6 +68,12 @@
<span>AI Settings</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/healthplix-settings') ?>','maindiv','HealthPlix Integration');">
<i class="bi bi-link-45deg"></i>
<span>HealthPlix Integration</span>
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/hospital-profile') ?>','maindiv','Hospital Profile');">
<i class="bi bi-building"></i>

View File

@ -75,6 +75,12 @@
<label class="form-label">HPR ID</label>
<input class="form-control" name="input_hpr_id" placeholder="ABDM HPR ID" type="text" value="<?= esc($formData['input_hpr_id'] ?? '') ?>">
</div>
<?php if (! empty($healthplix_enabled)) : ?>
<div class="col-md-4">
<label class="form-label">HealthPlix Doctor Identifier</label>
<input class="form-control" name="input_healthplix_doctor_identifier" placeholder="UUID from HealthPlix" type="text" value="<?= esc($formData['input_healthplix_doctor_identifier'] ?? '') ?>">
</div>
<?php endif; ?>
<div class="col-md-4">
<label class="form-label">Date of Birth</label>
<input class="form-control" name="datepicker_dob" type="date" value="<?= esc($doctorDobValue) ?>">

View File

@ -1,6 +1,7 @@
<?php
$doctorRegNo = '';
$doctorHprId = '';
$doctorHealthplixIdentifier = '';
$hospitalDefaultOpdDays = (int) ($hospital_default_opd_valid_no_days ?? 5);
if ($hospitalDefaultOpdDays < 1) {
$hospitalDefaultOpdDays = 5;
@ -36,6 +37,13 @@ if (!empty($data) && !empty($data[0])) {
break;
}
}
foreach (['healthplix_doctor_identifier', 'healthplix_identifier'] as $hpField) {
if (isset($data[0]->{$hpField}) && trim((string) $data[0]->{$hpField}) !== '') {
$doctorHealthplixIdentifier = trim((string) $data[0]->{$hpField});
break;
}
}
}
?>
<?php
@ -102,6 +110,12 @@ if (!empty($data) && !empty($data[0]) && !empty($data[0]->dob)) {
<label class="form-label">HPR ID</label>
<input class="form-control" name="input_hpr_id" placeholder="ABDM HPR ID" type="text" value="<?= esc($doctorHprId) ?>">
</div>
<?php if (! empty($healthplix_enabled)) : ?>
<div class="col-md-3">
<label class="form-label">HealthPlix Doctor Identifier</label>
<input class="form-control" name="input_healthplix_doctor_identifier" placeholder="UUID from HealthPlix" type="text" value="<?= esc($doctorHealthplixIdentifier) ?>">
</div>
<?php endif; ?>
<div class="col-md-3">
<label class="form-label">Date of Birth</label>
<input class="form-control" name="datepicker_dob" value="<?= esc($doctorDobValue) ?>" type="date" />

View File

@ -111,6 +111,15 @@
<div class="col-lg-3 col-md-4 label">ABHA ID</div>
<div class="col-lg-9 col-md-8"><?= esc($patientAbhaId) ?></div>
</div>
<?php $hpPatientSyncId = trim((string) ($data[0]->healthplix_sync_id ?? '')); ?>
<?php if ($hpPatientSyncId !== '') : ?>
<div class="row">
<div class="col-lg-3 col-md-4 label">HealthPlix ID</div>
<div class="col-lg-9 col-md-8">
<span class="badge bg-success-subtle text-success border border-success-subtle"><?= esc($hpPatientSyncId) ?></span>
</div>
</div>
<?php endif; ?>
<div class="row">
<div class="col-lg-3 col-md-4 label">Phone</div>
<div class="col-lg-9 col-md-8"><?=$data[0]->mphone1?></div>

View File

@ -220,6 +220,41 @@ $paymentHistoryRows = $payment_history_rows ?? [];
<a href="<?= base_url('opd_print/opd_PDF_print') ?>/<?= esc($opd_master[0]->opd_id ?? 0) ?>" target="_blank" class="btn btn-outline-secondary">Print OPD HEAD in Letter Head</a>
<a href="<?= base_url('opd_print/opd_blank_print') ?>/<?= esc($opd_master[0]->opd_id ?? 0) ?>" target="_blank" class="btn btn-outline-secondary">Print OPD HEAD in Blank Page</a>
<a href="<?= base_url('opd_print/opd_Cont_print') ?>/<?= esc($opd_master[0]->opd_id ?? 0) ?>" target="_blank" class="btn btn-outline-secondary">Print Cont. Paper</a>
<?php if (! empty($healthplix_sync_applicable)) : ?>
<?php
$hpOpdSyncId = trim((string) ($healthplix_opd_sync_id ?? ''));
?>
<?php if (! empty($healthplix_sync_done)) : ?>
<?php
$hpLogRef = $hpOpdSyncId;
if ($hpLogRef === '' && ! empty($healthplix_log['response_json'])) {
$hpResp = @json_decode((string) ($healthplix_log['response_json'] ?? ''), true);
$hpLogRef = trim((string) ($hpResp['id'] ?? ($hpResp['appointment_id'] ?? ($hpResp['reference_id'] ?? ''))));
}
if ($hpLogRef === '') {
$hpLogRef = trim((string) ($healthplix_log['entity_id'] ?? ''));
}
$hpDate = trim((string) ($healthplix_log['created_at'] ?? ''));
?>
<div class="d-flex flex-column align-items-start gap-1">
<button type="button" class="btn btn-success" disabled
title="Synced to HealthPlix<?= $hpDate !== '' ? ' on ' . esc($hpDate) : '' ?>">
HealthPlix ✓
</button>
<?php if ($hpLogRef !== '') : ?>
<small class="text-muted px-1" id="hp_opd_sync_id_display" title="HealthPlix Sync ID">🔗 <?= esc($hpLogRef) ?></small>
<?php endif; ?>
</div>
<?php else : ?>
<div class="d-flex flex-column align-items-start gap-1">
<button type="button" class="btn btn-danger" id="btn_healthplix_push"
data-opd-id="<?= esc($opd_master[0]->opd_id ?? 0) ?>">
HealthPlix — Sync
</button>
<small class="text-muted px-1" id="hp_opd_sync_id_display" style="display:none" title="HealthPlix Sync ID"></small>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<div class="col-sm-6">
@ -481,6 +516,43 @@ $paymentHistoryRows = $payment_history_rows ?? [];
}
});
$('#btn_healthplix_push').click(function() {
var btn = $(this);
var opdId = btn.data('opd-id');
var csrf = getCsrfPair();
btn.prop('disabled', true).text('Syncing...');
$.post('<?= base_url('Opd/healthplix_push') ?>/' + opdId, {
[csrf.name]: csrf.value
}, function(data) {
updateCsrf(data);
if (data.ok == 1) {
btn.removeClass('btn-danger').addClass('btn-success')
.text('HealthPlix ✓')
.prop('disabled', true);
// Show sync ID below the button
var syncId = (data.log && (data.log.sync_id || data.log.entity_id)) || '';
if (syncId) {
$('#hp_opd_sync_id_display').text('🔗 ' + syncId).show();
}
if (typeof notify === 'function') {
notify('success', 'HealthPlix', data.message || 'Appointment synced.');
}
} else {
btn.prop('disabled', false).text('HealthPlix — Retry');
if (typeof notify === 'function') {
notify('error', 'HealthPlix Sync Failed', data.message || 'Sync failed. Check HealthPlix settings.');
} else {
alert(data.message || 'HealthPlix sync failed. Check settings.');
}
}
}, 'json').fail(function() {
btn.prop('disabled', false).text('HealthPlix — Retry');
if (typeof notify === 'function') {
notify('error', 'HealthPlix', 'Network error. Try again.');
}
});
});
$('#btn_cr_org').click(function() {
var csrf = getCsrfPair();
if (confirm('Are you sure process this invoice')) {

View File

@ -77,7 +77,19 @@
<script src="<?= base_url('assets/vendor/jquery/jquery-4.0.0.min.js') ?>"></script>
<script src="<?= base_url('assets/vendor/jquery-ui/jquery-ui.min.js') ?>"></script>
<script src="<?= base_url('assets/vendor/datatables/jquery.dataTables.min.js') ?>"></script>
<script src="<?= base_url('assets/vendor/datatables/dataTables.bootstrap5.min.js') ?>"></script>
<script>
// The local DataTables file is a lightweight shim in this project.
// Load Bootstrap 5 adapter only when full DataTables core API exists.
(function() {
if (!(window.jQuery && jQuery.fn && jQuery.fn.dataTable && jQuery.fn.dataTable.defaults)) {
return;
}
var script = document.createElement('script');
script.src = "<?= base_url('assets/vendor/datatables/dataTables.bootstrap5.min.js') ?>";
document.head.appendChild(script);
})();
</script>
<script>
if (window.jQuery && typeof jQuery.isArray !== 'function') {
jQuery.isArray = Array.isArray;