Add patient list age fields

- Include Age and BirthdateConversion in patient list responses
- Keep Sex label transformation unchanged
- Preserve existing patient audit/update behavior
This commit is contained in:
mahdahar 2026-04-21 11:22:17 +07:00
parent 8fdaf0ade7
commit 4f87be295b

View File

@ -13,9 +13,9 @@ use App\Services\AuditService;
class PatientModel extends BaseModel {
protected $table = 'patient';
protected $primaryKey = 'InternalPID';
protected $allowedFields = ['PatientID', 'AlternatePID', 'Prefix', 'NameFirst', 'NameMiddle', 'NameMaiden', 'NameLast', 'Suffix', 'NameAlias', 'Sex', 'Birthdate', 'PlaceOfBirth', 'Street_1', 'Street_2', 'Street_3',
'City', 'Province', 'ZIP', 'EmailAddress1', 'EmailAddress2', 'Phone', 'MobilePhone', 'Custodian', 'AccountNumber', 'Country', 'Race', 'MaritalStatus', 'Religion', 'Ethnic', 'Citizenship',
'isDead', 'TimeOfDeath', 'LinkTo', 'CreateDate', 'DelDate' ];
protected $allowedFields = ['PatientID', 'AlternatePID', 'Prefix', 'NameFirst', 'NameMiddle', 'NameMaiden', 'NameLast', 'Suffix', 'NameAlias', 'Sex', 'Birthdate', 'PlaceOfBirth', 'Street_1', 'Street_2', 'Street_3',
'City', 'Province', 'ZIP', 'EmailAddress1', 'EmailAddress2', 'Phone', 'MobilePhone', 'Custodian', 'AccountNumber', 'Country', 'Race', 'MaritalStatus', 'Religion', 'Ethnic', 'Citizenship',
'isDead', 'TimeOfDeath', 'LinkTo', 'CreateDate', 'DelDate' ];
protected $useTimestamps = true;
protected $createdField = 'CreateDate';
@ -48,6 +48,16 @@ class PatientModel extends BaseModel {
$rows = ValueSet::transformLabels($rows, [
'Sex' => 'sex',
]);
foreach ($rows as &$row) {
$birthdate = $row['Birthdate'] ?? null;
$row['Age'] = empty($birthdate)
? ''
: $this->calculateAgeFromBirthdate($birthdate, null);
$row['BirthdateConversion'] = $this->formatedDateForDisplay($birthdate);
}
unset($row);
return $rows;
}
@ -82,14 +92,14 @@ class PatientModel extends BaseModel {
unset($patient['Identifier']);
unset($patient['Comment']);
$patient = ValueSet::transformLabels([$patient], [
'Sex' => 'sex',
'Country' => 'country',
'Race' => 'race',
'Religion' => 'religion',
'Ethnic' => 'ethnic',
'MaritalStatus' => 'marital_status',
])[0];
$patient = ValueSet::transformLabels([$patient], [
'Sex' => 'sex',
'Country' => 'country',
'Race' => 'race',
'Religion' => 'religion',
'Ethnic' => 'ethnic',
'MaritalStatus' => 'marital_status',
])[0];
$patient['PatIdt'] = null;
$patient['PatAtt'] = [];
@ -151,24 +161,24 @@ class PatientModel extends BaseModel {
$newInternalPID = $this->getInsertID();
$this->checkDbError($db, 'Insert patient');
$auditDiff = $this->buildAuditDiff([], $input);
AuditService::logData(
'PATIENT_REGISTERED',
'CREATE',
'patient',
(string) $newInternalPID,
'patient',
null,
null,
null,
'Patient registration',
[
'diff' => $auditDiff,
'patient_id' => $input['PatientID'] ?? null,
'validation_profile' => 'patient.create',
],
['entity_version' => 1]
);
$auditDiff = $this->buildAuditDiff([], $input);
AuditService::logData(
'PATIENT_REGISTERED',
'CREATE',
'patient',
(string) $newInternalPID,
'patient',
null,
null,
null,
'Patient registration',
[
'diff' => $auditDiff,
'patient_id' => $input['PatientID'] ?? null,
'validation_profile' => 'patient.create',
],
['entity_version' => 1]
);
if (!empty($patIdt)) {
$modelPatIdt->createPatIdt($patIdt, $newInternalPID);
@ -195,7 +205,7 @@ class PatientModel extends BaseModel {
}
}
public function updatePatient($input) {
public function updatePatient($input) {
$db = \Config\Database::connect();
$modelPatIdt = new PatIdtModel();
$modelPatCom = new PatComModel();
@ -219,30 +229,30 @@ class PatientModel extends BaseModel {
$db->transBegin();
try {
$InternalPID = $input['InternalPID'];
$previousData = $this->find($InternalPID) ?? [];
$this->where('InternalPID',$InternalPID)->set($input)->update();
$this->checkDbError($db, 'Update patient');
$changedFields = array_keys(array_diff_assoc((array) $previousData, (array) $input));
$auditDiff = $this->buildAuditDiff((array) $previousData, $input);
AuditService::logData(
'PATIENT_DEMOGRAPHICS_UPDATED',
'UPDATE',
'patient',
(string) $InternalPID,
'patient',
null,
null,
null,
'Patient data updated',
[
'diff' => $auditDiff,
'changed_fields' => $changedFields,
'validation_profile' => 'patient.update',
],
['entity_version' => 1]
);
$InternalPID = $input['InternalPID'];
$previousData = $this->find($InternalPID) ?? [];
$this->where('InternalPID',$InternalPID)->set($input)->update();
$this->checkDbError($db, 'Update patient');
$changedFields = array_keys(array_diff_assoc((array) $previousData, (array) $input));
$auditDiff = $this->buildAuditDiff((array) $previousData, $input);
AuditService::logData(
'PATIENT_DEMOGRAPHICS_UPDATED',
'UPDATE',
'patient',
(string) $InternalPID,
'patient',
null,
null,
null,
'Patient data updated',
[
'diff' => $auditDiff,
'changed_fields' => $changedFields,
'validation_profile' => 'patient.update',
],
['entity_version' => 1]
);
if (!empty($input['PatIdt'])) {
$modelPatIdt->updatePatIdt($input['PatIdt'], $InternalPID);
@ -348,170 +358,170 @@ class PatientModel extends BaseModel {
}
}
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');
}
public function updatePatientPartial(int $InternalPID, array $input): ?int {
$db = \Config\Database::connect();
$modelPatIdt = new PatIdtModel();
$modelPatCom = new PatComModel();
$modelPatAtt = new PatAttModel();
$patient = $this->find($InternalPID);
if (!$patient) {
return null;
}
$hasPatIdt = array_key_exists('PatIdt', $input);
$hasPatCom = array_key_exists('PatCom', $input);
$hasPatAtt = array_key_exists('PatAtt', $input);
$patIdt = $hasPatIdt ? $input['PatIdt'] : null;
$patCom = $hasPatCom ? $input['PatCom'] : null;
$patAtt = $hasPatAtt ? $input['PatAtt'] : null;
unset($input['PatIdt'], $input['PatCom'], $input['PatAtt']);
if (array_key_exists('Custodian', $input)) {
if (is_array($input['Custodian'])) {
$input['Custodian'] = $input['Custodian']['InternalPID'] ?? null;
}
if ($input['Custodian'] !== null && $input['Custodian'] !== '') {
$input['Custodian'] = (int) $input['Custodian'];
} else {
$input['Custodian'] = null;
}
}
if (array_key_exists('LinkTo', $input)) {
if (is_array($input['LinkTo'])) {
$internalPids = array_column($input['LinkTo'], 'InternalPID');
$internalPids = array_filter($internalPids, static fn($pid) => $pid !== null && $pid !== '');
$input['LinkTo'] = empty($internalPids) ? null : implode(',', $internalPids);
}
}
$allowedMap = array_flip($this->allowedFields);
$patientUpdate = array_intersect_key($input, $allowedMap);
unset($patientUpdate['CreateDate'], $patientUpdate['DelDate']);
$db->transBegin();
try {
$previousData = $this->find($InternalPID) ?? [];
if ($patientUpdate !== []) {
$this->where('InternalPID', $InternalPID)->set($patientUpdate)->update();
$this->checkDbError($db, 'Update patient');
}
if ($hasPatIdt) {
if ($patIdt === null || $patIdt === []) {
$modelPatIdt->deletePatIdt((string) $InternalPID);
$this->checkDbError($db, 'Delete patidt');
} else {
$modelPatIdt->updatePatIdt($patIdt, (string) $InternalPID);
$this->checkDbError($db, 'Update patidt');
}
}
if ($hasPatCom) {
if ($patCom === null) {
$modelPatCom->deletePatCom((string) $InternalPID);
$this->checkDbError($db, 'Delete patcom');
} else {
$modelPatCom->updatePatCom((string) $patCom, (string) $InternalPID);
$this->checkDbError($db, 'Update PatCom');
}
}
if ($hasPatAtt) {
if ($patAtt === null) {
$modelPatAtt->deletePatAtt((string) $InternalPID);
$this->checkDbError($db, 'Delete patatt');
} else {
$modelPatAtt->updatePatAtt((array) $patAtt, (string) $InternalPID);
$this->checkDbError($db, 'Update/Delete patatt');
}
}
$afterData = array_merge((array) $previousData, $patientUpdate);
$auditDiff = $this->buildAuditDiff((array) $previousData, $afterData);
$changedFields = array_column($auditDiff, 'field');
if ($hasPatIdt) {
$changedFields[] = 'PatIdt';
}
if ($hasPatCom) {
$changedFields[] = 'PatCom';
}
if ($hasPatAtt) {
$changedFields[] = 'PatAtt';
}
AuditService::logData(
'PATIENT_DEMOGRAPHICS_UPDATED',
'UPDATE',
'patient',
(string) $InternalPID,
'patient',
null,
null,
null,
'Patient data updated',
[
'diff' => $auditDiff,
'changed_fields' => array_values(array_unique($changedFields)),
'validation_profile' => 'patient.patch',
],
['entity_version' => 1]
);
$db->transCommit();
return $InternalPID;
} catch (\Exception $e) {
$db->transRollback();
throw $e;
}
}
private function buildAuditDiff(array $before, array $after): array {
$diff = [];
$fields = array_unique(array_merge(array_keys($before), array_keys($after)));
foreach ($fields as $field) {
$prev = $before[$field] ?? null;
$next = $after[$field] ?? null;
if ($prev === $next) {
continue;
}
$diff[] = [
'field' => $field,
'previous' => $prev,
'new' => $next,
];
}
return $diff;
}
private function checkDbError($db, string $context) {
$error = $db->error();
if (!empty($error['code'])) {
throw new \Exception(
"{$context} failed: {$error['code']} - {$error['message']}"
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');
}
public function updatePatientPartial(int $InternalPID, array $input): ?int {
$db = \Config\Database::connect();
$modelPatIdt = new PatIdtModel();
$modelPatCom = new PatComModel();
$modelPatAtt = new PatAttModel();
$patient = $this->find($InternalPID);
if (!$patient) {
return null;
}
$hasPatIdt = array_key_exists('PatIdt', $input);
$hasPatCom = array_key_exists('PatCom', $input);
$hasPatAtt = array_key_exists('PatAtt', $input);
$patIdt = $hasPatIdt ? $input['PatIdt'] : null;
$patCom = $hasPatCom ? $input['PatCom'] : null;
$patAtt = $hasPatAtt ? $input['PatAtt'] : null;
unset($input['PatIdt'], $input['PatCom'], $input['PatAtt']);
if (array_key_exists('Custodian', $input)) {
if (is_array($input['Custodian'])) {
$input['Custodian'] = $input['Custodian']['InternalPID'] ?? null;
}
if ($input['Custodian'] !== null && $input['Custodian'] !== '') {
$input['Custodian'] = (int) $input['Custodian'];
} else {
$input['Custodian'] = null;
}
}
if (array_key_exists('LinkTo', $input)) {
if (is_array($input['LinkTo'])) {
$internalPids = array_column($input['LinkTo'], 'InternalPID');
$internalPids = array_filter($internalPids, static fn($pid) => $pid !== null && $pid !== '');
$input['LinkTo'] = empty($internalPids) ? null : implode(',', $internalPids);
}
}
$allowedMap = array_flip($this->allowedFields);
$patientUpdate = array_intersect_key($input, $allowedMap);
unset($patientUpdate['CreateDate'], $patientUpdate['DelDate']);
$db->transBegin();
try {
$previousData = $this->find($InternalPID) ?? [];
if ($patientUpdate !== []) {
$this->where('InternalPID', $InternalPID)->set($patientUpdate)->update();
$this->checkDbError($db, 'Update patient');
}
if ($hasPatIdt) {
if ($patIdt === null || $patIdt === []) {
$modelPatIdt->deletePatIdt((string) $InternalPID);
$this->checkDbError($db, 'Delete patidt');
} else {
$modelPatIdt->updatePatIdt($patIdt, (string) $InternalPID);
$this->checkDbError($db, 'Update patidt');
}
}
if ($hasPatCom) {
if ($patCom === null) {
$modelPatCom->deletePatCom((string) $InternalPID);
$this->checkDbError($db, 'Delete patcom');
} else {
$modelPatCom->updatePatCom((string) $patCom, (string) $InternalPID);
$this->checkDbError($db, 'Update PatCom');
}
}
if ($hasPatAtt) {
if ($patAtt === null) {
$modelPatAtt->deletePatAtt((string) $InternalPID);
$this->checkDbError($db, 'Delete patatt');
} else {
$modelPatAtt->updatePatAtt((array) $patAtt, (string) $InternalPID);
$this->checkDbError($db, 'Update/Delete patatt');
}
}
$afterData = array_merge((array) $previousData, $patientUpdate);
$auditDiff = $this->buildAuditDiff((array) $previousData, $afterData);
$changedFields = array_column($auditDiff, 'field');
if ($hasPatIdt) {
$changedFields[] = 'PatIdt';
}
if ($hasPatCom) {
$changedFields[] = 'PatCom';
}
if ($hasPatAtt) {
$changedFields[] = 'PatAtt';
}
AuditService::logData(
'PATIENT_DEMOGRAPHICS_UPDATED',
'UPDATE',
'patient',
(string) $InternalPID,
'patient',
null,
null,
null,
'Patient data updated',
[
'diff' => $auditDiff,
'changed_fields' => array_values(array_unique($changedFields)),
'validation_profile' => 'patient.patch',
],
['entity_version' => 1]
);
$db->transCommit();
return $InternalPID;
} catch (\Exception $e) {
$db->transRollback();
throw $e;
}
}
private function buildAuditDiff(array $before, array $after): array {
$diff = [];
$fields = array_unique(array_merge(array_keys($before), array_keys($after)));
foreach ($fields as $field) {
$prev = $before[$field] ?? null;
$next = $after[$field] ?? null;
if ($prev === $next) {
continue;
}
$diff[] = [
'field' => $field,
'previous' => $prev,
'new' => $next,
];
}
return $diff;
}
private function checkDbError($db, string $context) {
$error = $db->error();
if (!empty($error['code'])) {
throw new \Exception(
"{$context} failed: {$error['code']} - {$error['message']}"
);
}
}
@ -529,23 +539,23 @@ class PatientModel extends BaseModel {
$this->delete($InternalPID);
$this->checkDbError($db, 'Delete patient');
$auditDiff = $this->buildAuditDiff((array) $previousData, []);
AuditService::logData(
'PATIENT_DELETED',
'DELETE',
'patient',
(string) $InternalPID,
'patient',
null,
$previousData,
null,
'Patient deleted',
[
'diff' => $auditDiff,
'patient_id' => $previousData['PatientID'] ?? null,
],
['entity_version' => 1]
);
$auditDiff = $this->buildAuditDiff((array) $previousData, []);
AuditService::logData(
'PATIENT_DELETED',
'DELETE',
'patient',
(string) $InternalPID,
'patient',
null,
$previousData,
null,
'Patient deleted',
[
'diff' => $auditDiff,
'patient_id' => $previousData['PatientID'] ?? null,
],
['entity_version' => 1]
);
$db->transCommit();
return $InternalPID;