From 1033e58ae5f09f480ba2547b82218062498cb1c4 Mon Sep 17 00:00:00 2001 From: Vahid Bazzaz Date: Thu, 20 Nov 2025 17:25:58 +0330 Subject: [PATCH 1/7] Add history log for beneficiaries merged to family via action button --- include/people.php | 1 + 1 file changed, 1 insertion(+) diff --git a/include/people.php b/include/people.php index 020b267bc..03af4f932 100644 --- a/include/people.php +++ b/include/people.php @@ -439,6 +439,7 @@ function () use ($cmsmain, $data) { } } }); + simpleBulkSaveChangeHistory('people', $ids, 'Merged to family (head: '.$oldest.')'); $success = true; $message = 'The merge has be successfully applied'; $redirect = true; From 45b28b1e154468708e5c8ee72bfb2336fb6f146a Mon Sep 17 00:00:00 2001 From: Vahid Bazzaz Date: Thu, 20 Nov 2025 17:26:46 +0330 Subject: [PATCH 2/7] Add history log for beneficiaries detached from family via action button --- include/people.php | 1 + 1 file changed, 1 insertion(+) diff --git a/include/people.php b/include/people.php index 03af4f932..afc48afa5 100644 --- a/include/people.php +++ b/include/people.php @@ -465,6 +465,7 @@ function () use ($cmsmain, $data) { db_query('UPDATE people SET parent_id = NULL WHERE id = :id', ['id' => $id]); } }); + simpleBulkSaveChangeHistory('people', $ids, 'Detached from family'); $redirect = true; $success = true; $message = ($success) ? 'Selected people have been detached' : 'Something went wrong'; From c79f24d37583743583afc0befca05b0bd58dffe0 Mon Sep 17 00:00:00 2001 From: Vahid Bazzaz Date: Thu, 20 Nov 2025 17:30:12 +0330 Subject: [PATCH 3/7] Add history log for beneficiary added to family via drag & drop --- include/people.php | 25 ++++++++++++++++++++++++- library/lib/list.php | 7 +++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/include/people.php b/include/people.php index afc48afa5..d3ed5b7d9 100644 --- a/include/people.php +++ b/include/people.php @@ -484,7 +484,30 @@ function () use ($cmsmain, $data) { $ids = json_decode((string) $_POST['ids']); // list($success, $message, $redirect, $aftermove) = listMove($table, $ids, true, 'correctdrops'); // Refactored list move method to use a transaction block and bulk insert for the correctdrops method - [$success, $message, $redirect, $aftermove] = listBulkMove($table, $ids, true, 'bulkcorrectdrops', true); + [$success, $message, $redirect, $aftermove, $parentChanges] = listBulkMove($table, $ids, true, 'bulkcorrectdrops', true); + + // Log history for drag & drop family operations + if (!empty($parentChanges)) { + $addedToFamily = []; + $removedFromFamily = []; + + foreach ($parentChanges as $change) { + if (is_null($change['old_parent_id']) && !is_null($change['new_parent_id'])) { + // Added to family + $addedToFamily[] = $change['id']; + } elseif (!is_null($change['old_parent_id']) && is_null($change['new_parent_id'])) { + // Removed from family + $removedFromFamily[] = $change['id']; + } + } + + if (!empty($addedToFamily)) { + simpleBulkSaveChangeHistory('people', $addedToFamily, 'Added to family via drag & drop'); + } + if (!empty($removedFromFamily)) { + simpleBulkSaveChangeHistory('people', $removedFromFamily, 'Removed from family via drag & drop'); + } + } break; diff --git a/library/lib/list.php b/library/lib/list.php index 1bb50ebbd..90a6df577 100644 --- a/library/lib/list.php +++ b/library/lib/list.php @@ -55,7 +55,8 @@ function listBulkMove($table, $ids, $regardparent = true, $hook = '', $updatetra $i = 1; $return = ''; - $hookIds = db_transaction(function () use ($ids, $hasParent, $table, $hook, $i, $updatetransactions) { + $parentChanges = []; + $hookIds = db_transaction(function () use ($ids, $hasParent, $table, $hook, $i, $updatetransactions, &$parentChanges) { $hookIds = []; $seq = []; foreach ($ids as $line) { @@ -72,6 +73,8 @@ function listBulkMove($table, $ids, $regardparent = true, $hook = '', $updatetra if ($updatetransactions && null != $new_parent_id) { db_query('UPDATE transactions SET people_id = :parent_id WHERE people_id = :id', ['parent_id' => $new_parent_id, 'id' => $id]); } + // Track parent_id changes for history logging + $parentChanges[] = ['id' => $id, 'old_parent_id' => $old_parent_id, 'new_parent_id' => $new_parent_id]; } } @@ -88,7 +91,7 @@ function listBulkMove($table, $ids, $regardparent = true, $hook = '', $updatetra $aftermove = $hook($hookIds); } - return [true, $return, false, $aftermove]; + return [true, $return, false, $aftermove, $parentChanges]; } function listRealDelete($table, $ids, $uri = false) From 03aa43cc4bd4a8718a0ef3309ff450bbfb2f5374 Mon Sep 17 00:00:00 2001 From: Vahid Bazzaz Date: Thu, 20 Nov 2025 17:30:39 +0330 Subject: [PATCH 4/7] Add history log for beneficiary removed from family via drag & drop From e6f068a7e6cd334c76f94dac06b4dcf88f3bd3fe Mon Sep 17 00:00:00 2001 From: Vahid Bazzaz Date: Thu, 20 Nov 2025 18:42:28 +0330 Subject: [PATCH 5/7] Add Cypress tests to verify family operation history logging --- .../5_3_Manage_Beneficiaries.js | 16 ++++++++- cypress/support/database.js | 26 ++++++++++++++ include/people.php | 8 ++--- library/ajax/testhistorycheck.php | 36 +++++++++++++++++++ 4 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 library/ajax/testhistorycheck.php diff --git a/cypress/e2e/1_feature_tests/5_3_Manage_Beneficiaries.js b/cypress/e2e/1_feature_tests/5_3_Manage_Beneficiaries.js index 3b296964b..cc0cbe41f 100644 --- a/cypress/e2e/1_feature_tests/5_3_Manage_Beneficiaries.js +++ b/cypress/e2e/1_feature_tests/5_3_Manage_Beneficiaries.js @@ -253,7 +253,15 @@ describe('Manage beneficiaries', () => { clickMergeButton(); verifyBeneficiaryRowLevel(TEST_LASTNAME1,0); verifyBeneficiaryRowLevel(TEST_LASTNAME2,1); - + + // Verify history logs for merge operation + cy.getBeneficiaryIdFromRow(TEST_LASTNAME1).then(id1 => { + cy.checkHistoryLog('people', id1, 'merged to family'); + }); + cy.getBeneficiaryIdFromRow(TEST_LASTNAME2).then(id2 => { + cy.checkHistoryLog('people', id2, 'merged to family'); + }); + //cleanup fullDeleteOfMergedUsers(); }); @@ -266,6 +274,12 @@ describe('Manage beneficiaries', () => { clickDetachButton(); verifyBeneficiaryRowLevel(TEST_LASTNAME1,0); verifyBeneficiaryRowLevel(TEST_LASTNAME2,0); + + // Verify history log for detach operation + cy.getBeneficiaryIdFromRow(TEST_LASTNAME2).then(id2 => { + cy.checkHistoryLog('people', id2, 'detached from family'); + }); + //cleanup fullDeleteTestedBeneficiaries([TEST_FIRSTNAME1,TEST_FIRSTNAME2]); }); diff --git a/cypress/support/database.js b/cypress/support/database.js index 5dd5b1b4b..9ce68c686 100644 --- a/cypress/support/database.js +++ b/cypress/support/database.js @@ -27,3 +27,29 @@ Cypress.Commands.add("testauth0user", (email) => { expect(response.body).to.contain("true"); }); }); + +Cypress.Commands.add("checkHistoryLog", (tablename, recordId, expectedChange) => { + cy.request({ + method: "POST", + url: "/ajax.php?file=testhistorycheck", + body: { + tablename: tablename, + record_id: recordId, + expected_change: expectedChange + }, + form: true + }).then(response => { + expect(response.status).to.eq(200); + const body = typeof response.body === 'string' ? JSON.parse(response.body) : response.body; + expect(body.found).to.eq(true); + expect(body.count).to.be.greaterThan(0); + }); +}); + +Cypress.Commands.add("getBeneficiaryIdFromRow", (lastname) => { + return cy.getRowWithText(lastname).then($row => { + const id = $row.closest('tr').attr('data-id'); + expect(id).to.not.be.undefined; + return parseInt(id); + }); +}); diff --git a/include/people.php b/include/people.php index d3ed5b7d9..0f0c83208 100644 --- a/include/people.php +++ b/include/people.php @@ -439,7 +439,7 @@ function () use ($cmsmain, $data) { } } }); - simpleBulkSaveChangeHistory('people', $ids, 'Merged to family (head: '.$oldest.')'); + simpleBulkSaveChangeHistory('people', $ids, 'merged to family (head: '.$oldest.')'); $success = true; $message = 'The merge has be successfully applied'; $redirect = true; @@ -465,7 +465,7 @@ function () use ($cmsmain, $data) { db_query('UPDATE people SET parent_id = NULL WHERE id = :id', ['id' => $id]); } }); - simpleBulkSaveChangeHistory('people', $ids, 'Detached from family'); + simpleBulkSaveChangeHistory('people', $ids, 'detached from family'); $redirect = true; $success = true; $message = ($success) ? 'Selected people have been detached' : 'Something went wrong'; @@ -502,10 +502,10 @@ function () use ($cmsmain, $data) { } if (!empty($addedToFamily)) { - simpleBulkSaveChangeHistory('people', $addedToFamily, 'Added to family via drag & drop'); + simpleBulkSaveChangeHistory('people', $addedToFamily, 'added to family via drag & drop'); } if (!empty($removedFromFamily)) { - simpleBulkSaveChangeHistory('people', $removedFromFamily, 'Removed from family via drag & drop'); + simpleBulkSaveChangeHistory('people', $removedFromFamily, 'removed from family via drag & drop'); } } diff --git a/library/ajax/testhistorycheck.php b/library/ajax/testhistorycheck.php new file mode 100644 index 000000000..8dbc6f285 --- /dev/null +++ b/library/ajax/testhistorycheck.php @@ -0,0 +1,36 @@ + 'No permission']); +} else { + $recordId = $_POST['record_id']; + $tablename = $_POST['tablename']; + $expectedChange = $_POST['expected_change']; + + // Query history table for matching record + $historyEntries = db_array( + 'SELECT * FROM history WHERE tablename = :tablename AND record_id = :record_id AND changes LIKE :changes ORDER BY changedate DESC', + [ + 'tablename' => $tablename, + 'record_id' => $recordId, + 'changes' => '%'.$expectedChange.'%', + ] + ); + + if (count($historyEntries) > 0) { + echo json_encode(['found' => true, 'count' => count($historyEntries), 'entries' => $historyEntries]); + } else { + echo json_encode(['found' => false, 'count' => 0]); + } +} From 671650c6a514ebfd57dca5cba51153c87ac03493 Mon Sep 17 00:00:00 2001 From: Vahid Bazzaz Date: Thu, 23 Jul 2026 20:20:27 +0400 Subject: [PATCH 6/7] Address PR review: log parent_id from/to and update modified timestamp - Move parent_id values out of the change message into the from/to columns of simpleBulkSaveChangeHistory, grouping beneficiaries by their old/new parent so multi-family batches log correctly - Prefix history messages with 'parent_id;' to name the changed column - Update modified/modified_by on affected records after history insert, guarded by db_fieldexists so it no-ops on tables without the column - Update Cypress assertions to expect the new message text --- .../5_3_Manage_Beneficiaries.js | 6 +-- include/people.php | 37 ++++++++++++------- library/lib/tools.php | 12 +++++- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/cypress/e2e/1_feature_tests/5_3_Manage_Beneficiaries.js b/cypress/e2e/1_feature_tests/5_3_Manage_Beneficiaries.js index cc0cbe41f..a76311dc3 100644 --- a/cypress/e2e/1_feature_tests/5_3_Manage_Beneficiaries.js +++ b/cypress/e2e/1_feature_tests/5_3_Manage_Beneficiaries.js @@ -256,10 +256,10 @@ describe('Manage beneficiaries', () => { // Verify history logs for merge operation cy.getBeneficiaryIdFromRow(TEST_LASTNAME1).then(id1 => { - cy.checkHistoryLog('people', id1, 'merged to family'); + cy.checkHistoryLog('people', id1, 'parent_id; merged to family'); }); cy.getBeneficiaryIdFromRow(TEST_LASTNAME2).then(id2 => { - cy.checkHistoryLog('people', id2, 'merged to family'); + cy.checkHistoryLog('people', id2, 'parent_id; merged to family'); }); //cleanup @@ -277,7 +277,7 @@ describe('Manage beneficiaries', () => { // Verify history log for detach operation cy.getBeneficiaryIdFromRow(TEST_LASTNAME2).then(id2 => { - cy.checkHistoryLog('people', id2, 'detached from family'); + cy.checkHistoryLog('people', id2, 'parent_id; detached from family'); }); //cleanup diff --git a/include/people.php b/include/people.php index 0f0c83208..871aee43b 100644 --- a/include/people.php +++ b/include/people.php @@ -439,7 +439,7 @@ function () use ($cmsmain, $data) { } } }); - simpleBulkSaveChangeHistory('people', $ids, 'merged to family (head: '.$oldest.')'); + simpleBulkSaveChangeHistory('people', $ids, 'parent_id; merged to family', [], ['int' => $oldest]); $success = true; $message = 'The merge has be successfully applied'; $redirect = true; @@ -450,9 +450,13 @@ function () use ($cmsmain, $data) { case 'detach': $ids = explode(',', (string) $_POST['ids']); + $parentIdsByBeneficiary = []; foreach ($ids as $key => $value) { - if (!db_value('SELECT parent_id FROM people WHERE id = :id', ['id' => $value])) { + $parentId = db_value('SELECT parent_id FROM people WHERE id = :id', ['id' => $value]); + if (!$parentId) { $containsmembers = true; + } else { + $parentIdsByBeneficiary[$value] = $parentId; } } if ($containsmembers) { @@ -465,7 +469,14 @@ function () use ($cmsmain, $data) { db_query('UPDATE people SET parent_id = NULL WHERE id = :id', ['id' => $id]); } }); - simpleBulkSaveChangeHistory('people', $ids, 'detached from family'); + // Group beneficiaries by their old parent_id for history logging + $beneficiariesByParent = []; + foreach ($parentIdsByBeneficiary as $beneficiaryId => $parentId) { + $beneficiariesByParent[$parentId][] = $beneficiaryId; + } + foreach ($beneficiariesByParent as $parentId => $beneficiaryIds) { + simpleBulkSaveChangeHistory('people', $beneficiaryIds, 'parent_id; detached from family', ['int' => $parentId], []); + } $redirect = true; $success = true; $message = ($success) ? 'Selected people have been detached' : 'Something went wrong'; @@ -488,24 +499,24 @@ function () use ($cmsmain, $data) { // Log history for drag & drop family operations if (!empty($parentChanges)) { - $addedToFamily = []; - $removedFromFamily = []; + $addedToFamilyByParent = []; + $removedFromFamilyByParent = []; foreach ($parentChanges as $change) { if (is_null($change['old_parent_id']) && !is_null($change['new_parent_id'])) { - // Added to family - $addedToFamily[] = $change['id']; + // Added to family - group by new parent + $addedToFamilyByParent[$change['new_parent_id']][] = $change['id']; } elseif (!is_null($change['old_parent_id']) && is_null($change['new_parent_id'])) { - // Removed from family - $removedFromFamily[] = $change['id']; + // Removed from family - group by old parent + $removedFromFamilyByParent[$change['old_parent_id']][] = $change['id']; } } - if (!empty($addedToFamily)) { - simpleBulkSaveChangeHistory('people', $addedToFamily, 'added to family via drag & drop'); + foreach ($addedToFamilyByParent as $parentId => $beneficiaryIds) { + simpleBulkSaveChangeHistory('people', $beneficiaryIds, 'parent_id; added to family via drag & drop', [], ['int' => $parentId]); } - if (!empty($removedFromFamily)) { - simpleBulkSaveChangeHistory('people', $removedFromFamily, 'removed from family via drag & drop'); + foreach ($removedFromFamilyByParent as $parentId => $beneficiaryIds) { + simpleBulkSaveChangeHistory('people', $beneficiaryIds, 'parent_id; removed from family via drag & drop', ['int' => $parentId], []); } } diff --git a/library/lib/tools.php b/library/lib/tools.php index 02ed012bd..7d9f53fe3 100644 --- a/library/lib/tools.php +++ b/library/lib/tools.php @@ -306,7 +306,7 @@ function simpleBulkSaveChangeHistory($table, $records, $changes, $from = [], $to if (is_iterable($records)) { for ($i = 0; $i < sizeof($records); ++$i) { $query .= "(:table{$i},:id{$i},:change{$i},:user_id{$i},:ip{$i},NOW(), :from_int{$i}, :from_float{$i}, :to_int{$i}, :to_float{$i})"; - $params = array_merge($params, ['table'.$i => $table, 'id'.$i => $records[$i], 'change'.$i => $changes, 'user_id'.$i => $_SESSION['user']['id'], 'ip'.$i => $_SERVER['REMOTE_ADDR'], 'from_int'.$i => $from['int'], 'from_float'.$i => $from['float'], 'to_int'.$i => $to['int'], 'to_float'.$i => $to['float']]); + $params = array_merge($params, ['table'.$i => $table, 'id'.$i => $records[$i], 'change'.$i => $changes, 'user_id'.$i => $_SESSION['user']['id'], 'ip'.$i => $_SERVER['REMOTE_ADDR'], 'from_int'.$i => $from['int'] ?? null, 'from_float'.$i => $from['float'] ?? null, 'to_int'.$i => $to['int'] ?? null, 'to_float'.$i => $to['float'] ?? null]); if ($i !== sizeof($records) - 1) { $query .= ','; } @@ -314,5 +314,15 @@ function simpleBulkSaveChangeHistory($table, $records, $changes, $from = [], $to } if (strlen($query) > 0) { db_query("INSERT INTO history (tablename, record_id, changes, user_id, ip, changedate, from_int, from_float, to_int, to_float) VALUES {$query}", $params); + + // Update modified timestamp for all affected records + if (db_fieldexists($table, 'modified')) { + $idPlaceholders = implode(',', array_map(fn ($i) => ":id{$i}", array_keys($records))); + $updateParams = ['user' => $_SESSION['user']['id']]; + foreach ($records as $i => $id) { + $updateParams["id{$i}"] = $id; + } + db_query("UPDATE {$table} SET modified = NOW(), modified_by = :user WHERE id IN ({$idPlaceholders})", $updateParams); + } } } From d5f5d3de45ed47d35064607809291661072386c6 Mon Sep 17 00:00:00 2001 From: Vahid Bazzaz Date: Sat, 25 Jul 2026 10:13:41 +0400 Subject: [PATCH 7/7] Fix merge history: don't log the family head as merged into itself The merge case passed all selected ids (including the oldest/head) to simpleBulkSaveChangeHistory, so the head got a 'parent_id; merged to family' entry with to_int pointing at its own id, even though its parent_id never changed. Filter out the head so only members whose parent_id actually changed are logged. Add a checkHistoryLogAbsent Cypress command and assert the head is not logged while the merged member is, locking in the fix. --- .../1_feature_tests/5_3_Manage_Beneficiaries.js | 6 ++++-- cypress/support/database.js | 17 +++++++++++++++++ include/people.php | 4 +++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/1_feature_tests/5_3_Manage_Beneficiaries.js b/cypress/e2e/1_feature_tests/5_3_Manage_Beneficiaries.js index a76311dc3..37b9f983f 100644 --- a/cypress/e2e/1_feature_tests/5_3_Manage_Beneficiaries.js +++ b/cypress/e2e/1_feature_tests/5_3_Manage_Beneficiaries.js @@ -254,10 +254,12 @@ describe('Manage beneficiaries', () => { verifyBeneficiaryRowLevel(TEST_LASTNAME1,0); verifyBeneficiaryRowLevel(TEST_LASTNAME2,1); - // Verify history logs for merge operation + // Verify history logs for merge operation. + // TEST_LASTNAME1 is the family head (level 0) - its parent_id does not change, so it must NOT be logged. cy.getBeneficiaryIdFromRow(TEST_LASTNAME1).then(id1 => { - cy.checkHistoryLog('people', id1, 'parent_id; merged to family'); + cy.checkHistoryLogAbsent('people', id1, 'parent_id; merged to family'); }); + // TEST_LASTNAME2 is a member (level 1) - it was merged into the family, so it must be logged. cy.getBeneficiaryIdFromRow(TEST_LASTNAME2).then(id2 => { cy.checkHistoryLog('people', id2, 'parent_id; merged to family'); }); diff --git a/cypress/support/database.js b/cypress/support/database.js index 9ce68c686..273fb10a9 100644 --- a/cypress/support/database.js +++ b/cypress/support/database.js @@ -46,6 +46,23 @@ Cypress.Commands.add("checkHistoryLog", (tablename, recordId, expectedChange) => }); }); +Cypress.Commands.add("checkHistoryLogAbsent", (tablename, recordId, expectedChange) => { + cy.request({ + method: "POST", + url: "/ajax.php?file=testhistorycheck", + body: { + tablename: tablename, + record_id: recordId, + expected_change: expectedChange + }, + form: true + }).then(response => { + expect(response.status).to.eq(200); + const body = typeof response.body === 'string' ? JSON.parse(response.body) : response.body; + expect(body.found).to.eq(false); + }); +}); + Cypress.Commands.add("getBeneficiaryIdFromRow", (lastname) => { return cy.getRowWithText(lastname).then($row => { const id = $row.closest('tr').attr('data-id'); diff --git a/include/people.php b/include/people.php index f0b5db62b..4328c0494 100644 --- a/include/people.php +++ b/include/people.php @@ -440,7 +440,9 @@ function () use ($cmsmain, $data) { } } }); - simpleBulkSaveChangeHistory('people', $ids, 'parent_id; merged to family', null, [], ['int' => $oldest]); + // Only log the members whose parent_id actually changed, not the family head itself + $mergedIds = array_values(array_filter($ids, fn ($id) => $id != $oldest)); + simpleBulkSaveChangeHistory('people', $mergedIds, 'parent_id; merged to family', null, [], ['int' => $oldest]); $success = true; $message = 'The merge has be successfully applied'; $redirect = true;