278 lines
7.2 KiB
JavaScript
278 lines
7.2 KiB
JavaScript
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;
|