Implement ST-304 energy meter
This commit is contained in:
parent
5c15620993
commit
c3e936b0db
10 changed files with 360 additions and 12 deletions
|
|
@ -25,14 +25,17 @@ import {
|
|||
ACTIVITY_IMPACT_OPTIONS,
|
||||
ACTIVITY_PRIORITY_OPTIONS,
|
||||
} from "@/lib/planning/form-options";
|
||||
import type { ActivityCategory } from "@/lib/planning/types";
|
||||
import { calculatePlanningMeterSnapshot, deriveActivityEnergyPoints } from "@/lib/planning/meter";
|
||||
import type { ActivityCategory, ActivityRecord } from "@/lib/planning/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ActivityFormProps = {
|
||||
categories: ActivityCategory[];
|
||||
activities: ActivityRecord[];
|
||||
dailyBudget: number | null;
|
||||
};
|
||||
|
||||
export function ActivityForm({ categories }: ActivityFormProps) {
|
||||
export function ActivityForm({ categories, activities, dailyBudget }: ActivityFormProps) {
|
||||
const [, formAction, isPending] = useActionState(createActivityAction, null);
|
||||
const [name, setName] = useState("");
|
||||
const [categoryId, setCategoryId] = useState<string>(categories[0]?.id ?? "");
|
||||
|
|
@ -44,6 +47,40 @@ export function ActivityForm({ categories }: ActivityFormProps) {
|
|||
() => categories.find((category) => category.id === categoryId) ?? null,
|
||||
[categories, categoryId],
|
||||
);
|
||||
const currentMeter = useMemo(
|
||||
() => calculatePlanningMeterSnapshot(activities, dailyBudget),
|
||||
[activities, dailyBudget],
|
||||
);
|
||||
const previewPoints = useMemo(() => {
|
||||
const parsedDuration = Number.parseInt(durationMinutes, 10);
|
||||
|
||||
if (!Number.isFinite(parsedDuration) || parsedDuration <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return deriveActivityEnergyPoints({
|
||||
durationMinutes: parsedDuration,
|
||||
impactLevel,
|
||||
status: "planned",
|
||||
});
|
||||
}, [durationMinutes, impactLevel]);
|
||||
const previewMeter = useMemo(() => {
|
||||
if (previewPoints === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return calculatePlanningMeterSnapshot(
|
||||
[
|
||||
...activities,
|
||||
{
|
||||
durationMinutes: Number.parseInt(durationMinutes, 10),
|
||||
impactLevel,
|
||||
status: "planned",
|
||||
} as ActivityRecord,
|
||||
],
|
||||
dailyBudget,
|
||||
);
|
||||
}, [activities, dailyBudget, durationMinutes, impactLevel, previewPoints]);
|
||||
|
||||
return (
|
||||
<form action={formAction} className="space-y-6" aria-busy={isPending}>
|
||||
|
|
@ -148,6 +185,23 @@ export function ActivityForm({ categories }: ActivityFormProps) {
|
|||
|
||||
<Separator />
|
||||
|
||||
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0 shadow-none">
|
||||
<CardContent className="space-y-2 py-5">
|
||||
<p className="text-sm font-semibold text-slate-900">Vooruitblik op de meter</p>
|
||||
<p className="text-sm leading-7 text-muted-foreground">
|
||||
{previewPoints === null
|
||||
? "Kies een geldige duur en impact om te zien hoeveel punten deze activiteit ongeveer toevoegt."
|
||||
: `Deze activiteit telt voorlopig voor ${previewPoints} punten. Je totaal zou dan uitkomen op ${previewMeter?.plannedPoints ?? currentMeter.plannedPoints} geplande punten.`}
|
||||
</p>
|
||||
{dailyBudget !== null && previewMeter ? (
|
||||
<p className="text-sm leading-7 text-slate-700">
|
||||
Dat is {previewMeter.dailyBudget} punten budget, met daarna nog{" "}
|
||||
<strong>{previewMeter.remainingBudget} punten ruimte</strong>.
|
||||
</p>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
|
|
@ -244,7 +298,7 @@ export function ActivityForm({ categories }: ActivityFormProps) {
|
|||
<p className="text-sm leading-7 text-muted-foreground">
|
||||
{isPending
|
||||
? "Je activiteit wordt opgeslagen..."
|
||||
: "Je activiteit wordt vandaag toegevoegd met status `gepland`."}
|
||||
: "Je activiteit wordt vandaag toegevoegd met status `gepland`, waarna de meter direct opnieuw wordt berekend."}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
|
|
|
|||
78
components/planning/energy-meter-card.tsx
Normal file
78
components/planning/energy-meter-card.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import type { PlanningMeterSnapshot } from "@/lib/planning/meter";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type EnergyMeterCardProps = {
|
||||
meter: PlanningMeterSnapshot;
|
||||
tone?: "default" | "subtle";
|
||||
};
|
||||
|
||||
function formatRemainingLabel(remainingBudget: number) {
|
||||
if (remainingBudget > 0) {
|
||||
return `${remainingBudget} punten over`;
|
||||
}
|
||||
|
||||
if (remainingBudget === 0) {
|
||||
return "Geen punten over";
|
||||
}
|
||||
|
||||
return `${Math.abs(remainingBudget)} punten erboven`;
|
||||
}
|
||||
|
||||
export function EnergyMeterCard({
|
||||
meter,
|
||||
tone = "default",
|
||||
}: EnergyMeterCardProps) {
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
"rounded-[1.75rem] border border-border/60 py-0 shadow-[0_12px_40px_rgba(71,85,105,0.08)]",
|
||||
tone === "default" ? "bg-card/90" : "bg-white/70",
|
||||
)}
|
||||
>
|
||||
<CardHeader className="pb-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
|
||||
EnergyMeter
|
||||
</p>
|
||||
<CardTitle className="text-lg text-slate-900">
|
||||
{meter.dailyBudget === null
|
||||
? `${meter.plannedPoints} geplande punten`
|
||||
: `${meter.plannedPoints} van ${meter.dailyBudget} punten gepland`}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 pb-6">
|
||||
<CardDescription className="text-sm leading-7 text-muted-foreground">
|
||||
{meter.dailyBudget === null
|
||||
? "Er is nog geen dagbudget beschikbaar. De meter wordt actief zodra je ochtendcheck-in van vandaag er staat."
|
||||
: "De meter blijft bewust eenvoudig: punten volgen uit duur en impact van je activiteiten."}
|
||||
</CardDescription>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="h-3 overflow-hidden rounded-full bg-secondary">
|
||||
<div
|
||||
className="h-full rounded-full bg-primary transition-[width]"
|
||||
style={{ width: `${meter.progressPercent ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 text-sm leading-7 text-slate-700">
|
||||
<p>
|
||||
<strong>Activiteiten:</strong> {meter.activityCount}
|
||||
</p>
|
||||
{meter.dailyBudget !== null ? (
|
||||
<p>
|
||||
<strong>Resterend:</strong> {formatRemainingLabel(meter.remainingBudget ?? 0)}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import {
|
|||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { deriveActivityEnergyPoints } from "@/lib/planning/meter";
|
||||
import type { ActivityCategory, ActivityRecord } from "@/lib/planning/types";
|
||||
|
||||
type TodayActivitiesListProps = {
|
||||
|
|
@ -89,6 +90,10 @@ export function TodayActivitiesList({
|
|||
<p>
|
||||
<strong>Prioriteit:</strong> {formatPriorityLabel(activity.priorityLevel)}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Punten:</strong>{" "}
|
||||
{deriveActivityEnergyPoints(activity)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue