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
+90 -7
View File
@@ -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:
@@ -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();
@@ -0,0 +1,27 @@
<?php
namespace goals\helpers;
enum goals_criteria_target_duration
{
case ENTIRE_DURATION;
case WEEKS;
case MONTHS;
case YEARS;
public static function tryFrom(string $param): ?goals_criteria_target_duration
{
return match (strtoupper($param)) {
'ENTIRE_DURATION' => 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;
}
}
@@ -0,0 +1,29 @@
<?php
namespace workfeed\config;
use Exception;
use traits\module_config_variable;
class workfeed_company_id_c
{
use module_config_variable;
/**
* @throws Exception
*/
public function __construct()
{
self::setupConfigVariable(
'workfeed',
'CompanyID',
'string',
false,
null,
'The Workfeed company identifier',
'123456',
false,
''
);
}
}
@@ -5,10 +5,12 @@ namespace workfeed;
require_once WD . '/modules/workfeed/config/workfeed_enabled_c.php';
require_once WD . '/modules/workfeed/config/workfeed_api_url_c.php';
require_once WD . '/modules/workfeed/config/workfeed_api_key_c.php';
require_once WD . '/modules/workfeed/config/workfeed_company_id_c.php';
use traits\module_config_t;
use workfeed\config\workfeed_api_key_c;
use workfeed\config\workfeed_api_url_c;
use workfeed\config\workfeed_company_id_c;
use workfeed\config\workfeed_enabled_c;
class workfeed_c
@@ -18,6 +20,7 @@ class workfeed_c
public workfeed_enabled_c $enabled;
public workfeed_api_url_c $api_url;
public workfeed_api_key_c $api_key;
public workfeed_company_id_c $company_id;
public function __construct()
{
@@ -26,10 +29,12 @@ class workfeed_c
workfeed_enabled_c::class,
workfeed_api_url_c::class,
workfeed_api_key_c::class,
workfeed_company_id_c::class,
]);
$this->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();
}
}