pub(in crate::app) fn capacity_row(
calculation: &crate::capacity::calculate::CapacityCalculation,
) -> crate::ui::schedule::CapacityRow {
let unit = calculation.unit.as_deref();
crate::ui::schedule::CapacityRow {
title: calculation.scope.display.clone(),
subtitle: format!(
"{kind} · {state} · committed {committed} of {capacity} · {utilization} · \
overflow {overflow}",
kind = calculation.scope.kind.as_str(),
state = calculation.state().as_str(),
committed = capacity_with_unit(calculation.committed, unit),
capacity = capacity_with_unit(calculation.capacity, unit),
utilization = capacity_utilization(calculation.utilization),
overflow = capacity_with_unit(calculation.overflow, unit),
),
warned: !calculation.diagnostics.is_empty(),
overflowing: calculation.is_over_capacity(),
}
}
pub(in crate::app) fn capacity_detail(
index: &crate::cycle_schedule::identity::ScheduleIndex,
calculation: &crate::capacity::calculate::CapacityCalculation,
stored: &std::collections::BTreeMap<String, crate::cycle_schedule::snapshot::CycleSnapshot>,
) -> crate::ui::schedule::CapacityDetail {
crate::ui::schedule::CapacityDetail {
heading: calculation.scope.display.clone(),
fields: capacity_fields(calculation),
diagnostics: calculation
.diagnostics
.iter()
.map(crate::capacity::warning::CapacityWarning::render)
.collect(),
snapshot: capacity_snapshot_view(index, calculation, stored),
}
}
pub(in crate::app) fn capacity_fields(
calculation: &crate::capacity::calculate::CapacityCalculation,
) -> Vec<(String, String)> {
vec![
(
"scope".to_string(),
calculation.scope.kind.as_str().to_string(),
),
("reference".to_string(), calculation.scope.reference.clone()),
("display".to_string(), calculation.scope.display.clone()),
(
"state".to_string(),
calculation.state().as_str().to_string(),
),
(
"capacity".to_string(),
capacity_number(calculation.capacity),
),
(
"unit".to_string(),
calculation.unit.clone().unwrap_or_else(|| "-".to_string()),
),
(
"committed".to_string(),
capacity_number(calculation.committed),
),
(
"completed".to_string(),
capacity_number(calculation.completed),
),
(
"remaining".to_string(),
capacity_number(calculation.remaining),
),
(
"overflow".to_string(),
capacity_number(calculation.overflow),
),
(
"utilization".to_string(),
capacity_utilization(calculation.utilization),
),
(
"counted_issues".to_string(),
capacity_number(calculation.counted_issues),
),
(
"uncounted_issues".to_string(),
capacity_number(calculation.uncounted_issues),
),
(
"unestimated_issues".to_string(),
capacity_number(calculation.unestimated_issues),
),
(
"source_issues".to_string(),
calculation.source.issues.to_string(),
),
(
"source_units".to_string(),
capacity_joined(&calculation.source.units),
),
(
"diagnostics".to_string(),
calculation.diagnostics.len().to_string(),
),
]
}
pub(in crate::app) fn capacity_snapshot_view(
index: &crate::cycle_schedule::identity::ScheduleIndex,
calculation: &crate::capacity::calculate::CapacityCalculation,
stored: &std::collections::BTreeMap<String, crate::cycle_schedule::snapshot::CycleSnapshot>,
) -> Option<crate::ui::schedule::CapacitySnapshotView> {
use crate::capacity::snapshot::CapacitySnapshot;
use crate::cycle_schedule::identity::{CycleReference, GeneratedCycleState};
if calculation.scope.kind != crate::capacity::config::CapacityScopeKind::Cycle {
return None;
}
let CycleReference::Generated { id, .. } =
crate::capacity::resolve_cycle(index, &calculation.scope.reference)
else {
return Some(crate::ui::schedule::CapacitySnapshotView {
label: "none (a hand-written cycle has no generated snapshot)".to_string(),
fields: Vec::new(),
});
};
let evidence = stored
.get(id.as_str())
.and_then(|stored| CapacitySnapshot::from_cycle_snapshot(stored).ok().flatten());
match evidence {
Some(evidence) => {
let historical = index
.cycle(&id)
.is_some_and(|cycle| cycle.state != GeneratedCycleState::Closed);
let label = if historical {
"immutable (this cycle has been reopened; the values above are the current \
reading)"
} else {
"immutable (captured at close)"
};
Some(crate::ui::schedule::CapacitySnapshotView {
label: label.to_string(),
fields: capacity_snapshot_fields(&evidence),
})
}
None => Some(crate::ui::schedule::CapacitySnapshotView {
label: "none (no close snapshot recorded for this cycle)".to_string(),
fields: Vec::new(),
}),
}
}
pub(in crate::app) fn capacity_snapshot_fields(
evidence: &crate::capacity::snapshot::CapacitySnapshot,
) -> Vec<(String, String)> {
vec![
("snapshot_id".to_string(), evidence.snapshot_id.clone()),
("closed_at".to_string(), evidence.closed_at.clone()),
("closed_by".to_string(), evidence.closed_by.clone()),
(
"scope".to_string(),
evidence.scope.kind.as_str().to_string(),
),
("reference".to_string(), evidence.scope.reference.clone()),
("display".to_string(), evidence.scope.display.clone()),
("state".to_string(), evidence.state.as_str().to_string()),
("capacity".to_string(), capacity_number(evidence.capacity)),
(
"unit".to_string(),
evidence.unit.clone().unwrap_or_else(|| "-".to_string()),
),
("committed".to_string(), capacity_number(evidence.committed)),
("completed".to_string(), capacity_number(evidence.completed)),
("remaining".to_string(), capacity_number(evidence.remaining)),
("overflow".to_string(), capacity_number(evidence.overflow)),
(
"utilization".to_string(),
capacity_utilization(evidence.utilization),
),
(
"counted_issues".to_string(),
capacity_number(evidence.counted_issues),
),
(
"uncounted_issues".to_string(),
capacity_number(evidence.uncounted_issues),
),
(
"unestimated_issues".to_string(),
capacity_number(evidence.unestimated_issues),
),
(
"source_issues".to_string(),
evidence.source.issues.to_string(),
),
(
"source_units".to_string(),
capacity_joined(&evidence.source.units),
),
(
"diagnostics".to_string(),
evidence.diagnostics.len().to_string(),
),
]
}
pub(in crate::app) fn capacity_cell(
calculation: &crate::capacity::calculate::CapacityCalculation,
) -> String {
if calculation.capacity.is_none() {
return String::new();
}
let unit = calculation.unit.as_deref();
let mut cell = format!(
"{committed}/{capacity} {unit} · {utilization}",
committed = capacity_number(calculation.committed),
capacity = capacity_number(calculation.capacity),
unit = unit.unwrap_or("-"),
utilization = capacity_utilization(calculation.utilization),
);
if calculation.is_over_capacity() {
cell.push_str(" · over");
}
cell
}
pub(in crate::app) fn issue_estimate_line(
estimate: Option<&crate::capacity::estimate::EstimateValue>,
unit: Option<&str>,
) -> String {
let unit = unit.unwrap_or("-");
let Some(estimate) = estimate else {
return "not in the capacity snapshot".to_string();
};
if estimate.is_unestimated() {
return "states no estimate — unestimated, counted as nothing (never 0)".to_string();
}
match (estimate.raw(), estimate.converted_value) {
(Some(raw), Some(value)) => format!("estimate {raw} → contributes {value}{unit}"),
(Some(raw), None) => format!("estimate {raw} → cannot be counted in {unit}"),
(None, _) => "states no estimate".to_string(),
}
}
pub(in crate::app) fn capacity_with_unit(value: Option<u64>, unit: Option<&str>) -> String {
value.map_or_else(
|| "-".to_string(),
|value| match unit {
Some(unit) => format!("{value}{unit}"),
None => value.to_string(),
},
)
}
pub(in crate::app) fn capacity_number<T: std::fmt::Display>(value: Option<T>) -> String {
value.map_or_else(|| "-".to_string(), |value| value.to_string())
}
pub(in crate::app) fn capacity_utilization(value: Option<u32>) -> String {
value.map_or_else(|| "-".to_string(), |value| format!("{value}%"))
}
pub(in crate::app) fn capacity_joined(units: &[String]) -> String {
if units.is_empty() {
"-".to_string()
} else {
units.join(", ")
}
}