{{-- Stakeholder Report — Page 2 (Logic Model read-out) Fully rewritten against the punch-list. Behavior contracts: 1. Goal-as-truth. If an item has any linked goal (projectLinks entry with linked_entity_type='goal'), the goal record supplies BOTH current and target values. The item description is display label only — never parsed for a number in that case. Items with no goal link show no denominator and no percent. 2. Guard: if aggregate target <= 0 or current/target > 5, drop the ratio (impossible math never reaches a funder) and Log it. 3. Templated read: max 2 lines per stage, one going-well + one exception, materiality-weighted at 20%. Vocabulary is fixed — no freeform prose. 4. Belief line is ONE sentence, ≤220 chars, built from first 3 activities + first impact. 5. Risk box: one weakest health badge, single sentence, single period. 6. Layout: max-width 900px, min-width:0 on every grid child, no horizontal blowout at 1280px. 7. Status color map is fixed — at-risk is white bg + inset ring, never cream. Vars in: $logicModel null | {canvasId, narrative, stageProgress, healthBadges, coverageMatrix, projectLinks, linkedGoals, projectMeta} $hasLM bool $scope 'strategy' | 'program' --}} @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; } } // 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) { // programRows carries stdClass rows (object[], like page-programs), so // cast before array access to avoid "Cannot use object as array". $lmRow = (array) $lmRow; if (($lmRow['type'] ?? '') === 'program') { $lmProgramCount++; } $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.' '.e(__($lmProgramCount === 1 ? 'stakeholder.lm.found_program' : 'stakeholder.lm.found_programs')); } if ($lmProjectCount > 0) { $lmFoundParts[] = ''.$lmProjectCount.' '.e(__($lmProjectCount === 1 ? 'stakeholder.lm.found_project' : 'stakeholder.lm.found_projects')); } $lmFoundStr = implode('·', $lmFoundParts); @endphp @if (! $lmHasContent)