clqms-be/app/Models/PatientModel.php
2025-10-06 13:23:18 +07:00

399 lines
14 KiB
PHP

<?php
namespace App\Models;
use CodeIgniter\Model;
use CodeIgniter\Database\RawSql;
class PatientModel extends Model {
protected $table = 'patient';
protected $primaryKey = 'InternalPID';
protected $allowedFields = ['PatientID', 'AlternatePID', 'Prefix', 'NameFirst', 'NameMiddle', 'NameMaiden', 'NameLast', 'Suffix', 'NameAlias', 'Gender', 'Birthdate', 'PlaceOfBirth', 'Street_1', 'Street_2', 'Street_3',
'City', 'Province', 'ZIP', 'EmailAddress1', 'EmailAddress2', 'Phone', 'MobilePhone', 'Custodian', 'AccountNumber', 'Country', 'Race', 'MaritalStatus', 'Religion', 'Ethnic', 'Citizenship',
'DeathIndicator', 'DeathDateTime', 'LinkTo' ];
public function getPatients($filters = []) {
$qname = "LOWER(CONCAT_WS(' ', IFNULL(Prefix,''), IFNULL(NameFirst,''), IFNULL(NameMiddle,''), IFNULL(NameLast,''), IFNULL(NameMaiden,''), IFNULL(Suffix,'')))";
$builder = $this->db->table($this->table);
$builder->select("InternalPID, PatientID, $qname as FullName, Gender, Birthdate, EmailAddress1 as Email, MobilePhone");
if (!empty($filters['Name'])) {
$rawSql = new RawSql($qname);
$builder->like($rawSql, $filters['Name'], 'both');
}
if (!empty($filters['InternalPID'])) {
$builder->where('InternalPID', $filters['InternalPID']);
}
if (!empty($filters['PatientID'])) {
$builder->like('PatientID', $filters['PatientID'], 'both');
}
if (!empty($filters['Birthdate'])) {
$builder->where('Birthdate', $filters['Birthdate']);
}
return $builder->get()->getResultArray();
}
public function getPatient($InternalPID) {
$rows = $this->db->table('patient p')
->select("
p.*,
country.VDesc as Country,
country.VID as CountryVID,
race.VDesc as Race,
race.VID as RaceVID,
religion.VDesc as Religion,
religion.VID as ReligionVID,
ethnic.VDesc as Ethnic,
ethnic.VID as EthnicVID,
gender.VDesc as Gender,
gender.VID as GenderVID,
deathindicator.VDesc as DeathIndicator,
deathindicator.VID as DeathIndicatorVID,
maritalstatus.VDesc as MaritalStatus,
maritalstatus.VID as MaritalStatusVID,
patcom.Comment as Comment,
patidt.IdentifierType,
patidt.Identifier,
patatt.Address
")
->join('valueset country', 'country.VID = p.Country', 'left')
->join('valueset race', 'race.VID = p.Race', 'left')
->join('valueset religion', 'religion.VID = p.Religion', 'left')
->join('valueset ethnic', 'ethnic.VID = p.Ethnic', 'left')
->join('valueset gender', 'gender.VID = p.Gender', 'left')
->join('valueset deathindicator', 'deathindicator.VID = p.DeathIndicator', 'left')
->join('valueset maritalstatus', 'maritalstatus.VID = p.MaritalStatus', 'left')
->join('patcom', 'patcom.InternalPID = p.InternalPID', 'left')
->join('patidt', 'patidt.InternalPID = p.InternalPID', 'left')
->join('patatt', 'patatt.InternalPID = p.InternalPID and patatt.DelDate is null', 'left')
->where('p.InternalPID', (int) $InternalPID)
->get()
->getResultArray();
if (empty($rows)) { return null; }
$patient = $rows[0];
if (method_exists($this, 'transformPatientData')) { $patient = $this->transformPatientData($patient); }
unset($patient['Address']);
unset($patient['IdentifierType']);
unset($patient['Identifier']);
unset($patient['Comment']);
// Default nested structures
$patient['PatIdt'] = null;
$patient['PatAtt'] = [];
foreach ($rows as $row) {
if ($row['IdentifierType'] && $row['Identifier'] && !$patient['PatIdt']) {
$patient['PatIdt'] = [
'IdentifierType' => $row['IdentifierType'],
'Identifier' => $row['Identifier'],
];
}
if ($row['Address']) {
$patient['PatAtt'][] = ['Address' => $row['Address']];
}
}
if (empty($patient['PatIdt'])) { $patient['PatIdt'] = null; }
if (empty($patient['PatAtt'])) { $patient['PatAtt'] = null; }
return $patient;
}
public function createPatient($input) {
$db = \Config\Database::connect();
$patidt = $input['PatIdt'] ?? [];
$patatt = $input['PatAtt'] ?? [];
$patcom['Comment'] = $input['PatCom'] ?? null;
$input['LinkTo'] = empty($input['LinkTo']) ? null : $input['LinkTo'];
$input['Birthdate'] = $this->isValidDateTime($input['Birthdate']);
$input['DeathDateTime'] = $this->isValidDateTime($input['DeathDateTime']);
try {
$db->transStart();
$this->insert($input);
$newInternalPID = $this->getInsertID();
$this->checkDbError($db, 'Insert patient');
if (!empty($patidt)) {
$patidt['InternalPID'] = $newInternalPID;
$db->table('patidt')->insert($patidt);
$this->checkDbError($db, 'Insert patidt');
}
if (!empty($patcom)) {
$patcom['InternalPID'] = $newInternalPID;
$db->table('patcom')->insert($patcom);
$this->checkDbError($db, 'Insert patcom');
}
if (!empty($patatt)) {
foreach ($patatt as &$row) {
$row['InternalPID'] = $newInternalPID;
}
$db->table('patatt')->upsertBatch($patatt);
$this->checkDbError($db, 'Insert patatt');
}
$db->transComplete();
if ($db->transStatus() === false) {
throw new \Exception("Failed to sync patient relations");
}
return $newInternalPID;
} catch (\Exception $e) {
// $db->transRollback();
throw $e;
}
}
public function updatePatient($input) {
$db = \Config\Database::connect();
$patidt = $input['PatIdt'] ?? [];
$patatt = $input['PatAtt'] ?? [];
$patcom['Comment'] = $input['PatCom'] ?? null;
$input['LinkTo'] = empty($input['LinkTo']) ? null : $input['LinkTo'];
$input['Birthdate'] = $this->isValidDateTime($input['Birthdate']);
$input['DeathDateTime'] = $this->isValidDateTime($input['DeathDateTime']);
try {
$db->transStart();
$InternalPID = $input['InternalPID'];
$this->where('InternalPID',$InternalPID)->set($input)->update();
$this->checkDbError($db, 'Update patient');
$now = date('Y-m-d H:i:s');
if (!empty($patidt)) {
$exists = $db->table('patidt')->where('InternalPID', $InternalPID)->get()->getRowArray();
if ($exists) {
$db->table('patidt')->where('InternalPID', $InternalPID)->update($patidt);
} else {
$patidt['InternalPID'] = $InternalPID;
$db->table('patidt')->insert($patidt);
$this->checkDbError($db, 'Update patidt');
}
} else {
$db->table('patidt')->where('InternalPID', $InternalPID)->delete();
$this->checkDbError($db, 'Update patidt');
}
if (!empty($patcom)) {
$exists = $db->table('patcom')->where('InternalPID', $InternalPID)->get()->getRowArray();
if ($exists) {
$db->table('patcom')->where('InternalPID', $InternalPID)->update($patcom);
$this->checkDbError($db, 'Update patcom');
} else {
$patcom['InternalPID'] = $InternalPID;
$db->table('patcom')->insert($patcom);
$this->checkDbError($db, 'Update patcom');
}
} else {
$db->table('patcom')->where('InternalPID', $InternalPID)->delete();
$this->checkDbError($db, 'Update patcom');
}
if (!empty($patatt)) {
// Ambil daftar address aktif (DelDate IS NULL) di DB
$oldActive = $db->table('patatt')
->select('Address')
->where('InternalPID', $InternalPID)
->where('DelDate', null)
->get()->getResultArray();
$oldActive = array_column($oldActive, 'Address');
// Normalisasi & dedup input baru (berdasarkan Address)
$mapNew = [];
foreach ($patatt as $row) {
if (!isset($row['Address'])) continue;
$mapNew[$row['Address']] = $row; // overwrite duplikat di input
}
$newData = array_keys($mapNew);
// Hitung yang perlu ditambah & dihapus
$added = array_diff($newData, $oldActive); // baru (belum aktif)
$removed = array_diff($oldActive, $newData); // dulu aktif tapi hilang di input
// 1) Soft delete yang dihapus
if (!empty($removed)) {
$db->table('patatt')
->where('InternalPID', $InternalPID)
->whereIn('Address', $removed)
->set('DelDate', $now)
->update();
$this->checkDbError($db, 'Update/Delete patatt');
}
// 2) Tambahkan yang baru
foreach ($added as $addr) {
$data = $mapNew[$addr];
$data['InternalPID'] = $InternalPID;
// Coba REACTIVATE satu baris yang pernah di-soft delete (kalau ada)
$builder = $db->table('patatt');
$builder->set('DelDate', null);
// Kalau ada kolom lain yang mau di-update saat re-activate, set di sini juga
// mis: $builder->set('Note', $data['Note'] ?? null);
$builder->where('InternalPID', $InternalPID)
->where('Address', $addr)
->where('DelDate IS NOT NULL', null, false)
->orderBy('PatAttID', 'DESC')
->limit(1)
->update();
$this->checkDbError($db, 'Update/Insert patatt');
if ($db->affectedRows() === 0) {
// Tidak ada baris soft-deleted untuk alamat ini → INSERT baru
$db->table('patatt')->insert($data);
$this->checkDbError($db, 'Update/Insert patatt');
}
}
} else {
// Input kosong → semua yang masih aktif di-soft delete
$db->table('patatt')->where('InternalPID', $InternalPID)->where('DelDate', null)->set('DelDate', $now)->update();
$this->checkDbError($db, 'Update/Delete patatt');
}
$db->transComplete();
if ($db->transStatus() === false) {
throw new \Exception('Failed to sync patient relations');
}
return $InternalPID;
} catch (\Exception $e) {
// $db->transRollback();
throw $e;
}
}
private function transformPatientData(array $patient): array {
$patient["Age"] = $this->calculateAgeFromBirthdate($patient["Birthdate"], $patient["DeathDateTime"]);
// $patient["Birthdate"] = $this->formatedDate($patient["Birthdate"]);
// $patient["CreateDate"] = $this->formatedDate($patient["CreateDate"]);
// $patient["DelDate"] = $this->formatedDate($patient["DelDate"]);
$patient["DeathDateTime"] = $this->formatedDate($patient["DeathDateTime"]);
$patient["BirthdateConversion"] = $this->formatedDateForDisplay($patient["Birthdate"]);
$patient["LinkTo"] = $this->getLinkedPatients($patient['LinkTo']);
$patient["Custodian"] = $this->getCustodian($patient['Custodian']);
$patient['PatCom'] = $patient['Comment'];
return $patient;
}
private function getLinkedPatients(?string $linkTo): ?array {
if (empty($linkTo)) { return null; }
$ids = array_filter(explode(',', $linkTo));
return $this->db->table('patient')
->select('InternalPID, PatientID')
->whereIn('InternalPID', $ids)
->get()
->getResultArray() ?: null;
}
private function getCustodian($custodianId): ?array {
if (empty($custodianId)) {
return null;
}
return $this->db->table('patient')
->select('InternalPID, PatientID')
->where('InternalPID', (int) $custodianId)
->get()
->getRowArray() ?: null;
}
// Conversion to (Years Months Days)
private function calculateAgeFromBirthdate($birthdate, $deathdatetime) {
$dob = new \DateTime($birthdate);
// Cek DeathTime
if ($deathdatetime == null) {
$today = new \DateTime();
} else {
$deathdatetime = new \DateTime($deathdatetime);
$today = $deathdatetime;
}
$diff = $today->diff($dob);
$formattedAge = "";
if ($diff->y > 0){
$formattedAge .= "{$diff->y} Years ";
}
if ($diff->m > 0){
$formattedAge .= "{$diff->m} Months ";
}
if ($diff->d > 0){
$formattedAge .= "{$diff->d} Days";
}
return $formattedAge;
}
// Conversion Time to Format Y-m-d\TH:i:s\Z
private function formatedDate($dateString) {
// return $date->format('Y-m-d H:i');
$dt = new \DateTime($dateString, new \DateTimeZone("UTC"));
// Format jadi ISO 8601 dengan 'Z'
return $dt->format('Y-m-d\TH:i:s\Z');
}
// Conversion Time to Format j M Y
private function formatedDateForDisplay($dateString) {
$date = \DateTime::createFromFormat('Y-m-d H:i', $dateString);
if (!$date) {
$timestamp = strtotime($dateString);
if ($timestamp) {
return date('j M Y', $timestamp);
}
return null;
}
return $date->format('j M Y');
}
// Check Error and Send Spesific Messages
private function checkDbError($db, string $context) {
$error = $db->error();
if (!empty($error['code'])) {
throw new \Exception(
"{$context} failed: {$error['code']} - {$error['message']}"
);
}
}
// Preventif 0000-00-00
private function isValidDateTime($datetime) {
try {
// Kalau input hanya Y-m-d (tanpa jam)
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $datetime)) {
$dt = \DateTime::createFromFormat('Y-m-d', $datetime);
return $dt ? $dt->format('Y-m-d') : null; // hanya tanggal
}
// Selain itu (ISO 8601 atau datetime lain), format ke Y-m-d H:i:s
$dt = new \DateTime($datetime);
return $dt->format('Y-m-d H:i:s');
} catch (\Exception $e) {
return null;
}
}
}