Add E-Atria immunization master sync
This commit is contained in:
parent
82fd31d105
commit
7a547eb443
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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 ?? ''),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
|
|||
364
app/Libraries/ImmunizationMasterSyncService.php
Normal file
364
app/Libraries/ImmunizationMasterSyncService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user