diff --git a/app/Config/Routes.php b/app/Config/Routes.php index ebc7aa9..9bc07b2 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -687,6 +687,7 @@ $routes->get('AbdmGateway/immunization_fhir_preview', 'AbdmGateway::immunization $routes->post('AbdmGateway/share_immunization_bundle', 'AbdmGateway::shareImmunizationBundle', ['filter' => $abdmPermFilter]); $routes->get('Immunization', 'Immunization::index', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']); $routes->get('Immunization/schedule_master', 'Immunization::scheduleMaster', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']); +$routes->post('Immunization/sync_uip_master', 'Immunization::syncUipMaster', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']); $routes->post('Immunization/update_schedule/(:num)', 'Immunization::updateSchedule/$1', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']); $routes->get('Immunization/schedule', 'Immunization::schedule', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']); $routes->get('Immunization/patient/(:num)', 'Immunization::patient/$1', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']); diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index 498eb11..e6157d5 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -2139,6 +2139,7 @@ class AbdmGateway extends BaseController $queueId = null; $bridgeRecordId = 0; $connectorError = null; + $bridgeResponse = []; try { $result = $this->connector->pushRecord([ 'patient_id' => (string) $patientId, @@ -2154,8 +2155,32 @@ class AbdmGateway extends BaseController 'queue_id' => $ccRef, 'record_data' => $bundle, ]); - $queueId = (string) ($result['queue_id'] ?? ''); - $bridgeRecordId = (int) ($result['id'] ?? 0); + $bridgeResponse = $result; + $queueId = (string) ($result['queue_id'] ?? $result['existing_queue_id'] ?? ''); + $bridgeRecordId = (int) ($result['record_id'] ?? $result['id'] ?? $result['existing_record_id'] ?? 0); + + $httpCode = (int) ($result['http_code'] ?? 0); + $resultOk = (int) ($result['ok'] ?? 0) === 1; + $errorCode = strtoupper(trim((string) ($result['error_code'] ?? ''))); + $isDuplicate = $httpCode === 409 || $errorCode === 'DUPLICATE_RECORD'; + + if ($isDuplicate) { + $resultOk = true; + } + + if (! $resultOk) { + $connectorError = trim((string) ( + $result['message'] + ?? $result['error_text'] + ?? ($result['error_code'] ?? '') + )); + if ($connectorError === '' && ! empty($result['errors'])) { + $connectorError = (string) json_encode($result['errors'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + if ($connectorError === '') { + $connectorError = 'Bridge push failed'; + } + } } catch (\Throwable $e) { $connectorError = $e->getMessage(); } @@ -2171,7 +2196,12 @@ class AbdmGateway extends BaseController 'abha_id' => $abhaId, 'patient_id' => $patientId, 'request' => ['patient_id' => $patientId, 'record_id' => $recordId, 'hi_type' => 'ImmunizationRecord'], - 'response' => ['queue_id' => $queueId], + 'response' => [ + 'queue_id' => $queueId, + 'bridge_record_id' => $bridgeRecordId > 0 ? $bridgeRecordId : null, + 'http_code' => $bridgeResponse['http_code'] ?? null, + 'error_code' => $bridgeResponse['error_code'] ?? null, + ], 'outcome' => $connectorError === null ? 'success' : 'failure', 'error_message' => (string) ($connectorError ?? ''), ]); diff --git a/app/Controllers/Immunization.php b/app/Controllers/Immunization.php index bb453cd..610e98d 100644 --- a/app/Controllers/Immunization.php +++ b/app/Controllers/Immunization.php @@ -2,6 +2,7 @@ namespace App\Controllers; +use App\Libraries\ImmunizationMasterSyncService; use App\Libraries\ImmunizationScheduleService; class Immunization extends BaseController @@ -40,9 +41,28 @@ class Immunization extends BaseController public function scheduleMaster() { + $syncService = new ImmunizationMasterSyncService($this->db); + return view('immunization/schedule_master', [ 'page_title' => 'UIP Schedule Master', 'items' => $this->loadScheduleRows(true), + 'sync_meta' => $syncService->latestSyncMeta(), + ]); + } + + public function syncUipMaster() + { + if (! $this->request->isAJAX()) { + return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']); + } + + $force = (int) ($this->request->getPost('force') ?? 0) === 1; + $result = (new ImmunizationMasterSyncService($this->db))->syncUipMaster($force); + $httpCode = (int) ($result['ok'] ?? 0) === 1 ? 200 : 400; + + return $this->response->setStatusCode($httpCode)->setJSON($result + [ + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), ]); } diff --git a/app/Database/Migrations/2026-07-19-000002_AddBridgeMetadataToImmunizationMaster.php b/app/Database/Migrations/2026-07-19-000002_AddBridgeMetadataToImmunizationMaster.php new file mode 100644 index 0000000..35bc689 --- /dev/null +++ b/app/Database/Migrations/2026-07-19-000002_AddBridgeMetadataToImmunizationMaster.php @@ -0,0 +1,112 @@ +addScheduleColumns(); + $this->createSyncLogTable(); + } + + public function down(): void + { + $this->forge->dropTable('immunization_master_sync_log', true); + + if (! $this->tableExists('immunization_schedule_master')) { + return; + } + + foreach (array_keys($this->scheduleColumns()) as $column) { + if ($this->columnExists('immunization_schedule_master', $column)) { + $this->forge->dropColumn('immunization_schedule_master', $column); + } + } + } + + private function addScheduleColumns(): void + { + if (! $this->tableExists('immunization_schedule_master')) { + return; + } + + foreach ($this->scheduleColumns() as $column => $definition) { + if ($this->columnExists('immunization_schedule_master', $column)) { + continue; + } + $this->forge->addColumn('immunization_schedule_master', [$column => $definition]); + } + } + + /** + * @return array> + */ + private function scheduleColumns(): array + { + return [ + 'schedule_code' => ['type' => 'VARCHAR', 'constraint' => 80, 'null' => true], + 'beneficiary_category' => ['type' => 'VARCHAR', 'constraint' => 40, 'null' => true], + 'gender_applicability' => ['type' => 'VARCHAR', 'constraint' => 20, 'null' => true], + 'min_age_days' => ['type' => 'INT', 'constraint' => 11, 'null' => true], + 'max_age_days' => ['type' => 'INT', 'constraint' => 11, 'null' => true], + 'gateway_schedule_id' => ['type' => 'INT', 'constraint' => 11, 'null' => true], + 'is_state_specific' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0], + 'state_code' => ['type' => 'VARCHAR', 'constraint' => 20, 'null' => true], + 'district_code' => ['type' => 'VARCHAR', 'constraint' => 20, 'null' => true], + 'applicability_note' => ['type' => 'TEXT', 'null' => true], + 'source_version_code' => ['type' => 'VARCHAR', 'constraint' => 80, 'null' => true], + 'source_checksum' => ['type' => 'VARCHAR', 'constraint' => 128, 'null' => true], + 'source_name' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], + 'source_url' => ['type' => 'VARCHAR', 'constraint' => 500, 'null' => true], + 'source_document_name' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], + 'effective_from' => ['type' => 'DATE', 'null' => true], + ]; + } + + private function createSyncLogTable(): void + { + if ($this->tableExists('immunization_master_sync_log')) { + return; + } + + $this->forge->addField([ + 'id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + 'auto_increment' => true, + ], + 'master_type' => ['type' => 'VARCHAR', 'constraint' => 60, 'default' => 'UIP_SCHEDULE'], + 'version_code' => ['type' => 'VARCHAR', 'constraint' => 80, 'null' => true], + 'effective_from' => ['type' => 'DATE', 'null' => true], + 'checksum' => ['type' => 'VARCHAR', 'constraint' => 128, 'null' => true], + 'source_name' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], + 'source_url' => ['type' => 'VARCHAR', 'constraint' => 500, 'null' => true], + 'source_document_name' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], + 'status' => ['type' => 'VARCHAR', 'constraint' => 30, 'default' => 'success'], + 'item_count' => ['type' => 'INT', 'constraint' => 11, 'default' => 0], + 'message' => ['type' => 'TEXT', 'null' => true], + 'synced_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addKey('id', true); + $this->forge->addKey('master_type'); + $this->forge->addKey('version_code'); + $this->forge->addKey('checksum'); + $this->forge->createTable('immunization_master_sync_log', true); + } + + private function tableExists(string $table): bool + { + $row = $this->db->query('SHOW TABLES LIKE ?', [$table])->getRowArray(); + return ! empty($row); + } + + private function columnExists(string $table, string $column): bool + { + $row = $this->db->query('SHOW COLUMNS FROM `' . $table . '` LIKE ?', [$column])->getRowArray(); + return ! empty($row); + } +} \ No newline at end of file diff --git a/app/Libraries/Abdm/EAtriaBridgeConnector.php b/app/Libraries/Abdm/EAtriaBridgeConnector.php index b4191a1..e521b3f 100644 --- a/app/Libraries/Abdm/EAtriaBridgeConnector.php +++ b/app/Libraries/Abdm/EAtriaBridgeConnector.php @@ -871,6 +871,30 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface return $this->get('/v3/opd/running-token-status'); } + // ------------------------------------------------------------------------- + // Immunization Master Data + // ------------------------------------------------------------------------- + + public function immunizationUipVersion(): array + { + return $this->get('/v3/master-data/immunization/uip/version'); + } + + public function immunizationUipSchedule(): array + { + return $this->get('/v3/master-data/immunization/uip'); + } + + public function immunizationUipChanges(string $sinceVersion): array + { + $query = []; + if ($sinceVersion !== '') { + $query['since_version'] = $sinceVersion; + } + + return $this->get('/v3/master-data/immunization/uip/changes', $query); + } + // ------------------------------------------------------------------------- // Bridge Records — list // ------------------------------------------------------------------------- diff --git a/app/Libraries/ImmunizationMasterSyncService.php b/app/Libraries/ImmunizationMasterSyncService.php new file mode 100644 index 0000000..d149d35 --- /dev/null +++ b/app/Libraries/ImmunizationMasterSyncService.php @@ -0,0 +1,364 @@ +db = $db ?? db_connect(); + $this->connector = $connector ?? AbdmConnectorFactory::make(); + } + + /** + * @return array + */ + public function latestSyncMeta(): array + { + if (! $this->db->tableExists('immunization_master_sync_log')) { + return []; + } + + return $this->db->table('immunization_master_sync_log') + ->where('master_type', 'UIP_SCHEDULE') + ->orderBy('id', 'DESC') + ->get(1) + ->getRowArray() ?? []; + } + + /** + * @return array + */ + public function syncUipMaster(bool $force = false): array + { + foreach (['immunization_vaccine_master', 'immunization_schedule_master'] as $table) { + if (! $this->db->tableExists($table)) { + return ['ok' => 0, 'error_text' => $table . ' table not found. Run migrations first.']; + } + } + + if (! method_exists($this->connector, 'immunizationUipVersion') || ! method_exists($this->connector, 'immunizationUipSchedule')) { + return ['ok' => 0, 'error_text' => 'Current ABDM connector does not support E-Atria immunization master sync.']; + } + + $remoteVersion = $this->connector->immunizationUipVersion(); + if ((int) ($remoteVersion['ok'] ?? 0) !== 1) { + $this->writeSyncLog([], 'failed', 0, $this->bridgeError($remoteVersion)); + return ['ok' => 0, 'error_text' => $this->bridgeError($remoteVersion), 'bridge_response' => $remoteVersion]; + } + + $local = $this->latestSyncMeta(); + $remoteVersionCode = trim((string) ($remoteVersion['version_code'] ?? '')); + $remoteChecksum = trim((string) ($remoteVersion['checksum'] ?? '')); + if (! $force + && $remoteVersionCode !== '' + && $remoteChecksum !== '' + && $remoteVersionCode === (string) ($local['version_code'] ?? '') + && $remoteChecksum === (string) ($local['checksum'] ?? '') + ) { + return [ + 'ok' => 1, + 'status' => 'already_current', + 'version_code' => $remoteVersionCode, + 'checksum' => $remoteChecksum, + 'message' => 'Local UIP master already matches bridge version.', + ]; + } + + $catalog = $this->connector->immunizationUipSchedule(); + if ((int) ($catalog['ok'] ?? 0) !== 1) { + $this->writeSyncLog($remoteVersion, 'failed', 0, $this->bridgeError($catalog)); + return ['ok' => 0, 'error_text' => $this->bridgeError($catalog), 'bridge_response' => $catalog]; + } + + $version = is_array($catalog['version'] ?? null) ? $catalog['version'] : $remoteVersion; + $items = $catalog['vaccines'] ?? $catalog['schedule'] ?? $catalog['items'] ?? []; + if (! is_array($items) || $items === []) { + $this->writeSyncLog($version, 'failed', 0, 'Bridge returned empty UIP schedule.'); + return ['ok' => 0, 'error_text' => 'Bridge returned empty UIP schedule.']; + } + + $counts = ['inserted' => 0, 'updated' => 0, 'vaccines' => 0]; + $syncedCodes = []; + + $this->db->transStart(); + foreach ($items as $item) { + if (! is_array($item)) { + continue; + } + + $vaccineId = $this->upsertVaccine($item); + if ($vaccineId <= 0) { + continue; + } + $counts['vaccines']++; + + $result = $this->upsertSchedule($item, $vaccineId, $version); + if ($result['schedule_code'] !== '') { + $syncedCodes[] = $result['schedule_code']; + } + if ($result['created']) { + $counts['inserted']++; + } else { + $counts['updated']++; + } + } + $this->deactivateMissingBridgeRows(array_values(array_unique($syncedCodes)), (string) ($version['version_code'] ?? '')); + $this->writeSyncLog($version, 'success', count($items), 'UIP schedule synced from E-Atria bridge.'); + $this->db->transComplete(); + + if (! $this->db->transStatus()) { + return ['ok' => 0, 'error_text' => 'Database transaction failed while syncing UIP schedule.']; + } + + return [ + 'ok' => 1, + 'status' => 'synced', + 'version_code' => (string) ($version['version_code'] ?? $remoteVersionCode), + 'checksum' => (string) ($version['checksum'] ?? $remoteChecksum), + 'item_count' => count($items), + 'inserted' => $counts['inserted'], + 'updated' => $counts['updated'], + 'message' => 'UIP schedule synced from E-Atria bridge.', + ]; + } + + private function upsertVaccine(array $item): int + { + $now = date('Y-m-d H:i:s'); + $vaccineName = trim((string) ($item['vaccine_name'] ?? $item['vaccine_display'] ?? $item['series_name'] ?? $item['vaccine_code'] ?? '')); + if ($vaccineName === '') { + return 0; + } + + $payload = $this->filterColumns('immunization_vaccine_master', [ + 'vaccine_name' => $vaccineName, + 'vaccine_code' => $this->nullableString($item['vaccine_code'] ?? null), + 'vaccine_code_system' => $this->nullableString($item['vaccine_code_system'] ?? null), + 'vaccine_display' => $this->nullableString($item['vaccine_display'] ?? $vaccineName), + 'target_disease_code' => $this->nullableString($item['target_disease_code'] ?? null), + 'target_disease_name' => $this->nullableString($item['target_disease_name'] ?? null), + 'route_code' => $this->nullableString($item['route_code'] ?? null), + 'route_name' => $this->nullableString($item['route_name'] ?? null), + 'site_code' => $this->nullableString($item['site_code'] ?? null), + 'site_name' => $this->nullableString($item['site_name'] ?? null), + 'is_active' => $this->boolInt($item['is_active'] ?? 1), + 'updated_at' => $now, + ]); + + $existing = []; + $vaccineCode = trim((string) ($payload['vaccine_code'] ?? '')); + if ($vaccineCode !== '') { + $existing = $this->db->table('immunization_vaccine_master')->where('vaccine_code', $vaccineCode)->get(1)->getRowArray() ?? []; + } + if ($existing === []) { + $existing = $this->db->table('immunization_vaccine_master')->where('vaccine_name', $vaccineName)->get(1)->getRowArray() ?? []; + } + + if ($existing !== []) { + $this->db->table('immunization_vaccine_master')->where('id', (int) $existing['id'])->update($payload); + return (int) $existing['id']; + } + + $payload['created_at'] = $now; + $this->db->table('immunization_vaccine_master')->insert($payload); + return (int) $this->db->insertID(); + } + + /** + * @return array{created:bool,schedule_id:int,schedule_code:string} + */ + private function upsertSchedule(array $item, int $vaccineId, array $version): array + { + $now = date('Y-m-d H:i:s'); + $scheduleCode = trim((string) ($item['schedule_code'] ?? '')); + $payload = $this->filterColumns('immunization_schedule_master', [ + 'schedule_code' => $this->nullableString($scheduleCode), + 'schedule_name' => $this->nullableString($item['schedule_name'] ?? 'UIP') ?? 'UIP', + 'beneficiary_category' => $this->nullableString($item['beneficiary_category'] ?? null), + 'gender_applicability' => $this->nullableString($item['gender_applicability'] ?? null), + 'age_label' => (string) ($item['age_label'] ?? ''), + 'age_value' => (int) ($item['age_value'] ?? 0), + 'age_unit' => (string) ($item['age_unit'] ?? 'days'), + 'age_offset_days' => (int) ($item['age_offset_days'] ?? 0), + 'min_age_days' => $this->nullableInt($item['min_age_days'] ?? null), + 'max_age_days' => $this->nullableInt($item['max_age_days'] ?? null), + 'gateway_schedule_id' => $this->nullableInt($item['id'] ?? null), + 'vaccine_master_id' => $vaccineId, + 'dose_number' => $this->nullableString($item['dose_number'] ?? null), + 'series_name' => $this->nullableString($item['series_name'] ?? null) ?? 'Indian Universal Immunization Programme', + 'series_doses' => $this->nullableString($item['series_doses'] ?? null), + 'notes' => $this->nullableString($item['notes'] ?? null), + 'sort_order' => (int) ($item['sort_order'] ?? 0), + 'is_uip' => 1, + 'is_active' => $this->boolInt($item['is_active'] ?? 1), + 'is_state_specific' => $this->boolInt($item['is_state_specific'] ?? 0), + 'state_code' => $this->nullableString($item['state_code'] ?? null), + 'district_code' => $this->nullableString($item['district_code'] ?? null), + 'applicability_note' => $this->nullableString($item['applicability_note'] ?? null), + 'source_version_code' => $this->nullableString($version['version_code'] ?? null), + 'source_checksum' => $this->nullableString($version['checksum'] ?? null), + 'source_name' => $this->nullableString($version['source_name'] ?? null), + 'source_url' => $this->nullableString($version['source_url'] ?? null), + 'source_document_name' => $this->nullableString($version['source_document_name'] ?? null), + 'effective_from' => $this->normalizeDate((string) ($version['effective_from'] ?? '')), + 'updated_at' => $now, + ]); + + $existing = $this->findSchedule($scheduleCode, $vaccineId, $payload); + if ($existing !== []) { + $scheduleId = (int) $existing['id']; + $this->db->table('immunization_schedule_master')->where('id', $scheduleId)->update($payload); + $this->syncGeneratedRecords($scheduleId, $item, $payload); + return ['created' => false, 'schedule_id' => $scheduleId, 'schedule_code' => $scheduleCode]; + } + + $payload['created_at'] = $now; + $this->db->table('immunization_schedule_master')->insert($payload); + return ['created' => true, 'schedule_id' => (int) $this->db->insertID(), 'schedule_code' => $scheduleCode]; + } + + private function findSchedule(string $scheduleCode, int $vaccineId, array $payload): array + { + $fields = $this->db->getFieldNames('immunization_schedule_master') ?? []; + if ($scheduleCode !== '' && in_array('schedule_code', $fields, true)) { + $row = $this->db->table('immunization_schedule_master')->where('schedule_code', $scheduleCode)->get(1)->getRowArray(); + if (! empty($row)) { + return $row; + } + } + + $row = $this->db->table('immunization_schedule_master') + ->where('vaccine_master_id', $vaccineId) + ->where('age_offset_days', (int) ($payload['age_offset_days'] ?? 0)) + ->where('dose_number', (string) ($payload['dose_number'] ?? '')) + ->get(1) + ->getRowArray() ?? []; + if (! empty($row)) { + return $row; + } + + return $this->db->table('immunization_schedule_master') + ->where('vaccine_master_id', $vaccineId) + ->where('age_offset_days', (int) ($payload['age_offset_days'] ?? 0)) + ->orderBy('id', 'ASC') + ->get(1) + ->getRowArray() ?? []; + } + + private function syncGeneratedRecords(int $scheduleId, array $item, array $schedulePayload): void + { + if (! $this->db->tableExists('immunization_records')) { + return; + } + + $this->db->table('immunization_records') + ->where('schedule_id', $scheduleId) + ->where('status !=', 'completed') + ->update($this->filterColumns('immunization_records', [ + 'vaccine_name' => (string) ($item['vaccine_name'] ?? $item['vaccine_display'] ?? $item['series_name'] ?? $item['vaccine_code'] ?? ''), + 'vaccine_code' => $this->nullableString($item['vaccine_code'] ?? null), + 'vaccine_code_system' => $this->nullableString($item['vaccine_code_system'] ?? null), + 'dose_number' => $this->nullableString($schedulePayload['dose_number'] ?? null), + 'route_code' => $this->nullableString($item['route_code'] ?? null), + 'route_name' => $this->nullableString($item['route_name'] ?? null), + 'site_code' => $this->nullableString($item['site_code'] ?? null), + 'site_name' => $this->nullableString($item['site_name'] ?? null), + 'notes' => $this->nullableString($schedulePayload['notes'] ?? null), + 'updated_at' => date('Y-m-d H:i:s'), + ])); + } + + /** + * @param array $syncedCodes + */ + private function deactivateMissingBridgeRows(array $syncedCodes, string $versionCode): void + { + if ($syncedCodes === [] || ! in_array('schedule_code', $this->db->getFieldNames('immunization_schedule_master') ?? [], true)) { + return; + } + + $builder = $this->db->table('immunization_schedule_master') + ->where('is_uip', 1) + ->where('schedule_code IS NOT NULL', null, false) + ->where('schedule_code !=', '') + ->whereNotIn('schedule_code', $syncedCodes); + if ($versionCode !== '') { + $builder->where('source_version_code !=', $versionCode); + } + $builder->update(['is_active' => 0, 'updated_at' => date('Y-m-d H:i:s')]); + } + + private function writeSyncLog(array $version, string $status, int $itemCount, string $message): void + { + if (! $this->db->tableExists('immunization_master_sync_log')) { + return; + } + + $this->db->table('immunization_master_sync_log')->insert([ + 'master_type' => (string) ($version['master_type'] ?? 'UIP_SCHEDULE'), + 'version_code' => $this->nullableString($version['version_code'] ?? null), + 'effective_from' => $this->normalizeDate((string) ($version['effective_from'] ?? '')), + 'checksum' => $this->nullableString($version['checksum'] ?? null), + 'source_name' => $this->nullableString($version['source_name'] ?? null), + 'source_url' => $this->nullableString($version['source_url'] ?? null), + 'source_document_name' => $this->nullableString($version['source_document_name'] ?? null), + 'status' => $status, + 'item_count' => $itemCount, + 'message' => $message, + 'synced_at' => date('Y-m-d H:i:s'), + ]); + } + + private function bridgeError(array $response): string + { + $message = trim((string) ($response['message'] ?? $response['error_text'] ?? $response['error_code'] ?? '')); + if ($message !== '') { + return $message; + } + return 'Bridge request failed.'; + } + + private function filterColumns(string $table, array $payload): array + { + $fields = $this->db->getFieldNames($table) ?? []; + return array_intersect_key($payload, array_flip($fields)); + } + + private function nullableString($value): ?string + { + $value = trim((string) ($value ?? '')); + return $value !== '' ? $value : null; + } + + private function nullableInt($value): ?int + { + if ($value === null || $value === '') { + return null; + } + return (int) $value; + } + + private function boolInt($value): int + { + return in_array($value, [1, '1', true, 'true', 'yes', 'Y'], true) ? 1 : 0; + } + + private function normalizeDate(string $value): ?string + { + $value = trim($value); + if ($value === '') { + return null; + } + $timestamp = strtotime($value); + return $timestamp === false ? null : date('Y-m-d', $timestamp); + } +} \ No newline at end of file diff --git a/app/Models/ImmunizationScheduleModel.php b/app/Models/ImmunizationScheduleModel.php index 1160e99..465ef60 100644 --- a/app/Models/ImmunizationScheduleModel.php +++ b/app/Models/ImmunizationScheduleModel.php @@ -12,10 +12,16 @@ class ImmunizationScheduleModel extends Model protected $useTimestamps = false; protected $allowedFields = [ 'schedule_name', + 'schedule_code', + 'beneficiary_category', + 'gender_applicability', 'age_label', 'age_value', 'age_unit', 'age_offset_days', + 'min_age_days', + 'max_age_days', + 'gateway_schedule_id', 'vaccine_master_id', 'dose_number', 'series_name', @@ -24,6 +30,16 @@ class ImmunizationScheduleModel extends Model 'sort_order', 'is_uip', 'is_active', + 'is_state_specific', + 'state_code', + 'district_code', + 'applicability_note', + 'source_version_code', + 'source_checksum', + 'source_name', + 'source_url', + 'source_document_name', + 'effective_from', 'created_at', 'updated_at', ]; diff --git a/app/Views/immunization/schedule_master.php b/app/Views/immunization/schedule_master.php index 56db57a..cc7efab 100644 --- a/app/Views/immunization/schedule_master.php +++ b/app/Views/immunization/schedule_master.php @@ -2,16 +2,26 @@ $csrfName = csrf_token(); $csrfHash = csrf_hash(); $items = isset($items) && is_array($items) ? $items : []; +$syncMeta = isset($sync_meta) && is_array($sync_meta) ? $sync_meta : []; ?>
UIP Schedule Master
-
Edit vaccine, age, route, dose and notes used for new patient schedules.
+
+ Edit vaccine, age, route, dose and notes used for new patient schedules. + + Bridge version: . + +
+
+
+ +
-
+
@@ -165,6 +175,11 @@ $items = isset($items) && is_array($items) ? $items : []; $('#uipMasterStatus').html('
' + esc(message) + '
'); } + function showSyncStatus(type, message) { + var cls = type === 'success' ? 'alert-success' : 'alert-warning'; + $('#uipSyncStatus').removeClass('d-none').html('
' + esc(message) + '
'); + } + function fillModal(item) { $('#scheduleId').val(item.id || ''); $('#ageLabel').val(item.age_label || ''); @@ -228,6 +243,37 @@ $items = isset($items) && is_array($items) ? $items : []; }); } + function syncUipMaster() { + var payload = csrfPayload(); + payload.force = 1; + $('#btnSyncUipMaster').prop('disabled', true).text('Syncing...'); + showSyncStatus('success', 'Syncing UIP master from bridge...'); + + $.ajax({ + url: '', + method: 'POST', + dataType: 'json', + data: payload, + headers: {'X-Requested-With': 'XMLHttpRequest'} + }).done(function (data) { + updateCsrf(data); + if (parseInt(data.ok || 0, 10) !== 1) { + showSyncStatus('warning', data.error_text || 'Sync failed'); + return; + } + showSyncStatus('success', data.message || 'UIP master synced. Reloading...'); + window.setTimeout(function () { + load_form('', 'UIP Schedule Master'); + }, 650); + }).fail(function (xhr) { + updateCsrf(xhr.responseJSON || {}); + var json = xhr.responseJSON || {}; + showSyncStatus('warning', json.error_text || json.message || xhr.statusText || 'Sync failed'); + }).always(function () { + $('#btnSyncUipMaster').prop('disabled', false).text('Sync From Bridge'); + }); + } + $('#uipScheduleMasterTable').on('click', '.btnEditSchedule', function () { var raw = $(this).attr('data-item') || '{}'; try { @@ -237,5 +283,6 @@ $items = isset($items) && is_array($items) ? $items : []; } }); $('#btnSaveSchedule').on('click', saveSchedule); + $('#btnSyncUipMaster').on('click', syncUipMaster); })(); \ No newline at end of file