409 lines
14 KiB
PHP
409 lines
14 KiB
PHP
<?php
|
|
namespace App\Models;
|
|
|
|
use CodeIgniter\Database\RawSql;
|
|
|
|
class PatientModel extends BaseUtcModel {
|
|
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', 'CreateDate', 'DelDate' ];
|
|
|
|
protected $useTimestamps = true;
|
|
protected $createdField = 'CreateDate';
|
|
protected $updatedField = '';
|
|
|
|
|
|
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->formattedDate($patient["DeathDateTime"]);
|
|
$patient["CreateDate"] = $this->formattedDate($patient["CreateDate"]);
|
|
$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 formattedDate(?string $dateString): ?string {
|
|
try {
|
|
if (empty($dateString)) {
|
|
return null;
|
|
}
|
|
|
|
$dt = new \DateTime($dateString, new \DateTimeZone("UTC"));
|
|
return $dt->format('Y-m-d\TH:i:s\Z'); // ISO 8601 UTC
|
|
} catch (\Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
if (empty($datetime) || $datetime=="") {return null; }
|
|
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;
|
|
}
|
|
}
|
|
|
|
}
|