const express = require("express"); const router = express.Router(); const { query } = require("../db"); const { v4: uuidv4 } = require("uuid"); const { success, error, notFound, badRequest, } = require("../utils/responseHandler"); const { authenticate } = require("../middleware/auth"); // Reusable SQL fragments const SELECT_LIST_WITH_COUNT = ` SELECT p.*, pr.name as profile_name, (SELECT COUNT(*) FROM plan_songs WHERE plan_id = p.id) as song_count FROM plans p LEFT JOIN profiles pr ON p.profile_id = pr.id `; const SELECT_LIST_SONGS = ` SELECT s.*, s.chords as key_chord, ps.order_index FROM songs s INNER JOIN plan_songs ps ON s.id = ps.song_id WHERE ps.plan_id = $1 ORDER BY ps.order_index ASC `; /** * Helper to add songs to a worship list */ const addSongsToList = async (planId, songs) => { if (!songs || !Array.isArray(songs) || songs.length === 0) return; const values = songs .map( (songId, index) => `('${uuidv4()}', '${planId}', '${songId}', ${index})`, ) .join(", "); await query(` INSERT INTO plan_songs (id, plan_id, song_id, order_index) VALUES ${values} `); }; /** * Helper to get next order index for a list */ const getNextOrderIndex = async (planId) => { const result = await query( "SELECT COALESCE(MAX(order_index), -1) + 1 as next_order FROM plan_songs WHERE plan_id = $1", [planId], ); return result.rows[0].next_order; }; // GET all worship lists (plans) router.get("/", async (req, res) => { try { const result = await query(` ${SELECT_LIST_WITH_COUNT} ORDER BY p.date DESC `); success(res, { lists: result.rows }); } catch (err) { error(res, "Failed to fetch worship lists"); } }); // GET single worship list by ID with songs router.get("/:id", async (req, res) => { try { const [listResult, songsResult] = await Promise.all([ query(`${SELECT_LIST_WITH_COUNT} WHERE p.id = $1`, [req.params.id]), query(SELECT_LIST_SONGS, [req.params.id]), ]); if (listResult.rows.length === 0) { return notFound(res, "Worship list"); } success(res, { list: listResult.rows[0], songs: songsResult.rows, }); } catch (err) { error(res, "Failed to fetch worship list"); } }); // POST create new worship list router.post("/", authenticate, async (req, res) => { try { const { date, profile_id, notes, songs } = req.body; if (!date) { return badRequest(res, "Date is required"); } const id = uuidv4(); const now = Math.floor(Date.now() / 1000); const result = await query( `INSERT INTO plans (id, date, profile_id, notes, created_at) VALUES ($1, $2, $3, $4, $5) RETURNING *`, [id, date, profile_id || null, notes || "", now], ); await addSongsToList(id, songs); success(res, { list: result.rows[0] }, 201); } catch (err) { error(res, "Failed to create worship list"); } }); // PUT update worship list router.put("/:id", authenticate, async (req, res) => { try { const { date, profile_id, notes, songs } = req.body; console.log(`[PUT /lists/${req.params.id}] Request:`, { date, profile_id, notes, songCount: songs?.length, songIds: songs?.slice(0, 3), }); const result = await query( `UPDATE plans SET date = COALESCE($1, date), profile_id = $2, notes = COALESCE($3, notes) WHERE id = $4 RETURNING *`, [date, profile_id, notes, req.params.id], ); if (result.rows.length === 0) { console.log(`[PUT /lists/${req.params.id}] NOT FOUND`); return notFound(res, "Worship list"); } console.log( `[PUT /lists/${req.params.id}] Plan updated, now updating songs...`, ); // Update songs if provided if (songs && Array.isArray(songs)) { await query("DELETE FROM plan_songs WHERE plan_id = $1", [req.params.id]); console.log( `[PUT /lists/${req.params.id}] Deleted old songs, adding ${songs.length} new songs`, ); await addSongsToList(req.params.id, songs); console.log(`[PUT /lists/${req.params.id}] Songs added successfully`); } console.log(`[PUT /lists/${req.params.id}] SUCCESS`); success(res, { list: result.rows[0] }); } catch (err) { console.error(`[PUT /lists/:id] ERROR:`, err.message); console.error(err.stack); error(res, "Failed to update worship list: " + err.message); } }); // DELETE worship list router.delete("/:id", authenticate, async (req, res) => { try { // plan_songs will be deleted via CASCADE const result = await query("DELETE FROM plans WHERE id = $1 RETURNING id", [ req.params.id, ]); if (result.rows.length === 0) { return notFound(res, "Worship list"); } success(res, { message: "Worship list deleted" }); } catch (err) { error(res, "Failed to delete worship list"); } }); // POST add song to worship list router.post("/:id/songs/:songId", authenticate, async (req, res) => { try { const { id, songId } = req.params; const nextOrder = await getNextOrderIndex(id); const psId = uuidv4(); const result = await query( `INSERT INTO plan_songs (id, plan_id, song_id, order_index) VALUES ($1, $2, $3, $4) ON CONFLICT (plan_id, song_id) DO NOTHING RETURNING *`, [psId, id, songId, nextOrder], ); success(res, { message: "Song added to worship list", added: result.rowCount > 0, }); } catch (err) { error(res, "Failed to add song to worship list", 500, { error: err.message, }); } }); // DELETE remove song from worship list router.delete("/:id/songs/:songId", authenticate, async (req, res) => { try { const { id, songId } = req.params; const result = await query( "DELETE FROM plan_songs WHERE plan_id = $1 AND song_id = $2 RETURNING *", [id, songId], ); success(res, { message: "Song removed from worship list", deleted: result.rowCount, }); } catch (err) { error(res, "Failed to remove song from worship list", 500, { error: err.message, }); } }); // PUT reorder songs in worship list router.put("/:id/reorder", authenticate, async (req, res) => { try { const { songs } = req.body; if (!songs || !Array.isArray(songs)) { return badRequest(res, "Songs array is required"); } // Batch update using CASE statement for better performance if (songs.length > 0) { const cases = songs .map((songId, index) => `WHEN song_id = '${songId}' THEN ${index}`) .join(" "); const songIds = songs.map((id) => `'${id}'`).join(", "); await query( ` UPDATE plan_songs SET order_index = CASE ${cases} END WHERE plan_id = $1 AND song_id IN (${songIds}) `, [req.params.id], ); } success(res, { message: "Songs reordered" }); } catch (err) { error(res, "Failed to reorder songs", 500, { error: err.message }); } }); // GET worship list count router.get("/stats/count", async (req, res) => { try { const result = await query("SELECT COUNT(*) as count FROM plans"); success(res, { count: parseInt(result.rows[0].count) }); } catch (err) { error(res, "Failed to count worship lists"); } }); module.exports = router;