calendar_admin.php
24.58 KB
<?php
require_once __DIR__ . '/../bootstrap.php';
requireAdmin();
$pageTitle = 'Calendar Admin';
$msg = [];
$err = [];
function isSystemCalendarCategory(array $cat): bool {
$id = (int)($cat['id'] ?? 0);
$name = strtolower(trim((string)($cat['name'] ?? '')));
$systemIds = [10, 20, 30, 40, 50];
$systemNames = ['jobs', 'meetings', 'incidents', 'annual leave', 'other'];
return in_array($id, $systemIds, true) || in_array($name, $systemNames, true);
}
$selectedCategoryId = isset($_GET['category_id']) ? (int)$_GET['category_id'] : 0;
$selectedCategory = null;
// Handle form submissions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Save Category
if (isset($_POST['action']) && $_POST['action'] === 'save_category') {
$id = isset($_POST['id']) && $_POST['id'] ? (int)$_POST['id'] : null;
$name = trim($_POST['name'] ?? '');
$color = trim($_POST['color'] ?? '#3788d8');
$sortOrder = isset($_POST['sort_order']) ? (int)$_POST['sort_order'] : 0;
if (!$name) {
$err[] = 'Category name is required';
}
if (empty($err)) {
try {
if ($id) {
$stmt = $pdo->prepare("UPDATE calendar_categories SET name=?, color=?, sort_order=? WHERE id=?");
$stmt->execute([$name, $color, $sortOrder, $id]);
$msg[] = 'Category updated successfully';
} else {
$stmt = $pdo->prepare("INSERT INTO calendar_categories (name, color, sort_order) VALUES (?, ?, ?)");
$stmt->execute([$name, $color, $sortOrder]);
$msg[] = 'Category created successfully';
}
logActivity('calendar.categories.manage', 'calendar_category', $id ?? $pdo->lastInsertId(), "Saved category: $name");
} catch (PDOException $e) {
$err[] = 'Database error: ' . $e->getMessage();
}
}
}
// Delete Category
if (isset($_POST['action']) && $_POST['action'] === 'delete_category') {
$id = (int)$_POST['id'];
try {
$chk = $pdo->prepare("SELECT id, name FROM calendar_categories WHERE id=?");
$chk->execute([$id]);
$catRow = $chk->fetch(PDO::FETCH_ASSOC);
if ($catRow && isSystemCalendarCategory($catRow)) {
$err[] = 'System categories cannot be deleted.';
} else {
$stmt = $pdo->prepare("DELETE FROM calendar_categories WHERE id=?");
$stmt->execute([$id]);
$msg[] = 'Category deleted successfully';
logActivity('calendar.categories.manage', 'calendar_category', $id, 'Deleted category');
}
} catch (PDOException $e) {
$err[] = 'Failed to delete category: ' . $e->getMessage();
}
}
// Save Custom Field
if (isset($_POST['action']) && $_POST['action'] === 'save_field') {
$id = isset($_POST['field_id']) && $_POST['field_id'] ? (int)$_POST['field_id'] : null;
$categoryId = (int)$_POST['category_id'];
$fieldName = trim($_POST['field_name'] ?? '');
$fieldLabel = trim($_POST['field_label'] ?? '');
$fieldType = trim($_POST['field_type'] ?? 'text');
$fieldOptionsText = trim($_POST['field_options'] ?? '');
$required = isset($_POST['required']) ? 1 : 0;
$sortOrder = isset($_POST['field_sort_order']) ? (int)$_POST['field_sort_order'] : 0;
// Guard: system categories should never have custom fields
try {
$chk = $pdo->prepare("SELECT id, name FROM calendar_categories WHERE id=?");
$chk->execute([$categoryId]);
$catRow = $chk->fetch(PDO::FETCH_ASSOC);
if (!$catRow) {
$err[] = 'Invalid category selected';
} elseif (isSystemCalendarCategory($catRow)) {
$err[] = 'Custom fields are only allowed on custom categories.';
}
} catch (Throwable $e) {
$err[] = 'Could not validate category.';
}
if (!$fieldName || !$fieldLabel) {
$err[] = 'Field name and label are required';
}
// Only allow types supported by the DB enum
$allowedTypes = ['text','textarea','number','select','user'];
if (!in_array($fieldType, $allowedTypes, true)) {
$err[] = 'Invalid field type';
}
// Convert field options (newline separated) to JSON array for select fields
$fieldOptionsJson = null;
if ($fieldType === 'select') {
$lines = array_values(array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $fieldOptionsText))));
$fieldOptionsJson = $lines ? json_encode($lines) : json_encode([]);
}
if (empty($err)) {
try {
if ($id) {
$stmt = $pdo->prepare("UPDATE calendar_category_fields SET field_name=?, field_label=?, field_type=?, field_options=?, required=?, sort_order=? WHERE id=?");
$stmt->execute([$fieldName, $fieldLabel, $fieldType, $fieldOptionsJson, $required, $sortOrder, $id]);
$msg[] = 'Field updated successfully';
} else {
$stmt = $pdo->prepare("INSERT INTO calendar_category_fields (category_id, field_name, field_label, field_type, field_options, required, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$categoryId, $fieldName, $fieldLabel, $fieldType, $fieldOptionsJson, $required, $sortOrder]);
$msg[] = 'Field created successfully';
}
logActivity('calendar.categories.fields', 'calendar_field', $id ?? $pdo->lastInsertId(), "Saved field: $fieldLabel");
} catch (PDOException $e) {
$err[] = 'Database error: ' . $e->getMessage();
}
}
}
// Delete Field
if (isset($_POST['action']) && $_POST['action'] === 'delete_field') {
$id = (int)$_POST['field_id'];
try {
$stmt = $pdo->prepare("DELETE FROM calendar_category_fields WHERE id=?");
$stmt->execute([$id]);
$msg[] = 'Field deleted successfully';
logActivity('calendar.categories.fields', 'calendar_field', $id, 'Deleted field');
} catch (PDOException $e) {
$err[] = 'Failed to delete field: ' . $e->getMessage();
}
}
}
// Fetch categories
$categories = [];
try {
$stmt = $pdo->query("SELECT * FROM calendar_categories ORDER BY sort_order ASC, name ASC");
$categories = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
error_log("Categories fetch error: " . $e->getMessage());
}
// Resolve selected category (only for custom categories)
if ($selectedCategoryId > 0) {
foreach ($categories as $c) {
if ((int)$c['id'] === $selectedCategoryId) {
$selectedCategory = $c;
break;
}
}
if ($selectedCategory && isSystemCalendarCategory($selectedCategory)) {
$selectedCategoryId = 0;
$selectedCategory = null;
}
}
// Custom categories (non-system)
$customCategories = array_values(array_filter($categories, fn($c) => !isSystemCalendarCategory($c)));
// Fetch fields (only for the selected custom category)
$fields = [];
if ($selectedCategoryId > 0 && $selectedCategory) {
try {
$stmt = $pdo->prepare("
SELECT f.*, c.name as category_name
FROM calendar_category_fields f
JOIN calendar_categories c ON f.category_id = c.id
WHERE f.category_id = ?
ORDER BY f.sort_order ASC, f.id ASC
");
$stmt->execute([$selectedCategoryId]);
$fields = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
error_log("Fields fetch error: " . $e->getMessage());
}
}
// --- Calendar Settings (stored in system_info) ---
if (($_POST['action'] ?? '') === 'set_calendar_settings') {
$newDefault = (int)($_POST['calendar_default_category_id'] ?? 10);
if ($newDefault <= 0) { $newDefault = 10; }
try {
$stmt = $pdo->prepare(
"INSERT INTO system_info (`key`,`value`) VALUES ('calendar_default_category_id', ?) "
. "ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)"
);
$stmt->execute([(string)$newDefault]);
$alerts[] = ['type' => 'success', 'message' => 'Calendar settings saved successfully'];
} catch (Throwable $e) {
error_log("Calendar settings save error: " . $e->getMessage());
$alerts[] = ['type' => 'error', 'message' => 'Failed to save calendar settings'];
}
}
$calendarDefaultCategoryId = 10;
try {
$stmt = $pdo->prepare("SELECT `value` FROM system_info WHERE `key`='calendar_default_category_id' LIMIT 1");
$stmt->execute();
$val = $stmt->fetchColumn();
if ($val !== false && (int)$val > 0) { $calendarDefaultCategoryId = (int)$val; }
} catch (Throwable $e) {
// ignore
}
include __DIR__ . '/../partials/header.php';
?>
<div class="content-header">
<div>
<h1 class="content-title">📅 Calendar Admin</h1>
<p class="content-subtitle">Manage calendar categories and custom fields</p>
</div>
<button class="btn btn-primary" type="button" onclick="openCategoryModal()">
➕ Add Category
</button>
</div>
<?php if ($msg): foreach ($msg as $m): ?>
<div class="alert alert-success"><?= htmlspecialchars($m) ?></div>
<?php endforeach; endif; ?>
<?php if ($err): foreach ($err as $e): ?>
<div class="alert alert-error"><?= htmlspecialchars($e) ?></div>
<?php endforeach; endif; ?>
<!-- Calendar Settings -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Calendar Settings</h3>
</div>
<div class="card-body">
<form method="post" class="form-inline" style="gap: 12px; flex-wrap: wrap;">
<input type="hidden" name="action" value="set_calendar_settings">
<div class="form-group">
<label for="calendar_default_category_id" style="min-width: 180px;">Default category for new events</label>
<select id="calendar_default_category_id" name="calendar_default_category_id" class="input" style="min-width: 260px;">
<?php foreach ($categories as $cat): ?>
<option value="<?= (int)$cat['id'] ?>" <?= ((int)$cat['id'] === (int)$calendarDefaultCategoryId) ? 'selected' : '' ?>>
<?= htmlspecialchars($cat['name']) ?> (<?= (int)$cat['id'] ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary">Save Settings</button>
</form>
<p class="text-muted" style="margin-top: 10px;">These defaults are applied during install and can be changed any time.</p>
</div>
</div>
<!-- Categories -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Calendar Categories (<?= count($categories) ?>)</h3>
</div>
<div class="card-body">
<?php if (empty($categories)): ?>
<p class="text-muted">No categories found. Create one to get started.</p>
<?php else: ?>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Color</th>
<th>Sort Order</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($categories as $cat): ?>
<tr>
<td><?= (int)$cat['id'] ?></td>
<td><?= htmlspecialchars($cat['name']) ?></td>
<td>
<span style="display: inline-block; width: 20px; height: 20px; background: <?= htmlspecialchars($cat['color']) ?>; border-radius: 3px; border: 1px solid #444;"></span>
<?= htmlspecialchars($cat['color']) ?>
</td>
<td><?= (int)$cat['sort_order'] ?></td>
<td>
<div style="display: flex; gap: 6px; flex-wrap: wrap;">
<button class="btn btn-xs btn-secondary" onclick='editCategory(<?= json_encode($cat) ?>)'>Edit</button>
<?php if (!isSystemCalendarCategory($cat)): ?>
<a class="btn btn-xs btn-primary" href="?category_id=<?= (int)$cat['id'] ?>#custom-fields">Fields</a>
<form method="POST" style="margin: 0;" onsubmit="return confirm('Delete this category?');">
<input type="hidden" name="action" value="delete_category">
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
<button type="submit" class="btn btn-danger btn-xs">🗑</button>
</form>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<?php if ($selectedCategoryId > 0 && $selectedCategory): ?>
<!-- Custom Fields -->
<div class="card" id="custom-fields" style="margin-top: 24px;">
<div class="card-header">
<h3 class="card-title">Custom Fields — <?= htmlspecialchars($selectedCategory['name']) ?> (<?= count($fields) ?>)</h3>
<button class="btn btn-primary" type="button" onclick="openFieldModal()">
➕ Add Field
</button>
</div>
<div class="card-body">
<?php if (empty($fields)): ?>
<p class="text-muted">No custom fields found.</p>
<?php else: ?>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Field Name</th>
<th>Field Label</th>
<th>Type</th>
<th>Required</th>
<th>Sort</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($fields as $field): ?>
<tr>
<td><?= (int)$field['id'] ?></td>
<td><?= htmlspecialchars($field['field_name']) ?></td>
<td><?= htmlspecialchars($field['field_label']) ?></td>
<td><?= htmlspecialchars($field['field_type']) ?></td>
<td><?= $field['required'] ? '✅' : '—' ?></td>
<td><?= (int)($field['sort_order'] ?? 0) ?></td>
<td>
<div style="display: flex; gap: 6px;">
<button class="btn btn-xs btn-secondary" onclick='editField(<?= json_encode($field) ?>)'>Edit</button>
<form method="POST" style="margin: 0;" onsubmit="return confirm('Delete this field?');">
<input type="hidden" name="action" value="delete_field">
<input type="hidden" name="field_id" value="<?= $field['id'] ?>">
<button type="submit" class="btn btn-danger btn-xs">🗑</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<?php else: ?>
<div class="card" id="custom-fields" style="margin-top: 24px;">
<div class="card-header">
<h3 class="card-title">Custom Fields</h3>
</div>
<div class="card-body">
<p class="text-muted">Select a <strong>custom</strong> category and click <strong>Fields</strong> to manage its custom fields.</p>
</div>
</div>
<?php endif; ?>
<!-- Category Modal -->
<div class="modal-overlay" id="categoryModal" style="display: none;">
<div class="modal modal-medium">
<div class="modal-header">
<h3 class="modal-title" id="categoryModalTitle">Add Category</h3>
<button class="modal-close" type="button" onclick="closeCategoryModal()">×</button>
</div>
<form method="POST" id="categoryForm">
<input type="hidden" name="action" value="save_category">
<input type="hidden" name="id" id="category_id">
<div class="modal-body">
<div class="form-group">
<label>Category Name *</label>
<input type="text" name="name" id="category_name" class="form-control" required>
</div>
<div class="form-group">
<label>Color</label>
<input type="color" name="color" id="category_color" class="form-control" value="#3788d8">
</div>
<div class="form-group">
<label>Sort Order</label>
<input type="number" name="sort_order" id="category_sort_order" class="form-control" value="0">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeCategoryModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
<!-- Field Modal -->
<div class="modal-overlay" id="fieldModal" style="display: none;">
<div class="modal modal-medium">
<div class="modal-header">
<h3 class="modal-title" id="fieldModalTitle">Add Field</h3>
<button class="modal-close" type="button" onclick="closeFieldModal()">×</button>
</div>
<form method="POST" id="fieldForm">
<input type="hidden" name="action" value="save_field">
<input type="hidden" name="field_id" id="field_id">
<div class="modal-body">
<div class="form-group">
<label>Category *</label>
<select name="category_id" id="field_category_id" class="form-control" required>
<option value="">Select Category</option>
<?php if (empty($customCategories)): ?>
<option value="" disabled>(No custom categories yet)</option>
<?php else: ?>
<?php foreach ($customCategories as $cat): ?>
<option value="<?= (int)$cat['id'] ?>"><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
<?php if (empty($customCategories)): ?>
<div class="text-muted" style="margin-top:6px;">Create a custom category first, then add fields to it.</div>
<?php endif; ?>
</div>
<div class="form-group">
<label>Field Name (internal) *</label>
<input type="text" name="field_name" id="field_name" class="form-control" required placeholder="e.g., customer_address">
</div>
<div class="form-group">
<label>Field Label (displayed) *</label>
<input type="text" name="field_label" id="field_label" class="form-control" required placeholder="e.g., Customer Address">
</div>
<div class="form-group">
<label>Field Type</label>
<select name="field_type" id="field_type" class="form-control">
<option value="text">Text</option>
<option value="textarea">Textarea</option>
<option value="number">Number</option>
<option value="select">Select</option>
<option value="user">User</option>
</select>
</div>
<div class="form-group" id="field_options_group" style="display:none;">
<label>Field Options (one per line)</label>
<textarea name="field_options" id="field_options" class="form-control" rows="4" placeholder="Option 1
Option 2"></textarea>
<div class="text-muted" style="margin-top:6px;">Only used for Select fields.</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="required" id="field_required">
Required Field
</label>
</div>
<div class="form-group">
<label>Sort Order</label>
<input type="number" name="field_sort_order" id="field_sort_order" class="form-control" value="0">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeFieldModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
<script>
function openCategoryModal() {
document.getElementById('categoryModalTitle').textContent = 'Add Category';
document.getElementById('categoryForm').reset();
document.getElementById('category_id').value = '';
document.getElementById('categoryModal').style.display = 'flex';
}
function editCategory(cat) {
document.getElementById('categoryModalTitle').textContent = 'Edit Category';
document.getElementById('category_id').value = cat.id;
document.getElementById('category_name').value = cat.name;
document.getElementById('category_color').value = cat.color;
document.getElementById('category_sort_order').value = cat.sort_order;
document.getElementById('categoryModal').style.display = 'flex';
}
function closeCategoryModal() {
document.getElementById('categoryModal').style.display = 'none';
}
function openFieldModal() {
const selectedCategoryId = <?= (int)$selectedCategoryId ?>;
document.getElementById('fieldModalTitle').textContent = 'Add Field';
document.getElementById('fieldForm').reset();
document.getElementById('field_id').value = '';
document.getElementById('field_options').value = '';
// If a category is selected (custom only), preselect it
if (selectedCategoryId > 0) {
const catSel = document.getElementById('field_category_id');
if (catSel) {
catSel.value = selectedCategoryId;
}
}
updateFieldOptionsVisibility();
document.getElementById('fieldModal').style.display = 'flex';
}
function editField(field) {
document.getElementById('fieldModalTitle').textContent = 'Edit Field';
document.getElementById('field_id').value = field.id;
document.getElementById('field_category_id').value = field.category_id;
document.getElementById('field_name').value = field.field_name;
document.getElementById('field_label').value = field.field_label;
document.getElementById('field_type').value = field.field_type;
document.getElementById('field_required').checked = field.required == 1;
document.getElementById('field_sort_order').value = field.sort_order;
// Populate select options (stored as JSON array)
let optsText = '';
if (field.field_options) {
try {
const parsed = (typeof field.field_options === 'string') ? JSON.parse(field.field_options) : field.field_options;
if (Array.isArray(parsed)) {
optsText = parsed.join("\n");
}
} catch (e) {
// ignore malformed JSON
}
}
document.getElementById('field_options').value = optsText;
updateFieldOptionsVisibility();
document.getElementById('fieldModal').style.display = 'flex';
}
function updateFieldOptionsVisibility() {
const typeSel = document.getElementById('field_type');
const group = document.getElementById('field_options_group');
if (!typeSel || !group) return;
group.style.display = (typeSel.value === 'select') ? 'block' : 'none';
}
// Wire up onchange
document.addEventListener('DOMContentLoaded', function () {
const typeSel = document.getElementById('field_type');
if (typeSel) {
typeSel.addEventListener('change', updateFieldOptionsVisibility);
updateFieldOptionsVisibility();
}
});
function closeFieldModal() {
document.getElementById('fieldModal').style.display = 'none';
}
</script>
<?php include __DIR__ . '/../partials/footer.php'; ?>