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:
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
/**
|
||||
* api/builder_session.php — Save or update a custom builder session.
|
||||
* Requires superuser+.
|
||||
*
|
||||
* POST — create or update session + steps
|
||||
* Body JSON: {
|
||||
* id? : int (existing session ID for update)
|
||||
* name : string
|
||||
* is_public : bool
|
||||
* subject_name? : string
|
||||
* subject_pronoun? : 'he'|'she'
|
||||
* subject_dates? : string
|
||||
* steps : [{prayer_id, attribution}, ...]
|
||||
* }
|
||||
*/
|
||||
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'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') json_err('Method not allowed', 405);
|
||||
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$body) json_err('Invalid JSON');
|
||||
|
||||
$name = trim($body['name'] ?? '');
|
||||
$is_public = (int)!empty($body['is_public']);
|
||||
$subject_name = trim($body['subject_name'] ?? '');
|
||||
$subject_pronoun = in_array($body['subject_pronoun'] ?? '', ['he','she']) ? $body['subject_pronoun'] : 'he';
|
||||
$subject_dates = trim($body['subject_dates'] ?? '');
|
||||
$steps = $body['steps'] ?? [];
|
||||
$session_id = (int)($body['id'] ?? 0);
|
||||
|
||||
if ($name === '') json_err('Session name is required');
|
||||
if (empty($steps)) json_err('Add at least one prayer to your sequence');
|
||||
|
||||
$valid_attributions = ['leader_all', 'leader_only', 'all_only', 'none'];
|
||||
|
||||
// Validate steps
|
||||
foreach ($steps as $i => $step) {
|
||||
$pid = (int)($step['prayer_id'] ?? 0);
|
||||
$att = $step['attribution'] ?? 'leader_all';
|
||||
if (!$pid) json_err("Step " . ($i + 1) . " is missing a prayer");
|
||||
if (!in_array($att, $valid_attributions)) json_err("Invalid attribution on step " . ($i + 1));
|
||||
}
|
||||
|
||||
// Verify all prayer_ids exist and are accessible
|
||||
$prayer_ids = array_unique(array_column($steps, 'prayer_id'));
|
||||
$in_placeholders = implode(',', array_fill(0, count($prayer_ids), '?'));
|
||||
$valid_stmt = $pdo->prepare("
|
||||
SELECT id FROM custom_prayers
|
||||
WHERE id IN ($in_placeholders)
|
||||
AND (is_global = 1 OR created_by = ?)
|
||||
");
|
||||
$valid_stmt->execute([...$prayer_ids, $uid]);
|
||||
$valid_ids = array_column($valid_stmt->fetchAll(), 'id');
|
||||
foreach ($prayer_ids as $pid) {
|
||||
if (!in_array((string)$pid, array_map('strval', $valid_ids))) {
|
||||
json_err("Prayer ID {$pid} not found or not accessible");
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
if ($session_id > 0) {
|
||||
// Update existing session — verify ownership
|
||||
$owner_stmt = $pdo->prepare("SELECT user_id FROM sessions WHERE id = ? AND occasion = 'custom'");
|
||||
$owner_stmt->execute([$session_id]);
|
||||
$existing = $owner_stmt->fetch();
|
||||
if (!$existing) json_err('Session not found', 404);
|
||||
if (!has_role('admin') && (int)$existing['user_id'] !== $uid) json_err('Access denied', 403);
|
||||
|
||||
$slug = unique_slug($name, $uid, 'sessions', $session_id);
|
||||
$pdo->prepare("
|
||||
UPDATE sessions
|
||||
SET name=?, is_public=?, subject_name=?, subject_pronoun=?, subject_dates=?,
|
||||
slug=?, updated_at=NOW()
|
||||
WHERE id=?
|
||||
")->execute([$name, $is_public, $subject_name ?: null, $subject_pronoun, $subject_dates ?: null, $slug, $session_id]);
|
||||
|
||||
// Replace all steps
|
||||
$pdo->prepare("DELETE FROM builder_steps WHERE session_id = ?")->execute([$session_id]);
|
||||
} else {
|
||||
// Check rosary limit
|
||||
if (!can_create_rosary($uid, (int)$user['rosary_limit'])) {
|
||||
json_err('You have reached your rosary limit. Contact an administrator.');
|
||||
}
|
||||
$slug = unique_slug($name, $uid);
|
||||
$pdo->prepare("
|
||||
INSERT INTO sessions
|
||||
(user_id, is_public, slug, name, occasion, mystery_set,
|
||||
subject_name, subject_pronoun, subject_dates)
|
||||
VALUES (?, ?, ?, ?, 'custom', 'custom', ?, ?, ?)
|
||||
")->execute([$uid, $is_public, $slug, $name, $subject_name ?: null, $subject_pronoun, $subject_dates ?: null]);
|
||||
$session_id = (int)$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
// Insert steps
|
||||
$step_stmt = $pdo->prepare(
|
||||
"INSERT INTO builder_steps (session_id, step_order, prayer_id, attribution)
|
||||
VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
foreach ($steps as $order => $step) {
|
||||
$step_stmt->execute([$session_id, $order, (int)$step['prayer_id'], $step['attribution']]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
// Return session URL
|
||||
$user_row = $pdo->prepare("SELECT username FROM users WHERE id = ?");
|
||||
$user_row->execute([$uid]);
|
||||
$username = $user_row->fetchColumn();
|
||||
$present_url = BASE_URL . '/' . rawurlencode($username) . '/' . rawurlencode($slug);
|
||||
|
||||
echo json_encode([
|
||||
'saved' => true,
|
||||
'session_id' => $session_id,
|
||||
'slug' => $slug,
|
||||
'present_url' => $present_url,
|
||||
'edit_url' => BASE_URL . '/admin/builder.php?id=' . $session_id,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
json_err('Save failed: ' . $e->getMessage());
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user