import React, { useState, useRef } from "react"; import { defaultData } from "../constants.js"; import { generateId, hashPassword } from "../utils.js"; // ─── Palette — matches the app's light theme exactly ───────────────────────── const C = { bg: "#ebe7e1", surface: "#fdfcfa", surface2: "#f7f4f0", border: "#ddd8d0", border2: "#e6e1da", text: "#1a1a18", text3: "#6a6660", text4: "#8c8880", danger: "#8a1a1a", dangerBg: "#fdf2f2", dangerBorder: "#e0b0b0", }; const S = { wrap: { background: C.bg, minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "'DM Mono','Courier New',monospace", color: C.text, padding: "32px 16px", }, card: { width: "100%", maxWidth: 460, background: C.surface, borderRadius: 14, padding: "44px 40px", boxShadow: "0 2px 24px rgba(60,50,40,0.10)", border: `1px solid ${C.border}`, }, logo: { fontFamily: "Krungthep,'Archivo Black',sans-serif", fontSize: 34, color: C.text, letterSpacing: "-0.02em", textAlign: "center", marginBottom: 4, }, sub: { textAlign: "center", fontSize: 10, color: C.text4, letterSpacing: "0.14em", marginBottom: 32, }, progress: { display: "flex", gap: 6, justifyContent: "center", marginBottom: 32, }, dot: (active, done) => ({ width: active ? 22 : 8, height: 8, borderRadius: 4, background: done ? C.text : active ? C.text : C.border, opacity: done ? 1 : active ? 1 : 0.4, transition: "all 0.3s", }), label: { fontSize: 9, letterSpacing: "0.14em", color: C.text4, display: "block", marginBottom: 5, marginTop: 16, }, input: { width: "100%", boxSizing: "border-box", background: C.surface, border: `1px solid ${C.border}`, borderRadius: 6, padding: "9px 12px", color: C.text, fontFamily: "'DM Mono',monospace", fontSize: 13, outline: "none", transition: "border-color 0.15s", }, inputErr: { border: `1px solid ${C.dangerBorder}` }, textarea: { width: "100%", boxSizing: "border-box", background: C.surface, border: `1px solid ${C.border}`, borderRadius: 6, padding: "9px 12px", color: C.text, fontFamily: "'DM Mono',monospace", fontSize: 13, outline: "none", resize: "vertical", minHeight: 64, }, err: { fontSize: 11, color: C.danger, marginTop: 4 }, hint: { fontSize: 11, color: C.text4, marginTop: 5, lineHeight: 1.5 }, btnPrimary: { width: "100%", background: C.text, border: "none", borderRadius: 8, padding: "12px 0", color: "#fff", fontFamily: "'DM Mono',monospace", fontSize: 13, cursor: "pointer", marginTop: 28, letterSpacing: "0.04em", transition: "opacity 0.15s", }, btnGhost: { width: "100%", background: "transparent", border: `1px solid ${C.border}`, borderRadius: 8, padding: "10px 0", color: C.text4, fontFamily: "'DM Mono',monospace", fontSize: 12, cursor: "pointer", marginTop: 10, }, }; // ─── Prompt for users with existing data ───────────────────────────────────── export function SetupPrompt({ onSetup, onSkip }) { return (
RAPPORT
NEUE VERSION
Neuer Einrichtungsassistent.
Rapport verfügt über einen neuen Setup-Assistenten für Studio, Admin-Account und Mitarbeiterprofil. Möchtest du ihn durchlaufen?
Neu einrichten löscht alle bestehenden Daten unwiderruflich.
); } // ─── Main setup wizard ──────────────────────────────────────────────────────── export default function Setup({ onComplete }) { const TOTAL = 3; const [step, setStep] = useState(1); const [studio, setStudio] = useState({ name: "", address: "", email: "", phone: "" }); const [account, setAccount] = useState({ displayName: "", username: "admin", password: "", confirm: "" }); const [errors, setErrors] = useState({}); const [showPw, setShowPw] = useState(false); const [importErr, setImportErr] = useState(""); const importRef = useRef(null); const handleImport = (e) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { try { const parsed = JSON.parse(ev.target.result); if (!parsed.settings || !Array.isArray(parsed.projects)) { setImportErr("Ungültiges Backup-Format."); return; } onComplete({ ...defaultData, ...parsed, settings: { ...defaultData.settings, ...parsed.settings, setupCompleted: true }, appRoles: parsed.appRoles || defaultData.appRoles, users: parsed.users || defaultData.users, }); } catch { setImportErr("Datei konnte nicht gelesen werden."); } }; reader.readAsText(file); e.target.value = ""; }; const setS = (k, v) => setStudio(s => ({ ...s, [k]: v })); const setA = (k, v) => setAccount(a => ({ ...a, [k]: v })); const clearErr = k => setErrors(e => { const n = { ...e }; delete n[k]; return n; }); const validate = () => { const errs = {}; if (step === 1 && !studio.name.trim()) errs.name = "Pflichtfeld"; if (step === 2) { if (!account.displayName.trim()) errs.displayName = "Pflichtfeld"; if (!account.username.trim()) errs.username = "Pflichtfeld"; if (account.password.length < 8) errs.password = "Mindestens 8 Zeichen"; if (account.password !== account.confirm) errs.confirm = "Passwörter stimmen nicht überein"; } setErrors(errs); return Object.keys(errs).length === 0; }; const next = () => { if (validate()) setStep(s => s + 1); }; const back = () => setStep(s => s - 1); const [busy, setBusy] = useState(false); const finish = async () => { if (busy) return; setBusy(true); try { const empId = generateId(); const { hash, salt } = await hashPassword(account.password); onComplete({ ...defaultData, settings: { ...defaultData.settings, setupCompleted: true, name: studio.name.trim(), address: studio.address.trim() || defaultData.settings.address, email: studio.email.trim() || defaultData.settings.email, phone: studio.phone.trim() || defaultData.settings.phone, }, users: [{ id: "admin", username: account.username.trim(), passwordHash: hash, passwordSalt: salt, role: "admin", displayName: account.displayName.trim(), appRoleId: "r-admin", employeeId: empId, }], employees: [{ id: empId, name: account.displayName.trim(), email: studio.email.trim() || "", wochenstunden: defaultData.settings.defaultWochenstunden || 35, ferienWochen: defaultData.settings.defaultFerienWochen || 5, eintrittsdatum: new Date().toISOString().slice(0, 10), aktiv: true, appUserId: "admin", }], }); } finally { setBusy(false); } }; const progressEl = (
{Array.from({ length: TOTAL }, (_, i) => (
))}
); const renderHeader = (n, title, lead) => ( <>
RAPPORT
SCHRITT {n} VON {TOTAL}
{progressEl}
{title}
{lead &&
{lead}
} ); // ── Step 1: Studio ────────────────────────────────────────────────────────── if (step === 1) return (
{renderHeader(1, "Willkommen.", "Lass uns Rapport für dein Studio einrichten. Das dauert weniger als zwei Minuten.")} { setS("name", e.target.value); clearErr("name"); }} onKeyDown={e => e.key === "Enter" && next()} /> {errors.name &&
{errors.name}
}