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_URL=https://your-project-ref.supabase.co
|
||||||
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_your_key_here
|
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
|
NEXT_PUBLIC_ENABLE_TEST_WIZARD=false
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,8 @@ persona-set in [inspannings-monitor-08-gebruikerspersonas-v01.docx](/Users/janpe
|
||||||
Benodigd:
|
Benodigd:
|
||||||
|
|
||||||
- `NEXT_PUBLIC_SUPABASE_URL`
|
- `NEXT_PUBLIC_SUPABASE_URL`
|
||||||
- `SUPABASE_SERVICE_ROLE_KEY`
|
- `SUPABASE_SECRET_KEY` voorkeur
|
||||||
|
- `SUPABASE_SERVICE_ROLE_KEY` mag nog als legacy alias
|
||||||
- `DEMO_USER_PASSWORD`
|
- `DEMO_USER_PASSWORD`
|
||||||
|
|
||||||
Uitvoeren:
|
Uitvoeren:
|
||||||
|
|
@ -110,6 +111,12 @@ Uitvoeren:
|
||||||
2. zet de drie env-vars lokaal
|
2. zet de drie env-vars lokaal
|
||||||
3. run `npm run seed:demo-users`
|
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:
|
De seeddata zelf staat in:
|
||||||
- [demo-personas.mjs](/Users/janpetervisser/Development/third/scripts/seed/demo-personas.mjs)
|
- [demo-personas.mjs](/Users/janpetervisser/Development/third/scripts/seed/demo-personas.mjs)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ export function ActivityEvaluationFields({
|
||||||
initialSkipReasonId ?? skipReasons[0]?.id ?? "",
|
initialSkipReasonId ?? skipReasons[0]?.id ?? "",
|
||||||
);
|
);
|
||||||
const [notes, setNotes] = useState(initialNotes ?? "");
|
const [notes, setNotes] = useState(initialNotes ?? "");
|
||||||
|
const selectedSkipReason =
|
||||||
|
skipReasons.find((skipReason) => skipReason.id === skipReasonId) ?? null;
|
||||||
|
|
||||||
if (status !== "skipped" && status !== "adjusted") {
|
if (status !== "skipped" && status !== "adjusted") {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -54,7 +56,9 @@ export function ActivityEvaluationFields({
|
||||||
onValueChange={(value) => setSkipReasonId(value ?? skipReasons[0]?.id ?? "")}
|
onValueChange={(value) => setSkipReasonId(value ?? skipReasons[0]?.id ?? "")}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-11 w-full rounded-[1.15rem] bg-background/80 px-4 text-sm">
|
<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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{skipReasons.map((skipReason) => (
|
{skipReasons.map((skipReason) => (
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,9 @@ export function ActivityForm({
|
||||||
onValueChange={(value) => setCategoryId(value ?? categories[0]?.id ?? "")}
|
onValueChange={(value) => setCategoryId(value ?? categories[0]?.id ?? "")}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-12 w-full rounded-[1.25rem] bg-background/80 px-4 text-base">
|
<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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,9 @@ export function AdHocActivityForm({
|
||||||
onValueChange={(value) => setCategoryId(value ?? categories[0]?.id ?? "")}
|
onValueChange={(value) => setCategoryId(value ?? categories[0]?.id ?? "")}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-12 w-full rounded-[1.25rem] bg-background/80 px-4 text-base">
|
<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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,6 @@ export function SettingsForm({ profileBundle }: SettingsFormProps) {
|
||||||
action={formAction}
|
action={formAction}
|
||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
aria-busy={isPending}
|
aria-busy={isPending}
|
||||||
encType="multipart/form-data"
|
|
||||||
>
|
>
|
||||||
<input type="hidden" name="locale" value={locale} />
|
<input type="hidden" name="locale" value={locale} />
|
||||||
<PreferenceHiddenFields draft={draft} />
|
<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" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <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
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// 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 { fileURLToPath } from "node:url";
|
||||||
import { createClient } from "@supabase/supabase-js";
|
import { createClient } from "@supabase/supabase-js";
|
||||||
import { demoPersonas } from "./seed/demo-personas.mjs";
|
import { demoPersonas } from "./seed/demo-personas.mjs";
|
||||||
|
import { demoUsageSeeds } from "./seed/demo-usage-seeds.mjs";
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const rootDir = path.resolve(__dirname, "..");
|
const rootDir = path.resolve(__dirname, "..");
|
||||||
|
|
@ -47,13 +48,22 @@ await loadEnvFile(path.join(rootDir, ".env.local"));
|
||||||
await loadEnvFile(path.join(rootDir, ".env"));
|
await loadEnvFile(path.join(rootDir, ".env"));
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
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 demoUserPassword = process.env.DEMO_USER_PASSWORD;
|
||||||
const isDryRun = process.argv.includes("--dry-run");
|
const isDryRun = process.argv.includes("--dry-run");
|
||||||
|
|
||||||
if (!supabaseUrl || !serviceRoleKey) {
|
if (!supabaseUrl || !serviceRoleKey) {
|
||||||
throw new Error(
|
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) {
|
function getAvatarPath(userId) {
|
||||||
return `${userId}/avatar`;
|
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() {
|
async function main() {
|
||||||
const existingUsersByEmail = await loadExistingUsersByEmail();
|
const existingUsersByEmail = await loadExistingUsersByEmail();
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
@ -227,11 +434,14 @@ async function main() {
|
||||||
|
|
||||||
await upsertProfile(userId, persona, avatarPath);
|
await upsertProfile(userId, persona, avatarPath);
|
||||||
await upsertSettings(userId, persona);
|
await upsertSettings(userId, persona);
|
||||||
|
const usageResult = await seedUsageData(userId, persona);
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
email: persona.email,
|
email: persona.email,
|
||||||
userId,
|
userId,
|
||||||
avatar: avatarPath ? "ja" : "nee",
|
avatar: avatarPath ? "ja" : "nee",
|
||||||
|
checkIns: usageResult.checkIns,
|
||||||
|
activities: usageResult.activities,
|
||||||
dryRun: isDryRun ? "ja" : "nee",
|
dryRun: isDryRun ? "ja" : "nee",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -240,7 +450,7 @@ async function main() {
|
||||||
console.log(
|
console.log(
|
||||||
isDryRun
|
isDryRun
|
||||||
? "Dry run klaar. Er zijn geen wijzigingen weggeschreven."
|
? "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