import '../about/external/data/Commitment.dart'; import '../about/internal/application/CapacityProfile.dart'; /// The result of a capacity check at plan time. class CapacityVerdict { /// Minutes the plan actually implies, after the estimation multipliers. double projectedMinutes; /// Minutes history says get done on this weekday, at p50. double historicalMinutes; /// The ceiling — historical minutes times the headroom factor. double allowedMinutes; bool blocked; /// Minutes that must come out of the plan before it will be accepted. double excessMinutes; String message; CapacityVerdict({ this.projectedMinutes = 0, this.historicalMinutes = 0, this.allowedMinutes = 0, this.blocked = false, this.excessMinutes = 0, this.message = "", }); } /// Chronic overdue is usually an overcommitment problem misdiagnosed as a /// laziness problem. This is the check that catches it. /// /// ``` /// projected_load = Sum (est_minutes x your_multiplier[category]) /// if projected_load > 0.85 x p50(historical_completed_minutes[weekday]): /// block, and force removal /// ``` class CapacityEngine { /// Plan against 85% of what you historically get done, not 100%. static const double headroom = 0.85; /// The plan as the app believes it, not as you estimated it. Your estimates /// are wrong and the multiplier is applied silently. static double projectedLoad( List plan, CapacityProfile profile, ) { double total = 0; for (Commitment commitment in plan) { total = total + (commitment.estMinutes * profile.multiplierFor(commitment.category)); } return total; } static CapacityVerdict check( List plan, CapacityProfile profile, int weekday, ) { final double projected = projectedLoad(plan, profile); final double historical = profile.capacityFor(weekday); final double allowed = historical * headroom; // No history yet — the app has not earned the right to block anything. if (historical <= 0) { return CapacityVerdict( projectedMinutes: projected, historicalMinutes: 0, allowedMinutes: 0, blocked: false, message: "Not enough history yet to judge this plan.", ); } if (projected <= allowed) { return CapacityVerdict( projectedMinutes: projected, historicalMinutes: historical, allowedMinutes: allowed, blocked: false, message: "This plan fits what you actually get done.", ); } final double excess = projected - allowed; return CapacityVerdict( projectedMinutes: projected, historicalMinutes: historical, allowedMinutes: allowed, blocked: true, excessMinutes: excess, message: "You have allocated ${_hours(projected)} of tasks into a day where you historically complete ${_hours(historical)}. Cut ${_hours(excess)}.", ); } /// The learned multiplier for a category, from what you said against what it /// took. Surfaced in the weekly review rather than hidden. static double learnMultiplier( List estimatedMinutes, List actualMinutes, ) { if (estimatedMinutes.isEmpty || estimatedMinutes.length != actualMinutes.length) { return 1.0; } double estimated = 0; double actual = 0; for (int index = 0; index < estimatedMinutes.length; index++) { estimated = estimated + estimatedMinutes[index]; actual = actual + actualMinutes[index]; } if (estimated <= 0) { return 1.0; } return actual / estimated; } static String _hours(double minutes) { if (minutes < 60) { return "${minutes.round()}min"; } final double hours = minutes / 60; return "${hours.toStringAsFixed(1)}h"; } }