feat(admin): ein Speichern-Button auf Einstellungs-Ebene mit Format-Validierung
- Speichern-Button aus dem einklappbaren „Notfall-Profil" auf die Karten-/ Einstellungs-Ebene verschoben (immer sichtbar) und in „Speichern" umbenannt. - Speichert jetzt ALLE Daten: alle Profilfelder + Standort-Freigabe + erlaubte Sprachen in einem PUT. - Vor JEDEM Speichern Format-Validierung pro Feld; bei Verstoß rotes Feld (border/ring-red) + Meckertext, Speichern wird abgebrochen. Rote Markierung verschwindet beim Nachbessern (input-Listener). - Regeln: PLZ = 5 Ziffern · Telefon = +/Ziffern/Leer/()/- (6–20) · Geburtsdatum = TT.MM.JJJJ (echtes Datum) · Kontakt-E-Mails/-Telefone = CSV gültiger Werte. Leere optionale Felder gelten als konform. Validierungslogik in node geprüft. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5d05e081d4
commit
57002dc022
1 changed files with 65 additions and 10 deletions
|
|
@ -330,6 +330,27 @@ async function copyText(text, btn) {
|
|||
const ALL_LANGS = [["de","DE"],["en","EN"],["fr","FR"],["es","ES"],["it","IT"],
|
||||
["pt","PT"],["pl","PL"],["ar","AR"],["ru","RU"],["zh","ZH"]];
|
||||
|
||||
// --- Format-Validierung der Notfall-Profil-Felder ------------------------
|
||||
const _isEmail = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
|
||||
const _isPhone = (v) => /^\+?[\d\s/().\-]{6,20}$/.test(v);
|
||||
function _isGermanDate(v) {
|
||||
const m = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(v);
|
||||
if (!m) return false;
|
||||
const d = +m[1], mo = +m[2], y = +m[3];
|
||||
const dt = new Date(y, mo - 1, d);
|
||||
return dt.getDate() === d && dt.getMonth() === mo - 1 && dt.getFullYear() === y && y >= 1900 && y <= 2100;
|
||||
}
|
||||
const _csvAll = (v, fn) => v.split(",").map((s) => s.trim()).filter(Boolean).every(fn);
|
||||
// pro Feld: ok(wert) wird NUR bei nicht-leerem Wert geprüft + Meckertext.
|
||||
const PROF_VALIDATORS = {
|
||||
postal_code: { ok: (v) => /^\d{5}$/.test(v), msg: "PLZ: genau 5 Ziffern" },
|
||||
phone_mobile: { ok: _isPhone, msg: "Mobil: gültige Telefonnummer (z. B. +49 151 …)" },
|
||||
phone_landline: { ok: _isPhone, msg: "Festnetz: gültige Telefonnummer" },
|
||||
birth_date: { ok: _isGermanDate, msg: "Geburtsdatum: TT.MM.JJJJ" },
|
||||
emergency_contacts: { ok: (v) => _csvAll(v, _isEmail), msg: "Kontakt-E-Mail(s): gültige Adressen (Komma-getrennt)" },
|
||||
emergency_phones: { ok: (v) => _csvAll(v, _isPhone), msg: "Kontakt-Telefon(e): gültige Nummern (Komma-getrennt)" },
|
||||
};
|
||||
|
||||
// Wendet die vom Admin erlaubten Sprachen auf das Sprachmenü an. Leer = alle erlaubt.
|
||||
// Genau eine erlaubte Sprache -> Menü ausblenden (kein Stress für den Nutzer).
|
||||
function applyAllowedLanguages(prefs) {
|
||||
|
|
@ -1100,20 +1121,22 @@ function buildUserCard(u) {
|
|||
<input class="prof-f w-1/2 rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500" data-k="phone_mobile" placeholder="Mobil" />
|
||||
<input class="prof-f w-1/2 rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500" data-k="phone_landline" placeholder="Festnetz" />
|
||||
</div>
|
||||
<input class="prof-f w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500" data-k="birth_date" placeholder="Geburtsdatum (z. B. 1940-05-01)" />
|
||||
<input class="prof-f w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500" data-k="birth_date" placeholder="Geburtsdatum (TT.MM.JJJJ)" />
|
||||
<textarea class="prof-f w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500" data-k="medical_notes" rows="2" placeholder="Medizinische Hinweise (Allergien, Medikamente, Vorerkrankungen)"></textarea>
|
||||
<input class="prof-f w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500" data-k="emergency_contacts" placeholder="Kontakt-E-Mail(s), Komma-getrennt" />
|
||||
<input class="prof-f w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500" data-k="emergency_phones" placeholder="Kontakt-Telefon(e), Komma-getrennt" />
|
||||
<label class="flex items-center gap-2 text-xs text-slate-600 dark:text-slate-300">
|
||||
<input type="checkbox" class="prof-consent" /> Standort im Notfall mitsenden
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="prof-save rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-xs px-3 py-1.5 transition-colors">Profil speichern</button>
|
||||
<span class="prof-status text-xs text-slate-400"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Speichern (Einstellungs-Ebene) — prüft Datenformate, speichert ALLE Felder + Sprachen -->
|
||||
<div class="mt-3 pt-3 border-t border-slate-100 dark:border-slate-700 flex items-center gap-2 flex-wrap">
|
||||
<button class="prof-save rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-sm px-4 py-1.5 transition-colors">Speichern</button>
|
||||
<span class="prof-status text-xs text-slate-400"></span>
|
||||
</div>
|
||||
|
||||
<!-- Erinnerungen -->
|
||||
<div class="mt-3 pt-3 border-t border-slate-100 dark:border-slate-700">
|
||||
<button class="btn-mem text-xs text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 flex items-center gap-1 transition-colors">
|
||||
|
|
@ -1170,17 +1193,49 @@ function buildUserCard(u) {
|
|||
profSection.classList.toggle("hidden", open);
|
||||
profArrow.textContent = open ? "▶" : "▼";
|
||||
});
|
||||
const profStatus = card.querySelector(".prof-status");
|
||||
|
||||
// Prüft jedes Feld gegen sein Format (nur nicht-leere Werte), markiert Fehler rot.
|
||||
// Liefert die Liste der Meckertexte (leer = alles konform).
|
||||
function validateProfile() {
|
||||
const errs = [];
|
||||
PROF_KEYS.forEach((k) => {
|
||||
const el = card.querySelector(`.prof-f[data-k="${k}"]`);
|
||||
if (!el) return;
|
||||
const v = el.value.trim();
|
||||
const rule = PROF_VALIDATORS[k];
|
||||
const bad = !!v && !!rule && !rule.ok(v);
|
||||
el.classList.toggle("border-red-500", bad);
|
||||
el.classList.toggle("ring-2", bad);
|
||||
el.classList.toggle("ring-red-500", bad);
|
||||
if (bad) errs.push(rule.msg);
|
||||
});
|
||||
return errs;
|
||||
}
|
||||
// Rote Markierung verschwindet, sobald nachgebessert wird.
|
||||
PROF_KEYS.forEach((k) => {
|
||||
const el = card.querySelector(`.prof-f[data-k="${k}"]`);
|
||||
if (el) el.addEventListener("input", validateProfile);
|
||||
});
|
||||
|
||||
card.querySelector(".prof-save").addEventListener("click", async () => {
|
||||
const body = { location_consent: !!consentEl.checked };
|
||||
const errs = validateProfile();
|
||||
if (errs.length) {
|
||||
profStatus.className = "prof-status text-xs text-red-600 dark:text-red-400";
|
||||
profStatus.textContent = "Bitte korrigieren: " + errs.join(" · ");
|
||||
return;
|
||||
}
|
||||
const body = { location_consent: !!consentEl.checked, allowed_languages: allowed.join(",") };
|
||||
PROF_KEYS.forEach((k) => {
|
||||
const el = card.querySelector(`.prof-f[data-k="${k}"]`);
|
||||
body[k] = el ? el.value.trim() : "";
|
||||
});
|
||||
const status = card.querySelector(".prof-status");
|
||||
status.textContent = "speichere …";
|
||||
profStatus.className = "prof-status text-xs text-slate-400";
|
||||
profStatus.textContent = "speichere …";
|
||||
const res = await adminFetch(`/api/admin/users/${uid}/prefs`, "PUT", body);
|
||||
status.textContent = res ? "✓ gespeichert" : "Fehler";
|
||||
setTimeout(() => { status.textContent = ""; }, 2500);
|
||||
profStatus.className = "prof-status text-xs " + (res ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400");
|
||||
profStatus.textContent = res ? "✓ alle Daten gespeichert" : "Fehler beim Speichern";
|
||||
if (res) setTimeout(() => { profStatus.textContent = ""; }, 3000);
|
||||
});
|
||||
|
||||
// Admin-Rechte (persistentes Flag) ein-/ausschalten
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue