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["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) { 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; } } }