source: server.js@ b374c85

main
Last change on this file since b374c85 was d8deee6, checked in by GitHub <noreply@โ€ฆ>, 3 months ago

Add files via upload

  • Property mode set to 100644
File size: 43.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
65console.log('๐Ÿ“ก Attempting to connect to PostgreSQL...');
66console.log(` Host: ${process.env.DB_HOST}:${process.env.DB_PORT}`);
67console.log(` Database: ${process.env.DB_NAME}`);
68
69pool.connect()
70 .then(client => {
71 console.log('โœ… Connected to PostgreSQL database!');
72 client.release();
73 })
74 .catch(err => {
75 console.error('โŒ DB connection error:', err.message);
76 console.error(' Make sure SSH tunnel is running on port 9999');
77 console.error(' SSH: ssh -L 9999:db_server:5432 t_wedding_planner2025@194.149.135.130');
78 console.error(' Full error:', err.stack);
79 });
80
81// Helper: always use the project schema
82const Q = (text, params) => pool.query(`SET search_path TO project; ${text}`, params);
83
84// =============================================================
85// EMAIL TRANSPORTER
86// =============================================================
87const transporter = nodemailer.createTransport({
88 service: 'gmail',
89 auth: {
90 user: process.env.EMAIL_USER,
91 pass: process.env.EMAIL_PASS
92 }
93});
94
95// =============================================================
96// AUTH MIDDLEWARE โ€” protect routes that require login
97// =============================================================
98function requireAuth(req, res, next) {
99 if (req.session && req.session.user) return next();
100 return res.status(401).json({ error: 'Not authenticated. Please log in.' });
101}
102
103// =============================================================
104// AUTH ROUTES
105// =============================================================
106
107// POST /api/auth/register
108app.post('/api/auth/register', async (req, res) => {
109 const { first_name, last_name, email, password, phone_number, gender, birthday } = req.body;
110 if (!first_name || !last_name || !email || !password)
111 return res.status(400).json({ error: 'First name, last name, email and password are required.' });
112
113 try {
114 // Check email not already taken
115 const exists = await pool.query(
116 'SELECT user_id FROM project."user" WHERE email = $1', [email]
117 );
118 if (exists.rows.length > 0)
119 return res.status(409).json({ error: 'Email already registered.' });
120
121 const hash = await bcrypt.hash(password, 10);
122
123 const result = await pool.query(
124 `INSERT INTO project."user"(first_name, last_name, email, password_hash, phone_number, gender, birthday)
125 VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING user_id, first_name, last_name, email, gender`,
126 [first_name, last_name, email, hash, phone_number || null, gender || null, birthday || null]
127 );
128
129 const user = result.rows[0];
130 req.session.user = user;
131 res.status(201).json({ message: 'Registered successfully.', user });
132 } catch (err) {
133 console.error('Register error:', err.message);
134 res.status(500).json({ error: 'Server error during registration.' });
135 }
136});
137
138// POST /api/auth/login
139app.post('/api/auth/login', async (req, res) => {
140 const { email, password } = req.body;
141 if (!email || !password)
142 return res.status(400).json({ error: 'Email and password are required.' });
143
144 try {
145 const result = await pool.query(
146 'SELECT * FROM project."user" WHERE email = $1', [email]
147 );
148 if (result.rows.length === 0)
149 return res.status(401).json({ error: 'Invalid email or password.' });
150
151 const user = result.rows[0];
152 const match = await bcrypt.compare(password, user.password_hash);
153 if (!match)
154 return res.status(401).json({ error: 'Invalid email or password.' });
155
156 req.session.user = {
157 user_id: user.user_id,
158 first_name: user.first_name,
159 last_name: user.last_name,
160 email: user.email,
161 gender: user.gender
162 };
163 res.json({ message: 'Logged in.', user: req.session.user });
164 } catch (err) {
165 console.error('Login error:', err.message);
166 res.status(500).json({ error: 'Server error during login.' });
167 }
168});
169
170// POST /api/auth/logout
171app.post('/api/auth/logout', (req, res) => {
172 req.session.destroy(() => res.json({ message: 'Logged out.' }));
173});
174
175// GET /api/auth/me โ€” check current session
176app.get('/api/auth/me', (req, res) => {
177 if (req.session && req.session.user)
178 return res.json({ user: req.session.user });
179 res.status(401).json({ error: 'Not logged in.' });
180});
181
182// =============================================================
183// USER / PROFILE
184// =============================================================
185
186// GET /api/users/:id
187app.get('/api/users/:id', requireAuth, async (req, res) => {
188 try {
189 const result = await pool.query(
190 'SELECT user_id, first_name, last_name, email, phone_number, gender, birthday FROM project."user" WHERE user_id = $1',
191 [req.params.id]
192 );
193 if (result.rows.length === 0) return res.status(404).json({ error: 'User not found.' });
194 res.json(result.rows[0]);
195 } catch (err) {
196 console.error(err.message);
197 res.status(500).json({ error: 'Server error.' });
198 }
199});
200
201// PUT /api/users/:id โ€” update profile
202app.put('/api/users/:id', requireAuth, async (req, res) => {
203 const { first_name, last_name, email, phone_number, gender, birthday } = req.body;
204 try {
205 const result = await pool.query(
206 `UPDATE project."user"
207 SET first_name=$1, last_name=$2, email=$3, phone_number=$4, gender=$5, birthday=$6
208 WHERE user_id=$7 RETURNING user_id, first_name, last_name, email, phone_number, gender, birthday`,
209 [first_name, last_name, email, phone_number, gender, birthday || null, req.params.id]
210 );
211 if (result.rows.length === 0) return res.status(404).json({ error: 'User not found.' });
212 // Update session too
213 req.session.user = { ...req.session.user, ...result.rows[0] };
214 res.json(result.rows[0]);
215 } catch (err) {
216 console.error(err.message);
217 res.status(500).json({ error: 'Server error.' });
218 }
219});
220
221// =============================================================
222// WEDDINGS (UC0003)
223// =============================================================
224
225// GET /api/weddings โ€” get wedding(s) for logged-in user
226app.get('/api/weddings', requireAuth, async (req, res) => {
227 try {
228 const result = await pool.query(
229 'SELECT * FROM project.wedding WHERE user_id = $1 ORDER BY date',
230 [req.session.user.user_id]
231 );
232 res.json(result.rows);
233 } catch (err) {
234 console.error(err.message);
235 res.status(500).json({ error: 'Server error.' });
236 }
237});
238
239// GET /api/weddings/:id
240app.get('/api/weddings/:id', requireAuth, async (req, res) => {
241 try {
242 const result = await pool.query(
243 'SELECT * FROM project.wedding WHERE wedding_id = $1 AND user_id = $2',
244 [req.params.id, req.session.user.user_id]
245 );
246 if (result.rows.length === 0) return res.status(404).json({ error: 'Wedding not found.' });
247 res.json(result.rows[0]);
248 } catch (err) {
249 console.error(err.message);
250 res.status(500).json({ error: 'Server error.' });
251 }
252});
253
254// POST /api/weddings โ€” create wedding
255app.post('/api/weddings', requireAuth, async (req, res) => {
256 const { date, budget, notes } = req.body;
257 if (!date) return res.status(400).json({ error: 'Wedding date is required.' });
258 try {
259 const result = await pool.query(
260 'INSERT INTO project.wedding("date", budget, notes, user_id) VALUES ($1,$2,$3,$4) RETURNING *',
261 [date, budget || null, notes || null, req.session.user.user_id]
262 );
263 res.status(201).json(result.rows[0]);
264 } catch (err) {
265 console.error(err.message);
266 res.status(500).json({ error: 'Server error.' });
267 }
268});
269
270// PUT /api/weddings/:id
271app.put('/api/weddings/:id', requireAuth, async (req, res) => {
272 const { date, budget, notes } = req.body;
273 try {
274 const result = await pool.query(
275 `UPDATE project.wedding SET "date"=$1, budget=$2, notes=$3
276 WHERE wedding_id=$4 AND user_id=$5 RETURNING *`,
277 [date, budget || null, notes || null, req.params.id, req.session.user.user_id]
278 );
279 if (result.rows.length === 0) return res.status(404).json({ error: 'Wedding not found.' });
280 res.json(result.rows[0]);
281 } catch (err) {
282 console.error(err.message);
283 res.status(500).json({ error: 'Server error.' });
284 }
285});
286
287// DELETE /api/weddings/:id
288app.delete('/api/weddings/:id', requireAuth, async (req, res) => {
289 try {
290 await pool.query(
291 'DELETE FROM project.wedding WHERE wedding_id=$1 AND user_id=$2',
292 [req.params.id, req.session.user.user_id]
293 );
294 res.json({ message: 'Wedding deleted.' });
295 } catch (err) {
296 console.error(err.message);
297 res.status(500).json({ error: 'Server error.' });
298 }
299});
300
301// =============================================================
302// EVENTS (UC0005)
303// =============================================================
304
305// GET /api/weddings/:wid/events
306app.get('/api/weddings/:wid/events', requireAuth, async (req, res) => {
307 try {
308 const result = await pool.query(
309 'SELECT * FROM project.event WHERE wedding_id=$1 ORDER BY "date", start_time',
310 [req.params.wid]
311 );
312 res.json(result.rows);
313 } catch (err) {
314 console.error(err.message);
315 res.status(500).json({ error: 'Server error.' });
316 }
317});
318
319// POST /api/weddings/:wid/events
320app.post('/api/weddings/:wid/events', requireAuth, async (req, res) => {
321 const { event_type, date, start_time, end_time, status } = req.body;
322 if (!event_type || !date || !start_time || !end_time)
323 return res.status(400).json({ error: 'event_type, date, start_time, end_time are required.' });
324 try {
325 const result = await pool.query(
326 `INSERT INTO project.event(event_type,"date",start_time,end_time,status,wedding_id)
327 VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`,
328 [event_type, date, start_time, end_time, status || 'scheduled', req.params.wid]
329 );
330 res.status(201).json(result.rows[0]);
331 } catch (err) {
332 console.error(err.message);
333 res.status(500).json({ error: 'Server error.' });
334 }
335});
336
337// PUT /api/events/:id
338app.put('/api/events/:id', requireAuth, async (req, res) => {
339 const { event_type, date, start_time, end_time, status } = req.body;
340 try {
341 const result = await pool.query(
342 `UPDATE project.event SET event_type=$1,"date"=$2,start_time=$3,end_time=$4,status=$5
343 WHERE event_id=$6 RETURNING *`,
344 [event_type, date, start_time, end_time, status, req.params.id]
345 );
346 if (result.rows.length === 0) return res.status(404).json({ error: 'Event not found.' });
347 res.json(result.rows[0]);
348 } catch (err) {
349 console.error(err.message);
350 res.status(500).json({ error: 'Server error.' });
351 }
352});
353
354// DELETE /api/events/:id
355app.delete('/api/events/:id', requireAuth, async (req, res) => {
356 try {
357 await pool.query('DELETE FROM project.event WHERE event_id=$1', [req.params.id]);
358 res.json({ message: 'Event deleted.' });
359 } catch (err) {
360 console.error(err.message);
361 res.status(500).json({ error: 'Server error.' });
362 }
363});
364
365// =============================================================
366// GUESTS (UC0004)
367// =============================================================
368
369// GET /api/weddings/:wid/guests
370app.get('/api/weddings/:wid/guests', requireAuth, async (req, res) => {
371 try {
372 const result = await pool.query(
373 'SELECT guest_id, first_name, last_name, email, role, wedding_id FROM project.guest WHERE wedding_id=$1 ORDER BY last_name, first_name',
374 [req.params.wid]
375 );
376 res.json(result.rows);
377 } catch (err) {
378 console.error(err.message);
379 res.status(500).json({ error: 'Server error.' });
380 }
381});
382
383// POST /api/weddings/:wid/guests โ€” add guest + send email invite
384app.post('/api/weddings/:wid/guests', requireAuth, async (req, res) => {
385 const { first_name, last_name, email, role } = req.body;
386 if (!first_name || !last_name)
387 return res.status(400).json({ error: 'First name and last name are required.' });
388
389 try {
390 // Insert guest
391 const result = await pool.query(
392 'INSERT INTO project.guest(first_name,last_name,email,wedding_id) VALUES ($1,$2,$3,$4) RETURNING *',
393 [first_name, last_name, email || null, req.params.wid]
394 );
395 const guest = result.rows[0];
396
397 // Fetch wedding info for the email
398 const weddingResult = await pool.query(
399 `SELECT w."date", u.first_name AS owner_first, u.last_name AS owner_last
400 FROM project.wedding w
401 JOIN project."user" u ON w.user_id = u.user_id
402 WHERE w.wedding_id = $1`,
403 [req.params.wid]
404 );
405 const wedding = weddingResult.rows[0];
406
407 // Fetch all events for this wedding so guest can RSVP
408 const eventsResult = await pool.query(
409 'SELECT * FROM project.event WHERE wedding_id=$1 ORDER BY "date", start_time',
410 [req.params.wid]
411 );
412 const events = eventsResult.rows;
413
414 // Create initial RSVP records with status "invited" for each event
415 for (const event of events) {
416 try {
417 await pool.query(
418 `INSERT INTO project.event_rsvp(status, response_date, guest_id, event_id)
419 VALUES ('invited', CURRENT_DATE, $1, $2)
420 ON CONFLICT (guest_id, event_id) DO NOTHING`,
421 [guest.guest_id, event.event_id]
422 );
423 } catch (rsvpErr) {
424 console.warn(`โš ๏ธ Could not create RSVP record for guest ${guest.guest_id}, event ${event.event_id}:`, rsvpErr.message);
425 }
426 }
427
428 // Send email invitation if email provided
429 if (email && events.length > 0) {
430 // Generate a secure token per event for the RSVP links
431 const eventLinks = events.map(ev => {
432 const token = crypto
433 .createHmac('sha256', 'wedding_rsvp_secret')
434 .update(`${guest.guest_id}-${ev.event_id}`)
435 .digest('hex');
436 const base = `http://localhost:${PORT}`;
437 return `
438 <tr>
439 <td style="padding:8px 0; font-size:15px; color:#2c1f1f;">
440 <strong>${ev.event_type}</strong> โ€” ${ev.date} ${ev.start_time}โ€“${ev.end_time}
441 </td>
442 <td style="padding:8px 0; text-align:right;">
443 <a href="${base}/rsvp.html?guest=${guest.guest_id}&event=${ev.event_id}&token=${token}&action=accepted"
444 style="background:#3a9e6b;color:#fff;padding:6px 14px;border-radius:6px;text-decoration:none;margin-right:6px;font-size:13px;">
445 โœ… Accept
446 </a>
447 <a href="${base}/rsvp.html?guest=${guest.guest_id}&event=${ev.event_id}&token=${token}&action=declined"
448 style="background:#c94545;color:#fff;padding:6px 14px;border-radius:6px;text-decoration:none;font-size:13px;">
449 โŒ Decline
450 </a>
451 </td>
452 </tr>`;
453 }).join('');
454
455 const mailOptions = {
456 from: `"Wedding Planner" <${process.env.EMAIL_USER}>`,
457 to: email,
458 subject: `You're Invited! ๐Ÿ’ ${wedding.owner_first} & ${wedding.owner_last}'s Wedding`,
459 html: `
460 <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;">
461 <div style="background:linear-gradient(135deg,#d4606a,#e8949c);padding:32px;text-align:center;">
462 <div style="font-size:36px;margin-bottom:8px;">๐Ÿ’</div>
463 <h1 style="color:#fff;font-size:26px;margin:0;">You're Invited!</h1>
464 <p style="color:rgba(255,255,255,0.9);margin:8px 0 0;">Wedding of ${wedding.owner_first} &amp; ${wedding.owner_last}</p>
465 </div>
466 <div style="padding:32px;">
467 <p style="font-size:16px;color:#2c1f1f;">Dear <strong>${first_name} ${last_name}</strong>,</p>
468 <p style="font-size:15px;color:#6b4f4f;line-height:1.6;">
469 We are delighted to invite you to celebrate the wedding of
470 <strong>${wedding.owner_first} &amp; ${wedding.owner_last}</strong>
471 on <strong>${wedding.date}</strong>.
472 </p>
473 <p style="font-size:15px;color:#2c1f1f;font-weight:600;margin-top:24px;">Please RSVP for each event:</p>
474 <table style="width:100%;border-collapse:collapse;">
475 ${eventLinks}
476 </table>
477 <p style="font-size:13px;color:#a08080;margin-top:24px;">
478 We look forward to celebrating with you. If you have any questions, please contact us directly.
479 </p>
480 </div>
481 <div style="background:#f2e8db;padding:16px;text-align:center;">
482 <p style="font-size:12px;color:#a08080;margin:0;">Wedding Planner App ยท Sent with love ๐Ÿ’Œ</p>
483 </div>
484 </div>`
485 };
486
487 try {
488 await transporter.sendMail(mailOptions);
489 console.log(`๐Ÿ“ง Invite sent to ${email}`);
490 } catch (mailErr) {
491 // Don't fail the guest insert if email fails โ€” just log it
492 console.warn('โš ๏ธ Email send failed (guest still added):', mailErr.message);
493 }
494 }
495
496 res.status(201).json({ guest, emailSent: !!(email && events.length > 0) });
497 } catch (err) {
498 console.error(err.message);
499 res.status(500).json({ error: 'Server error.' });
500 }
501});
502
503// PUT /api/guests/:id
504app.put('/api/guests/:id', requireAuth, async (req, res) => {
505 const { first_name, last_name, email, role } = req.body;
506 try {
507 const result = await pool.query(
508 'UPDATE project.guest SET first_name=$1,last_name=$2,email=$3,role=$4 WHERE guest_id=$5 RETURNING *',
509 [first_name, last_name, email || null, role || 'Guest', req.params.id]
510 );
511 if (result.rows.length === 0) return res.status(404).json({ error: 'Guest not found.' });
512 res.json(result.rows[0]);
513 } catch (err) {
514 console.error(err.message);
515 res.status(500).json({ error: 'Server error.' });
516 }
517});
518
519// DELETE /api/guests/:id
520app.delete('/api/guests/:id', requireAuth, async (req, res) => {
521 try {
522 await pool.query('DELETE FROM project.guest WHERE guest_id=$1', [req.params.id]);
523 res.json({ message: 'Guest deleted.' });
524 } catch (err) {
525 console.error(err.message);
526 res.status(500).json({ error: 'Server error.' });
527 }
528});
529
530// =============================================================
531// RSVP (UC0009)
532// =============================================================
533
534// GET /api/rsvp?guest_id=&event_id= โ€” check existing RSVP
535app.get('/api/rsvp', async (req, res) => {
536 const { guest_id, event_id } = req.query;
537 try {
538 const result = await pool.query(
539 'SELECT * FROM project.event_rsvp WHERE guest_id=$1 AND event_id=$2',
540 [guest_id, event_id]
541 );
542 res.json(result.rows[0] || null);
543 } catch (err) {
544 console.error(err.message);
545 res.status(500).json({ error: 'Server error.' });
546 }
547});
548
549// POST /api/rsvp โ€” guest submits RSVP (called from rsvp.html, no auth needed)
550app.post('/api/rsvp', async (req, res) => {
551 const { guest_id, event_id, status, token } = req.body;
552 if (!guest_id || !event_id || !status)
553 return res.status(400).json({ error: 'guest_id, event_id and status are required.' });
554
555 // Verify token (protects against random submissions)
556 const expected = crypto
557 .createHmac('sha256', 'wedding_rsvp_secret')
558 .update(`${guest_id}-${event_id}`)
559 .digest('hex');
560 if (token !== expected)
561 return res.status(403).json({ error: 'Invalid or expired RSVP link.' });
562
563 const validStatuses = ['accepted', 'declined', 'pending', 'invited'];
564 if (!validStatuses.includes(status))
565 return res.status(400).json({ error: 'Status must be accepted, declined, pending, or invited.' });
566
567 try {
568 // Upsert: update if exists, insert if not
569 const result = await pool.query(
570 `INSERT INTO project.event_rsvp(status, response_date, guest_id, event_id)
571 VALUES ($1, CURRENT_DATE, $2, $3)
572 ON CONFLICT (guest_id, event_id)
573 DO UPDATE SET status=$1, response_date=CURRENT_DATE
574 RETURNING *`,
575 [status, guest_id, event_id]
576 );
577
578 // Fetch guest + event info for the confirmation response
579 const guestInfo = await pool.query(
580 `SELECT g.first_name, g.last_name, e.event_type, e.date, e.start_time, e.end_time
581 FROM project.guest g, project.event e
582 WHERE g.guest_id=$1 AND e.event_id=$2`,
583 [guest_id, event_id]
584 );
585
586 res.json({
587 rsvp: result.rows[0],
588 guest: guestInfo.rows[0] || null
589 });
590 } catch (err) {
591 console.error(err.message);
592 res.status(500).json({ error: 'Server error.' });
593 }
594});
595
596// GET /api/weddings/:wid/rsvp โ€” all RSVPs for a wedding (for admin view)
597app.get('/api/weddings/:wid/rsvp', requireAuth, async (req, res) => {
598 try {
599 const result = await pool.query(
600 `SELECT er.response_id, er.status, er.response_date,
601 g.guest_id, g.first_name, g.last_name, g.email,
602 e.event_id, e.event_type, e.date AS event_date
603 FROM project.event_rsvp er
604 JOIN project.guest g ON er.guest_id = g.guest_id
605 JOIN project.event e ON er.event_id = e.event_id
606 WHERE e.wedding_id = $1
607 ORDER BY e.date, g.last_name`,
608 [req.params.wid]
609 );
610 res.json(result.rows);
611 } catch (err) {
612 console.error(err.message);
613 res.status(500).json({ error: 'Server error.' });
614 }
615});
616
617// =============================================================
618// ATTENDANCE / SEATING (UC0011)
619// =============================================================
620
621// GET /api/weddings/:wid/attendance โ€” full seating list for a wedding
622app.get('/api/weddings/:wid/attendance', requireAuth, async (req, res) => {
623 try {
624 const result = await pool.query(
625 `SELECT a.attendance_id, a.status, a.table_number, a.role,
626 g.guest_id, g.first_name, g.last_name,
627 e.event_id, e.event_type
628 FROM project.attendance a
629 JOIN project.guest g ON a.guest_id = g.guest_id
630 JOIN project.event e ON a.event_id = e.event_id
631 WHERE e.wedding_id = $1
632 ORDER BY a.table_number NULLS LAST, g.last_name`,
633 [req.params.wid]
634 );
635 res.json(result.rows);
636 } catch (err) {
637 console.error(err.message);
638 res.status(500).json({ error: 'Server error.' });
639 }
640});
641
642// GET /api/events/:eid/attendance โ€” seating for a specific event
643app.get('/api/events/:eid/attendance', requireAuth, async (req, res) => {
644 try {
645 const result = await pool.query(
646 `SELECT a.attendance_id, a.status, a.table_number, a.role,
647 g.guest_id, g.first_name, g.last_name
648 FROM project.attendance a
649 JOIN project.guest g ON a.guest_id = g.guest_id
650 WHERE a.event_id = $1
651 ORDER BY a.table_number NULLS LAST, g.last_name`,
652 [req.params.eid]
653 );
654 res.json(result.rows);
655 } catch (err) {
656 console.error(err.message);
657 res.status(500).json({ error: 'Server error.' });
658 }
659});
660
661// POST /api/attendance โ€” assign guest to event with table + role
662app.post('/api/attendance', requireAuth, async (req, res) => {
663 const { table_number, role, guest_id, event_id } = req.body;
664 if (!guest_id || !event_id || !role)
665 return res.status(400).json({ error: 'guest_id, event_id and role are required.' });
666 try {
667 // Fetch the guest's RSVP status for this event
668 const rsvpResult = await pool.query(
669 'SELECT status FROM project.event_rsvp WHERE guest_id=$1 AND event_id=$2',
670 [guest_id, event_id]
671 );
672 const rsvpStatus = rsvpResult.rows[0]?.status || 'pending';
673
674 const result = await pool.query(
675 `INSERT INTO project.attendance(status, table_number, role, guest_id, event_id)
676 VALUES ($1,$2,$3,$4,$5)
677 ON CONFLICT (guest_id, event_id)
678 DO UPDATE SET status=$1, table_number=$2, role=$3
679 RETURNING *`,
680 [rsvpStatus, table_number || null, role, guest_id, event_id]
681 );
682 res.status(201).json(result.rows[0]);
683 } catch (err) {
684 console.error(err.message);
685 res.status(500).json({ error: 'Server error.' });
686 }
687});
688
689// PUT /api/attendance/:id โ€” update seating assignment
690app.put('/api/attendance/:id', requireAuth, async (req, res) => {
691 const { table_number, role } = req.body;
692 try {
693 // Fetch the attendance record to get guest_id and event_id
694 const attendanceRecord = await pool.query(
695 'SELECT guest_id, event_id FROM project.attendance WHERE attendance_id=$1',
696 [req.params.id]
697 );
698 if (attendanceRecord.rows.length === 0)
699 return res.status(404).json({ error: 'Record not found.' });
700
701 const { guest_id, event_id } = attendanceRecord.rows[0];
702
703 // Fetch the guest's RSVP status for this event
704 const rsvpResult = await pool.query(
705 'SELECT status FROM project.event_rsvp WHERE guest_id=$1 AND event_id=$2',
706 [guest_id, event_id]
707 );
708 const rsvpStatus = rsvpResult.rows[0]?.status || 'pending';
709
710 const result = await pool.query(
711 `UPDATE project.attendance SET status=$1, table_number=$2, role=$3
712 WHERE attendance_id=$4 RETURNING *`,
713 [rsvpStatus, table_number || null, role, req.params.id]
714 );
715 if (result.rows.length === 0) return res.status(404).json({ error: 'Record not found.' });
716 res.json(result.rows[0]);
717 } catch (err) {
718 console.error(err.message);
719 res.status(500).json({ error: 'Server error.' });
720 }
721});
722
723// DELETE /api/attendance/:id
724app.delete('/api/attendance/:id', requireAuth, async (req, res) => {
725 try {
726 await pool.query('DELETE FROM project.attendance WHERE attendance_id=$1', [req.params.id]);
727 res.json({ message: 'Attendance record deleted.' });
728 } catch (err) {
729 console.error(err.message);
730 res.status(500).json({ error: 'Server error.' });
731 }
732});
733
734// =============================================================
735// VENUES (UC0006)
736// =============================================================
737
738// GET /api/venues โ€” all venues with type name
739app.get('/api/venues', requireAuth, async (req, res) => {
740 try {
741 const result = await pool.query(
742 `SELECT v.*, vt.type_name
743 FROM project.venue v
744 JOIN project.venue_type vt ON v.type_id = vt.type_id
745 ORDER BY v.city, v.name`
746 );
747 res.json(result.rows);
748 } catch (err) {
749 console.error(err.message);
750 res.status(500).json({ error: 'Server error.' });
751 }
752});
753
754// GET /api/weddings/:wid/venue-bookings
755app.get('/api/weddings/:wid/venue-bookings', requireAuth, async (req, res) => {
756 try {
757 const result = await pool.query(
758 `SELECT vb.*, v.name AS venue_name, v.location, v.city
759 FROM project.venue_booking vb
760 JOIN project.venue v ON vb.venue_id = v.venue_id
761 WHERE vb.wedding_id = $1
762 ORDER BY vb.date`,
763 [req.params.wid]
764 );
765 res.json(result.rows);
766 } catch (err) {
767 console.error(err.message);
768 res.status(500).json({ error: 'Server error.' });
769 }
770});
771
772// POST /api/weddings/:wid/venue-bookings
773app.post('/api/weddings/:wid/venue-bookings', requireAuth, async (req, res) => {
774 const { date, start_time, end_time, status, price, venue_id } = req.body;
775 if (!date || !start_time || !end_time || !venue_id)
776 return res.status(400).json({ error: 'date, start_time, end_time and venue_id are required.' });
777
778 // Check availability: no overlapping booking for same venue on same date
779 try {
780 const conflict = await pool.query(
781 `SELECT booking_id FROM project.venue_booking
782 WHERE venue_id=$1 AND "date"=$2
783 AND NOT (end_time <= $3 OR start_time >= $4)`,
784 [venue_id, date, start_time, end_time]
785 );
786 if (conflict.rows.length > 0)
787 return res.status(409).json({ error: 'Venue is already booked during this time slot.' });
788
789 const result = await pool.query(
790 `INSERT INTO project.venue_booking("date",start_time,end_time,status,price,venue_id,wedding_id)
791 VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`,
792 [date, start_time, end_time, status || 'confirmed', price || 0, venue_id, req.params.wid]
793 );
794 res.status(201).json(result.rows[0]);
795 } catch (err) {
796 console.error(err.message);
797 res.status(500).json({ error: 'Server error.' });
798 }
799});
800
801// DELETE /api/venue-bookings/:id
802app.delete('/api/venue-bookings/:id', requireAuth, async (req, res) => {
803 try {
804 await pool.query('DELETE FROM project.venue_booking WHERE booking_id=$1', [req.params.id]);
805 res.json({ message: 'Venue booking cancelled.' });
806 } catch (err) {
807 console.error(err.message);
808 res.status(500).json({ error: 'Server error.' });
809 }
810});
811
812// =============================================================
813// BANDS (UC0007)
814// =============================================================
815
816// GET /api/bands
817app.get('/api/bands', requireAuth, async (req, res) => {
818 try {
819 const result = await pool.query('SELECT * FROM project.band ORDER BY band_name');
820 res.json(result.rows);
821 } catch (err) {
822 console.error(err.message);
823 res.status(500).json({ error: 'Server error.' });
824 }
825});
826
827// GET /api/weddings/:wid/band-bookings
828app.get('/api/weddings/:wid/band-bookings', requireAuth, async (req, res) => {
829 try {
830 const result = await pool.query(
831 `SELECT bb.*, b.band_name, b.genre
832 FROM project.band_booking bb
833 JOIN project.band b ON bb.band_id = b.band_id
834 WHERE bb.wedding_id=$1 ORDER BY bb.date`,
835 [req.params.wid]
836 );
837 res.json(result.rows);
838 } catch (err) {
839 console.error(err.message);
840 res.status(500).json({ error: 'Server error.' });
841 }
842});
843
844// POST /api/weddings/:wid/band-bookings
845app.post('/api/weddings/:wid/band-bookings', requireAuth, async (req, res) => {
846 const { date, start_time, end_time, status, band_id } = req.body;
847 if (!date || !start_time || !end_time || !band_id)
848 return res.status(400).json({ error: 'date, start_time, end_time and band_id are required.' });
849 try {
850 const result = await pool.query(
851 `INSERT INTO project.band_booking("date",start_time,end_time,status,band_id,wedding_id)
852 VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`,
853 [date, start_time, end_time, status || 'confirmed', band_id, req.params.wid]
854 );
855 res.status(201).json(result.rows[0]);
856 } catch (err) {
857 console.error(err.message);
858 res.status(500).json({ error: 'Server error.' });
859 }
860});
861
862// DELETE /api/band-bookings/:id
863app.delete('/api/band-bookings/:id', requireAuth, async (req, res) => {
864 try {
865 await pool.query('DELETE FROM project.band_booking WHERE booking_id=$1', [req.params.id]);
866 res.json({ message: 'Band booking removed.' });
867 } catch (err) {
868 console.error(err.message);
869 res.status(500).json({ error: 'Server error.' });
870 }
871});
872
873// =============================================================
874// PHOTOGRAPHERS (UC0008)
875// =============================================================
876
877// GET /api/photographers
878app.get('/api/photographers', requireAuth, async (req, res) => {
879 try {
880 const result = await pool.query('SELECT * FROM project.photographer ORDER BY name');
881 res.json(result.rows);
882 } catch (err) {
883 console.error(err.message);
884 res.status(500).json({ error: 'Server error.' });
885 }
886});
887
888// GET /api/weddings/:wid/photographer-bookings
889app.get('/api/weddings/:wid/photographer-bookings', requireAuth, async (req, res) => {
890 try {
891 const result = await pool.query(
892 `SELECT pb.*, p.name AS photographer_name, p.email AS photographer_email
893 FROM project.photographer_booking pb
894 JOIN project.photographer p ON pb.photographer_id = p.photographer_id
895 WHERE pb.wedding_id=$1 ORDER BY pb.date`,
896 [req.params.wid]
897 );
898 res.json(result.rows);
899 } catch (err) {
900 console.error(err.message);
901 res.status(500).json({ error: 'Server error.' });
902 }
903});
904
905// POST /api/weddings/:wid/photographer-bookings
906app.post('/api/weddings/:wid/photographer-bookings', requireAuth, async (req, res) => {
907 const { date, start_time, end_time, status, photographer_id } = req.body;
908 if (!date || !start_time || !end_time || !photographer_id)
909 return res.status(400).json({ error: 'date, start_time, end_time and photographer_id are required.' });
910 try {
911 const result = await pool.query(
912 `INSERT INTO project.photographer_booking("date",start_time,end_time,status,photographer_id,wedding_id)
913 VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`,
914 [date, start_time, end_time, status || 'confirmed', photographer_id, req.params.wid]
915 );
916 res.status(201).json(result.rows[0]);
917 } catch (err) {
918 console.error(err.message);
919 res.status(500).json({ error: 'Server error.' });
920 }
921});
922
923// DELETE /api/photographer-bookings/:id
924app.delete('/api/photographer-bookings/:id', requireAuth, async (req, res) => {
925 try {
926 await pool.query('DELETE FROM project.photographer_booking WHERE booking_id=$1', [req.params.id]);
927 res.json({ message: 'Photographer booking removed.' });
928 } catch (err) {
929 console.error(err.message);
930 res.status(500).json({ error: 'Server error.' });
931 }
932});
933
934// =============================================================
935// CHURCHES + PRIESTS
936// =============================================================
937
938// GET /api/churches โ€” all churches with their linked priest
939app.get('/api/churches', requireAuth, async (req, res) => {
940 try {
941 const result = await pool.query(
942 `SELECT c.*, p.name AS priest_name, p.contact AS priest_contact
943 FROM project.church c
944 LEFT JOIN project.priest p ON p.church_id = c.church_id
945 ORDER BY c.name`
946 );
947 res.json(result.rows);
948 } catch (err) {
949 console.error(err.message);
950 res.status(500).json({ error: 'Server error.' });
951 }
952});
953
954// GET /api/weddings/:wid/church-bookings
955app.get('/api/weddings/:wid/church-bookings', requireAuth, async (req, res) => {
956 try {
957 const result = await pool.query(
958 `SELECT cb.*, c.name AS church_name, c.location, c.contact,
959 p.name AS priest_name, p.contact AS priest_contact
960 FROM project.church_booking cb
961 JOIN project.church c ON cb.church_id = c.church_id
962 LEFT JOIN project.priest p ON p.church_id = c.church_id
963 WHERE cb.wedding_id=$1 ORDER BY cb.date`,
964 [req.params.wid]
965 );
966 res.json(result.rows);
967 } catch (err) {
968 console.error(err.message);
969 res.status(500).json({ error: 'Server error.' });
970 }
971});
972
973// POST /api/weddings/:wid/church-bookings
974app.post('/api/weddings/:wid/church-bookings', requireAuth, async (req, res) => {
975 const { date, start_time, end_time, status, church_id } = req.body;
976 if (!date || !start_time || !end_time || !church_id)
977 return res.status(400).json({ error: 'date, start_time, end_time and church_id are required.' });
978 try {
979 const result = await pool.query(
980 `INSERT INTO project.church_booking("date",start_time,end_time,status,church_id,wedding_id)
981 VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`,
982 [date, start_time, end_time, status || 'confirmed', church_id, req.params.wid]
983 );
984 res.status(201).json(result.rows[0]);
985 } catch (err) {
986 console.error(err.message);
987 res.status(500).json({ error: 'Server error.' });
988 }
989});
990
991// DELETE /api/church-bookings/:id
992app.delete('/api/church-bookings/:id', requireAuth, async (req, res) => {
993 try {
994 await pool.query('DELETE FROM project.church_booking WHERE booking_id=$1', [req.params.id]);
995 res.json({ message: 'Church booking removed.' });
996 } catch (err) {
997 console.error(err.message);
998 res.status(500).json({ error: 'Server error.' });
999 }
1000});
1001
1002// =============================================================
1003// REGISTRARS
1004// =============================================================
1005
1006// GET /api/registrars
1007app.get('/api/registrars', requireAuth, async (req, res) => {
1008 try {
1009 const result = await pool.query('SELECT * FROM project.registrar ORDER BY name');
1010 res.json(result.rows);
1011 } catch (err) {
1012 console.error(err.message);
1013 res.status(500).json({ error: 'Server error.' });
1014 }
1015});
1016
1017// GET /api/weddings/:wid/registrar-bookings
1018app.get('/api/weddings/:wid/registrar-bookings', requireAuth, async (req, res) => {
1019 try {
1020 const result = await pool.query(
1021 `SELECT rb.*, r.name AS registrar_name, r.location
1022 FROM project.registrar_booking rb
1023 JOIN project.registrar r ON rb.registrar_id = r.registrar_id
1024 WHERE rb.wedding_id=$1 ORDER BY rb.date`,
1025 [req.params.wid]
1026 );
1027 res.json(result.rows);
1028 } catch (err) {
1029 console.error(err.message);
1030 res.status(500).json({ error: 'Server error.' });
1031 }
1032});
1033
1034// POST /api/weddings/:wid/registrar-bookings
1035app.post('/api/weddings/:wid/registrar-bookings', requireAuth, async (req, res) => {
1036 const { date, start_time, end_time, status, registrar_id } = req.body;
1037 if (!date || !start_time || !end_time || !registrar_id)
1038 return res.status(400).json({ error: 'date, start_time, end_time and registrar_id are required.' });
1039 try {
1040 const result = await pool.query(
1041 `INSERT INTO project.registrar_booking("date",start_time,end_time,status,registrar_id,wedding_id)
1042 VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`,
1043 [date, start_time, end_time, status || 'confirmed', registrar_id, req.params.wid]
1044 );
1045 res.status(201).json(result.rows[0]);
1046 } catch (err) {
1047 console.error(err.message);
1048 res.status(500).json({ error: 'Server error.' });
1049 }
1050});
1051
1052// DELETE /api/registrar-bookings/:id
1053app.delete('/api/registrar-bookings/:id', requireAuth, async (req, res) => {
1054 try {
1055 await pool.query('DELETE FROM project.registrar_booking WHERE booking_id=$1', [req.params.id]);
1056 res.json({ message: 'Registrar booking removed.' });
1057 } catch (err) {
1058 console.error(err.message);
1059 res.status(500).json({ error: 'Server error.' });
1060 }
1061});
1062
1063// =============================================================
1064// START SERVER
1065// =============================================================
1066app.listen(PORT, () => {
1067 console.log(`\n๐ŸŒธ Wedding Planner server running at http://localhost:${PORT}`);
1068 console.log(` Dashboard โ†’ http://localhost:${PORT}/Wedding_Planner.html`);
1069 console.log(` Login โ†’ http://localhost:${PORT}/login.html`);
1070 console.log(` RSVP page โ†’ http://localhost:${PORT}/rsvp.html\n`);
1071});
Note: See TracBrowser for help on using the repository browser.