Changeset b7d8a4d
- Timestamp:
- 06/07/26 21:34:27 (4 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)
- Files:
-
- 2 edited
-
Wedding_Planner.html (modified) (9 diffs)
-
server.js (modified) (16 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'); -
server.js
r3679996 rb7d8a4d 61 61 port: parseInt(process.env.DB_PORT, 10), 62 62 ssl: { rejectUnauthorized: false } 63 }); 64 65 // POST /api/weddings/:wid/guests/bulk — add multiple guests in a transaction 66 app.post('/api/weddings/:wid/guests/bulk', requireAuth, async (req, res) => { 67 const { guests, sendEmails } = req.body; 68 if (!Array.isArray(guests) || guests.length === 0) return res.status(400).json({ error: 'guests array is required.' }); 69 70 const wid = req.params.wid; 71 const client = await pool.connect(); 72 try { 73 // Ensure wedding exists 74 const wRes = await client.query('SET search_path TO project; SELECT wedding_id, "date" FROM wedding WHERE wedding_id=$1', [wid]); 75 if (wRes.rows.length === 0) { 76 client.release(); 77 return res.status(404).json({ error: 'Wedding not found.' }); 78 } 79 80 // Fetch events for this wedding 81 const evRes = await client.query('SELECT event_id, event_type, "date", start_time, end_time FROM project.event WHERE wedding_id=$1 ORDER BY "date", start_time', [wid]); 82 const events = evRes.rows; 83 84 await client.query('BEGIN'); 85 const inserted = []; 86 for (const g of guests) { 87 const first_name = (g.first_name || '').trim(); 88 const last_name = (g.last_name || '').trim(); 89 const email = (g.email || null); 90 const role = (g.role || null); 91 if (!first_name || !last_name) { 92 await client.query('ROLLBACK'); 93 return res.status(400).json({ error: 'Each guest must have first_name and last_name.' }); 94 } 95 const ins = await client.query('INSERT INTO project.guest(first_name,last_name,email,wedding_id,role) VALUES ($1,$2,$3,$4,$5) RETURNING *', [first_name, last_name, email, wid, role]); 96 const guest = ins.rows[0]; 97 inserted.push(guest); 98 99 // Create RSVP records for each event 100 for (const ev of events) { 101 try { 102 await client.query( 103 `INSERT INTO project.event_rsvp(status, response_date, guest_id, event_id) 104 VALUES ('invited', CURRENT_DATE, $1, $2) 105 ON CONFLICT (guest_id, event_id) DO NOTHING`, 106 [guest.guest_id, ev.event_id] 107 ); 108 } catch (e) { 109 // Log and continue 110 console.warn('Could not create RSVP for guest', guest.guest_id, 'event', ev.event_id, e.message); 111 } 112 } 113 } 114 115 await client.query('COMMIT'); 116 117 // Optionally send emails after commit 118 if (sendEmails) { 119 for (const guest of inserted) { 120 if (!guest.email) continue; 121 try { 122 // Build event links 123 const eventLinks = events.map(ev => { 124 const token = crypto.createHmac('sha256', 'wedding_rsvp_secret').update(`${guest.guest_id}-${ev.event_id}`).digest('hex'); 125 const base = `http://localhost:${PORT}`; 126 return ` 127 <tr> 128 <td style="padding:8px 0; font-size:15px; color:#2c1f1f;"> 129 <strong>${ev.event_type}</strong> — ${ev.date} ${ev.start_time}–${ev.end_time} 130 </td> 131 <td style="padding:8px 0; text-align:right;"> 132 <a href="${base}/rsvp.html?guest=${guest.guest_id}&event=${ev.event_id}&token=${token}&action=accepted" style="background:#3a9e6b;color:#fff;padding:6px 14px;border-radius:6px;text-decoration:none;margin-right:6px;font-size:13px;">✅ Accept</a> 133 <a href="${base}/rsvp.html?guest=${guest.guest_id}&event=${ev.event_id}&token=${token}&action=declined" style="background:#c94545;color:#fff;padding:6px 14px;border-radius:6px;text-decoration:none;font-size:13px;">❌ Decline</a> 134 </td> 135 </tr>`; 136 }).join(''); 137 138 const mailOptions = { 139 from: `"Wedding Planner" <${process.env.EMAIL_USER}>`, 140 to: guest.email, 141 subject: `You're Invited! 💍 ${req.session.user.first_name} & ${req.session.user.last_name}'s Wedding`, 142 html: ` 143 <div style="font-family:'DM Sans',Arial,sans-serif;max-width:600px;margin:auto;background:#fdf8f3;border-radius:12px;overflow:hidden;border:1px solid #ecddd0;"> 144 <div style="background:linear-gradient(135deg,#d4606a,#e8949c);padding:32px;text-align:center;"> 145 <div style="font-size:36px;margin-bottom:8px;">💍</div> 146 <h1 style="color:#fff;font-size:26px;margin:0;">You're Invited!</h1> 147 <p style="color:rgba(255,255,255,0.9);margin:8px 0 0;">Wedding Invitation</p> 148 </div> 149 <div style="padding:32px;"> 150 <p style="font-size:16px;color:#2c1f1f;">Dear <strong>${guest.first_name} ${guest.last_name}</strong>,</p> 151 <p style="font-size:15px;color:#6b4f4f;line-height:1.6;">Please RSVP for the events below:</p> 152 <table style="width:100%;border-collapse:collapse;">${eventLinks}</table> 153 </div> 154 <div style="background:#f2e8db;padding:16px;text-align:center;"></div> 155 </div>` 156 }; 157 await transporter.sendMail(mailOptions); 158 } catch (mailErr) { 159 console.warn('Failed to send invite to', guest.email, mailErr.message); 160 } 161 } 162 } 163 164 client.release(); 165 res.json({ inserted: inserted.length, guests: inserted }); 166 } catch (err) { 167 try { await client.query('ROLLBACK'); } catch (e) { /* ignore */ } 168 client.release(); 169 console.error('Bulk guest insert error:', err.message); 170 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message }); 171 res.status(500).json({ error: 'Server error during bulk guest insert.' }); 172 } 63 173 }); 64 174 … … 83 193 84 194 // ============================================================= 195 // Service layer: Booking validation utilities 196 // ============================================================= 197 class ApiError extends Error { 198 constructor(status, message) { 199 super(message); 200 this.status = status; 201 } 202 } 203 204 const BookingService = { 205 // Normalize date value (DATE from PG may be string or Date) 206 _fmtDate(d) { 207 if (!d) return null; 208 if (d instanceof Date) return d.toISOString().slice(0,10); 209 // assume string YYYY-MM-DD 210 return String(d).slice(0,10); 211 }, 212 213 // Check if two time intervals overlap: [s1,e1) and [s2,e2) 214 isOverlapping(s1, e1, s2, e2) { 215 if (!s1 || !e1 || !s2 || !e2) return false; 216 // times are strings like HH:MM:SS or HH:MM 217 const toSec = t => { 218 const parts = String(t).split(':').map(Number); 219 return (parts[0]||0)*3600 + (parts[1]||0)*60 + (parts[2]||0); 220 }; 221 const a1 = toSec(s1), b1 = toSec(e1), a2 = toSec(s2), b2 = toSec(e2); 222 return a1 < b2 && a2 < b1; 223 }, 224 225 // Gather all bookings/events for a given wedding and date 226 async getBookingsForWeddingDate(wedding_id, date) { 227 const q = ` 228 SELECT 'venue' AS type, booking_id AS id, start_time, end_time 229 FROM project.venue_booking WHERE wedding_id=$1 AND "date"=$2 230 UNION ALL 231 SELECT 'church' AS type, booking_id AS id, start_time, end_time 232 FROM project.church_booking WHERE wedding_id=$1 AND "date"=$2 233 UNION ALL 234 SELECT 'registrar' AS type, booking_id AS id, start_time, end_time 235 FROM project.registrar_booking WHERE wedding_id=$1 AND "date"=$2 236 UNION ALL 237 SELECT 'photographer' AS type, booking_id AS id, start_time, end_time 238 FROM project.photographer_booking WHERE wedding_id=$1 AND "date"=$2 239 UNION ALL 240 SELECT 'band' AS type, booking_id AS id, start_time, end_time 241 FROM project.band_booking WHERE wedding_id=$1 AND "date"=$2 242 UNION ALL 243 SELECT 'event' AS type, event_id AS id, start_time, end_time 244 FROM project.event WHERE wedding_id=$1 AND "date"=$2 245 `; 246 const res = await pool.query(q, [wedding_id, date]); 247 return res.rows; 248 }, 249 250 // Validate booking date equals wedding date 251 async validateDateMatchesWedding(wedding_id, date) { 252 // Use SQL date cast/comparison to avoid client-side format issues 253 const r = await pool.query('SELECT 1 FROM project.wedding WHERE wedding_id=$1 AND "date" = $2::date', [wedding_id, date]); 254 if (r.rows.length === 0) { 255 // Determine whether wedding not found or date mismatch 256 const exists = await pool.query('SELECT 1 FROM project.wedding WHERE wedding_id=$1', [wedding_id]); 257 if (exists.rows.length === 0) throw new ApiError(404, 'Wedding not found.'); 258 throw new ApiError(400, 'Booking is only allowed on the wedding date.'); 259 } 260 }, 261 262 // Validate time slot overlap across all bookings for the wedding/date 263 // exclude optional { type, id } to allow updating existing record 264 async validateNoOverlap(wedding_id, date, start_time, end_time, exclude) { 265 const bookings = await BookingService.getBookingsForWeddingDate(wedding_id, date); 266 for (const b of bookings) { 267 if (exclude && String(exclude.type) === String(b.type) && String(exclude.id) === String(b.id)) continue; 268 if (BookingService.isOverlapping(start_time, end_time, b.start_time, b.end_time)) { 269 throw new ApiError(409, 'Time slot conflict detected: this booking overlaps with another scheduled event.'); 270 } 271 } 272 }, 273 274 // Registrar location must match wedding venue location for that wedding date 275 // We require an existing venue booking on the wedding date and compare locations 276 async validateRegistrarLocation(wedding_id, date, registrar_id) { 277 // find registrar 278 const r = await pool.query('SELECT location FROM project.registrar WHERE registrar_id=$1', [registrar_id]); 279 if (r.rows.length === 0) throw new ApiError(404, 'Registrar not found.'); 280 const regLoc = r.rows[0].location; 281 282 // find venue booking(s) for the wedding on the date 283 const vb = await pool.query('SELECT v.location FROM project.venue_booking vb JOIN project.venue v ON vb.venue_id=v.venue_id WHERE vb.wedding_id=$1 AND vb."date"=$2', [wedding_id, date]); 284 if (vb.rows.length === 0) { 285 // no venue booked on that date -> not allowed 286 throw new ApiError(400, 'Registrar booking is only allowed at the wedding venue location.'); 287 } 288 // require at least one matching venue location 289 const match = vb.rows.some(x => String(x.location).trim().toLowerCase() === String(regLoc).trim().toLowerCase()); 290 if (!match) throw new ApiError(400, 'Registrar booking is only allowed at the wedding venue location.'); 291 } 292 }; 293 294 // ============================================================= 85 295 // EMAIL TRANSPORTER 86 296 // ============================================================= … … 323 533 return res.status(400).json({ error: 'event_type, date, start_time, end_time are required.' }); 324 534 try { 535 // Business validations 536 await BookingService.validateDateMatchesWedding(req.params.wid, date); 537 await BookingService.validateNoOverlap(req.params.wid, date, start_time, end_time, null); 538 325 539 const result = await pool.query( 326 540 `INSERT INTO project.event(event_type,"date",start_time,end_time,status,wedding_id) … … 331 545 } catch (err) { 332 546 console.error(err.message); 547 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message }); 333 548 res.status(500).json({ error: 'Server error.' }); 334 549 } … … 339 554 const { event_type, date, start_time, end_time, status } = req.body; 340 555 try { 556 // fetch existing event to know wedding_id and current values 557 const evRes = await pool.query('SELECT * FROM project.event WHERE event_id=$1', [req.params.id]); 558 if (evRes.rows.length === 0) return res.status(404).json({ error: 'Event not found.' }); 559 const ev = evRes.rows[0]; 560 const weddingId = ev.wedding_id; 561 // Validate date matches wedding and no overlap (exclude this event) 562 await BookingService.validateDateMatchesWedding(weddingId, date); 563 await BookingService.validateNoOverlap(weddingId, date, start_time, end_time, { type: 'event', id: req.params.id }); 564 341 565 const result = await pool.query( 342 566 `UPDATE project.event SET event_type=$1,"date"=$2,start_time=$3,end_time=$4,status=$5 … … 348 572 } catch (err) { 349 573 console.error(err.message); 574 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message }); 350 575 res.status(500).json({ error: 'Server error.' }); 351 576 } … … 778 1003 // Check availability: no overlapping booking for same venue on same date 779 1004 try { 1005 // Business validations 1006 await BookingService.validateDateMatchesWedding(req.params.wid, date); 1007 await BookingService.validateNoOverlap(req.params.wid, date, start_time, end_time, null); 1008 780 1009 const conflict = await pool.query( 781 1010 `SELECT booking_id FROM project.venue_booking … … 795 1024 } catch (err) { 796 1025 console.error(err.message); 1026 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message }); 797 1027 res.status(500).json({ error: 'Server error.' }); 798 1028 } … … 848 1078 return res.status(400).json({ error: 'date, start_time, end_time and band_id are required.' }); 849 1079 try { 1080 // Business validations 1081 await BookingService.validateDateMatchesWedding(req.params.wid, date); 1082 await BookingService.validateNoOverlap(req.params.wid, date, start_time, end_time, null); 850 1083 const result = await pool.query( 851 1084 `INSERT INTO project.band_booking("date",start_time,end_time,status,band_id,wedding_id) … … 856 1089 } catch (err) { 857 1090 console.error(err.message); 1091 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message }); 858 1092 res.status(500).json({ error: 'Server error.' }); 859 1093 } … … 909 1143 return res.status(400).json({ error: 'date, start_time, end_time and photographer_id are required.' }); 910 1144 try { 1145 // Business validations 1146 await BookingService.validateDateMatchesWedding(req.params.wid, date); 1147 await BookingService.validateNoOverlap(req.params.wid, date, start_time, end_time, null); 911 1148 const result = await pool.query( 912 1149 `INSERT INTO project.photographer_booking("date",start_time,end_time,status,photographer_id,wedding_id) … … 917 1154 } catch (err) { 918 1155 console.error(err.message); 1156 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message }); 919 1157 res.status(500).json({ error: 'Server error.' }); 920 1158 } … … 977 1215 return res.status(400).json({ error: 'date, start_time, end_time and church_id are required.' }); 978 1216 try { 1217 // Business validations 1218 await BookingService.validateDateMatchesWedding(req.params.wid, date); 1219 await BookingService.validateNoOverlap(req.params.wid, date, start_time, end_time, null); 979 1220 const result = await pool.query( 980 1221 `INSERT INTO project.church_booking("date",start_time,end_time,status,church_id,wedding_id) … … 985 1226 } catch (err) { 986 1227 console.error(err.message); 1228 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message }); 987 1229 res.status(500).json({ error: 'Server error.' }); 988 1230 } … … 1038 1280 return res.status(400).json({ error: 'date, start_time, end_time and registrar_id are required.' }); 1039 1281 try { 1282 // Business validations 1283 await BookingService.validateDateMatchesWedding(req.params.wid, date); 1284 await BookingService.validateNoOverlap(req.params.wid, date, start_time, end_time, null); 1285 await BookingService.validateRegistrarLocation(req.params.wid, date, registrar_id); 1286 1040 1287 const result = await pool.query( 1041 1288 `INSERT INTO project.registrar_booking("date",start_time,end_time,status,registrar_id,wedding_id) … … 1046 1293 } catch (err) { 1047 1294 console.error(err.message); 1295 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message }); 1048 1296 res.status(500).json({ error: 'Server error.' }); 1049 1297 }
Note:
See TracChangeset
for help on using the changeset viewer.
