From 4ba822a555261f371c93e75c82e95f69d518d1be Mon Sep 17 00:00:00 2001 From: Gloria <55953099+gloriafolaron@users.noreply.github.com> Date: Tue, 21 Jul 2026 22:05:57 -0400 Subject: [PATCH 1/7] feat(reports): 'Build your Logic Model' empty-state on the report LM page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a strategy has no Logic Model canvas, the report's Logic Model page showed a bare one-line string. Existing users who never built a logic model (but whose report depends on it) got a dead end. Replace it with an inviting empty-state: what a logic model unlocks + a 'Build a Logic Model' CTA to /logicmodelcanvas/showCanvas (the strategy's canvas, which shows the 'Start your Logic Model' 3-door screen incl. the reverse 'build from what's already here' flow). CTA is strategy-scoped; program scope keeps the explanatory copy only. Note: only reachable once a user already has a type='strategy' project — the upstream gap (flat-project users can't reach the strategy report at all) is separate and needs a product decision. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01GYSPNgLdUwEnxgYqTxMc85 --- .../partials/stakeholder/page-lm.blade.php | 26 ++++++++++++++++++- app/Language/en-US.ini | 4 +++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php b/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php index dd31ab0af0..04b553850c 100644 --- a/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php +++ b/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php @@ -144,7 +144,31 @@ @if (! $hasLM) -
{{ __('stakeholder.lm.no_canvas') }}
+ +
+
+ +
{{ __('stakeholder.lm.empty_title') }}
+
{{ __('stakeholder.lm.empty_body') }}
+ @if (($scope ?? '') === 'strategy') + + {{ __('stakeholder.lm.empty_cta') }} + +
{{ __('stakeholder.lm.empty_hint') }}
+ @endif +
+
@else @php $stages = $logicModel['coverageMatrix']['stages'] ?? []; diff --git a/app/Language/en-US.ini b/app/Language/en-US.ini index 0cd68cd6e9..416021edb5 100644 --- a/app/Language/en-US.ini +++ b/app/Language/en-US.ini @@ -3002,6 +3002,10 @@ stakeholder.kpi.this_period = "this period" stakeholder.overview.coming_title = "Peak this period · Needs attention · ToC narrative · Theory-health strip" stakeholder.overview.coming_hint = "The recommend-and-override hero, execution-lens needs-attention block, Theory-of-Change sentence, and theory-health strip land next." stakeholder.lm.no_canvas = "No Logic Model canvas authored yet. Build one on the Board to see it read out here." +stakeholder.lm.empty_title = "Build your Logic Model" +stakeholder.lm.empty_body = "A Logic Model connects your programs, goals, and milestones into one story — what you do, what changes, and why it matters. It powers this page and the coverage read-out." +stakeholder.lm.empty_cta = "Build a Logic Model" +stakeholder.lm.empty_hint = "We'll scan the work already in this strategy and suggest what maps to each stage — you pick what fits." stakeholder.lm.coming_title = "5-stage read-out with task-card standard + connection-health badges" stakeholder.lm.coming_hint = "Cards per stage with assumption/hypothesis, linked projects, and a fold-out \"this period\" read-out. Connection badges between stages from zp_logicmodel_health." stakeholder.rc.coming_title = "Resource summary + resource-backed coverage matrix" From 26c4765102690d1838a4b71a9e8f42cbd14cc992 Mon Sep 17 00:00:00 2001 From: Gloria <55953099+gloriafolaron@users.noreply.github.com> Date: Wed, 22 Jul 2026 07:07:06 -0400 Subject: [PATCH 2/7] fix(reports): LM empty-state triggers on empty board, not just missing board MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validation surfaced that StrategyReport gates logicModel on board EXISTENCE, not item count — so a blank Logic Model board (0 items) makes $hasLM true and renders a hollow skeleton (columns, empty stages, no prompt) instead of the empty-state. The reverse flow creates a board on start, so that hollow state is reachable. Key the empty-state on item count instead: compute $lmHasContent (any stage carries items) and show the 'Build your Logic Model' populate-from-your-work CTA whenever the model is empty — whether no board exists or a board exists with nothing in it. Unifies the three states (no board / blank board / filled) into one coherent 'here's how to populate it from what you already have' story. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01GYSPNgLdUwEnxgYqTxMc85 --- .../partials/stakeholder/page-lm.blade.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php b/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php index 04b553850c..d1b60787ff 100644 --- a/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php +++ b/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php @@ -143,7 +143,24 @@ .rd-scope .p2-drift b{color:var(--rd-text-1);font-weight:600;} -@if (! $hasLM) +@php + // Empty-state trigger is keyed on ITEM COUNT, not just board existence. + // A blank Logic Model board (0 items) makes $hasLM true but leaves the + // read-out hollow; the reverse flow itself creates a board on start, so + // gating only on board-existence would strand users in an empty skeleton. + // Treat "no board" and "board with no items" identically — both land on + // the populate-from-your-work empty-state below. + $lmStages = $hasLM ? ($logicModel['coverageMatrix']['stages'] ?? []) : []; + $lmHasContent = false; + foreach ($lmStages as $lmStage) { + if (count($lmStage['items'] ?? []) > 0) { + $lmHasContent = true; + break; + } + } +@endphp + +@if (! $lmHasContent)
- -
{{ __('stakeholder.lm.empty_title') }}
+ +

{{ __('stakeholder.lm.empty_title') }}

{{ __('stakeholder.lm.empty_body') }}
@if (($scope ?? '') === 'strategy') - {{ __('stakeholder.lm.empty_cta') }} + {{ __('stakeholder.lm.empty_cta') }}
{{ __('stakeholder.lm.empty_hint') }}
@endif From 773525eecb6561edd22af3f7d1148cef30119070 Mon Sep 17 00:00:00 2001 From: Gloria <55953099+gloriafolaron@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:29:06 -0400 Subject: [PATCH 4/7] design(reports): tinted panel + 'we found your work' line on the LM empty-state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tinted zone behind the card (soft mint gradient) so the white invitation lifts off the page — closer to the onboarding sample. - 'We found N programs · M projects in this strategy' line above the CTA, derived from $programRows (type='program' count + summed projectCount). Strategy scope only; self-hides when there's no linked work. Makes the invitation concrete ('we see your existing work'), matching the reverse flow's 'we found existing work' banner. - deck passes $programRows into page-lm; 5 new i18n keys. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01GYSPNgLdUwEnxgYqTxMc85 --- .../partials/stakeholder/deck.blade.php | 2 +- .../partials/stakeholder/page-lm.blade.php | 54 +++++++++++++++---- app/Language/en-US.ini | 5 ++ 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/app/Domain/Reports/Templates/partials/stakeholder/deck.blade.php b/app/Domain/Reports/Templates/partials/stakeholder/deck.blade.php index 0127a9c427..fd35c7fe9c 100644 --- a/app/Domain/Reports/Templates/partials/stakeholder/deck.blade.php +++ b/app/Domain/Reports/Templates/partials/stakeholder/deck.blade.php @@ -375,7 +375,7 @@ class="rd-picker-opt @if ($period->preset === ReportPeriod::PRESET_NEXT_QUARTER) {{-- ═══ Page 2 — Logic Model read-out ═════════════════ --}}
- @include('reports::partials.stakeholder.page-lm', compact('logicModel', 'hasLM', 'report')) + @include('reports::partials.stakeholder.page-lm', compact('logicModel', 'hasLM', 'report', 'programRows'))
{{-- ═══ Page 3 — Resources & Coverage ═════════════════ --}} diff --git a/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php b/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php index cc27989685..9ccf0423eb 100644 --- a/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php +++ b/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php @@ -158,13 +158,37 @@ break; } } + + // Existing linked work in this strategy, for the empty-state's "we found + // your work" line. programRows carry type + projectCount; program rows + // count as programs, and projectCount sums to total leaf projects (direct + // + under programs). Empty at program scope, so the line self-hides there. + $lmProgramCount = 0; + $lmProjectCount = 0; + foreach ($programRows ?? [] as $lmRow) { + if (($lmRow['type'] ?? '') === 'program') { + $lmProgramCount++; + } + $lmProjectCount += (int) ($lmRow['projectCount'] ?? 0); + } + $lmFoundParts = []; + if ($lmProgramCount > 0) { + $lmFoundParts[] = $lmProgramCount.' '.__($lmProgramCount === 1 ? 'stakeholder.lm.found_program' : 'stakeholder.lm.found_programs'); + } + if ($lmProjectCount > 0) { + $lmFoundParts[] = $lmProjectCount.' '.__($lmProjectCount === 1 ? 'stakeholder.lm.found_project' : 'stakeholder.lm.found_projects'); + } + $lmFoundStr = implode(' · ', $lmFoundParts); @endphp @if (! $lmHasContent)
-
- -

{{ __('stakeholder.lm.empty_title') }}

-
{{ __('stakeholder.lm.empty_body') }}
- @if (($scope ?? '') === 'strategy') - - {{ __('stakeholder.lm.empty_cta') }} - -
{{ __('stakeholder.lm.empty_hint') }}
- @endif +
+
+ +

{{ __('stakeholder.lm.empty_title') }}

+
{{ __('stakeholder.lm.empty_body') }}
+ @if (($scope ?? '') === 'strategy') + @if ($lmFoundStr !== '') +
+ + {{ sprintf(__('stakeholder.lm.empty_found'), $lmFoundStr) }} +
+ @endif + + {{ __('stakeholder.lm.empty_cta') }} + +
{{ __('stakeholder.lm.empty_hint') }}
+ @endif +
@else diff --git a/app/Language/en-US.ini b/app/Language/en-US.ini index 416021edb5..c1738759b6 100644 --- a/app/Language/en-US.ini +++ b/app/Language/en-US.ini @@ -3006,6 +3006,11 @@ stakeholder.lm.empty_title = "Build your Logic Model" stakeholder.lm.empty_body = "A Logic Model connects your programs, goals, and milestones into one story — what you do, what changes, and why it matters. It powers this page and the coverage read-out." stakeholder.lm.empty_cta = "Build a Logic Model" stakeholder.lm.empty_hint = "We'll scan the work already in this strategy and suggest what maps to each stage — you pick what fits." +stakeholder.lm.empty_found = "We found %s in this strategy" +stakeholder.lm.found_program = "program" +stakeholder.lm.found_programs = "programs" +stakeholder.lm.found_project = "project" +stakeholder.lm.found_projects = "projects" stakeholder.lm.coming_title = "5-stage read-out with task-card standard + connection-health badges" stakeholder.lm.coming_hint = "Cards per stage with assumption/hypothesis, linked projects, and a fold-out \"this period\" read-out. Connection badges between stages from zp_logicmodel_health." stakeholder.rc.coming_title = "Resource summary + resource-backed coverage matrix" From 0c2e57159557bb2399ce98d61ac663da7051fb76 Mon Sep 17 00:00:00 2001 From: Gloria <55953099+gloriafolaron@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:44:44 -0400 Subject: [PATCH 5/7] design(reports): unsmoosh the 'we found' chip on the LM empty-state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Roomier chip: 10x20 padding, 13px, wider gaps. - Totals read as totals: bolded counts + a spaced separator span so '3 programs · 12 projects' has breathing room. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01GYSPNgLdUwEnxgYqTxMc85 --- .../partials/stakeholder/page-lm.blade.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php b/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php index 9ccf0423eb..e3cef6f217 100644 --- a/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php +++ b/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php @@ -171,14 +171,17 @@ } $lmProjectCount += (int) ($lmRow['projectCount'] ?? 0); } + // Bold the counts so the totals read as totals. Every piece is server-side + // (ints + translated nouns, both e()-escaped), so the {!! !!} render below + // carries no user input. $lmFoundParts = []; if ($lmProgramCount > 0) { - $lmFoundParts[] = $lmProgramCount.' '.__($lmProgramCount === 1 ? 'stakeholder.lm.found_program' : 'stakeholder.lm.found_programs'); + $lmFoundParts[] = ''.$lmProgramCount.' '.e(__($lmProgramCount === 1 ? 'stakeholder.lm.found_program' : 'stakeholder.lm.found_programs')); } if ($lmProjectCount > 0) { - $lmFoundParts[] = $lmProjectCount.' '.__($lmProjectCount === 1 ? 'stakeholder.lm.found_project' : 'stakeholder.lm.found_projects'); + $lmFoundParts[] = ''.$lmProjectCount.' '.e(__($lmProjectCount === 1 ? 'stakeholder.lm.found_project' : 'stakeholder.lm.found_projects')); } - $lmFoundStr = implode(' · ', $lmFoundParts); + $lmFoundStr = implode('·', $lmFoundParts); @endphp @if (! $lmHasContent) @@ -187,8 +190,10 @@ tokens are ~3:1 and fail as body copy), so the invitation stays legible. */ .rd-scope .p2-lm-emptyzone{background:linear-gradient(180deg,#f3f9f7 0%,#fafcfb 62%);border-radius:24px;padding:12px;margin:36px auto;max-width:672px;} .rd-scope .p2-lm-empty{margin:0 auto;text-align:center;padding:52px 40px 48px;background:var(--rd-panel);border:1px solid var(--rd-line);border-radius:18px;box-shadow:0 3px 14px rgba(20,40,50,.05);} - .rd-scope .p2-lm-empty .found{display:flex;width:fit-content;align-items:center;gap:8px;font-size:12.5px;color:#356f5b;background:#eef7f2;border:1px solid #d8ece2;border-radius:20px;padding:7px 15px;margin:0 auto 24px;} - .rd-scope .p2-lm-empty .found i{color:#3E937A;font-size:12px;} + .rd-scope .p2-lm-empty .found{display:flex;width:fit-content;max-width:100%;align-items:center;gap:10px;font-size:13px;line-height:1.4;color:#356f5b;background:#eef7f2;border:1px solid #d8ece2;border-radius:22px;padding:10px 20px;margin:0 auto 26px;} + .rd-scope .p2-lm-empty .found i{color:#3E937A;font-size:12px;flex:none;} + .rd-scope .p2-lm-empty .found b{color:#2c5f4d;font-weight:700;} + .rd-scope .p2-lm-empty .found .sep{color:#9ec7b6;margin:0 9px;font-weight:400;} .rd-scope .p2-lm-empty .ic{width:62px;height:62px;border-radius:18px;background:linear-gradient(135deg,var(--rd-s3,#3F72B0),var(--rd-s4,#3E937A));display:inline-flex;align-items:center;justify-content:center;margin-bottom:22px;box-shadow:0 8px 20px rgba(62,147,122,.24);} .rd-scope .p2-lm-empty .ic i{color:#fff;font-size:26px;} .rd-scope .p2-lm-empty h2.t{font-size:22px;font-weight:600;color:var(--rd-text-1);margin:0 0 12px;letter-spacing:-.2px;line-height:1.25;} @@ -209,7 +214,7 @@ @if ($lmFoundStr !== '')
- {{ sprintf(__('stakeholder.lm.empty_found'), $lmFoundStr) }} + {!! sprintf(e(__('stakeholder.lm.empty_found')), $lmFoundStr) !!}
@endif From ed56694fc81bd3e89b8c99b8758ac2957d536b2a Mon Sep 17 00:00:00 2001 From: Gloria <55953099+gloriafolaron@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:52:14 -0400 Subject: [PATCH 6/7] =?UTF-8?q?design(reports):=20tighten=20the=20'we=20fo?= =?UTF-8?q?und'=20separator=20gap=20(18px=E2=86=9210px)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 9px-per-side separator margin pushed the two totals too far apart; 5px each side reads as one cohesive '3 programs · 12 projects' unit. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01GYSPNgLdUwEnxgYqTxMc85 --- .../Reports/Templates/partials/stakeholder/page-lm.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php b/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php index e3cef6f217..33923c7579 100644 --- a/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php +++ b/app/Domain/Reports/Templates/partials/stakeholder/page-lm.blade.php @@ -193,7 +193,7 @@ .rd-scope .p2-lm-empty .found{display:flex;width:fit-content;max-width:100%;align-items:center;gap:10px;font-size:13px;line-height:1.4;color:#356f5b;background:#eef7f2;border:1px solid #d8ece2;border-radius:22px;padding:10px 20px;margin:0 auto 26px;} .rd-scope .p2-lm-empty .found i{color:#3E937A;font-size:12px;flex:none;} .rd-scope .p2-lm-empty .found b{color:#2c5f4d;font-weight:700;} - .rd-scope .p2-lm-empty .found .sep{color:#9ec7b6;margin:0 9px;font-weight:400;} + .rd-scope .p2-lm-empty .found .sep{color:#9ec7b6;margin:0 5px;font-weight:400;} .rd-scope .p2-lm-empty .ic{width:62px;height:62px;border-radius:18px;background:linear-gradient(135deg,var(--rd-s3,#3F72B0),var(--rd-s4,#3E937A));display:inline-flex;align-items:center;justify-content:center;margin-bottom:22px;box-shadow:0 8px 20px rgba(62,147,122,.24);} .rd-scope .p2-lm-empty .ic i{color:#fff;font-size:26px;} .rd-scope .p2-lm-empty h2.t{font-size:22px;font-weight:600;color:var(--rd-text-1);margin:0 0 12px;letter-spacing:-.2px;line-height:1.25;} From bb09f1ff7ecf6e9ff319b5883333c8ed62ad3fc7 Mon Sep 17 00:00:00 2001 From: Gloria <55953099+gloriafolaron@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:09:18 -0400 Subject: [PATCH 7/7] chore(reports): drop unused stakeholder.lm.no_canvas lang key The empty-state rewrite replaced its only usage (empty_title/body/cta/hint); the key was left defined but unreferenced. Copilot flagged it. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01GYSPNgLdUwEnxgYqTxMc85 --- app/Language/en-US.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Language/en-US.ini b/app/Language/en-US.ini index c1738759b6..2e89ad3185 100644 --- a/app/Language/en-US.ini +++ b/app/Language/en-US.ini @@ -3001,7 +3001,6 @@ stakeholder.kpi.hours_logged = "Hours logged" stakeholder.kpi.this_period = "this period" stakeholder.overview.coming_title = "Peak this period · Needs attention · ToC narrative · Theory-health strip" stakeholder.overview.coming_hint = "The recommend-and-override hero, execution-lens needs-attention block, Theory-of-Change sentence, and theory-health strip land next." -stakeholder.lm.no_canvas = "No Logic Model canvas authored yet. Build one on the Board to see it read out here." stakeholder.lm.empty_title = "Build your Logic Model" stakeholder.lm.empty_body = "A Logic Model connects your programs, goals, and milestones into one story — what you do, what changes, and why it matters. It powers this page and the coverage read-out." stakeholder.lm.empty_cta = "Build a Logic Model"