From b3fa5e020dd803766e965655b5bc88d2597e2131 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 29 Jun 2026 10:49:06 +0200 Subject: [PATCH 1/8] Add help text for all dashboard elements --- .../controllers/dashboard_viz_controller.js | 2 +- assets/styles/app.css | 9 +++-- templates/dashboard/index.html.twig | 38 ++++++++++++++----- translations/messages.da.yaml | 16 +++++--- translations/messages.en.yaml | 16 +++++--- 5 files changed, 57 insertions(+), 24 deletions(-) diff --git a/assets/controllers/dashboard_viz_controller.js b/assets/controllers/dashboard_viz_controller.js index 9c86ec1..3c49b06 100644 --- a/assets/controllers/dashboard_viz_controller.js +++ b/assets/controllers/dashboard_viz_controller.js @@ -308,7 +308,7 @@ export default class extends Controller { el.innerHTML = ""; if (!this.viz.collaboration.length) { el.innerHTML = - '

Ingen tværgående temaer endnu — kategorisér initiativer for at finde sammenfald.

'; + '

Ingen tværgående områder endnu — kategorisér initiativer for at finde sammenfald.

'; return; } // Hold the entrance paused until the panel scrolls into view; once seen, diff --git a/assets/styles/app.css b/assets/styles/app.css index 7926893..7c93f54 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -475,9 +475,9 @@ h3 { padding: var(--itk-space-4) var(--itk-space-5); border-bottom: 1px solid var(--itk-slate-100); display: flex; - align-items: center; - justify-content: space-between; - gap: var(--itk-space-3); + flex-direction: column; + align-items: flex-start; + gap: var(--itk-space-1); } .card__title { @@ -573,7 +573,8 @@ h3 { .card__sub { font-size: var(--itk-text-sm); color: var(--itk-slate-500); - margin: 0 0 var(--itk-space-4); + margin: 0; + line-height: 1.4; } .feed-scroll { diff --git a/templates/dashboard/index.html.twig b/templates/dashboard/index.html.twig index 1b6dcfe..e9ad4fc 100644 --- a/templates/dashboard/index.html.twig +++ b/templates/dashboard/index.html.twig @@ -26,17 +26,19 @@
{{ 'dashboard.themes_heatmap'|trans }} +

{{ 'dashboard.themes_heatmap_sub'|trans }}

-

{{ 'dashboard.themes_heatmap_sub'|trans }}

-
{{ 'dashboard.collaboration'|trans }}
-
+
+ {{ 'dashboard.collaboration'|trans }}

{{ 'dashboard.collaboration_sub'|trans }}

+
+
@@ -58,32 +60,50 @@
-
{{ 'dashboard.status_by_dept'|trans }}
+
+ {{ 'dashboard.status_by_dept'|trans }} +

{{ 'dashboard.status_by_dept_sub'|trans }}

+
-
{{ 'dashboard.by_status'|trans }}
+
+ {{ 'dashboard.by_status'|trans }} +

{{ 'dashboard.by_status_sub'|trans }}

+
-
{{ 'dashboard.funding_sources'|trans }}
+
+ {{ 'dashboard.funding_sources'|trans }} +

{{ 'dashboard.funding_sources_sub'|trans }}

+
-
{{ 'dashboard.budget_by_dept'|trans }}
+
+ {{ 'dashboard.budget_by_dept'|trans }} +

{{ 'dashboard.budget_by_dept_sub'|trans }}

+
-
{{ 'dashboard.timeline'|trans }}
+
+ {{ 'dashboard.timeline'|trans }} +

{{ 'dashboard.timeline_sub'|trans }}

+
-
{{ 'dashboard.reach'|trans }}
+
+ {{ 'dashboard.reach'|trans }} +

{{ 'dashboard.reach_sub'|trans }}

+
diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index 6acf675..f9a3232 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -131,20 +131,26 @@ dashboard: departments_involved: Afdelinger involveret departments_involved_hint: med mindst ét initiativ collaboration_count: Mulige samarbejder - collaboration_count_hint: temaer på tværs af afdelinger + collaboration_count_hint: områder på tværs af afdelinger by_status: Fordeling på status + by_status_sub: Alle initiativer fordelt på deres aktuelle status. recent: Senest oprettede no_status: Uden status empty: Der er endnu ikke oprettet nogen initiativer. - themes_heatmap: Temaer på tværs af afdelinger - themes_heatmap_sub: Mørkere = flere initiativer. Markerede temaer går på tværs af mindst tre afdelinger. + themes_heatmap: Områder på tværs af afdelinger + themes_heatmap_sub: Antal initiativer pr. område i hver afdeling. collaboration: Muligt samarbejde - collaboration_sub: Initiativer i forskellige afdelinger med fælles tema. + collaboration_sub: Initiativer i forskellige afdelinger med fælles område. status_by_dept: Status pr. afdeling + status_by_dept_sub: Initiativernes status fordelt på hver afdeling. funding_sources: Finansieringskilder + funding_sources_sub: Antal initiativer pr. finansieringskilde. budget_by_dept: Budget pr. afdeling - reach: Temaer med flest afdelinger + budget_by_dept_sub: Samlet budget for initiativerne i hver afdeling. + reach: Områder med flest afdelinger + reach_sub: Hvor mange afdelinger hvert område er repræsenteret i. timeline: Tidslinje + timeline_sub: Hvert initiativs tidsperiode på en tidslinje. initiative: title: Titel diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index 92ae714..9f0e070 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -131,20 +131,26 @@ dashboard: departments_involved: Departments involved departments_involved_hint: with at least one initiative collaboration_count: Possible collaborations - collaboration_count_hint: themes across departments + collaboration_count_hint: areas across departments by_status: By status + by_status_sub: All initiatives grouped by their current status. recent: Recently created no_status: No status empty: No initiatives have been created yet. - themes_heatmap: Themes across departments - themes_heatmap_sub: Darker = more initiatives. Marked themes span at least three departments. + themes_heatmap: Areas across departments + themes_heatmap_sub: Number of initiatives per area in each department. Darker = more; marked areas span at least three departments. collaboration: Possible collaboration - collaboration_sub: Initiatives in different departments sharing a theme. + collaboration_sub: Initiatives in different departments sharing an area. status_by_dept: Status by department + status_by_dept_sub: Initiative statuses broken down by department. funding_sources: Funding sources + funding_sources_sub: Number of initiatives per funding source. budget_by_dept: Budget by department - reach: Themes by department reach + budget_by_dept_sub: Total budget of the initiatives in each department. + reach: Areas by department reach + reach_sub: How many departments each area appears in. timeline: Timeline + timeline_sub: Each initiative's time period on a timeline. initiative: title: Title From 7d087df4072ff5d05b0d44573ccbd14c5afeb7bf Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 29 Jun 2026 11:57:32 +0200 Subject: [PATCH 2/8] =?UTF-8?q?Move=20Aktivitet=20up,=20and=20add=20Dit=20?= =?UTF-8?q?igangv=C3=A6rende=20arbejde=20boks,=20showing=20the=20users=20c?= =?UTF-8?q?urrent=20unfinished=20work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/styles/app.css | 87 +++++++++++++++++++++++++ src/Controller/DashboardController.php | 8 ++- src/Entity/Initiative.php | 18 +++++ src/Repository/ContactRepository.php | 16 ++++- src/Repository/InitiativeRepository.php | 22 +++++-- templates/activity/_item.html.twig | 18 ++--- templates/dashboard/index.html.twig | 80 ++++++++++++++++++----- tests/Unit/Entity/InitiativeTest.php | 20 ++++++ translations/messages.da.yaml | 13 +++- translations/messages.en.yaml | 11 ++++ 10 files changed, 260 insertions(+), 33 deletions(-) diff --git a/assets/styles/app.css b/assets/styles/app.css index 7c93f54..12dbd89 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -471,6 +471,24 @@ h3 { padding: var(--itk-space-5); } +.card--stretch { + align-self: stretch; + display: flex; + flex-direction: column; +} + +.feed-fill { + position: relative; + flex: 1; + min-height: 230px; +} + +.feed-fill .feed-scroll { + position: absolute; + inset: 0; + height: auto; +} + .card__header { padding: var(--itk-space-4) var(--itk-space-5); border-bottom: 1px solid var(--itk-slate-100); @@ -845,6 +863,75 @@ h3 { color: var(--itk-slate-500); } +a.recent-item { + color: inherit; + text-decoration: none; + transition: background-color 0.15s ease; +} + +a.recent-item:hover { + background-color: color-mix(in srgb, var(--itk-blue) 9%, transparent); +} + +.recent-item__cta { + display: none; + align-items: center; + gap: 4px; + font-size: var(--itk-text-xs); + font-weight: 600; + color: var(--itk-blue); + white-space: nowrap; +} + +a.recent-item:hover .badge { + display: none; +} + +a.recent-item:hover .recent-item__cta { + display: inline-flex; +} + +.recent-item__body { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.recent-item__body .recent-item__title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.todo-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--itk-space-2); + height: 100%; + padding: var(--itk-space-5); + text-align: center; +} + +.todo-empty__icon { + color: var(--itk-green); + line-height: 0; +} + +.todo-empty__title { + font-weight: 600; + color: var(--itk-ink); +} + +.todo-empty__hint { + max-width: 34ch; + margin: 0 0 var(--itk-space-2); + font-size: var(--itk-text-sm); + color: var(--itk-slate-500); +} + .table-wrap { overflow-x: auto; } diff --git a/src/Controller/DashboardController.php b/src/Controller/DashboardController.php index 8d63386..2053b0d 100644 --- a/src/Controller/DashboardController.php +++ b/src/Controller/DashboardController.php @@ -4,6 +4,8 @@ namespace App\Controller; +use App\Entity\User; +use App\Repository\ContactRepository; use App\Repository\InitiativeRepository; use App\Service\DashboardData; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -13,11 +15,15 @@ class DashboardController extends AbstractController { #[Route('/', name: 'app_dashboard', methods: ['GET'])] - public function index(InitiativeRepository $initiatives, DashboardData $dashboardData): Response + public function index(InitiativeRepository $initiatives, ContactRepository $contacts, DashboardData $dashboardData): Response { + $user = $this->getUser(); + return $this->render('dashboard/index.html.twig', [ 'recent' => $initiatives->findRecent(8), 'viz' => $dashboardData->build(), + 'unfinishedInitiatives' => $user instanceof User ? $initiatives->findUnfinishedListByCreator($user) : [], + 'incompleteContacts' => $user instanceof User ? $contacts->findIncompleteListByCreator($user) : [], ]); } } diff --git a/src/Entity/Initiative.php b/src/Entity/Initiative.php index d9c48bd..91e5649 100644 --- a/src/Entity/Initiative.php +++ b/src/Entity/Initiative.php @@ -501,6 +501,24 @@ public function getCompletionPercentage(): int return (int) round(\count(array_filter($checks)) / \count($checks) * 100); } + /** + * Whether the initiative has been edited since it was created — drives the + * "updated" vs "created" label in the dashboard activity feed. Only counts as + * an edit once it's more than a day past creation, so the initial save and any + * same-day tweaks still read as "created". + */ + public function wasUpdatedAfterCreation(): bool + { + $created = $this->getCreatedAt(); + $updated = $this->getUpdatedAt(); + + if (null === $created || null === $updated) { + return false; + } + + return $updated->getTimestamp() - $created->getTimestamp() > 60 * 60 * 24; + } + public function __toString(): string { return (string) $this->title; diff --git a/src/Repository/ContactRepository.php b/src/Repository/ContactRepository.php index a7cd004..8aa5f2f 100644 --- a/src/Repository/ContactRepository.php +++ b/src/Repository/ContactRepository.php @@ -63,6 +63,18 @@ public function findOrCreate(string $name): Contact * Used by the mascot to nudge them to fill in the rest. */ public function findIncompleteByCreator(User $user): ?Contact + { + return $this->findIncompleteListByCreator($user, 1)[0] ?? null; + } + + /** + * The user's contacts that still lack an email (created name-only and not yet + * finished), most recent first, capped at $limit. Surfaced on the dashboard + * as outstanding work. + * + * @return Contact[] + */ + public function findIncompleteListByCreator(User $user, int $limit = 6): array { // createdBy is a ManyToOne to the UserInterface (resolved to User via // resolve_target_entities); binding the entity to a ULID FK doesn't match, @@ -72,8 +84,8 @@ public function findIncompleteByCreator(User $user): ?Contact ->andWhere("(c.email IS NULL OR c.email = '')") ->setParameter('user', $user->getId(), 'ulid') ->orderBy('c.createdAt', 'DESC') - ->setMaxResults(1) + ->setMaxResults($limit) ->getQuery() - ->getOneOrNullResult(); + ->getResult(); } } diff --git a/src/Repository/InitiativeRepository.php b/src/Repository/InitiativeRepository.php index af4636e..18ce917 100644 --- a/src/Repository/InitiativeRepository.php +++ b/src/Repository/InitiativeRepository.php @@ -180,10 +180,21 @@ public function countByCreator(User $user): int /** * The creator's least-complete initiative that isn't fully filled in yet, or - * null if none are outstanding. Completion is computed in PHP (not a stored - * column), so this scans only the creator's 50 most recent initiatives. + * null if none are outstanding. */ public function findUnfinishedByCreator(User $user): ?Initiative + { + return $this->findUnfinishedListByCreator($user, 1)[0] ?? null; + } + + /** + * The creator's incomplete initiatives (completion below 100 %), least-complete + * first, capped at $limit. Completion is computed in PHP (not a stored column), + * so this scans only the creator's 50 most recent initiatives. + * + * @return Initiative[] + */ + public function findUnfinishedListByCreator(User $user, int $limit = 6): array { // See countByCreator: match the raw ULID FK, not the entity. $initiatives = $this->createQueryBuilder('i') @@ -204,7 +215,7 @@ public function findUnfinishedByCreator(User $user): ?Initiative static fn (Initiative $a, Initiative $b): int => $a->getCompletionPercentage() <=> $b->getCompletionPercentage(), ); - return $unfinished[0] ?? null; + return \array_slice($unfinished, 0, $limit); } /** @@ -232,12 +243,15 @@ public function countByStatus(): array } /** + * Most recently touched initiatives (created or edited) for the activity feed, + * newest first. Ordered by updatedAt so an edit resurfaces the initiative. + * * @return Initiative[] */ public function findRecent(int $limit = 5): array { return $this->createQueryBuilder('i') - ->orderBy('i.createdAt', 'DESC') + ->orderBy('i.updatedAt', 'DESC') ->setMaxResults($limit) ->getQuery() ->getResult(); diff --git a/templates/activity/_item.html.twig b/templates/activity/_item.html.twig index 8adc949..8afa337 100644 --- a/templates/activity/_item.html.twig +++ b/templates/activity/_item.html.twig @@ -1,9 +1,9 @@ -
-
- {% if url %}{{ title }}{% else %}{{ title }}{% endif %} -
- {%- if actor %}{{ actor }} · {% endif -%} - {{ ('activity.action.' ~ action)|trans }} · {{ at|date('d.m.Y H:i') }} -
-
-
+{% set row_tag = url ? 'a' : 'div' %} +<{{ row_tag }} id="activity-item-{{ id }}" class="recent-item"{% if url %} href="{{ url }}"{% endif %}> + + {{ title }} + {%- if actor %}{{ actor }} · {% endif -%}{{ at|date('d.m.Y H:i') }} + + {{ ('activity.action.' ~ action)|trans|capitalize }} + {% if url %}{{ 'action.view'|trans }} →{% endif %} + diff --git a/templates/dashboard/index.html.twig b/templates/dashboard/index.html.twig index e9ad4fc..b0e2ffe 100644 --- a/templates/dashboard/index.html.twig +++ b/templates/dashboard/index.html.twig @@ -23,6 +23,70 @@
+
+
+ {{ 'dashboard.unfinished.title'|trans }} +

{{ 'dashboard.unfinished.sub'|trans }}

+
+
+ {% if unfinishedInitiatives is empty and incompleteContacts is empty %} +
+
{{ 'dashboard.unfinished.empty_title'|trans }}
+
+ {% else %} + {% for initiative in unfinishedInitiatives %} + + + {{ initiative.title }} + {{ 'dashboard.unfinished.initiative'|trans }} + + {{ 'dashboard.unfinished.percent_done'|trans({'%percent%': initiative.completionPercentage}) }} + {{ 'dashboard.unfinished.cta'|trans }} → + + {% endfor %} + {% for contact in incompleteContacts %} + + + {{ contact.name }} + {{ 'dashboard.unfinished.contact'|trans }} + + {{ 'dashboard.unfinished.contact_badge'|trans }} + {{ 'dashboard.unfinished.cta'|trans }} → + + {% endfor %} + {% endif %} +
+
+ +
+
+ {{ 'activity.title'|trans }} +

{{ 'activity.subtitle'|trans }}

+
+
+
+ {% if recent is empty %} +
+
{{ 'activity.empty'|trans }}
+
+ {% else %} + {% for initiative in recent %} + {% set updated = initiative.wasUpdatedAfterCreation %} + {% set actor = updated ? initiative.modifiedBy : initiative.createdBy %} + {{ include('activity/_item.html.twig', { + id: initiative.id, + action: updated ? 'updated' : 'created', + title: initiative.title, + url: path('app_initiative_show', {id: initiative.id}), + actor: actor ? actor.name : null, + at: updated ? initiative.updatedAt : initiative.createdAt, + }) }} + {% endfor %} + {% endif %} +
+
+
+
{{ 'dashboard.themes_heatmap'|trans }} @@ -43,22 +107,6 @@
-
-
{{ 'activity.title'|trans }}
-
- {% for initiative in recent %} - {{ include('activity/_item.html.twig', { - id: initiative.id, - action: 'created', - title: initiative.title, - url: path('app_initiative_show', {id: initiative.id}), - actor: initiative.createdBy ? initiative.createdBy.name : null, - at: initiative.createdAt, - }) }} - {% endfor %} -
-
-
{{ 'dashboard.status_by_dept'|trans }} diff --git a/tests/Unit/Entity/InitiativeTest.php b/tests/Unit/Entity/InitiativeTest.php index 97a5f2c..b522e32 100644 --- a/tests/Unit/Entity/InitiativeTest.php +++ b/tests/Unit/Entity/InitiativeTest.php @@ -95,6 +95,26 @@ public function testCompletionPercentage(): void self::assertSame(100, $full->getCompletionPercentage()); } + public function testWasUpdatedAfterCreation(): void + { + // No timestamps yet (entity not persisted) — treated as not updated. + self::assertFalse((new Initiative())->wasUpdatedAfterCreation()); + + $created = new \DateTimeImmutable('2025-01-01 10:00:00'); + + // Edited within a day of creation: still reads as "created". + $fresh = new Initiative(); + $fresh->setCreatedAt($created); + $fresh->setUpdatedAt($created->modify('+5 hours')); + self::assertFalse($fresh->wasUpdatedAfterCreation()); + + // Edited more than a day after creation. + $edited = new Initiative(); + $edited->setCreatedAt($created); + $edited->setUpdatedAt($created->modify('+2 days')); + self::assertTrue($edited->wasUpdatedAfterCreation()); + } + public function testFundingRoundTrip(): void { $initiative = (new Initiative())->setFunding([Funding::MunicipalBudget, Funding::EuFunds]); diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index f9a3232..8430eac 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -16,7 +16,7 @@ mascot: blanks: "“%title%” er %percent% % udfyldt — der mangler stadig et par felter. 📝" ending: "“%title%” er %percent% % færdig. Giv den en værdig afslutning! 🏁" strong: "“%title%” er %percent% % færdig — lad os tage den sidste bid! 💪" - finish_cta: Gør færdig + finish_cta: Gå til redigering contact: details: "Kontaktpersonen “%name%” som du har oprettet mangler stadig oplysninger — Vil du gøre den færdig?" quick: "Du tilføjede “%name%” i farten — vil du udfylde resten? ✏️" @@ -60,6 +60,8 @@ autosave: activity: title: Aktivitet + subtitle: Seneste aktivitet på platformen + empty: Ingen aktivitet at vise action: created: oprettede updated: opdaterede @@ -151,6 +153,15 @@ dashboard: reach_sub: Hvor mange afdelinger hvert område er repræsenteret i. timeline: Tidslinje timeline_sub: Hvert initiativs tidsperiode på en tidslinje. + unfinished: + title: Dit igangværende arbejde + sub: Dine initiativer og kontaktpersoner der mangler de sidste detaljer. + initiative: Initiativ + contact: Kontaktperson + contact_badge: Mangler oplysninger + percent_done: "%percent% % udfyldt" + cta: Gå til redigering + empty_title: Intet igangværende arbejde initiative: title: Titel diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index 9f0e070..9b139ae 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -60,6 +60,8 @@ autosave: activity: title: Activity + subtitle: Latest activity on the platform + empty: No activity to show action: created: created updated: updated @@ -151,6 +153,15 @@ dashboard: reach_sub: How many departments each area appears in. timeline: Timeline timeline_sub: Each initiative's time period on a timeline. + unfinished: + title: Your work + sub: Your initiatives and contacts that are still missing details. + initiative: Initiative + contact: Contact + contact_badge: Missing details + percent_done: "%percent% % filled in" + cta: Finish + empty_title: You're all caught up initiative: title: Title From ea77c6ac06b1d6269c42cbb003d610062c14f3fe Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 29 Jun 2026 12:12:15 +0200 Subject: [PATCH 3/8] Updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfae926..d16fbb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* [PR-18](https://github.com/itk-dev/itk-project-database/pull/18) + Rework the dashboard with an outstanding-work panel and a redesigned activity + feed, add help text to every graph, and fix the mascot's finish nudges. * [PR-16](https://github.com/itk-dev/itk-project-database/pull/16) Turn the strategies and tags fields into a searchable, shared tag pool where new entries are capitalised and reused as suggestions. From 4a036edc7e2ac756cbd96e0dec6972b81af6fae9 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Mon, 29 Jun 2026 12:20:50 +0200 Subject: [PATCH 4/8] =?UTF-8?q?Display=20default=20empty=20text=20in=20Omr?= =?UTF-8?q?=C3=A5der=20p=C3=A5=20tv=C3=A6rs=20af=20afdelinger,=20until=20b?= =?UTF-8?q?oth=20afdelinger=20and=20omr=C3=A5der=20exists?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/dashboard_viz_controller.js | 19 +++++++++++++++++++ assets/styles/app.css | 6 ++++++ templates/dashboard/index.html.twig | 2 +- translations/messages.da.yaml | 1 + translations/messages.en.yaml | 1 + 5 files changed, 28 insertions(+), 1 deletion(-) diff --git a/assets/controllers/dashboard_viz_controller.js b/assets/controllers/dashboard_viz_controller.js index 3c49b06..bd24e8e 100644 --- a/assets/controllers/dashboard_viz_controller.js +++ b/assets/controllers/dashboard_viz_controller.js @@ -65,6 +65,10 @@ export default class extends Controller { "timeline", ]; + static values = { + empty: String, + }; + connect() { this.reduce = window.matchMedia( "(prefers-reduced-motion: reduce)", @@ -221,6 +225,18 @@ export default class extends Controller { buildHeatmap() { const d = this.viz; const el = this.heatmapTarget; + + // The heatmap only makes sense once there are both rows (departments) and + // columns (areas) to cross; until then show a placeholder, not bare headers. + if (!d.areas.length || !d.departments.length) { + el.style.gridTemplateColumns = ""; + el.classList.remove("heat--paused"); + el.innerHTML = `

${this.esc(this.emptyValue)}

`; + this.heatColHeads = []; + this.heatCells = []; + return; + } + el.style.gridTemplateColumns = `minmax(120px, 168px) repeat(${d.areas.length}, minmax(40px, 1fr))`; if (!this.reduce) { el.classList.add("heat--paused"); @@ -254,6 +270,9 @@ export default class extends Controller { updateHeatmap(initial = false) { const d = this.viz; + if (!d.areas.length || !d.departments.length) { + return; + } let max = 1; d.heatmap.forEach((row) => row.forEach((v) => { diff --git a/assets/styles/app.css b/assets/styles/app.css index 12dbd89..c26b4d4 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -628,6 +628,12 @@ h3 { min-width: 560px; } +.heat-empty { + margin: 0; + color: var(--itk-slate-500); + font-size: var(--itk-text-sm); +} + .heat__collabel { display: flex; flex-direction: column; diff --git a/templates/dashboard/index.html.twig b/templates/dashboard/index.html.twig index b0e2ffe..435a044 100644 --- a/templates/dashboard/index.html.twig +++ b/templates/dashboard/index.html.twig @@ -3,7 +3,7 @@ {% block title %}{{ 'dashboard.title'|trans }} · {{ 'app.name'|trans }}{% endblock %} {% block body %} -
+