Initial fullstack scaffold: Events, Guests, Budget, Bookings

- Express backend with PostgreSQL (JWT auth, full CRUD)
- React + Vite + TailwindCSS frontend in Hebrew (RTL)
- Features: Digital Booking System, Guest Management, Smart Budget Management
- Docker Compose with postgres healthcheck
- Auto-runs migrations on startup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fullstack Developer
2026-02-21 18:28:03 +00:00
parent ae6f833207
commit e003c7146d
28 changed files with 2034 additions and 0 deletions

74
routes/auth.js Normal file
View File

@@ -0,0 +1,74 @@
const express = require('express');
const router = express.Router();
const pool = require('../db');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'airewit-secret-key-2026';
// Register
router.post('/register', async (req, res) => {
const { email, name, password } = req.body;
if (!email || !name || !password) {
return res.status(400).json({ error: 'Missing required fields' });
}
try {
const hashed = await bcrypt.hash(password, 10);
const result = await pool.query(
'INSERT INTO users (email, name, password) VALUES ($1, $2, $3) RETURNING id, email, name, created_at',
[email, name, hashed]
);
const user = result.rows[0];
const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' });
res.status(201).json({ token, user });
} catch (err) {
if (err.code === '23505') {
return res.status(409).json({ error: 'Email already registered' });
}
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Login
router.post('/login', async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Missing email or password' });
}
try {
const result = await pool.query('SELECT * FROM users WHERE email = $1', [email]);
if (result.rows.length === 0) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const user = result.rows[0];
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' });
res.json({ token, user: { id: user.id, email: user.email, name: user.name } });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Middleware
function authMiddleware(req, res, next) {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const token = auth.split(' ')[1];
const payload = jwt.verify(token, JWT_SECRET);
req.userId = payload.userId;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
}
module.exports = router;
module.exports.authMiddleware = authMiddleware;

75
routes/bookings.js Normal file
View File

@@ -0,0 +1,75 @@
const express = require('express');
const router = express.Router();
const pool = require('../db');
const { authMiddleware } = require('./auth');
// Get all bookings for an event
router.get('/event/:eventId', authMiddleware, async (req, res) => {
try {
const result = await pool.query(
`SELECT b.* FROM bookings b
JOIN events e ON b.event_id = e.id
WHERE b.event_id = $1 AND e.user_id = $2
ORDER BY b.created_at DESC`,
[req.params.eventId, req.userId]
);
res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Create booking
router.post('/', authMiddleware, async (req, res) => {
const { event_id, supplier_name, supplier_type, contact_info, cost, status, notes } = req.body;
if (!event_id || !supplier_name) return res.status(400).json({ error: 'event_id and supplier_name are required' });
try {
const eventCheck = await pool.query('SELECT id FROM events WHERE id=$1 AND user_id=$2', [event_id, req.userId]);
if (eventCheck.rows.length === 0) return res.status(403).json({ error: 'Forbidden' });
const result = await pool.query(
`INSERT INTO bookings (event_id, supplier_name, supplier_type, contact_info, cost, status, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
[event_id, supplier_name, supplier_type, contact_info, cost || 0, status || 'pending', notes]
);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Update booking
router.put('/:id', authMiddleware, async (req, res) => {
const { supplier_name, supplier_type, contact_info, cost, status, notes } = req.body;
try {
const result = await pool.query(
`UPDATE bookings SET supplier_name=$1, supplier_type=$2, contact_info=$3, cost=$4, status=$5, notes=$6
WHERE id=$7 AND event_id IN (SELECT id FROM events WHERE user_id=$8) RETURNING *`,
[supplier_name, supplier_type, contact_info, cost, status, notes, req.params.id, req.userId]
);
if (result.rows.length === 0) return res.status(404).json({ error: 'Booking not found' });
res.json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Delete booking
router.delete('/:id', authMiddleware, async (req, res) => {
try {
const result = await pool.query(
`DELETE FROM bookings WHERE id=$1 AND event_id IN (SELECT id FROM events WHERE user_id=$2) RETURNING id`,
[req.params.id, req.userId]
);
if (result.rows.length === 0) return res.status(404).json({ error: 'Booking not found' });
res.json({ message: 'Booking deleted' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;

109
routes/budget.js Normal file
View File

@@ -0,0 +1,109 @@
const express = require('express');
const router = express.Router();
const pool = require('../db');
const { authMiddleware } = require('./auth');
// Get all budget items for an event
router.get('/event/:eventId', authMiddleware, async (req, res) => {
try {
const result = await pool.query(
`SELECT b.* FROM budget_items b
JOIN events e ON b.event_id = e.id
WHERE b.event_id = $1 AND e.user_id = $2
ORDER BY b.category, b.created_at ASC`,
[req.params.eventId, req.userId]
);
res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Get budget summary for event
router.get('/event/:eventId/summary', authMiddleware, async (req, res) => {
try {
const [itemsRes, eventRes] = await Promise.all([
pool.query(
`SELECT category,
SUM(estimated_cost) as estimated,
SUM(COALESCE(actual_cost, 0)) as actual,
COUNT(*) as count
FROM budget_items b
JOIN events e ON b.event_id = e.id
WHERE b.event_id = $1 AND e.user_id = $2
GROUP BY category`,
[req.params.eventId, req.userId]
),
pool.query('SELECT budget FROM events WHERE id=$1 AND user_id=$2', [req.params.eventId, req.userId]),
]);
const totalEstimated = itemsRes.rows.reduce((s, r) => s + parseFloat(r.estimated || 0), 0);
const totalActual = itemsRes.rows.reduce((s, r) => s + parseFloat(r.actual || 0), 0);
const eventBudget = eventRes.rows[0]?.budget || 0;
res.json({
event_budget: parseFloat(eventBudget),
total_estimated: totalEstimated,
total_actual: totalActual,
remaining_budget: parseFloat(eventBudget) - totalEstimated,
over_budget: totalEstimated > parseFloat(eventBudget),
categories: itemsRes.rows,
});
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Create budget item
router.post('/', authMiddleware, async (req, res) => {
const { event_id, category, description, estimated_cost, actual_cost, status } = req.body;
if (!event_id || !category) return res.status(400).json({ error: 'event_id and category are required' });
try {
const eventCheck = await pool.query('SELECT id FROM events WHERE id=$1 AND user_id=$2', [event_id, req.userId]);
if (eventCheck.rows.length === 0) return res.status(403).json({ error: 'Forbidden' });
const result = await pool.query(
`INSERT INTO budget_items (event_id, category, description, estimated_cost, actual_cost, status)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
[event_id, category, description, estimated_cost || 0, actual_cost, status || 'planned']
);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Update budget item
router.put('/:id', authMiddleware, async (req, res) => {
const { category, description, estimated_cost, actual_cost, status } = req.body;
try {
const result = await pool.query(
`UPDATE budget_items SET category=$1, description=$2, estimated_cost=$3, actual_cost=$4, status=$5
WHERE id=$6 AND event_id IN (SELECT id FROM events WHERE user_id=$7) RETURNING *`,
[category, description, estimated_cost, actual_cost, status, req.params.id, req.userId]
);
if (result.rows.length === 0) return res.status(404).json({ error: 'Budget item not found' });
res.json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Delete budget item
router.delete('/:id', authMiddleware, async (req, res) => {
try {
const result = await pool.query(
`DELETE FROM budget_items WHERE id=$1 AND event_id IN (SELECT id FROM events WHERE user_id=$2) RETURNING id`,
[req.params.id, req.userId]
);
if (result.rows.length === 0) return res.status(404).json({ error: 'Budget item not found' });
res.json({ message: 'Budget item deleted' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;

103
routes/events.js Normal file
View File

@@ -0,0 +1,103 @@
const express = require('express');
const router = express.Router();
const pool = require('../db');
const { authMiddleware } = require('./auth');
// Get all events for user
router.get('/', authMiddleware, async (req, res) => {
try {
const result = await pool.query(
'SELECT * FROM events WHERE user_id = $1 ORDER BY date ASC',
[req.userId]
);
res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Get single event
router.get('/:id', authMiddleware, async (req, res) => {
try {
const result = await pool.query(
'SELECT * FROM events WHERE id = $1 AND user_id = $2',
[req.params.id, req.userId]
);
if (result.rows.length === 0) return res.status(404).json({ error: 'Event not found' });
res.json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Create event
router.post('/', authMiddleware, async (req, res) => {
const { name, date, location, event_type, budget, notes } = req.body;
if (!name || !date) return res.status(400).json({ error: 'Name and date are required' });
try {
const result = await pool.query(
`INSERT INTO events (user_id, name, date, location, event_type, budget, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
[req.userId, name, date, location, event_type || 'general', budget || 0, notes]
);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Update event
router.put('/:id', authMiddleware, async (req, res) => {
const { name, date, location, event_type, budget, status, notes } = req.body;
try {
const result = await pool.query(
`UPDATE events SET name=$1, date=$2, location=$3, event_type=$4, budget=$5, status=$6, notes=$7
WHERE id=$8 AND user_id=$9 RETURNING *`,
[name, date, location, event_type, budget, status, notes, req.params.id, req.userId]
);
if (result.rows.length === 0) return res.status(404).json({ error: 'Event not found' });
res.json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Delete event
router.delete('/:id', authMiddleware, async (req, res) => {
try {
const result = await pool.query(
'DELETE FROM events WHERE id=$1 AND user_id=$2 RETURNING id',
[req.params.id, req.userId]
);
if (result.rows.length === 0) return res.status(404).json({ error: 'Event not found' });
res.json({ message: 'Event deleted' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Get event stats
router.get('/:id/stats', authMiddleware, async (req, res) => {
try {
const [guestsRes, budgetRes, bookingsRes] = await Promise.all([
pool.query('SELECT rsvp_status, COUNT(*) FROM guests WHERE event_id=$1 GROUP BY rsvp_status', [req.params.id]),
pool.query('SELECT SUM(estimated_cost) as estimated, SUM(actual_cost) as actual FROM budget_items WHERE event_id=$1', [req.params.id]),
pool.query('SELECT SUM(cost) as total_bookings FROM bookings WHERE event_id=$1', [req.params.id]),
]);
res.json({
guests: guestsRes.rows,
budget: budgetRes.rows[0],
bookings: bookingsRes.rows[0],
});
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;

118
routes/guests.js Normal file
View File

@@ -0,0 +1,118 @@
const express = require('express');
const router = express.Router();
const pool = require('../db');
const { authMiddleware } = require('./auth');
// Get all guests for an event
router.get('/event/:eventId', authMiddleware, async (req, res) => {
try {
const result = await pool.query(
`SELECT g.* FROM guests g
JOIN events e ON g.event_id = e.id
WHERE g.event_id = $1 AND e.user_id = $2
ORDER BY g.name ASC`,
[req.params.eventId, req.userId]
);
res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Get single guest
router.get('/:id', authMiddleware, async (req, res) => {
try {
const result = await pool.query(
`SELECT g.* FROM guests g
JOIN events e ON g.event_id = e.id
WHERE g.id = $1 AND e.user_id = $2`,
[req.params.id, req.userId]
);
if (result.rows.length === 0) return res.status(404).json({ error: 'Guest not found' });
res.json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Create guest
router.post('/', authMiddleware, async (req, res) => {
const { event_id, name, phone, email, rsvp_status, table_number, seat_number, dietary_restriction, notes } = req.body;
if (!event_id || !name) return res.status(400).json({ error: 'event_id and name are required' });
try {
// Verify event belongs to user
const eventCheck = await pool.query('SELECT id FROM events WHERE id=$1 AND user_id=$2', [event_id, req.userId]);
if (eventCheck.rows.length === 0) return res.status(403).json({ error: 'Forbidden' });
const result = await pool.query(
`INSERT INTO guests (event_id, name, phone, email, rsvp_status, table_number, seat_number, dietary_restriction, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *`,
[event_id, name, phone, email, rsvp_status || 'pending', table_number, seat_number, dietary_restriction, notes]
);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Update guest
router.put('/:id', authMiddleware, async (req, res) => {
const { name, phone, email, rsvp_status, table_number, seat_number, dietary_restriction, notes } = req.body;
try {
const result = await pool.query(
`UPDATE guests SET name=$1, phone=$2, email=$3, rsvp_status=$4, table_number=$5, seat_number=$6,
dietary_restriction=$7, notes=$8
WHERE id=$9 AND event_id IN (SELECT id FROM events WHERE user_id=$10) RETURNING *`,
[name, phone, email, rsvp_status, table_number, seat_number, dietary_restriction, notes, req.params.id, req.userId]
);
if (result.rows.length === 0) return res.status(404).json({ error: 'Guest not found' });
res.json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Delete guest
router.delete('/:id', authMiddleware, async (req, res) => {
try {
const result = await pool.query(
`DELETE FROM guests WHERE id=$1 AND event_id IN (SELECT id FROM events WHERE user_id=$2) RETURNING id`,
[req.params.id, req.userId]
);
if (result.rows.length === 0) return res.status(404).json({ error: 'Guest not found' });
res.json({ message: 'Guest deleted' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Bulk import guests
router.post('/bulk', authMiddleware, async (req, res) => {
const { event_id, guests } = req.body;
if (!event_id || !Array.isArray(guests)) return res.status(400).json({ error: 'event_id and guests array required' });
try {
const eventCheck = await pool.query('SELECT id FROM events WHERE id=$1 AND user_id=$2', [event_id, req.userId]);
if (eventCheck.rows.length === 0) return res.status(403).json({ error: 'Forbidden' });
const inserted = [];
for (const g of guests) {
const r = await pool.query(
`INSERT INTO guests (event_id, name, phone, email, rsvp_status, dietary_restriction)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
[event_id, g.name, g.phone, g.email, g.rsvp_status || 'pending', g.dietary_restriction]
);
inserted.push(r.rows[0]);
}
res.status(201).json(inserted);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;