Add support for weekday-specific and time-of-day progress alerts in goals_criteria

- Introduce `progress_alert_weekdays` and `progress_alert_time_of_day` fields with parsing, validation, and sanitization logic.
- Add `goals_criteria_progress_alert_weekday` enum for supported weekdays.
- Update `toArray` and OpenAPI specification to support new fields and maintain backward compatibility.
This commit is contained in:
Jeppe Bundgaard
2026-01-27 08:52:12 +01:00
parent 4eacd04c2a
commit 043382c6f4
3 changed files with 159 additions and 0 deletions
+21
View File
@@ -6166,6 +6166,27 @@ components:
Max length depends on destination: 160 characters for SMS; 1024 characters for EMAIL/SLACK/other.
The canonical field name is snake_case `progress_alert_format`.
For backward-compatibility the API also accepts camelCase `progressAlertFormat` on input.
progress_alert_weekdays:
type: array
description: |
Weekdays on which progress alerts should be sent.
Use one or more of: MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY.
The canonical field name is snake_case `progress_alert_weekdays`.
For backward-compatibility the API also accepts camelCase `progressAlertWeekdays` on input.
items:
type: string
enum: [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
example: [MONDAY, WEDNESDAY, FRIDAY]
progress_alert_time_of_day:
type: string
nullable: true
description: |
Time of day (with timezone) when progress alerts should be sent.
Format: `HH:MMZ` or `HH:MM±HH:MM` (24-hour clock with UTC offset). Examples: `14:30Z`, `09:15+02:00`, `18:45-05:00`.
The canonical field name is snake_case `progress_alert_time_of_day`.
For backward-compatibility the API also accepts camelCase `progressAlertTimeOfDay` on input.
pattern: '^([01]\d|2[0-3]):[0-5]\d(?:Z|[+-](?:[01]\d|2[0-3]):?[0-5]\d)$'
example: "14:30+02:00"
DepartmentGoal:
type: object
@@ -8,6 +8,7 @@ use goals\helpers\goals_criteria_progress_alert_frequency;
use goals\helpers\goals_criteria_progress_alert_destination;
use goals\helpers\goals_criteria_progress_alert_progress_type;
use goals\helpers\goals_criteria_progress_alert_style;
use goals\helpers\goals_criteria_progress_alert_weekday;
use goals\interfaces\goals_criteria_i;
use goals\traits\goals_result_parser_t;
use goals\traits\goals_target_t;
@@ -66,6 +67,16 @@ class goals_criteria implements goals_criteria_i
* @var string|null $progress_alert_format
*/
public ?string $progress_alert_format = null;
/**
* Weekdays on which progress alerts should be sent
* @var goals_criteria_progress_alert_weekday[] $progress_alert_weekdays
*/
public array $progress_alert_weekdays = [];
/**
* Time of day (with timezone) when progress alerts should be sent, formatted as HH:MM with timezone (e.g., 14:30Z or 14:30+02:00)
* @var string|null $progress_alert_time_of_day
*/
public ?string $progress_alert_time_of_day = null;
/**
* Constructor
*/
@@ -182,6 +193,46 @@ class goals_criteria implements goals_criteria_i
$criteria->progress_alert_format = trim((string)$data['progressAlertFormat']);
}
// Parse progress alert weekdays (array of strings)
if (isset($data['progress_alert_weekdays']) && is_array($data['progress_alert_weekdays'])) {
$seen = [];
$weekdays = [];
foreach ($data['progress_alert_weekdays'] as $wd) {
if (!is_string($wd)) { continue; }
$e = goals_criteria_progress_alert_weekday::tryFrom($wd);
if ($e !== null) {
$key = $e->name;
if (!isset($seen[$key])) {
$seen[$key] = true;
$weekdays[] = $e;
}
}
}
$criteria->progress_alert_weekdays = $weekdays;
} elseif (isset($data['progressAlertWeekdays']) && is_array($data['progressAlertWeekdays'])) {
$seen = [];
$weekdays = [];
foreach ($data['progressAlertWeekdays'] as $wd) {
if (!is_string($wd)) { continue; }
$e = goals_criteria_progress_alert_weekday::tryFrom($wd);
if ($e !== null) {
$key = $e->name;
if (!isset($seen[$key])) {
$seen[$key] = true;
$weekdays[] = $e;
}
}
}
$criteria->progress_alert_weekdays = $weekdays;
}
// 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']);
} elseif (isset($data['progressAlertTimeOfDay']) && is_string($data['progressAlertTimeOfDay'])) {
$criteria->progress_alert_time_of_day = trim((string)$data['progressAlertTimeOfDay']);
}
// Parse timeframe with validation
if (isset($data['start']) && is_string($data['start'])) {
try {
@@ -328,6 +379,8 @@ class goals_criteria implements goals_criteria_i
'progress_alert_progress_type' => $this->progress_alert_progress_type?->name ?? goals_criteria_progress_alert_progress_type::NONE->name,
'progress_alert_style' => $this->progress_alert_style?->name ?? goals_criteria_progress_alert_style::NONE->name,
'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,
];
}
@@ -480,5 +533,57 @@ class goals_criteria implements goals_criteria_i
$this->progress_alert_format = $fmt === '' ? null : $fmt;
}
}
// Sanitize weekdays: keep only valid enum values, dedupe, and normalize order (Mon..Sun)
if (is_array($this->progress_alert_weekdays)) {
$seen = [];
$clean = [];
foreach ($this->progress_alert_weekdays as $wd) {
$enum = $wd instanceof goals_criteria_progress_alert_weekday
? $wd
: (is_string($wd) ? goals_criteria_progress_alert_weekday::tryFrom($wd) : null);
if ($enum !== null) {
$k = $enum->name;
if (!isset($seen[$k])) {
$seen[$k] = true;
$clean[] = $enum;
}
}
}
// Order by weekday index
$order = [
'MONDAY' => 1,
'TUESDAY' => 2,
'WEDNESDAY' => 3,
'THURSDAY' => 4,
'FRIDAY' => 5,
'SATURDAY' => 6,
'SUNDAY' => 7,
];
usort($clean, function ($a, $b) use ($order) {
$an = $a->name ?? (string)$a;
$bn = $b->name ?? (string)$b;
return ($order[$an] ?? 99) <=> ($order[$bn] ?? 99);
});
$this->progress_alert_weekdays = $clean;
}
// Sanitize time of day with timezone (HH:MMZ or HH:MM±HH:MM)
if (is_string($this->progress_alert_time_of_day)) {
$time = trim($this->progress_alert_time_of_day);
// Accept Z, +HH:MM, -HH:MM (offset colon optional: +HHMM)
$pattern = '/^([01]\d|2[0-3]):[0-5]\d(?:Z|[+-](?:[01]\d|2[0-3]):?[0-5]\d)$/';
if (!preg_match($pattern, $time)) {
// Try to normalize offsets like +HHMM to +HH:MM
if (preg_match('/^([01]\d|2[0-3]):([0-5]\d)([+-])(\d{2})(\d{2})$/', $time, $m)) {
$time = sprintf('%s:%s%s%s:%s', $m[1], $m[2], $m[3], $m[4], $m[5]);
}
}
if (preg_match($pattern, $time)) {
$this->progress_alert_time_of_day = $time;
} else {
$this->progress_alert_time_of_day = null;
}
}
}
}
@@ -0,0 +1,33 @@
<?php
namespace goals\helpers;
enum goals_criteria_progress_alert_weekday
{
case MONDAY;
case TUESDAY;
case WEDNESDAY;
case THURSDAY;
case FRIDAY;
case SATURDAY;
case SUNDAY;
public static function tryFrom(string $param): ?goals_criteria_progress_alert_weekday
{
return match (strtoupper($param)) {
'MON', 'MONDAY' => self::MONDAY,
'TUE', 'TUES', 'TUESDAY' => self::TUESDAY,
'WED', 'WEDNESDAY' => self::WEDNESDAY,
'THU', 'THUR', 'THURS', 'THURSDAY' => self::THURSDAY,
'FRI', 'FRIDAY' => self::FRIDAY,
'SAT', 'SATURDAY' => self::SATURDAY,
'SUN', 'SUNDAY' => self::SUNDAY,
default => null,
};
}
public function equals(goals_criteria_progress_alert_weekday $param): bool
{
return $this === $param;
}
}