/* Seite Personalvermittlung (personalvermittlung.html): Fachkräfte für Handwerk und Technik.
Erst der Weg für Unternehmen (Suche, Ablauf, Konditionen, Anfrage), dann der für Bewerber
(offene Stellen aus stellen.js, Bewerbung mit optionalem Lebenslauf an api-bewerbung.php).
Spec: docs/superpowers/specs/2026-10-08-neue-leistungsseiten-design.md, Abschnitt 6 */
const BEWERBUNG_MAX_MB = 5;
const BEWERBUNG_ENDUNGEN = ["pdf", "docx", "jpg", "jpeg", "png"];
const PERSONAL_SUCHE = [
{ Icon: UserPlus, titel: "Direktansprache", text: "Wir sprechen passende Fachkräfte gezielt an – auch die, die gerade nicht aktiv suchen." },
{ Icon: Globe, titel: "Anzeigen auf allen Kanälen", text: "Indeed, XING, LinkedIn und die Jobbörse der Bundesagentur für Arbeit – wir schalten und betreuen die Anzeigen." },
{ Icon: Workflow, titel: "Bewerber-Funnel", text: "Bewerben in zwei Minuten, auch ohne Lebenslauf – so erreichen wir Handwerker, die kein Anschreiben schreiben wollen.", link: ["leistungen.html#funnel-systeme", "Mehr zu Funnel-Systemen"] },
{ Icon: Phone, titel: "Vorauswahl am Telefon", text: "Wir führen die ersten Gespräche und stellen Ihnen nur Kandidaten vor, die wirklich passen." },
];
const PERSONAL_SCHRITTE = [
{ when: "Schritt 01 · kostenlos", title: "Bedarf aufnehmen", body: "Wir klären Aufgaben, Qualifikation, Gehaltsrahmen und Start – und ob die Suche realistisch ist.", cta: "#anfrage", ctaLabel: "Suchauftrag besprechen" },
{ when: "Schritt 02 · ab Tag 1", title: "Suche & Ansprache", body: "Direktansprache, Anzeigen und Bewerber-Funnel laufen parallel. Sie müssen nichts tun." },
{ when: "Schritt 03 · Shortlist", title: "Geprüfte Kandidaten", body: "Sie bekommen eine kurze Liste mit Kandidaten, die wir vorher am Telefon geprüft haben." },
{ when: "Schritt 04 · Einstellung", title: "Einstellung & Nachbetreuung", body: "Sie entscheiden und stellen ein. In der Probezeit bleiben wir Ansprechpartner – mit Nachbesetzungsgarantie." },
];
function personalFaq(k) {
return [
{ q: "Was kostet die Vermittlung?", a: `${k.honorarProzent} % des Brutto-Jahresgehalts der eingestellten Person, zuzüglich Umsatzsteuer. Es gibt keine Grundgebühr und keine Vorkasse.` },
{ q: "Wann zahle ich?", a: "Erst wenn der Arbeitsvertrag unterschrieben ist. Kommt keine Einstellung zustande, kostet Sie die Suche nichts." },
{ q: "Was passiert, wenn die Person in der Probezeit geht?", a: `Verlässt die vermittelte Person Ihr Unternehmen in den ersten ${k.garantieMonate} Monaten, suchen wir kostenlos nach. Die Einzelheiten stehen im Vermittlungsvertrag.` },
{ q: "Zahlen Bewerber etwas?", a: "Nein, nie. Für Bewerberinnen und Bewerber ist unsere Vermittlung immer kostenlos." },
{ q: "Ist das Leiharbeit?", a: "Nein. Wir vermitteln nur – angestellt wird direkt bei Ihnen. Eine Erlaubnis zur Arbeitnehmerüberlassung ist dafür nicht nötig." },
];
}
// Konditionen auf einen Blick: großer Prozentwert links, Bedingungen rechts
function Konditionen({ k }) {
return (
Sie zahlen nur bei Erfolg.>}
lead="Keine Grundgebühr, keine Vorkasse, kein Risiko: Das Honorar wird erst fällig, wenn jemand bei Ihnen anfängt."
>
Honorar
{k.honorarProzent} %
des Brutto-Jahresgehalts, zzgl. USt
{[
"Fällig erst mit Unterschrift unter den Arbeitsvertrag",
"Keine Vorkasse, keine Grundgebühr",
`${k.garantieMonate} Monate Nachbesetzungsgarantie: Verlässt die Person Ihr Unternehmen in dieser Zeit, suchen wir kostenlos nach`,
"Für Bewerber immer kostenlos",
].map((t) => (
);
}
function Stellenliste({ stellen, onBewerben }) {
const [offen, setOffen] = useState(null);
return (
Offene Stellen.>}
lead="Aktuelle Stellen bei Betrieben aus Handwerk und Technik. Für Sie ist die Vermittlung kostenlos."
>
{stellen.length ? (
<>
{/* Initiativbewerbung als letzte Zeile der Liste, wie bei Sketch (Abschnitt 17) */}
Nichts Passendes dabei?
>
) : (
Gerade keine offenen Stellen
Neue Stellen kommen laufend dazu. Bewerben Sie sich initiativ – wir melden uns, sobald etwas zu Ihnen passt.
)}
);
}
// Mobbin-Pattern: Datei-Feld als beschriftete Fläche mit Dateiname, Größe, „Entfernen“ und Ablegen per Ziehen -> MOBBIN-PATTERNS.md, Abschnitt 17
function DateiFeld({ id, datei, onDatei, error }) {
const inputRef = useRef(null);
const [ziel, setZiel] = useState(false);
const errId = id + "-fehler";
const pruefen = (f) => {
const endung = (f.name.split(".").pop() || "").toLowerCase();
if (!BEWERBUNG_ENDUNGEN.includes(endung)) {
onDatei(null, "Bitte als PDF, Word-Datei (DOCX), JPG oder PNG hochladen.");
} else if (f.size > BEWERBUNG_MAX_MB * 1024 * 1024) {
onDatei(null, `Die Datei ist zu groß (höchstens ${BEWERBUNG_MAX_MB} MB).`);
} else {
onDatei(f, "");
}
};
const waehlen = (e) => {
const f = e.target.files && e.target.files[0];
if (f) pruefen(f);
e.target.value = ""; // dieselbe Datei lässt sich nach „Entfernen“ erneut wählen
};
// Hineingezogene Datei annehmen – sonst öffnet der Browser sie und das Formular ist weg
const ablegen = (e) => {
e.preventDefault();
setZiel(false);
const f = e.dataTransfer.files && e.dataTransfer.files[0];
if (f) pruefen(f);
};
// Das Datei-Feld bleibt immer im DOM (unsichtbar), damit der Fokus nach „Entfernen“ dorthin zurück kann;
// sichtbar ist entweder die Auswahlfläche (Label) oder die gewählte Datei
return (
{ e.preventDefault(); if (!ziel) setZiel(true); }}
onDragLeave={(e) => { if (!e.currentTarget.contains(e.relatedTarget)) setZiel(false); }}
onDrop={ablegen}>
Lebenslauf (optional)
{datei ? (
);
}
function CheckFeld({ id, checked, onChange, error, children }) {
const errId = id + "-fehler";
return (
{error &&
{error}
}
);
}
const BEWERBUNG_LEER = { stelle: "", vorname: "", nachname: "", email: "", telefon: "", wohnort: "", beruf: "", erfahrung: "", start: "", website: "", datenschutz: false, pool: false };
function bewerbungFehler(w, datei) {
const e = {};
if (!w.stelle) e.stelle = "Bitte wählen Sie eine Stelle oder „Initiativbewerbung“.";
if (!w.vorname.trim()) e.vorname = "Bitte geben Sie Ihren Vornamen an.";
if (!w.nachname.trim()) e.nachname = "Bitte geben Sie Ihren Nachnamen an.";
if (!w.email.trim() || !/^\S+@\S+\.\S+$/.test(w.email)) e.email = "Bitte eine gültige E-Mail-Adresse angeben, z. B. max@web.de.";
if (!w.telefon.trim() || w.telefon.replace(/\D/g, "").length < 6) e.telefon = "Bitte eine gültige Telefonnummer angeben (mind. 6 Ziffern).";
if (!datei) {
if (!w.beruf.trim()) e.beruf = "Bitte geben Sie Ihren Beruf oder Ihre Qualifikation an.";
if (!w.erfahrung.trim()) e.erfahrung = "Bitte geben Sie Ihre Berufserfahrung an.";
if (!w.start.trim()) e.start = "Bitte geben Sie an, ab wann Sie anfangen können.";
}
if (!w.datenschutz) e.datenschutz = "Bitte bestätigen Sie den Datenschutzhinweis.";
return e;
}
function BewerbungsFormular({ stellen, gewaehlt }) {
const [w, setW] = useState(BEWERBUNG_LEER);
const [datei, setDatei] = useState(null);
const [dateiFehler, setDateiFehler] = useState("");
const [fehler, setFehler] = useState({});
const [sending, setSending] = useState(false);
const [sendError, setSendError] = useState("");
const [gesendet, setGesendet] = useState(false);
const dankRef = useRef(null);
useEffect(() => { if (gewaehlt) setW((x) => ({ ...x, stelle: gewaehlt.id })); }, [gewaehlt]);
useEffect(() => { if (gesendet && dankRef.current) dankRef.current.focus(); }, [gesendet]);
const upd = (k, v) => {
setW((x) => ({ ...x, [k]: v }));
if (fehler[k]) setFehler((e) => ({ ...e, [k]: undefined }));
};
const senden = async (ev) => {
ev.preventDefault();
const f = bewerbungFehler(w, datei);
setFehler(f);
const erstes = Object.keys(f)[0];
if (erstes) {
const el = document.getElementById("bewerben-" + erstes);
if (el) el.focus();
return;
}
setSending(true);
setSendError("");
const fd = new FormData();
Object.entries(w).forEach(([k, v]) => {
if (k === "datenschutz" || k === "pool") { if (v) fd.append(k, "ja"); } else fd.append(k, v);
});
if (datei) fd.append("lebenslauf", datei);
let meldung = "Verbindungsfehler – bitte versuchen Sie es erneut.";
try {
// Ohne Content-Type-Header: der Browser setzt multipart/form-data mit Boundary selbst
const res = await fetch("/api-bewerbung.php", { method: "POST", body: fd });
if (res.ok) { setSending(false); setGesendet(true); return; }
meldung = (await res.json().catch(() => ({}))).error || "Die Bewerbung konnte nicht gesendet werden.";
} catch (err) {
console.error("Bewerbung Fehler:", err);
}
setSendError(meldung);
setSending(false);
};
const optionen = [...stellen.map((s) => [s.id, s.titel]), ["initiativ", "Initiativbewerbung"]];
return (
In zwei Minuten beworben.>}
lead="Lebenslauf hochladen oder drei kurze Angaben machen – beides reicht. Wir melden uns persönlich bei Ihnen."
>
{!gesendet ? (
) : (
Vielen Dank!
Ihre Bewerbung ist angekommen. Wir melden uns persönlich bei Ihnen – meist innerhalb von 2 Werktagen.
)}
);
}
function PersonalPage() {
const k = (window.SAWORA_ANGEBOTE || {}).personal;
const stellen = window.SAWORA_STELLEN || [];
// Neues Objekt je Klick, damit auch dieselbe Stelle erneut vorgewählt wird
const [gewaehlt, setGewaehlt] = useState(null);
const bewerben = (id) => {
setGewaehlt({ id });
scrollToId("bewerben");
};
return (
Fachkräfte für Handwerk und Technik – bezahlt erst bei Einstellung.>}
lead="Wir suchen per Direktansprache und Anzeigen, führen die Vorauswahl und stellen Ihnen geprüfte Kandidaten vor. Sie zahlen nur, wenn jemand bei Ihnen anfängt."
actions={<>
Stelle besetzen Offene Stellen
>}
facts={[
[k ? k.honorarProzent + " % nur bei Einstellung" : "Nur bei Einstellung", "primary"],
["Keine Vorkasse", "secondary"],
[k ? k.garantieMonate + " Monate Nachbesetzungsgarantie" : "Nachbesetzungsgarantie", "primary"],
["Für Bewerber kostenlos", "secondary", "#stellen"],
]}
/>
Wir finden die Leute, die nicht suchen.>}
lead="Gute Handwerker und Techniker sind selten arbeitslos. Deshalb kombinieren wir Direktansprache, Anzeigen und einen Bewerbungsweg ohne Hürden."
>
Vom Suchauftrag zur Einstellung.>}
lead="Vier Etappen, ein Ansprechpartner – Sie sprechen nur mit Kandidaten, die passen."
/>
{k && }
Welche Stelle sollen wir besetzen?>}
lead="Beschreiben Sie kurz, wen Sie suchen – wir melden uns innerhalb von 24 Stunden und besprechen den Suchauftrag."
knopf="Suchauftrag anfragen"
felder={[
{ name: "position", label: "Gesuchte Position", art: "text", placeholder: "z. B. Elektroniker (m/w/d)" },
{ name: "anzahl", label: "Anzahl", art: "select", optionen: ["1", "2–3", "4 oder mehr"] },
{ name: "standort", label: "Einsatzort", art: "text", placeholder: "z. B. Neuss" },
{ name: "nachricht", label: "Was ist Ihnen wichtig? (optional)", art: "textarea", placeholder: "Qualifikation, Arbeitszeit, Gehaltsrahmen, Start" },
]}
/>
{k && }
);
}
window.SAWORA_SEITEN = Object.assign(window.SAWORA_SEITEN || {}, { personalvermittlung: PersonalPage });