Files

278 lines
7.2 KiB
JavaScript
Raw Permalink Normal View History

2026-01-27 18:04:50 -06:00
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;