Add demo usage seeds and seed script polish
This commit is contained in:
parent
b8c52e7948
commit
1ea2fcb826
9 changed files with 498 additions and 9 deletions
|
|
@ -1,3 +1,5 @@
|
|||
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_your_key_here
|
||||
SUPABASE_SECRET_KEY=sb_secret_your_local_admin_key_here
|
||||
DEMO_USER_PASSWORD=DemoPassword123!
|
||||
NEXT_PUBLIC_ENABLE_TEST_WIZARD=false
|
||||
|
|
|
|||
|
|
@ -101,7 +101,8 @@ persona-set in [inspannings-monitor-08-gebruikerspersonas-v01.docx](/Users/janpe
|
|||
Benodigd:
|
||||
|
||||
- `NEXT_PUBLIC_SUPABASE_URL`
|
||||
- `SUPABASE_SERVICE_ROLE_KEY`
|
||||
- `SUPABASE_SECRET_KEY` voorkeur
|
||||
- `SUPABASE_SERVICE_ROLE_KEY` mag nog als legacy alias
|
||||
- `DEMO_USER_PASSWORD`
|
||||
|
||||
Uitvoeren:
|
||||
|
|
@ -110,6 +111,12 @@ Uitvoeren:
|
|||
2. zet de drie env-vars lokaal
|
||||
3. run `npm run seed:demo-users`
|
||||
|
||||
Voor bestaande lokale setups accepteert het script tijdelijk ook:
|
||||
- `NEXT_PUBLIC_SUPABASE_SERVICE_KEY`
|
||||
|
||||
Maar mijn advies is om voor seedscripts alleen deze nette niet-public adminnaam te gebruiken:
|
||||
- `SUPABASE_SECRET_KEY`
|
||||
|
||||
De seeddata zelf staat in:
|
||||
- [demo-personas.mjs](/Users/janpetervisser/Development/third/scripts/seed/demo-personas.mjs)
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ export function ActivityEvaluationFields({
|
|||
initialSkipReasonId ?? skipReasons[0]?.id ?? "",
|
||||
);
|
||||
const [notes, setNotes] = useState(initialNotes ?? "");
|
||||
const selectedSkipReason =
|
||||
skipReasons.find((skipReason) => skipReason.id === skipReasonId) ?? null;
|
||||
|
||||
if (status !== "skipped" && status !== "adjusted") {
|
||||
return null;
|
||||
|
|
@ -54,7 +56,9 @@ export function ActivityEvaluationFields({
|
|||
onValueChange={(value) => setSkipReasonId(value ?? skipReasons[0]?.id ?? "")}
|
||||
>
|
||||
<SelectTrigger className="h-11 w-full rounded-[1.15rem] bg-background/80 px-4 text-sm">
|
||||
<SelectValue placeholder="Kies een skip-reden" />
|
||||
<SelectValue placeholder="Kies een skip-reden">
|
||||
{selectedSkipReason?.labelNl}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{skipReasons.map((skipReason) => (
|
||||
|
|
|
|||
|
|
@ -150,7 +150,9 @@ export function ActivityForm({
|
|||
onValueChange={(value) => setCategoryId(value ?? categories[0]?.id ?? "")}
|
||||
>
|
||||
<SelectTrigger className="h-12 w-full rounded-[1.25rem] bg-background/80 px-4 text-base">
|
||||
<SelectValue placeholder="Kies een categorie" />
|
||||
<SelectValue placeholder="Kies een categorie">
|
||||
{selectedCategory?.labelNl}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((category) => (
|
||||
|
|
|
|||
|
|
@ -150,7 +150,9 @@ export function AdHocActivityForm({
|
|||
onValueChange={(value) => setCategoryId(value ?? categories[0]?.id ?? "")}
|
||||
>
|
||||
<SelectTrigger className="h-12 w-full rounded-[1.25rem] bg-background/80 px-4 text-base">
|
||||
<SelectValue placeholder="Kies een categorie" />
|
||||
<SelectValue placeholder="Kies een categorie">
|
||||
{selectedCategory?.labelNl}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((category) => (
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ export function SettingsForm({ profileBundle }: SettingsFormProps) {
|
|||
action={formAction}
|
||||
className="space-y-6"
|
||||
aria-busy={isPending}
|
||||
encType="multipart/form-data"
|
||||
>
|
||||
<input type="hidden" name="locale" value={locale} />
|
||||
<PreferenceHiddenFields draft={draft} />
|
||||
|
|
|
|||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import path from "node:path";
|
|||
import { fileURLToPath } from "node:url";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import { demoPersonas } from "./seed/demo-personas.mjs";
|
||||
import { demoUsageSeeds } from "./seed/demo-usage-seeds.mjs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = path.resolve(__dirname, "..");
|
||||
|
|
@ -47,13 +48,22 @@ await loadEnvFile(path.join(rootDir, ".env.local"));
|
|||
await loadEnvFile(path.join(rootDir, ".env"));
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
const serviceRoleKey =
|
||||
process.env.SUPABASE_SECRET_KEY ??
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY ??
|
||||
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_KEY;
|
||||
const demoUserPassword = process.env.DEMO_USER_PASSWORD;
|
||||
const isDryRun = process.argv.includes("--dry-run");
|
||||
|
||||
if (!supabaseUrl || !serviceRoleKey) {
|
||||
throw new Error(
|
||||
"Zet NEXT_PUBLIC_SUPABASE_URL en SUPABASE_SERVICE_ROLE_KEY in je omgeving voordat je demo-users seedt.",
|
||||
"Zet NEXT_PUBLIC_SUPABASE_URL en een admin key (liefst SUPABASE_SECRET_KEY, anders SUPABASE_SERVICE_ROLE_KEY) in je omgeving voordat je demo-users seedt.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!process.env.SUPABASE_SECRET_KEY && process.env.NEXT_PUBLIC_SUPABASE_SERVICE_KEY) {
|
||||
console.warn(
|
||||
"Let op: het seedscript gebruikt nu NEXT_PUBLIC_SUPABASE_SERVICE_KEY als fallback. Gebruik liever SUPABASE_SECRET_KEY in .env.local voor lokale admin-taken.",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +80,73 @@ const supabase = createClient(supabaseUrl, serviceRoleKey, {
|
|||
},
|
||||
});
|
||||
|
||||
const BUDGET_FORMULA_VERSION = 1;
|
||||
|
||||
function deriveEnergyLevel(energyScore) {
|
||||
if (energyScore <= 2) {
|
||||
return "zeer_laag";
|
||||
}
|
||||
|
||||
if (energyScore <= 4) {
|
||||
return "laag";
|
||||
}
|
||||
|
||||
if (energyScore <= 6) {
|
||||
return "midden";
|
||||
}
|
||||
|
||||
if (energyScore <= 8) {
|
||||
return "redelijk";
|
||||
}
|
||||
|
||||
return "hoog";
|
||||
}
|
||||
|
||||
function deriveBudgetSnapshot(energyScore) {
|
||||
return {
|
||||
energy_level: deriveEnergyLevel(energyScore),
|
||||
daily_budget: energyScore,
|
||||
budget_formula_version: BUDGET_FORMULA_VERSION,
|
||||
};
|
||||
}
|
||||
|
||||
function formatDatePart(value) {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
|
||||
function getDatePartsInTimezone(date, timezone) {
|
||||
const formatter = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: timezone,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
|
||||
const parts = formatter.formatToParts(date);
|
||||
const year = parts.find((part) => part.type === "year")?.value;
|
||||
const month = parts.find((part) => part.type === "month")?.value;
|
||||
const day = parts.find((part) => part.type === "day")?.value;
|
||||
|
||||
if (!year || !month || !day) {
|
||||
throw new Error(`Kon lokale datum niet bepalen voor timezone ${timezone}.`);
|
||||
}
|
||||
|
||||
return { year, month, day };
|
||||
}
|
||||
|
||||
function getDateStringWithOffset(timezone, dayOffset) {
|
||||
const now = new Date();
|
||||
const { year, month, day } = getDatePartsInTimezone(now, timezone);
|
||||
const baseDate = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
||||
baseDate.setUTCDate(baseDate.getUTCDate() + dayOffset);
|
||||
|
||||
return [
|
||||
baseDate.getUTCFullYear(),
|
||||
formatDatePart(baseDate.getUTCMonth() + 1),
|
||||
formatDatePart(baseDate.getUTCDate()),
|
||||
].join("-");
|
||||
}
|
||||
|
||||
function getAvatarPath(userId) {
|
||||
return `${userId}/avatar`;
|
||||
}
|
||||
|
|
@ -217,6 +294,136 @@ async function upsertSettings(userId, persona) {
|
|||
}
|
||||
}
|
||||
|
||||
async function loadReferenceMap(tableName) {
|
||||
const { data, error } = await supabase.from(tableName).select("id, key");
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Kon referentiedata uit ${tableName} niet laden: ${error.message}`);
|
||||
}
|
||||
|
||||
return new Map(data.map((row) => [row.key, row.id]));
|
||||
}
|
||||
|
||||
async function seedUsageData(userId, persona) {
|
||||
const usageSeed = demoUsageSeeds[persona.email];
|
||||
|
||||
if (!usageSeed) {
|
||||
return {
|
||||
checkIns: 0,
|
||||
activities: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const categoryIdsByKey = await loadReferenceMap("activity_categories");
|
||||
const skipReasonIdsByKey = await loadReferenceMap("skip_reasons");
|
||||
|
||||
const checkIns = usageSeed.checkIns.map((entry) => {
|
||||
const checkInDate = getDateStringWithOffset(persona.timezone, entry.dayOffset);
|
||||
|
||||
return {
|
||||
user_id: userId,
|
||||
check_in_date: checkInDate,
|
||||
energy_score: entry.energyScore,
|
||||
sleep_quality: entry.sleepQuality,
|
||||
...deriveBudgetSnapshot(entry.energyScore),
|
||||
};
|
||||
});
|
||||
|
||||
const activityDateMap = new Map();
|
||||
|
||||
for (const dayGroup of usageSeed.activities) {
|
||||
activityDateMap.set(
|
||||
dayGroup.dayOffset,
|
||||
getDateStringWithOffset(persona.timezone, dayGroup.dayOffset),
|
||||
);
|
||||
}
|
||||
|
||||
if (isDryRun) {
|
||||
return {
|
||||
checkIns: checkIns.length,
|
||||
activities: usageSeed.activities.reduce(
|
||||
(count, dayGroup) => count + dayGroup.items.length,
|
||||
0,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (checkIns.length > 0) {
|
||||
const { error } = await supabase.from("morning_check_ins").upsert(checkIns, {
|
||||
onConflict: "user_id,check_in_date",
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Kon check-ins voor ${persona.email} niet seeden: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const seededDates = [...new Set([...activityDateMap.values()])];
|
||||
|
||||
if (seededDates.length > 0) {
|
||||
const { error: deleteError } = await supabase
|
||||
.from("activities")
|
||||
.delete()
|
||||
.eq("user_id", userId)
|
||||
.in("activity_date", seededDates);
|
||||
|
||||
if (deleteError) {
|
||||
throw new Error(`Kon bestaande activiteiten voor ${persona.email} niet vervangen: ${deleteError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const activityRows = usageSeed.activities.flatMap((dayGroup) => {
|
||||
const activityDate = activityDateMap.get(dayGroup.dayOffset);
|
||||
|
||||
return dayGroup.items.map((item) => {
|
||||
const categoryId = categoryIdsByKey.get(item.categoryKey);
|
||||
|
||||
if (!categoryId) {
|
||||
throw new Error(
|
||||
`Onbekende activity category key '${item.categoryKey}' voor ${persona.email}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const skipReasonId = item.skipReasonKey
|
||||
? skipReasonIdsByKey.get(item.skipReasonKey)
|
||||
: null;
|
||||
|
||||
if (item.skipReasonKey && !skipReasonId) {
|
||||
throw new Error(
|
||||
`Onbekende skip reason key '${item.skipReasonKey}' voor ${persona.email}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
user_id: userId,
|
||||
activity_date: activityDate,
|
||||
source: item.source,
|
||||
status: item.status,
|
||||
name: item.name,
|
||||
category_id: categoryId,
|
||||
duration_minutes: item.durationMinutes,
|
||||
impact_level: item.impactLevel,
|
||||
priority_level: item.priorityLevel,
|
||||
skip_reason_id: skipReasonId,
|
||||
notes: item.notes ?? null,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
if (activityRows.length > 0) {
|
||||
const { error } = await supabase.from("activities").insert(activityRows);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Kon activiteiten voor ${persona.email} niet seeden: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
checkIns: checkIns.length,
|
||||
activities: activityRows.length,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const existingUsersByEmail = await loadExistingUsersByEmail();
|
||||
const results = [];
|
||||
|
|
@ -227,11 +434,14 @@ async function main() {
|
|||
|
||||
await upsertProfile(userId, persona, avatarPath);
|
||||
await upsertSettings(userId, persona);
|
||||
const usageResult = await seedUsageData(userId, persona);
|
||||
|
||||
results.push({
|
||||
email: persona.email,
|
||||
userId,
|
||||
avatar: avatarPath ? "ja" : "nee",
|
||||
checkIns: usageResult.checkIns,
|
||||
activities: usageResult.activities,
|
||||
dryRun: isDryRun ? "ja" : "nee",
|
||||
});
|
||||
}
|
||||
|
|
@ -240,7 +450,7 @@ async function main() {
|
|||
console.log(
|
||||
isDryRun
|
||||
? "Dry run klaar. Er zijn geen wijzigingen weggeschreven."
|
||||
: "Demo-gebruikers, profielen, settings en avatars zijn gesynchroniseerd.",
|
||||
: "Demo-gebruikers, profielen, settings, avatars en usage-seeds zijn gesynchroniseerd.",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
263
scripts/seed/demo-usage-seeds.mjs
Normal file
263
scripts/seed/demo-usage-seeds.mjs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
export const demoUsageSeeds = {
|
||||
"lisa.vermeulen+demo@example.com": {
|
||||
summary:
|
||||
"Lichte, fluctuerende week voor Lisa met nadruk op doseren, korte taken en herstelmomenten.",
|
||||
checkIns: [
|
||||
{
|
||||
dayOffset: -4,
|
||||
energyScore: 4,
|
||||
sleepQuality: "matig",
|
||||
},
|
||||
{
|
||||
dayOffset: -3,
|
||||
energyScore: 2,
|
||||
sleepQuality: "slecht",
|
||||
},
|
||||
{
|
||||
dayOffset: -2,
|
||||
energyScore: 5,
|
||||
sleepQuality: "goed",
|
||||
},
|
||||
{
|
||||
dayOffset: -1,
|
||||
energyScore: 3,
|
||||
sleepQuality: "matig",
|
||||
},
|
||||
{
|
||||
dayOffset: 0,
|
||||
energyScore: 2,
|
||||
sleepQuality: "slecht",
|
||||
},
|
||||
],
|
||||
activities: [
|
||||
{
|
||||
dayOffset: -4,
|
||||
items: [
|
||||
{
|
||||
source: "planned",
|
||||
status: "completed",
|
||||
name: "Douchen en rustig aankleden",
|
||||
categoryKey: "huishouden",
|
||||
durationMinutes: 20,
|
||||
impactLevel: "midden",
|
||||
priorityLevel: "hoog",
|
||||
notes: null,
|
||||
},
|
||||
{
|
||||
source: "planned",
|
||||
status: "completed",
|
||||
name: "Korte wandeling door de straat",
|
||||
categoryKey: "beweging",
|
||||
durationMinutes: 15,
|
||||
impactLevel: "laag",
|
||||
priorityLevel: "normaal",
|
||||
notes: null,
|
||||
},
|
||||
{
|
||||
source: "planned",
|
||||
status: "adjusted",
|
||||
name: "Mail naar studieadviseur sturen",
|
||||
categoryKey: "administratie",
|
||||
durationMinutes: 20,
|
||||
impactLevel: "midden",
|
||||
priorityLevel: "normaal",
|
||||
notes: "In twee korte blokken gedaan met een pauze tussendoor.",
|
||||
},
|
||||
{
|
||||
source: "ad_hoc",
|
||||
status: "completed",
|
||||
name: "Liggen in donkere kamer",
|
||||
categoryKey: "rust_herstel",
|
||||
durationMinutes: 45,
|
||||
impactLevel: "laag",
|
||||
priorityLevel: "laag",
|
||||
notes: "Extra rust genomen na de wandeling om hoofdpijn voor te blijven.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
dayOffset: -3,
|
||||
items: [
|
||||
{
|
||||
source: "planned",
|
||||
status: "completed",
|
||||
name: "Ontbijt maken",
|
||||
categoryKey: "huishouden",
|
||||
durationMinutes: 15,
|
||||
impactLevel: "laag",
|
||||
priorityLevel: "hoog",
|
||||
notes: null,
|
||||
},
|
||||
{
|
||||
source: "planned",
|
||||
status: "skipped",
|
||||
name: "Videobellen met vriendin",
|
||||
categoryKey: "sociaal",
|
||||
durationMinutes: 20,
|
||||
impactLevel: "midden",
|
||||
priorityLevel: "laag",
|
||||
skipReasonKey: "te_belastend",
|
||||
notes: "Te veel last van licht en geluid, daarom verplaatst.",
|
||||
},
|
||||
{
|
||||
source: "planned",
|
||||
status: "adjusted",
|
||||
name: "Lezen op laptop",
|
||||
categoryKey: "vrije_tijd",
|
||||
durationMinutes: 20,
|
||||
impactLevel: "midden",
|
||||
priorityLevel: "laag",
|
||||
notes: "Na tien minuten gestopt door hoofdpijn en daarna audio geluisterd.",
|
||||
},
|
||||
{
|
||||
source: "ad_hoc",
|
||||
status: "completed",
|
||||
name: "Middag slapen",
|
||||
categoryKey: "rust_herstel",
|
||||
durationMinutes: 60,
|
||||
impactLevel: "laag",
|
||||
priorityLevel: "laag",
|
||||
notes: "Herstel na een prikkelrijke ochtend.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
dayOffset: -2,
|
||||
items: [
|
||||
{
|
||||
source: "planned",
|
||||
status: "completed",
|
||||
name: "Kleine was draaien",
|
||||
categoryKey: "huishouden",
|
||||
durationMinutes: 25,
|
||||
impactLevel: "midden",
|
||||
priorityLevel: "normaal",
|
||||
notes: null,
|
||||
},
|
||||
{
|
||||
source: "planned",
|
||||
status: "completed",
|
||||
name: "Korte wandeling naar het parkje",
|
||||
categoryKey: "beweging",
|
||||
durationMinutes: 20,
|
||||
impactLevel: "laag",
|
||||
priorityLevel: "normaal",
|
||||
notes: null,
|
||||
},
|
||||
{
|
||||
source: "planned",
|
||||
status: "completed",
|
||||
name: "Bericht naar mentor van oude studie",
|
||||
categoryKey: "administratie",
|
||||
durationMinutes: 15,
|
||||
impactLevel: "laag",
|
||||
priorityLevel: "laag",
|
||||
notes: null,
|
||||
},
|
||||
{
|
||||
source: "ad_hoc",
|
||||
status: "completed",
|
||||
name: "Luisteren naar rustige podcast",
|
||||
categoryKey: "vrije_tijd",
|
||||
durationMinutes: 30,
|
||||
impactLevel: "laag",
|
||||
priorityLevel: "laag",
|
||||
notes: "Lagere prikkelactiviteit dan gepland schermgebruik.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
dayOffset: -1,
|
||||
items: [
|
||||
{
|
||||
source: "planned",
|
||||
status: "completed",
|
||||
name: "Douchen en haren wassen",
|
||||
categoryKey: "huishouden",
|
||||
durationMinutes: 20,
|
||||
impactLevel: "midden",
|
||||
priorityLevel: "hoog",
|
||||
notes: null,
|
||||
},
|
||||
{
|
||||
source: "planned",
|
||||
status: "skipped",
|
||||
name: "Boekje in de tuin lezen",
|
||||
categoryKey: "vrije_tijd",
|
||||
durationMinutes: 25,
|
||||
impactLevel: "laag",
|
||||
priorityLevel: "laag",
|
||||
skipReasonKey: "energie_te_laag",
|
||||
notes: "Na het douchen was de energie al op.",
|
||||
},
|
||||
{
|
||||
source: "planned",
|
||||
status: "adjusted",
|
||||
name: "Kamer opruimen",
|
||||
categoryKey: "huishouden",
|
||||
durationMinutes: 25,
|
||||
impactLevel: "midden",
|
||||
priorityLevel: "normaal",
|
||||
notes: "Alleen nachtkastje en stoel gedaan, rest doorgeschoven.",
|
||||
},
|
||||
{
|
||||
source: "ad_hoc",
|
||||
status: "completed",
|
||||
name: "Extra rust onder deken",
|
||||
categoryKey: "rust_herstel",
|
||||
durationMinutes: 45,
|
||||
impactLevel: "laag",
|
||||
priorityLevel: "laag",
|
||||
notes: "Bewust meer herstel ingepland om crash te voorkomen.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
dayOffset: 0,
|
||||
items: [
|
||||
{
|
||||
source: "planned",
|
||||
status: "completed",
|
||||
name: "Ontbijt en medicatie",
|
||||
categoryKey: "huishouden",
|
||||
durationMinutes: 15,
|
||||
impactLevel: "laag",
|
||||
priorityLevel: "hoog",
|
||||
notes: null,
|
||||
},
|
||||
{
|
||||
source: "planned",
|
||||
status: "planned",
|
||||
name: "Korte wandeling rond het blok",
|
||||
categoryKey: "beweging",
|
||||
durationMinutes: 10,
|
||||
impactLevel: "laag",
|
||||
priorityLevel: "normaal",
|
||||
notes: null,
|
||||
},
|
||||
{
|
||||
source: "planned",
|
||||
status: "skipped",
|
||||
name: "Tien minuten administratie",
|
||||
categoryKey: "administratie",
|
||||
durationMinutes: 15,
|
||||
impactLevel: "midden",
|
||||
priorityLevel: "laag",
|
||||
skipReasonKey: "energie_te_laag",
|
||||
notes: "Eerst rust nodig voordat schermwerk lukt.",
|
||||
},
|
||||
{
|
||||
source: "ad_hoc",
|
||||
status: "completed",
|
||||
name: "Rustpauze in verduisterde kamer",
|
||||
categoryKey: "rust_herstel",
|
||||
durationMinutes: 60,
|
||||
impactLevel: "laag",
|
||||
priorityLevel: "laag",
|
||||
notes: "Extra herstel toegevoegd nadat de ochtend zwaarder voelde dan verwacht.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue