Implement ST-304 energy meter
This commit is contained in:
parent
5c15620993
commit
c3e936b0db
10 changed files with 360 additions and 12 deletions
89
lib/planning/meter.test.ts
Normal file
89
lib/planning/meter.test.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
calculatePlannedPointsTotal,
|
||||
calculatePlanningMeterSnapshot,
|
||||
deriveActivityEnergyPoints,
|
||||
} from "./meter";
|
||||
|
||||
describe("deriveActivityEnergyPoints", () => {
|
||||
it("geeft 1 punt voor een korte lage activiteit", () => {
|
||||
expect(
|
||||
deriveActivityEnergyPoints({ durationMinutes: 15, impactLevel: "laag", status: "planned" }),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it("geeft 2 punten voor een middellange middenactiviteit", () => {
|
||||
expect(
|
||||
deriveActivityEnergyPoints({
|
||||
durationMinutes: 30,
|
||||
impactLevel: "midden",
|
||||
status: "planned",
|
||||
}),
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
it("geeft 4 punten voor een langere hoge activiteit", () => {
|
||||
expect(
|
||||
deriveActivityEnergyPoints({ durationMinutes: 60, impactLevel: "hoog", status: "planned" }),
|
||||
).toBe(4);
|
||||
});
|
||||
|
||||
it("telt een geskipt item niet mee", () => {
|
||||
expect(
|
||||
deriveActivityEnergyPoints({ durationMinutes: 45, impactLevel: "hoog", status: "skipped" }),
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("calculatePlanningMeterSnapshot", () => {
|
||||
it("somt punten van activiteiten op", () => {
|
||||
expect(
|
||||
calculatePlannedPointsTotal([
|
||||
{ durationMinutes: 30, impactLevel: "midden", status: "planned" },
|
||||
{ durationMinutes: 90, impactLevel: "laag", status: "planned" },
|
||||
]),
|
||||
).toBe(4);
|
||||
});
|
||||
|
||||
it("berekent resterend budget en percentage", () => {
|
||||
const snapshot = calculatePlanningMeterSnapshot(
|
||||
[
|
||||
{ durationMinutes: 30, impactLevel: "midden", status: "planned" },
|
||||
{ durationMinutes: 60, impactLevel: "hoog", status: "planned" },
|
||||
],
|
||||
8,
|
||||
);
|
||||
|
||||
expect(snapshot.plannedPoints).toBe(6);
|
||||
expect(snapshot.remainingBudget).toBe(2);
|
||||
expect(snapshot.progressPercent).toBe(75);
|
||||
expect(snapshot.isOverBudget).toBe(false);
|
||||
});
|
||||
|
||||
it("werkt ook zonder dagbudget", () => {
|
||||
const snapshot = calculatePlanningMeterSnapshot(
|
||||
[{ durationMinutes: 45, impactLevel: "midden", status: "planned" }],
|
||||
null,
|
||||
);
|
||||
|
||||
expect(snapshot.plannedPoints).toBe(2);
|
||||
expect(snapshot.dailyBudget).toBeNull();
|
||||
expect(snapshot.remainingBudget).toBeNull();
|
||||
expect(snapshot.progressPercent).toBeNull();
|
||||
});
|
||||
|
||||
it("markeert overschrijding zonder alarmerende blokkade", () => {
|
||||
const snapshot = calculatePlanningMeterSnapshot(
|
||||
[
|
||||
{ durationMinutes: 60, impactLevel: "hoog", status: "planned" },
|
||||
{ durationMinutes: 120, impactLevel: "hoog", status: "planned" },
|
||||
],
|
||||
6,
|
||||
);
|
||||
|
||||
expect(snapshot.plannedPoints).toBe(9);
|
||||
expect(snapshot.remainingBudget).toBe(-3);
|
||||
expect(snapshot.isOverBudget).toBe(true);
|
||||
expect(snapshot.progressPercent).toBe(100);
|
||||
});
|
||||
});
|
||||
101
lib/planning/meter.ts
Normal file
101
lib/planning/meter.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import type {
|
||||
ActivityImpactLevel,
|
||||
ActivityRecord,
|
||||
ActivityStatus,
|
||||
} from "@/lib/planning/types";
|
||||
|
||||
export type PlanningMeterSnapshot = {
|
||||
plannedPoints: number;
|
||||
activityCount: number;
|
||||
dailyBudget: number | null;
|
||||
remainingBudget: number | null;
|
||||
progressRatio: number | null;
|
||||
progressPercent: number | null;
|
||||
isOverBudget: boolean;
|
||||
};
|
||||
|
||||
type ActivityMeterInput = {
|
||||
durationMinutes: number;
|
||||
impactLevel: ActivityImpactLevel;
|
||||
status?: ActivityStatus;
|
||||
};
|
||||
|
||||
function deriveDurationBandPoints(durationMinutes: number) {
|
||||
if (durationMinutes <= 15) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (durationMinutes <= 45) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (durationMinutes <= 90) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
function deriveImpactAdjustment(impactLevel: ActivityImpactLevel) {
|
||||
if (impactLevel === "laag") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (impactLevel === "hoog") {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function deriveActivityEnergyPoints(input: ActivityMeterInput): number {
|
||||
if (input.status === "skipped") {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.max(
|
||||
1,
|
||||
deriveDurationBandPoints(input.durationMinutes) + deriveImpactAdjustment(input.impactLevel),
|
||||
);
|
||||
}
|
||||
|
||||
export function calculatePlannedPointsTotal(
|
||||
activities: Pick<ActivityRecord, "durationMinutes" | "impactLevel" | "status">[],
|
||||
): number {
|
||||
return activities.reduce(
|
||||
(total, activity) => total + deriveActivityEnergyPoints(activity),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
export function calculatePlanningMeterSnapshot(
|
||||
activities: Pick<ActivityRecord, "durationMinutes" | "impactLevel" | "status">[],
|
||||
dailyBudget: number | null,
|
||||
): PlanningMeterSnapshot {
|
||||
const plannedPoints = calculatePlannedPointsTotal(activities);
|
||||
|
||||
if (dailyBudget === null) {
|
||||
return {
|
||||
plannedPoints,
|
||||
activityCount: activities.length,
|
||||
dailyBudget: null,
|
||||
remainingBudget: null,
|
||||
progressRatio: null,
|
||||
progressPercent: null,
|
||||
isOverBudget: false,
|
||||
};
|
||||
}
|
||||
|
||||
const remainingBudget = dailyBudget - plannedPoints;
|
||||
const progressRatio = dailyBudget > 0 ? plannedPoints / dailyBudget : 0;
|
||||
|
||||
return {
|
||||
plannedPoints,
|
||||
activityCount: activities.length,
|
||||
dailyBudget,
|
||||
remainingBudget,
|
||||
progressRatio,
|
||||
progressPercent: Math.min(100, Math.round(progressRatio * 100)),
|
||||
isOverBudget: remainingBudget < 0,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue