Changeset 5b2dd1d for Wedding_Planner.html
- Timestamp:
- 06/21/26 20:23:47 (3 weeks ago)
- Branches:
- main
- Children:
- 768ae81
- Parents:
- 5025f0f
- git-author:
- Marija Taseska <108740625+marijataseska@…> (06/21/26 20:23:47)
- git-committer:
- GitHub <noreply@…> (06/21/26 20:23:47)
- File:
-
- 1 edited
-
Wedding_Planner.html (modified) (15 diffs)
Legend:
- Unmodified
- Added
- Removed
-
Wedding_Planner.html
r5025f0f r5b2dd1d 339 339 let ATTENDANCE = []; 340 340 let RSVPS = []; 341 let BUDGET_STATUS = null; // { predefined_budget, total_cost, exceeded, over_by, ... } 342 343 // ============================================================= 344 // COUPLE NAMES (local-only, used for invitation emails) 345 // Stored in localStorage — never sent to/stored in the database. 346 // ============================================================= 347 const COUPLE_NAMES_KEY = 'wp_couple_names'; 348 function getCoupleNames(){ 349 try { 350 const raw = localStorage.getItem(COUPLE_NAMES_KEY); 351 if(!raw) return { bride_name:'', groom_name:'' }; 352 const parsed = JSON.parse(raw); 353 return { bride_name: parsed.bride_name || '', groom_name: parsed.groom_name || '' }; 354 } catch(e){ return { bride_name:'', groom_name:'' }; } 355 } 356 function saveCoupleNames(bride_name, groom_name){ 357 localStorage.setItem(COUPLE_NAMES_KEY, JSON.stringify({ bride_name, groom_name })); 358 } 359 360 // Show alert + toast when a booking pushes total spending over the predefined budget 361 function showBudgetAlert(budget) { 362 if (!budget || !budget.exceeded) return; 363 const over = Number(budget.over_by).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); 364 const spent = Number(budget.total_cost).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); 365 const predefined = Number(budget.predefined_budget).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); 366 const msg = `⚠️ Budget Exceeded!\n\nPredefined budget: $${predefined}\nSpent so far: $${spent}\nOver by: $${over}`; 367 showToast(`Budget exceeded by $${over}`, 'danger'); 368 alert(msg); 369 } 370 371 // ============================================================= 372 // BUDGET CALCULATION (runs entirely from already-loaded data) 373 // Venue : guests × price_per_guest (per confirmed booking) 374 // Band : hours × price_per_hour (booking_cost from server) 375 // Photo : hours × price_per_hour (booking_cost from server) 376 // ============================================================= 377 function calcBudgetSpent() { 378 const guestCount = GUESTS.length; 379 380 const venueCost = VENUE_BOOKINGS.reduce((sum, b) => { 381 const venue = VENUES.find(v => String(v.venue_id) === String(b.venue_id)); 382 const ppg = venue ? Number(venue.price_per_guest || 0) : 0; 383 return sum + ppg * guestCount; 384 }, 0); 385 386 // Calculate hours from time strings "HH:MM" or "HH:MM:SS" 387 const toHours = (start, end) => { 388 const toMin = t => { const p = String(t).split(':').map(Number); return p[0]*60+(p[1]||0); }; 389 return Math.max(0, (toMin(end) - toMin(start)) / 60); 390 }; 391 392 const bandCost = BAND_BOOKINGS.reduce((sum, b) => { 393 const hours = b.hours ? Number(b.hours) : toHours(b.start_time, b.end_time); 394 return sum + hours * Number(b.price_per_hour || 0); 395 }, 0); 396 397 const photoCost = PHOTOGRAPHER_BOOKINGS.reduce((sum, b) => { 398 const hours = b.hours ? Number(b.hours) : toHours(b.start_time, b.end_time); 399 return sum + hours * Number(b.price_per_hour || 0); 400 }, 0); 401 402 const total = venueCost + bandCost + photoCost; 403 const predefined = CURRENT_WEDDING ? Number(CURRENT_WEDDING.budget || 0) : 0; 404 const exceeded = predefined > 0 && total > predefined; 405 406 return { venueCost, bandCost, photoCost, total, predefined, exceeded, over_by: exceeded ? +(total - predefined).toFixed(2) : 0 }; 407 } 408 409 // Check and show alert if budget is exceeded — call after any operation that changes cost/guests 410 function checkBudgetAndAlert() { 411 if (!CURRENT_WEDDING || !CURRENT_WEDDING.budget) return; 412 const s = calcBudgetSpent(); 413 if (s.exceeded) { 414 const over = Number(s.over_by).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); 415 const spent = Number(s.total).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); 416 const predefined = Number(s.predefined).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); 417 showToast(`⚠️ Budget exceeded by $${over}`, 'danger'); 418 alert(`⚠️ Budget Exceeded!\n\nPredefined budget: $${predefined}\nSpent so far: $${spent}\nOver by: $${over}\n\nBreakdown:\n Venue: $${Number(s.venueCost).toLocaleString()}\n Band: $${Number(s.bandCost).toLocaleString()}\n Photographer: $${Number(s.photoCost).toLocaleString()}`); 419 } 420 } 341 421 342 422 // ============================================================= … … 406 486 if (cbR && cbR.ok) CHURCH_BOOKINGS = await cbR.json(); 407 487 if (attR && attR.ok) ATTENDANCE = await attR.json(); 488 // Load budget status (predefined vs spent) 489 const budR = await api('GET', `/api/weddings/${wid}/budget`); 490 if (budR && budR.ok) BUDGET_STATUS = await budR.json(); 408 491 } 409 492 … … 468 551 const rsvpR = await api('GET', `/api/weddings/${wid}/rsvp`); 469 552 if (rsvpR && rsvpR.ok) RSVPS = await rsvpR.json(); 553 // Refresh budget status 554 const budR = await api('GET', `/api/weddings/${wid}/budget`); 555 if (budR && budR.ok) BUDGET_STATUS = await budR.json(); 470 556 renderApp(); 471 557 } … … 806 892 <div class="stat-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M12 2l3 7H21l-5.5 4 2 7L12 16l-5.5 4 2-7L3 9h6z"/></svg></div> 807 893 <div class="stat-label">Budget</div> 808 <div class="stat-value">$ ${w.budget ?(w.budget/1000).toFixed(0)+'k' : '—'}</div>809 <div class="stat-sub"> Totalbudget</div>894 <div class="stat-value">${w.budget ? '$'+(w.budget/1000).toFixed(0)+'k' : '—'}</div> 895 <div class="stat-sub">Predefined budget</div> 810 896 </div> 811 897 <div class="stat-card stat-green"> … … 835 921 <div class="info-row"><span class="info-label">Date</span><span class="info-val">${w.date}</span></div> 836 922 <div class="info-row"><span class="info-label">Budget</span><span class="info-val">${w.budget ? '$'+Number(w.budget).toLocaleString() : 'Not set'}</span></div> 923 ${(() => { 924 const s = calcBudgetSpent(); 925 if (!s.predefined && !s.total) return ''; 926 const pct = s.predefined > 0 ? Math.min((s.total / s.predefined) * 100, 100) : 0; 927 const spentFmt = '$' + Number(s.total).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); 928 const barColor = s.exceeded ? 'var(--rose)' : pct > 80 ? '#f59e0b' : '#22c55e'; 929 return `<div class="info-row" style="flex-direction:column;align-items:flex-start;gap:6px;padding-bottom:4px;"> 930 <div style="display:flex;justify-content:space-between;width:100%;"> 931 <span class="info-label">Budget Spent</span> 932 <span class="info-val" style="color:${barColor};font-weight:700;">${spentFmt}${s.exceeded ? ' ⚠️ Over!' : ''}</span> 933 </div> 934 ${s.predefined > 0 ? `<div style="width:100%;background:#e5e7eb;border-radius:999px;height:7px;overflow:hidden;"> 935 <div style="height:100%;width:${pct.toFixed(1)}%;background:${barColor};border-radius:999px;"></div> 936 </div> 937 <div style="font-size:11px;color:var(--text-mid);"> 938 ${pct.toFixed(0)}% of $${Number(s.predefined).toLocaleString()} used 939 ${s.exceeded ? ' — over by $'+Number(s.over_by).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2}) : ''} 940 · Venue $${Number(s.venueCost).toLocaleString()} · Band $${Number(s.bandCost).toLocaleString()} · Photo $${Number(s.photoCost).toLocaleString()} 941 </div>` : ''} 942 </div>`; 943 })()} 837 944 <div class="info-row"><span class="info-label">Notes</span><span class="info-val" style="font-size:12px;color:var(--text-mid);">${w.notes || '—'}</span></div> 838 945 <div class="info-row"><span class="info-label">Photographers</span><span class="info-val">${PHOTOGRAPHER_BOOKINGS.length} booked</span></div> … … 988 1095 showModal('Import Guests from CSV', body, async () => { 989 1096 const sendEmails = document.getElementById('csvSendEmails').checked; 990 const res = await api('POST', `/api/weddings/${CURRENT_WEDDING.wedding_id}/guests/bulk`, { guests: rows, sendEmails }); 1097 const couple = getCoupleNames(); 1098 const res = await api('POST', `/api/weddings/${CURRENT_WEDDING.wedding_id}/guests/bulk`, { guests: rows, sendEmails, bride_name: couple.bride_name, groom_name: couple.groom_name }); 991 1099 if(res && res.ok){ const d = await res.json(); closeModal(); showToast(`${d.inserted} guests imported.`,'success'); await reload(); } 992 1100 else { const d = await res.json().catch(()=>null); showToast((d&&d.error) || 'Error importing guests.','danger'); } … … 1095 1203 const first = document.getElementById('gFirst').value.trim(); 1096 1204 if(!first){ showToast('First name is required.','danger'); return; } 1205 const couple = getCoupleNames(); 1097 1206 const res = await api('POST',`/api/weddings/${CURRENT_WEDDING.wedding_id}/guests`,{ 1098 1207 first_name: first, 1099 1208 last_name: document.getElementById('gLast').value.trim(), 1100 1209 email: document.getElementById('gEmail').value.trim(), 1101 role: document.getElementById('gRole').value 1210 role: document.getElementById('gRole').value, 1211 bride_name: couple.bride_name, 1212 groom_name: couple.groom_name 1102 1213 }); 1103 1214 if(res && res.ok){ … … 1106 1217 showToast(d.emailSent ? 'Guest added & invitation email sent! 📧' : 'Guest added (no email provided).','success'); 1107 1218 await reload(); 1219 checkBudgetAndAlert(); 1108 1220 } else showToast('Could not add guest.','danger'); 1109 1221 } … … 1279 1391 }); 1280 1392 if(res && res.ok){ closeModal(); showToast('Guest assigned to table!','success'); await reload(); } 1281 else { const d=await res.json(); showToast(d.error||'Error assigning guest.','danger'); } 1393 else { 1394 const d=await res.json(); 1395 const msg = d.error||'Error assigning guest.'; 1396 showToast(msg,'danger'); 1397 alert(msg); 1398 } 1282 1399 } 1283 1400 ); … … 1310 1427 }); 1311 1428 if(res && res.ok){ closeModal(); showToast('Seating updated!','success'); await reload(); } 1312 else showToast('Error updating seating.','danger'); 1429 else { 1430 const d=await res.json().catch(()=>({})); 1431 const msg = d.error||'Error updating seating.'; 1432 showToast(msg,'danger'); 1433 alert(msg); 1434 } 1313 1435 } 1314 1436 ); … … 1384 1506 status: 'confirmed' 1385 1507 }); 1386 if(res && res.ok){ closeModal(); showToast('Venue booked!','success'); await reload(); } 1508 if(res && res.ok){ 1509 closeModal(); 1510 showToast('Venue booked!','success'); 1511 await reload(); 1512 checkBudgetAndAlert(); 1513 } 1387 1514 else { const d=await res.json(); showToast(d.error||'Error booking venue.','danger'); } 1388 1515 } … … 1452 1579 status: 'confirmed' 1453 1580 }); 1454 if(res && res.ok){ closeModal(); showToast('Band booked!','success'); await reload(); } 1581 if(res && res.ok){ 1582 closeModal(); 1583 showToast('Band booked!','success'); 1584 await reload(); 1585 checkBudgetAndAlert(); 1586 } 1455 1587 else { const d=await res.json(); showToast(d.error||'Error booking band.','danger'); } 1456 1588 } … … 1520 1652 status: 'confirmed' 1521 1653 }); 1522 if(res && res.ok){ closeModal(); showToast('Photographer booked!','success'); await reload(); } 1654 if(res && res.ok){ 1655 closeModal(); 1656 showToast('Photographer booked!','success'); 1657 await reload(); 1658 checkBudgetAndAlert(); 1659 } 1523 1660 else { const d=await res.json(); showToast(d.error||'Error booking photographer.','danger'); } 1524 1661 } … … 1671 1808 function renderSettingsPage(container){ 1672 1809 const u = CURRENT_USER; 1810 const couple = getCoupleNames(); 1673 1811 const wrap = document.createElement('div'); 1674 1812 wrap.innerHTML = ` … … 1702 1840 <button class="btn btn-ghost" onclick="renderApp()">Reset</button> 1703 1841 </div> 1842 </div> 1843 <div class="card" style="margin-top:18px;"> 1844 <div class="card-header"><h3>💍 Invitation Names</h3></div> 1845 <div class="gold-rule"></div> 1846 <p class="muted" style="margin-bottom:16px;">These names are used on guest invitations </p> 1847 <div class="form-grid"> 1848 <div class="form-group"><label class="form-label">Bride's Name</label><input id="sBrideName" placeholder="e.g. Maria" value="${escapeHtml(couple.bride_name)}"/></div> 1849 <div class="form-group"><label class="form-label">Groom's Name</label><input id="sGroomName" placeholder="e.g. John" value="${escapeHtml(couple.groom_name)}"/></div> 1850 </div> 1851 <div style="display:flex;gap:10px;margin-top:20px;"> 1852 <button class="btn btn-primary" id="btnSaveCoupleNames">Save Names</button> 1853 </div> 1704 1854 </div>`; 1705 1855 container.appendChild(wrap); 1856 1857 wrap.querySelector('#btnSaveCoupleNames').addEventListener('click', () => { 1858 const bride_name = document.getElementById('sBrideName').value.trim(); 1859 const groom_name = document.getElementById('sGroomName').value.trim(); 1860 saveCoupleNames(bride_name, groom_name); 1861 showToast('Invitation names saved on this device!','success'); 1862 }); 1706 1863 1707 1864 wrap.querySelector('#btnSaveProfile').addEventListener('click', async () => {
Note:
See TracChangeset
for help on using the changeset viewer.
