Refactor goals_progress_alert_renderer for enhanced department summary rendering

- Simplify multi-period department progress calculations by restructuring logic to render by period first.
- Add support for daily target calculations and timeframe-based target adjustments in `goals_criteria`.
- Introduce `countOperatingDaysInTimeframe` and related helper methods for improved alert scheduling and progress tracking.
This commit is contained in:
Jeppe Bundgaard
2026-01-29 20:20:05 +01:00
parent b3b12b58fd
commit cd2190d976
2 changed files with 133 additions and 76 deletions
@@ -643,4 +643,15 @@ class goals_criteria implements goals_criteria_i
$new_criteria->departments->set([(new \objects\departments_o())->select($department_id)]);
return $new_criteria;
}
public function getAlertDaysForDepartment(int $deptId): array
{
$alert_days = [];
foreach ($this->progress_alert_weekdays as $weekday) {
if ($weekday instanceof goals_criteria_progress_alert_weekday) {
$alert_days[] = $weekday;
}
}
return $alert_days;
}
}
@@ -135,11 +135,6 @@ class goals_progress_alert_renderer
* Renders a Danish multi-line progress summary for yesterday, week-to-date, and month-to-date.
* If $includeTargets is true and a target is set, shows progress as "X ud af Y (Z%)".
* Otherwise, shows just the raw counts.
* It should display like this:
* {$departmentName}:
* Igår: 5 ud af 10 (50%)
* Ugen total: 20 ud af 50 (40%)
* Måneden total: 75 ud af 100 (75%)
*/
// Collect departments to summarize (id, label, highlight) using stable ID list
$departments = [];
@@ -172,88 +167,139 @@ class goals_progress_alert_renderer
$outLines[] = (string)$hLine;
}
}
foreach ($departments as $deptData) {
$lines = [];
$lines[] = $deptData['label'] . ':';
// Calculate counts on the fly for each period using the criteria with department filter
$deptId = $deptData['id'];
$yesterdayCount = 0;
$weekTotalCount = 0;
$monthTotalCount = 0;
// Change grouping: render by period, then list departments under each period
$periods = [
[
'label' => 'Igår',
'range' => self::getYesterdayRange(),
],
[
'label' => 'Ugen total',
'range' => self::getWeekToDateRange(),
],
[
'label' => 'Måneden total',
'range' => self::getMonthToDateRange(),
],
];
if ($deptId !== null) {
// Base dept-filtered criteria
$base = $criteria->withDepartmentFilter((int)$deptId);
foreach ($periods as $idx => $period) {
$label = (string)$period['label'];
[$ps, $pe] = $period['range'];
$range = self::clampToCriteriaWindow($criteria, $ps, $pe);
// Yesterday
[$ys, $ye] = self::getYesterdayRange();
$yr = self::clampToCriteriaWindow($criteria, $ys, $ye);
if ($yr !== null) {
[$ys2, $ye2] = $yr;
// Period header
$outLines[] = $label . ':';
foreach ($departments as $deptData) {
$deptId = $deptData['id'];
$count = 0;
if ($deptId !== null && $range !== null) {
[$rs, $re] = $range;
$base = $criteria->withDepartmentFilter((int)$deptId);
$c = clone $base;
// goals_criteria timeframe expects \DateTime (mutable), convert from Immutable
$c->start = \DateTime::createFromImmutable($ys2);
$c->end = \DateTime::createFromImmutable($ye2);
$yesterdayCount = (int)self::getProgress($c);
// goals_criteria timeframe expects \DateTimeInterface
$c->start = $rs;
$c->end = $re;
$count = self::getProgress($c);
}
// Week-to-date (Mon..today)
[$ws, $we] = self::getWeekToDateRange();
$wr = self::clampToCriteriaWindow($criteria, $ws, $we);
if ($wr !== null) {
[$ws2, $we2] = $wr;
$c = clone $base;
$c->start = \DateTime::createFromImmutable($ws2);
$c->end = \DateTime::createFromImmutable($we2);
$weekTotalCount = (int)self::getProgress($c);
if ($includeTargets && isset($criteria->target)) {
// Set the target to (days since start of period) * (daily target)
$target = self::getDailyTarget($criteria);
$target = self::getTargetForDepartmentInTimeframe(
$criteria,
$rs,
$re
);
$percent = $target > 0 ? round(($count / max(1, $target)) * 100, 2) : 0.0;
// If the $department is set and matches the current department, make the related line stand out (*text here*)
$line = $deptData['highlight']
? "> *" . sprintf('%s: %d ud af %d (%.2f%%)', $deptData['label'], $count, $target, $percent) . "*"
: "- " . sprintf('%s: %d ud af %d (%.2f%%)', $deptData['label'], $count, $target, $percent);
} else {
// If the $department is set and matches the current department, make the related line stand out (*text here*)
$line = $deptData['highlight']
? "> *" .sprintf('%s: %d', $deptData['label'], $count) . "*"
: " - " .sprintf('%s: %d', $deptData['label'], $count);
}
$outLines[] = $line;
}
// Month-to-date (1st..today)
[$ms, $me] = self::getMonthToDateRange();
$mr = self::clampToCriteriaWindow($criteria, $ms, $me);
if ($mr !== null) {
[$ms2, $me2] = $mr;
$c = clone $base;
$c->start = \DateTime::createFromImmutable($ms2);
$c->end = \DateTime::createFromImmutable($me2);
$monthTotalCount = (int)self::getProgress($c);
}
}
// Yesterday
if ($includeTargets && isset($criteria->target)) {
$target = (int)$criteria->target;
$percentYest = $target > 0 ? round(($yesterdayCount / max(1, $target)) * 100, 2) : 0.0;
$lines[] = sprintf('Igår: %d ud af %d (%.2f%%)', $yesterdayCount, $target, $percentYest);
} else {
$lines[] = sprintf('Igår: %d', $yesterdayCount);
}
// Week total
if ($includeTargets && isset($criteria->target)) {
$target = (int)$criteria->target;
$percentWeek = $target > 0 ? round(($weekTotalCount / max(1, $target)) * 100, 2) : 0.0;
$lines[] = sprintf('Ugen total: %d ud af %d (%.2f%%)', $weekTotalCount, $target, $percentWeek);
} else {
$lines[] = sprintf('Ugen total: %d', $weekTotalCount);
}
// Month total
if ($includeTargets && isset($criteria->target)) {
$target = (int)$criteria->target;
$percentMonth = $target > 0 ? round(($monthTotalCount / max(1, $target)) * 100, 2) : 0.0;
$lines[] = sprintf('Måneden total: %d ud af %d (%.2f%%)', $monthTotalCount, $target, $percentMonth);
} else {
$lines[] = sprintf('Måneden total: %d', $monthTotalCount);
}
// Highlight if needed
if ($deptData['highlight']) {
$outLines[] = ">>> " . implode("\n", $lines);
} else {
$outLines[] = implode("\n", $lines);
// Add a blank line between periods (but not after the last one)
if ($idx < count($periods) - 1) {
$outLines[] = '';
}
}
return implode("\n\n", $outLines);
return implode("\n", $outLines);
}
// --- Date range helpers for summary periods ---
/**
* Get the amount of days that alerts will be sent during the timeframe
* @param goals_criteria $criteria
* @param \DateTimeImmutable $rs
* @param \DateTimeImmutable $re
* @return int
*/
public function countOperatingDaysInTimeframe(
goals_criteria $criteria,
\DateTimeImmutable $rs,
\DateTimeImmutable $re
): int
{
$operatingWeekDays = $criteria->operating_days_of_week ?? [1, 2, 3, 4, 5]; // Default to Mon-Fri
$operatingDays = 0;
$current = $rs;
while ($current <= $re) {
$dayOfWeek = (int)$current->format('N'); // 1 (Mon) to 7 (Sun)
if (in_array($dayOfWeek, $operatingWeekDays, true)) {
$operatingDays++;
}
$current = $current->modify('+1 day');
}
return $operatingDays;
}
private static function getDailyTarget(goals_criteria $criteria): float
{
$operatingDays = (new goals_progress_alert_renderer)->countOperatingDaysInTimeframe(
$criteria,
$criteria->start instanceof \DateTimeInterface ? \DateTimeImmutable::createFromMutable($criteria->start) : new \DateTimeImmutable('now'),
$criteria->end instanceof \DateTimeInterface ? \DateTimeImmutable::createFromMutable($criteria->end) : new \DateTimeImmutable('now')
);
if ($operatingDays <= 0) {
return 0.0;
}
return ((int)($criteria->target ?? 0)) / $operatingDays;
}
/**
* @param goals_criteria $criteria
* @param \DateTimeImmutable $rs
* @param \DateTimeImmutable $re
* @return int
*/
private static function getTargetForDepartmentInTimeframe(
goals_criteria $criteria,
\DateTimeImmutable $rs,
\DateTimeImmutable $re
): int
{
$dailyTarget = self::getDailyTarget($criteria);
if ($dailyTarget <= 0.0) {
return 0;
}
$operatingDays = (new goals_progress_alert_renderer)->countOperatingDaysInTimeframe(
$criteria,
$rs,
$re
);
return (int)round($dailyTarget * $operatingDays);
}
// --- Date range helpers for summary periods ---
private static function getYesterdayRange(): array
{
$now = new \DateTimeImmutable('now');