recreate project
This commit is contained in:
@@ -0,0 +1,452 @@
|
||||
<?= $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() ?>
|
||||
Reference in New Issue
Block a user