<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Mitglieder-Bewertungen</title>
<style>
:root{
--main:#2b6777;
--main-dark:#1f4e58;
--bg:#f5f7f8;
--card:#ffffff;
--text:#1c2b2f;
--accent:#f2a541;
--danger:#c0392b;
--border:#d9e2e4;
}
*{box-sizing:border-box;}
body{
margin:0;
font-family:-apple-system,Segoe UI,Roboto,Arial,sans-serif;
background:var(--bg);
color:var(--text);
padding-bottom:80px;
}
header{
background:var(--main);
color:#fff;
padding:16px;
text-align:center;
font-size:1.4em;
font-weight:bold;
position:sticky;
top:0;
z-index:10;
}
.container{padding:12px;max-width:600px;margin:0 auto;}
.btn{
display:inline-block;
padding:14px 18px;
border:none;
border-radius:10px;
background:var(--main);
color:#fff;
font-size:1em;
font-weight:600;
cursor

ointer;
text-align:center;
min-height:48px;
}
.btn:active{background:var(--main-dark);}
.btn.secondary{background:#8fa6ab;}
.btn.danger{background:var(--danger);}
.btn.accent{background:var(--accent);color:#332200;}
.btn-row{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0;}
.btn-row .btn{flex:1;}
.card{
background:var(--card);
border-radius:12px;
padding:14px;
margin-bottom:12px;
box-shadow:0 1px 3px rgba(0,0,0,0.1);
border:1px solid var(--border);
}
.member-row{
display:flex;
align-items:center;
gap:12px;
cursor

ointer;
}
.avatar{
width:48px;height:48px;border-radius:50%;
background:var(--main);
color:#fff;
display:flex;align-items:center;justify-content:center;
font-weight:bold;font-size:1.2em;
flex-shrink:0;
overflow:hidden;
}
.avatar img{width:100%;height:100%;object-fit:cover;}
.member-info{flex:1;min-width:0;}
.member-name{font-size:1.1em;font-weight:600;}
.member-avg{font-size:0.95em;color:#555;}
.stars{color:var(--accent);font-size:1.1em;}
input,select{
width:100%;
padding:12px;
font-size:1em;
border:1px solid var(--border);
border-radius:8px;
margin-bottom:10px;
background:#fff;
color:var(--text);
}
label{font-weight:600;display:block;margin-bottom:4px;font-size:0.95em;}
.modal-bg{
position:fixed;top:0;left:0;right:0;bottom:0;
background:rgba(0,0,0,0.5);
display:none;
align-items:flex-start;
justify-content:center;
z-index:100;
overflow-y:auto;
padding:20px 0;
}
.modal-bg.active{display:flex;}
.modal{
background:#fff;
border-radius:14px;
padding:18px;
width:90%;
max-width:480px;
margin-top:20px;
}
.modal h2{margin-top:0;font-size:1.2em;color:var(--main);}
.rating-select{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px;}
.rating-btn{
flex:1;
min-width:40px;
padding:10px 0;
border-radius:8px;
border:2px solid var(--border);
background:#fff;
font-weight:bold;
text-align:center;
cursor

ointer;
}
.rating-btn.selected{background:var(--accent);border-color:var(--accent);color:#332200;}
.history-entry{
display:flex;
justify-content:space-between;
align-items:center;
padding:8px 0;
border-bottom:1px solid var(--border);
font-size:0.95em;
}
.history-entry:last-child{border-bottom:none;}
.empty-hint{color:#888;text-align:center;padding:30px 10px;}
.sort-row{display:flex;gap:8px;margin-bottom:12px;}
.sort-row select{margin-bottom:0;}
.top-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:10px;}
.small-btn{padding:8px 12px;font-size:0.85em;min-height:auto;}
.close-x{
position:absolute;top:10px;right:14px;
font-size:1.4em;cursor

ointer;color:#888;
}
.modal{position:relative;}
footer{
text-align:center;
font-size:0.75em;
color:#999;
padding:20px 10px;
}
</style>
</head>
<body>
<header>👥 Mitglieder-Bewertungen</header>
<div class="container">
<div class="btn-row">
<button class="btn" onclick="openMemberModal()">➕ Mitglied hinzufügen</button>
</div>
<div class="sort-row">
<select id="sortSelect" onchange="renderList()">
<option value="name">Sortieren: Name (A-Z)</option>
<option value="avg_desc">Sortieren: Bewertung (hoch → niedrig)</option>
<option value="avg_asc">Sortieren: Bewertung (niedrig → hoch)</option>
</select>
</div>
<div id="memberList"></div>
<div class="btn-row">
<button class="btn secondary" onclick="exportData()">⬇️ Backup exportieren</button>
<button class="btn secondary" onclick="document.getElementById('importFile').click()">⬆️ Backup importieren</button>
</div>
<input type="file" id="importFile" accept=".json" style="display:none" onchange="importData(event)">
</div>
<footer>Alle Daten werden nur lokal auf diesem Gerät gespeichert.</footer>
<!-- Modal: Mitglied hinzufügen/bearbeiten -->
<div class="modal-bg" id="memberModalBg">
<div class="modal">
<span class="close-x" onclick="closeMemberModal()">✕</span>
<h2 id="memberModalTitle">Mitglied hinzufügen</h2>
<label>Name</label>
<input type="text" id="memberNameInput" placeholder="z. B. Anna Müller">
<label>Foto (optional)</label>
<input type="file" id="memberPhotoInput" accept="image/*">
<div id="photoPreviewWrap" style="margin-bottom:10px;"></div>
<div class="btn-row">
<button class="btn" onclick="saveMember()">Speichern</button>
<button class="btn secondary" onclick="closeMemberModal()">Abbrechen</button>
</div>
</div>
</div>
<!-- Modal: Mitglied Detail / Verlauf / Bewerten -->
<div class="modal-bg" id="detailModalBg">
<div class="modal">
<span class="close-x" onclick="closeDetailModal()">✕</span>
<h2 id="detailName"></h2>
<p id="detailAvg" style="font-size:1.1em;"></p>
<div class="btn-row">
<button class="btn accent" onclick="openRateModal()">⭐ Neue Bewertung</button>
</div>
<div class="top-actions">
<button class="btn secondary small-btn" onclick="editMember()">✏️ Bearbeiten</button>
<button class="btn danger small-btn" onclick="deleteMember()">🗑️ Löschen</button>
</div>
<h2 style="margin-top:20px;">Verlauf</h2>
<div id="historyList"></div>
</div>
</div>
<!-- Modal: Bewertung abgeben -->
<div class="modal-bg" id="rateModalBg">
<div class="modal">
<span class="close-x" onclick="closeRateModal()">✕</span>
<h2>Bewertung abgeben</h2>
<label>Bewertung (1-10)</label>
<div class="rating-select" id="ratingSelect"></div>
<label>Datum</label>
<input type="date" id="rateDateInput">
<div class="btn-row">
<button class="btn" onclick="saveRating()">Speichern</button>
<button class="btn secondary" onclick="closeRateModal()">Abbrechen</button>
</div>
</div>
</div>
<script>
const STORAGE_KEY = 'mitglieder_bewertungen_app_v1';
let data = { members: [] };
let currentEditId = null;
let currentDetailId = null;
let selectedRating = null;
let pendingPhotoDataUrl = null;
function loadData(){
const raw = localStorage.getItem(STORAGE_KEY);
if(raw){
try{ data = JSON.parse(raw); }catch(e){ data = { members: [] }; }
}
if(!data.members) data.members = [];
}
function saveData(){
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
function uid(){
return 'id_' + Date.now() + '_' + Math.random().toString(36).slice(2,8);
}
function avgOf(member){
if(!member.ratings || member.ratings.length===0) return null;
const sum = member.ratings.reduce((a,r)=>a+r.value,0);
return sum / member.ratings.length;
}
function starsHtml(avg){
if(avg===null) return '<span style="color:#999">Noch keine Bewertung</span>';
const rounded = Math.round(avg/2);
let s = '';
for(let i=0;i<5;i++){ s += i<rounded ? '★' : '☆'; }
return '<span class="stars">'+s+'</span> ('+avg.toFixed(1)+' / 10)';
}
function renderList(){
const listEl = document.getElementById('memberList');
listEl.innerHTML = '';
if(data.members.length===0){
listEl.innerHTML = '<div class="empty-hint">Noch keine Mitglieder vorhanden.<br>Tippe oben auf "Mitglied hinzufügen".</div>';
return;
}
let members = [...data.members];
const sortMode = document.getElementById('sortSelect').value;
members.forEach(m=>m._avg = avgOf(m));
if(sortMode==='name'){
members.sort((a,b)=>a.name.localeCompare(b.name,'de'));
} else if(sortMode==='avg_desc'){
members.sort((a,b)=>(b._avg??-1)-(a._avg??-1));
} else if(sortMode==='avg_asc'){
members.sort((a,b)=>(a._avg??999)-(b._avg??999));
}
members.forEach(m=>{
const card = document.createElement('div');
card.className = 'card';
const avg = m._avg;
const initials = m.name.trim().slice(0,1).toUpperCase();
let avatarHtml = m.photo ? '<img src="'+m.photo+'">' : initials;
card.innerHTML = `
<div class="member-row" onclick="openDetail('${m.id}')">
<div class="avatar">${avatarHtml}</div>
<div class="member-info">
<div class="member-name">${escapeHtml(m.name)}</div>
<div class="member-avg">${starsHtml(avg)}</div>
</div>
</div>
`;
listEl.appendChild(card);
});
}
function escapeHtml(str){
const d = document.createElement('div');
d.innerText = str;
return d.innerHTML;
}
// --- Member Modal ---
function openMemberModal(editId){
currentEditId = editId || null;
document.getElementById('memberModalTitle').innerText = editId ? 'Mitglied bearbeiten' : 'Mitglied hinzufügen';
pendingPhotoDataUrl = null;
document.getElementById('memberPhotoInput').value = '';
const preview = document.getElementById('photoPreviewWrap');
preview.innerHTML = '';
if(editId){
const m = data.members.find(x=>x.id===editId);
document.getElementById('memberNameInput').value = m.name;
if(m.photo){
pendingPhotoDataUrl = m.photo;
preview.innerHTML = '<img src="'+m.photo+'" style="width:60px;height:60px;border-radius:50%;object-fit:cover;">';
}
} else {
document.getElementById('memberNameInput').value = '';
}
document.getElementById('memberModalBg').classList.add('active');
}
function closeMemberModal(){
document.getElementById('memberModalBg').classList.remove('active');
}
document.getElementById('memberPhotoInput').addEventListener('change', function(e){
const file = e.target.files[0];
if(!file) return;
const reader = new FileReader();
reader.onload = function(ev){
pendingPhotoDataUrl = ev.target.result;
document.getElementById('photoPreviewWrap').innerHTML =
'<img src="'+ev.target.result+'" style="width:60px;height:60px;border-radius:50%;object-fit:cover;">';
};
reader.readAsDataURL(file);
});
function saveMember(){
const name = document.getElementById('memberNameInput').value.trim();
if(!name){ alert('Bitte einen Namen eingeben.'); return; }
if(currentEditId){
const m = data.members.find(x=>x.id===currentEditId);
m.name = name;
m.photo = pendingPhotoDataUrl;
} else {
data.members.push({
id: uid(),
name: name,
photo: pendingPhotoDataUrl,
ratings: []
});
}
saveData();
closeMemberModal();
renderList();
}
// --- Detail Modal ---
function openDetail(id){
currentDetailId = id;
const m = data.members.find(x=>x.id===id);
document.getElementById('detailName').innerText = m.name;
document.getElementById('detailAvg').innerHTML = 'Durchschnitt: ' + starsHtml(avgOf(m));
renderHistory(m);
document.getElementById('detailModalBg').classList.add('active');
}
function closeDetailModal(){
document.getElementById('detailModalBg').classList.remove('active');
renderList();
}
function renderHistory(m){
const el = document.getElementById('historyList');
if(!m.ratings || m.ratings.length===0){
el.innerHTML = '<div class="empty-hint">Noch keine Bewertungen.</div>';
return;
}
const sorted = [...m.ratings].sort((a,b)=>b.date.localeCompare(a.date));
el.innerHTML = sorted.map(r=>`
<div class="history-entry">
<span>${formatDate(r.date)}</span>
<span><b>${r.value} / 10</b>
<span style="color:var(--danger);cursor

ointer;margin-left:10px;" onclick="deleteRating('${r.id}')">✕</span>
</span>
</div>
`).join('');
}
function formatDate(iso){
const parts = iso.split('-');
return parts[2]+'.'+parts[1]+'.'+parts[0];
}
function editMember(){
closeDetailModal();
openMemberModal(currentDetailId);
}
function deleteMember(){
if(!confirm('Dieses Mitglied wirklich löschen? Alle Bewertungen gehen verloren.')) return;
data.members = data.members.filter(x=>x.id!==currentDetailId);
saveData();
closeDetailModal();
}
function deleteRating(ratingId){
const m = data.members.find(x=>x.id===currentDetailId);
m.ratings = m.ratings.filter(r=>r.id!==ratingId);
saveData();
openDetail(currentDetailId);
}
// --- Rate Modal ---
function openRateModal(){
selectedRating = null;
const sel = document.getElementById('ratingSelect');
sel.innerHTML = '';
for(let i=1;i<=10;i++){
const b = document.createElement('div');
b.className = 'rating-btn';
b.innerText = i;
b.onclick = function(){
selectedRating = i;
document.querySelectorAll('.rating-btn').forEach(x=>x.classList.remove('selected'));
b.classList.add('selected');
};
sel.appendChild(b);
}
const today = new Date().toISOString().slice(0,10);
document.getElementById('rateDateInput').value = today;
document.getElementById('rateModalBg').classList.add('active');
}
function closeRateModal(){
document.getElementById('rateModalBg').classList.remove('active');
}
function saveRating(){
if(!selectedRating){ alert('Bitte eine Bewertung von 1 bis 10 auswählen.'); return; }
const date = document.getElementById('rateDateInput').value;
if(!date){ alert('Bitte ein Datum wählen.'); return; }
const m = data.members.find(x=>x.id===currentDetailId);
m.ratings.push({ id: uid(), value: selectedRating, date: date });
saveData();
closeRateModal();
openDetail(currentDetailId);
}
// --- Export / Import ---
function exportData(){
const blob = new Blob([JSON.stringify(data, null, 2)], {type:'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const stamp = new Date().toISOString().slice(0,10);
a.href = url;
a.download = 'mitglieder_backup_'+stamp+'.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function importData(event){
const file = event.target.files[0];
if(!file) return;
const reader = new FileReader();
reader.onload = function(e){
try{
const imported = JSON.parse(e.target.result);
if(!imported.members) throw new Error('ungültig');
if(!confirm('Import überschreibt alle aktuellen Daten. Fortfahren?')) return;
data = imported;
saveData();
renderList();
alert('Import erfolgreich!');
}catch(err){
alert('Fehler: Die Datei ist kein gültiges Backup.');
}
};
reader.readAsText(file);
event.target.value = '';
}
// --- PWA Manifest (inline, via Blob) ---
(function setupManifest(){
const manifest = {
name: "Mitglieder-Bewertungen",
short_name: "Bewertungen",
start_url: ".",
display: "standalone",
background_color: "#f5f7f8",
theme_color: "#2b6777",
icons: [
{
src: "data:image/svg+xml,%3Csvg xmlns='
http://www.w3.org/2000/svg' viewBox='0 0 192 192'%3E%3Crect width='192' height='192' rx='30' fill='%232b6777'/%3E%3Ctext x='96' y='125' font-size='90' text-anchor='middle' fill='white' font-family='Arial'%3E★%3C/text%3E%3C/svg%3E",
sizes: "192x192",
type: "image/svg+xml"
}
]
};
const blob = new Blob([JSON.stringify(manifest)], {type:'application/json'});
const manifestUrl = URL.createObjectURL(blob);
const link = document.createElement('link');
link.rel = 'manifest';
link.href = manifestUrl;
document.head.appendChild(link);
const themeMeta = document.createElement('meta');
themeMeta.name = 'theme-color';
themeMeta.content = '#2b6777';
document.head.appendChild(themeMeta);
const appleCapable = document.createElement('meta');
appleCapable.name = 'apple-mobile-web-app-capable';
appleCapable.content = 'yes';
document.head.appendChild(appleCapable);
const appleTitle = document.createElement('meta');
appleTitle.name = 'apple-mobile-web-app-title';
appleTitle.content = 'Bewertungen';
document.head.appendChild(appleTitle);
})();
loadData();
renderList();
</script>
</body>
</html>