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.
Neu einrichten
Vorhandene Daten behalten →
);
}
// ─── 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.")}
STUDIO / UNTERNEHMEN *
{ setS("name", e.target.value); clearErr("name"); }}
onKeyDown={e => e.key === "Enter" && next()}
/>
{errors.name &&
{errors.name}
}
ADRESSE
);
// ── Step 2: Admin-Account ───────────────────────────────────────────────────
if (step === 2) return (
{renderHeader(2, "Dein Account.", "Dieser Account hat vollen Systemzugriff. Weitere Mitarbeitende können später mit eingeschränkten Berechtigungen hinzugefügt werden.")}
DEIN NAME *
{ setA("displayName", e.target.value); clearErr("displayName"); }}
/>
{errors.displayName &&
{errors.displayName}
}
Wird als Mitarbeiterprofil angelegt und im System angezeigt.
BENUTZERNAME *
{ setA("username", e.target.value.toLowerCase().replace(/\s/g, "")); clearErr("username"); }}
/>
{errors.username &&
{errors.username}
}
PASSWORT *
{ setA("password", e.target.value); clearErr("password"); }}
/>
setShowPw(v => !v)}
style={{ position: "absolute", right: 10, top: "50%", transform: "translateY(-50%)", background: "none", border: "none", color: C.text4, cursor: "pointer", fontSize: 11, fontFamily: "inherit" }}>
{showPw ? "verbergen" : "anzeigen"}
{errors.password &&
{errors.password}
}
PASSWORT BESTÄTIGEN *
{ setA("confirm", e.target.value); clearErr("confirm"); }}
onKeyDown={e => e.key === "Enter" && next()}
/>
{errors.confirm &&
{errors.confirm}
}
Weiter →
← Zurück
);
// ── Step 3: Done ────────────────────────────────────────────────────────────
return (
{renderHeader(3, "Alles bereit.")}
{[
{ label: "STUDIO", value: studio.name },
{ label: "MITARBEITER", value: account.displayName },
{ label: "BENUTZERNAME", value: account.username },
{ label: "ADRESSE", value: studio.address || "—" },
].map(({ label, value }) => (
))}
Projekte, Mitarbeitende, Einstellungen und Berechtigungen können jederzeit in der App angepasst werden.
{busy ? "…" : "Rapport starten →"}
← Zurück
);
}