Add E-Atria immunization master sync

This commit is contained in:
DevSoftTech Server 2026-07-19 01:00:43 +05:30
parent 82fd31d105
commit 7a547eb443
8 changed files with 619 additions and 5 deletions

View File

@ -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']);

View File

@ -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 ?? ''),
]);

View File

@ -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(),
]);
}

View File

@ -0,0 +1,112 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddBridgeMetadataToImmunizationMaster extends Migration
{
public function up(): void
{
$this->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<string,array<string,mixed>>
*/
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);
}
}

View File

@ -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
// -------------------------------------------------------------------------

View File

@ -0,0 +1,364 @@
<?php
namespace App\Libraries;
use App\Libraries\Abdm\AbdmConnectorFactory;
use CodeIgniter\Database\BaseConnection;
class ImmunizationMasterSyncService
{
private BaseConnection $db;
/** @var object */
private $connector;
public function __construct(?BaseConnection $db = null, ?object $connector = null)
{
$this->db = $db ?? db_connect();
$this->connector = $connector ?? AbdmConnectorFactory::make();
}
/**
* @return array<string,mixed>
*/
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<string,mixed>
*/
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<int,string> $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);
}
}

View File

@ -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',
];

View File

@ -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 : [];
?>
<div class="container-fluid py-3 immunization-master-ui">
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<h5 class="mb-0">UIP Schedule Master</h5>
<div class="small text-muted">Edit vaccine, age, route, dose and notes used for new patient schedules.</div>
<div class="small text-muted">
Edit vaccine, age, route, dose and notes used for new patient schedules.
<?php if (! empty($syncMeta)) : ?>
Bridge version: <?= esc((string) ($syncMeta['version_code'] ?? '')) ?><?= ! empty($syncMeta['synced_at']) ? ' synced ' . esc((string) $syncMeta['synced_at']) : '' ?>.
<?php endif; ?>
</div>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-success" id="btnSyncUipMaster">Sync From Bridge</button>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="load_form('<?= site_url('Immunization') ?>','Immunization Record')">Back to Immunization</button>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="load_form('<?= site_url('Immunization') ?>','Immunization Record')">Back to Immunization</button>
</div>
<div class="px-3 pt-3 d-none" id="uipSyncStatus"></div>
<div class="card-body p-0">
<input type="hidden" id="uipCsrfName" value="<?= esc($csrfName) ?>">
<input type="hidden" id="uipCsrfHash" value="<?= esc($csrfHash) ?>">
@ -165,6 +175,11 @@ $items = isset($items) && is_array($items) ? $items : [];
$('#uipMasterStatus').html('<div class="alert ' + cls + ' py-2 mb-0">' + esc(message) + '</div>');
}
function showSyncStatus(type, message) {
var cls = type === 'success' ? 'alert-success' : 'alert-warning';
$('#uipSyncStatus').removeClass('d-none').html('<div class="alert ' + cls + ' py-2 mb-0">' + esc(message) + '</div>');
}
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: '<?= site_url('Immunization/sync_uip_master') ?>',
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('<?= site_url('Immunization/schedule_master') ?>', '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);
})();
</script>