Update index.html: Stammvertreter-Feature + vereinfachter Admin-Tab

This commit is contained in:
2026-04-18 14:56:24 +00:00
parent ad59027f5b
commit a92f8466ca

View File

@@ -1330,6 +1330,13 @@
<div style="padding:12px 0 6px; font-size:0.78rem; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:1px;">Aktive Vertretungen</div>
<div id="substitutes-list"></div>
</div>
<div style="padding:0 16px 12px;border-top:1px solid var(--border);margin-top:4px;">
<div style="padding:12px 0 6px;display:flex;align-items:center;justify-content:space-between;">
<div style="font-size:0.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px;">Stammvertreter-Vorrat</div>
<button class="btn btn-secondary btn-sm" onclick="openAddSv()">+ Person</button>
</div>
<div id="sv-list-container"></div>
</div>
</div>
</div>
</div>
@@ -1452,6 +1459,38 @@
</div>
</div>
<!-- Stammvertreter Modal -->
<div class="modal-overlay" id="modal-sv">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="sv-modal-title">Stammvertreter hinzufügen</div>
<button class="modal-close" onclick="closeModal('modal-sv')">✕</button>
</div>
<input type="hidden" id="sv-modal-id">
<label>Name *</label>
<input id="sv-modal-name" type="text" placeholder="Vor- und Nachname">
<div class="form-row">
<div><label>Firma</label><input id="sv-modal-company" type="text" placeholder="Firma GmbH"></div>
<div><label>Fachgebiet</label><input id="sv-modal-specialty" type="text" placeholder="Fachgebiet"></div>
</div>
<label>Foto</label>
<div class="offline-photo-row" id="sv-photo-row">
<img class="offline-photo-thumb" id="sv-photo-thumb" src="" alt="" style="display:none;">
<div class="offline-photo-actions">
<label class="btn-file-label">📁 Datei wählen
<input type="file" id="sv-photo-file" accept="image/*" style="display:none" onchange="handleSvPhotoFile(this)">
</label>
<button class="btn btn-danger btn-sm" id="sv-remove-photo" onclick="removeSvPhoto()" style="display:none;">✕ Entfernen</button>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-danger btn-sm" id="sv-delete-btn" onclick="deleteStammvertreter(document.getElementById('sv-modal-id').value)" style="display:none;">Löschen</button>
<button class="btn btn-ghost" onclick="closeModal('modal-sv')">Abbrechen</button>
<button class="btn btn-primary" onclick="saveStammvertreterModal()">Speichern</button>
</div>
</div>
</div>
<!-- Substitute Quick-Pick Modal -->
<div class="modal-overlay" id="modal-substitute">
<div class="modal">
@@ -1461,6 +1500,12 @@
</div>
<input type="hidden" id="sub-modal-member-id">
<div id="sub-sv-picker" style="display:none;margin-bottom:12px;">
<div style="font-size:0.75rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;">Stammvertreter wählen</div>
<div id="sub-sv-list" style="display:flex;flex-wrap:wrap;gap:8px;max-height:110px;overflow-y:auto;"></div>
<hr style="margin:12px 0 0;border:none;border-top:1px solid var(--border);">
</div>
<label>Vertreter Name *</label>
<input id="sub-modal-name" type="text" placeholder="Name">
@@ -1751,6 +1796,151 @@ function extrasKey(name) {
return (name || '').toLowerCase().trim();
}
// ============================================================
// STAMMVERTRETER (Profilvorrat für Vertretungen)
// ============================================================
function stammvertreterKey() {
return activeChapter ? `bni-stammvertreter-${activeChapter.code}` : 'bni-stammvertreter';
}
function loadStammvertreter() {
try { return JSON.parse(localStorage.getItem(stammvertreterKey()) || '[]'); } catch(e) { return []; }
}
function saveStammvertreter(list) {
localStorage.setItem(stammvertreterKey(), JSON.stringify(list));
}
let _svEditingPhotoB64 = null;
function openAddSv() {
document.getElementById('sv-modal-id').value = '';
document.getElementById('sv-modal-title').textContent = 'Stammvertreter hinzufügen';
document.getElementById('sv-modal-name').value = '';
document.getElementById('sv-modal-company').value = '';
document.getElementById('sv-modal-specialty').value = '';
document.getElementById('sv-delete-btn').style.display = 'none';
_svEditingPhotoB64 = null;
_updateSvPhotoUI(null);
openModal('modal-sv');
}
function openEditSv(svId) {
const sv = loadStammvertreter().find(s => s.id === svId);
if (!sv) return;
document.getElementById('sv-modal-id').value = sv.id;
document.getElementById('sv-modal-title').textContent = 'Stammvertreter bearbeiten';
document.getElementById('sv-modal-name').value = sv.name;
document.getElementById('sv-modal-company').value = sv.company || '';
document.getElementById('sv-modal-specialty').value = sv.specialty || '';
document.getElementById('sv-delete-btn').style.display = '';
_svEditingPhotoB64 = sv.photoB64 || null;
_updateSvPhotoUI(_svEditingPhotoB64);
openModal('modal-sv');
}
function _updateSvPhotoUI(b64) {
_svEditingPhotoB64 = b64;
const thumb = document.getElementById('sv-photo-thumb');
const btn = document.getElementById('sv-remove-photo');
if (b64) { thumb.src = b64; thumb.style.display = 'block'; btn.style.display = 'inline-flex'; }
else { thumb.src = ''; thumb.style.display = 'none'; btn.style.display = 'none'; }
}
function handleSvPhotoFile(input) {
const file = input.files && input.files[0];
if (!file) return;
resizePhotoToBase64(file).then(b64 => _updateSvPhotoUI(b64)).catch(e => alert(e.message));
input.value = '';
}
function removeSvPhoto() { _updateSvPhotoUI(null); }
function saveStammvertreterModal() {
const name = document.getElementById('sv-modal-name').value.trim();
if (!name) { alert('Bitte Namen eingeben.'); return; }
const list = loadStammvertreter();
const existingId = document.getElementById('sv-modal-id').value;
const sv = {
id: existingId || 'sv-' + Date.now(),
name,
company: document.getElementById('sv-modal-company').value.trim(),
specialty: document.getElementById('sv-modal-specialty').value.trim(),
photoB64: _svEditingPhotoB64 || null,
};
if (existingId) {
const idx = list.findIndex(s => s.id === existingId);
if (idx >= 0) list[idx] = sv; else list.push(sv);
} else {
list.push(sv);
}
saveStammvertreter(list);
closeModal('modal-sv');
renderStammvertreterSection();
}
function deleteStammvertreter(svId) {
if (!confirm('Stammvertreter löschen?')) return;
saveStammvertreter(loadStammvertreter().filter(s => s.id !== svId));
closeModal('modal-sv');
renderStammvertreterSection();
}
function renderStammvertreterSection() {
const container = document.getElementById('sv-list-container');
if (!container) return;
const list = loadStammvertreter();
if (list.length === 0) {
container.innerHTML = `<div style="color:var(--muted);font-size:0.82rem;padding:2px 0 8px;">Noch keine gespeichert. → <a href="admin.html" target="_blank" style="color:var(--red);">Admin öffnen</a> zum Importieren aus anderen Chaptern.</div>`;
return;
}
container.innerHTML = '';
list.forEach(sv => {
const photo = sv.photoB64
? `<img src="${sv.photoB64}" style="width:34px;height:34px;border-radius:50%;object-fit:cover;flex-shrink:0;">`
: `<div style="width:34px;height:34px;border-radius:50%;background:${getColor(sv.name)};display:flex;align-items:center;justify-content:center;color:white;font-size:0.68rem;font-weight:700;flex-shrink:0;">${getInitials(sv.name)}</div>`;
const div = document.createElement('div');
div.style.cssText = 'display:flex;align-items:center;gap:8px;padding:5px 0;border-bottom:1px solid var(--border);';
div.innerHTML = `${photo}<div style="flex:1;min-width:0;"><div style="font-size:0.83rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${escHtml(sv.name)}</div><div style="font-size:0.72rem;color:var(--muted);">${escHtml(sv.company||'')}${sv.specialty?' · '+escHtml(sv.specialty):''}</div></div><button class="btn btn-ghost btn-sm" style="padding:3px 7px;" onclick="openEditSv('${sv.id}')">✏</button>`;
container.appendChild(div);
});
}
function renderSvPicker() {
const list = loadStammvertreter();
const picker = document.getElementById('sub-sv-picker');
const container = document.getElementById('sub-sv-list');
if (!picker || !container) return;
if (list.length === 0) { picker.style.display = 'none'; return; }
picker.style.display = '';
container.innerHTML = '';
list.forEach(sv => {
const photoHtml = sv.photoB64
? `<img src="${sv.photoB64}" style="width:32px;height:32px;border-radius:50%;object-fit:cover;">`
: `<div style="width:32px;height:32px;border-radius:50%;background:${getColor(sv.name)};display:flex;align-items:center;justify-content:center;color:white;font-size:0.68rem;font-weight:700;">${getInitials(sv.name)}</div>`;
const card = document.createElement('div');
card.style.cssText = 'display:flex;align-items:center;gap:7px;padding:6px 9px;background:#f8f9fa;border:1px solid var(--border);border-radius:8px;cursor:pointer;';
card.innerHTML = `${photoHtml}<div><div style="font-size:0.79rem;font-weight:600;">${escHtml(sv.name)}</div><div style="font-size:0.69rem;color:var(--muted);">${escHtml(sv.company||'')}</div></div>`;
card.onclick = () => pickStammvertreter(sv.id);
container.appendChild(card);
});
}
function pickStammvertreter(svId) {
const sv = loadStammvertreter().find(s => s.id === svId);
if (!sv) return;
document.getElementById('sub-modal-name').value = sv.name;
document.getElementById('sub-modal-company').value = sv.company || '';
document.getElementById('sub-modal-specialty').value = sv.specialty || '';
document.getElementById('sub-modal-photo').value = '';
if (sv.photoB64) {
const extras = loadExtras();
const key = extrasKey(sv.name);
if (!extras[key]) extras[key] = {};
extras[key].photoB64 = sv.photoB64;
saveExtras(extras);
}
document.getElementById('sub-sv-picker').style.display = 'none';
}
// Resize an image File to base64 JPEG (max maxPx on longest side)
function resizePhotoToBase64(file, maxPx = 400) {
return new Promise((resolve, reject) => {
@@ -2418,6 +2608,7 @@ function renderControl() {
renderSubstituteMemberSelect();
renderInvitedBySelect();
renderAdminPanel();
renderStammvertreterSection();
updateNextButton();
syncDot = document.getElementById('sync-dot');
}
@@ -3058,6 +3249,7 @@ function openSubstituteModal(memberId) {
document.getElementById('sub-modal-specialty').value = sub.specialty || '';
document.getElementById('sub-modal-photo').value = sub.photoUrl || '';
document.getElementById('sub-modal-skip-round2').checked = sub.skipRound2 || false;
renderSvPicker();
openModal('modal-substitute');
}
@@ -4259,26 +4451,12 @@ function renderAdminPanel() {
<div style="background:#f8f9fa;border:1px solid var(--border);border-radius:8px;padding:12px;margin-bottom:14px;">
<div style="font-size:0.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">Aktives Chapter</div>
<div style="font-weight:600;margin-bottom:2px;">${cfg.chapterName || '—'}</div>
<div style="font-size:0.82rem;color:var(--muted);">Version: <strong>${APP_VERSION}</strong> · Lizenzprüfung: ${lastChecked}</div>
<div style="font-size:0.82rem;color:var(--muted);">Version: <strong>${APP_VERSION}</strong></div>
<div style="font-size:0.82rem;color:var(--muted);">Lizenzprüfung: ${lastChecked}</div>
</div>
<div style="font-size:0.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">Mitglieder laden</div>
<button class="btn btn-secondary btn-sm" style="width:100%;margin-bottom:4px;" onclick="fetchChapterMembersFromGitea()">🔄 Vom Server laden</button>
<div style="font-size:0.78rem;color:var(--muted);margin-bottom:14px;">Letzter Download: ${lastSync}</div>
<div style="font-size:0.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">Mitglieder hochladen (Admin)</div>
<div style="display:flex;gap:6px;margin-bottom:6px;">
<input id="admin-token-input" type="password" placeholder="Gitea API Token"
value="${hasToken ? '••••••••' : ''}"
style="flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:6px;font-size:0.82rem;"
onfocus="if(this.value==='••••••••')this.value=''">
<button class="btn btn-secondary btn-sm" onclick="saveAdminToken()">Speichern</button>
</div>
<button class="btn btn-primary btn-sm" style="width:100%;margin-bottom:4px;" onclick="uploadMembersToGitea()">📤 Auf Gitea hochladen (inkl. Fotos)</button>
<div id="upload-status" style="font-size:0.78rem;min-height:18px;margin-bottom:4px;"></div>
<div style="font-size:0.78rem;color:var(--muted);margin-bottom:14px;">Letzter Upload: ${lastUpload}</div>
<button class="btn btn-secondary btn-sm" style="width:100%;margin-bottom:16px;" onclick="checkForUpdates()">🔍 Auf Programm-Updates prüfen</button>
<button class="btn btn-secondary btn-sm" style="width:100%;margin-bottom:6px;" onclick="fetchChapterMembersFromGitea()">🔄 Mitglieder vom Server laden</button>
<div style="font-size:0.78rem;color:var(--muted);margin-bottom:16px;">Letzter Download: ${lastSync}</div>
<a href="admin.html" target="_blank" class="btn btn-primary btn-sm" style="width:100%;display:block;text-align:center;text-decoration:none;margin-bottom:16px;">⚙ Admin-Panel öffnen</a>
` : `<div style="color:var(--muted);font-size:0.88rem;margin-bottom:20px;">Kein Chapter aktiviert.</div>`}
<div style="border-top:1px solid var(--border);padding-top:14px;">
<button class="btn btn-ghost btn-sm" style="width:100%;" onclick="confirmSwitchChapter()">↔ Chapter wechseln</button>