Add Rosary Builder — custom prayer-sequence sessions

Superuser+ can now build a custom prayer sequence from scratch:
- Two-panel builder UI: step sequence (left) + searchable prayer library (right)
- 16 standard prayers seeded globally; users can create private custom prayers
- Admin can promote private prayers to global and manage the library
- Four attribution modes per step: Leader/All, Leader only, All together, None
- Optional subject name/pronoun for variable substitution in prayers
- Custom sessions fully presented via the existing presenter (auto-split works)
- migrate_v4.php creates custom_prayers + builder_steps tables

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-13 19:07:11 -07:00
parent 663fde3909
commit f3fa77da17
9 changed files with 2086 additions and 2 deletions
+181
View File
@@ -0,0 +1,181 @@
<?php
/**
* api/prayers_api.php — CRUD for custom_prayers.
* Requires superuser+.
*
* GET ?q=&source=all|standard|mine — list prayers
* POST — create prayer (body JSON)
* PUT ?id=X — update prayer (body JSON)
* DELETE ?id=X — delete prayer
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../includes/auth.php';
header('Content-Type: application/json');
function json_err(string $msg, int $code = 400): never {
http_response_code($code);
echo json_encode(['error' => $msg]);
exit;
}
require_auth();
if (!has_role('superuser')) json_err('Access denied', 403);
$pdo = get_pdo();
$user = current_user();
$uid = (int)$user['id'];
$is_admin = has_role('admin');
$method = $_SERVER['REQUEST_METHOD'];
// ─────────────────────────────────────────────────
// GET — list prayers
// ─────────────────────────────────────────────────
if ($method === 'GET') {
$source = $_GET['source'] ?? 'all';
$q = trim($_GET['q'] ?? '');
$where = [];
$params = [];
if ($source === 'standard' || $source === 'mine') {
if ($source === 'standard') {
$where[] = 'cp.is_global = 1 AND u.role = \'superadmin\'';
} else {
$where[] = 'cp.created_by = ?';
$params[] = $uid;
}
} else {
// all: global prayers + own private prayers
$where[] = '(cp.is_global = 1 OR cp.created_by = ?)';
$params[] = $uid;
}
if ($q !== '') {
$like = '%' . $q . '%';
$where[] = '(cp.name LIKE ? OR cp.leader_text LIKE ? OR cp.all_text LIKE ?)';
$params[] = $like;
$params[] = $like;
$params[] = $like;
}
$sql = "
SELECT cp.id, cp.name, cp.leader_text, cp.all_text,
cp.is_global, cp.created_by,
u.role AS creator_role,
IF(cp.is_global=1 AND u.role='superadmin', 'standard',
IF(cp.is_global=1, 'global', 'mine')) AS source_tag
FROM custom_prayers cp
LEFT JOIN users u ON u.id = cp.created_by
WHERE " . implode(' AND ', $where) . "
ORDER BY (cp.is_global=1 AND u.role='superadmin') DESC, cp.name ASC
";
$st = $pdo->prepare($sql);
$st->execute($params);
echo json_encode($st->fetchAll());
exit;
}
// ─────────────────────────────────────────────────
// POST — create prayer
// ─────────────────────────────────────────────────
if ($method === 'POST') {
$body = json_decode(file_get_contents('php://input'), true);
if (!$body) json_err('Invalid JSON');
$name = trim($body['name'] ?? '');
$leader = trim($body['leader_text'] ?? '');
$all = trim($body['all_text'] ?? '');
$global = $is_admin ? (int)!empty($body['is_global']) : 0;
if ($name === '') json_err('Prayer name is required');
$st = $pdo->prepare(
"INSERT INTO custom_prayers (name, leader_text, all_text, is_global, created_by)
VALUES (?, ?, ?, ?, ?)"
);
$st->execute([$name, $leader, $all, $global, $uid]);
$new_id = (int)$pdo->lastInsertId();
$row = $pdo->prepare("
SELECT cp.id, cp.name, cp.leader_text, cp.all_text, cp.is_global, cp.created_by,
u.role AS creator_role,
IF(cp.is_global=1 AND u.role='superadmin', 'standard',
IF(cp.is_global=1, 'global', 'mine')) AS source_tag
FROM custom_prayers cp LEFT JOIN users u ON u.id = cp.created_by
WHERE cp.id = ?
");
$row->execute([$new_id]);
echo json_encode(['created' => true, 'prayer' => $row->fetch()]);
exit;
}
// ─────────────────────────────────────────────────
// PUT — update prayer
// ─────────────────────────────────────────────────
if ($method === 'PUT') {
$id = (int)($_GET['id'] ?? 0);
$body = json_decode(file_get_contents('php://input'), true);
if (!$id || !$body) json_err('Invalid request');
$st = $pdo->prepare("SELECT * FROM custom_prayers WHERE id = ?");
$st->execute([$id]);
$prayer = $st->fetch();
if (!$prayer) json_err('Prayer not found', 404);
$can_edit = $is_admin || (int)$prayer['created_by'] === $uid;
if (!$can_edit) json_err('Access denied', 403);
$name = trim($body['name'] ?? $prayer['name']);
$leader = trim($body['leader_text'] ?? $prayer['leader_text']);
$all = trim($body['all_text'] ?? $prayer['all_text']);
$global = $is_admin ? (int)!empty($body['is_global']) : (int)$prayer['is_global'];
if ($name === '') json_err('Prayer name is required');
$pdo->prepare(
"UPDATE custom_prayers SET name=?, leader_text=?, all_text=?, is_global=?, updated_at=NOW()
WHERE id=?"
)->execute([$name, $leader, $all, $global, $id]);
$row = $pdo->prepare("
SELECT cp.id, cp.name, cp.leader_text, cp.all_text, cp.is_global, cp.created_by,
u.role AS creator_role,
IF(cp.is_global=1 AND u.role='superadmin', 'standard',
IF(cp.is_global=1, 'global', 'mine')) AS source_tag
FROM custom_prayers cp LEFT JOIN users u ON u.id = cp.created_by
WHERE cp.id = ?
");
$row->execute([$id]);
echo json_encode(['updated' => true, 'prayer' => $row->fetch()]);
exit;
}
// ─────────────────────────────────────────────────
// DELETE — remove prayer
// ─────────────────────────────────────────────────
if ($method === 'DELETE') {
$id = (int)($_GET['id'] ?? 0);
if (!$id) json_err('Missing id');
$st = $pdo->prepare("SELECT * FROM custom_prayers WHERE id = ?");
$st->execute([$id]);
$prayer = $st->fetch();
if (!$prayer) json_err('Prayer not found', 404);
$can_delete = $is_admin || (int)$prayer['created_by'] === $uid;
if (!$can_delete) json_err('Access denied', 403);
// Prevent deleting if used in builder_steps
$st2 = $pdo->prepare("SELECT COUNT(*) FROM builder_steps WHERE prayer_id = ?");
$st2->execute([$id]);
if ((int)$st2->fetchColumn() > 0) {
json_err('Cannot delete: this prayer is used in one or more sessions. Remove it from those sessions first.');
}
$pdo->prepare("DELETE FROM custom_prayers WHERE id = ?")->execute([$id]);
echo json_encode(['deleted' => true]);
exit;
}
json_err('Method not allowed', 405);