Hypertext Rails

Documentation

Getting Started

Communication Center

Procore / Project Groups

Other Features

Compiled Template Guide

Overview

The compiled template (@compiled_template) renders all dynamic components in a structured multi-row layout. This guide covers layout structure, component syntax, variable resolution, and display logic.


Layout Structure

The template uses a 4-row grid system with specific column assignments:

┌─────────────────────────────────────────────────────────────┐
│ ROW 1: HEADER (Fixed)                                       │
│ [Project Name] [Check-in Volume] [Month] [Logo]             │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ ROW 2: MORALE + CHARTS (4 columns)                          │
│ [Label] [Morale Gauges] [Charts] [Risk Indicators]          │
│  (60px)   (25%)        (50%)    (25%)                        │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ ROW 3: INSIGHTS (4 columns)                                  │
│ [Label] [Col 0] [Col 1] [Col 2] [Col 3]                     │
│  (60px)  (25%)  (25%)  (25%)  (25%)                         │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ ROW 4: REGULAR LISTS (Dynamic columns)                      │
│ [Label] [List 1] [List 2] [List 3] [List 4]                 │
│  (60px)  (25%)   (25%)   (25%)   (25%)                      │
└─────────────────────────────────────────────────────────────┘

Display Logic: What Gets Displayed?

Dynamic components are displayed, but they use project variables as their data sources.

  • Project variables are NOT displayed directly (e.g., {{this_months_morale}} by itself)
  • Dynamic components ARE displayed (e.g., {{GAUGE:Team Morale:this_months_morale:this_months_morale_delta}})
  • Dynamic components pull data from project variables (the value_var and delta_var in component syntax)

How It Works

Project variables are raw data points that exist in the system: - {{this_months_morale}} - Returns a number (e.g., "85") - {{baseline_morale}} - Returns a number (e.g., "78") - {{project_name}} - Returns a string (e.g., "My Project")

These are NOT displayed directly in the compiled template. They are data sources.

Dynamic components are visual elements that get displayed: - {{GAUGE:Label:value_var:delta_var}} - Displays as a gauge with circular progress - {{LIST:Label:value_var:delta_var}} - Displays as a list - {{THEME:Label:value_var:delta_var}} - Displays as a theme component - {{BAR:Label|config}} - Displays as a bar chart - {{LINE:Label|config}} - Displays as a line chart - {{PIE:Label|config}} - Displays as a pie chart

These ARE displayed in the compiled template.

The Connection: {{GAUGE:Team Morale:this_months_morale:this_months_morale_delta}} ↑ ↑ ↑ Label value_var delta_var (project variable) (project variable)


Row 1: Header (Fixed Content)

Location: Top row, full width Grid: Single row, no columns Content: Static header information

Position Content Source
Left Project Name + Report Type delivery.project.name + delivery.comms_instance.template.template_type
Center-Left Check-in Volume (51) + Change (38%) Hardcoded values (placeholder)
Center-Right Current Month Date.current.strftime('%B %Y')
Right PepTalk Logo Static text

Code Location: Lines 590-606 in _compiled_template.html.erb


Row 2: Morale + Charts (4 Columns)

Location: Second row Grid: grid-template-columns: 60px 1fr 2fr 1fr (4 columns) Total Width: 100% of container (max-width: 1160px)

Column 0: Vertical Label (60px)

  • Content: Vertical text "MORALE"
  • Style: Rotated text, blue background
  • Code Location: Lines 610-612

Column 1: Morale Section (25% width)

  • Content: Team Morale Gauge + Additional Gauges
  • Components:

    • Primary Gauge: First gauge from gauges array (excluding highest_risk_day and highest_risk_hour)
    • Displays: Gauge value (e.g., 74)
    • Shows: Delta percentage (e.g., "↑ 26% from last month")
    • Additional Gauges: All remaining regular gauges (if any)
    • Displayed as label-value pairs below the main gauge
  • Gauge Selection Logic: ruby risk_gauges = gauges.select { |g| %w[highest_risk_day highest_risk_hour].include?(g[:value_var]) } regular_gauges = gauges.reject { |g| %w[highest_risk_day highest_risk_hour].include?(g[:value_var]) } morale_gauge = regular_gauges.first

  • Code Location: Lines 613-674

Column 2: Charts Section (50% width)

  • Content: All chart components stacked vertically
  • Components: All items from charts array

    • Each chart rendered with:
    • Chart title (from chart[:label] or config title)
    • Chart.js canvas element
    • Chart type: BAR, LINE, or PIE (from chart[:component_type])
  • Chart Types Supported:

    • BAR - Bar chart
    • LINE - Line chart
    • PIE - Pie chart
    • CHART - Generic (inferred from label)
  • Fallback: If no charts present, displays "Weekly Morale" placeholder chart

  • Code Location: Lines 676-704

Column 3: Risk Section (25% width)

  • Content: Risk indicator gauges
  • Components: Gauges with value_var of:

    • highest_risk_day - Displays calendar icon + day name
    • highest_risk_hour - Displays clock icon + hour
  • Selection Logic: ruby risk_gauges = gauges.select { |g| %w[highest_risk_day highest_risk_hour].include?(g[:value_var]) }

  • Code Location: Lines 706-732


Row 3: Insights (4 Columns)

Location: Third row Grid: grid-template-columns: 60px repeat(4, 1fr) (5 columns total) Label Column: 60px vertical label "INSIGHTS" Content Columns: 4 equal-width columns (25% each)

Column 0: Vertical Label (60px)

  • Content: Vertical text "INSIGHTS"
  • Code Location: Lines 797-799

Column 1 (Index 0): "What Could Be Better"

  • Component Type: LIST
  • Variable: value_var == 'what_could_be_better'
  • Title: "What Could Be Better"
  • Data Structure: ruby { type: 'list', data: { value_var: 'what_could_be_better', value: Array of hashes [ { label: "Answer 1", percentage: 45.5, count: 10, total_responses: 22 }, { label: "Answer 2", percentage: 31.8, count: 7, total_responses: 22 } ], label: "What Could Be Better" }, title: 'What Could Be Better' }
  • Rendering: Feedback list with numbered items, percentages, and progress bars
  • Code Location: Lines 739, 754

Column 2 (Index 1): "What Is Going Well"

  • Component Type: LIST
  • Variable: value_var == 'what_is_going_well'
  • Title: "What Is Going Well"
  • Data Structure: Same as "What Could Be Better" but with positive styling
  • Rendering: Feedback list with green styling for first item
  • Code Location: Lines 740, 755

Column 3 (Index 2): Spotcheck Components

  • Component Type: SPOTCHECK
  • Variable: Components from spotchecks array
  • Data Structure: ruby { type: 'spotcheck', data: { question: "Question text", items: Array of hashes [ { label: "Answer", percentage: 45.5, responses: 10 } ], start_date: Date, end_date: Date, display_options: { show_date_range: true, ... } }, title: "Spotcheck Title" }
  • Rendering: Shows question, top answer, and metadata (date range, response count)
  • Distribution Logic: If other columns are empty, spotchecks fill them
  • Code Location: Lines 748-750, 756, 759-775

Column 4 (Index 3): Action Tracker

  • Component Type: LIST
  • Variable: value_var == 'action_tracker'
  • Title: From list[:label] or "Actions Being Taken"
  • Data Structure: ruby { type: 'action_tracker', data: { value_var: 'action_tracker', value: "Status|Category|Action\nStatus|Category|Action", # Newline-separated delta_var: "show_status=1|show_feedback=1", # Optional display options label: "Actions Being Taken" }, title: "Actions Being Taken" }
  • Rendering: Action items with status badges, category, and action text
  • Code Location: Lines 741, 757

Column Distribution Logic

The template uses intelligent distribution to fill empty columns:

  1. Primary Assignment:

    • Column 0 → "What Could Be Better" (if exists)
    • Column 1 → "What Is Going Well" (if exists)
    • Column 2 → Spotcheck components (if any)
    • Column 3 → Action Tracker (if exists)
  2. Fallback Distribution:

    • If any primary columns are empty AND spotchecks exist:
      • Spotchecks are distributed to empty columns (starting with column 2)
    • If themes exist:
      • Themes fill empty columns first
      • Then themes are distributed across all columns if needed
  3. Code Location: Lines 752-794


Row 4: Regular Lists (Dynamic Columns)

Location: Fourth row (only shown if regular lists exist) Grid: grid-template-columns: 60px repeat(4, 1fr) Content: All lists EXCEPT special lists (whatcouldbebetter, whatisgoingwell, action_tracker)

Column 0: Vertical Label (60px)

  • Content: Vertical text "LISTS"
  • Code Location: Lines 1031-1033

Columns 1-4: Regular List Components

  • Component Type: LIST
  • Variables: All lists where value_var is NOT:

    • what_could_be_better
    • what_is_going_well
    • action_tracker
  • Selection Logic: ruby regular_lists = lists.reject { |l| %w[what_could_be_better what_is_going_well action_tracker].include?(l[:value_var].to_s) }

  • Data Structure: ruby { type: 'list', data: { value_var: 'regular_list_name', value: "Item 1\nItem 2\nItem 3" # Newline or comma-separated string label: "List Label" }, title: "List Label" }

  • Rendering: Simple numbered list (first 3 items, truncated to 25 chars)

  • Code Location: Lines 1028-1065


Team Morale Gauge Syntax

The Team Morale gauge in Row 2, Column 1 displays the first regular gauge component found in your template (excluding risk gauges).

Template Syntax

{{GAUGE:Label:value_var:delta_var}}

Syntax Breakdown

  • GAUGE - Component type (must be uppercase)
  • Label - Display label (e.g., "Team Morale", "Current Morale")
  • value_var - Variable name for the main value
  • delta_var - Variable name for the delta/change value (optional)

Common Morale Variables

  1. this_months_morale

    • Description: Current month's morale score (0-100)
    • Resolved by: resolve_this_months_morale method
    • Calculation: Uses MoraleCalculationService.calculate_team_morale_from_submissions
  2. baseline_morale

    • Description: Baseline morale score (0-100)
    • Resolved by: resolve_baseline_morale method
    • Calculation: Uses MoraleCalculationService.calculate_baseline_morale_from_submissions
    • Baseline Range: Determined by BaselineRangeCalculator.default_for_project
  3. multi_project_average_morale

    • Description: Average morale across multiple projects for a stakeholder
    • Resolved by: resolve_multi_project_average_morale method
    • Note: Falls back to this_months_morale if only one project

Delta Variables (Optional)

Delta variables show the change/trend. Common patterns: - this_months_morale_delta - Change in current month's morale - baseline_morale_delta - Change in baseline morale - Any other variable that returns a percentage change

Example Syntax

Example 1: Current Month's Morale {{GAUGE:Team Morale:this_months_morale:this_months_morale_delta}}

Example 2: Baseline Morale {{GAUGE:Baseline Morale:baseline_morale:baseline_morale_delta}}

Example 3: Without Delta {{GAUGE:Team Morale:this_months_morale}}

How It Works in Compiled Template

Selection Logic: ```ruby

Separate risk gauges from regular gauges

riskgauges = gauges.select { |g| %w[highestriskday highestriskhour].include?(g[:valuevar]) } regulargauges = gauges.reject { |g| %w[highestriskday highestriskhour].include?(g[:valuevar]) }

Use the FIRST regular gauge as the Team Morale gauge

moralegauge = regulargauges.present? && regulargauges.any? ? regulargauges.first : nil ```

Key Points: 1. First Gauge Wins: The first regular gauge in your template becomes the "Team Morale" gauge 2. Risk Gauges Excluded: Gauges with value_var of highest_risk_day or highest_risk_hour are excluded 3. Fallback Value: If no gauges are found, defaults to value 74 and delta "↓ 26%"

Display Logic: Once selected, the gauge displays: - Value: morale_gauge[:value].to_i (converted to integer, 0-100) - Delta: morale_gauge[:delta] (string like "↑ 26%" or "↓ 15%") - Gauge Circle: Visual percentage based on (value / 100 * 75).round(1)

Additional Gauges: If you have multiple regular gauges in your template, the first one becomes the main "Team Morale" gauge, and the rest are displayed as additional gauges below it.


Morale Variables - Data Wiring

this_months_morale

Date Range: - Uses: current_month_date_range = today.beginning_of_month..today.end_of_month - Note: The range is the full month, but only submissions that exist up to the current day are included (since future submissions don't exist yet) - Effectively: Start of month to current day

Code Location: app/services/template_variable_resolver_service.rb:376-395

Formula Used: - Calls MoraleCalculationService.calculate_team_morale_from_submissions - For dates >= 2025-01-01: Uses weighted formula - Score 5 (energised): 1.5x weight - Score 4 (relaxed): 1.0x weight - Score 3 (indifferent): 0.5x weight - Score 1-2 (anxious/uncertain): 0x weight (don't count) - Formula: ((energised × 1.5) + (relaxed × 1.0) + (indifferent × 0.5)) / total × 100 - For dates < 2025-01-01: Legacy formula - Only scores 5 and 4 count - Formula: (energised + relaxed) / total × 100

baseline_morale

Configuration Source: - Uses DashboardSetting.baseline_calculation_period from the project's dashboard_setting - Maps period to range via DashboardSetting::PERIOD_TO_RANGE: - 0'this_month_partial' - 1'last_month' - 3'last_3_months' (default) - 6'last_6_months' - 12'last_12_months'

Code Location: app/services/template_variable_resolver_service.rb:397-417

Formula Used: - Routes to different calculation methods based on baseline_range: - 'last_3_months'calculate_3_month_baseline_morale - Calculates morale for each of the last 3 months separately - Each month uses appropriate formula (2025+ vs pre-2025) - Averages the 3 monthly scores - 'last_6_months' or 'last_12_months'calculate_monthly_baseline_morale - Calculates morale for each month in the range - Averages all monthly scores - 'this_month_partial' or 'last_month'calculate_daily_baseline_morale - Calculates morale for each day - Averages all daily scores

multi_project_average_morale

Project Selection: - Gets projects from project.dashboard_setting.benchmark_calculation_projects_array - Filters to only active projects (status != 0) - Falls back to resolve_this_months_morale if no benchmark projects configured

Code Location: app/services/template_variable_resolver_service.rb:456-486

Current Behavior: - Uses benchmark projects from dashboard_setting.benchmark_calculation_projects - Uses baselinecalculationperiod from dashboard_setting to determine date range - Calculates team morale for each benchmark project using the baseline date range - Averages all project morale scores

Formula Used: - Uses calculate_team_morale_from_submissions for each project - Date range determined by baseline_calculation_period (e.g., 3 = last3months) - Averages all project morale scores

Summary Table

Variable Date Range Source Formula Configuration
this_months_morale Current month (beginningofmonth..endofmonth) calculate_team_morale_from_submissions None - always current month
baseline_morale From dashboard_setting.baseline_calculation_period calculate_baseline_morale_from_submissions DashboardSetting::PERIOD_TO_RANGE mapping
multi_project_average_morale From dashboard_setting.baseline_calculation_period for benchmark projects calculate_team_morale_from_submissions (per project, then average) Uses benchmark_calculation_projects and baseline_calculation_period

Delta/Comparison Variables in Gauge Components

How Delta Variables Work

Template Syntax: {{GAUGE:Label:value_var:delta_var}}

Example: {{GAUGE:test:baseline_morale:engagement_rate}}

What Gets Fetched

Both variables are fetched independently:

  1. Value Variable (value_var): baseline_morale

    • Fetches: Baseline morale calculation
    • Data source: resolve_baseline_morale
    • Returns: String number (e.g., "78")
  2. Delta Variable (delta_var): engagement_rate

    • Fetches: Engagement rate calculation
    • Data source: resolve_engagement_rate
    • Returns: String number (e.g., "85")

Code Location: app/services/template_variable_resolver_service.rb:747-755

value = resolve_single_variable(value_var) || '0'  # Resolves baseline_morale
delta = resolve_single_variable(delta_var) || '0'  # Resolves engagement_rate

What Gets Displayed

In Compiled Template (Row 2, Column 1)

Code Location: app/views/communication_center/dynamic_components/_compiled_template.html.erb:620-661

Display Structure: 1. Main Gauge Value: Shows the value_var result (e.g., "78" for baselinemorale) 2. Delta Display: Shows the `deltavar` result formatted with arrow and percentage

Delta Formatting Logic: ruby if morale_delta.present? && morale_delta.to_s != '0' delta_value = morale_delta.to_s.gsub(/[^0-9.-]/, '').to_f rescue 0 delta_class = delta_value < 0 ? 'negative' : 'positive' delta_arrow = delta_value < 0 ? '↓' : '↑' delta_text = morale_delta.to_s.match?(/%/) ? morale_delta : "#{morale_delta.to_s.gsub('-', '')}%" end

Display Output: ┌─────────────────┐ │ Team Morale │ │ 78 │ ← value_var (baseline_morale) │ ↑ 85% │ ← delta_var (engagement_rate) formatted │ from last month│ └─────────────────┘

Important Note: The text says "from last month" but this is hardcoded - it doesn't actually mean the delta is comparing to last month. It's just display text.

Delta Formatting Behavior

The system assumes delta_var contains a delta/change value and formats it accordingly:

  1. Extracts numeric value: ruby delta_value = delta.to_s.gsub(/[^0-9.-]/, '').to_f

  2. Determines arrow direction:

    • Positive number → (up arrow)
    • Negative number → (down arrow)
    • Zero → No arrow
  3. Formats as percentage: ruby delta_text = delta.to_s.match?(/%/) ? delta : "#{delta}%"

  4. Displays:

    • Positive: ↑ 85%
    • Negative: ↓ 15%
    • Zero: (0%)

Problem with Non-Delta Variables

If you use a non-delta variable like engagement_rate as delta_var:

  • engagement_rate returns "85" (not a change, just a value)
  • System formats it as: ↑ 85% (treats it as positive change)
  • Display shows: "↑ 85% from last month" (misleading - it's not a change)

This is misleading because: - engagement_rate is not a delta/change value - It's just another metric (average score converted to percentage) - The arrow and "from last month" text suggest it's a change, but it's not

Intended vs Actual Usage

Intended Usage (Delta Variables): Delta variables should represent changes/comparisons: - this_months_morale_delta - Change in current month's morale - baseline_morale_delta - Change in baseline morale - Variables that return percentage changes (e.g., "↑ 5%", "↓ 10%")

Actual Usage (Any Variable): Currently, ANY variable can be used as deltavar: - `engagementrate- Works but misleading (not a delta) -responsescount- Works but misleading (not a delta) -baselinemorale` - Works but misleading (not a delta)

The system doesn't validate that delta_var is actually a delta value.


Component Data Structure Reference

Gauge Component

{
  component_type: 'GAUGE',
  label: "Team Morale",
  value: 74,  # Integer (0-100)
  delta: "↑ 26%",  # String with arrow and percentage
  value_var: 'team_morale',  # Original variable name
  delta_var: 'team_morale_delta',
  is_chart: false
}

Chart Component

{
  component_type: 'BAR',  # or 'LINE', 'PIE'
  label: "Weekly Morale",
  value: "title=Weekly Morale|color=green|legend=none|x=weekly",
  delta: nil,
  value_var: nil,
  delta_var: nil,
  is_chart: true
}

List Component (Regular)

{
  component_type: 'LIST',
  label: "List Label",
  value: "Item 1\nItem 2\nItem 3",  # String (newline or comma-separated)
  delta: nil,
  value_var: 'list_name',
  delta_var: nil,
  is_chart: false
}

List Component (Feedback - What Could Be Better / What Is Going Well)

{
  component_type: 'LIST',
  label: "What Could Be Better",
  value: [  # Array of hashes
    { label: "Answer 1", percentage: 45.5, count: 10, total_responses: 22 },
    { label: "Answer 2", percentage: 31.8, count: 7, total_responses: 22 }
  ],
  delta: nil,
  value_var: 'what_could_be_better',  # or 'what_is_going_well'
  delta_var: nil,
  is_chart: false
}

List Component (Action Tracker)

{
  component_type: 'LIST',
  label: "Actions Being Taken",
  value: "Open|Category|Action text\nClosed|Category|Action text",
  delta: nil,
  value_var: 'action_tracker',
  delta_var: "show_status=1|show_feedback=1",  # Optional display options
  is_chart: false
}

Theme Component

{
  component_type: 'THEME',
  label: "Theme Name",
  value: "Theme description text",
  delta: "↑ 15%",  # Percentage change
  value_var: 'theme_name',
  delta_var: 'theme_delta',
  is_chart: false
}

Spotcheck Component

{
  component_type: 'SPOTCHECK',
  label: "Spotcheck Label",
  value_var: 'spotcheck',
  question: "Question text?",
  items: [  # Sorted by responses (descending)
    { label: "Answer 1", percentage: 45.5, responses: 10, total_responses: 22 },
    { label: "Answer 2", percentage: 31.8, responses: 7, total_responses: 22 }
  ],
  top_answer: { label: "Answer 1", percentage: 45.5, responses: 10 },
  start_date: Date,
  end_date: Date,
  display_options: {
    show_date_range: true,
    show_top_response_pct: true,
    show_total_responses: true
  }
}

Quick Reference: Component Placement

Component Type Row Column Variable Name Notes
Gauges (Regular) 2 1 (Morale) Any (except risk) First gauge = main morale, rest = additional
Gauges (Risk) 2 3 (Risk) highest_risk_day, highest_risk_hour Special risk indicators
Charts 2 2 (Charts) Any chart type All charts stacked vertically
List (What Could Be Better) 3 0 what_could_be_better Feedback list with percentages
List (What Is Going Well) 3 1 what_is_going_well Feedback list with percentages
List (Action Tracker) 3 3 action_tracker Action items with status
Spotcheck 3 2 (or distributed) spotcheck Question + top answer
Theme 3 Any empty Any theme variable Fills empty columns
List (Regular) 4 1-4 Any other list Excludes special lists above

Variable Resolution Flow

  1. Template Content Parsing:

    • Service scans template for {{COMPONENT_TYPE:...}} tokens
    • Example: {{GAUGE:Team Morale:team_morale:team_morale_delta}
  2. Token Parsing:

    • Component type extracted (GAUGE, LIST, BAR, etc.)
    • Label, valuevar, deltavar extracted
    • Special cases identified (spotcheck feedback, action tracker, risk gauges)
  3. Value Resolution:

    • Standard variables → TemplateVariableResolverService
    • Special cases → Specialized resolvers
    • Values stored in component hash
  4. Categorization:

    • Components sorted into: gauges, charts, lists, themes, spotchecks
  5. Template Rendering:

    • Each category passed to template as separate arrays
    • Template assigns components to specific rows/columns based on value_var

Key Code Locations

  • Service: app/services/compiled_template_image_service.rb
  • Template: app/views/communication_center/dynamic_components/_compiled_template.html.erb
  • Row 1 Header: Lines 590-606
  • Row 2 Content: Lines 608-733
  • Row 3 Insights: Lines 737-1025
  • Row 4 Lists: Lines 1027-1065
  • Component Distribution: Lines 738-794
  • Variable Resolver: app/services/template_variable_resolver_service.rb
    • resolve_this_months_morale: Line 376
    • resolve_baseline_morale: Line 397
    • resolve_multi_project_average_morale: Line 456
    • resolve_single_variable: Line 259

Summary

The compiled template uses a fixed 4-row layout with intelligent column distribution:

  • Row 1: Fixed header (project info, stats, logo)
  • Row 2: Morale gauges (left), Charts (center), Risk indicators (right)
  • Row 3: Four insight columns (What Could Be Better, What Is Going Well, Spotchecks, Action Tracker) with fallback distribution
  • Row 4: Regular lists in dynamic columns

Components are identified by their value_var attribute, which determines their placement in the grid. Special variables like what_could_be_better, action_tracker, and highest_risk_day have fixed positions, while regular components fill available space.

To display Team Morale in Row 2, Column 1: 1. Add a GAUGE component to your template 2. Use syntax: {{GAUGE:Label:value_var:delta_var}} 3. Common variables: this_months_morale, baseline_morale 4. The first regular gauge in your template becomes the Team Morale gauge 5. Additional regular gauges appear below it

Example: {{GAUGE:Team Morale:this_months_morale:this_months_morale_delta}}

This will display in Row 2, Column 1 as the main Team Morale gauge with the current month's morale value and delta.