Files
alrahma_sunday_school/app/Views/nav_builder/index.php
T
2026-02-10 22:11:06 -05:00

453 lines
14 KiB
PHP

<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<!-- DataTables CSS (Bootstrap 5) -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.8/css/dataTables.bootstrap5.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/responsive/2.5.0/css/responsive.bootstrap5.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/rowgroup/1.4.1/css/rowGroup.bootstrap5.min.css">
<style>
#menuTableWrap {
position: relative;
z-index: 2;
background: #fff;
}
#menuTable.table thead th {
white-space: nowrap;
background: #fff;
position: sticky;
top: 0;
z-index: 3;
}
#menuTable.table td,
#menuTable.table th {
vertical-align: middle;
}
#menuTable.table tr.odd-row > td {
background-color: #fff;
}
#menuTable.table tr.even-row > td {
background-color: #f9fbfd;
}
.badge-role {
background: #34fd02ff;
color: #141414ff;
font-weight: 500;
}
table.dataTable tr.dtrg-group,
table.dataTable tr.dtrg-group.dtrg-start {
background: #f3f6f9 !important;
font-weight: 600;
color: #34495e;
}
#menuTable_wrapper .dataTables_length,
#menuTable_wrapper .dataTables_filter {
margin-bottom: .5rem;
}
#menuTable_wrapper .row {
align-items: center;
}
</style>
<div class="container mt-4"
data-nav-endpoint="<?= site_url('api/nav-builder') ?>"
data-delete-base="<?= site_url('nav-builder/delete') ?>">
<h3>Navbar Builder</h3>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<div class="container-fluid px-0 mt-3">
<div class="card mb-4 rounded-0 border-0" id="menuTableCard">
<div class="card-header">Current Menu</div>
<div class="card-body">
<div id="menuTableWrap" class="table-responsive">
<table id="menuTable" class="table table-striped table-hover align-middle nowrap" style="width:100%">
<thead>
<tr>
<th>Actions</th>
<th>Label</th>
<th>URL</th>
<th>Parent</th>
<th>Roles</th>
<th>Order</th>
<th>Enabled</th>
<th>Depth</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="8" class="text-center text-muted">Loading menu…</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-5">
<div class="card mb-3">
<div class="card-header">Add / Edit Item</div>
<div class="card-body">
<form method="post" action="<?= site_url('nav-builder/save') ?>" id="navBuilderForm">
<?= csrf_field() ?>
<input type="hidden" name="id" id="item_id">
<div class="mb-2">
<label class="form-label">Parent</label>
<select name="menu_parent_id" id="menu_parent_id" class="form-select">
<option value="">— Top level —</option>
</select>
</div>
<div class="mb-2">
<label class="form-label">Label</label>
<input class="form-control" name="label" id="label" required>
</div>
<div class="mb-2">
<label class="form-label">URL (relative or absolute)</label>
<input class="form-control" name="url" id="url" placeholder="e.g. administrator/student_profiles">
</div>
<div class="row">
<div class="col-md-4 mb-2">
<label class="form-label">Target</label>
<select name="target" id="target" class="form-select">
<option value="">(default)</option>
<option value="_blank">_blank</option>
<option value="_self">_self</option>
</select>
</div>
<div class="col-md-4 mb-2">
<label class="form-label">Order</label>
<input class="form-control" type="number" name="sort_order" id="sort_order" value="0">
</div>
<div class="col-md-4 mb-2 d-flex align-items-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="is_enabled" id="is_enabled" checked>
<label class="form-check-label">Enabled</label>
</div>
</div>
</div>
<div class="mb-2">
<label class="form-label">Roles (who can see)</label>
<select name="roles[]" id="roles" class="form-select" multiple size="6"></select>
<small class="text-muted">Hold Ctrl/⌘ to select multiple.</small>
</div>
<button class="btn btn-primary" type="submit">Save</button>
<button class="btn btn-secondary" type="button" id="resetFormButton">Reset</button>
</form>
</div>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
document.addEventListener('DOMContentLoaded', () => {
const container = document.querySelector('[data-nav-endpoint]');
if (!container) {
return;
}
const navEndpoint = container.dataset.navEndpoint;
const deleteBase = container.dataset.deleteBase;
const tableBody = document.querySelector('#menuTable tbody');
const parentSelect = document.getElementById('menu_parent_id');
const rolesSelect = document.getElementById('roles');
const resetButton = document.getElementById('resetFormButton');
const form = document.getElementById('navBuilderForm');
let items = [];
let itemsById = new Map();
const hasDataTables = () =>
!!(window.jQuery && window.jQuery.fn && window.jQuery.fn.DataTable);
const destroyDataTable = () => {
if (!hasDataTables()) return;
if (window.jQuery.fn.DataTable.isDataTable('#menuTable')) {
window.jQuery('#menuTable').DataTable().clear().destroy();
}
};
const initDataTable = () => {
if (!hasDataTables()) return;
window.jQuery('#menuTable').DataTable({
responsive: true,
pageLength: 100,
order: [
[3, 'asc'],
[1, 'asc'],
],
stripeClasses: ['odd-row', 'even-row'],
columnDefs: [
{ targets: [7], visible: false, searchable: false }, // depth
{ targets: [0], orderable: false, searchable: false }, // actions
{ targets: [3], visible: false }, // parent (used for grouping)
],
rowGroup: {
dataSrc: 3,
startRender: (rows, group) =>
`${group} <span class="text-muted">(${rows.count()})</span>`,
},
});
};
const renderParentOptions = (options) => {
if (!parentSelect) return;
const selectedValue = parentSelect.value;
parentSelect.innerHTML = '<option value="">— Top level —</option>';
options
.filter((opt) => opt && typeof opt === 'object')
.forEach((opt) => {
const option = document.createElement('option');
option.value = String(opt.id ?? '');
option.textContent = opt.label ?? '';
parentSelect.appendChild(option);
});
const existing = Array.from(parentSelect.options).find((opt) => opt.value === selectedValue);
if (existing) {
parentSelect.value = selectedValue;
}
};
const renderRolesOptions = (roles) => {
if (!rolesSelect) return;
const previouslySelected = Array.from(rolesSelect.selectedOptions).map((opt) => opt.value);
rolesSelect.innerHTML = '';
roles
.filter((role) => role && typeof role === 'object')
.forEach((role) => {
const option = document.createElement('option');
option.value = String(role.id ?? '');
option.textContent = role.name ?? '';
rolesSelect.appendChild(option);
});
Array.from(rolesSelect.options).forEach((option) => {
option.selected = previouslySelected.includes(option.value);
});
};
const buildBadge = (text) => {
const span = document.createElement('span');
span.className = 'badge badge-role me-1 mb-1';
span.textContent = text;
return span;
};
const renderTable = () => {
if (!tableBody) return;
destroyDataTable();
itemsById = new Map();
tableBody.innerHTML = '';
if (!Array.isArray(items) || items.length === 0) {
const row = document.createElement('tr');
const cell = document.createElement('td');
cell.colSpan = 8;
cell.classList.add('text-center', 'text-muted');
cell.textContent = 'No menu items found.';
row.appendChild(cell);
tableBody.appendChild(row);
return;
}
items.forEach((item) => {
itemsById.set(item.id, item);
const tr = document.createElement('tr');
tr.dataset.itemId = item.id ?? '';
const actionsTd = document.createElement('td');
actionsTd.classList.add('text-nowrap');
const editBtn = document.createElement('button');
editBtn.type = 'button';
editBtn.className = 'btn btn-sm btn-outline-primary me-1';
editBtn.textContent = 'Edit';
editBtn.addEventListener('click', () => editItemById(item.id));
actionsTd.appendChild(editBtn);
const deleteLink = document.createElement('a');
const sanitizedBase = (deleteBase || '').replace(/\/+$/, '');
deleteLink.href = `${sanitizedBase}/${item.id}`;
deleteLink.className = 'btn btn-sm btn-outline-danger';
deleteLink.textContent = 'Delete';
deleteLink.addEventListener('click', (event) => {
if (!confirm('Delete?')) {
event.preventDefault();
}
});
actionsTd.appendChild(deleteLink);
tr.appendChild(actionsTd);
const labelTd = document.createElement('td');
labelTd.dataset.order = (item.label ?? '').toLowerCase();
const indentSpan = document.createElement('span');
indentSpan.classList.add('text-muted');
indentSpan.textContent = '— '.repeat(item.depth ?? 0);
labelTd.appendChild(indentSpan);
labelTd.appendChild(document.createTextNode(item.label ?? ''));
tr.appendChild(labelTd);
const urlTd = document.createElement('td');
if (item.url) {
urlTd.textContent = item.url;
} else {
urlTd.innerHTML = '<span class="text-muted">—</span>';
}
tr.appendChild(urlTd);
const parentTd = document.createElement('td');
parentTd.textContent = item.parent_label ?? '—';
tr.appendChild(parentTd);
const rolesTd = document.createElement('td');
if (Array.isArray(item.roles?.names) && item.roles.names.length > 0) {
item.roles.names.forEach((roleName) => {
if (!roleName) return;
rolesTd.appendChild(buildBadge(roleName));
});
} else {
rolesTd.innerHTML = '<span class="text-muted">—</span>';
}
tr.appendChild(rolesTd);
const orderTd = document.createElement('td');
orderTd.textContent = Number.isFinite(item.order) ? item.order : 0;
tr.appendChild(orderTd);
const enabledTd = document.createElement('td');
if (item.enabled) {
enabledTd.innerHTML = '<span class="badge bg-success">Yes</span>';
} else {
enabledTd.innerHTML = '<span class="badge bg-light text-dark">No</span>';
}
tr.appendChild(enabledTd);
const depthTd = document.createElement('td');
depthTd.textContent = item.depth ?? 0;
tr.appendChild(depthTd);
tableBody.appendChild(tr);
});
initDataTable();
};
const resetForm = () => {
if (!form) return;
form.reset();
const idField = document.getElementById('item_id');
if (idField) idField.value = '';
if (parentSelect) parentSelect.value = '';
if (rolesSelect) {
Array.from(rolesSelect.options).forEach((option) => {
option.selected = false;
});
}
};
const editItemById = (id) => {
const item = itemsById.get(id);
if (!item) return;
resetForm();
document.getElementById('item_id').value = item.id ?? '';
document.getElementById('menu_parent_id').value =
item.parent_id !== null && item.parent_id !== undefined
? String(item.parent_id)
: '';
document.getElementById('label').value = item.label ?? '';
document.getElementById('url').value = item.url ?? '';
document.getElementById('target').value = item.target ?? '';
document.getElementById('sort_order').value =
Number.isFinite(item.order) ? item.order : 0;
document.getElementById('is_enabled').checked = !!item.enabled;
if (rolesSelect && Array.isArray(item.roles?.ids)) {
const ids = item.roles.ids.map((value) => String(value));
Array.from(rolesSelect.options).forEach((option) => {
option.selected = ids.includes(option.value);
});
}
window.scrollTo({
top: 0,
behavior: 'smooth',
});
};
const fetchNavData = () => {
if (!navEndpoint) return;
tableBody.innerHTML = `
<tr>
<td colspan="8" class="text-center text-muted">Loading menu…</td>
</tr>
`;
return fetch(navEndpoint, {
headers: { 'Accept': 'application/json' },
credentials: 'same-origin',
})
.then((response) => {
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
})
.then((payload) => {
items = Array.isArray(payload?.items) ? payload.items : [];
renderParentOptions(payload?.parentOptions ?? []);
renderRolesOptions(payload?.roles ?? []);
renderTable();
resetForm();
})
.catch((error) => {
console.error(error);
tableBody.innerHTML = `
<tr>
<td colspan="8" class="text-center text-danger">
Failed to load menu items.
</td>
</tr>
`;
});
};
if (resetButton) {
resetButton.addEventListener('click', resetForm);
}
fetchNavData();
});
</script>
<?= $this->endSection() ?>