Demo-Portal mit Beispieldaten - hier wird nichts gespeichert.← Zurück zur Website
Willkommen im Hochzeitsportal
Das ist genau das Portal, das jedes gebuchte Brautpaar bekommt: Musikwünsche eintragen, No-Gos festlegen, Ablauf und besondere Momente planen - alles an einem Ort. Hier seht ihr es mit Beispieldaten von "Sarah & Tom", schaut euch gerne überall um. Es wird nichts gespeichert.
Ihr habt eure Wünsche gemeldet, beats per micha hat Bescheid bekommen. Ihr könnt trotzdem jederzeit weiter Änderungen eintragen, ich sehe jede Anpassung.
Alles Wichtige zu eurem Tag auf einen Blick.
willkommen
SARAH&TOM
- 24.11.2026 -
✦
Euer Tag
Datum
24.11.2026
Uhrzeit
14:00 - 01:00 Uhr
Location
Gut Kaltenbrunn
✦
0 von 5
Eure fünf Schritte
Laden...
Kurze Nachricht an mich
Landet sofort auf meinem Handy
Verlauf
💬 Noch keine Nachrichten - schreibt gerne jederzeit
So läuft es ab
1
Portal abschließen Wenn alles drin ist, klickt unten auf „Abschließen“.
2
Ich schaue alles durch Ich melde mich bei Fragen per E-Mail oder Telefon.
3
Finales Telefonat Ca. eine Woche vor der Hochzeit klären wir die letzten Details.
✓ Danke! Ich habe eure Eingaben - ich melde mich in den nächsten Tagen bei euch.
Damit ich euren Geschmack treffe.
Bewertet mit den Sternen auf jeder Kachel:★ Mag ich★★ Sehr gerne★★★ UnbedingtNochmal auf denselben Stern tippen setzt zurück.Bewegt die Maus über eine Kachel, um kurz reinzuhören.Tippt eine Kachel an, um kurz reinzuhören - nochmal tippen stoppt.
Ein Song pro Zeile, einfach reinkopieren · Nummerierungen werden automatisch entfernt:
Spotify-Playlist als Must-Have übernehmen
Link einfügen - ich übernehme die einzelnen Songs in eure Liste oben, zusammen mit den von Hand eingegebenen maximal 30 insgesamt.
Spotify & Playlist-Links0 / 5 Links
Das ist Inspiration für meinen Mix - keine Ablaufliste, ich spiele nicht einfach die Playlist durch. Songs die auf jeden Fall laufen sollen, gehören einzeln oben unter „Must-Have-Songs" (max. 30 Songs).
Maximal 99 Songs pro Playlist - mehr kann ich technisch nicht importieren.
Songs für besondere Momente
💍TrauungSongs für Einzug, Auszug und Zeremonie.›
Die Braut betritt den Ort der Trauung.
Direkt nach dem Ja-Wort, als Ehepaar hinaus.
z.B. beim Ringtausch.
z.B. Ringtausch oder Fürbittengebet.
Weiterer Song während der Zeremonie.
💃Hochzeitstanz & FamilieDie emotionalsten Tänze des Abends.›
Euer erster Tanz als Ehepaar.
Mein Tipp zum Hochzeitstanz
Der erste Tanz setzt die emotionale Stimmung für den ganzen Abend. Ich empfehle einen Song, der euch beiden wirklich etwas bedeutet · nicht zwingend ein klassischer Walzer. Nach ca. 1 Minute hole ich auf Wunsch alle anderen Paare auf die Tanzfläche.
Wichtig: Gebt mir rechtzeitig Bescheid, ob ihr eine Choreographie plant · dann halte ich die volle Länge.
Optional - z.B. wenn Eltern oder weiteres Brautpaar mittanzt.
Ein besonderer Tanz der Braut mit ihrem Vater.
Ein besonderer Tanz des Bräutigams mit seiner Mutter.
🎂HöhepunkteSongs für besondere Momente der Feier.›
Ihr betretet als Paar den Saal zur Feier.
Läuft beim gemeinsamen Anschneiden.
Läuft beim Brautstraußwerfen.
🔥AbschiedDer finale Song eures Abends.›
Der Song ganz am Ende - ein krönender Abschluss.
Diese Songs bitte nicht0 / 30
Interpret + Titel - ich spiele sie garantiert nicht, egal wer danach fragt. Warum höchstens 30?
30 Songs sind eingetragen - mehr kann ich am Abend nicht zuverlässig im Kopf behalten. Warum höchstens 30? Wenn euch noch etwas einfällt: oben einen Eintrag entfernen, dann geht das Feld wieder auf.
Wenn ihr eine Playlist mit Songs habt, die nicht laufen sollen: Link einfügen - ich übernehme die einzelnen Songs in die Liste oben, zusammen mit den von Hand eingegebenen höchstens 30 insgesamt.
Ganze Richtungen ausschließen
Zum Beispiel: kein Hardrock, kein Schlager, kein Techno. Das zählt nicht gegen die 30 Songs - eine Richtung auszuschließen ist etwas anderes als einzelne Titel.
Häkchen setzen = dieser Punkt findet statt. Uhrzeit = grobe Orientierung für mich. Notizfeld = Hinweise zur Koordination (z.B. “Ansage erwünscht”, “Musik läuft schon”). Songs für Einzug, Hochzeitstanz & Co. tragt ihr direkt hier beim jeweiligen Punkt ein.
Eigene Programmpunkte
Habt ihr besondere Ideen die nicht in die Zeremonie passen? Tragt sie hier ein.
Wird mit eurer Buchung abgeglichen · bitte kontrollieren und ggf. korrigieren.
Adresse der Location
Logistik & Technik
Damit ich den Aufbau gut planen kann - je mehr ich vorher weiß, desto entspannter wird der Hochzeitstag. Was ihr noch nicht wisst, lasst ihr frei oder wählt „Weiß ich noch nicht“.
⚠ Ohne Sonnenschutz ist DJ-Equipment bei Hitze und Direktsonne nur eingeschränkt einsetzbar · bitte mit der Location klären.
Wichtig für die Beschallungsplanung
Zeiten & Auflagen
Damit ich punktgenau aufbauen kann und keinen Ärger mit der Location gibt.
Ab wann darf ich in die Location zum Aufbauen?
Erscheint im Ablauf automatisch als „Offiz. Ende der Feier“.
Mein Tipp zur Lautstärke
Beim Dinner: Hintergrundmusik in angenehmer Gesprächslautstärke. Beim Abendprogramm: ansteigende Energie. Für die Tanzfläche: Konzertlautstärke, wenn der Raum voll ist · Schall braucht Körper zum Absorbieren. Gibt es eine Begrenzung ab einer Uhrzeit oder in dB, wählt sie oben aus und schreibt Details in die Besonderheiten.
Damit ich euch nicht erst am Hochzeitstag kennenlerne.
Felder mit Pflicht brauche ich für die Buchungsbestätigung. Alles andere ist optional.
Das Brautpaar
Gäste
Tanzstimmung & Gäste
Beeinflusst wann und wie stark ich die Tanzfläche anheize.
Bestimmt den Anteil englischsprachiger Musik.
Musikalische Wurzeln OPTIONAL
Spielt bei einem von euch - oder eurer Familie - eine andere Musikkultur eine Rolle? Das kann ein anderes Land sein (z.B. weil Verwandte aus der Türkei oder Italien kommen) oder auch eine deutsche Region mit eigener Tradition (z.B. bayerische Blasmusik oder Kölner Karneval). Dann kann ich gezielt passende Songs vorbereiten. Nur ausfüllen, wenn's für euch relevant ist.
Ansprechpartner & Trauzeugen OPTIONAL
Wer ist mein Ansprechpartner am Hochzeitstag? Trauzeugen sind ideal - ich rufe dort an wenn ich jemanden kurzfristig vor Ort brauche, ohne euch zu stören.
Damit ich bei Bedarf die richtigen Personen direkt erreichen kann - ohne euch am großen Tag zu stören.
Euer Foto OPTIONAL
Pärchenfoto hochladen
Ein gemeinsames Foto von euch beiden. Damit ich eure Gesichter vor eurem Tag schon im Kopf habe und euch nicht wie Fremde begrüße.
Die Sitzordnung hat direkten Einfluss auf die Musik: Sitzen die älteren Gäste direkt neben der Tanzfläche, wähle ich die Lautstärke und das Tempo entsprechend behutsamer. Sitzen sie eher am Rand oder im Nebenraum, kann die Party früher Fahrt aufnehmen.
Mein Tipp: Ältere Gäste möglichst nicht direkt neben den Boxen platzieren · das schont Nerven und Beziehungen. Für die Tanzfläche gilt: je gemischter die Altersgruppen, desto abwechslungsreicher und breiter spiele ich den Musikstil.
Der Hochzeitstanz2 Min
Der erste Tanz setzt die emotionale Stimmung für den ganzen Abend. Ich empfehle einen Song, der euch beiden wirklich etwas bedeutet · nicht zwingend ein klassischer Walzer. Nach ca. 1 Minute hole ich auf Wunsch alle anderen Paare auf die Tanzfläche.
Wichtig: Gebt mir rechtzeitig Bescheid ob ihr eine Choreographie plant · dann halte ich die volle Länge.
Moderation & Ansagen
Mein Ansatz: Die Musik spricht für sich. Ich übernehme die wichtigen Ansagen · Einzug, Eröffnung der Tanzfläche, Tortenanschnitt · aber ich überzeuge lieber mit dem richtigen Song zum richtigen Moment als mit unnötigem Gerede zwischen den Tracks.
Kurz gesagt: Weniger ist mehr. Wenn ihr euch mehr Führung durch den Abend wünscht, sagt es mir · aber meine Bühne ist die Tanzfläche, nicht das Mikrofon.
Songwünsche von Gästen
Auf meinen Hochzeiten gibt es keine Musikwunschkarten. Nicht weil mir eure Gäste egal sind · sondern weil ein Zettel mit „Macarena" um 23 Uhr nichts über die Stimmung auf der Tanzfläche sagt. Ich lese den Raum. Das ist mein Job.
Wünsche direkt an mich: kein Problem, ich höre zu. Ob und wann ich einen Wunsch spiele, hängt von der Stimmung ab · nicht von der Reihenfolge auf dem Zettel. Songs die ihr definitiv nicht hören wollt, tragt bitte im Songs-Tab als No-Go-Songs ein.
Meine Regel: 10 Leute tanzen und jemand wünscht eine Ballade · ich warte auf den richtigen Moment. Die Tanzfläche bleibt mein Kompass.
Lautstärke1 Min
Beim Dinner: Hintergrundmusik, angenehme Gesprächslautstärke. Beim Abendprogramm: ansteigende Energie. Für die Tanzfläche: Konzertlautstärke wenn der Raum voll ist · Schall braucht Körper zum Absorbieren.
Lautstärkebegrenzung? Tragt sie bitte unter ein · ich berücksichtige das von Anfang an.
Wann startet die Tanzfläche?
Der typische Ablauf: Sektempfang mit Loungemusik → Dinner mit Hintergrundmusik → Abendprogramm (Spiele, Reden, Torte) → Hochzeitstanz → Tanzfläche öffnet. Das Timing hängt stark vom Ablauf ab.
Meine Erfahrung: Die ersten 30 Minuten auf der Tanzfläche sind oft verhalten · erst wenn ein"Anheizer-Song" kommt, explodiert die Stimmung. Den kenne ich nach dem Abend.
Spiele & Programmpunkte · Timing ist alles
Meine dringende Empfehlung: Alle Spiele und Showeinlagen vor dem ersten Tanz * also bevor die Tanzfläche eröffnet wird. Der Grund ist simpel: Jedes Spiel nach dem Öffnen der Tanzfläche räumt sie wieder leer. Wer einmal seinen Groove verloren hat, kommt oft nicht zurück.
Die goldene Regel: Erst feiern, dann tanzen · und dann durchgehend tanzen. Unterbrechungen kosten Energie die ich vorher hart aufgebaut habe. Plant die Spiele im Ablauf-Tab entsprechend ein, ich halte mich daran.
Was ich sonst noch brauche
Alles optional · je mehr, desto besser vorbereitet bin ich
Raumplan oder Skizze der Location (wo stehe ich, wo tanzt ihr?)
Ablaufplan · auch wenn er sich noch ändert
Info zur Steckdosensituation in DJ-Nähe
Parkmöglichkeit für Aufbau (ich komme ca. 2h vorher)
Euer Vertrag und alles zum Nachlesen.
🕒
Eure Unterlagen sind in Vorbereitung
Eure Auftragsbestätigung erscheint hier, sobald ich sie freischalte. Ihr bekommt automatisch eine E-Mail.
Bis dahin könnt ihr schon Musik & Details eintragen
Lade hier alles hoch, was mir bei der Vorbereitung helfen könnte · Grundriss, Sitzplan, Ablaufplan, Inspirationen. Max. 10 Dateien · max. 20 MB gesamt.
Druckt das PDF gerne aus oder hebt es als Erinnerung auf.
ALLES FERTIG!
Sobald ihr bestätigt, bekomme ich sofort eine Benachrichtigung und melde mich bei euch für die letzten Details. Das Portal bleibt offen - Änderungen sind weiterhin möglich.
GEHEIME NACHRICHT
Nur ich sehe das. Dein Partner oder deine Partnerin bekommt diese Nachricht nie zu sehen.
Von
Für
Deine geheime Nachricht an mich
WARUM NUR 30 SONGS?
30 Must-Have-Songs sind kein Limit · sie sind das Geheimnis.
Wenn ihr mir 30 Songs nennt, die ihr wirklich liebt, bekomme ich ein klares, ehrliches Bild eures Musikgeschmacks. Ich erkenne Muster: welche Genres euch bewegen, welcher Energie ihr mögt, was euch zum Tanzen bringt.
Aus diesen 30 Titeln kann ich dann hunderte weitere Songs ableiten, die perfekt dazu passen · als Fülltitel, Übergänge und Überraschungen, die sich für euch genau richtig anfühlen werden.
Bei mehr als 30 Songs verliert die Liste ihren Fokus. Alles wird gleich wichtig · und damit verliere ich den roten Faden, was euch wirklich am Herzen liegt. Weniger ist hier definitiv mehr.
Diese Songs konnten nicht übernommen werden:
🚫 WARUM HÖCHSTENS 30?
Zwei Gründe, beide praktisch: Ich muss sie am Abend im Kopf haben, während ich auf die Tanzfläche reagiere - eine lange Verbotsliste kann ich in dem Moment nicht zuverlässig abrufen.
Und je mehr ausgeschlossen ist, desto weniger Spielraum bleibt mir, spontan auf eure Gäste einzugehen. Genau dieser Spielraum ist es, der einen Abend trägt.
Dazu kommt etwas, das die Sache entspannt: Ich spiele im Mix, nicht in voller Länge. Ein Titel läuft je nach Song etwa eine bis zwei Minuten, im Schnitt anderthalb. Sollte also doch einmal einer dabei sein, der nicht ganz euer Fall ist, ist er schnell wieder vorbei. Die Liste muss deshalb gar nicht jeden Grenzfall abdecken - sie ist für das gedacht, was wirklich gar nicht geht.
Ganze Richtungen zählen nicht mit. Wenn ihr etwas grundsätzlich nicht wollt - kein Schlager, kein Techno - tragt das weiter unten unter „Ganze Richtungen ausschließen“ ein. Das ist etwas anderes als einzelne Titel und kostet euch keinen Platz in dieser Liste.
Diese Songs konnten nicht übernommen werden:
0&&diff<=7){
txt+=(txt?' · ':'')+'noch '+diff+' Tag'+(diff===1?'':'e')+'';
} else if(diff>0&&diff<=14){
txt+=(txt?' · ':'')+'noch '+diff+' Tage';
} else if(diff>0&&diff<=365){
txt+=(txt?' · ':'')+'noch '+diff+' Tage';
} else if(diff===0){
txt+=(txt?' · ':'')+'Heute ist euer großer Tag!';
}
}
if(txt){cd.innerHTML=txt;setTimeout(function(){cd.classList.add('show');},600);}
}
}
document.addEventListener('DOMContentLoaded',function(){
var mi=document.getElementById('muss-input');
if(!mi) return;
mi.addEventListener('paste',function(e){
var txt=(e.clipboardData||window.clipboardData).getData('text');
if(!txt||txt.indexOf('\n')<0) return; // einzeilig → normal
e.preventDefault();
mi.value=txt;
setTimeout(addMuss,10);
});
});
function delMuss(i) {
clearTimeout(_asTimer); // Ausstehenden Autosave stoppen
mussSongs.splice(i,1);
renderMussList();
savePortalNow('Song entfernt');
}
// UX-Audit Etappe B: eingeklappte Import-Bereiche (Bulk, Spotify, Playlist-Links)
function _importToggle(id, link){
var w = document.getElementById(id); if(!w) return;
var open = (w.style.display === 'none');
w.style.display = open ? 'block' : 'none';
if(link){ link.setAttribute('aria-expanded', open ? 'true' : 'false'); link.classList.toggle('open', open); }
}
function renderPlaylistList() {
var max=5, cur=playlists.length;
var cEl=document.getElementById('playlist-counter');
if(cEl){ cEl.textContent=cur+' / '+max; cEl.style.color=cur>=max?'var(--warning)':cur>=max-1?'var(--gold)':'var(--muted)'; }
// Etappe B: gespeicherte Playlist-Links duerfen nicht hinter dem Textlink verschwinden
if(cur>0){ var _iw=document.getElementById('muss-import-wrap'); if(_iw&&_iw.style.display==='none') _importToggle('muss-import-wrap', document.getElementById('muss-import-toggle')); }
var addRow=document.getElementById('playlist-add-row');
if(addRow){ addRow.style.display=cur>=max?'none':'flex'; }
if(playlists.length === 0){
document.getElementById('playlist-list').innerHTML = '
'
+'
\u{1F517}
'
+'
Habt ihr eine Spotify- oder YouTube-Playlist?
'
+'
Verlinkt sie hier — ich h\u00f6re vor der Hochzeit rein und kann eure Lieblings-Stimmung viel besser treffen.
'
+'
';
return;
}
document.getElementById('playlist-list').innerHTML = playlists.map(function(p,i){
var link = p.link || '';
var label = p.label && p.label !== link ? p.label : '';
var cardId = 'pl-card-'+i;
var isSpotify = link.includes('spotify.com');
var isApple = link.includes('music.apple.com');
var isSC = link.includes('soundcloud.com');
var ytId = (link.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([A-Za-z0-9_-]{11})/) || [])[1];
var accentColor = isSpotify?'#1DB954': isApple?'#FC3C44': isSC?'#FF5500': ytId?'#FF0000':'var(--gold)';
var serviceName = isSpotify?'Spotify': isApple?'Apple Music': isSC?'SoundCloud': ytId?'YouTube': 'Link';
var openLabel = isSpotify?'In Spotify öffnen': isApple?'In Apple Music öffnen': isSC?'Auf SoundCloud öffnen': ytId?'In YouTube öffnen':'Öffnen';
var displayLabel = label || serviceName;
var initCover = ytId
? ''
: '
';
}).join('');
}
// v17.290: Playlists ab 100 Songs werden abgelehnt, erlaubt sind hoechstens 99 — an
// einem Abend laufen etwa 60 bis 90 Titel, laengere Listen sind nicht spielbar und
// nehmen dem DJ den Spielraum, spontan auf die Gaeste zu reagieren. Geprueft wird
// serverseitig (action=playlist-count), weil der Link selbst die Anzahl nicht verraet.
var PLAYLIST_MAX_SONGS = 99;
var SPOTIFY_PL_RE = /(?:open\.spotify\.com\/(?:intl-[a-z]{2}\/)?playlist\/|spotify:playlist:)[A-Za-z0-9]{22}/;
function addPlaylist() {
if(playlists.length>=5){ return; }
var l=document.getElementById('playlist-input').value.trim(); if(!l) return;
var lbl=document.getElementById('playlist-label').value.trim()||l;
// Nur Spotify laesst sich zaehlen — YouTube/Apple/SoundCloud unveraendert durchlassen
if(!SPOTIFY_PL_RE.test(l)){ _plCommit(l,lbl); return; }
var btn=document.querySelector('#playlist-add-row .add-btn-small');
var btnTxt=btn?btn.textContent:'';
if(btn){ btn.disabled=true; btn.textContent='Prüfe…'; }
fetch('?action=playlist-count&url='+encodeURIComponent(l)+'&token='+encodeURIComponent(TOKEN))
.then(function(r){ return r.json(); })
.catch(function(){ return {ok:false,reason:'network'}; })
.then(function(d){
if(btn){ btn.disabled=false; btn.textContent=btnTxt; }
// d.capped = Spotify hat bei 100 Titeln abgeschnitten, es sind also 100 ODER MEHR.
// Genau 100 wird damit mit abgelehnt — gewollt, das Ziel sind Listen UNTER 100.
if(d && d.ok && (d.capped || d.total > PLAYLIST_MAX_SONGS)){
showPlaylistZuGross(d.total, d.name, d.capped);
return; // Link wird NICHT uebernommen
}
// Nicht zaehlbar (privat, API-Problem, offline): bewusst durchlassen statt blockieren —
// ein technischer Aussetzer darf das Brautpaar nicht am Speichern hindern.
if(d && !d.ok && d.reason==='not_public'){
showToast('Playlist ist privat — bitte in Spotify auf "öffentlich" stellen');
}
_plCommit(l,lbl);
});
}
function _plCommit(link,label){
playlists.push({link:link,label:label});
document.getElementById('playlist-input').value='';
document.getElementById('playlist-label').value='';
renderPlaylistList();
savePortalNow('Link gespeichert');
}
function showPlaylistZuGross(total,name,capped){
// Bei capped kennen wir die genaue Zahl nicht (Spotify liefert max. 100 aus) —
// dann bewusst "100 oder mehr" schreiben statt eine falsche Zahl zu behaupten.
var menge = capped ? '100 oder mehr Songs' : total+' Songs';
var el=document.getElementById('modal-playlist-zu-gross');
if(!el){ showToast('Playlist hat '+menge+' — bitte auf höchstens '+PLAYLIST_MAX_SONGS+' kürzen'); return; }
var info=document.getElementById('modal-pl-info');
if(info){
info.innerHTML = (name?'Eure Playlist '+name.replace(/ enthält ':'Diese Playlist enthält ')
+ ''+menge+'.';
}
el.style.display='flex';
}
function delPL(i) {
clearTimeout(_asTimer); // Ausstehenden Autosave stoppen
playlists.splice(i,1);
renderPlaylistList();
savePortalNow('Link entfernt');
}
function collectSongs() {
var momente={};
SMKEYS.forEach(function(id){ var el=document.getElementById('song-'+id); if(el) momente[id]=el.value.trim(); });
// tabu = nur der Freitext (s.nogo[] bleibt separat — CRM rendert beides)
var tabuEl = document.getElementById('songs-tabu');
var tabuKombiniert = tabuEl ? tabuEl.value.trim() : '';
// mussSongs: artist/titel trennen für CRM-Kompatibilität
var mussNormiert = mussSongs.map(function(s){
// Kompatibilität: ältere Einträge können {titel} oder {title} haben
var rawTitel = s.titel || s.title || s.name || '';
var parsed = _mussParseTitle(rawTitel);
return {
titel: parsed.title || rawTitel || '—',
artist: parsed.artist || s.artist || '',
wann: s.wann || s.moment || '',
cover: s.cover || '',
preview: s.preview || ''
};
}).filter(function(s){ return s.titel && s.titel !== '—' || s.artist; });
// Nur Eintraege mitgeben, deren Text sich seit dem Nachschlagen nicht mehr
// geaendert hat - sonst reicht das Portal ein Cover weiter, das zu einem
// anderen Song gehoert. Das ist schlimmer als gar keins, weil es im Briefing
// glaubwuerdig falsch aussieht.
var metaGeprueft = {};
SMKEYS.forEach(function(id){
var m = momenteMeta[id];
if(m && momente[id] && m.roh === momente[id]) metaGeprueft[id] = m;
});
return {muss:mussNormiert, playlists:playlists, nogo:nogoSongs, nogoGenres:nogoGenres, tabu:tabuKombiniert, momente:momente, momenteMeta:metaGeprueft};
}
// ── Location ──────────────────────────────────────────────────────────
// ── Textarea: Höhe automatisch an Inhalt anpassen ──────────────────
function _taAutoResize(el){
if(!el) return;
el.style.height = 'auto';
el.style.height = el.scrollHeight + 'px';
if(!el._taResizeBound){
el._taResizeBound = true;
el.addEventListener('input', function(){ _taAutoResize(this); });
}
}
function initLocation() {
var l = P.location || {};
// Locationname aus Buchung vorbelegen wenn noch kein Portalwert gespeichert
var _bookingName = document.getElementById('loc-name')?.getAttribute('value') || '';
if(!l.name && _bookingName) {
l.name = _bookingName;
P.location = l; // in P übernehmen damit Progress-Check greift
}
['name','strasse','plz','stadt','land','indoor','djpos','parkplatz','entfernung','tanzflaeche','aufzug','besonderheiten','aufbauzeit','sperrstunde','lautstaerke'].forEach(function(id){
var el=document.getElementById('loc-'+id); if(el&&l[id]!=null) el.value=l[id];
});
if(l.indoor) locIndoorHint(l.indoor);
// Adresse in obere Box spiegeln wenn schon vorhanden
var _sub=[l.strasse||'',[l.plz||'',l.stadt||''].filter(Boolean).join(' ')].filter(Boolean).join(', ');
if(_sub){ var sb=document.getElementById('loc-booking-sub'); if(sb&&!sb.textContent.trim()) sb.textContent=_sub; }
if(l.kontakte){
['foto','video','mod','planner'].forEach(function(k){
var kd=l.kontakte[k]||{};
var n=document.getElementById('kont-'+k+'-name');
var t=document.getElementById('kont-'+k+'-tel');
if(n&&kd.name) n.value=kd.name;
if(t&&kd.tel) t.value=kd.tel;
});
}
if(l.aktualisiert){ var badge=document.getElementById('loc-update-badge'); if(badge) badge.style.display='inline'; }
_taAutoResize(document.getElementById('loc-besonderheiten'));
_taAutoResize(document.getElementById('per-weiteres'));
}
function collectLocation() {
var r={};
['name','strasse','plz','stadt','land','indoor','djpos','parkplatz','entfernung','tanzflaeche','aufzug','besonderheiten','aufbauzeit','sperrstunde','lautstaerke'].forEach(function(id){
var el=document.getElementById('loc-'+id); if(el) r[id]=el.value;
});
['foto','video','mod','planner'].forEach(function(k){
var n=document.getElementById('kont-'+k+'-name');
var t=document.getElementById('kont-'+k+'-tel');
if((n&&n.value)||(t&&t.value)){
if(!r.kontakte) r.kontakte={};
r.kontakte[k]={name:n?n.value:'',tel:t?t.value:''};
}
});
if(window._locPortalAktualisiert) r.aktualisiert=true;
if(P.location && P.location.bestaetigt) r.bestaetigt=true; // Etappe A/C: 'Stimmt so' ueberlebt das Neusammeln
return r;
}
// ── Persönliches ──────────────────────────────────────────────────────
// ── Portal-Pärchenfoto (ein gemeinsames Bild) ────────────────────────
var _fotoTarget = null;
function triggerFotoUpload(who){
_fotoTarget = who;
var inp = document.getElementById('foto-file-input');
if(inp){ inp.value=''; inp.click(); }
}
function onFotoFileChosen(inp){
var file = inp.files && inp.files[0];
if(!file || !_fotoTarget) return;
if(!file.type.startsWith('image/')){ showFotoStatus(_fotoTarget,'⚠ Nur Bilder erlaubt','var(--warning)'); return; }
if(_fotoTarget === 'paerchen'){
openPortalCropTool(file); // Erst zuschneiden, dann hochladen
} else {
showFotoStatus(_fotoTarget,'⏳ Wird komprimiert…','var(--muted)');
compressFoto(file, function(dataUrl){ uploadFoto(_fotoTarget, dataUrl); });
}
}
// ── Portal-Crop-Tool: Pärchenfoto auf 16:10 zuschneiden ─────────────────
function openPortalCropTool(file){
var reader = new FileReader();
reader.onload = function(e){
var img = new Image();
img.onload = function(){ _pcOpenModal(img); };
img.onerror = function(){ showFotoStatus('paerchen','⚠ Bild konnte nicht geladen werden','var(--warning)'); };
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
function _pcOpenModal(img){
var existing = document.getElementById('_portal_crop_modal');
if(existing) existing.remove();
var modal = document.createElement('div');
modal.id = '_portal_crop_modal';
modal.style.cssText = 'position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.92);display:flex;align-items:center;justify-content:center;padding:12px';
modal.innerHTML =
'
'
+'
'
+'✂ Pärchenfoto zuschneiden'
+''
+'
'
+'
'
+'
Verschiebe den goldenen Rahmen auf den gewünschten Ausschnitt. Ecke rechts unten zum Skalieren ziehen.
'
+'
'
+''
+'
'
+'
'
+'Vorschau:'
+''
+'
'
+'
'
+''
+''
+'
'
+'
'
+'
';
document.body.appendChild(modal);
var canvas = document.getElementById('_pc_canvas');
var ASPECT = 16/10;
var maxW = Math.min(window.innerWidth - 64, 760, img.width);
var scale = maxW / img.width;
canvas.width = Math.round(img.width * scale);
canvas.height = Math.round(img.height * scale);
// Crop-Rahmen: so groß wie möglich bei 16:10, zentriert
var bw = Math.round(Math.min(canvas.width * 0.92, canvas.height * ASPECT * 0.92));
var bh = Math.round(bw / ASPECT);
if(bh > canvas.height * 0.92){ bh = Math.round(canvas.height * 0.92); bw = Math.round(bh * ASPECT); }
var box = {
x: Math.round((canvas.width - bw) / 2),
y: Math.round((canvas.height - bh) / 2),
w: bw, h: bh
};
window._pc_img = img;
window._pc_scale = scale;
window._pc_box = box;
_pcRender(canvas, img, box);
_pcBindEvents(canvas, box);
}
function _pcRender(canvas, img, box){
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// Vignette mit Rechteck-Loch (Offscreen → symmetrisch)
var mask = document.createElement('canvas');
mask.width = canvas.width; mask.height = canvas.height;
var mctx = mask.getContext('2d');
mctx.fillStyle = 'rgba(0,0,0,0.6)';
mctx.fillRect(0, 0, mask.width, mask.height);
mctx.globalCompositeOperation = 'destination-out';
mctx.fillRect(box.x, box.y, box.w, box.h);
ctx.drawImage(mask, 0, 0);
// Rahmen
ctx.strokeStyle = '#C8A84B';
ctx.lineWidth = 2;
ctx.strokeRect(box.x + 1, box.y + 1, box.w - 2, box.h - 2);
// Drittel-Linien (Goldlinie-Gitter als Orientierungshilfe)
ctx.strokeStyle = 'rgba(200,168,75,0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(box.x + box.w/3, box.y); ctx.lineTo(box.x + box.w/3, box.y + box.h);
ctx.moveTo(box.x + box.w*2/3, box.y); ctx.lineTo(box.x + box.w*2/3, box.y + box.h);
ctx.moveTo(box.x, box.y + box.h/3); ctx.lineTo(box.x + box.w, box.y + box.h/3);
ctx.moveTo(box.x, box.y + box.h*2/3); ctx.lineTo(box.x + box.w, box.y + box.h*2/3);
ctx.stroke();
// Ecken-Handles
ctx.fillStyle = '#C8A84B';
var hs = 10;
[[box.x, box.y],[box.x+box.w-hs, box.y],[box.x, box.y+box.h-hs],[box.x+box.w-hs, box.y+box.h-hs]].forEach(function(pt){
ctx.fillRect(pt[0], pt[1], hs, hs);
});
// Preview
var prev = document.getElementById('_pc_preview');
if(prev && window._pc_scale){
var pc = prev.getContext('2d');
pc.clearRect(0, 0, 192, 120);
var sx = Math.round(box.x / window._pc_scale);
var sy = Math.round(box.y / window._pc_scale);
var sw = Math.round(box.w / window._pc_scale);
var sh = Math.round(box.h / window._pc_scale);
pc.drawImage(img, sx, sy, sw, sh, 0, 0, 192, 120);
}
}
function _pcBindEvents(canvas, box){
var ASPECT = 16/10;
var mode = null;
var startX, startY, startBox;
var getPos = function(e){
var r = canvas.getBoundingClientRect();
var touch = (e.touches && e.touches[0]) ? e.touches[0] : e;
return {
x: (touch.clientX - r.left) * (canvas.width / r.width),
y: (touch.clientY - r.top) * (canvas.height / r.height)
};
};
var hitResize = function(px, py){
return px >= box.x + box.w - 20 && px <= box.x + box.w + 4
&& py >= box.y + box.h - 20 && py <= box.y + box.h + 4;
};
var hitDrag = function(px, py){
return px > box.x + 4 && px < box.x + box.w - 4
&& py > box.y + 4 && py < box.y + box.h - 4;
};
var onDown = function(e){
e.preventDefault();
var pos = getPos(e);
startX = pos.x; startY = pos.y;
startBox = {x:box.x, y:box.y, w:box.w, h:box.h};
if(hitResize(pos.x, pos.y)) mode = 'resize';
else if(hitDrag(pos.x, pos.y)) mode = 'drag';
};
var onMove = function(e){
e.preventDefault();
var pos = getPos(e);
if(!mode){
canvas.style.cursor = hitResize(pos.x,pos.y) ? 'se-resize' : (hitDrag(pos.x,pos.y) ? 'grab' : 'default');
return;
}
var dx = pos.x - startX, dy = pos.y - startY;
if(mode === 'drag'){
box.x = Math.max(0, Math.min(canvas.width - box.w, startBox.x + dx));
box.y = Math.max(0, Math.min(canvas.height - box.h, startBox.y + dy));
} else {
var newW = Math.max(60, Math.min(canvas.width - box.x, startBox.w + dx));
var newH = Math.round(newW / ASPECT);
if(box.y + newH > canvas.height){ newH = canvas.height - box.y; newW = Math.round(newH * ASPECT); }
box.w = newW; box.h = newH;
}
_pcRender(canvas, window._pc_img, box);
};
var onUp = function(){ mode = null; canvas.style.cursor = 'default'; };
canvas.addEventListener('mousedown', onDown);
canvas.addEventListener('mousemove', onMove);
canvas.addEventListener('mouseup', onUp);
canvas.addEventListener('mouseleave', onUp);
canvas.addEventListener('touchstart', onDown, {passive:false});
canvas.addEventListener('touchmove', onMove, {passive:false});
canvas.addEventListener('touchend', onUp);
}
async function _pcSave(){
var btn = document.getElementById('_pc_save_btn');
if(btn){ btn.disabled=true; btn.textContent='⏳ Verarbeitung…'; }
var box = window._pc_box;
var img = window._pc_img;
var scale = window._pc_scale;
var sx = Math.round(box.x / scale);
var sy = Math.round(box.y / scale);
var sw = Math.round(box.w / scale);
var sh = Math.round(box.h / scale);
// Zielauflösung 16:10 → max 1600px Breite
var outW = Math.min(1600, sw);
var outH = Math.round(outW / (16/10));
var offscreen = document.createElement('canvas');
offscreen.width = outW; offscreen.height = outH;
offscreen.getContext('2d').drawImage(img, sx, sy, sw, sh, 0, 0, outW, outH);
// Komprimierung auf max 250 KB
var q = 0.88, dataUrl;
do {
dataUrl = offscreen.toDataURL('image/jpeg', q);
q = Math.round((q - 0.08) * 100) / 100;
} while(dataUrl.length * 0.75 > 250 * 1024 && q > 0.1);
var kb = Math.round(dataUrl.length * 0.75 / 1024);
showFotoStatus('paerchen','⏳ Hochladen ('+kb+' KB)…','var(--muted)');
document.getElementById('_portal_crop_modal').remove();
await uploadFoto('paerchen', dataUrl);
}
function compressFoto(file, cb){
var reader = new FileReader();
reader.onload = function(e){
var img = new Image();
img.onload = function(){
var w=img.width, h=img.height;
var isPaerchen = (_fotoTarget === 'paerchen');
// ── Mindestgröße ───────────────────────────────────────────────
if(w < 200 || h < 200){
showFotoStatus(_fotoTarget,'⚠ Bild zu klein (mind. 200×200px)','var(--warning)');
return;
}
// ── Verhältnis-Check ──────────────────────────────────────────
// Pärchenbild: deutlich liberaler (kann Querformat sein), nur Extreme abblocken
var ratio = w / h;
if(isPaerchen){
if(ratio > 3.5){
showFotoStatus(_fotoTarget,'⚠ Bild zu breit (Panorama) * bitte normales Foto wählen','var(--warning)');
return;
}
if(ratio < 0.4){
showFotoStatus(_fotoTarget,'⚠ Bild zu schmal/hoch * bitte normales Foto wählen','var(--warning)');
return;
}
} else {
// Legacy: Portrait-Check für Einzelfoto
if(ratio > 2.2){
showFotoStatus(_fotoTarget,'⚠ Bitte ein Portraitfoto wählen','var(--warning)');
return;
}
if(h < w * 0.5){
showFotoStatus(_fotoTarget,'⚠ Bitte ein Hochformat- oder quadratisches Bild wählen','var(--warning)');
return;
}
}
// Dateigröße * file.size in Bytes
if(file.size < 5000){
showFotoStatus(_fotoTarget,'⚠ Bilddatei zu klein * bitte ein qualitativ besseres Foto wählen','var(--warning)');
return;
}
// ── Komprimierung ───────────────────────────────────────────────
// Pärchenbild größer (für späteres Cropping im CRM), Einzelfoto kompakter
var MAX = isPaerchen ? 1200 : 500;
var MAX_KB = isPaerchen ? 450 : 200;
var r=Math.min(MAX/w,MAX/h,1); w=Math.round(w*r); h=Math.round(h*r);
var cv=document.createElement('canvas'); cv.width=w; cv.height=h;
cv.getContext('2d').drawImage(img,0,0,w,h);
var q=0.88, du;
do { du=cv.toDataURL('image/jpeg',q); q=Math.round((q-0.08)*100)/100; }
while(du.length*0.75 > MAX_KB*1024 && q>0.1);
var kb=Math.round(du.length*0.75/1024);
showFotoStatus(_fotoTarget,'⏳ Hochladen ('+kb+' KB)…','var(--muted)');
cb(du);
};
img.src=e.target.result;
};
reader.readAsDataURL(file);
}
async function uploadFoto(who, dataUrl){
try{
var b64=dataUrl.split(',')[1];
var res=await fetch(window.location.href.split('?')[0],{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({action:'upload-foto',aid:AID,token:TOKEN,who:who,foto_base64:b64,
kunde_id:OD_KUNDE_ID,kunde_name:OD_KUNDE_NAME})});
var data=await res.json().catch(function(){return {ok:false,error:'Ungültige Antwort'};});
if(data.ok){
var img=document.getElementById('foto-'+who+'-img');
var ph =document.getElementById('foto-'+who+'-placeholder');
var del=document.getElementById('foto-'+who+'-del');
var wrap=document.getElementById('foto-'+who+'-wrap');
if(img){img.src=dataUrl;img.style.display='block';}
if(ph) ph.style.display='none';
if(del)del.style.display='inline';
if(wrap)wrap.classList.add('has-photo');
if(!P.persoenlich)P.persoenlich={};
P.persoenlich['foto_'+who]=data.url;
// Pärchenbild: Base64 einmalig für CRM-Cropping mitpersistieren, danach aus RAM entfernen
if(who==='paerchen') P.persoenlich.paerchenFotoBase64=dataUrl;
updateWelcomeBannerFoto(who, dataUrl);
savePortalNow('Foto gespeichert').then(function(){
// Base64 nach erfolgreichem Save aus RAM entfernen — URL (foto_paerchen) bleibt erhalten
if(who==='paerchen' && P.persoenlich) delete P.persoenlich.paerchenFotoBase64;
});
showFotoStatus(who,'✓ Gespeichert','rgba(39,174,96,0.85)');
setTimeout(function(){showFotoStatus(who,'','');},3000);
} else { showFotoStatus(who,'⚠ '+(data.error||'Fehler'),'var(--warning)'); }
}catch(e){ showFotoStatus(who,'⚠ '+e.message,'var(--warning)'); }
}
function deleteFoto(who){
var img=document.getElementById('foto-'+who+'-img');
var ph =document.getElementById('foto-'+who+'-placeholder');
var del=document.getElementById('foto-'+who+'-del');
var wrap=document.getElementById('foto-'+who+'-wrap');
if(img){img.src='';img.style.display='none';}
if(ph) ph.style.display='';
if(del)del.style.display='none';
if(wrap)wrap.classList.remove('has-photo');
// Aus Datensatz entfernen
if(!P.persoenlich) P.persoenlich={};
P.persoenlich['foto_'+who]=''; // leer statt gelöscht * damit Supabase es überschreibt
// Welcome-Banner leeren
updateWelcomeBannerFoto(who, null);
// Sofort speichern damit Löschung in DB landet
showFotoStatus(who,'Wird entfernt…','var(--muted)');
savePortalNow().then(function(){
showFotoStatus(who,'Entfernt','rgba(39,174,96,0.8)');
setTimeout(function(){ showFotoStatus(who,'',''); },2000);
}).catch(function(){
showFotoStatus(who,'Fehler beim Entfernen','var(--warning)');
});
}
function showFotoStatus(who,msg,color){
var el=document.getElementById('foto-'+who+'-status');
if(!el)return; el.textContent=msg; el.style.color=color||'var(--muted)';
}
function initFotos(){
// Pärchenbild aus gespeicherten Daten anzeigen (foto_paerchen)
var url = P.persoenlich && P.persoenlich.foto_paerchen;
if(url){
var img = document.getElementById('foto-paerchen-img');
var ph = document.getElementById('foto-paerchen-placeholder');
var del = document.getElementById('foto-paerchen-del');
var wrap = document.getElementById('foto-paerchen-wrap');
if(img){ img.src=url; img.style.display='block'; }
if(ph) ph.style.display='none';
if(del) del.style.display='inline';
if(wrap) wrap.classList.add('has-photo');
updateWelcomeBannerFoto('paerchen', url);
}
_updateProgSection();
}
function updateWelcomeBannerFoto(who, url){
// Nur Pärchenbild wird im Banner als Hintergrund gezeigt
if(who !== 'paerchen') return;
var wrap = document.getElementById('wb-foto-paerchen');
var img = document.getElementById('wb-img-paerchen');
if(!url || url===''){
if(wrap) wrap.style.display='none';
if(img){ img.src=''; }
} else if(wrap && img){
img.src = url;
wrap.style.display = 'block';
var banner = document.getElementById('welcome-banner');
if(banner) banner.style.minHeight = '240px';
}
_updateProgSection();
}
function _updateProgSection(){
// Welcome-Banner: sichtbar wenn Pärchenfoto vorhanden ist
var hasPaerchen = !!(P.persoenlich && P.persoenlich.foto_paerchen);
var banner = document.getElementById('welcome-banner');
if(banner) banner.style.display = hasPaerchen ? 'flex' : 'none';
}
// ── Musikalische Wurzeln: Länderliste + Flaggen-Logik ─────────────────
var COUNTRIES=[{c:'AF',n:'Afghanistan'},{c:'EG',n:'Ägypten'},{c:'AL',n:'Albanien'},{c:'DZ',n:'Algerien'},{c:'AD',n:'Andorra'},{c:'AO',n:'Angola'},{c:'AG',n:'Antigua und Barbuda'},{c:'GQ',n:'Äquatorialguinea'},{c:'AR',n:'Argentinien'},{c:'AM',n:'Armenien'},{c:'AZ',n:'Aserbaidschan'},{c:'ET',n:'Äthiopien'},{c:'AU',n:'Australien'},{c:'BS',n:'Bahamas'},{c:'BH',n:'Bahrain'},{c:'BD',n:'Bangladesch'},{c:'BB',n:'Barbados'},{c:'BY',n:'Belarus'},{c:'BE',n:'Belgien'},{c:'BZ',n:'Belize'},{c:'BJ',n:'Benin'},{c:'BT',n:'Bhutan'},{c:'BO',n:'Bolivien'},{c:'BA',n:'Bosnien und Herzegowina'},{c:'BW',n:'Botsuana'},{c:'BR',n:'Brasilien'},{c:'BN',n:'Brunei'},{c:'BG',n:'Bulgarien'},{c:'BF',n:'Burkina Faso'},{c:'BI',n:'Burundi'},{c:'CL',n:'Chile'},{c:'CN',n:'China'},{c:'CK',n:'Cookinseln'},{c:'CR',n:'Costa Rica'},{c:'CI',n:'Elfenbeinküste'},{c:'DK',n:'Dänemark'},{c:'DE',n:'Deutschland'},{c:'DM',n:'Dominica'},{c:'DO',n:'Dominikanische Republik'},{c:'DJ',n:'Dschibuti'},{c:'EC',n:'Ecuador'},{c:'SV',n:'El Salvador'},{c:'ER',n:'Eritrea'},{c:'EE',n:'Estland'},{c:'SZ',n:'Eswatini'},{c:'FJ',n:'Fidschi'},{c:'FI',n:'Finnland'},{c:'FR',n:'Frankreich'},{c:'GA',n:'Gabun'},{c:'GM',n:'Gambia'},{c:'GE',n:'Georgien'},{c:'GH',n:'Ghana'},{c:'GD',n:'Grenada'},{c:'GR',n:'Griechenland'},{c:'GT',n:'Guatemala'},{c:'GN',n:'Guinea'},{c:'GW',n:'Guinea-Bissau'},{c:'GY',n:'Guyana'},{c:'HT',n:'Haiti'},{c:'HN',n:'Honduras'},{c:'IN',n:'Indien'},{c:'ID',n:'Indonesien'},{c:'IQ',n:'Irak'},{c:'IR',n:'Iran'},{c:'IE',n:'Irland'},{c:'IS',n:'Island'},{c:'IL',n:'Israel'},{c:'IT',n:'Italien'},{c:'JM',n:'Jamaika'},{c:'JP',n:'Japan'},{c:'YE',n:'Jemen'},{c:'JO',n:'Jordanien'},{c:'KH',n:'Kambodscha'},{c:'CM',n:'Kamerun'},{c:'CA',n:'Kanada'},{c:'CV',n:'Kap Verde'},{c:'KZ',n:'Kasachstan'},{c:'QA',n:'Katar'},{c:'KE',n:'Kenia'},{c:'KG',n:'Kirgisistan'},{c:'KI',n:'Kiribati'},{c:'CO',n:'Kolumbien'},{c:'KM',n:'Komoren'},{c:'CD',n:'Kongo (Demokratische Republik)'},{c:'CG',n:'Kongo (Republik)'},{c:'KP',n:'Nordkorea'},{c:'KR',n:'Südkorea'},{c:'HR',n:'Kroatien'},{c:'CU',n:'Kuba'},{c:'KW',n:'Kuwait'},{c:'LA',n:'Laos'},{c:'LS',n:'Lesotho'},{c:'LV',n:'Lettland'},{c:'LB',n:'Libanon'},{c:'LR',n:'Liberia'},{c:'LY',n:'Libyen'},{c:'LI',n:'Liechtenstein'},{c:'LT',n:'Litauen'},{c:'LU',n:'Luxemburg'},{c:'MG',n:'Madagaskar'},{c:'MW',n:'Malawi'},{c:'MY',n:'Malaysia'},{c:'MV',n:'Malediven'},{c:'ML',n:'Mali'},{c:'MT',n:'Malta'},{c:'MA',n:'Marokko'},{c:'MH',n:'Marshallinseln'},{c:'MR',n:'Mauretanien'},{c:'MU',n:'Mauritius'},{c:'MX',n:'Mexiko'},{c:'FM',n:'Mikronesien'},{c:'MD',n:'Moldau'},{c:'MC',n:'Monaco'},{c:'MN',n:'Mongolei'},{c:'ME',n:'Montenegro'},{c:'MZ',n:'Mosambik'},{c:'MM',n:'Myanmar'},{c:'NA',n:'Namibia'},{c:'NR',n:'Nauru'},{c:'NP',n:'Nepal'},{c:'NZ',n:'Neuseeland'},{c:'NI',n:'Nicaragua'},{c:'NL',n:'Niederlande'},{c:'NE',n:'Niger'},{c:'NG',n:'Nigeria'},{c:'MK',n:'Nordmazedonien'},{c:'NO',n:'Norwegen'},{c:'OM',n:'Oman'},{c:'AT',n:'Österreich'},{c:'PK',n:'Pakistan'},{c:'PW',n:'Palau'},{c:'PS',n:'Palästina'},{c:'PA',n:'Panama'},{c:'PG',n:'Papua-Neuguinea'},{c:'PY',n:'Paraguay'},{c:'PE',n:'Peru'},{c:'PH',n:'Philippinen'},{c:'PL',n:'Polen'},{c:'PT',n:'Portugal'},{c:'RW',n:'Ruanda'},{c:'RO',n:'Rumänien'},{c:'RU',n:'Russland'},{c:'SB',n:'Salomonen'},{c:'ZM',n:'Sambia'},{c:'WS',n:'Samoa'},{c:'SM',n:'San Marino'},{c:'ST',n:'São Tomé und Príncipe'},{c:'SA',n:'Saudi-Arabien'},{c:'SE',n:'Schweden'},{c:'CH',n:'Schweiz'},{c:'SN',n:'Senegal'},{c:'RS',n:'Serbien'},{c:'SC',n:'Seychellen'},{c:'SL',n:'Sierra Leone'},{c:'ZW',n:'Simbabwe'},{c:'SG',n:'Singapur'},{c:'SK',n:'Slowakei'},{c:'SI',n:'Slowenien'},{c:'SO',n:'Somalia'},{c:'ES',n:'Spanien'},{c:'LK',n:'Sri Lanka'},{c:'KN',n:'St. Kitts und Nevis'},{c:'LC',n:'St. Lucia'},{c:'VC',n:'St. Vincent und die Grenadinen'},{c:'ZA',n:'Südafrika'},{c:'SD',n:'Sudan'},{c:'SS',n:'Südsudan'},{c:'SR',n:'Suriname'},{c:'SY',n:'Syrien'},{c:'TJ',n:'Tadschikistan'},{c:'TW',n:'Taiwan'},{c:'TZ',n:'Tansania'},{c:'TH',n:'Thailand'},{c:'TL',n:'Timor-Leste'},{c:'TG',n:'Togo'},{c:'TO',n:'Tonga'},{c:'TT',n:'Trinidad und Tobago'},{c:'TD',n:'Tschad'},{c:'CZ',n:'Tschechien'},{c:'TN',n:'Tunesien'},{c:'TR',n:'Türkei'},{c:'TM',n:'Turkmenistan'},{c:'TV',n:'Tuvalu'},{c:'UG',n:'Uganda'},{c:'UA',n:'Ukraine'},{c:'HU',n:'Ungarn'},{c:'UY',n:'Uruguay'},{c:'UZ',n:'Usbekistan'},{c:'VU',n:'Vanuatu'},{c:'VA',n:'Vatikanstadt'},{c:'VE',n:'Venezuela'},{c:'AE',n:'Vereinigte Arabische Emirate'},{c:'US',n:'USA'},{c:'GB',n:'Vereinigtes Königreich'},{c:'VN',n:'Vietnam'},{c:'CF',n:'Zentralafrikanische Republik'},{c:'CY',n:'Zypern'}];
// Deutsche Regionen ganz oben in der Liste — eigene Musikrichtung innerhalb
// Deutschlands (z.B. bayerische Volksmusik, Kölner Karneval). Kein offizielles
// Unicode-Flaggen-Emoji für Bundesländer/Regionen verfügbar, daher einheitlich
// die Deutschland-Flagge als Kennzeichnung.
// Echte, regionsspezifische Flaggen für die deutschen Regionen (als Inline-SVG,
// kein Hotlinking zu Wikimedia nötig — Landesfarben als einfache Streifen,
// gut erkennbar auch in kleiner Darstellung).
function _stripeFlagDataUri(colors){
var n=colors.length, h=100/n, rects='';
colors.forEach(function(c,i){ rects += ''; });
var svg = '';
return 'data:image/svg+xml;utf8,'+encodeURIComponent(svg);
}
var REGIONS_DE=[
{n:'Bayern (Volksmusik, Oktoberfest)', img:_stripeFlagDataUri(['#FFFFFF','#0080C8'])},
{n:'Rheinland / Köln (Karneval, Kölsch)', img:_stripeFlagDataUri(['#009B3A','#FFFFFF','#DD0000'])},
{n:'Hamburg / Norddeutschland', img:_stripeFlagDataUri(['#FFFFFF','#C8102E','#FFFFFF'])},
{n:'Berlin', img:_stripeFlagDataUri(['#C8102E','#FFFFFF','#C8102E'])},
{n:'Sachsen / Erzgebirge', img:_stripeFlagDataUri(['#FFFFFF','#0A7B3E'])},
{n:'Schwaben / Baden-Württemberg', img:_stripeFlagDataUri(['#000000','#F4C300'])},
{n:'Ostfriesland', img:_stripeFlagDataUri(['#000000','#DD0000','#F4C300'])}
];
// Echte Flaggen-Bilder statt Emoji: Emoji-Flaggen rendern auf vielen Windows-
// Systemen nur als Buchstaben-Box (kein Flaggen-Font hinterlegt). flagcdn.com
// liefert echte PNG-Flaggen für Länder, funktioniert auf jedem Gerät identisch.
function flagImgUrl(code){
return code ? 'https://flagcdn.com/24x18/'+code.toLowerCase()+'.png' : '';
}
function _flagSrc(o){ return o.img || flagImgUrl(o.code); }
var _MUSIKLAND_OPTIONS = null;
function _musiklandOptions(){
if(_MUSIKLAND_OPTIONS) return _MUSIKLAND_OPTIONS;
var ctys = COUNTRIES.slice().sort(function(a,b){ return a.n.localeCompare(b.n,'de'); })
.map(function(c){ return {n:c.n, code:c.c.toLowerCase()}; });
_MUSIKLAND_OPTIONS = REGIONS_DE.concat(ctys);
return _MUSIKLAND_OPTIONS;
}
function musiklandShowDrop(inputId, dropId){
var inp=document.getElementById(inputId), drop=document.getElementById(dropId);
if(!inp || !drop) return;
var q=(inp.value||'').trim().toLowerCase();
var opts=_musiklandOptions();
var matches = q ? opts.filter(function(o){ return o.n.toLowerCase().indexOf(q)>=0; }) : opts;
matches = matches.slice(0,30);
if(!matches.length){ drop.style.display='none'; return; }
drop.innerHTML = matches.map(function(o){
return '
'
+ ''
+ ''+o.n.replace(/&/g,'&').replace(/'
+ '
';
}).join('');
drop.style.display='block';
}
function musiklandHideDrop(dropId){
var drop=document.getElementById(dropId);
if(drop) drop.style.display='none';
}
function musiklandPick(inputId, dropId, name){
var inp=document.getElementById(inputId);
if(inp) inp.value=name;
musiklandHideDrop(dropId);
var flagId = 'flag-'+inputId.replace('per-','');
updateMusiklandFlag(inputId, flagId);
}
function updateMusiklandFlag(inputId, flagId){
var inp=document.getElementById(inputId), flagImg=document.getElementById(flagId);
if(!inp || !flagImg) return;
var val=(inp.value||'').trim().toLowerCase();
if(!val){ flagImg.style.display='none'; return; }
var match = _musiklandOptions().find(function(o){ return o.n.toLowerCase()===val; });
if(match){ flagImg.src = _flagSrc(match); flagImg.style.display='block'; }
else { flagImg.style.display='none'; }
}
function initPersoenlich() {
var p = P.persoenlich || {};
['braut','braeutigam','nachnamePartner','nachname','telefon','telefon2','alterBraut','alterBraeutigam','gaeste','altersgruppe','kinder','beduerfnisse','weiteres','tanzaffinitaet','international','musiklandBraut','musiklandBraeutigam'].forEach(function(id){
var el=document.getElementById('per-'+id); if(el&&p[id]!=null) el.value=p[id];
});
updateMusiklandFlag('per-musiklandBraut','flag-musiklandBraut');
updateMusiklandFlag('per-musiklandBraeutigam','flag-musiklandBraeutigam');
// Pärchenbild-URL aus gespeicherten Daten nach P.persoenlich zurückschreiben
if(p.foto_paerchen) { if(!P.persoenlich) P.persoenlich={}; P.persoenlich.foto_paerchen=p.foto_paerchen; }
if(p.paerchenFotoBase64) { if(!P.persoenlich) P.persoenlich={}; P.persoenlich.paerchenFotoBase64=p.paerchenFotoBase64; }
var pTa = document.getElementById('per-tanzaffinitaet');
var pIn = document.getElementById('per-international');
if(pTa && p.tanzaffinitaet) pTa.value = p.tanzaffinitaet;
if(pIn && p.international) pIn.value = p.international;
tzInit(p.trauzeugen);
_taAutoResize(document.getElementById('per-weiteres'));
}
// ── Buchungsdaten-Abgleich ──────────────────────────────────────────
function normalizeRef(s){ return (s||'').toLowerCase().replace(/[\s\-]/g,''); }
function validateTelefon(id){
var el=document.getElementById(id);
if(!el) return;
el.addEventListener('blur',function(){
var v=el.value.trim();
if(!v) return;
var ok = /^[+0][\d\s\-\/\(\)]{6,}$/.test(v);
if(!ok){
el.style.borderColor='var(--warning)';
var hint=el.parentNode.querySelector('.tel-hint');
if(!hint){ hint=document.createElement('div'); hint.className='tel-hint'; hint.style.cssText='font-size:14px;color:var(--warning);margin-top:3px'; el.parentNode.appendChild(hint); }
hint.textContent='Format: +49... oder 0...';
} else {
el.style.borderColor='';
var hint=el.parentNode.querySelector('.tel-hint');
if(hint) hint.remove();
}
});
}
// ── Dynamische Trauzeugen ─────────────────────────────────────────────
var _tzData = [];
function tzRender(){
var c=document.getElementById('tz-list');
if(!c) return;
if(_tzData.length===0){
c.innerHTML='
'
+'
\u{1F465}
'
+'
Trauzeug*innen oder Ansprechpartner
'
+'
Wer ist am Hochzeitstag direkter Ansprechpartner f\u00fcr mich? Trauzeug*innen sind ideal — falls etwas zu kl\u00e4ren ist, ohne euch zu st\u00f6ren.
';
}).join('');
}
function renderQuickMsgHistory(){
var el=document.getElementById('quick-msg-history');
if(!el) return;
var sent = (P&&P.nachrichten)||[];
var recv = (P&&P.nachrichtenDJ)||[];
// Combine and tag with sender
var all = sent.map(function(m){return {ts:m.ts,text:m.text,von:'braut'};});
recv.forEach(function(m){all.push({ts:m.ts,text:m.text,von:'dj'});});
all.sort(function(a,b){return new Date(a.ts)-new Date(b.ts);});
var title='
Verlauf
';
if(!all.length){
el.innerHTML=title+'
💬 Noch keine Nachrichten — schreibt gerne jederzeit
';
return;
}
var items=all.slice(-8).map(function(m){
var d=new Date(m.ts);
var dt=d.toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit'})+' · '+d.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'});
var isDJ = m.von==='dj';
var bg = isDJ ? 'rgba(200,168,75,0.08)' : 'var(--surface2)';
var brd = isDJ ? 'rgba(200,168,75,0.25)' : 'var(--border)';
var label = isDJ ? 'DJ Micha' : 'Ihr';
return '
'
+'
'+label+''+dt+' Uhr
'
+'
'+m.text.replace(//g,'>')+'
'
+'
';
}).join('');
el.innerHTML=title+items;
}
function tzAdd(){ _tzData.push({name:'',tel:''}); tzRender(); }
function tzRemove(i){ _tzData.splice(i,1); tzRender(); }
function tzCollect(){ return _tzData.slice(); }
function tzInit(saved){
_tzData=[];
if(saved && saved.length) _tzData=saved.map(function(t){return {name:t.name||'',tel:t.tel||''};});
else if(saved===undefined){
// Kompatibilität mit altem Format (tz1name, tz2name)
var p=P.persoenlich||{};
if(p.tz1name||p.tz1tel) _tzData.push({name:p.tz1name||'',tel:p.tz1tel||''});
if(p.tz2name||p.tz2tel) _tzData.push({name:p.tz2name||'',tel:p.tz2tel||''});
}
tzRender();
}
function checkMatchField(inputId, badgeId, refValue){
var inp = document.getElementById(inputId);
var badge = document.getElementById(badgeId);
if(!inp || !badge) return;
function update(){
var val = normalizeRef(inp.value);
var ref = normalizeRef(refValue);
if(!val){ badge.className='match-badge empty'; return; }
if(!ref){ badge.className='match-badge empty'; return; }
if(val === ref){ badge.className='match-badge empty'; }
else {
var refShow = refValue.trim().slice(0,20);
badge.textContent='Vertrag: '+refShow; badge.className='match-badge diff';
badge.title='Im Vertrag: '+(refValue||'').replace(/[<>"&]/g,function(c){return {'<':'<','>':'>','"':'"','&':'&'}[c];})+' | Eingetragen: '+(inp.value||'').replace(/[<>"&]/g,function(c){return {'<':'<','>':'>','"':'"','&':'&'}[c];});
}
}
inp.addEventListener('input', update);
inp.addEventListener('blur', update);
update();
}
function initMatchChecks(){
var R = window.KUNDEN_REF || {};
checkMatchField('per-braut', 'mb-braut', R.brautVorname||'');
checkMatchField('per-braeutigam', 'mb-braeutigam', R.braeutigamVorname||'');
checkMatchField('per-nachname', 'mb-nachname', R.brautNachname||'');
checkMatchField('per-nachnamePartner','mb-nachnamePartner', R.partnerNachname||'');
checkMatchField('per-telefon', 'mb-telefon', R.telefon||'');
}
document.addEventListener('DOMContentLoaded', function(){ initMatchChecks(); validateTelefon('per-telefon'); validateTelefon('per-telefon2'); validateTelefon('per-tz1-tel'); validateTelefon('per-tz2-tel');
// Drag & Drop am Pärchenfoto-Slot
var _slot = document.getElementById('foto-paerchen-wrap');
if(_slot){
_slot.addEventListener('dragover', function(e){
e.preventDefault();
_slot.style.borderColor='var(--gold)'; _slot.style.background='rgba(200,168,75,0.08)';
});
_slot.addEventListener('dragleave', function(){
_slot.style.borderColor=''; _slot.style.background='';
});
_slot.addEventListener('drop', function(e){
e.preventDefault();
_slot.style.borderColor=''; _slot.style.background='';
var f = e.dataTransfer.files && e.dataTransfer.files[0];
if(!f) return;
if(!f.type.startsWith('image/')){ showFotoStatus('paerchen','⚠ Nur Bilder erlaubt','var(--warning)'); return; }
_fotoTarget = 'paerchen';
openPortalCropTool(f);
});
}
});
function _jzToggle(btn){
btn.classList.toggle('jz-on');
var on = btn.classList.contains('jz-on');
btn.style.background = on ? 'var(--gold)' : 'rgba(200,168,75,0.06)';
btn.style.color = on ? 'var(--dark)' : 'var(--muted)';
btn.style.borderColor = on ? 'var(--gold)' : 'rgba(200,168,75,0.25)';
}
function _jzCollect(){
var result = [];
document.querySelectorAll('#per-jahrzehnte-chips .jz-chip.jz-on').forEach(function(b){
result.push(b.dataset.jz);
});
return result;
}
function _jzInit(vals){
if(!vals||!vals.length) return;
document.querySelectorAll('#per-jahrzehnte-chips .jz-chip').forEach(function(b){
if(vals.indexOf(b.dataset.jz)>=0) _jzToggle(b);
});
}
function collectPersoenlich() {
var r={};
['braut','braeutigam','nachnamePartner','nachname','telefon','telefon2','alterBraut','alterBraeutigam','gaeste','altersgruppe','kinder','beduerfnisse','weiteres','tanzaffinitaet','international','musiklandBraut','musiklandBraeutigam'].forEach(function(id){
var el=document.getElementById('per-'+id); if(el) r[id.replace(/-/g,'')]=el.value;
});
// Trauzeugen aus _tzData (dynamisches System, nicht DOM-IDs)
var _tz=tzCollect();
if(_tz[0]){ r.tz1name=_tz[0].name||''; r.tz1tel=_tz[0].tel||''; }
if(_tz[1]){ r.tz2name=_tz[1].name||''; r.tz2tel=_tz[1].tel||''; }
// Foto-URL aus P übernehmen (kein Form-Feld * wird separat hochgeladen)
// Atmosphäre-Freitext
if(P.persoenlich && P.persoenlich.foto_paerchen) r.foto_paerchen = P.persoenlich.foto_paerchen;
if(P.persoenlich && P.persoenlich.paerchenFotoBase64) r.paerchenFotoBase64 = P.persoenlich.paerchenFotoBase64;
// Legacy-Felder weiter mit-persistieren falls noch alte Daten existieren (Rückwärtskompatibilität)
if(P.persoenlich && P.persoenlich.foto_braut) r.foto_braut = P.persoenlich.foto_braut;
if(P.persoenlich && P.persoenlich.foto_braeutigam) r.foto_braeutigam = P.persoenlich.foto_braeutigam;
return r;
}
// ── Dokumente ─────────────────────────────────────────────────────────
var uploadedFiles = [];
var stagedFiles = []; // bereitgestellt, noch nicht hochgeladen
var MAX_FILES = 10;
var MAX_TOTAL_MB = 20;
var stagedCategories = {}; // {filename: category}
function initDokumente() {
uploadedFiles = P.dokumente ? JSON.parse(JSON.stringify(P.dokumente)) : [];
stagedFiles = [];
renderFileList();
renderStageList();
}
// ── Bereitgestellte Dateien (Staging) ─────────────────────────────────
function stageFiles(files){
var errors = [];
Array.from(files).forEach(function(file){
var totalCount = uploadedFiles.length + stagedFiles.length;
var totalBytes = uploadedFiles.reduce(function(s,f){return s+(f.size||0);},0)
+ stagedFiles.reduce(function(s,f){return s+f.size;},0);
if(totalCount >= MAX_FILES){
errors.push(file.name + ': Max. ' + MAX_FILES + ' Dateien erlaubt.'); return;
}
if(totalBytes + file.size > MAX_TOTAL_MB * 1024 * 1024){
errors.push(file.name + ': Gesamtlimit ' + MAX_TOTAL_MB + ' MB überschritten.'); return;
}
// Duplikat prüfen
var isDup = uploadedFiles.some(function(f){return f.name===file.name;})
|| stagedFiles.some(function(f){return f.name===file.name;});
if(isDup){ errors.push(file.name + ': Datei bereits vorhanden.'); return; }
stagedFiles.push(file);
});
var fi = document.getElementById('file-input');
if(fi) fi.value = '';
if(errors.length) showStageError(errors.join('\n'));
renderStageList();
updateUploadZoneState();
}
function renderStageList(){
var area = document.getElementById('stage-area');
var list = document.getElementById('stage-list');
var btn = document.getElementById('upload-all-btn');
if(!area||!list) return;
if(!stagedFiles.length){ area.style.display='none'; return; }
area.style.display = 'block';
var totalKB = Math.round(stagedFiles.reduce(function(s,f){return s+f.size;},0)/1024);
var CATS = [
{v:'', l:'Kategorie wählen (optional)'},
{v:'Raumplan', l:'📍 Raumplan / Grundriss'},
{v:'Sitzplan', l:'🦸 Sitzplan'},
{v:'Ablaufplan', l:'📋 Ablaufplan'},
{v:'Songwunschliste', l:'🎵 Songwunschliste'},
{v:'Inspiration', l:'✨ Inspiration / Moodboard'},
{v:'Kontakte', l:'📞 Kontaktliste Dienstleister'},
{v:'Sonstiges', l:'🗄 Sonstiges'}
];
list.innerHTML = stagedFiles.map(function(f,i){
var ext = (f.name||'').split('.').pop().toLowerCase();
var icons = {pdf:'📄',jpg:'🖼',jpeg:'🖼',png:'🖼',docx:'📝',xlsx:'📊'};
var icon = icons[ext] || '📎';
var size = f.size>1048576?(f.size/1048576).toFixed(1)+' MB':Math.round(f.size/1024)+' KB';
var cur = stagedCategories[f.name] || '';
var curL = (CATS.find(function(x){return x.v===cur;})||CATS[0]).l;
var opts = CATS.map(function(cat){
return '
' + cat.l + '
';
}).join('');
return '
'
+ '
'
+ '
' + icon + '
'
+ '
' + esc(f.name) + '
'
+ '
' + size + '
'
+ ''
+ '
'
+ '
'
+ '
' + curL + '▾
'
+ '
' + opts + '
'
+ '
'
+ '
';
}).join('');
// Attach click handlers via event delegation after render
list.querySelectorAll('.cat-drop-btn').forEach(function(btn){
btn.addEventListener('click', function(e){
e.stopPropagation();
var drop = btn.closest('.cat-drop');
var wasOpen = drop.classList.contains('open');
document.querySelectorAll('.cat-drop.open').forEach(function(d){d.classList.remove('open');});
if(!wasOpen) drop.classList.add('open');
});
});
list.querySelectorAll('.cat-drop-opt').forEach(function(opt){
opt.addEventListener('click', function(e){
e.stopPropagation();
var drop = opt.closest('.cat-drop');
var fname = drop.dataset.fname;
var val = opt.dataset.val;
stagedCategories[fname] = val;
drop.classList.remove('open');
var lbl = drop.querySelector('.cat-btn-lbl');
if(lbl) lbl.innerHTML = opt.innerHTML;
drop.querySelectorAll('.cat-drop-opt').forEach(function(o){
o.classList.toggle('selected', o.dataset.val===val);
});
});
});
if(btn) btn.textContent = stagedFiles.length + ' Datei' + (stagedFiles.length>1?'en':'') + ' hochladen (' + totalKB + ' KB)';
}
// Close dropdowns on outside click
document.addEventListener('click', function(e){
if(!e.target.closest('.cat-drop'))
document.querySelectorAll('.cat-drop.open').forEach(function(d){d.classList.remove('open');});
});
function unstageFile(i){
stagedFiles.splice(i,1);
renderStageList();
updateUploadZoneState();
}
function showStageError(msg){
var area = document.getElementById('stage-area');
if(!area) return;
var err = document.getElementById('stage-error');
if(!err){
err = document.createElement('div');
err.id = 'stage-error';
err.style.cssText = 'font-size:14px;color:var(--warning);background:rgba(196,132,106,0.08);border:1px solid rgba(196,132,106,0.25);border-radius:var(--r);padding:8px 12px;margin-bottom:8px;white-space:pre-line';
area.insertBefore(err, area.firstChild);
area.style.display='block';
}
err.textContent = '⚠ ' + msg;
setTimeout(function(){ if(err.parentNode) err.parentNode.removeChild(err); },4000);
}
// ── Alle bereitgestellten Dateien hochladen ────────────────────────────
function uploadAllStaged(){
if(!stagedFiles.length) return;
var btn = document.getElementById('upload-all-btn');
if(btn){ btn.disabled=true; btn.textContent='⏳ Wird hochgeladen…'; }
var toUpload = stagedFiles.slice();
stagedFiles = [];
renderStageList();
var done = 0;
toUpload.forEach(function(file){
var reader = new FileReader();
reader.onload = function(e){
var dataUrl = e.target.result;
uploadedFiles.push({name:file.name,size:file.size,type:file.type,data:dataUrl,category:stagedCategories[file.name]||''});
delete stagedCategories[file.name];
renderFileList();
savePortalNow('Datei hinzugefügt');
uploadFileToOneDrive(file.name, dataUrl);
done++;
if(done===toUpload.length && btn){ btn.disabled=false; }
};
reader.readAsDataURL(file);
});
updateUploadZoneState();
}
// ── Bereits hochgeladene Dateien ───────────────────────────────────────
function renderFileList() {
var c = document.getElementById('file-list');
if(!c) return;
if(!uploadedFiles.length){
c.innerHTML = '
Noch keine Dateien hochgeladen.
';
updateUploadZoneState(); return;
}
var totalKB = Math.round(uploadedFiles.reduce(function(s,f){return s+(f.size||0);},0)/1024);
var remaining = MAX_FILES - uploadedFiles.length - stagedFiles.length;
var counterColor = remaining<=0?'var(--warning)':remaining===1?'var(--gold)':'var(--muted)';
c.innerHTML = '
';
}).join('');
updateUploadZoneState();
}
function delFile(i){uploadedFiles.splice(i,1);renderFileList();savePortalNow('Datei entfernt');}
function updateUploadZoneState(){
var zone = document.getElementById('upload-zone');
var fi = document.getElementById('file-input');
if(!zone||!fi) return;
var full = (uploadedFiles.length + stagedFiles.length) >= MAX_FILES;
zone.style.opacity = full?'0.4':'';
zone.style.pointerEvents = full?'none':'';
fi.disabled = full;
}
// Vollaudit 09.09.2026, Restbefunde-Nachpruefung, Befund HZP-08:
// uploadAllStaged() schreibt eine Datei ZUERST in uploadedFiles (und damit in
// die Datenbank), bevor der OneDrive-Upload ueberhaupt beginnt. Schlaegt er
// fehl, blieb der Eintrag bisher ohne Inhalt und ohne URL fuer immer stehen -
// ein Phantom-Dokument, das sich wegen der Dublettenpruefung (Name bereits in
// uploadedFiles) auch nicht erneut hochladen liess, aber im DJ-Briefing
// trotzdem als "hochgeladen" erschien. Bei jedem Fehlschlag den Phantom-
// Eintrag wieder entfernen und die Entfernung persistieren - dieselbe Datei
// laesst sich danach erneut hochladen.
function _odPhantomEntfernen(filename){
var idx = uploadedFiles.findIndex(function(f){ return f.name===filename && !f.odUploaded; });
if(idx<0) return;
uploadedFiles.splice(idx,1);
renderFileList();
savePortalNow('Upload fehlgeschlagen, Eintrag entfernt');
}
async function uploadFileToOneDrive(filename, dataUrl){
// Debug-Info (hilft bei Fehleranalyse)
if(!OD_KUNDE_ID){
console.warn('[OD-Upload] Fehlende Kunden-ID * Upload übersprungen');
showOdError(filename, 'Keine Kunden-ID gefunden. Bitte DJ kontaktieren.');
_odPhantomEntfernen(filename);
return;
}
try{
var b64 = dataUrl.split(',')[1];
if(!b64){ console.error('[OD-Upload] Base64 fehlt'); _odPhantomEntfernen(filename); return; }
var res = await fetch(window.location.href.split('?')[0], {
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({
action: 'onedrive-upload',
aid: AID,
token: TOKEN,
pdf_base64: b64,
filename: filename,
kunde_id: OD_KUNDE_ID,
kunde_name: OD_KUNDE_NAME,
year: OD_YEAR,
dokument_typ: 'Hochzeitsportal',
is_signed: false,
root_path: OD_ROOT
})
});
var data = await res.json().catch(function(e){
console.error('[OD-Upload] JSON-Parse-Fehler:', e);
return {ok:false, error:'Server-Antwort ungültig'};
});
if(data.ok){
var fIdx = uploadedFiles.findIndex(function(f){ return f.name===filename && f.data; });
if(fIdx>=0){
uploadedFiles[fIdx].data=null;
uploadedFiles[fIdx].odUploaded=true;
uploadedFiles[fIdx].odUrl=data.embedUrl||data.webUrl||null;
}
savePortalNow('Datei in OneDrive gesichert');
renderFileList();
var okEl = document.getElementById('save-ok');
if(okEl){
okEl.textContent = '☁️ ' + filename + ' → OneDrive';
okEl.style.display = 'flex';
okEl.style.background = 'rgba(39,174,96,0.15)';
okEl.style.color = '#27AE60';
setTimeout(function(){okEl.style.display='none';}, 3500);
}
} else {
var errMsg = data.error || 'Unbekannter Fehler';
console.error('[OD-Upload] Fehlgeschlagen:', errMsg);
showOdError(filename, errMsg);
_odPhantomEntfernen(filename);
}
}catch(e){
console.error('[OD-Upload] Exception:', e);
showOdError(filename, e.message||'Netzwerkfehler');
_odPhantomEntfernen(filename);
}
}
function showOdError(filename, msg){
var okEl=document.getElementById('save-ok');
if(!okEl) return;
okEl.textContent='⚠️ '+filename+': '+msg;
okEl.style.display='flex';
okEl.style.background='rgba(196,132,106,0.15)';
okEl.style.color='var(--warning)';
setTimeout(function(){okEl.style.display='none';},6000);
}
// ── Drag & Drop ───────────────────────────────────────────────────────
(function(){
var zone = document.getElementById('upload-zone');
if(!zone) return;
zone.addEventListener('dragover', function(e){
e.preventDefault(); e.stopPropagation();
zone.classList.add('drag-over');
});
zone.addEventListener('dragleave', function(e){
e.preventDefault();
// Nur entfernen wenn wirklich aus der Zone raus (nicht bei child-hover)
if(!zone.contains(e.relatedTarget)) zone.classList.remove('drag-over');
});
zone.addEventListener('drop', function(e){
e.preventDefault(); e.stopPropagation();
zone.classList.remove('drag-over');
var files = e.dataTransfer.files;
if(files && files.length) stageFiles(files);
});
})();
// ── Übersicht-Zusammenfassung ─────────────────────────────────────────
function renderAbschlussZusammenfassung(){
var el = document.getElementById('abschluss-summary');
if(!el) return;
// Tiefenaudit 01.09.2026, Befund H21 (Nachtrag): "var P = window.P || {}"
// ueberschattete hier die echte, aeussere P-Variable mit einem IMMER
// leeren Objekt (window.P war nie gesetzt, siehe Kommentar bei P._loadedAt
// oben) - die Uebersicht-Zusammenfassung zeigte dadurch unabhaengig vom
// tatsaechlichen Stand ausnahmslos "-" fuer jede Zeile. Ohne diese Zeile
// greift die Funktion auf die echte, aeussere P-Variable zu.
var rows = [];
// Über euch
var per = P.persoenlich || {};
var name = [per.braut, per.braeutigam].filter(Boolean).join(' & ') || '—';
var gaeste = per.gaeste ? per.gaeste + ' Gäste' : '—';
rows.push({label:'Über euch', val: name + ' · ' + gaeste, ok: !!(per.braut && per.braeutigam)});
// Songs
var songs = P.songs || {};
var muss = (songs.muss || []).length;
var nogo = (songs.nogo || []).length;
var nogoG = (songs.nogoGenres || []).length;
var tanz = (songs.momente && songs.momente.ersteTanz) ? '♪ ' + songs.momente.ersteTanz : '';
var songVal = muss + ' Must-Have' + (muss!==1?'s':'');
if(nogo) songVal += ' · ' + nogo + ' No-Go' + (nogo!==1?'s':'');
if(nogoG) songVal += ' · ' + nogoG + ' Richtung' + (nogoG!==1?'en':'');
if(tanz) songVal += ' · Hochzeitstanz: ' + tanz.substring(0,30);
rows.push({label:'Musik', val: songVal || '—', ok: muss > 0});
// Ablauf
var abl = Array.isArray(P.ablauf) ? P.ablauf : [];
var aktiv = abl.filter(function(a){ return a.aktiv; }).length;
rows.push({label:'Ablauf', val: aktiv + ' Programmpunkte aktiv', ok: aktiv >= 3});
// Location
var loc = P.location || {};
var locName = loc.name || '—';
rows.push({label:'Location', val: locName, ok: !!(loc.name)});
// Genres
var dna = P.musikDna || {};
var jz = (dna.jahrzehnte||[]).join(', ') || '—';
rows.push({label:'Genres', val: jz, ok: !!(dna.energie || (dna.jahrzehnte||[]).length)});
// Dokumente
var docs = (P.dokumente || []).length;
rows.push({label:'Dateien', val: docs ? docs + ' Datei' + (docs!==1?'en':'') + ' hochgeladen' : 'Keine Uploads', ok: true});
var allOk = rows.filter(function(r){ return !r.ok; }).length === 0;
var html = '
Eure Zusammenfassung
';
rows.forEach(function(r){
html += '
'
+ '
' + r.label + '
'
+ '
' + r.val + '
'
+ '
' + (r.ok ? '✓' : '○') + '
'
+ '
';
});
if(!allOk){
html += '
○ = noch nicht vollständig — ihr könnt trotzdem abschicken, ich melde mich dann bei euch.
';
}
el.innerHTML = html;
}
function renderOverviewSummary(){/* no-op: feature disabled */}
// ── Fortschritt ───────────────────────────────────────────────────────
// ── Mini-Belohnungen: Toast wenn ein Tab vollständig wird ────────
window._tabRewardsShown = window._tabRewardsShown || {};
function checkTabReward(checks){
if(!checks) return;
var labels = {
persoenlich:'Über euch ist komplett',
songs:'Songs sind drin',
musikdna:'Musikstil festgelegt',
ablauf:'Ablauf steht',
location:'Location erfasst'
};
Object.keys(labels).forEach(function(k){
if(checks[k] && !window._tabRewardsShown[k]){
window._tabRewardsShown[k] = true;
// Toast nur zeigen wenn nicht beim ersten Laden
if(window._initialChecksDone){
showToast(labels[k] + ' · super!');
}
}
});
}
function showToast(msg){
var t = document.createElement('div');
t.setAttribute('role','status');
t.setAttribute('aria-live','polite');
t.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%) translateY(20px);background:rgba(200,168,75,0.95);color:#0a0906;padding:12px 20px;border-radius:var(--r-lg,12px);font-size:14px;font-weight:600;box-shadow:0 8px 28px rgba(0,0,0,0.4);z-index:5000;opacity:0;transition: all .3s ease;font-family:var(--font-b,sans-serif);max-width:320px;text-align:center';
t.textContent = msg;
document.body.appendChild(t);
requestAnimationFrame(function(){ t.style.opacity='1'; t.style.transform='translateX(-50%) translateY(0)'; });
setTimeout(function(){
t.style.opacity='0'; t.style.transform='translateX(-50%) translateY(20px)';
setTimeout(function(){ t.remove(); }, 350);
}, 2800);
}
// UX-Audit Etappe A: der aus der Buchung vorbelegte Locationname allein zaehlt
// nicht mehr als "erledigt" (die Startseite zeigte sonst 20 % vor der ersten
// Eingabe). Erledigt ist die Location, wenn das Paar sie bestaetigt oder
// korrigiert hat oder mindestens eine Logistikangabe gemacht hat.
function _locationErledigt(){
var l = P.location || {};
if(!l.name) return false;
if(l.aktualisiert || l.bestaetigt) return true;
return !!(l.indoor || l.tanzflaeche || l.parkplatz || l.aufzug || l.djpos || l.lautstaerke || l.besonderheiten || l.strasse || l.entfernung || l.aufbauzeit);
}
// Rote Pflichtfeld-Warnungen erst, nachdem der Bereich einmal geoeffnet wurde
// oder ein Abschluss versucht wurde - nicht schon beim ersten Blick.
var _besuchteTabs = {};
function _besuchtKey(){ try{ return 'bpm_hwp_besucht_' + AID; }catch(e){ return 'bpm_hwp_besucht'; } }
function _besuchtMerken(id){ _besuchteTabs[id] = true; try{ sessionStorage.setItem(_besuchtKey(), JSON.stringify(_besuchteTabs)); }catch(e){} }
function _bereichBesucht(k){ if(window._abschlussVersucht) return true; var t = (k === 'musikdna') ? 'songs' : k; return !!_besuchteTabs[t]; }
(function(){ try{ var s = sessionStorage.getItem(_besuchtKey()); if(s) _besuchteTabs = JSON.parse(s) || {}; }catch(e){} })();
function updateProgress() {
// Musikstil: min. 3 Genre-Kacheln explizit bewertet (> 0 = tatsaechlich
// angetippt) ODER md-hinweise / md-energie ausgefuellt - verhindert
// False-Positive durch Defaults.
//
// Vollaudit 14.09.2026, Befund HZP2-07: collectMusikDNA() schreibt fuer
// JEDE der 28 Genre-Kacheln einen Wert, unbewertet ist 0 (chip.dataset.v
// faellt bei fehlender Bewertung auf 0 zurueck, nicht auf 2). Der
// Vergleich "!== 2" zaehlte damit ALLE 28 unbewerteten Kacheln bei jedem
// Speichern als "geaendert" - der hier daneben stehende "min. 3"-Schwellwert
// griff dadurch faktisch nie: 28 ist immer >= 3. checks.musikdna war so
// dauerhaft wahr, sobald das Paar irgendwo im Portal irgendetwas
// speicherte, auch wenn kein einziges Genre bewertet war.
var _musikChanged = 0;
if(P.musikDna){
['party','bg'].forEach(function(grp){
if(P.musikDna[grp]) Object.values(P.musikDna[grp]).forEach(function(v){ if(parseInt(v) > 0) _musikChanged++; });
});
}
// Vollaudit 14.09.2026, Befund HZP2-07 (Nebenbefund): tanzaffinitaet und
// international liegen in P.persoenlich, nie in P.musikDna - dieser Teil
// des Ausdrucks war toter Code, der nie wahr werden konnte.
var _musikExtra = !!(P.musikDna && (P.musikDna.hinweise || P.musikDna.energie || (P.musikDna.jahrzehnte && P.musikDna.jahrzehnte.length)))
|| !!(P.persoenlich && (P.persoenlich.tanzaffinitaet || P.persoenlich.international));
// Pflichtfelder-Check je Bereich - aus derselben Liste wie der Abschluss (PORTAL_PFLICHT_FELDER),
// nicht mehr aus einer eigenen, engeren Kopie. Damit sagt die Kachel "Pflichtfelder fehlen!" auch
// für Location, nicht nur für Persönliches - vorher konnte "Persönlich: Erledigt" stehen, während
// der Abschluss wegen fehlender Location-Pflichtfelder trotzdem blockierte.
// Vor "checks" berechnet (Vollaudit 09.09.2026, Befund HZP-11): checks.persoenlich braucht
// pflichtOffenTabs bereits als Quelle, siehe Kommentar dort.
var _fehlendJetzt = (typeof pflichtFehlend==='function') ? pflichtFehlend() : [];
var pflichtOffenTabs = {};
_fehlendJetzt.forEach(function(f){ pflichtOffenTabs[f.tab]=true; });
var checks = {
musikdna: _musikChanged >= 3 || _musikExtra,
songs: !!(P.songs && P.songs.muss && P.songs.muss.length >= 1),
ablauf: !!(P.ablauf && P.ablauf.some(function(x){return x.aktiv || x.uhrzeit || x.notiz;})),
location: _locationErledigt(),
// Vollaudit 09.09.2026, Befund HZP-11: die eigene, schwaechere Bedingung
// (gaeste/altersgruppe/weiteres - altersgruppe ist laut PORTAL_PFLICHT_FELDER
// gar kein Pflichtfeld) konnte "Erledigt" zeigen, obwohl echte Pflichtfelder
// wie per-braut/per-telefon noch leer waren - und "Erledigt" gewann in der
// Anzeige unten vor der Pflichtfeld-Warnung. Jetzt dieselbe Quelle wie die
// Warnung selbst: erledigt heisst, dass fuer diesen Bereich nichts mehr fehlt.
persoenlich:!pflichtOffenTabs['persoenlich'],
dokumente: !!(P.dokumente && P.dokumente.length > 0)
};
// "Bereiche insgesamt" für Ring/Balken zählt nur die fünf gestalterischen Bereiche - Dokumente
// ist ein optionaler Upload, kein Briefing-Bereich, und sollte nie verhindern, dass "fertig" auch
// wirklich 100% zeigt (Befund 4.8).
var keys = Object.keys(checks);
var ringKeys = keys.filter(function(k){ return k!=='dokumente'; });
var done = ringKeys.filter(function(k){return checks[k];}).length;
keys.forEach(function(k){
var el=document.getElementById('prog-'+k); if(!el) return;
var row=el.closest('.progress-row');
if(checks[k]){
el.textContent='Erledigt';el.className='pr-ok';
if(row) row.style.borderLeft='';
} else if(pflichtOffenTabs[k] && _bereichBesucht(k)){
el.textContent='Pflichtfelder fehlen!';el.className='pr-warn-hard';
if(row) row.style.cssText+='border-left:3px solid var(--warning);padding-left:8px;border-radius:0 var(--r) var(--r) 0';
} else {
el.textContent='Ausfüllen →';el.className='pr-no';
if(row) row.style.borderLeft='';
}
});
var bar=document.getElementById('prog-bar');
if(bar){
if(!bar._animated){
bar._animated=true;
// Erster Aufruf: kurz warten damit CSS-Transition sichtbar ist
setTimeout(function(){bar.style.width=Math.round(done/ringKeys.length*100)+'%';},350);
} else {
bar.style.width=Math.round(done/ringKeys.length*100)+'%';
}
}
var txt=document.getElementById('prog-text'); if(txt) txt.textContent=done+' von '+ringKeys.length+' Bereichen ausgefüllt'+(done===ringKeys.length?' 🎉':'');
// Tab-Badges aktualisieren
if(typeof checkTabReward==='function') checkTabReward(checks);
['persoenlich','songs','musikdna','ablauf','location'].forEach(function(k){
var b = document.getElementById('tbadge-'+k);
if(!b) return;
if(checks[k]){ b.textContent='✓'; b.classList.add('show'); }
else { b.classList.remove('show'); }
});
// Sichtbarkeit Kacheln nach jedem Update sicherstellen
if(typeof _updateProgSection==='function') _updateProgSection();
// CTA ausblenden wenn schon begonnen
var ctaEl=document.getElementById('start-cta');
if(ctaEl && done > 0) ctaEl.style.display='none';
// Ring animieren
var pct=Math.round(done/ringKeys.length*100);
var ring=document.getElementById('prog-ring');
if(ring){ var circ=201; ring.style.strokeDashoffset=circ-(circ*pct/100); }
var pctEl=document.getElementById('prog-pct'); if(pctEl) pctEl.textContent=pct+'%';
if(typeof window._bpmNextStepUpdate==='function') window._bpmNextStepUpdate();
}
// ── Autosave ───────────────────────────────────────────────────────────
var _asTimer=null, _saveInFlight=false, _savePendingSnap=null, _savePendingReason=null;
// Save-System: max. 1 aktiver Request, neuester Snap gewinnt
var _dataStats={sent:0,saves:0};
async function _executeSave(snap, reason){
// Offline-Check: kein Save-Versuch wenn keine Verbindung
if(!navigator.onLine){
var okEl=document.getElementById('save-ok');
if(okEl){
okEl.textContent='\u{1F4F6} Offline \u2014 wird gespeichert sobald Verbindung zur\u00fcck';
okEl.style.background='rgba(196,132,106,0.18)';
okEl.style.border='1px solid rgba(196,132,106,0.4)';
okEl.style.color='var(--warning)';
okEl.style.display='block';
}
window.addEventListener('online',function(){
if(okEl){
okEl.textContent='\u2713 Wiederverbunden \u2014 speichere...';
okEl.style.background='rgba(39,174,96,0.15)';
okEl.style.border='1px solid rgba(39,174,96,0.3)';
okEl.style.color='#27AE60';
}
savePortalNow('Wiederverbunden');
},{once:true});
_saveInFlight=false;
return;
}
_saveInFlight=true;
var okEl=document.getElementById('save-ok');
if(okEl){okEl.textContent='⏳ Speichert…';okEl.style.background='rgba(20,16,8,0.9)';okEl.style.border='1px solid rgba(200,168,75,0.3)';okEl.style.color='var(--gold)';okEl.style.display='block';}
// Befund HZP-02: bekannter Speicherstand geht mit, damit der Server einen
// Konflikt (zweites Geraet hat inzwischen gespeichert) erkennen kann.
var _payload=JSON.stringify({action:'save',aid:AID,token:TOKEN,portalDaten:snap,gespeichertAm:PORTAL_GESPEICHERT_AM});
_dataStats.sent+=_payload.length;_dataStats.saves++;
var _statEl=document.getElementById('data-stat');
if(_statEl)_statEl.textContent='↑ '+(_dataStats.sent/1024).toFixed(1)+' KB in '+_dataStats.saves+' Saves';
// Retry-Counter (pro snap) für exponential backoff
snap._retries = snap._retries || 0;
try{
// 15s Timeout via AbortController
var ctrl = new AbortController();
var to = setTimeout(function(){ ctrl.abort(); }, 15000);
var r=await fetch(window.location.href.split('?')[0],{
method:'POST',
headers:{'Content-Type':'application/json'},
body:_payload,
signal: ctrl.signal
});
clearTimeout(to);
// HTTP-Fehler (500, 502, 503, 504) → wie Netzwerkfehler behandeln
if(r.status >= 500 && r.status < 600){
throw new Error('Server '+r.status);
}
if(r.status === 401 || r.status === 403){
// Token-Problem * kein Retry sinnvoll
var d = {ok:false, error:'Sitzung abgelaufen * bitte neu laden'};
} else {
var d=await r.json().catch(function(){return {ok:false,error:'parse error'};});
}
// Tiefenaudit 01.09.2026, Befund 4.10: hier stand roher Servertext (z.B.
// "Forbidden" oder "HTTP 502") direkt in der fuer das Brautpaar sichtbaren
// Statuszeile - technisch korrekt, aber fuer einen Endkunden bedeutungslos
// ("darf ich hier nicht speichern?"). Kunden bekommen jetzt einen
// verstaendlichen Satz, der Rohtext steht weiterhin im Debug-Zaehler unten
// (#data-stat), falls der DJ beim Support mitlesen muss.
if(okEl){
okEl.textContent=d.ok?('✓ '+(reason||'Gespeichert')):'⚠ Speichern gerade nicht möglich · wir versuchen es weiter';
okEl.style.color=d.ok?'#27AE60':'var(--warning)';okEl.style.background=d.ok?'rgba(8,20,8,0.9)':'rgba(30,10,10,0.9)';okEl.style.border='1px solid '+(d.ok?'rgba(39,174,96,0.4)':'rgba(196,132,106,0.4)');setTimeout(function(){okEl.style.display='none';},3000);
if(!d.ok && _statEl) _statEl.textContent='⚠ '+(d.error||'Fehler').slice(0,60);
}
if(d.ok){
P=snap; window._lastSaveMs=Date.now(); updateLastActivity();
// Befund HZP-02: Baseline fuer die naechste Konflikt-Pruefung auf den
// frisch gesetzten Serverstand ziehen.
if(d.gespeichertAm) PORTAL_GESPEICHERT_AM = d.gespeichertAm;
lsBackupClear(); // Server hat bestätigt * lokales Backup nicht mehr nötig
var _stamp=document.getElementById('save-stamp');if(_stamp){var _n=new Date();var _activeTab=document.querySelector('.tab-btn.active')?.dataset?.tab||document.querySelector('[data-tab].active')?.dataset?.tab||'';var _tabLabels={overview:'Start',persoenlich:'Persönlich',songs:'Musik',musikdna:'Genres',location:'Planung',ablauf:'Ablauf',dokumente:'Dokumente',tipps:'Tipps'};var _tabLbl=_tabLabels[_activeTab]||'';_stamp.innerHTML='💾 '+(_tabLbl?_tabLbl+' · ':'')+'gespeichert · '+_n.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'});_stamp.style.display='block';
// Brief visual pulse to acknowledge save
_stamp.style.color='rgba(200,168,75,1)';
_stamp.style.borderColor='rgba(200,168,75,0.5)';
_stamp.style.transform='scale(1.05)';
setTimeout(function(){
_stamp.style.color='rgba(200,168,75,0.7)';
_stamp.style.borderColor='rgba(200,168,75,0.2)';
_stamp.style.transform='scale(1)';
}, 800);}
maybeShowOtherUser(d.serverTs);
// Location-Flag nach Save zurücksetzen (verhindert wiederholte Bestätigungs-Buttons im CRM)
window._locPortalAktualisiert=false;
var hs=document.getElementById('header-save-status');
if(hs){
var now=new Date();
var t=now.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'});
var dStr=now.toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit'});
hs.innerHTML='Gespeichert '+t;
hs.style.color='';
hs.style.display='inline-flex';
}
updateProgress();
renderOverviewSummary();
if(typeof checkAbschlussReady==='function') checkAbschlussReady();
// Befund 4.10: sobald wieder erfolgreich gespeichert wurde, braucht es
// den manuellen Notfall-Button nicht mehr.
var _fabOk=document.getElementById('bpm-fab'); if(_fabOk) _fabOk.classList.remove('show');
} else if(d.conflict && !window._hzpKonfliktGezeigt){
// Befund HZP-02: kein stilles Ueberschreiben mehr - das andere Geraet
// hat inzwischen gespeichert. Einmalig zeigen (Autosave würde den
// Konflikt sonst bei jedem Tick erneut melden, bis neu geladen wird).
window._hzpKonfliktGezeigt = true;
if(typeof openModal === 'function'){
openModal({
title:'💑 Dein Partner hat gespeichert',
body:'
Euer anderes Gerät hat inzwischen gespeichert. Damit nichts verloren geht, bitte die Seite neu laden, bevor ihr weitermacht.
',
actions:[
{label:'Jetzt neu laden', style:'primary', onClick:function(){ location.reload(); }}
]
});
}
}
} catch(e){
d={ok:false,error:String(e.message||e)};
snap._retries = (snap._retries||0) + 1;
if(snap._retries <= 4){
// Exponential backoff: 4s, 8s, 16s, 32s
var delay = 4000 * Math.pow(2, snap._retries - 1);
var delaySec = Math.round(delay/1000);
if(okEl){
okEl.textContent = '⚠ '+(e.name==='AbortError'?'Timeout':'Fehler')+' * Retry '+snap._retries+'/4 in '+delaySec+'s';
okEl.style.color='var(--warning)';
okEl.style.background='rgba(30,10,10,0.9)';
okEl.style.border='1px solid rgba(196,132,106,0.4)';
okEl.style.display='block';
}
clearTimeout(window._saveRetryTimer);
window._saveRetryTimer=setTimeout(function(){
if(okEl){okEl.textContent='↻ Versuche erneut…'; okEl.style.color='var(--gold)';}
_saveInFlight=false;
_executeSave(snap, reason);
}, delay);
} else {
// Aufgegeben * Brautpaar muss manuell speichern
if(okEl){
okEl.textContent='⚠ Speichern dauerhaft fehlgeschlagen * bitte manuell speichern';
okEl.style.color='var(--warning)';
}
// Lokales Backup ist trotzdem gesichert (lsBackupSnap wurde vor dem Versuch aufgerufen)
if(typeof showToast==='function') showToast('Verbindung scheint dauerhaft zu fehlen · eure Eingaben sind aber lokal gesichert');
// Befund 4.10: einzige Stelle, an der der FAB tatsaechlich noetig ist -
// Autosave hat aufgegeben, ein Klick ist die einzige verbleibende Aktion.
var _fab=document.getElementById('bpm-fab'); if(_fab) _fab.classList.add('show');
}
}
if(!window._saveRetryTimer||d.ok){_saveInFlight=false;}
// Falls während des Requests ein neuerer Snap eingetroffen ist → sofort senden
if(_savePendingSnap){
var ps=_savePendingSnap, pr=_savePendingReason;
_savePendingSnap=null; _savePendingReason=null;
_executeSave(ps, pr);
}
return d||{ok:false};
}
// ── Mehrbenutzer-Hinweis: dezenter Aktivitäts-Check ─────────────
window._lastKnownSave = null;
window._otherUserDetected = false;function maybeShowOtherUser(serverTs){
if(!serverTs) return;
var server = new Date(serverTs).getTime();
var ourLast = window._lastKnownSave ? new Date(window._lastKnownSave).getTime() : 0;
if(server > ourLast + 10000 && !window._otherUserDetected){
window._otherUserDetected = true;
// v17.311 (Nutzerwunsch): Hinweis abgeschaltet. Er erschien auch dann, wenn
// gar kein zweiter Mensch am Portal war — der Vergleich schlaegt schon an,
// wenn der Server einen Speicherstand zurueckmeldet, der mehr als zehn
// Sekunden nach dem eigenen liegt. Das passiert auch beim Wechsel zwischen
// Geraeten oder nach einer Pause im selben Browser.
// Die Erkennung selbst bleibt bestehen (window._otherUserDetected), nur die
// Einblendung entfaellt — so laesst sie sich spaeter wieder aktivieren.
// showToast('💑 Auch dein Partner war gerade aktiv');
setTimeout(function(){ window._otherUserDetected = false; }, 60000);
}
window._lastKnownSave = serverTs;
}
// ── Lokales Backup gegen Datenverlust (Crash/Tab-Close offline) ──
// Spiegelt jeden Snapshot lokal. Wird gelöscht sobald Server bestätigt.
// Sicherheitsaudit 09.09.2026, Befund HZP-18: window.AID war hier immer
// undefined - "const AID=..." (oben) erzeugt eine block-/skriptscoped
// Bindung, KEINE Eigenschaft von window (anders als "var"/Funktionsdeklara-
// tionen). Jede Anfrage bekam dadurch denselben Schluessel
// "bpm_portal_backup_unknown" - das lokale Crash-Backup einer Anfrage konnte
// so faelschlich als das einer anderen gelten. Bare AID (die tatsaechliche
// Bindung) ist hier im selben
MAXIMAL 99 SONGS
Technisch kann ich höchstens 99 Songs pro Playlist übernehmen - mehr lässt der Import in meine DJ-Software nicht zu.
Das reicht aber völlig: Die Playlist ist für mich eine grobe Richtung, um euren gemeinsamen Musikgeschmack kennenzulernen - keine Ablaufliste, die ich Song für Song durchspiele.
Songs die auf jeden Fall laufen sollen, tragt bitte einzeln oben unter „Must-Have-Songs" ein (max. 30) - die spiele ich garantiert.
Sucht am besten eure Favoriten heraus und legt dafür eine neue Playlist mit höchstens 99 Titeln an.
IMPRESSUM
Dieses Portal ist ein nicht-öffentliches Kundenportal und richtet sich ausschließlich an vertraglich gebundene Kunden von beats per micha. Es unterliegt keiner Impressumspflicht nach § 5 TMG, da es nicht öffentlich zugänglich ist.
Angaben gemäß § 5 TMG
beats per micha
Inhaber: Michael Zimmermann Am Waldhang 1 86551
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit:
ec.europa.eu/consumers/odr
Wir sind nicht bereit oder verpflichtet, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.
Haftungsausschluss
Die Inhalte dieses Portals wurden mit größtmöglicher Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte übernehmen wir keine Gewähr.
Portal v17.348 · 04.08.2026
DATENSCHUTZERKLÄRUNG
1. VERANTWORTLICHER›
beats per micha, Michael Zimmermann
Am Waldhang 1, 86551 E-Mail: dj@beats-per-micha.de
2. WELCHE DATEN WERDEN VERARBEITET?›
Dieses Portal verarbeitet ausschließlich Daten, die ihr freiwillig eingebt: Musikwünsche, Ablaufplanung, Location-Angaben sowie Vornamen und persönliche Angaben zur Hochzeit. Technisch werden außerdem Zugriffszeiten und Geräteinformationen (Browser, Geräteart) protokolliert, um den Dienst bereitzustellen.
3. RECHTSGRUNDLAGE (DSGVO)›
Die Verarbeitung erfolgt auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung) sowie Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an der technischen Bereitstellung des Dienstes).
4. SPEICHERUNG & HOSTING›
Eure Eingaben werden in einer Datenbank bei Supabase Inc. (EU-Region Frankfurt) gespeichert. Supabase ist als Auftragsverarbeiter gem. Art. 28 DSGVO vertraglich gebunden. Der Webserver wird bei IONOS SE, Deutschland, betrieben.
Hochgeladene Dateien (Profilfotos, Dokumente) werden bei Microsoft OneDrive (Microsoft Corporation) auf europäischen Servern gespeichert. Microsoft ist nach den EU-Standardvertragsklauseln und dem EU-US Data Privacy Framework als Auftragsverarbeiter gem. Art. 28 DSGVO eingebunden. Datenschutzerklärung Microsoft: privacy.microsoft.com.
Der Zugriff auf eure Portal-Daten ist durch einen einmaligen Zugangs-Token geschützt. Nur ihr (mit eurem persönlichen Link) und beats per micha haben Zugriff.
5. COOKIES & LOKALE SPEICHERUNG›
Dieses Portal verwendet keine Tracking-Cookies und kein Analyse-Werkzeug.
Technisch notwendig ist der localStorage eures Browsers für zwei Zwecke:
(1) Session-Zähler (zeigt euch beim ersten Aufruf einen Willkommens-Hinweis)
(2) Lokales Sicherheitsbackup: damit eure Eingaben bei Verbindungsabbruch oder Tab-Crash nicht verloren gehen, wird ein Snapshot lokal auf eurem Gerät zwischengespeichert. Sobald der Server die Speicherung bestätigt, wird der lokale Snapshot gelöscht. Maximale Aufbewahrungszeit: 7 Tage.
Zusätzlich merkt sich der sessionStorage eures Browsers, auf welchem Reiter ihr zuletzt wart, damit ihr nach einem Neuladen der Seite dort weitermacht. Dieser Wert wird gelöscht, sobald ihr den Browser-Tab schließt.
Es werden keine Daten an Werbenetzwerke übertragen. Eine Einwilligung nach § 25 TTDSG ist für technisch notwendige Speicherung nicht erforderlich.
6. WEITERGABE AN DRITTE›
Eine Weitergabe eurer personenbezogenen Daten an Dritte findet nicht statt, außer wenn dies zur Vertragserfüllung notwendig ist (z.B. Hostinganbieter als Auftragsverarbeiter) oder ihr ausdrücklich eingewilligt habt.
7. SPEICHERDAUER›
Eure Daten werden nach Abschluss des Events und Ablauf gesetzlicher Aufbewahrungsfristen (max. 10 Jahre für steuerrelevante Unterlagen) gelöscht. Auf euren ausdrücklichen Wunsch erfolgt eine frühere Löschung, sofern keine gesetzlichen Aufbewahrungspflichten entgegenstehen.
Das Portal wird automatisch 30 Tage nach eurem Hochzeitsdatum geschlossen. Die Daten bleiben aber für die genannten Aufbewahrungsfristen weiter im System gespeichert.
8. SICHERHEIT DER VERARBEITUNG›
beats per micha trifft technisch und organisatorisch geeignete Maßnahmen nach Art. 32 DSGVO:
* Verschlüsselte Übertragung aller Daten via HTTPS/TLS
* Token-basierter Zugang: euer Portal ist nur über euren persönlichen Link erreichbar
* Content-Security-Policy: Schutz gegen Cross-Site-Scripting (XSS) und Datenabfluss
* EU-Hosting: keine Übertragung in unsichere Drittländer ohne Garantien
* Zugriffsbeschränkung: nur autorisierte Auftragsverarbeiter haben Zugang
9. EXTERNE DIENSTE & DRITTANBIETER›
Dieses Portal nutzt folgende externe Dienste, die technisch notwendig sind:
Google Fonts (Google LLC)
Zur Darstellung der Schriften werden Fonts von Google Fonts geladen. Dabei wird eure IP-Adresse an Server von Google LLC, USA, übertragen. Google Fonts ist eingebunden über eine direkte Verbindung zu fonts.googleapis.com. Datenschutzerklärung Google: policies.google.com/privacy. Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO.
Deezer (Deezer SA)
Für Musik-Vorschauen der Genre-Auswahl werden Audiodaten über die Deezer-API (api.deezer.com) abgerufen. Dies erfolgt nur wenn ihr die Vorschau aktiv nutzt. Deezer SA, Frankreich. Datenschutz: deezer.com/legal/personal-datas. Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO.
Open-Meteo
Für die Wettervorschau (ab 2 Wochen vor dem Event) wird die Open-Meteo-API genutzt. Open-Meteo ist ein Open-Source-Wetterdienst ohne Tracking. Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO.
OpenStreetMap / Nominatim
Für die Locations-Suche wird die Nominatim-API von OpenStreetMap genutzt. Dabei wird eure Suchanfrage (Ortsname) übertragen. Betreiber: OpenStreetMap Foundation, UK. Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO.
Telegram (Telegram Messenger)
Interne Benachrichtigungen an den DJ werden über die Telegram-API übertragen. Übermittelt werden ausschließlich interne Status-Informationen (z.B. „Portal wurde ausgefüllt") ohne personenbezogene Daten des Brautpaares. Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO.
10. EURE RECHTE (DSGVO ART. 15-22)›
Ihr habt jederzeit das Recht auf:
* Auskunft (Art. 15) · welche Daten von euch gespeichert sind
* Berichtigung (Art. 16) * Korrektur falscher Daten
* Löschung (Art. 17) * sofern keine Aufbewahrungspflicht entgegensteht
* Einschränkung (Art. 18) * vorübergehende Sperrung der Verarbeitung
* Datenübertragbarkeit (Art. 20) · Export eurer Daten in einem strukturierten Format
* Widerspruch (Art. 21) * gegen die Verarbeitung
So nutzt ihr eure Rechte:
Im Portal könnt ihr direkt:
(a) Eure Eingaben jederzeit ändern oder einzelne Einträge löschen
(b) Ein PDF-Briefing aller eurer Daten herunterladen (Glückwunsch-Modal nach Portal-Abschluss · erfüllt Art. 20)
(c) Einen Daten-Export als JSON anfordern Daten-Export starten
Für komplette Löschung oder Auskunft an Dritte: formlose E-Mail an dj@beats-per-micha.de.