From 8c9388546b3a919016b36dd420986e0d3d6bfaaa Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Tue, 24 Mar 2026 11:23:08 +0100 Subject: [PATCH] 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. --- openapi.yaml | 97 ++++++++++++++-- .../modules/goals/classes/goals_criteria.php | 105 ++++++++++++++++++ .../goals_criteria_target_duration.php | 27 +++++ .../workfeed/config/workfeed_company_id_c.php | 29 +++++ .../nginx/app/modules/workfeed/workfeed_c.php | 5 + 5 files changed, 256 insertions(+), 7 deletions(-) create mode 100644 services/nginx/app/modules/goals/helpers/goals_criteria_target_duration.php create mode 100644 services/nginx/app/modules/workfeed/config/workfeed_company_id_c.php diff --git a/openapi.yaml b/openapi.yaml index ce0b5680..a355d4f6 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -8640,6 +8640,30 @@ paths: application/json: schema: $ref: '#/components/schemas/WorkfeedConfigListResponse' + examples: + default: + summary: Workfeed module configuration + value: + success: true + data: + - module: workfeed + variable: enabled + type: bool + value: true + - module: workfeed + variable: api_url + type: string + value: https://api.workfeed.io + - module: workfeed + variable: api_key + type: string + value: wf_live_xxxxxxxxxxxxxxxxx + - module: workfeed + variable: CompanyID + type: string + value: "123456" + meta: [] + includes: [] post: tags: [Config] summary: Update Workfeed config @@ -10106,18 +10130,56 @@ components: - type: string required: [module, variable, type, value] - WorkfeedConfigEntry: + WorkfeedConfigEnabledEntry: type: object properties: module: { type: string, enum: [workfeed] } - variable: { type: string, enum: [enabled, api_url, api_key] } - type: { type: string, enum: [bool, string] } - value: - oneOf: - - type: boolean - - type: string + variable: { type: string, enum: [enabled] } + type: { type: string, enum: [bool] } + value: { type: boolean } required: [module, variable, type, value] + WorkfeedConfigApiUrlEntry: + type: object + properties: + module: { type: string, enum: [workfeed] } + variable: { type: string, enum: [api_url] } + type: { type: string, enum: [string] } + value: { type: string, example: "https://api.workfeed.io" } + required: [module, variable, type, value] + + WorkfeedConfigApiKeyEntry: + type: object + properties: + module: { type: string, enum: [workfeed] } + variable: { type: string, enum: [api_key] } + type: { type: string, enum: [string] } + value: { type: string, example: "wf_live_xxxxxxxxxxxxxxxxx" } + required: [module, variable, type, value] + + WorkfeedConfigCompanyIdEntry: + type: object + properties: + module: { type: string, enum: [workfeed] } + variable: { type: string, enum: [CompanyID] } + type: { type: string, enum: [string] } + value: { type: string, example: "123456" } + required: [module, variable, type, value] + + WorkfeedConfigEntry: + oneOf: + - $ref: '#/components/schemas/WorkfeedConfigEnabledEntry' + - $ref: '#/components/schemas/WorkfeedConfigApiUrlEntry' + - $ref: '#/components/schemas/WorkfeedConfigApiKeyEntry' + - $ref: '#/components/schemas/WorkfeedConfigCompanyIdEntry' + discriminator: + propertyName: variable + mapping: + enabled: '#/components/schemas/WorkfeedConfigEnabledEntry' + api_url: '#/components/schemas/WorkfeedConfigApiUrlEntry' + api_key: '#/components/schemas/WorkfeedConfigApiKeyEntry' + CompanyID: '#/components/schemas/WorkfeedConfigCompanyIdEntry' + GatewayApiConfigEntry: type: object properties: @@ -10319,6 +10381,27 @@ components: properties: data: { type: array, items: { $ref: '#/components/schemas/WorkfeedConfigEntry' } } required: [data] + example: + success: true + data: + - module: workfeed + variable: enabled + type: bool + value: true + - module: workfeed + variable: api_url + type: string + value: https://api.workfeed.io + - module: workfeed + variable: api_key + type: string + value: wf_live_xxxxxxxxxxxxxxxxx + - module: workfeed + variable: CompanyID + type: string + value: "123456" + meta: [] + includes: [] GatewayApiConfigListResponse: allOf: diff --git a/services/nginx/app/modules/goals/classes/goals_criteria.php b/services/nginx/app/modules/goals/classes/goals_criteria.php index c600998a..0cedbd36 100644 --- a/services/nginx/app/modules/goals/classes/goals_criteria.php +++ b/services/nginx/app/modules/goals/classes/goals_criteria.php @@ -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 */ 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(); diff --git a/services/nginx/app/modules/goals/helpers/goals_criteria_target_duration.php b/services/nginx/app/modules/goals/helpers/goals_criteria_target_duration.php new file mode 100644 index 00000000..50c9c046 --- /dev/null +++ b/services/nginx/app/modules/goals/helpers/goals_criteria_target_duration.php @@ -0,0 +1,27 @@ + goals_criteria_target_duration::ENTIRE_DURATION, + 'WEEKS' => goals_criteria_target_duration::WEEKS, + 'MONTHS' => goals_criteria_target_duration::MONTHS, + 'YEARS' => goals_criteria_target_duration::YEARS, + default => null, + }; + } + + public function equals(goals_criteria_target_duration $param): bool + { + return $this === $param; + } +} diff --git a/services/nginx/app/modules/workfeed/config/workfeed_company_id_c.php b/services/nginx/app/modules/workfeed/config/workfeed_company_id_c.php new file mode 100644 index 00000000..7a8c5144 --- /dev/null +++ b/services/nginx/app/modules/workfeed/config/workfeed_company_id_c.php @@ -0,0 +1,29 @@ +enabled = new workfeed_enabled_c(); $this->api_url = new workfeed_api_url_c(); $this->api_key = new workfeed_api_key_c(); + $this->company_id = new workfeed_company_id_c(); } }