Refactor goals_criteria and goals_progress_alert_renderer for improved department filtering and summary rendering

- Add cloning logic in `withDepartmentFilter` to avoid mutating original `goals_criteria` objects.
- Rename method parameter in `render` and refactor `renderDanishPeriodSummary` for enhanced readability and flexibility.
- Introduce helper methods for date range and criteria window adjustments to streamline department progress calculations.
This commit is contained in:
Jeppe Bundgaard
2026-01-29 19:42:03 +01:00
parent 84bfbdad25
commit b3b12b58fd
2 changed files with 166 additions and 58 deletions
@@ -637,7 +637,9 @@ class goals_criteria implements goals_criteria_i
public function withDepartmentFilter(int $department_id): goals_criteria
{
// Clone the criteria and create a fresh departments collection to avoid mutating the original
$new_criteria = clone $this;
$new_criteria->departments = new goals_criteria_departments();
$new_criteria->departments->set([(new \objects\departments_o())->select($department_id)]);
return $new_criteria;
}
@@ -13,7 +13,7 @@ use objects\departments_o;
class goals_progress_alert_renderer
{
public static function render(goals_criteria $criteria, ?departments_o $department = null): string
public static function render(goals_criteria $criteria, ?departments_o $department_to_highlight = null): string
{
$label = (string)($criteria->label ?? 'Goal');
$count = (int)self::getProgress($criteria);
@@ -28,7 +28,7 @@ class goals_progress_alert_renderer
// Prepare readable names for fields
$departmentsList = $criteria->departments?->list() ?? [];
$usersList = $criteria->users?->list() ?? [];
$productsList = $criteria->products?->list() ?? [];
$productsList = $criteria->products?->list() ?? [];
$deptNames = self::commaNamesFromObjects($departmentsList, 'department');
$customerNames = self::commaNamesFromObjects($usersList, 'user');
@@ -53,13 +53,13 @@ class goals_progress_alert_renderer
$deptNames !== '' ? "$deptLabel: $deptNames" : null,
$customerNames !== '' ? "$customerLabel: $customerNames" : null,
$productNames !== '' ? "$productLabel: $productNames" : null,
], department: $department),
], department: $department_to_highlight),
PType::ALL => self::renderDanishPeriodSummary($criteria, includeTargets: $target > 0,
header: [
$deptNames !== '' ? "$deptLabel: $deptNames" : null,
$customerNames !== '' ? "$customerLabel: $customerNames" : null,
$productNames !== '' ? "$productLabel: $productNames" : null,
], department: $department),
], department: $department_to_highlight),
// Legacy single-line fallbacks
PType::PERCENTAGE_ONLY => sprintf('Progress: %s%%', number_format($percent, 2)),
PType::COUNT_AND_TARGET => sprintf('Progress: %d / %d', $count, $target),
@@ -124,69 +124,175 @@ class goals_progress_alert_renderer
return 0;
}
private static function renderDanishPeriodSummary(goals_criteria $criteria, bool $includeTargets, array $header = [], ?departments_o $department = null): string
private static function renderDanishPeriodSummary(
goals_criteria $criteria,
bool $includeTargets,
?array $header = null,
?departments_o $department = null
): string
{
/**
* > Afdelinger: Taulov, Taastrup
* > Kunder: DHL FREIGHT (DENMARK ONLY) A/S, DHL FREIGHT (Trailer Danmark NTP), DHL FREIGHT (Trailere Finland NTP), DHL FREIGHT (Trailere Sverige NTP)
* > Produkt: Trailer
* > Igår: 4
* > Heraf:
* > - Taastrup: 2
* > - Taulov: 2
* > Ugen total: 9
* > Heraf:
* > - Taastrup: 7
* > - Taulov: 2
* > Måneden total: 15
* > Heraf:
* > - Taastrup: 10
* > - Taulov: 5
* 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%)
*/
// Build period windows
$now = new \DateTimeImmutable('now');
$yesterdayStart = $now->modify('-1 day')->setTime(0, 0, 0);
$yesterdayEnd = $now->modify('-1 day')->setTime(23, 59, 59);
// Week start (Monday) to now
$weekStart = (new \DateTimeImmutable('monday this week'))->setTime(0, 0, 0);
if ($now->format('N') === '1') { // if Monday, "monday this week" is today, ok
$weekStart = $now->setTime(0, 0, 0);
// Collect departments to summarize (id, label, highlight) using stable ID list
$departments = [];
$deptIds = $criteria->departments?->listIDs() ?? [];
foreach ($deptIds as $deptId) {
$deptId = (int)$deptId;
$deptName = '';
try {
$deptObj = (new \objects\departments_o())->select($deptId);
if (isset($deptObj->name)) {
$deptName = (string)$deptObj->name->value();
}
if ($deptName === '') {
$deptName = 'Afdeling ' . (string)$deptId;
}
} catch (\Throwable) {
$deptName = 'Afdeling (ukendt)';
}
$departments[] = [
'id' => $deptId,
'label' => $deptName,
'highlight' => ($department && isset($department->id) && (int)$department->id === $deptId)
];
}
// Month start to now
$monthStart = $now->setDate((int)$now->format('Y'), (int)$now->format('m'), 1)->setTime(0, 0, 0);
$yCount = self::progressForWindow($criteria, $yesterdayStart, $yesterdayEnd);
$wCount = self::progressForWindow($criteria, $weekStart, $now);
$mCount = self::progressForWindow($criteria, $monthStart, $now);
$target = (int)($criteria->target ?? 0);
$lines = array_values(array_filter($header));
if ($includeTargets && $target > 0) {
$yPct = number_format($yCount > 0 ? ($yCount / max(1, $target)) * 100 : 0, 0);
$wPct = number_format($wCount > 0 ? ($wCount / max(1, $target)) * 100 : 0, 0);
$mPct = number_format($mCount > 0 ? ($mCount / max(1, $target)) * 100 : 0, 0);
$lines[] = sprintf('Igår: %d ud af %d (%s%%)', $yCount, $target, $yPct);
$lines[] = sprintf('Ugen total: %d ud af %d (%s%%)', $wCount, $target, $wPct);
$lines[] = sprintf('Måneden total: %d ud af %d (%s%%)', $mCount, $target, $mPct);
} else {
$lines[] = sprintf('Igår: %d', $yCount);
$lines[] = sprintf('Ugen total: %d', $wCount);
$lines[] = sprintf('Måneden total: %d', $mCount);
// Build the output lines
$outLines = [];
// Prepend header lines if provided
if (is_array($header)) {
foreach (array_filter($header, fn($v) => $v !== null && $v !== '') as $hLine) {
$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;
return implode("\n", $lines);
if ($deptId !== null) {
// Base dept-filtered criteria
$base = $criteria->withDepartmentFilter((int)$deptId);
// Yesterday
[$ys, $ye] = self::getYesterdayRange();
$yr = self::clampToCriteriaWindow($criteria, $ys, $ye);
if ($yr !== null) {
[$ys2, $ye2] = $yr;
$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);
}
// 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);
}
// 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);
}
}
return implode("\n\n", $outLines);
}
private static function progressForWindow(goals_criteria $criteria, \DateTimeInterface $start, \DateTimeInterface $end): int
// --- Date range helpers for summary periods ---
private static function getYesterdayRange(): array
{
// Clone criteria and adjust timeframe
$clone = clone $criteria;
$clone->start = (clone \DateTime::createFromInterface($start));
$clone->end = (clone \DateTime::createFromInterface($end));
return self::getProgress($clone);
$now = new \DateTimeImmutable('now');
$y = $now->modify('-1 day');
return [$y->setTime(0, 0, 0), $y->setTime(23, 59, 59)];
}
private static function getWeekToDateRange(): array
{
$now = new \DateTimeImmutable('now');
$monday = $now->modify('monday this week');
return [$monday->setTime(0, 0, 0), $now->setTime(23, 59, 59)];
}
private static function getMonthToDateRange(): array
{
$now = new \DateTimeImmutable('now');
$first = $now->modify('first day of this month');
return [$first->setTime(0, 0, 0), $now->setTime(23, 59, 59)];
}
private static function clampToCriteriaWindow(goals_criteria $criteria, \DateTimeImmutable $start, \DateTimeImmutable $end): ?array
{
$sTs = $start->getTimestamp();
$eTs = $end->getTimestamp();
if ($criteria->start instanceof \DateTimeInterface) {
$cs = $criteria->start->getTimestamp();
if ($sTs < $cs) { $sTs = $cs; }
}
if ($criteria->end instanceof \DateTimeInterface) {
$ce = $criteria->end->getTimestamp();
if ($eTs > $ce) { $eTs = $ce; }
}
if ($sTs > $eTs) { return null; }
// Preserve the timezone of provided start/end
$tz = $start->getTimezone();
$s = (new \DateTimeImmutable('@' . $sTs))->setTimezone($tz);
$e = (new \DateTimeImmutable('@' . $eTs))->setTimezone($tz);
return [$s, $e];
}
/**