Enhance department goal criteria handling

- Refactored department daily targets logic for optimization and better maintainability.
- Ensured department-specific targets are validated, sanitized, and consistently applied in updates.
- Improved database interaction safety by preventing unnecessary updates for unchanged object properties.
- Adjusted route and API behavior for criteria and department updates to handle validations comprehensively.
This commit is contained in:
Jeppe Bundgaard
2026-02-25 14:15:19 +01:00
parent ffc5d6bc21
commit 11a677416b
6 changed files with 65 additions and 88 deletions
@@ -81,7 +81,7 @@ class goals_criteria implements goals_criteria_i
* Optional per-department custom daily targets (department_id => daily_target)
* @var array<int,int>
*/
public array $department_daily_targets = [];
public array $department_daily_targets;
/**
* Constructor
*/
@@ -96,7 +96,6 @@ class goals_criteria implements goals_criteria_i
$this->progress_alert_destination = goals_criteria_progress_alert_destination::NONE;
$this->progress_alert_progress_type = goals_criteria_progress_alert_progress_type::NONE;
$this->progress_alert_style = goals_criteria_progress_alert_style::NONE;
}
/**
@@ -255,72 +254,23 @@ class goals_criteria implements goals_criteria_i
// Parse progress alert time of day (string with timezone)
if (isset($data['progress_alert_time_of_day']) && is_string($data['progress_alert_time_of_day'])) {
$criteria->progress_alert_time_of_day = trim((string)$data['progress_alert_time_of_day']);
$criteria->progress_alert_time_of_day = trim($data['progress_alert_time_of_day']);
} elseif (isset($data['progressAlertTimeOfDay']) && is_string($data['progressAlertTimeOfDay'])) {
$criteria->progress_alert_time_of_day = trim((string)$data['progressAlertTimeOfDay']);
$criteria->progress_alert_time_of_day = trim($data['progressAlertTimeOfDay']);
}
// Parse department_daily_targets (assoc map or array of objects)
if (isset($data['department_daily_targets'])) {
$map = [];
if (is_array($data['department_daily_targets'])) {
$src = $data['department_daily_targets'];
// Case A: assoc map { "12": 3, "15": 5 }
$assocLike = array_keys($src) !== range(0, count($src) - 1);
if ($assocLike) {
foreach ($src as $k => $v) {
if (is_numeric($k) && is_numeric($v)) {
$dk = (int)$k; $dv = (int)$v;
if ($dk > 0 && $dv >= 0) { $map[$dk] = $dv; }
}
}
} else {
// Case B: list of objects [{id:12, daily_target:3}] or [{department_id:12, daily_target:3}]
foreach ($src as $item) {
if (!is_array($item) && !is_object($item)) { continue; }
$arr = (array)$item;
$dk = null;
if (isset($arr['id']) && is_numeric($arr['id'])) { $dk = (int)$arr['id']; }
elseif (isset($arr['department_id']) && is_numeric($arr['department_id'])) { $dk = (int)$arr['department_id']; }
$dv = null;
if (isset($arr['daily_target']) && is_numeric($arr['daily_target'])) { $dv = (int)$arr['daily_target']; }
elseif (isset($arr['dailyTarget']) && is_numeric($arr['dailyTarget'])) { $dv = (int)$arr['dailyTarget']; }
if ($dk !== null && $dk > 0 && $dv !== null && $dv >= 0) {
$map[$dk] = $dv;
}
}
// Parse department daily targets (expects object with department_id => target)
if (isset($data['department_daily_targets']) && is_array($data['department_daily_targets'])) {
$targets = [];
foreach ($data['department_daily_targets'] as $deptId => $target) {
if (is_numeric($deptId) && is_numeric($target)) {
$id = (int)$deptId;
$t = max(0, (int)$target);
$targets[$id] = $t;
}
}
$criteria->department_daily_targets = $map;
} elseif (isset($data['departmentDailyTargets']) && is_array($data['departmentDailyTargets'])) {
$map = [];
$src = $data['departmentDailyTargets'];
$assocLike = array_keys($src) !== range(0, count($src) - 1);
if ($assocLike) {
foreach ($src as $k => $v) {
if (is_numeric($k) && is_numeric($v)) {
$dk = (int)$k; $dv = (int)$v;
if ($dk > 0 && $dv >= 0) { $map[$dk] = $dv; }
}
}
} else {
foreach ($src as $item) {
if (!is_array($item) && !is_object($item)) { continue; }
$arr = (array)$item;
$dk = null;
if (isset($arr['id']) && is_numeric($arr['id'])) { $dk = (int)$arr['id']; }
elseif (isset($arr['department_id']) && is_numeric($arr['department_id'])) { $dk = (int)$arr['department_id']; }
$dv = null;
if (isset($arr['daily_target']) && is_numeric($arr['daily_target'])) { $dv = (int)$arr['daily_target']; }
elseif (isset($arr['dailyTarget']) && is_numeric($arr['dailyTarget'])) { $dv = (int)$arr['dailyTarget']; }
if ($dk !== null && $dk > 0 && $dv !== null && $dv >= 0) {
$map[$dk] = $dv;
}
}
}
$criteria->department_daily_targets = $map;
$criteria->department_daily_targets = $targets;
}
// Parse timeframe with validation
if (isset($data['start']) && is_string($data['start'])) {
try {
@@ -478,20 +428,7 @@ class goals_criteria implements goals_criteria_i
'progress_alert_format' => $this->progress_alert_format,
'progress_alert_weekdays' => array_map(fn($e) => ($e instanceof goals_criteria_progress_alert_weekday) ? $e->name : (string)$e, $this->progress_alert_weekdays ?? []),
'progress_alert_time_of_day' => $this->progress_alert_time_of_day,
'department_daily_targets' => (function(){
$map = [];
if (is_array($this->department_daily_targets)) {
// Only include targets for departments present in criteria
$deptIds = $this->departments?->listIDs() ?? [];
foreach ($this->department_daily_targets as $k => $v) {
$dk = (int)$k; $dv = (int)$v;
if (in_array($dk, $deptIds, true) && $dk > 0 && $dv >= 0) {
$map[$dk] = $dv;
}
}
}
return $map;
})(),
'department_daily_targets' => $this->department_daily_targets
];
}
@@ -589,13 +526,12 @@ class goals_criteria implements goals_criteria_i
public function validateAndSanitize(): void
{
// Normalize custom daily targets map: ints and non-negative; filter to selected departments
if (!is_array($this->department_daily_targets)) { $this->department_daily_targets = []; }
$normalized = [];
$deptIds = $this->departments?->listIDs() ?? [];
foreach ($this->department_daily_targets as $k => $v) {
if (!is_numeric($k) || !is_numeric($v)) { continue; }
$dk = (int)$k; $dv = (int)$v;
if ($dk > 0 && $dv >= 0 && in_array($dk, $deptIds, true)) {
if ($dk > 0 && $dv >= 0 && in_array($dk, $deptIds)) {
$normalized[$dk] = $dv;
}
}
@@ -708,6 +644,8 @@ class goals_criteria implements goals_criteria_i
$this->progress_alert_time_of_day = null;
}
}
$this->department_daily_targets = array_filter($this->department_daily_targets, fn($v) => is_numeric($v) && $v >= 0);
}
/**
@@ -34,7 +34,7 @@ trait goals_criteria_departments_t
public function listIDs(): array
{
return array_map(function ($department) {
return $department->id;
return (int)$department->id;
}, $this->list);
}
@@ -41,6 +41,8 @@ class department_goals_o extends db
foreach ($departments as $dept) {
$dept->requireSelected();
}
// Set the departments on the criteria object so validation and output inclusion works correctly
$criteria->departments->set($departments);
// Validate and sanitize criteria object
$criteria->validateAndSanitize();
// Add the object
@@ -188,8 +188,23 @@ class departmentGoalsRoute
if ($response->isRequestParameterSet('criteria')) {
$criteriaInput = self::getParameter('criteria');
$this->requireTypeIn($criteriaInput, ['array', 'object']);
//var_dump($criteriaInput);
// Preserve unicode characters when (re)encoding criteria
$criteria = goals_criteria::fromJson(json_encode($criteriaInput, JSON_UNESCAPED_UNICODE));
// Ensure the criteria object knows its departments (from input or existing object)
// so toArray() preserves department-specific daily targets.
$deptIds = isset($dataToUpdate['departments']) ? $dataToUpdate['departments'] : (array)$goal->departments->value();
$deptObjs = [];
foreach ($deptIds as $dId) {
$d = (new departments_o())->select((int)$dId);
if ($d->exists()) { $deptObjs[] = $d; }
}
$criteria->departments->set($deptObjs);
// Add empty daily_department_targets for any departments that have none but are in the criteria, to avoid validation errors and ensure they are preserved in output
$criteria->validateAndSanitize();
$dataToUpdate['criteria'] = $criteria->toArray();
}
@@ -197,7 +212,8 @@ class departmentGoalsRoute
$response->error('No updatable fields provided. Allowed: departments, criteria', 400);
}
$goal->update($dataToUpdate);
$goal->criteria->update($dataToUpdate['criteria'] ?? $goal->criteria->value());
$goal->departments->update($dataToUpdate['departments'] ?? $goal->departments->value());
$response->success($goal->asArray());
}, [
+5 -5
View File
@@ -30,7 +30,7 @@ class exampleRoute
// Check if the time is between 07:00 and 17:00
if (!redis->exists('system:last_sent_monday_message') && (date('H') >= 7 && date('H') < 17)) {
// Set the key to expire in 24 hours
redis->set('system:last_sent_monday_message', date('Y-m-d H:i:s'));
redis->set('system:last_sent_monday_message', (string)time());
redis->expire('system:last_sent_monday_message', 86400);
// Send the messages
$departments = [
@@ -50,7 +50,7 @@ class exampleRoute
$date_start = date('Y-m-d 00:00:00', strtotime("-$days days")); // $days ago at 00:00:00
$department->sendPeriodStatisticsToSlack($date_start, $date_end); // Sends message to "daglig-ledelse".
}
$department->sendSlackInternalStatisticNotification($date_start, $date_end, $departments);
//$department->sendSlackInternalStatisticNotification($date_start, $date_end, $departments);
}
}
$response->success(['message' => 'Hello World!']);
@@ -59,7 +59,7 @@ class exampleRoute
$this->get('/tmp-send-email', function () {
global $response;
$response->error(['message' => 'This route is deprecated.']);
$emailAddress = "2jepp9350@gmail.com";
$emailAddress = "my@truckwash.dk";
$customer_name = "John Doe";
/**
* Tillykke med din nye kredit konto!
@@ -74,7 +74,7 @@ class exampleRoute
* CVR: 40650717
*/
$email = new email();
$email->sendWelcomeEmailToCustomer($customer_number = 12345679, $emailAddress);
$email->sendWelcomeEmailToCustomer($customer_number = 43632122, $emailAddress);
$response->success(['message' => 'This route is deprecated.']);
});
@@ -189,7 +189,7 @@ class exampleRoute
$this->get('/debug', function () {
global $response;
$response->success(['message' => 'Debugging route!']);
//$response->success(['message' => 'Debugging route!']);
$machine_1 = new machine_1();
//$machine_1->debugAssets();
$machine_1->current_step = 0;
+24 -3
View File
@@ -6,6 +6,7 @@ use attachments\helpers\attachment;
use attachments\helpers\attachment_content;
use classes\attachments;
use classes\db;
use classes\object_property;
use classes\response;
use Exception;
use mysqli_result;
@@ -1162,9 +1163,29 @@ trait db_object_t
$value = $db->escape_string($value);
$set[] = "$key = '$value'";
}
$set = implode(', ', $set);
$sql = "UPDATE $this->table SET $set WHERE id = $this->id";
$db->query($sql);
// Loop through all the keys, and find the property for each key, validate that it's class is object_property, and if the value is the same as the current value of the property, remove it from the set array to prevent unnecessary updates and triggering of the object changed event
foreach ( $data as $key => $value ) {
/**
* @var object_property $property
*/
if (property_exists($this, $key) && $this->$key instanceof object_property) {
$currentValue = $this->$key->value();
// If the value is the same as the current value of the property, remove it from the set array to prevent unnecessary updates and triggering of the object changed event
if ($currentValue == $value) { // Remove the key from the set array
foreach ( $set as $index => $setValue ) {
if (str_starts_with($setValue, "$key =")) {
unset($set[$index]);
break;
}
}
}
// Update the value of the property
$this->$key->set($value);
}
}
if (empty($set)) {
return;
}
// Trigger the object changed event
self::objectChanged();
}