/* 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

); } // Mobbin-Pattern: Stellenliste mit aufklappbaren Karten -> MOBBIN-PATTERNS.md, Abschnitt 17 function StellenKarte({ s, offen, onToggle, onBewerben }) { const koerperId = "stelle-" + s.id; return (
  • {s.kurz}

    {[["Aufgaben", s.aufgaben], ["Profil", s.profil], ["Das bietet der Betrieb", s.vorteile]].map(([titel, liste]) => (
    {titel}
      {(liste || []).map((t) => (
    • {t}
    • ))}
    ))}
    {s.arbeitgeber}
  • ); } 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 ? (
    {datei.name} {datei.size < 1024 * 1024 ? Math.max(1, Math.round(datei.size / 1024)) + " KB" : (datei.size / 1024 / 1024).toLocaleString("de-DE", { maximumFractionDigits: 1 }) + " MB"}
    ) : ( )} {error && }
    ); } function CheckFeld({ id, checked, onChange, error, children }) { const errId = id + "-fehler"; return (
    {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 ? (
    upd("stelle", v)} optionen={optionen} error={fehler.stelle} />
    upd("vorname", v)} error={fehler.vorname} autoComplete="given-name" /> upd("nachname", v)} error={fehler.nachname} autoComplete="family-name" /> upd("email", v)} error={fehler.email} autoComplete="email" inputMode="email" /> upd("telefon", v)} error={fehler.telefon} autoComplete="tel" inputMode="tel" /> upd("wohnort", v)} autoComplete="address-level2" /> { setDatei(f); setDateiFehler(meldung); }} />
    {!datei && (

    Kein Lebenslauf? Kein Problem. Dann reichen diese drei Angaben:

    upd("beruf", v)} placeholder="z. B. Elektroniker" error={fehler.beruf} /> upd("erfahrung", v)} placeholder="z. B. 5" error={fehler.erfahrung} inputMode="numeric" /> upd("start", v)} placeholder="z. B. sofort" error={fehler.start} />
    )} {/* Köderfeld gegen Bots: für Menschen unsichtbar und nicht per Tab erreichbar */}
    upd("datenschutz", v)} error={fehler.datenschutz}> Ich habe die Hinweise zum Datenschutz gelesen. Meine Angaben werden nur für die Vermittlung genutzt und erst nach Rücksprache mit mir an ein Unternehmen weitergegeben. * upd("pool", v)}> Sie dürfen mich 12 Monate lang für weitere passende Stellen kontaktieren (Bewerberpool, jederzeit widerrufbar).
    {sendError && (
    {sendError} {/* Die Server-Meldung bei Fehler 500 nennt die Adresse schon */} {!sendError.includes(CONTACT.email) && <> Sie erreichen uns auch direkt unter {CONTACT.email}.}
    )}

    * Pflichtfelder. Die Vermittlung ist für Sie kostenlos.

    {sending ? "Wird gesendet …" : <>Bewerbung senden }
    ) : (

    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 });