Files
guyg1474andClaude Opus 5.5 819219fb2d Rollen (Admin/Mitglied), Seite Kooperationen, Textanpassungen
- Gemeinsame Zugänge mit Rollen: Admin darf alles, Mitglied nutzt Kalender,
  Fahrtplaner, Treffpunkte und bearbeitet Mitglieder (kein Hinzufügen,
  Löschen, Import, keine Patches, keine Statistik-Korrektur)
- Server prüft die Rechte (403), Konten ohne Rolle gelten als Admin
- npm run user: add <name> [admin|mitglied], role <name> <rolle>
- Neue öffentliche Seite Kooperationen mit Titelbild, Anführer,
  Social-Media-Links und Beschreibung, Pflege durch Admin
- Kopfzeile bleibt bei mittlerer Breite einzeilig
- Texte auf Wunsch gekürzt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:47:03 +02:00

233 lines
7.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { $, api, esc, sortByName, seatsLabel, showError, renderHeader, isAdmin } from './common.js';
renderHeader('gruppe');
// Admin: Mitglieder hinzufügen, löschen, importieren. Mitglied-Zugang: nur bearbeiten.
// Der Server prüft das ebenfalls – hier wird nur ausgeblendet, was ohnehin abgelehnt würde.
let admin = false;
let members = [];
let points = [];
const form = $('#member-form');
$('#member-hascar').addEventListener('change', (e) => {
$('#member-seats-wrap').hidden = !e.target.checked;
});
// Auswahl "fährt am liebsten mit bei" – alle anderen Mitglieder
function fillRidesWith(selfId, selected) {
$('#member-rideswith').innerHTML = '<option value="">(keine Vorliebe)</option>'
+ sortByName(members.filter((m) => m.id !== selfId))
.map((m) => `<option value="${m.id}" ${m.id === selected ? 'selected' : ''}>${esc(m.name)}</option>`).join('');
}
// Auswahl Standard-Treffpunkt
function fillMemberPoint(selected) {
$('#member-point').innerHTML = '<option value="">(Treffpunkt der Fahrt)</option>'
+ points.map((p) => `<option value="${p.id}" ${p.id === selected ? 'selected' : ''}>${esc(p.name)}</option>`).join('');
}
function resetForm() {
form.reset();
fillRidesWith();
fillMemberPoint();
$('#member-id').value = '';
$('#member-seats-wrap').hidden = true;
$('#member-cancel').hidden = true;
$('#member-form-title').textContent = 'Mitglied hinzufügen';
// Ohne Admin-Rechte gibt es das Formular nur zum Bearbeiten
$('#member-form-card').hidden = !admin;
}
$('#member-cancel').addEventListener('click', resetForm);
form.addEventListener('submit', async (e) => {
e.preventDefault();
const body = {
name: $('#member-name').value.trim(),
external: $('#member-external').checked,
hasCar: $('#member-hascar').checked,
seats: $('#member-seats').value,
alwaysDrives: $('#member-always').checked,
ridesWith: $('#member-rideswith').value || null,
meetingPoint: $('#member-point').value || null,
};
const id = $('#member-id').value;
try {
if (id) await api('PUT', `/members/${id}`, body);
else await api('POST', '/members', body);
resetForm();
await load();
} catch (err) {
showError(err);
}
});
function edit(id) {
const m = members.find((x) => x.id === id);
$('#member-id').value = m.id;
$('#member-name').value = m.name;
$('#member-external').checked = !!m.external;
fillMemberPoint(m.meetingPoint);
$('#member-hascar').checked = m.hasCar;
$('#member-seats').value = m.seats || 4;
$('#member-always').checked = !!m.alwaysDrives;
fillRidesWith(m.id, m.ridesWith);
$('#member-seats-wrap').hidden = !m.hasCar;
$('#member-cancel').hidden = false;
$('#member-form-title').textContent = 'Mitglied bearbeiten';
$('#member-form-card').hidden = false;
$('#member-form-card').scrollIntoView({ behavior: 'smooth', block: 'start' });
$('#member-name').focus({ preventScroll: true });
}
async function remove(id) {
const m = members.find((x) => x.id === id);
if (!confirm(`${m.name} wirklich aus der Gruppe löschen?`)) return;
try {
await api('DELETE', `/members/${id}`);
await load();
} catch (err) {
showError(err);
}
}
function render() {
if (!$('#member-id').value) fillRidesWith();
const nameOf = (id) => members.find((m) => m.id === id)?.name;
const pointOf = (id) => points.find((p) => p.id === id)?.name;
$('#members-empty').hidden = members.length > 0;
$('#members-list').innerHTML = sortByName(members).map((m) => `
<li>
<span class="name">${esc(m.name)}${m.external ? ' <span class="badge">Extern</span>' : ''}</span>
${m.hasCar && pointOf(m.meetingPoint) ? `<span class="meta">ab ${esc(pointOf(m.meetingPoint))}</span>` : ''}
${nameOf(m.ridesWith) ? `<span class="meta">fährt gern bei ${esc(nameOf(m.ridesWith))} mit</span>` : ''}
<span class="badge">${m.hasCar ? `Auto · ${seatsLabel(m.seats)}${m.alwaysDrives ? ' · fährt immer' : ''}` : 'kein Auto'}</span>
<span class="actions">
<button class="link" data-edit="${m.id}">Bearbeiten</button>
${admin ? `<button class="link danger" data-del="${m.id}">Löschen</button>` : ''}
</span>
</li>`).join('');
}
$('#members-list').addEventListener('click', (e) => {
if (e.target.dataset.edit) edit(e.target.dataset.edit);
if (e.target.dataset.del) remove(e.target.dataset.del);
});
$('#export-btn').addEventListener('click', () => {
const blob = new Blob([JSON.stringify(members, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'hcrb-gruppe.json';
a.click();
URL.revokeObjectURL(a.href);
});
$('#import-file').addEventListener('change', async (e) => {
const file = e.target.files[0];
e.target.value = '';
if (!file) return;
let data;
try {
data = JSON.parse(await file.text());
if (!Array.isArray(data)) throw new Error();
} catch {
alert('Die Datei konnte nicht gelesen werden.');
return;
}
if (!confirm(`${data.length} Mitglieder importieren? Die aktuelle Gruppe wird ersetzt.`)) return;
try {
await api('POST', '/members/import', data);
await load();
} catch (err) {
showError(err);
}
});
// ---------- Treffpunkte ----------
const pointForm = $('#point-form');
function resetPointForm() {
pointForm.reset();
$('#point-id').value = '';
$('#point-cancel').hidden = true;
$('#point-form-title').textContent = 'Treffpunkt hinzufügen';
}
$('#point-cancel').addEventListener('click', resetPointForm);
pointForm.addEventListener('submit', async (e) => {
e.preventDefault();
const body = { name: $('#point-name').value.trim(), address: $('#point-address').value.trim() };
const id = $('#point-id').value;
try {
if (id) await api('PUT', `/meeting-points/${id}`, body);
else await api('POST', '/meeting-points', body);
resetPointForm();
await loadPoints();
} catch (err) {
showError(err);
}
});
function renderPoints() {
$('#points-empty').hidden = points.length > 0;
$('#points-list').innerHTML = points.map((p) => `
<li>
<span class="name">${esc(p.name)}</span>
<span class="meta">${esc(p.address)}</span>
<span class="actions">
<button class="link" data-edit="${p.id}">Bearbeiten</button>
<button class="link danger" data-del="${p.id}">Löschen</button>
</span>
</li>`).join('');
}
$('#points-list').addEventListener('click', async (e) => {
const { edit: editId, del } = e.target.dataset;
if (editId) {
const p = points.find((x) => x.id === editId);
$('#point-id').value = p.id;
$('#point-name').value = p.name;
$('#point-address').value = p.address;
$('#point-cancel').hidden = false;
$('#point-form-title').textContent = 'Treffpunkt bearbeiten';
$('#point-name').focus();
}
if (del) {
const p = points.find((x) => x.id === del);
if (!confirm(`Treffpunkt „${p.name}“ wirklich löschen?`)) return;
try {
await api('DELETE', `/meeting-points/${del}`);
await loadPoints();
} catch (err) {
showError(err);
}
}
});
async function loadPoints() {
points = await api('GET', '/meeting-points');
renderPoints();
const editing = members.find((m) => m.id === $('#member-id').value);
fillMemberPoint(editing ? $('#member-point').value : undefined);
render();
}
async function load() {
members = await api('GET', '/members');
render();
}
async function init() {
admin = await isAdmin();
$('#members-role-hint').hidden = admin;
$('#import-btn').hidden = !admin;
if (!admin) $('#backup-text').textContent = 'Gruppe als JSON-Datei sichern.';
resetForm();
await load();
await loadPoints();
}
init().catch(showError);