Add support for progress alert rendering in goals module
- Implement `goals_progress_alert_renderer` service to compose and render progress alert messages based on destination, progress type, and style. - Add format classes (`sms_progress_alert_format`, `email_progress_alert_format`, `slack_progress_alert_format`) with length constraints. - Extend `goals_criteria` to include alert customization properties (`progress_alert_destination`, `progress_alert_progress_type`, `progress_alert_style`, `progress_alert_format`) with parsing and validation logic. - Update `toArray` and `fromJson` methods to support new fields.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace goals\formats;
|
||||
|
||||
use goals\traits\goals_criteria_progress_alert_format_t;
|
||||
|
||||
class email_progress_alert_format implements \JsonSerializable
|
||||
{
|
||||
use goals_criteria_progress_alert_format_t;
|
||||
|
||||
private string $text;
|
||||
|
||||
public const MAX_LEN = 1024;
|
||||
|
||||
public function __construct(string $text)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace goals\formats;
|
||||
|
||||
use goals\traits\goals_criteria_progress_alert_format_t;
|
||||
|
||||
class slack_progress_alert_format implements \JsonSerializable
|
||||
{
|
||||
use goals_criteria_progress_alert_format_t;
|
||||
|
||||
private string $text;
|
||||
|
||||
public const MAX_LEN = 1024;
|
||||
|
||||
public function __construct(string $text)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace goals\formats;
|
||||
|
||||
use goals\traits\goals_criteria_progress_alert_format_t;
|
||||
|
||||
class sms_progress_alert_format implements \JsonSerializable
|
||||
{
|
||||
use goals_criteria_progress_alert_format_t;
|
||||
|
||||
private string $text;
|
||||
|
||||
public const MAX_LEN = 160;
|
||||
|
||||
public function __construct(string $text)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace goals\services;
|
||||
|
||||
use goals\classes\goals_criteria;
|
||||
use goals\formats\email_progress_alert_format;
|
||||
use goals\formats\slack_progress_alert_format;
|
||||
use goals\formats\sms_progress_alert_format;
|
||||
use goals\helpers\goals_criteria_progress_alert_destination as Dest;
|
||||
use goals\helpers\goals_criteria_progress_alert_progress_type as PType;
|
||||
use goals\helpers\goals_criteria_progress_alert_style as Style;
|
||||
|
||||
class goals_progress_alert_renderer
|
||||
{
|
||||
public static function render(goals_criteria $criteria): string
|
||||
{
|
||||
$label = (string)($criteria->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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user