53bf667f54
Experts (Module 4) - Pagina Experts (CRM): experts uitnodigen per e-mail (zonder SMTP wordt de link getoond), rondes koppelen, opnieuw uitnodigen, verwijderen en "Inloggen als" (alleen lezen) om mee te kijken - Uitnodigingspagina waar een expert zijn wachtwoord instelt (token 7 dagen geldig, eenmalig, alleen als hash opgeslagen) - Expertpagina: projecten in waardebepaling uit de eigen rondes, project plan lezen en eigen beoordeling geven (3 scores, advies Bonus/Malus, toelichting) - Rollen admin/expert: alle beheerpagina's en -acties controleren de rol - Rikx calculatie gebruikt de beoordelingen van de experts in plaats van de handmatige expertkolommen; experts koppelen ook vanaf de rondepagina Waardebepaling projecten - Nieuwe pagina met projecten in waardebepaling op kosten/impact (laag naar hoog), filter op ronde, Rikx Waarde-veld dat live doorrekent en opslaat in de ronde, en een 75%-lijn bij een gekozen ronde Projecten - Rikx calculatie als tab, zichtbaar vanaf status Waardebepaling, met keuze rekenmethode (Arbeid; Jeugd nog niet beschikbaar) en beoordeling goedgekeurd/afgekeurd met reden - Impactmaker gekoppeld aan een sociaal ondernemer - Opslaan-knop bovenaan; ongeldige velden op een verborgen tab worden getoond - Project toevoegen maakt direct een project aan; aparte pagina verwijderd - Waarderingssystematiek Rikx juni 2023 toegevoegd aan _doc Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
2.8 KiB
TypeScript
79 lines
2.8 KiB
TypeScript
"use server";
|
|
|
|
import { redirect } from "next/navigation";
|
|
import { randomBytes, scryptSync } from "crypto";
|
|
import { getPlatformDb } from "../../lib/platform-db";
|
|
import { requireAdmin } from "../../lib/auth";
|
|
|
|
function requireAuth() {
|
|
// Called inside server actions — session check is synchronous-safe via cache
|
|
}
|
|
|
|
function hashPassword(password: string): string {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(password, salt, 64).toString("hex");
|
|
return `${salt}:${hash}`;
|
|
}
|
|
|
|
export async function createTenant(formData: FormData) {
|
|
const session = await requireAdmin();
|
|
|
|
const name = (formData.get("name") as string).trim();
|
|
const slug = (formData.get("slug") as string).trim().toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
const adminEmail = (formData.get("admin_email") as string).trim();
|
|
const adminPassword = formData.get("admin_password") as string;
|
|
const adminName = (formData.get("admin_name") as string).trim();
|
|
|
|
if (!name || !slug || !adminEmail || !adminPassword || !adminName) {
|
|
redirect("/tenants?error=missing");
|
|
}
|
|
|
|
const db = getPlatformDb();
|
|
|
|
const existing = db.prepare("SELECT id FROM accounts WHERE slug = ?").get(slug);
|
|
if (existing) redirect("/tenants?error=slug");
|
|
|
|
db.prepare("INSERT INTO accounts (name, slug) VALUES (?, ?)").run(name, slug);
|
|
const account = db.prepare("SELECT id FROM accounts WHERE slug = ?").get(slug) as { id: number };
|
|
|
|
db.prepare(
|
|
"INSERT INTO users (account_id, email, password_hash, name, role) VALUES (?, ?, ?, ?, 'admin')"
|
|
).run(account.id, adminEmail, hashPassword(adminPassword), adminName);
|
|
|
|
redirect("/tenants");
|
|
}
|
|
|
|
export async function deleteTenant(formData: FormData) {
|
|
const session = await requireAdmin();
|
|
|
|
const accountId = Number(formData.get("account_id"));
|
|
if (!accountId) redirect("/tenants");
|
|
|
|
const db = getPlatformDb();
|
|
// Cascade via foreign keys: users, participants, measurements are deleted automatically
|
|
db.prepare("DELETE FROM accounts WHERE id = ?").run(accountId);
|
|
|
|
redirect("/tenants");
|
|
}
|
|
|
|
export async function updateTenant(formData: FormData) {
|
|
const session = await requireAdmin();
|
|
|
|
const accountId = Number(formData.get("account_id"));
|
|
const name = ((formData.get("name") as string) ?? "").trim();
|
|
const slug = ((formData.get("slug") as string) ?? "").trim().toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
|
|
if (!accountId) redirect("/tenants");
|
|
if (!name || !slug) redirect(`/tenants/${accountId}?error=missing`);
|
|
|
|
const db = getPlatformDb();
|
|
const existing = db
|
|
.prepare("SELECT id FROM accounts WHERE slug = ? AND id != ?")
|
|
.get(slug, accountId);
|
|
if (existing) redirect(`/tenants/${accountId}?error=slug`);
|
|
|
|
db.prepare("UPDATE accounts SET name = ?, slug = ? WHERE id = ?").run(name, slug, accountId);
|
|
|
|
redirect("/tenants");
|
|
}
|