diff --git a/services/nginx/app/modules/goals/classes/goals_criteria.php b/services/nginx/app/modules/goals/classes/goals_criteria.php index 5f99cebc..597fbfb0 100644 --- a/services/nginx/app/modules/goals/classes/goals_criteria.php +++ b/services/nginx/app/modules/goals/classes/goals_criteria.php @@ -5,6 +5,9 @@ namespace goals\classes; use classes\db; use goals\helpers\goals_criteria_type; 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\interfaces\goals_criteria_i; use goals\traits\goals_result_parser_t; use goals\traits\goals_target_t; @@ -43,6 +46,26 @@ class goals_criteria implements goals_criteria_i * @var goals_criteria_progress_alert_frequency $progress_alert_frequency */ public goals_criteria_progress_alert_frequency $progress_alert_frequency; + /** + * Progress alert destination for the goal + * @var goals_criteria_progress_alert_destination $progress_alert_destination + */ + public goals_criteria_progress_alert_destination $progress_alert_destination; + /** + * Progress alert progress type for the goal + * @var goals_criteria_progress_alert_progress_type $progress_alert_progress_type + */ + public goals_criteria_progress_alert_progress_type $progress_alert_progress_type; + /** + * Progress alert style for the goal + * @var goals_criteria_progress_alert_style $progress_alert_style + */ + public goals_criteria_progress_alert_style $progress_alert_style; + /** + * Progress alert format (free-form string for now; rendering is handled elsewhere) + * @var string|null $progress_alert_format + */ + public ?string $progress_alert_format = null; /** * Constructor */ @@ -54,6 +77,9 @@ class goals_criteria implements goals_criteria_i $this->start = new \DateTime(); $this->end = new \DateTime(); $this->progress_alert_frequency = goals_criteria_progress_alert_frequency::NONE; + $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; } @@ -110,6 +136,52 @@ class goals_criteria implements goals_criteria_i } } + // Parse progress alert destination (ignore unknowns) + if (isset($data['progress_alert_destination']) && is_string($data['progress_alert_destination'])) { + $dest = goals_criteria_progress_alert_destination::tryFrom($data['progress_alert_destination']); + if ($dest !== null) { + $criteria->progress_alert_destination = $dest; + } + } elseif (isset($data['progressAlertDestination']) && is_string($data['progressAlertDestination'])) { + $dest = goals_criteria_progress_alert_destination::tryFrom($data['progressAlertDestination']); + if ($dest !== null) { + $criteria->progress_alert_destination = $dest; + } + } + + // Parse progress alert progress type (ignore unknowns) + if (isset($data['progress_alert_progress_type']) && is_string($data['progress_alert_progress_type'])) { + $ptype = goals_criteria_progress_alert_progress_type::tryFrom($data['progress_alert_progress_type']); + if ($ptype !== null) { + $criteria->progress_alert_progress_type = $ptype; + } + } elseif (isset($data['progressAlertProgressType']) && is_string($data['progressAlertProgressType'])) { + $ptype = goals_criteria_progress_alert_progress_type::tryFrom($data['progressAlertProgressType']); + if ($ptype !== null) { + $criteria->progress_alert_progress_type = $ptype; + } + } + + // Parse progress alert style (ignore unknowns) + if (isset($data['progress_alert_style']) && is_string($data['progress_alert_style'])) { + $style = goals_criteria_progress_alert_style::tryFrom($data['progress_alert_style']); + if ($style !== null) { + $criteria->progress_alert_style = $style; + } + } elseif (isset($data['progressAlertStyle']) && is_string($data['progressAlertStyle'])) { + $style = goals_criteria_progress_alert_style::tryFrom($data['progressAlertStyle']); + if ($style !== null) { + $criteria->progress_alert_style = $style; + } + } + + // Parse progress alert format (string) + if (isset($data['progress_alert_format']) && is_string($data['progress_alert_format'])) { + $criteria->progress_alert_format = trim((string)$data['progress_alert_format']); + } elseif (isset($data['progressAlertFormat']) && is_string($data['progressAlertFormat'])) { + $criteria->progress_alert_format = trim((string)$data['progressAlertFormat']); + } + // Parse timeframe with validation if (isset($data['start']) && is_string($data['start'])) { try { @@ -252,6 +324,10 @@ class goals_criteria implements goals_criteria_i 'departments' => $departments, 'products' => $products, 'progress_alert_frequency' => $this->progress_alert_frequency?->name ?? goals_criteria_progress_alert_frequency::NONE->name, + 'progress_alert_destination' => $this->progress_alert_destination?->name ?? goals_criteria_progress_alert_destination::NONE->name, + '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, ]; } @@ -264,6 +340,14 @@ class goals_criteria implements goals_criteria_i return json_encode($this->toArray()); } + /** + * Render a progress alert message based on current destination, progress type, style and optional template + */ + public function renderProgressAlert(): string + { + return \goals\services\goals_progress_alert_renderer::render($this); + } + private function getProgress(): int { return match ($this->type) { @@ -361,6 +445,15 @@ class goals_criteria implements goals_criteria_i if (!in_array($this->progress_alert_frequency, goals_criteria_progress_alert_frequency::cases())) { $this->progress_alert_frequency = goals_criteria_progress_alert_frequency::NONE; } + if (!in_array($this->progress_alert_destination, goals_criteria_progress_alert_destination::cases())) { + $this->progress_alert_destination = goals_criteria_progress_alert_destination::NONE; + } + if (!in_array($this->progress_alert_progress_type, goals_criteria_progress_alert_progress_type::cases())) { + $this->progress_alert_progress_type = goals_criteria_progress_alert_progress_type::NONE; + } + if (!in_array($this->progress_alert_style, goals_criteria_progress_alert_style::cases())) { + $this->progress_alert_style = goals_criteria_progress_alert_style::NONE; + } // Sanitize label if (is_string($this->label)) { @@ -372,5 +465,20 @@ class goals_criteria implements goals_criteria_i $this->label = $label; } } + + // Sanitize progress alert format (length depends on destination) + if (is_string($this->progress_alert_format)) { + $fmt = trim($this->progress_alert_format); + $fmt = strip_tags($fmt); + $fmt = preg_replace('/\s+/', ' ', $fmt); + if ($fmt !== null) { + $maxLen = match ($this->progress_alert_destination) { + \goals\helpers\goals_criteria_progress_alert_destination::SMS => 160, + default => 1024, + }; + $fmt = mb_substr($fmt, 0, $maxLen); + $this->progress_alert_format = $fmt === '' ? null : $fmt; + } + } } } \ No newline at end of file diff --git a/services/nginx/app/modules/goals/formats/email_progress_alert_format.php b/services/nginx/app/modules/goals/formats/email_progress_alert_format.php new file mode 100644 index 00000000..f3a08331 --- /dev/null +++ b/services/nginx/app/modules/goals/formats/email_progress_alert_format.php @@ -0,0 +1,28 @@ +text = $text; + } + + public function render(): string + { + $rendered = trim($this->text); + if (mb_strlen($rendered) > self::MAX_LEN) { + $rendered = mb_substr($rendered, 0, self::MAX_LEN); + } + return $rendered; + } +} diff --git a/services/nginx/app/modules/goals/formats/slack_progress_alert_format.php b/services/nginx/app/modules/goals/formats/slack_progress_alert_format.php new file mode 100644 index 00000000..b89705b3 --- /dev/null +++ b/services/nginx/app/modules/goals/formats/slack_progress_alert_format.php @@ -0,0 +1,29 @@ +text = $text; + } + + public function render(): string + { + $rendered = trim($this->text); + // Slack supports simple markdown; no transformation here — enforce overall length + if (mb_strlen($rendered) > self::MAX_LEN) { + $rendered = mb_substr($rendered, 0, self::MAX_LEN); + } + return $rendered; + } +} diff --git a/services/nginx/app/modules/goals/formats/sms_progress_alert_format.php b/services/nginx/app/modules/goals/formats/sms_progress_alert_format.php new file mode 100644 index 00000000..ac39434c --- /dev/null +++ b/services/nginx/app/modules/goals/formats/sms_progress_alert_format.php @@ -0,0 +1,29 @@ +text = $text; + } + + public function render(): string + { + $rendered = trim($this->text); + // Enforce SMS length hard limit + if (mb_strlen($rendered) > self::MAX_LEN) { + $rendered = mb_substr($rendered, 0, self::MAX_LEN); + } + return $rendered; + } +} diff --git a/services/nginx/app/modules/goals/services/goals_progress_alert_renderer.php b/services/nginx/app/modules/goals/services/goals_progress_alert_renderer.php new file mode 100644 index 00000000..921d7ded --- /dev/null +++ b/services/nginx/app/modules/goals/services/goals_progress_alert_renderer.php @@ -0,0 +1,89 @@ +label ?? 'Goal'); + $count = (int)self::getProgress($criteria); + $target = (int)($criteria->target ?? 0); + $percent = $target > 0 ? round(($count / max(1, $target)) * 100, 2) : 0.0; + $dateFmt = 'Y-m-d'; + $tfStart = $criteria->start instanceof \DateTimeInterface ? $criteria->start->format($dateFmt) : ''; + $tfEnd = $criteria->end instanceof \DateTimeInterface ? $criteria->end->format($dateFmt) : ''; + $timeframe = ($tfStart && $tfEnd) ? "$tfStart → $tfEnd" : ''; + $deptList = implode(',', $criteria->departments?->listIDs() ?? []); + + $prefix = match ($criteria->progress_alert_style) { + Style::DEPARTMENT_COMPARE => '[Dept compare] ', + Style::COLLECTIVE => '[Collective] ', + Style::SINGLE_DEPARTMENT => '[Single] ', + default => '' + }; + + $body = match ($criteria->progress_alert_progress_type) { + PType::PERCENTAGE_ONLY => sprintf('Progress: %s%%', number_format($percent, 2)), + PType::COUNT_ONLY => sprintf('Progress: %d', $count), + PType::COUNT_AND_TARGET => sprintf('Progress: %d / %d', $count, $target), + PType::ALL => sprintf('Progress: %s%% (%d / %d)', number_format($percent, 2), $count, $target), + default => sprintf('Progress: %s%%', number_format($percent, 2)), + }; + + $parts = array_filter([ + $prefix . $label, + $body, + $timeframe, + $deptList ? "Depts: $deptList" : null, + ]); + + $composed = implode(' | ', $parts); + + // If a custom template string is provided, use it as a printf-style template + // Supported tokens: {label}, {percent}, {count}, {target}, {timeframe}, {departments}, {prefix} + if (is_string($criteria->progress_alert_format) && $criteria->progress_alert_format !== '') { + $tmpl = $criteria->progress_alert_format; + $map = [ + '{label}' => $label, + '{percent}' => number_format($percent, 2), + '{count}' => (string)$count, + '{target}' => (string)$target, + '{timeframe}' => $timeframe, + '{departments}' => $deptList, + '{prefix}' => $prefix, + '{body}' => $body, + ]; + $composed = strtr($tmpl, $map); + } + + // Wrap in proper destination format class to enforce channel limits + return match ($criteria->progress_alert_destination) { + Dest::SMS => (new sms_progress_alert_format($composed))->render(), + Dest::EMAIL => (new email_progress_alert_format($composed))->render(), + Dest::SLACK => (new slack_progress_alert_format($composed))->render(), + default => $composed, + }; + } + + private static function getProgress(goals_criteria $criteria): int + { + $ref = new \ReflectionClass($criteria); + if ($ref->hasMethod('getProgress')) { + $m = $ref->getMethod('getProgress'); + $m->setAccessible(true); + /** @var int $val */ + $val = $m->invoke($criteria); + return (int)$val; + } + return 0; + } +}