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}`.
This commit is contained in:
Jeppe Bundgaard
2026-01-27 09:43:23 +01:00
parent 1a5084891d
commit 5536475102
@@ -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);
}
}