favicon and some TODOs

This commit is contained in:
2026-09-18 16:47:15 -04:00
parent 748702a806
commit 31d5322b38
11 changed files with 708 additions and 100 deletions
+59 -3
View File
@@ -1,6 +1,16 @@
import type { Config, EstimationResult, MissingPeriod, Plan, Selections } from "./types";
import type {
BalanceComparison,
Config,
EstimationResult,
MissingPeriod,
Plan,
Selections,
} from "./types";
import { clampDate, daysBetween, inclusiveDayCount, overlapDays } from "./dates";
/** A balance is treated as "on track" when within this fraction of expected. */
const ON_TRACK_TOLERANCE = 0.02;
/** The active missing periods for a set of ids, in config order. */
function activePeriods(config: Config, ids: Set<string>): MissingPeriod[] {
return config.missingPeriods.filter((p) => ids.has(p.id));
@@ -76,18 +86,64 @@ export function estimate(config: Config, selections: Selections): EstimationResu
const consumedMealBlocks = clamp(blockRate * consumedDays, 0, plan.totalMealBlocks);
const consumedFlexDollars = clamp(flexRate * consumedDays, 0, plan.totalFlexDollars);
const remainingMealBlocks = plan.totalMealBlocks - consumedMealBlocks;
const remainingFlexDollars = plan.totalFlexDollars - consumedFlexDollars;
// Active days still ahead of the target date (for catch-up guidance).
const activeDaysRemaining = Math.max(0, total - consumedDays);
return {
perWeekMealBlocks: blockRate * 7,
perWeekFlexDollars: flexRate * 7,
consumedMealBlocks,
consumedFlexDollars,
remainingMealBlocks: plan.totalMealBlocks - consumedMealBlocks,
remainingFlexDollars: plan.totalFlexDollars - consumedFlexDollars,
remainingMealBlocks,
remainingFlexDollars,
totalActiveDays: total,
activeDaysConsumed: consumedDays,
activeDaysRemaining,
blockComparison: compareBalance(
selections.actualMealBlocks,
remainingMealBlocks,
activeDaysRemaining
),
flexComparison: compareBalance(
selections.actualFlexDollars,
remainingFlexDollars,
activeDaysRemaining
),
};
}
/**
* Compare an actual balance the user entered against the on-pace expected
* remaining amount. Returns null when no actual balance was entered.
*
* A higher actual balance than expected means the student is spending slower
* than the even pace (ahead / not running low). The suggested per-week rate is
* the pace that would use the actual balance up exactly over the days left.
*/
export function compareBalance(
actual: number | null,
expectedRemaining: number,
activeDaysRemaining: number
): BalanceComparison | null {
if (actual === null) return null;
const difference = actual - expectedRemaining;
let status: BalanceComparison["status"];
const tolerance = Math.max(ON_TRACK_TOLERANCE * expectedRemaining, 0.5);
if (Math.abs(difference) <= tolerance) status = "onTrack";
else if (difference > 0) status = "ahead";
else status = "behind";
const suggestedPerWeek =
activeDaysRemaining > 0 ? (actual / activeDaysRemaining) * 7 : null;
return { actual, expected: expectedRemaining, difference, status, suggestedPerWeek };
}
function clamp(v: number, lo: number, hi: number): number {
return Math.min(hi, Math.max(lo, v));
}