import express from 'express' import cors from 'cors' import dotenv from 'dotenv' import { pool } from './db.js' dotenv.config() const app = express() app.use(cors()) app.use(express.json()) // Simple health app.get('/api/health', (req, res) => res.json({ ok: true })) // List available cargobikes app.get('/api/bikes', async (req, res) => { // fixed bike types const bikes = [1000, 2000, 3000, 4000, 5000] res.json({ bikes }) }) // Create a booking (supports single bike_type or multiple bike_types) app.post('/api/bookings', async (req, res) => { try { const { bike_type, bike_types, start_date, end_date, start_time, end_time, name, email } = req.body const multi = Array.isArray(bike_types) ? bike_types.filter((v) => Number.isFinite(Number(v))).map(Number) : [] const single = Number.isFinite(Number(bike_type)) ? Number(bike_type) : null if ((!single && multi.length === 0) || !start_date || !end_date || !name || !email) { return res.status(400).json({ error: 'Missing fields' }) } // Explicitly set status to 'pending' to avoid relying solely on DB defaults const text = `INSERT INTO bookings(bike_type, bike_types, start_date, start_time, end_date, end_time, name, email, status) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *` const values = [single, multi.length ? multi.join(',') : null, start_date, start_time || null, end_date, end_time || null, name, email, 'pending'] const result = await pool.query(text, values) res.status(201).json({ booking: result.rows[0] }) } catch (err) { console.error(err) res.status(500).json({ error: 'Server error' }) } }) // List bookings (simple) app.get('/api/bookings', async (req, res) => { try { const result = await pool.query('SELECT * FROM bookings ORDER BY created_at DESC LIMIT 100') res.json({ bookings: result.rows }) } catch (err) { console.error(err) res.status(500).json({ error: 'Server error' }) } }) // Update booking status app.patch('/api/bookings/:id/status', async (req, res) => { try { const id = Number(req.params.id) if (!Number.isFinite(id)) return res.status(400).json({ error: 'Invalid id' }) const { status } = req.body || {} const allowed = ['pending', 'accepted', 'refused'] if (!allowed.includes(status)) return res.status(400).json({ error: 'Invalid status' }) // Read current status to enforce immutability once accepted const current = await pool.query('SELECT status FROM bookings WHERE id = $1', [id]) if (current.rowCount === 0) return res.status(404).json({ error: 'Booking not found' }) const curr = current.rows[0].status if (curr === 'accepted' && status !== 'accepted') { return res.status(400).json({ error: 'Accepted bookings cannot change status' }) } const result = await pool.query('UPDATE bookings SET status = $1 WHERE id = $2 RETURNING *', [status, id]) if (result.rowCount === 0) return res.status(404).json({ error: 'Booking not found' }) res.json({ booking: result.rows[0] }) } catch (err) { console.error(err) res.status(500).json({ error: 'Server error' }) } }) // Update booking fields (non-status) app.patch('/api/bookings/:id', async (req, res) => { try { const id = Number(req.params.id) if (!Number.isFinite(id)) return res.status(400).json({ error: 'Invalid id' }) const allowed = ['bike_type', 'bike_types', 'start_date', 'start_time', 'end_date', 'end_time', 'name', 'email'] const entries = Object.entries(req.body || {}).filter(([k, v]) => allowed.includes(k)) if (entries.length === 0) return res.status(400).json({ error: 'No valid fields to update' }) // Build dynamic update const sets = [] const values = [] for (let i = 0; i < entries.length; i++) { const [k, v] = entries[i] sets.push(`${k} = $${i + 1}`) values.push(v) } values.push(id) const sql = `UPDATE bookings SET ${sets.join(', ')} WHERE id = $${values.length} RETURNING *` const result = await pool.query(sql, values) if (result.rowCount === 0) return res.status(404).json({ error: 'Booking not found' }) res.json({ booking: result.rows[0] }) } catch (err) { console.error(err) res.status(500).json({ error: 'Server error' }) } }) // Delete a booking by id app.delete('/api/bookings/:id', async (req, res) => { try { const id = Number(req.params.id) if (!Number.isFinite(id)) return res.status(400).json({ error: 'Invalid id' }) const result = await pool.query('DELETE FROM bookings WHERE id = $1 RETURNING *', [id]) if (result.rowCount === 0) return res.status(404).json({ error: 'Booking not found' }) res.json({ ok: true, booking: result.rows[0] }) } catch (err) { console.error(err) res.status(500).json({ error: 'Server error' }) } }) // --- ensure DB tables exist (auto-migration for dev) --- async function ensureTables() { const createSql = ` CREATE TABLE IF NOT EXISTS bookings ( id SERIAL PRIMARY KEY, bike_type INTEGER, bike_types TEXT, start_date DATE NOT NULL, start_time TIME, end_date DATE NOT NULL, end_time TIME, name TEXT NOT NULL, email TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at TIMESTAMP WITH TIME ZONE DEFAULT now() ); ` try { await pool.query(createSql) // Ensure columns and constraints for backward compatibility await pool.query(`ALTER TABLE bookings ADD COLUMN IF NOT EXISTS start_time TIME;`) await pool.query(`ALTER TABLE bookings ADD COLUMN IF NOT EXISTS end_time TIME;`) await pool.query(`ALTER TABLE bookings ADD COLUMN IF NOT EXISTS bike_types TEXT;`) await pool.query(`ALTER TABLE bookings ADD COLUMN IF NOT EXISTS status TEXT;`) // Ensure default and not-null for status await pool.query(`ALTER TABLE bookings ALTER COLUMN status SET DEFAULT 'pending';`) await pool.query(`UPDATE bookings SET status = 'pending' WHERE status IS NULL;`) await pool.query(`ALTER TABLE bookings ALTER COLUMN status SET NOT NULL;`) // bike_type nullable for multi-type support await pool.query(`ALTER TABLE bookings ALTER COLUMN bike_type DROP NOT NULL;`) console.log('Database: bookings table is present (created/migrated if needed)') } catch (err) { console.warn('Warning: could not ensure bookings table (DB may be unavailable).', err.message) } } const port = process.env.PORT || 3000 // Run migrations (if possible) then start server ;(async () => { await ensureTables() app.listen(port, () => console.log(`Backend listening on port ${port}`)) })()