Initial commit - Church Music Database
This commit is contained in:
277
new-site/backend/routes/lists.js
Normal file
277
new-site/backend/routes/lists.js
Normal file
@@ -0,0 +1,277 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user