// Velden van stap 1 "Project plan". Eén plek voor labels, types en keuzelijsten: // het formulier en de server action werken allebei vanuit deze definitie. // Velden met `column: true` zijn echte kolommen in `projects` (nodig voor de Rikx calculatie); // alle andere waarden staan als JSON in `projects.project_plan`. // // Keuzelijsten komen uit de antwoorden in _doc/Rikx rekenmethode uitleg.xlsx, // tabblad "Projecten(rikx ronde 4)", behalve BUDGET_BESTEDING (voorstel, nog niet uit een bron). export const ANDERS = "Anders, namelijk"; export const DOELGROEPEN = [ "Werkzoekenden (WW/Participatiewet)", "Arbeidsbeperkten (WIA/Wajong/Garantiebanen/WSW)", "Jongeren zonder startkwalificatie", "Leerlingen (BOL/BBL) (VSO/praktijkonderwijs)", "Niet-uitkeringsgerechtigden", ANDERS, ]; export const SECTOREN = [ "Zorg & Welzijn", "Bouw, Techniek & Productie", "Handel & Dienstverlening", "Transport & Logistiek", "Horeca & Detailhandel", "ICT", "Media & Communicatie", "Onderwijs & Cultuur (Wetenschap)", "Justitie, veiligheid en openbaar bestuur", ANDERS, ]; export const INDIRECTE_IMPACT = [ "Groei menselijk kapitaal (vaardigheden, competenties, opleiding, stages, startkwalificatie)", "Wegnemen persoonlijke belemmeringen (multiproblematiek, stress, schulden, armoede)", "Inzet op motivationele aspecten (zelfbeeld, zelfvertrouwen)", "Betrekken sociale hulpbronnen (netwerk, ouders, omgeving)", ]; export const LEVENSGEBIEDEN = [ "Financiën", "Werk & Opleiding", "Huisvesting", "Lichamelijke gezondheid", "Geestelijke gezondheid", "Sociale activiteiten", "Sociaal Netwerk", "Verslaving", "Justitie", ANDERS, ]; export const DUUR_PROFIJT = [ "Minder dan een jaar", "Een jaar tot een aantal jaren", "Meer dan 5 jaar", "Meer dan 10 jaar", "Levenslang", ]; export const INTENSITEIT = [ "Een hele week (bijvoorbeeld 5 dagen)", "Een aantal uren verspreid over een langere periode", ]; export const BUDGET_BESTEDING = [ "Personeelskosten (begeleiding, training, coaching)", "Opleiding en certificering", "Materialen en middelen", "Vergoeding voor deelnemers", "Werving van deelnemers", "Huisvesting en locatie", "Overhead / indirecte kosten", ANDERS, ]; export const JA_NEE = ["Ja", "Nee"]; export const JA_NEE_GEDEELTELIJK = ["Ja", "Nee", "Gedeeltelijk"]; type Base = { key: string; label: string; hint?: string; // Volle breedte in het formulier wide?: boolean; // Alleen tonen (en opslaan) als een ander veld deze waarde heeft showIf?: { key: string; equals: string }; // Vervolgvraag: direct onder de vorige vraag, ingesprongen, zodat zichtbaar is waar hij bij hoort followUp?: boolean; // Begint op een nieuwe rij (bv. een vraag met vervolgvraag) newRow?: boolean; }; export type PlanField = Base & ( | { kind: "textarea" } | { kind: "number"; column?: boolean; min?: number; max?: number; step?: number | "any" } | { kind: "date" } | { kind: "select"; options: string[] } | { kind: "multiselect"; options: string[] } ); export type PlanGroup = { title: string; fields: PlanField[] }; const aantal = (key: string, label: string): PlanField => ({ key, label, kind: "number", min: 0, step: 1 }); export const PROJECT_PLAN_GROUPS: PlanGroup[] = [ { title: "Project", fields: [ { key: "korte_omschrijving", label: "Korte projectomschrijving", kind: "textarea", wide: true }, { key: "concrete_resultaten", label: "Welke concrete resultaten verwacht je te behalen?", kind: "textarea", wide: true }, { key: "startdatum", label: "Geplande startdatum project", kind: "date" }, { key: "einddatum", label: "Geplande einddatum project", kind: "date" }, ], }, { title: "Doelgroep en bereik", fields: [ { key: "doelgroepen", label: "Op welke doelgroep(en) richt je project zich primair?", kind: "multiselect", options: DOELGROEPEN }, { key: "sectoren", label: "In welke sector(en) vinden de activiteiten en/of uitstroom naar werk plaats?", kind: "multiselect", options: SECTOREN }, { ...aantal("deelnemers_totaal", "Hoeveel deelnemers verwacht je in totaal te kunnen bereiken met dit project?"), wide: true }, ], }, { title: "Verwachte resultaten deelnemers", fields: [ { key: "deelnemers_werk", label: "Totaal aantal deelnemers begeleid naar duurzame uitstroom werk", hint: "Verwacht aantal deelnemers dat het project succesvol verlaat. Telt mee in de Rikx calculatie.", kind: "number", column: true, min: 0, step: 1, }, { key: "deelnemers_skills", label: "Totaal aantal deelnemers voorzien van nieuwe skills", hint: "Deelnemers die niet per se uitstromen maar wel nieuwe vaardigheden opdoen. Telt mee in de Rikx calculatie.", kind: "number", column: true, min: 0, step: 1, }, aantal("aantal_opleiding", "Aantal deelnemers dat naar verwachting een erkende opleiding (MBO), certificaat of training zal afronden"), aantal("aantal_betaalde_baan", "Aantal deelnemers dat naar verwachting zal doorstromen naar een betaalde baan"), aantal("aantal_vervolgopleiding", "Aantal deelnemers dat naar verwachting zal doorstromen naar een vervolgopleiding"), aantal("aantal_stage", "Aantal deelnemers dat naar verwachting zal doorstromen naar een stage of werkervaringsplaats"), aantal("aantal_dagbesteding", "Aantal deelnemers dat naar verwachting dagbesteding zal vinden"), aantal("aantal_vaardigheden", "Aantal deelnemers dat naar verwachting vaardigheden zal ontwikkelen"), ], }, { title: "Impact", fields: [ { key: "levensgebieden", label: "Op welke levensgebieden verwacht je dat de deelnemers positief worden beïnvloed?", kind: "multiselect", options: LEVENSGEBIEDEN }, { key: "indirecte_impact", label: "Indirecte impact categorieën", kind: "multiselect", options: INDIRECTE_IMPACT }, { key: "levensgebieden_toelichting", label: "Licht kort toe hoe jouw project impact heeft op de geselecteerde levensgebieden", kind: "textarea", wide: true }, { key: "duur_profijt", label: "Hoelang verwacht je dat de deelnemers profijt hebben van de impact van het project?", kind: "select", options: DUUR_PROFIJT }, ], }, { title: "Begeleiding en aanpak", fields: [ { key: "begeleiding", label: "Hoe wordt begeleiding geregeld tijdens het traject?", kind: "textarea", wide: true }, { key: "uren_per_week", label: "Hoeveel uur training, opleiding of coaching besteed je gemiddeld per deelnemer per week?", kind: "number", min: 0, step: "any" }, { key: "intensiteit", label: "Is dit een hele week (bijvoorbeeld 5 dagen) of een aantal uren verspreid over een langere periode?", kind: "select", options: INTENSITEIT }, { key: "na_afronding", label: "Wat gebeurt er na afronding van het project met de deelnemers?", kind: "textarea", wide: true }, { key: "aanpassingen_afstand", label: "Welke aanpassingen maakt jouw project voor deelnemers met een afstand tot de arbeidsmarkt?", kind: "textarea", wide: true }, { key: "diversiteit", label: "Heeft jouw project specifieke aandacht voor diversiteit en inclusie op de arbeidsmarkt?", kind: "select", options: JA_NEE, newRow: true }, { key: "diversiteit_toelichting", label: "Zo ja, licht toe", kind: "textarea", wide: true, followUp: true, showIf: { key: "diversiteit", equals: "Ja" } }, ], }, { title: "Monitoring en evaluatie", fields: [ { key: "meetinstrumenten", label: "Welke meetinstrumenten of evaluatiemethoden ga je gebruiken?", kind: "textarea", wide: true }, { key: "monitoring", label: "Hoe ga je de voortgang en resultaten van jouw project monitoren?", kind: "textarea", wide: true }, ], }, { title: "Financiën", fields: [ { key: "projectkosten", label: "Projectkosten (€)", hint: "Benodigd bedrag om het project te kunnen starten.", kind: "number", column: true, min: 0, step: 0.01 }, { key: "projectkosten_omschrijving", label: "Projectkosten omschrijving", kind: "textarea", wide: true }, { key: "financiering_extern_bedrag", label: "Financiëring extern bedrag (€)", hint: "Deel van de projectkosten dat niet via Rikx wordt gefinancierd.", kind: "number", column: true, min: 0, step: 0.01, }, { key: "financiering_extern_pct", label: "Percentage financiëring extern (%)", hint: "In te vullen door een Rikx-medewerker.", kind: "number", column: true, min: 0, max: 100, step: "any", }, { key: "financiering_extern_toelichting", label: "Financiëring extern toelichting", kind: "textarea", wide: true }, { key: "budget_besteding", label: "Hoe zal het aangevraagde budget worden besteed?", kind: "multiselect", options: BUDGET_BESTEDING }, { key: "financieel_duurzaam", label: "Is dit project financieel duurzaam?", kind: "select", options: JA_NEE_GEDEELTELIJK, newRow: true }, { key: "financieel_duurzaam_toelichting", label: "Licht toe", kind: "textarea", wide: true, followUp: true }, ], }, { title: "Overig", fields: [ { key: "vergelijkbare_projecten", label: "Zijn er vergelijkbare projecten waarvan aangetoond is dat ze effectief zijn?", kind: "select", options: JA_NEE, newRow: true }, { key: "vergelijkbare_projecten_toelichting", label: "Licht toe", kind: "textarea", wide: true, followUp: true }, { key: "expertise_partners", label: "Heb je specifieke expertise of samenwerkingspartners die bijdragen aan het succes van dit project?", kind: "textarea", wide: true }, { key: "overig", label: "Is er iets anders wat je wilt delen over jouw project?", kind: "textarea", wide: true }, ], }, ]; export const PROJECT_PLAN_FIELDS = PROJECT_PLAN_GROUPS.flatMap((g) => g.fields); export type ProjectPlanValue = string | number | string[]; export type ProjectPlan = Record; // Sleutel van het "Anders, namelijk"-tekstveld bij een keuzelijst export const andersKey = (key: string) => `${key}_anders`; export function parseProjectPlanJson(json: string | null): ProjectPlan { if (!json) return {}; try { const value = JSON.parse(json); return value && typeof value === "object" && !Array.isArray(value) ? value : {}; } catch { return {}; } } const MAX_TEXT = 10_000; // Leest de JSON-velden van het project plan uit het formulier. // Geeft een foutcode terug bij ongeldige invoer; kolomvelden worden hier overgeslagen. export function readProjectPlan(formData: FormData): { plan: ProjectPlan } | { error: "invalid" | "datum" } { const plan: ProjectPlan = {}; const text = (key: string) => ((formData.get(key) as string | null) ?? "").trim().slice(0, MAX_TEXT); for (const field of PROJECT_PLAN_FIELDS) { if (field.kind === "number" && field.column) continue; if (field.showIf && text(field.showIf.key) !== field.showIf.equals) continue; switch (field.kind) { case "textarea": { const v = text(field.key); if (v) plan[field.key] = v; break; } case "number": { const raw = text(field.key); if (!raw) break; const n = Number(raw.replace(",", ".")); if (!Number.isFinite(n) || n < (field.min ?? -Infinity) || n > (field.max ?? Infinity)) { return { error: "invalid" }; } plan[field.key] = n; break; } case "date": { const v = text(field.key); if (!v) break; if (!/^\d{4}-\d{2}-\d{2}$/.test(v) || Number.isNaN(Date.parse(v))) return { error: "datum" }; plan[field.key] = v; break; } case "select": { const v = text(field.key); if (field.options.includes(v)) plan[field.key] = v; break; } case "multiselect": { const values = formData.getAll(field.key).filter((v): v is string => typeof v === "string" && field.options.includes(v)); if (values.length) plan[field.key] = [...new Set(values)]; if (values.includes(ANDERS)) { const anders = text(andersKey(field.key)); if (anders) plan[andersKey(field.key)] = anders; } break; } } } if (typeof plan.startdatum === "string" && typeof plan.einddatum === "string" && plan.einddatum < plan.startdatum) { return { error: "datum" }; } return { plan }; }