Ignore:
Timestamp:
06/21/26 20:23:47 (3 weeks ago)
Author:
GitHub <noreply@…>
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)
Message:

Add files via upload

File:
1 edited

Legend:

Unmodified
Added
Removed
  • Wedding_Planner.html

    r5025f0f r5b2dd1d  
    339339let ATTENDANCE     = [];
    340340let RSVPS = [];
     341let 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// =============================================================
     347const COUPLE_NAMES_KEY = 'wp_couple_names';
     348function 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}
     356function 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
     361function 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// =============================================================
     377function 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
     410function 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}
    341421
    342422// =============================================================
     
    406486        if (cbR && cbR.ok) CHURCH_BOOKINGS = await cbR.json();
    407487        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();
    408491    }
    409492
     
    468551    const rsvpR = await api('GET', `/api/weddings/${wid}/rsvp`);
    469552    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();
    470556    renderApp();
    471557}
     
    806892            <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>
    807893            <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">Total budget</div>
     894            <div class="stat-value">${w.budget ? '$'+(w.budget/1000).toFixed(0)+'k' : '—'}</div>
     895            <div class="stat-sub">Predefined budget</div>
    810896        </div>
    811897        <div class="stat-card stat-green">
     
    835921                <div class="info-row"><span class="info-label">Date</span><span class="info-val">${w.date}</span></div>
    836922                <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                            &nbsp;·&nbsp; Venue $${Number(s.venueCost).toLocaleString()} · Band $${Number(s.bandCost).toLocaleString()} · Photo $${Number(s.photoCost).toLocaleString()}
     941                        </div>` : ''}
     942                    </div>`;
     943                })()}
    837944                <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>
    838945                <div class="info-row"><span class="info-label">Photographers</span><span class="info-val">${PHOTOGRAPHER_BOOKINGS.length} booked</span></div>
     
    9881095            showModal('Import Guests from CSV', body, async () => {
    9891096                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 });
    9911099                if(res && res.ok){ const d = await res.json(); closeModal(); showToast(`${d.inserted} guests imported.`,'success'); await reload(); }
    9921100                else { const d = await res.json().catch(()=>null); showToast((d&&d.error) || 'Error importing guests.','danger'); }
     
    10951203            const first = document.getElementById('gFirst').value.trim();
    10961204            if(!first){ showToast('First name is required.','danger'); return; }
     1205            const couple = getCoupleNames();
    10971206            const res = await api('POST',`/api/weddings/${CURRENT_WEDDING.wedding_id}/guests`,{
    10981207                first_name: first,
    10991208                last_name: document.getElementById('gLast').value.trim(),
    11001209                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
    11021213            });
    11031214            if(res && res.ok){
     
    11061217                showToast(d.emailSent ? 'Guest added & invitation email sent! 📧' : 'Guest added (no email provided).','success');
    11071218                await reload();
     1219                checkBudgetAndAlert();
    11081220            } else showToast('Could not add guest.','danger');
    11091221        }
     
    12791391            });
    12801392            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            }
    12821399        }
    12831400    );
     
    13101427            });
    13111428            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            }
    13131435        }
    13141436    );
     
    13841506                status: 'confirmed'
    13851507            });
    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            }
    13871514            else { const d=await res.json(); showToast(d.error||'Error booking venue.','danger'); }
    13881515        }
     
    14521579                status: 'confirmed'
    14531580            });
    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            }
    14551587            else { const d=await res.json(); showToast(d.error||'Error booking band.','danger'); }
    14561588        }
     
    15201652                status: 'confirmed'
    15211653            });
    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            }
    15231660            else { const d=await res.json(); showToast(d.error||'Error booking photographer.','danger'); }
    15241661        }
     
    16711808function renderSettingsPage(container){
    16721809    const u = CURRENT_USER;
     1810    const couple = getCoupleNames();
    16731811    const wrap = document.createElement('div');
    16741812    wrap.innerHTML = `
     
    17021840            <button class="btn btn-ghost" onclick="renderApp()">Reset</button>
    17031841        </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>
    17041854    </div>`;
    17051855    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    });
    17061863
    17071864    wrap.querySelector('#btnSaveProfile').addEventListener('click', async () => {
Note: See TracChangeset for help on using the changeset viewer.