Changeset b7d8a4d for Wedding_Planner.html
- Timestamp:
- 06/07/26 21:34:27 (5 weeks ago)
- Branches:
- main
- Children:
- 679113b
- Parents:
- 3679996
- git-author:
- Marija Taseska <108740625+marijataseska@…> (06/07/26 21:34:27)
- git-committer:
- GitHub <noreply@…> (06/07/26 21:34:27)
- File:
-
- 1 edited
-
Wedding_Planner.html (modified) (9 diffs)
Legend:
- Unmodified
- Added
- Removed
-
Wedding_Planner.html
r3679996 rb7d8a4d 324 324 let CURRENT_USER = null; 325 325 let CURRENT_WEDDING = null; 326 let WEDDINGS = []; 326 327 let EVENTS = []; 327 328 let GUESTS = []; … … 337 338 let REGISTRAR_BOOKINGS = []; 338 339 let ATTENDANCE = []; 340 let RSVPS = []; 339 341 340 342 // ============================================================= … … 370 372 if (wRes && wRes.ok) { 371 373 const weddings = await wRes.json(); 374 WEDDINGS = weddings; 372 375 CURRENT_WEDDING = weddings[0] || null; 373 376 } … … 405 408 } 406 409 410 // Load RSVPs for this wedding (admin view) 411 if (wid) { 412 const rsvpR = await api('GET', `/api/weddings/${wid}/rsvp`); 413 if (rsvpR && rsvpR.ok) RSVPS = await rsvpR.json(); 414 } 415 407 416 const [venR, banR, phR, chR, regR] = await Promise.all([ 408 417 api('GET', '/api/venues'), … … 424 433 async function reload() { 425 434 if (!CURRENT_WEDDING) return; 435 // Refresh list of weddings first (in case user created/removed/switches) 436 try { 437 const wListR = await api('GET', '/api/weddings'); 438 if (wListR && wListR.ok) { 439 WEDDINGS = await wListR.json(); 440 // keep CURRENT_WEDDING reference in sync 441 if (CURRENT_WEDDING) { 442 const found = WEDDINGS.find(w => String(w.wedding_id) === String(CURRENT_WEDDING.wedding_id)); 443 if (found) CURRENT_WEDDING = found; 444 } 445 } 446 } catch (e) { /* ignore */ } 447 426 448 const wid = CURRENT_WEDDING.wedding_id; 427 449 const [evR, guR, vbR, pbR, bbR, rbR, cbR, attR] = await Promise.all([ … … 443 465 if (cbR && cbR.ok) CHURCH_BOOKINGS = await cbR.json(); 444 466 if (attR && attR.ok) ATTENDANCE = await attR.json(); 467 // Refresh RSVP list as well 468 const rsvpR = await api('GET', `/api/weddings/${wid}/rsvp`); 469 if (rsvpR && rsvpR.ok) RSVPS = await rsvpR.json(); 445 470 renderApp(); 446 471 } … … 473 498 a.classList.toggle('active', page ? `/${page}` === ctx : false); 474 499 }); 500 // Render wedding selector in the topbar actions so user can switch between weddings 501 try { 502 const topbarActions = document.querySelector('.topbar .topbar-actions'); 503 // remove old selector if present 504 const oldSel = document.getElementById('weddingSelect'); 505 if (oldSel) oldSel.remove(); 506 const oldBtn = document.getElementById('btnNewWedding'); 507 if (oldBtn) oldBtn.remove(); 508 509 // Create selector 510 const sel = document.createElement('select'); 511 sel.id = 'weddingSelect'; 512 sel.style.minWidth = '200px'; 513 sel.style.padding = '6px 8px'; 514 sel.style.borderRadius = '8px'; 515 sel.style.border = '1px solid var(--border)'; 516 sel.style.background = '#fff'; 517 sel.style.marginRight = '8px'; 518 const noneOpt = document.createElement('option'); 519 noneOpt.value = ''; 520 noneOpt.textContent = '— Select Wedding —'; 521 sel.appendChild(noneOpt); 522 WEDDINGS.forEach(w => { 523 const o = document.createElement('option'); 524 o.value = w.wedding_id; 525 o.textContent = `${w.date} ${w.notes?'- '+(w.notes||'') : ''}`; 526 if (CURRENT_WEDDING && String(CURRENT_WEDDING.wedding_id) === String(w.wedding_id)) o.selected = true; 527 sel.appendChild(o); 528 }); 529 sel.addEventListener('change', async (e) => { 530 const val = e.target.value; 531 if (!val) return; 532 const found = WEDDINGS.find(x => String(x.wedding_id) === String(val)); 533 if (found) { 534 CURRENT_WEDDING = found; 535 await reload(); 536 renderApp(); 537 } 538 }); 539 topbarActions.insertBefore(sel, topbarActions.firstChild); 540 541 // New wedding button 542 const newBtn = document.createElement('button'); 543 newBtn.id = 'btnNewWedding'; 544 newBtn.className = 'btn btn-sm'; 545 newBtn.style.marginRight = '8px'; 546 newBtn.textContent = '+ Wedding'; 547 newBtn.addEventListener('click', openCreateWeddingModal); 548 topbarActions.insertBefore(newBtn, topbarActions.firstChild); 549 } catch (e) { /* ignore UI insertion errors */ } 475 550 const content = document.getElementById('page-content'); 476 551 content.innerHTML = ''; … … 893 968 if(!CURRENT_WEDDING){ renderNoWedding(container); return; } 894 969 const wrap = document.createElement('div'); 895 wrap.innerHTML = `<div class="card"><div class="card-header"><h3>Guest List</h3><div class="card-actions"><button class="btn btn-primary" id="btnAddGuest">+ Add Guest</button>< /div></div><div id="guestTableContainer"></div></div>`;970 wrap.innerHTML = `<div class="card"><div class="card-header"><h3>Guest List</h3><div class="card-actions"><button class="btn btn-primary" id="btnAddGuest">+ Add Guest</button><button class="btn" id="btnUploadGuests">Upload CSV</button></div></div><div id="guestTableContainer"></div></div>`; 896 971 container.appendChild(wrap); 897 972 wrap.querySelector('#btnAddGuest').addEventListener('click', openAddGuestModal); 898 899 const guestRsvpMap = {}; 900 if(EVENTS.length > 0){ 901 const firstEvent = EVENTS[0]; 902 // RSVP mapping removed for revert 973 wrap.querySelector('#btnUploadGuests').addEventListener('click', () => { 974 if(!CURRENT_WEDDING){ showToast('Select or create a wedding first.','danger'); return; } 975 const input = document.createElement('input'); 976 input.type = 'file'; 977 input.accept = '.csv,text/csv'; 978 input.addEventListener('change', async (e) => { 979 const file = e.target.files && e.target.files[0]; 980 if(!file) return; 981 const text = await file.text(); 982 const rows = parseCSV(text); 983 if(rows.length === 0){ showToast('No guests found in CSV.','danger'); return; } 984 985 const previewRows = rows.slice(0,200).map(r=>`<tr><td>${escapeHtml(r.first_name||'')}</td><td>${escapeHtml(r.last_name||'')}</td><td>${escapeHtml(r.email||'')}</td><td>${escapeHtml(r.role||'')}</td></tr>`).join(''); 986 const body = `<div style="max-height:320px;overflow:auto;"><table style="width:100%;border-collapse:collapse;"><thead><tr><th style="text-align:left;padding:6px">First</th><th style="text-align:left;padding:6px">Last</th><th style="text-align:left;padding:6px">Email</th><th style="text-align:left;padding:6px">Role</th></tr></thead><tbody>${previewRows}</tbody></table></div><div style="margin-top:12px;"><label><input type="checkbox" id="csvSendEmails"/> Send invitation emails</label></div><p class="muted" style="margin-top:10px;">${rows.length} guests parsed. Showing first ${Math.min(rows.length,200)} rows.</p>`; 987 988 showModal('Import Guests from CSV', body, async () => { 989 const sendEmails = document.getElementById('csvSendEmails').checked; 990 const res = await api('POST', `/api/weddings/${CURRENT_WEDDING.wedding_id}/guests/bulk`, { guests: rows, sendEmails }); 991 if(res && res.ok){ const d = await res.json(); closeModal(); showToast(`${d.inserted} guests imported.`,'success'); await reload(); } 992 else { const d = await res.json().catch(()=>null); showToast((d&&d.error) || 'Error importing guests.','danger'); } 993 }); 994 }); 995 input.click(); 996 }); 997 998 // Build table after ensuring we have up-to-date RSVP data for this wedding 999 (async () => { 1000 // Fetch fresh RSVPs for this wedding to ensure the UI reflects guest-submitted responses 1001 try { 1002 const r = await api('GET', `/api/weddings/${CURRENT_WEDDING.wedding_id}/rsvp`); 1003 if (r && r.ok) RSVPS = await r.json(); 1004 } catch (e) { /* ignore, we'll use cached RSVPS if available */ } 1005 1006 const guestRsvpMap = {}; 1007 if (EVENTS.length > 0) { 1008 const firstEvent = EVENTS[0]; 1009 RSVPS.forEach(rv => { 1010 if (String(rv.event_id) === String(firstEvent.event_id)) guestRsvpMap[String(rv.guest_id)] = rv.status; 1011 }); 1012 } 1013 1014 buildDataTable({ 1015 container: wrap.querySelector('#guestTableContainer'), 1016 columns: [ 1017 { header:'First Name', accessor:'first_name' }, 1018 { header:'Last Name', accessor:'last_name' }, 1019 { header:'Email', accessor:'email' }, 1020 { header:'Status', accessor:'status', render: v => { 1021 const color = v === 'accepted' ? 'badge-success' : v === 'declined' ? 'badge-danger' : v === 'invited' ? 'badge-warn' : 'badge-purple'; 1022 return `<span class="badge ${color}">${v || 'pending'}</span>`; 1023 }}, 1024 { header:'Invite', accessor:'email', sortable:false, render: (v,row) => 1025 v ? `<span class="badge badge-success">✉ Invite sent</span>` : `<span class="badge badge-warn">No email</span>` 1026 } 1027 ], 1028 rows: GUESTS.map(g=>({ id:g.guest_id, first_name:g.first_name, last_name:g.last_name, email:g.email||'', status: guestRsvpMap[String(g.guest_id)] || 'pending' })), 1029 onEdit: row => editGuestModal(row.id), 1030 onDelete: async row => { 1031 const res = await api('DELETE',`/api/guests/${row.id}`); 1032 if(res && res.ok){ showToast('Guest removed.',''); await reload(); } 1033 else showToast('Could not remove guest.','danger'); 1034 } 1035 }); 1036 })(); 1037 } 1038 1039 // CSV helpers for guest import 1040 function splitCSVLine(line){ 1041 const out = []; 1042 let cur = ''; 1043 let inQuotes = false; 1044 for(let i=0;i<line.length;i++){ 1045 const ch = line[i]; 1046 if(ch === '"'){ 1047 if(inQuotes && line[i+1] === '"'){ cur += '"'; i++; } 1048 else inQuotes = !inQuotes; 1049 } else if(ch === ',' && !inQuotes){ out.push(cur); cur = ''; } 1050 else cur += ch; 903 1051 } 904 905 buildDataTable({ 906 container: wrap.querySelector('#guestTableContainer'), 907 columns: [ 908 { header:'First Name', accessor:'first_name' }, 909 { header:'Last Name', accessor:'last_name' }, 910 { header:'Email', accessor:'email' }, 911 { header:'Status', accessor:'status', render: v => { 912 const color = v === 'accepted' ? 'badge-success' : v === 'declined' ? 'badge-danger' : v === 'invited' ? 'badge-warn' : 'badge-purple'; 913 return `<span class="badge ${color}">${v || 'pending'}</span>`; 914 }}, 915 { header:'Invite', accessor:'email', sortable:false, render: (v,row) => 916 v ? `<span class="badge badge-success">✉ Invite sent</span>` : `<span class="badge badge-warn">No email</span>` 917 } 918 ], 919 rows: GUESTS.map(g=>({ id:g.guest_id, first_name:g.first_name, last_name:g.last_name, email:g.email||'', status: 'pending' })), 920 onEdit: row => editGuestModal(row.id), 921 onDelete: async row => { 922 const res = await api('DELETE',`/api/guests/${row.id}`); 923 if(res && res.ok){ showToast('Guest removed.',''); await reload(); } 924 else showToast('Could not remove guest.','danger'); 925 } 926 }); 927 } 1052 out.push(cur); 1053 return out.map(s=>s.trim()); 1054 } 1055 1056 function parseCSV(text){ 1057 if(!text) return []; 1058 const lines = text.split(/\r?\n/).map(l=>l.trim()).filter(l=>l.length>0); 1059 if(lines.length === 0) return []; 1060 const first = splitCSVLine(lines[0]).map(h=>h.toLowerCase()); 1061 const hasHeader = first.some(h=>['first','first_name','first name','last','last_name','email','role'].includes(h)); 1062 const rows = []; 1063 const start = hasHeader ? 1 : 0; 1064 let headers = first; 1065 if(!hasHeader) headers = ['first_name','last_name','email','role']; 1066 for(let i=start;i<lines.length;i++){ 1067 const cols = splitCSVLine(lines[i]); 1068 const obj = {}; 1069 for(let j=0;j<headers.length;j++){ 1070 const key = headers[j] ? headers[j].toLowerCase().replace(/ /g,'_') : `col${j}`; 1071 obj[key] = cols[j] ? cols[j].trim().replace(/^"|"$/g,'') : ''; 1072 } 1073 const out = { 1074 first_name: obj.first_name || obj.first || '', 1075 last_name: obj.last_name || obj.last || '', 1076 email: obj.email || '', 1077 role: obj.role || '' 1078 }; 1079 if(out.first_name || out.last_name) rows.push(out); 1080 } 1081 return rows; 1082 } 1083 1084 function escapeHtml(s){ return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); } 928 1085 929 1086 function openAddGuestModal(){ … … 1568 1725 // UTILITY 1569 1726 // ============================================================= 1727 // When the window gains focus, refresh wedding-specific data so changes submitted via RSVP links 1728 // (which happen outside this UI) are reflected in both Guests and Seating pages. 1729 window.addEventListener('focus', () => { if (CURRENT_WEDDING) reload(); }); 1730 1570 1731 function focusSearch(){ 1571 1732 const s = document.querySelector('.search-input');
Note:
See TracChangeset
for help on using the changeset viewer.
