Implement ST-303 activity autocomplete
This commit is contained in:
parent
307686de68
commit
8ab83205dd
10 changed files with 355 additions and 10 deletions
|
|
@ -25,6 +25,7 @@ product, niet als medisch hulpmiddel. Release 1 blijft smal:
|
|||
- statusflows voor activiteiten van vandaag (`gepland`, `uitgevoerd`, `overgeslagen`, `aangepast`)
|
||||
- contextuele evaluatievelden voor overgeslagen en aangepaste activiteiten
|
||||
- dagoverzicht op planning met gepland versus werkelijk en statusverdeling
|
||||
- autocomplete op basis van eerdere eigen activiteiten voor sneller hergebruik in planning
|
||||
- energiemeter met lopend totaal ten opzichte van het dagbudget
|
||||
- niet-blokkerende waarschuwing bij budgetoverschrijding in planning en dashboard
|
||||
- eerste unit tests voor budget- en meterlogica via `Vitest`
|
||||
|
|
@ -134,6 +135,6 @@ zichtbaar als `NEXT_PUBLIC_ENABLE_TEST_WIZARD=true` staat.
|
|||
|
||||
## Eerstvolgende bouwstappen
|
||||
|
||||
1. `ST-303` Autocomplete op eerdere activiteiten toevoegen
|
||||
2. `ST-105` RLS-policy tests en hardening afronden
|
||||
3. `npm run test` toevoegen aan CI
|
||||
1. `ST-105` RLS-policy tests en hardening afronden
|
||||
2. logging en monitoring toevoegen
|
||||
3. rate limiting op kritieke mutaties
|
||||
|
|
|
|||
|
|
@ -94,11 +94,13 @@ export default async function PlanningPage({ searchParams }: PlanningPageProps)
|
|||
<ActivityForm
|
||||
categories={planningPageData.categories}
|
||||
activities={planningPageData.activities}
|
||||
suggestions={planningPageData.suggestions}
|
||||
dailyBudget={checkInStatus?.todayCheckIn?.dailyBudget ?? null}
|
||||
/>
|
||||
<AdHocActivityForm
|
||||
categories={planningPageData.categories}
|
||||
activities={planningPageData.activities}
|
||||
suggestions={planningPageData.suggestions}
|
||||
dailyBudget={checkInStatus?.todayCheckIn?.dailyBudget ?? null}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { useActionState, useMemo, useState } from "react";
|
||||
import { createActivityAction } from "@/app/planning/actions";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { ActivitySuggestionList } from "@/components/planning/activity-suggestion-list";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
|
|
@ -27,16 +28,22 @@ import {
|
|||
ACTIVITY_PRIORITY_OPTIONS,
|
||||
} from "@/lib/planning/form-options";
|
||||
import { calculatePlanningMeterSnapshot, deriveActivityEnergyPoints } from "@/lib/planning/meter";
|
||||
import type { ActivityCategory, ActivityRecord } from "@/lib/planning/types";
|
||||
import type { ActivityCategory, ActivityRecord, ActivitySuggestion } from "@/lib/planning/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ActivityFormProps = {
|
||||
categories: ActivityCategory[];
|
||||
activities: ActivityRecord[];
|
||||
suggestions: ActivitySuggestion[];
|
||||
dailyBudget: number | null;
|
||||
};
|
||||
|
||||
export function ActivityForm({ categories, activities, dailyBudget }: ActivityFormProps) {
|
||||
export function ActivityForm({
|
||||
categories,
|
||||
activities,
|
||||
suggestions,
|
||||
dailyBudget,
|
||||
}: ActivityFormProps) {
|
||||
const [, formAction, isPending] = useActionState(createActivityAction, null);
|
||||
const [name, setName] = useState("");
|
||||
const [categoryId, setCategoryId] = useState<string>(categories[0]?.id ?? "");
|
||||
|
|
@ -83,6 +90,14 @@ export function ActivityForm({ categories, activities, dailyBudget }: ActivityFo
|
|||
);
|
||||
}, [activities, dailyBudget, durationMinutes, impactLevel, previewPoints]);
|
||||
|
||||
function applySuggestion(suggestion: ActivitySuggestion) {
|
||||
setName(suggestion.name);
|
||||
setCategoryId(suggestion.categoryId);
|
||||
setDurationMinutes(String(suggestion.durationMinutes));
|
||||
setImpactLevel(suggestion.impactLevel);
|
||||
setPriorityLevel(suggestion.priorityLevel);
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={formAction} className="space-y-6" aria-busy={isPending}>
|
||||
<input type="hidden" name="categoryId" value={categoryId} />
|
||||
|
|
@ -117,6 +132,13 @@ export function ActivityForm({ categories, activities, dailyBudget }: ActivityFo
|
|||
value={name}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
/>
|
||||
<ActivitySuggestionList
|
||||
categories={categories}
|
||||
suggestions={suggestions}
|
||||
query={name}
|
||||
disabled={isPending}
|
||||
onSelect={applySuggestion}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
|
|
|
|||
107
components/planning/activity-suggestion-list.tsx
Normal file
107
components/planning/activity-suggestion-list.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import type { ActivityCategory, ActivitySuggestion } from "@/lib/planning/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ActivitySuggestionListProps = {
|
||||
categories: ActivityCategory[];
|
||||
suggestions: ActivitySuggestion[];
|
||||
query: string;
|
||||
disabled?: boolean;
|
||||
showPriority?: boolean;
|
||||
onSelect: (suggestion: ActivitySuggestion) => void;
|
||||
};
|
||||
|
||||
function normalizeQuery(value: string) {
|
||||
return value.trim().replace(/\s+/g, " ").toLocaleLowerCase("nl-NL");
|
||||
}
|
||||
|
||||
function getCategoryLabel(categories: ActivityCategory[], categoryId: string) {
|
||||
return categories.find((category) => category.id === categoryId)?.labelNl ?? "Onbekende categorie";
|
||||
}
|
||||
|
||||
function formatImpactLabel(value: ActivitySuggestion["impactLevel"]) {
|
||||
if (value === "laag") {
|
||||
return "Lage impact";
|
||||
}
|
||||
|
||||
if (value === "hoog") {
|
||||
return "Hoge impact";
|
||||
}
|
||||
|
||||
return "Middenimpact";
|
||||
}
|
||||
|
||||
function formatPriorityLabel(value: ActivitySuggestion["priorityLevel"]) {
|
||||
if (value === "laag") {
|
||||
return "Lage prioriteit";
|
||||
}
|
||||
|
||||
if (value === "hoog") {
|
||||
return "Hoge prioriteit";
|
||||
}
|
||||
|
||||
return "Normale prioriteit";
|
||||
}
|
||||
|
||||
export function ActivitySuggestionList({
|
||||
categories,
|
||||
suggestions,
|
||||
query,
|
||||
disabled = false,
|
||||
showPriority = true,
|
||||
onSelect,
|
||||
}: ActivitySuggestionListProps) {
|
||||
const normalizedQuery = normalizeQuery(query);
|
||||
const visibleSuggestions = useMemo(() => {
|
||||
const filtered = normalizedQuery
|
||||
? suggestions.filter((suggestion) =>
|
||||
normalizeQuery(suggestion.name).includes(normalizedQuery),
|
||||
)
|
||||
: suggestions;
|
||||
|
||||
return filtered.slice(0, normalizedQuery ? 5 : 4);
|
||||
}, [normalizedQuery, suggestions]);
|
||||
|
||||
if (visibleSuggestions.length === 0) {
|
||||
if (!normalizedQuery) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="text-sm leading-7 text-muted-foreground">
|
||||
Geen eerdere match gevonden. Je kunt deze activiteit gewoon nieuw opslaan.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{normalizedQuery ? "Vergelijkbare eerdere activiteiten" : "Snel hergebruiken"}
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
{visibleSuggestions.map((suggestion) => (
|
||||
<button
|
||||
key={suggestion.id}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => onSelect(suggestion)}
|
||||
className={cn(
|
||||
"rounded-[1.1rem] border border-border/65 bg-background/78 px-4 py-3 text-left transition hover:border-primary/35 hover:bg-card disabled:pointer-events-none disabled:opacity-60",
|
||||
)}
|
||||
>
|
||||
<span className="block text-sm font-semibold text-foreground">{suggestion.name}</span>
|
||||
<span className="mt-1 block text-sm leading-6 text-muted-foreground">
|
||||
{getCategoryLabel(categories, suggestion.categoryId)} · {suggestion.durationMinutes} min ·{" "}
|
||||
{formatImpactLabel(suggestion.impactLevel)}
|
||||
{showPriority ? ` · ${formatPriorityLabel(suggestion.priorityLevel)}` : ""}
|
||||
{suggestion.useCount > 1 ? ` · ${suggestion.useCount}× gebruikt` : ""}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import { useActionState, useMemo, useState } from "react";
|
||||
import { createAdHocActivityAction } from "@/app/planning/actions";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { ActivitySuggestionList } from "@/components/planning/activity-suggestion-list";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
|
|
@ -28,18 +29,20 @@ import {
|
|||
calculatePlanningMeterSnapshot,
|
||||
deriveActivityEnergyPoints,
|
||||
} from "@/lib/planning/meter";
|
||||
import type { ActivityCategory, ActivityRecord } from "@/lib/planning/types";
|
||||
import type { ActivityCategory, ActivityRecord, ActivitySuggestion } from "@/lib/planning/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type AdHocActivityFormProps = {
|
||||
categories: ActivityCategory[];
|
||||
activities: ActivityRecord[];
|
||||
suggestions: ActivitySuggestion[];
|
||||
dailyBudget: number | null;
|
||||
};
|
||||
|
||||
export function AdHocActivityForm({
|
||||
categories,
|
||||
activities,
|
||||
suggestions,
|
||||
dailyBudget,
|
||||
}: AdHocActivityFormProps) {
|
||||
const [, formAction, isPending] = useActionState(createAdHocActivityAction, null);
|
||||
|
|
@ -87,6 +90,13 @@ export function AdHocActivityForm({
|
|||
);
|
||||
}, [activities, dailyBudget, durationMinutes, impactLevel, previewPoints]);
|
||||
|
||||
function applySuggestion(suggestion: ActivitySuggestion) {
|
||||
setName(suggestion.name);
|
||||
setCategoryId(suggestion.categoryId);
|
||||
setDurationMinutes(String(suggestion.durationMinutes));
|
||||
setImpactLevel(suggestion.impactLevel);
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={formAction} className="space-y-6" aria-busy={isPending}>
|
||||
<input type="hidden" name="categoryId" value={categoryId} />
|
||||
|
|
@ -121,6 +131,14 @@ export function AdHocActivityForm({
|
|||
value={name}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
/>
|
||||
<ActivitySuggestionList
|
||||
categories={categories}
|
||||
suggestions={suggestions}
|
||||
query={name}
|
||||
disabled={isPending}
|
||||
showPriority={false}
|
||||
onSelect={applySuggestion}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
|
|
|
|||
|
|
@ -78,13 +78,13 @@ Status: `ST-201`, `ST-202`, `ST-203`, `ST-204` en `ST-205` zijn inmiddels gereal
|
|||
|
||||
Doel: de gebruiker kan activiteiten voor de dag plannen binnen een eenvoudig energiemodel.
|
||||
|
||||
Status: `ST-301`, `ST-302`, `ST-304` en `ST-305` zijn inmiddels gerealiseerd in de app. De volgende logische stap ligt nu in `ST-303` en `EPIC-05`.
|
||||
Status: `ST-301`, `ST-302`, `ST-303`, `ST-304` en `ST-305` zijn inmiddels gerealiseerd in de app. De dagplanningloop voor release 1 is daarmee functioneel rond.
|
||||
|
||||
| Story ID | Titel | Type | Definition of done |
|
||||
| --- | --- | --- | --- |
|
||||
| ST-301 | Datamodel voor activiteiten implementeren | Build | Afgerond: migraties en seed-data voor categorieën en skip-redenen zijn aanwezig |
|
||||
| ST-302 | Planningformulier bouwen | UI | Afgerond: activiteit kan met naam, categorie, duur, impact en prioriteit worden aangemaakt |
|
||||
| ST-303 | Autocomplete op eerdere activiteiten toevoegen | UX | Veelgebruikte activiteiten zijn snel opnieuw te kiezen |
|
||||
| ST-303 | Autocomplete op eerdere activiteiten toevoegen | UX | Afgerond: planning en ad-hocformulier bieden nu snelle hergebruiksuggesties uit eigen historie |
|
||||
| ST-304 | EnergyMeter en lopend totaal implementeren | Logic/UI | Afgerond: totaal update direct na elke wijziging |
|
||||
| ST-305 | Overschrijdingswaarschuwing toevoegen | UX | Afgerond: gebruiker krijgt feedback maar behoudt regie |
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { getAuthenticatedUser } from "@/lib/auth/session";
|
||||
import { getLocalDateForTimezone } from "@/lib/dates";
|
||||
import { calculateDayOverviewSnapshot } from "@/lib/planning/day-overview";
|
||||
import { buildActivitySuggestions } from "@/lib/planning/suggestions";
|
||||
import type {
|
||||
ActivityCategory,
|
||||
CreateAdHocActivitySubmission,
|
||||
|
|
@ -130,6 +131,26 @@ async function readActivitiesByDate(
|
|||
return (data ?? []).map(mapActivityRow);
|
||||
}
|
||||
|
||||
async function readRecentActivitiesForSuggestions(
|
||||
supabase: SupabaseServerClient,
|
||||
userId: string,
|
||||
currentActivityDate: string,
|
||||
): Promise<ActivityRecord[]> {
|
||||
const { data, error } = await supabase
|
||||
.from("activities")
|
||||
.select(ACTIVITY_COLUMNS)
|
||||
.eq("user_id", userId)
|
||||
.neq("activity_date", currentActivityDate)
|
||||
.order("updated_at", { ascending: false })
|
||||
.limit(60);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Activiteitensuggesties konden niet worden geladen: ${error.message}`);
|
||||
}
|
||||
|
||||
return (data ?? []).map(mapActivityRow);
|
||||
}
|
||||
|
||||
async function assertCategoryExists(
|
||||
supabase: SupabaseServerClient,
|
||||
categoryId: string,
|
||||
|
|
@ -250,19 +271,24 @@ export async function getPlanningPageDataForCurrentUser(): Promise<PlanningPageD
|
|||
return null;
|
||||
}
|
||||
|
||||
const [categories, skipReasons, activitiesStatus] = await Promise.all([
|
||||
const currentActivityDate = getLocalDateForTimezone(profileBundle.profile.timezone);
|
||||
const supabase = await createClient();
|
||||
|
||||
const [categories, skipReasons, activitiesStatus, recentActivities] = await Promise.all([
|
||||
listActivityCategories(),
|
||||
listSkipReasons(),
|
||||
getTodayActivitiesForCurrentUser(),
|
||||
readRecentActivitiesForSuggestions(supabase, user.id, currentActivityDate),
|
||||
]);
|
||||
|
||||
return {
|
||||
timezone: profileBundle.profile.timezone,
|
||||
activityDate:
|
||||
activitiesStatus?.activityDate ?? getLocalDateForTimezone(profileBundle.profile.timezone),
|
||||
activitiesStatus?.activityDate ?? currentActivityDate,
|
||||
categories,
|
||||
skipReasons,
|
||||
activities: activitiesStatus?.activities ?? [],
|
||||
suggestions: buildActivitySuggestions(recentActivities),
|
||||
dayOverview: calculateDayOverviewSnapshot(activitiesStatus?.activities ?? []),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
75
lib/planning/suggestions.test.ts
Normal file
75
lib/planning/suggestions.test.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { buildActivitySuggestions } from "./suggestions";
|
||||
|
||||
describe("buildActivitySuggestions", () => {
|
||||
it("dedupliceert dezelfde activiteit en telt gebruik", () => {
|
||||
const suggestions = buildActivitySuggestions([
|
||||
{
|
||||
name: " Was opvouwen ",
|
||||
categoryId: "cat-a",
|
||||
durationMinutes: 30,
|
||||
impactLevel: "laag",
|
||||
priorityLevel: "normaal",
|
||||
createdAt: "2026-04-18T08:00:00.000Z",
|
||||
updatedAt: "2026-04-18T08:00:00.000Z",
|
||||
},
|
||||
{
|
||||
name: "Was opvouwen",
|
||||
categoryId: "cat-a",
|
||||
durationMinutes: 30,
|
||||
impactLevel: "laag",
|
||||
priorityLevel: "normaal",
|
||||
createdAt: "2026-04-19T08:00:00.000Z",
|
||||
updatedAt: "2026-04-19T09:00:00.000Z",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(suggestions).toHaveLength(1);
|
||||
expect(suggestions[0]).toMatchObject({
|
||||
name: "Was opvouwen",
|
||||
categoryId: "cat-a",
|
||||
durationMinutes: 30,
|
||||
impactLevel: "laag",
|
||||
priorityLevel: "normaal",
|
||||
useCount: 2,
|
||||
lastUsedAt: "2026-04-19T09:00:00.000Z",
|
||||
});
|
||||
});
|
||||
|
||||
it("sorteert op meest recent gebruik en houdt verschillende varianten apart", () => {
|
||||
const suggestions = buildActivitySuggestions([
|
||||
{
|
||||
name: "Boodschappen",
|
||||
categoryId: "cat-a",
|
||||
durationMinutes: 45,
|
||||
impactLevel: "midden",
|
||||
priorityLevel: "hoog",
|
||||
createdAt: "2026-04-17T08:00:00.000Z",
|
||||
updatedAt: "2026-04-17T08:00:00.000Z",
|
||||
},
|
||||
{
|
||||
name: "Telefoontje",
|
||||
categoryId: "cat-b",
|
||||
durationMinutes: 15,
|
||||
impactLevel: "laag",
|
||||
priorityLevel: "normaal",
|
||||
createdAt: "2026-04-19T10:00:00.000Z",
|
||||
updatedAt: "2026-04-19T10:00:00.000Z",
|
||||
},
|
||||
{
|
||||
name: "Boodschappen",
|
||||
categoryId: "cat-a",
|
||||
durationMinutes: 45,
|
||||
impactLevel: "midden",
|
||||
priorityLevel: "laag",
|
||||
createdAt: "2026-04-18T08:00:00.000Z",
|
||||
updatedAt: "2026-04-18T08:00:00.000Z",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(suggestions).toHaveLength(3);
|
||||
expect(suggestions[0].name).toBe("Telefoontje");
|
||||
expect(suggestions[1].priorityLevel).toBe("laag");
|
||||
expect(suggestions[2].priorityLevel).toBe("hoog");
|
||||
});
|
||||
});
|
||||
82
lib/planning/suggestions.ts
Normal file
82
lib/planning/suggestions.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import type { ActivityRecord, ActivitySuggestion } from "./types";
|
||||
|
||||
type ActivitySuggestionInput = Pick<
|
||||
ActivityRecord,
|
||||
| "name"
|
||||
| "categoryId"
|
||||
| "durationMinutes"
|
||||
| "impactLevel"
|
||||
| "priorityLevel"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
>;
|
||||
|
||||
function normalizeSuggestionName(value: string) {
|
||||
return value.trim().replace(/\s+/g, " ").toLocaleLowerCase("nl-NL");
|
||||
}
|
||||
|
||||
function cleanSuggestionName(value: string) {
|
||||
return value.trim().replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
function buildSuggestionKey(activity: ActivitySuggestionInput) {
|
||||
return [
|
||||
normalizeSuggestionName(activity.name),
|
||||
activity.categoryId,
|
||||
activity.durationMinutes,
|
||||
activity.impactLevel,
|
||||
activity.priorityLevel,
|
||||
].join("::");
|
||||
}
|
||||
|
||||
function compareSuggestions(a: ActivitySuggestion, b: ActivitySuggestion) {
|
||||
if (a.lastUsedAt !== b.lastUsedAt) {
|
||||
return b.lastUsedAt.localeCompare(a.lastUsedAt);
|
||||
}
|
||||
|
||||
if (a.useCount !== b.useCount) {
|
||||
return b.useCount - a.useCount;
|
||||
}
|
||||
|
||||
return a.name.localeCompare(b.name, "nl-NL");
|
||||
}
|
||||
|
||||
export function buildActivitySuggestions(
|
||||
activities: ActivitySuggestionInput[],
|
||||
limit = 12,
|
||||
): ActivitySuggestion[] {
|
||||
const suggestions = new Map<string, ActivitySuggestion>();
|
||||
|
||||
for (const activity of activities) {
|
||||
const key = buildSuggestionKey(activity);
|
||||
const lastUsedAt = activity.updatedAt ?? activity.createdAt;
|
||||
const existing = suggestions.get(key);
|
||||
|
||||
if (!existing) {
|
||||
suggestions.set(key, {
|
||||
id: key,
|
||||
name: cleanSuggestionName(activity.name),
|
||||
categoryId: activity.categoryId,
|
||||
durationMinutes: activity.durationMinutes,
|
||||
impactLevel: activity.impactLevel,
|
||||
priorityLevel: activity.priorityLevel,
|
||||
lastUsedAt,
|
||||
useCount: 1,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
existing.useCount += 1;
|
||||
|
||||
if (lastUsedAt.localeCompare(existing.lastUsedAt) > 0) {
|
||||
existing.lastUsedAt = lastUsedAt;
|
||||
existing.name = cleanSuggestionName(activity.name);
|
||||
existing.categoryId = activity.categoryId;
|
||||
existing.durationMinutes = activity.durationMinutes;
|
||||
existing.impactLevel = activity.impactLevel;
|
||||
existing.priorityLevel = activity.priorityLevel;
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(suggestions.values()).sort(compareSuggestions).slice(0, limit);
|
||||
}
|
||||
|
|
@ -46,6 +46,17 @@ export type ActivityRecord = {
|
|||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type ActivitySuggestion = {
|
||||
id: string;
|
||||
name: string;
|
||||
categoryId: string;
|
||||
durationMinutes: number;
|
||||
impactLevel: ActivityImpactLevel;
|
||||
priorityLevel: ActivityPriorityLevel;
|
||||
lastUsedAt: string;
|
||||
useCount: number;
|
||||
};
|
||||
|
||||
export type CreateActivitySubmission = {
|
||||
name: string;
|
||||
categoryId: string;
|
||||
|
|
@ -84,5 +95,6 @@ export type PlanningPageData = {
|
|||
categories: ActivityCategory[];
|
||||
skipReasons: SkipReason[];
|
||||
activities: ActivityRecord[];
|
||||
suggestions: ActivitySuggestion[];
|
||||
dayOverview: DayOverviewSnapshot;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue