Add advanced target duration configurations and parsing logic to Goals module. Update Workfeed with new CompanyID config. Extend OpenAPI spec with detailed schema mappings and examples.

This commit is contained in:
Jeppe Bundgaard
2026-03-24 11:23:08 +01:00
parent 40171d9719
commit 8c9388546b
5 changed files with 256 additions and 7 deletions
@@ -3,6 +3,7 @@
namespace goals\classes;
use classes\db;
use goals\helpers\goals_criteria_target_duration;
use goals\helpers\goals_criteria_type;
use goals\helpers\goals_criteria_progress_alert_frequency;
use goals\helpers\goals_criteria_progress_alert_destination;
@@ -87,6 +88,18 @@ class goals_criteria implements goals_criteria_i
* @var array<int,float>
*/
public array $department_weekly_targets;
/**
* Optional advanced target duration mode.
* When null, legacy target behavior is preserved.
* @var goals_criteria_target_duration|null
*/
public ?goals_criteria_target_duration $target_duration = null;
/**
* Optional cadence amount for advanced target duration modes.
* Used for WEEKS, MONTHS, YEARS. Ignored for ENTIRE_DURATION.
* @var int|null
*/
public ?int $target_duration_every = null;
/**
* Constructor
*/
@@ -103,6 +116,8 @@ class goals_criteria implements goals_criteria_i
$this->progress_alert_style = goals_criteria_progress_alert_style::NONE;
$this->department_daily_targets = [];
$this->department_weekly_targets = [];
$this->target_duration = null;
$this->target_duration_every = null;
}
/**
@@ -134,6 +149,31 @@ class goals_criteria implements goals_criteria_i
}
}
// Parse advanced target duration mode (ignore unknowns)
$rawTargetDuration = null;
if (isset($data['target_duration']) && is_string($data['target_duration'])) {
$rawTargetDuration = $data['target_duration'];
} elseif (isset($data['targetDuration']) && is_string($data['targetDuration'])) {
$rawTargetDuration = $data['targetDuration'];
}
if (is_string($rawTargetDuration)) {
$duration = goals_criteria_target_duration::tryFrom($rawTargetDuration);
if ($duration !== null) {
$criteria->target_duration = $duration;
}
}
// Parse advanced target duration cadence value (>=1)
$rawTargetDurationEvery = null;
if (isset($data['target_duration_every'])) {
$rawTargetDurationEvery = $data['target_duration_every'];
} elseif (isset($data['targetDurationEvery'])) {
$rawTargetDurationEvery = $data['targetDurationEvery'];
}
if (is_numeric($rawTargetDurationEvery)) {
$criteria->target_duration_every = max(1, (int)$rawTargetDurationEvery);
}
// Sanitize label: trim, strip tags, collapse whitespace, max length 255
if (isset($data['label']) && is_string($data['label'])) {
$label = trim($data['label']);
@@ -277,6 +317,16 @@ class goals_criteria implements goals_criteria_i
}
}
$criteria->department_daily_targets = $targets;
} elseif (isset($data['departmentDailyTargets']) && is_array($data['departmentDailyTargets'])) {
$targets = [];
foreach ($data['departmentDailyTargets'] as $deptId => $target) {
if (is_numeric($deptId) && is_numeric($target)) {
$id = (int)$deptId;
$t = max(0.0, (float)$target);
$targets[$id] = $t;
}
}
$criteria->department_daily_targets = $targets;
}
// Parse department weekly targets (expects object with department_id => target)
if (isset($data['department_weekly_targets']) && is_array($data['department_weekly_targets'])) {
@@ -447,6 +497,8 @@ class goals_criteria implements goals_criteria_i
return [
'type' => $this->type?->name ?? goals_criteria_type::NONE->name,
'target' => $this->target ?? 0,
'target_duration' => $this->target_duration?->name,
'target_duration_every' => $this->target_duration_every,
'label' => (string)$this->label,
'start' => ($this->start instanceof \DateTimeInterface) ? $this->start->format(DATE_ATOM) : null,
'end' => ($this->end instanceof \DateTimeInterface) ? $this->end->format(DATE_ATOM) : null,
@@ -608,6 +660,11 @@ class goals_criteria implements goals_criteria_i
return $this->getProgress();
}
public function usesAdvancedTargetDuration(): bool
{
return $this->target_duration instanceof goals_criteria_target_duration;
}
public function validateAndSanitize(): void
{
// Normalize custom daily targets map: ints and non-negative; filter to selected departments
@@ -636,6 +693,20 @@ class goals_criteria implements goals_criteria_i
$this->target = 0;
}
// Normalize advanced target duration values
if ($this->target_duration !== null && !in_array($this->target_duration, goals_criteria_target_duration::cases(), true)) {
$this->target_duration = null;
}
if ($this->target_duration === goals_criteria_target_duration::ENTIRE_DURATION) {
$this->target_duration_every = null;
} elseif ($this->target_duration !== null) {
$every = (int)($this->target_duration_every ?? 1);
$this->target_duration_every = max(1, $every);
} else {
// Strict legacy compatibility branch marker: null duration means old behavior.
$this->target_duration_every = null;
}
// Ensure timeframe is valid
if (($this->start instanceof \DateTimeInterface) && ($this->end instanceof \DateTimeInterface)) {
if ($this->end < $this->start) {
@@ -751,6 +822,21 @@ class goals_criteria implements goals_criteria_i
{
$results = [];
$department_ids = $this->departments->listIDs();
if ($this->usesAdvancedTargetDuration()) {
foreach ($department_ids as $dept_id) {
$deptId = (int)$dept_id;
$results[$deptId] = [
'all' => $this->getAdvancedProgressDetailsForDepartment($deptId, null, $department_ids),
'today' => $this->getAdvancedProgressDetailsForDepartment($deptId, 'today', $department_ids),
'week' => $this->getAdvancedProgressDetailsForDepartment($deptId, 'week', $department_ids),
'month' => $this->getAdvancedProgressDetailsForDepartment($deptId, 'month', $department_ids),
'year' => $this->getAdvancedProgressDetailsForDepartment($deptId, 'year', $department_ids),
'to_date' => $this->getAdvancedProgressDetailsForDepartment($deptId, 'to_date', $department_ids),
];
}
return $results;
}
foreach ($department_ids as $dept_id) {
$dept_criteria = clone $this;
$dept_criteria->departments->set([(new \objects\departments_o())->select($dept_id)]);
@@ -787,6 +873,25 @@ class goals_criteria implements goals_criteria_i
$criteria->setTimeframeByName($timeframe);
}
if ($this->usesAdvancedTargetDuration()) {
$target = 0.0;
if (($criteria->start instanceof \DateTimeInterface) && ($criteria->end instanceof \DateTimeInterface)) {
$target = $this->calculateTargetForRange(
$criteria->start,
$criteria->end,
null,
$this->departments->listIDs()
);
}
return [
'count' => $criteria->getProgress(),
'target' => $target,
'date_from' => ($criteria->start instanceof \DateTimeInterface) ? $criteria->start->format(DATE_ATOM) : null,
'date_end' => ($criteria->end instanceof \DateTimeInterface) ? $criteria->end->format(DATE_ATOM) : null,
];
}
$target = 0.0;
$active_dept_ids = $criteria->departments->listIDs();