Update index.html: Stammvertreter-Feature + vereinfachter Admin-Tab
This commit is contained in:
216
index.html
216
index.html
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user