From 55364751025351f6db897f0de9d3fde6b23d76ab Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Tue, 27 Jan 2026 09:43:23 +0100 Subject: [PATCH] Add multi-line progress alert rendering and support for department, customer, and product names - Introduce `renderDanishPeriodSummary` to display detailed progress (daily, weekly, monthly) with optional targets. - Add `commaNamesFromObjects` helper to extract and format names for departments, customers, and products. - Update alert composition logic to differentiate between single-line and multi-line rendering. - Extend `progress_alert_format` to support new tokens `{department_names}`, `{customer_names}`, and `{product_names}`. --- .../goals_progress_alert_renderer.php | 142 ++++++++++++++++-- 1 file changed, 131 insertions(+), 11 deletions(-) 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 index 921d7ded..2de9379b 100644 --- a/services/nginx/app/modules/goals/services/goals_progress_alert_renderer.php +++ b/services/nginx/app/modules/goals/services/goals_progress_alert_renderer.php @@ -24,6 +24,11 @@ class goals_progress_alert_renderer $timeframe = ($tfStart && $tfEnd) ? "$tfStart → $tfEnd" : ''; $deptList = implode(',', $criteria->departments?->listIDs() ?? []); + // Prepare readable names for fields + $deptNames = self::commaNamesFromObjects($criteria->departments?->list() ?? [], 'department'); + $customerNames = self::commaNamesFromObjects($criteria->users?->list() ?? [], 'user'); + $productNames = self::commaNamesFromObjects($criteria->products?->list() ?? [], 'product'); + $prefix = match ($criteria->progress_alert_style) { Style::DEPARTMENT_COMPARE => '[Dept compare] ', Style::COLLECTIVE => '[Collective] ', @@ -32,24 +37,39 @@ class goals_progress_alert_renderer }; $body = match ($criteria->progress_alert_progress_type) { + // New DK multi-line formats for COUNT_ONLY and ALL + PType::COUNT_ONLY => self::renderDanishPeriodSummary($criteria, includeTargets: false, + header: [ + $deptNames !== '' ? "Afdeling: $deptNames" : null, + $customerNames !== '' ? "Kunde: $customerNames" : null, + $productNames !== '' ? "Produkt: $productNames" : null, + ]), + PType::ALL => self::renderDanishPeriodSummary($criteria, includeTargets: $target > 0, + header: [ + $deptNames !== '' ? "Afdeling: $deptNames" : null, + $customerNames !== '' ? "Kunde: $customerNames" : null, + $productNames !== '' ? "Produkt: $productNames" : null, + ]), + // Legacy single-line fallbacks 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); + // Keep old composition only for legacy one-line body strings + $composed = $body; + if (!str_contains($body, "\n")) { + $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} + // Supported tokens: {label}, {percent}, {count}, {target}, {timeframe}, {departments}, {prefix}, {body}, {department_names}, {customer_names}, {product_names} if (is_string($criteria->progress_alert_format) && $criteria->progress_alert_format !== '') { $tmpl = $criteria->progress_alert_format; $map = [ @@ -61,6 +81,9 @@ class goals_progress_alert_renderer '{departments}' => $deptList, '{prefix}' => $prefix, '{body}' => $body, + '{department_names}' => $deptNames, + '{customer_names}' => $customerNames, + '{product_names}' => $productNames, ]; $composed = strtr($tmpl, $map); } @@ -86,4 +109,101 @@ class goals_progress_alert_renderer } return 0; } + + private static function renderDanishPeriodSummary(goals_criteria $criteria, bool $includeTargets, array $header = []): string + { + // 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); + } + + // 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); + } + + return implode("\n", $lines); + } + + private static function progressForWindow(goals_criteria $criteria, \DateTimeInterface $start, \DateTimeInterface $end): int + { + // Clone criteria and adjust timeframe + $clone = clone $criteria; + $clone->start = (clone \DateTime::createFromInterface($start)); + $clone->end = (clone \DateTime::createFromInterface($end)); + return self::getProgress($clone); + } + + /** + * Try to extract comma-separated names from domain objects using common patterns. + * Fallback to IDs or customer numbers when necessary. + * @param array $objects + * @param string $type one of: department|user|product + */ + private static function commaNamesFromObjects(array $objects, string $type): string + { + $names = []; + foreach ($objects as $obj) { + try { + switch ($type) { + case 'department': + // departments_o has ->name object_property + if (isset($obj->name)) { + $val = (string)$obj->name->value(); + if ($val !== '') { $names[] = $val; break; } + } + // fallback to id + if (isset($obj->id)) { $names[] = (string)$obj->id; } + break; + case 'user': + // users_o may have display_name or economic customer name cache + if (isset($obj->display_name)) { + $val = (string)$obj->display_name->value(); + if ($val !== '') { $names[] = $val; break; } + } + if (method_exists($obj, 'getCustomerName') && isset($obj->customer_number)) { + $num = (int)$obj->customer_number->value(); + $name = $obj->getCustomerName($num); + if (is_string($name) && $name !== '') { $names[] = $name; break; } + } + if (isset($obj->customer_number)) { $names[] = (string)$obj->customer_number->value(); } + break; + case 'product': + if (isset($obj->name)) { + $val = (string)$obj->name->value(); + if ($val !== '') { $names[] = $val; break; } + } + if (isset($obj->id)) { $names[] = (string)$obj->id; } + break; + } + } catch (\Throwable) { + // ignore and continue + } + } + return implode(', ', $names); + } }