source: server.js@ 679113b

main
Last change on this file since 679113b was b7d8a4d, checked in by GitHub <noreply@…>, 5 weeks ago

Add files via upload

  • Property mode set to 100644
File size: 57.0 KB
Line 
1// =============================================================
2// Wedding Planner — Complete Backend (Node.js + Express)
3// Database: PostgreSQL via server
4// Auth: Session-based (login / register)
5// Email: Gmail via Nodemailer
6// =============================================================
7
8// Load environment variables
9require('dotenv').config();
10
11const express = require('express');
12const { Pool } = require('pg');
13const cors = require('cors');
14
15const app = express();
16
17app.use(cors({
18 origin: "http://localhost:3000",
19 credentials: true
20}));
21
22app.use(express.json());
23const bcrypt = require('bcryptjs');
24const session = require('express-session');
25const nodemailer = require('nodemailer');
26const crypto = require('crypto');
27const path = require('path');
28const PORT = process.env.PORT || 3000;
29
30// =============================================================
31// MIDDLEWARE
32// =============================================================
33app.use(cors({ origin: 'http://localhost:3000', credentials: true }));
34app.use(express.json());
35app.use(express.urlencoded({ extended: true }));
36
37// Serve all HTML files from the same folder as this server.js
38app.use(express.static(path.join(__dirname)));
39
40// Session setup
41app.use(session({
42 secret: 'wedding_planner_secret_key_2025',
43 resave: false,
44 saveUninitialized: false,
45 cookie: {
46 secure: false, // set true only if using HTTPS
47 httpOnly: true,
48 maxAge: 1000 * 60 * 60 * 8 // 8 hours
49 }
50}));
51
52// =============================================================
53// DATABASE CONNECTION
54//
55// =============================================================
56const pool = new Pool({
57 user: process.env.DB_USER,
58 host: process.env.DB_HOST,
59 database: process.env.DB_NAME,
60 password: process.env.DB_PASSWORD,
61 port: parseInt(process.env.DB_PORT, 10),
62 ssl: { rejectUnauthorized: false }
63});
64
65// POST /api/weddings/:wid/guests/bulk — add multiple guests in a transaction
66app.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 }
173});
174
175console.log('📡 Attempting to connect to PostgreSQL...');
176console.log(` Host: ${process.env.DB_HOST}:${process.env.DB_PORT}`);
177console.log(` Database: ${process.env.DB_NAME}`);
178
179pool.connect()
180 .then(client => {
181 console.log('✅ Connected to PostgreSQL database!');
182 client.release();
183 })
184 .catch(err => {
185 console.error('❌ DB connection error:', err.message);
186 console.error(' Make sure SSH tunnel is running on port 9999');
187 console.error(' SSH: ssh -L 9999:db_server:5432 t_wedding_planner2025@194.149.135.130');
188 console.error(' Full error:', err.stack);
189 });
190
191// Helper: always use the project schema
192const Q = (text, params) => pool.query(`SET search_path TO project; ${text}`, params);
193
194// =============================================================
195// Service layer: Booking validation utilities
196// =============================================================
197class ApiError extends Error {
198 constructor(status, message) {
199 super(message);
200 this.status = status;
201 }
202}
203
204const 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// =============================================================
295// EMAIL TRANSPORTER
296// =============================================================
297const transporter = nodemailer.createTransport({
298 service: 'gmail',
299 auth: {
300 user: process.env.EMAIL_USER,
301 pass: process.env.EMAIL_PASS
302 }
303});
304
305// =============================================================
306// AUTH MIDDLEWARE — protect routes that require login
307// =============================================================
308function requireAuth(req, res, next) {
309 if (req.session && req.session.user) return next();
310 return res.status(401).json({ error: 'Not authenticated. Please log in.' });
311}
312
313// =============================================================
314// AUTH ROUTES
315// =============================================================
316
317// POST /api/auth/register
318app.post('/api/auth/register', async (req, res) => {
319 const { first_name, last_name, email, password, phone_number, gender, birthday } = req.body;
320 if (!first_name || !last_name || !email || !password)
321 return res.status(400).json({ error: 'First name, last name, email and password are required.' });
322
323 try {
324 // Check email not already taken
325 const exists = await pool.query(
326 'SELECT user_id FROM project."user" WHERE email = $1', [email]
327 );
328 if (exists.rows.length > 0)
329 return res.status(409).json({ error: 'Email already registered.' });
330
331 const hash = await bcrypt.hash(password, 10);
332
333 const result = await pool.query(
334 `INSERT INTO project."user"(first_name, last_name, email, password_hash, phone_number, gender, birthday)
335 VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING user_id, first_name, last_name, email, gender`,
336 [first_name, last_name, email, hash, phone_number || null, gender || null, birthday || null]
337 );
338
339 const user = result.rows[0];
340 req.session.user = user;
341 res.status(201).json({ message: 'Registered successfully.', user });
342 } catch (err) {
343 console.error('Register error:', err.message);
344 res.status(500).json({ error: 'Server error during registration.' });
345 }
346});
347
348// POST /api/auth/login
349app.post('/api/auth/login', async (req, res) => {
350 const { email, password } = req.body;
351 if (!email || !password)
352 return res.status(400).json({ error: 'Email and password are required.' });
353
354 try {
355 const result = await pool.query(
356 'SELECT * FROM project."user" WHERE email = $1', [email]
357 );
358 if (result.rows.length === 0)
359 return res.status(401).json({ error: 'Invalid email or password.' });
360
361 const user = result.rows[0];
362 const match = await bcrypt.compare(password, user.password_hash);
363 if (!match)
364 return res.status(401).json({ error: 'Invalid email or password.' });
365
366 req.session.user = {
367 user_id: user.user_id,
368 first_name: user.first_name,
369 last_name: user.last_name,
370 email: user.email,
371 gender: user.gender
372 };
373 res.json({ message: 'Logged in.', user: req.session.user });
374 } catch (err) {
375 console.error('Login error:', err.message);
376 res.status(500).json({ error: 'Server error during login.' });
377 }
378});
379
380// POST /api/auth/logout
381app.post('/api/auth/logout', (req, res) => {
382 req.session.destroy(() => res.json({ message: 'Logged out.' }));
383});
384
385// GET /api/auth/me — check current session
386app.get('/api/auth/me', (req, res) => {
387 if (req.session && req.session.user)
388 return res.json({ user: req.session.user });
389 res.status(401).json({ error: 'Not logged in.' });
390});
391
392// =============================================================
393// USER / PROFILE
394// =============================================================
395
396// GET /api/users/:id
397app.get('/api/users/:id', requireAuth, async (req, res) => {
398 try {
399 const result = await pool.query(
400 'SELECT user_id, first_name, last_name, email, phone_number, gender, birthday FROM project."user" WHERE user_id = $1',
401 [req.params.id]
402 );
403 if (result.rows.length === 0) return res.status(404).json({ error: 'User not found.' });
404 res.json(result.rows[0]);
405 } catch (err) {
406 console.error(err.message);
407 res.status(500).json({ error: 'Server error.' });
408 }
409});
410
411// PUT /api/users/:id — update profile
412app.put('/api/users/:id', requireAuth, async (req, res) => {
413 const { first_name, last_name, email, phone_number, gender, birthday } = req.body;
414 try {
415 const result = await pool.query(
416 `UPDATE project."user"
417 SET first_name=$1, last_name=$2, email=$3, phone_number=$4, gender=$5, birthday=$6
418 WHERE user_id=$7 RETURNING user_id, first_name, last_name, email, phone_number, gender, birthday`,
419 [first_name, last_name, email, phone_number, gender, birthday || null, req.params.id]
420 );
421 if (result.rows.length === 0) return res.status(404).json({ error: 'User not found.' });
422 // Update session too
423 req.session.user = { ...req.session.user, ...result.rows[0] };
424 res.json(result.rows[0]);
425 } catch (err) {
426 console.error(err.message);
427 res.status(500).json({ error: 'Server error.' });
428 }
429});
430
431// =============================================================
432// WEDDINGS (UC0003)
433// =============================================================
434
435// GET /api/weddings — get wedding(s) for logged-in user
436app.get('/api/weddings', requireAuth, async (req, res) => {
437 try {
438 const result = await pool.query(
439 'SELECT * FROM project.wedding WHERE user_id = $1 ORDER BY date',
440 [req.session.user.user_id]
441 );
442 res.json(result.rows);
443 } catch (err) {
444 console.error(err.message);
445 res.status(500).json({ error: 'Server error.' });
446 }
447});
448
449// GET /api/weddings/:id
450app.get('/api/weddings/:id', requireAuth, async (req, res) => {
451 try {
452 const result = await pool.query(
453 'SELECT * FROM project.wedding WHERE wedding_id = $1 AND user_id = $2',
454 [req.params.id, req.session.user.user_id]
455 );
456 if (result.rows.length === 0) return res.status(404).json({ error: 'Wedding not found.' });
457 res.json(result.rows[0]);
458 } catch (err) {
459 console.error(err.message);
460 res.status(500).json({ error: 'Server error.' });
461 }
462});
463
464// POST /api/weddings — create wedding
465app.post('/api/weddings', requireAuth, async (req, res) => {
466 const { date, budget, notes } = req.body;
467 if (!date) return res.status(400).json({ error: 'Wedding date is required.' });
468 try {
469 const result = await pool.query(
470 'INSERT INTO project.wedding("date", budget, notes, user_id) VALUES ($1,$2,$3,$4) RETURNING *',
471 [date, budget || null, notes || null, req.session.user.user_id]
472 );
473 res.status(201).json(result.rows[0]);
474 } catch (err) {
475 console.error(err.message);
476 res.status(500).json({ error: 'Server error.' });
477 }
478});
479
480// PUT /api/weddings/:id
481app.put('/api/weddings/:id', requireAuth, async (req, res) => {
482 const { date, budget, notes } = req.body;
483 try {
484 const result = await pool.query(
485 `UPDATE project.wedding SET "date"=$1, budget=$2, notes=$3
486 WHERE wedding_id=$4 AND user_id=$5 RETURNING *`,
487 [date, budget || null, notes || null, req.params.id, req.session.user.user_id]
488 );
489 if (result.rows.length === 0) return res.status(404).json({ error: 'Wedding not found.' });
490 res.json(result.rows[0]);
491 } catch (err) {
492 console.error(err.message);
493 res.status(500).json({ error: 'Server error.' });
494 }
495});
496
497// DELETE /api/weddings/:id
498app.delete('/api/weddings/:id', requireAuth, async (req, res) => {
499 try {
500 await pool.query(
501 'DELETE FROM project.wedding WHERE wedding_id=$1 AND user_id=$2',
502 [req.params.id, req.session.user.user_id]
503 );
504 res.json({ message: 'Wedding deleted.' });
505 } catch (err) {
506 console.error(err.message);
507 res.status(500).json({ error: 'Server error.' });
508 }
509});
510
511// =============================================================
512// EVENTS (UC0005)
513// =============================================================
514
515// GET /api/weddings/:wid/events
516app.get('/api/weddings/:wid/events', requireAuth, async (req, res) => {
517 try {
518 const result = await pool.query(
519 'SELECT * FROM project.event WHERE wedding_id=$1 ORDER BY "date", start_time',
520 [req.params.wid]
521 );
522 res.json(result.rows);
523 } catch (err) {
524 console.error(err.message);
525 res.status(500).json({ error: 'Server error.' });
526 }
527});
528
529// POST /api/weddings/:wid/events
530app.post('/api/weddings/:wid/events', requireAuth, async (req, res) => {
531 const { event_type, date, start_time, end_time, status } = req.body;
532 if (!event_type || !date || !start_time || !end_time)
533 return res.status(400).json({ error: 'event_type, date, start_time, end_time are required.' });
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
539 const result = await pool.query(
540 `INSERT INTO project.event(event_type,"date",start_time,end_time,status,wedding_id)
541 VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`,
542 [event_type, date, start_time, end_time, status || 'scheduled', req.params.wid]
543 );
544 res.status(201).json(result.rows[0]);
545 } catch (err) {
546 console.error(err.message);
547 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message });
548 res.status(500).json({ error: 'Server error.' });
549 }
550});
551
552// PUT /api/events/:id
553app.put('/api/events/:id', requireAuth, async (req, res) => {
554 const { event_type, date, start_time, end_time, status } = req.body;
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
565 const result = await pool.query(
566 `UPDATE project.event SET event_type=$1,"date"=$2,start_time=$3,end_time=$4,status=$5
567 WHERE event_id=$6 RETURNING *`,
568 [event_type, date, start_time, end_time, status, req.params.id]
569 );
570 if (result.rows.length === 0) return res.status(404).json({ error: 'Event not found.' });
571 res.json(result.rows[0]);
572 } catch (err) {
573 console.error(err.message);
574 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message });
575 res.status(500).json({ error: 'Server error.' });
576 }
577});
578
579// DELETE /api/events/:id
580app.delete('/api/events/:id', requireAuth, async (req, res) => {
581 try {
582 await pool.query('DELETE FROM project.event WHERE event_id=$1', [req.params.id]);
583 res.json({ message: 'Event deleted.' });
584 } catch (err) {
585 console.error(err.message);
586 res.status(500).json({ error: 'Server error.' });
587 }
588});
589
590// =============================================================
591// GUESTS (UC0004)
592// =============================================================
593
594// GET /api/weddings/:wid/guests
595app.get('/api/weddings/:wid/guests', requireAuth, async (req, res) => {
596 try {
597 const result = await pool.query(
598 'SELECT guest_id, first_name, last_name, email, role, wedding_id FROM project.guest WHERE wedding_id=$1 ORDER BY last_name, first_name',
599 [req.params.wid]
600 );
601 res.json(result.rows);
602 } catch (err) {
603 console.error(err.message);
604 res.status(500).json({ error: 'Server error.' });
605 }
606});
607
608// POST /api/weddings/:wid/guests — add guest + send email invite
609app.post('/api/weddings/:wid/guests', requireAuth, async (req, res) => {
610 const { first_name, last_name, email, role } = req.body;
611 if (!first_name || !last_name)
612 return res.status(400).json({ error: 'First name and last name are required.' });
613
614 try {
615 // Insert guest
616 const result = await pool.query(
617 'INSERT INTO project.guest(first_name,last_name,email,wedding_id) VALUES ($1,$2,$3,$4) RETURNING *',
618 [first_name, last_name, email || null, req.params.wid]
619 );
620 const guest = result.rows[0];
621
622 // Fetch wedding info for the email
623 const weddingResult = await pool.query(
624 `SELECT w."date", u.first_name AS owner_first, u.last_name AS owner_last
625 FROM project.wedding w
626 JOIN project."user" u ON w.user_id = u.user_id
627 WHERE w.wedding_id = $1`,
628 [req.params.wid]
629 );
630 const wedding = weddingResult.rows[0];
631
632 // Fetch all events for this wedding so guest can RSVP
633 const eventsResult = await pool.query(
634 'SELECT * FROM project.event WHERE wedding_id=$1 ORDER BY "date", start_time',
635 [req.params.wid]
636 );
637 const events = eventsResult.rows;
638
639 // Create initial RSVP records with status "invited" for each event
640 for (const event of events) {
641 try {
642 await pool.query(
643 `INSERT INTO project.event_rsvp(status, response_date, guest_id, event_id)
644 VALUES ('invited', CURRENT_DATE, $1, $2)
645 ON CONFLICT (guest_id, event_id) DO NOTHING`,
646 [guest.guest_id, event.event_id]
647 );
648 } catch (rsvpErr) {
649 console.warn(`⚠️ Could not create RSVP record for guest ${guest.guest_id}, event ${event.event_id}:`, rsvpErr.message);
650 }
651 }
652
653 // Send email invitation if email provided
654 if (email && events.length > 0) {
655 // Generate a secure token per event for the RSVP links
656 const eventLinks = events.map(ev => {
657 const token = crypto
658 .createHmac('sha256', 'wedding_rsvp_secret')
659 .update(`${guest.guest_id}-${ev.event_id}`)
660 .digest('hex');
661 const base = `http://localhost:${PORT}`;
662 return `
663 <tr>
664 <td style="padding:8px 0; font-size:15px; color:#2c1f1f;">
665 <strong>${ev.event_type}</strong> — ${ev.date} ${ev.start_time}–${ev.end_time}
666 </td>
667 <td style="padding:8px 0; text-align:right;">
668 <a href="${base}/rsvp.html?guest=${guest.guest_id}&event=${ev.event_id}&token=${token}&action=accepted"
669 style="background:#3a9e6b;color:#fff;padding:6px 14px;border-radius:6px;text-decoration:none;margin-right:6px;font-size:13px;">
670 ✅ Accept
671 </a>
672 <a href="${base}/rsvp.html?guest=${guest.guest_id}&event=${ev.event_id}&token=${token}&action=declined"
673 style="background:#c94545;color:#fff;padding:6px 14px;border-radius:6px;text-decoration:none;font-size:13px;">
674 ❌ Decline
675 </a>
676 </td>
677 </tr>`;
678 }).join('');
679
680 const mailOptions = {
681 from: `"Wedding Planner" <${process.env.EMAIL_USER}>`,
682 to: email,
683 subject: `You're Invited! 💍 ${wedding.owner_first} & ${wedding.owner_last}'s Wedding`,
684 html: `
685 <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;">
686 <div style="background:linear-gradient(135deg,#d4606a,#e8949c);padding:32px;text-align:center;">
687 <div style="font-size:36px;margin-bottom:8px;">💍</div>
688 <h1 style="color:#fff;font-size:26px;margin:0;">You're Invited!</h1>
689 <p style="color:rgba(255,255,255,0.9);margin:8px 0 0;">Wedding of ${wedding.owner_first} &amp; ${wedding.owner_last}</p>
690 </div>
691 <div style="padding:32px;">
692 <p style="font-size:16px;color:#2c1f1f;">Dear <strong>${first_name} ${last_name}</strong>,</p>
693 <p style="font-size:15px;color:#6b4f4f;line-height:1.6;">
694 We are delighted to invite you to celebrate the wedding of
695 <strong>${wedding.owner_first} &amp; ${wedding.owner_last}</strong>
696 on <strong>${wedding.date}</strong>.
697 </p>
698 <p style="font-size:15px;color:#2c1f1f;font-weight:600;margin-top:24px;">Please RSVP for each event:</p>
699 <table style="width:100%;border-collapse:collapse;">
700 ${eventLinks}
701 </table>
702 <p style="font-size:13px;color:#a08080;margin-top:24px;">
703 We look forward to celebrating with you. If you have any questions, please contact us directly.
704 </p>
705 </div>
706 <div style="background:#f2e8db;padding:16px;text-align:center;">
707 <p style="font-size:12px;color:#a08080;margin:0;">Wedding Planner App · Sent with love 💌</p>
708 </div>
709 </div>`
710 };
711
712 try {
713 await transporter.sendMail(mailOptions);
714 console.log(`📧 Invite sent to ${email}`);
715 } catch (mailErr) {
716 // Don't fail the guest insert if email fails — just log it
717 console.warn('⚠️ Email send failed (guest still added):', mailErr.message);
718 }
719 }
720
721 res.status(201).json({ guest, emailSent: !!(email && events.length > 0) });
722 } catch (err) {
723 console.error(err.message);
724 res.status(500).json({ error: 'Server error.' });
725 }
726});
727
728// PUT /api/guests/:id
729app.put('/api/guests/:id', requireAuth, async (req, res) => {
730 const { first_name, last_name, email, role } = req.body;
731 try {
732 const result = await pool.query(
733 'UPDATE project.guest SET first_name=$1,last_name=$2,email=$3,role=$4 WHERE guest_id=$5 RETURNING *',
734 [first_name, last_name, email || null, role || 'Guest', req.params.id]
735 );
736 if (result.rows.length === 0) return res.status(404).json({ error: 'Guest not found.' });
737 res.json(result.rows[0]);
738 } catch (err) {
739 console.error(err.message);
740 res.status(500).json({ error: 'Server error.' });
741 }
742});
743
744// DELETE /api/guests/:id
745app.delete('/api/guests/:id', requireAuth, async (req, res) => {
746 try {
747 await pool.query('DELETE FROM project.guest WHERE guest_id=$1', [req.params.id]);
748 res.json({ message: 'Guest deleted.' });
749 } catch (err) {
750 console.error(err.message);
751 res.status(500).json({ error: 'Server error.' });
752 }
753});
754
755// =============================================================
756// RSVP (UC0009)
757// =============================================================
758
759// GET /api/rsvp?guest_id=&event_id= — check existing RSVP
760app.get('/api/rsvp', async (req, res) => {
761 const { guest_id, event_id } = req.query;
762 try {
763 const result = await pool.query(
764 'SELECT * FROM project.event_rsvp WHERE guest_id=$1 AND event_id=$2',
765 [guest_id, event_id]
766 );
767 res.json(result.rows[0] || null);
768 } catch (err) {
769 console.error(err.message);
770 res.status(500).json({ error: 'Server error.' });
771 }
772});
773
774// POST /api/rsvp — guest submits RSVP (called from rsvp.html, no auth needed)
775app.post('/api/rsvp', async (req, res) => {
776 const { guest_id, event_id, status, token } = req.body;
777 if (!guest_id || !event_id || !status)
778 return res.status(400).json({ error: 'guest_id, event_id and status are required.' });
779
780 // Verify token (protects against random submissions)
781 const expected = crypto
782 .createHmac('sha256', 'wedding_rsvp_secret')
783 .update(`${guest_id}-${event_id}`)
784 .digest('hex');
785 if (token !== expected)
786 return res.status(403).json({ error: 'Invalid or expired RSVP link.' });
787
788 const validStatuses = ['accepted', 'declined', 'pending', 'invited'];
789 if (!validStatuses.includes(status))
790 return res.status(400).json({ error: 'Status must be accepted, declined, pending, or invited.' });
791
792 try {
793 // Upsert: update if exists, insert if not
794 const result = await pool.query(
795 `INSERT INTO project.event_rsvp(status, response_date, guest_id, event_id)
796 VALUES ($1, CURRENT_DATE, $2, $3)
797 ON CONFLICT (guest_id, event_id)
798 DO UPDATE SET status=$1, response_date=CURRENT_DATE
799 RETURNING *`,
800 [status, guest_id, event_id]
801 );
802
803 // Fetch guest + event info for the confirmation response
804 const guestInfo = await pool.query(
805 `SELECT g.first_name, g.last_name, e.event_type, e.date, e.start_time, e.end_time
806 FROM project.guest g, project.event e
807 WHERE g.guest_id=$1 AND e.event_id=$2`,
808 [guest_id, event_id]
809 );
810
811 res.json({
812 rsvp: result.rows[0],
813 guest: guestInfo.rows[0] || null
814 });
815 } catch (err) {
816 console.error(err.message);
817 res.status(500).json({ error: 'Server error.' });
818 }
819});
820
821// GET /api/weddings/:wid/rsvp — all RSVPs for a wedding (for admin view)
822app.get('/api/weddings/:wid/rsvp', requireAuth, async (req, res) => {
823 try {
824 const result = await pool.query(
825 `SELECT er.response_id, er.status, er.response_date,
826 g.guest_id, g.first_name, g.last_name, g.email,
827 e.event_id, e.event_type, e.date AS event_date
828 FROM project.event_rsvp er
829 JOIN project.guest g ON er.guest_id = g.guest_id
830 JOIN project.event e ON er.event_id = e.event_id
831 WHERE e.wedding_id = $1
832 ORDER BY e.date, g.last_name`,
833 [req.params.wid]
834 );
835 res.json(result.rows);
836 } catch (err) {
837 console.error(err.message);
838 res.status(500).json({ error: 'Server error.' });
839 }
840});
841
842// =============================================================
843// ATTENDANCE / SEATING (UC0011)
844// =============================================================
845
846// GET /api/weddings/:wid/attendance — full seating list for a wedding
847app.get('/api/weddings/:wid/attendance', requireAuth, async (req, res) => {
848 try {
849 const result = await pool.query(
850 `SELECT a.attendance_id, a.status, a.table_number, a.role,
851 g.guest_id, g.first_name, g.last_name,
852 e.event_id, e.event_type
853 FROM project.attendance a
854 JOIN project.guest g ON a.guest_id = g.guest_id
855 JOIN project.event e ON a.event_id = e.event_id
856 WHERE e.wedding_id = $1
857 ORDER BY a.table_number NULLS LAST, g.last_name`,
858 [req.params.wid]
859 );
860 res.json(result.rows);
861 } catch (err) {
862 console.error(err.message);
863 res.status(500).json({ error: 'Server error.' });
864 }
865});
866
867// GET /api/events/:eid/attendance — seating for a specific event
868app.get('/api/events/:eid/attendance', requireAuth, async (req, res) => {
869 try {
870 const result = await pool.query(
871 `SELECT a.attendance_id, a.status, a.table_number, a.role,
872 g.guest_id, g.first_name, g.last_name
873 FROM project.attendance a
874 JOIN project.guest g ON a.guest_id = g.guest_id
875 WHERE a.event_id = $1
876 ORDER BY a.table_number NULLS LAST, g.last_name`,
877 [req.params.eid]
878 );
879 res.json(result.rows);
880 } catch (err) {
881 console.error(err.message);
882 res.status(500).json({ error: 'Server error.' });
883 }
884});
885
886// POST /api/attendance — assign guest to event with table + role
887app.post('/api/attendance', requireAuth, async (req, res) => {
888 const { table_number, role, guest_id, event_id } = req.body;
889 if (!guest_id || !event_id || !role)
890 return res.status(400).json({ error: 'guest_id, event_id and role are required.' });
891 try {
892 // Fetch the guest's RSVP status for this event
893 const rsvpResult = await pool.query(
894 'SELECT status FROM project.event_rsvp WHERE guest_id=$1 AND event_id=$2',
895 [guest_id, event_id]
896 );
897 const rsvpStatus = rsvpResult.rows[0]?.status || 'pending';
898
899 const result = await pool.query(
900 `INSERT INTO project.attendance(status, table_number, role, guest_id, event_id)
901 VALUES ($1,$2,$3,$4,$5)
902 ON CONFLICT (guest_id, event_id)
903 DO UPDATE SET status=$1, table_number=$2, role=$3
904 RETURNING *`,
905 [rsvpStatus, table_number || null, role, guest_id, event_id]
906 );
907 res.status(201).json(result.rows[0]);
908 } catch (err) {
909 console.error(err.message);
910 res.status(500).json({ error: 'Server error.' });
911 }
912});
913
914// PUT /api/attendance/:id — update seating assignment
915app.put('/api/attendance/:id', requireAuth, async (req, res) => {
916 const { table_number, role } = req.body;
917 try {
918 // Fetch the attendance record to get guest_id and event_id
919 const attendanceRecord = await pool.query(
920 'SELECT guest_id, event_id FROM project.attendance WHERE attendance_id=$1',
921 [req.params.id]
922 );
923 if (attendanceRecord.rows.length === 0)
924 return res.status(404).json({ error: 'Record not found.' });
925
926 const { guest_id, event_id } = attendanceRecord.rows[0];
927
928 // Fetch the guest's RSVP status for this event
929 const rsvpResult = await pool.query(
930 'SELECT status FROM project.event_rsvp WHERE guest_id=$1 AND event_id=$2',
931 [guest_id, event_id]
932 );
933 const rsvpStatus = rsvpResult.rows[0]?.status || 'pending';
934
935 const result = await pool.query(
936 `UPDATE project.attendance SET status=$1, table_number=$2, role=$3
937 WHERE attendance_id=$4 RETURNING *`,
938 [rsvpStatus, table_number || null, role, req.params.id]
939 );
940 if (result.rows.length === 0) return res.status(404).json({ error: 'Record not found.' });
941 res.json(result.rows[0]);
942 } catch (err) {
943 console.error(err.message);
944 res.status(500).json({ error: 'Server error.' });
945 }
946});
947
948// DELETE /api/attendance/:id
949app.delete('/api/attendance/:id', requireAuth, async (req, res) => {
950 try {
951 await pool.query('DELETE FROM project.attendance WHERE attendance_id=$1', [req.params.id]);
952 res.json({ message: 'Attendance record deleted.' });
953 } catch (err) {
954 console.error(err.message);
955 res.status(500).json({ error: 'Server error.' });
956 }
957});
958
959// =============================================================
960// VENUES (UC0006)
961// =============================================================
962
963// GET /api/venues — all venues with type name
964app.get('/api/venues', requireAuth, async (req, res) => {
965 try {
966 const result = await pool.query(
967 `SELECT v.*, vt.type_name
968 FROM project.venue v
969 JOIN project.venue_type vt ON v.type_id = vt.type_id
970 ORDER BY v.city, v.name`
971 );
972 res.json(result.rows);
973 } catch (err) {
974 console.error(err.message);
975 res.status(500).json({ error: 'Server error.' });
976 }
977});
978
979// GET /api/weddings/:wid/venue-bookings
980app.get('/api/weddings/:wid/venue-bookings', requireAuth, async (req, res) => {
981 try {
982 const result = await pool.query(
983 `SELECT vb.*, v.name AS venue_name, v.location, v.city
984 FROM project.venue_booking vb
985 JOIN project.venue v ON vb.venue_id = v.venue_id
986 WHERE vb.wedding_id = $1
987 ORDER BY vb.date`,
988 [req.params.wid]
989 );
990 res.json(result.rows);
991 } catch (err) {
992 console.error(err.message);
993 res.status(500).json({ error: 'Server error.' });
994 }
995});
996
997// POST /api/weddings/:wid/venue-bookings
998app.post('/api/weddings/:wid/venue-bookings', requireAuth, async (req, res) => {
999 const { date, start_time, end_time, status, price, venue_id } = req.body;
1000 if (!date || !start_time || !end_time || !venue_id)
1001 return res.status(400).json({ error: 'date, start_time, end_time and venue_id are required.' });
1002
1003 // Check availability: no overlapping booking for same venue on same date
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
1009 const conflict = await pool.query(
1010 `SELECT booking_id FROM project.venue_booking
1011 WHERE venue_id=$1 AND "date"=$2
1012 AND NOT (end_time <= $3 OR start_time >= $4)`,
1013 [venue_id, date, start_time, end_time]
1014 );
1015 if (conflict.rows.length > 0)
1016 return res.status(409).json({ error: 'Venue is already booked during this time slot.' });
1017
1018 const result = await pool.query(
1019 `INSERT INTO project.venue_booking("date",start_time,end_time,status,price,venue_id,wedding_id)
1020 VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`,
1021 [date, start_time, end_time, status || 'confirmed', price || 0, venue_id, req.params.wid]
1022 );
1023 res.status(201).json(result.rows[0]);
1024 } catch (err) {
1025 console.error(err.message);
1026 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message });
1027 res.status(500).json({ error: 'Server error.' });
1028 }
1029});
1030
1031// DELETE /api/venue-bookings/:id
1032app.delete('/api/venue-bookings/:id', requireAuth, async (req, res) => {
1033 try {
1034 await pool.query('DELETE FROM project.venue_booking WHERE booking_id=$1', [req.params.id]);
1035 res.json({ message: 'Venue booking cancelled.' });
1036 } catch (err) {
1037 console.error(err.message);
1038 res.status(500).json({ error: 'Server error.' });
1039 }
1040});
1041
1042// =============================================================
1043// BANDS (UC0007)
1044// =============================================================
1045
1046// GET /api/bands
1047app.get('/api/bands', requireAuth, async (req, res) => {
1048 try {
1049 const result = await pool.query('SELECT * FROM project.band ORDER BY band_name');
1050 res.json(result.rows);
1051 } catch (err) {
1052 console.error(err.message);
1053 res.status(500).json({ error: 'Server error.' });
1054 }
1055});
1056
1057// GET /api/weddings/:wid/band-bookings
1058app.get('/api/weddings/:wid/band-bookings', requireAuth, async (req, res) => {
1059 try {
1060 const result = await pool.query(
1061 `SELECT bb.*, b.band_name, b.genre
1062 FROM project.band_booking bb
1063 JOIN project.band b ON bb.band_id = b.band_id
1064 WHERE bb.wedding_id=$1 ORDER BY bb.date`,
1065 [req.params.wid]
1066 );
1067 res.json(result.rows);
1068 } catch (err) {
1069 console.error(err.message);
1070 res.status(500).json({ error: 'Server error.' });
1071 }
1072});
1073
1074// POST /api/weddings/:wid/band-bookings
1075app.post('/api/weddings/:wid/band-bookings', requireAuth, async (req, res) => {
1076 const { date, start_time, end_time, status, band_id } = req.body;
1077 if (!date || !start_time || !end_time || !band_id)
1078 return res.status(400).json({ error: 'date, start_time, end_time and band_id are required.' });
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);
1083 const result = await pool.query(
1084 `INSERT INTO project.band_booking("date",start_time,end_time,status,band_id,wedding_id)
1085 VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`,
1086 [date, start_time, end_time, status || 'confirmed', band_id, req.params.wid]
1087 );
1088 res.status(201).json(result.rows[0]);
1089 } catch (err) {
1090 console.error(err.message);
1091 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message });
1092 res.status(500).json({ error: 'Server error.' });
1093 }
1094});
1095
1096// DELETE /api/band-bookings/:id
1097app.delete('/api/band-bookings/:id', requireAuth, async (req, res) => {
1098 try {
1099 await pool.query('DELETE FROM project.band_booking WHERE booking_id=$1', [req.params.id]);
1100 res.json({ message: 'Band booking removed.' });
1101 } catch (err) {
1102 console.error(err.message);
1103 res.status(500).json({ error: 'Server error.' });
1104 }
1105});
1106
1107// =============================================================
1108// PHOTOGRAPHERS (UC0008)
1109// =============================================================
1110
1111// GET /api/photographers
1112app.get('/api/photographers', requireAuth, async (req, res) => {
1113 try {
1114 const result = await pool.query('SELECT * FROM project.photographer ORDER BY name');
1115 res.json(result.rows);
1116 } catch (err) {
1117 console.error(err.message);
1118 res.status(500).json({ error: 'Server error.' });
1119 }
1120});
1121
1122// GET /api/weddings/:wid/photographer-bookings
1123app.get('/api/weddings/:wid/photographer-bookings', requireAuth, async (req, res) => {
1124 try {
1125 const result = await pool.query(
1126 `SELECT pb.*, p.name AS photographer_name, p.email AS photographer_email
1127 FROM project.photographer_booking pb
1128 JOIN project.photographer p ON pb.photographer_id = p.photographer_id
1129 WHERE pb.wedding_id=$1 ORDER BY pb.date`,
1130 [req.params.wid]
1131 );
1132 res.json(result.rows);
1133 } catch (err) {
1134 console.error(err.message);
1135 res.status(500).json({ error: 'Server error.' });
1136 }
1137});
1138
1139// POST /api/weddings/:wid/photographer-bookings
1140app.post('/api/weddings/:wid/photographer-bookings', requireAuth, async (req, res) => {
1141 const { date, start_time, end_time, status, photographer_id } = req.body;
1142 if (!date || !start_time || !end_time || !photographer_id)
1143 return res.status(400).json({ error: 'date, start_time, end_time and photographer_id are required.' });
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);
1148 const result = await pool.query(
1149 `INSERT INTO project.photographer_booking("date",start_time,end_time,status,photographer_id,wedding_id)
1150 VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`,
1151 [date, start_time, end_time, status || 'confirmed', photographer_id, req.params.wid]
1152 );
1153 res.status(201).json(result.rows[0]);
1154 } catch (err) {
1155 console.error(err.message);
1156 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message });
1157 res.status(500).json({ error: 'Server error.' });
1158 }
1159});
1160
1161// DELETE /api/photographer-bookings/:id
1162app.delete('/api/photographer-bookings/:id', requireAuth, async (req, res) => {
1163 try {
1164 await pool.query('DELETE FROM project.photographer_booking WHERE booking_id=$1', [req.params.id]);
1165 res.json({ message: 'Photographer booking removed.' });
1166 } catch (err) {
1167 console.error(err.message);
1168 res.status(500).json({ error: 'Server error.' });
1169 }
1170});
1171
1172// =============================================================
1173// CHURCHES + PRIESTS
1174// =============================================================
1175
1176// GET /api/churches — all churches with their linked priest
1177app.get('/api/churches', requireAuth, async (req, res) => {
1178 try {
1179 const result = await pool.query(
1180 `SELECT c.*, p.name AS priest_name, p.contact AS priest_contact
1181 FROM project.church c
1182 LEFT JOIN project.priest p ON p.church_id = c.church_id
1183 ORDER BY c.name`
1184 );
1185 res.json(result.rows);
1186 } catch (err) {
1187 console.error(err.message);
1188 res.status(500).json({ error: 'Server error.' });
1189 }
1190});
1191
1192// GET /api/weddings/:wid/church-bookings
1193app.get('/api/weddings/:wid/church-bookings', requireAuth, async (req, res) => {
1194 try {
1195 const result = await pool.query(
1196 `SELECT cb.*, c.name AS church_name, c.location, c.contact,
1197 p.name AS priest_name, p.contact AS priest_contact
1198 FROM project.church_booking cb
1199 JOIN project.church c ON cb.church_id = c.church_id
1200 LEFT JOIN project.priest p ON p.church_id = c.church_id
1201 WHERE cb.wedding_id=$1 ORDER BY cb.date`,
1202 [req.params.wid]
1203 );
1204 res.json(result.rows);
1205 } catch (err) {
1206 console.error(err.message);
1207 res.status(500).json({ error: 'Server error.' });
1208 }
1209});
1210
1211// POST /api/weddings/:wid/church-bookings
1212app.post('/api/weddings/:wid/church-bookings', requireAuth, async (req, res) => {
1213 const { date, start_time, end_time, status, church_id } = req.body;
1214 if (!date || !start_time || !end_time || !church_id)
1215 return res.status(400).json({ error: 'date, start_time, end_time and church_id are required.' });
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);
1220 const result = await pool.query(
1221 `INSERT INTO project.church_booking("date",start_time,end_time,status,church_id,wedding_id)
1222 VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`,
1223 [date, start_time, end_time, status || 'confirmed', church_id, req.params.wid]
1224 );
1225 res.status(201).json(result.rows[0]);
1226 } catch (err) {
1227 console.error(err.message);
1228 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message });
1229 res.status(500).json({ error: 'Server error.' });
1230 }
1231});
1232
1233// DELETE /api/church-bookings/:id
1234app.delete('/api/church-bookings/:id', requireAuth, async (req, res) => {
1235 try {
1236 await pool.query('DELETE FROM project.church_booking WHERE booking_id=$1', [req.params.id]);
1237 res.json({ message: 'Church booking removed.' });
1238 } catch (err) {
1239 console.error(err.message);
1240 res.status(500).json({ error: 'Server error.' });
1241 }
1242});
1243
1244// =============================================================
1245// REGISTRARS
1246// =============================================================
1247
1248// GET /api/registrars
1249app.get('/api/registrars', requireAuth, async (req, res) => {
1250 try {
1251 const result = await pool.query('SELECT * FROM project.registrar ORDER BY name');
1252 res.json(result.rows);
1253 } catch (err) {
1254 console.error(err.message);
1255 res.status(500).json({ error: 'Server error.' });
1256 }
1257});
1258
1259// GET /api/weddings/:wid/registrar-bookings
1260app.get('/api/weddings/:wid/registrar-bookings', requireAuth, async (req, res) => {
1261 try {
1262 const result = await pool.query(
1263 `SELECT rb.*, r.name AS registrar_name, r.location
1264 FROM project.registrar_booking rb
1265 JOIN project.registrar r ON rb.registrar_id = r.registrar_id
1266 WHERE rb.wedding_id=$1 ORDER BY rb.date`,
1267 [req.params.wid]
1268 );
1269 res.json(result.rows);
1270 } catch (err) {
1271 console.error(err.message);
1272 res.status(500).json({ error: 'Server error.' });
1273 }
1274});
1275
1276// POST /api/weddings/:wid/registrar-bookings
1277app.post('/api/weddings/:wid/registrar-bookings', requireAuth, async (req, res) => {
1278 const { date, start_time, end_time, status, registrar_id } = req.body;
1279 if (!date || !start_time || !end_time || !registrar_id)
1280 return res.status(400).json({ error: 'date, start_time, end_time and registrar_id are required.' });
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
1287 const result = await pool.query(
1288 `INSERT INTO project.registrar_booking("date",start_time,end_time,status,registrar_id,wedding_id)
1289 VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`,
1290 [date, start_time, end_time, status || 'confirmed', registrar_id, req.params.wid]
1291 );
1292 res.status(201).json(result.rows[0]);
1293 } catch (err) {
1294 console.error(err.message);
1295 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message });
1296 res.status(500).json({ error: 'Server error.' });
1297 }
1298});
1299
1300// DELETE /api/registrar-bookings/:id
1301app.delete('/api/registrar-bookings/:id', requireAuth, async (req, res) => {
1302 try {
1303 await pool.query('DELETE FROM project.registrar_booking WHERE booking_id=$1', [req.params.id]);
1304 res.json({ message: 'Registrar booking removed.' });
1305 } catch (err) {
1306 console.error(err.message);
1307 res.status(500).json({ error: 'Server error.' });
1308 }
1309});
1310
1311// =============================================================
1312// START SERVER
1313// =============================================================
1314app.listen(PORT, () => {
1315 console.log(`\n🌸 Wedding Planner server running at http://localhost:${PORT}`);
1316 console.log(` Dashboard → http://localhost:${PORT}/Wedding_Planner.html`);
1317 console.log(` Login → http://localhost:${PORT}/login.html`);
1318 console.log(` RSVP page → http://localhost:${PORT}/rsvp.html\n`);
1319});
Note: See TracBrowser for help on using the repository browser.