feat: add modules management view with DevExtreme data grid and level permissions interface
This commit is contained in:
@@ -56,167 +56,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="block-content tab-content overflow-hidden">
|
||||
<!-- Modules Tab -->
|
||||
<div class="tab-pane active" id="btabs-modules" role="tabpanel">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text bg-white border-right-0">
|
||||
<i class="si si-magnifier"></i>
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" name="search" id="search" placeholder="{{e2("Search Module...")}}"
|
||||
class="form-control border-left-0 pl-0 bg-white">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover table-vcenter table-sm custom-table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th class="text-center" style="width: 70px;">{{__("Status")}}</th>
|
||||
<th class="text-center" style="width: 70px;">{{__("Mobile")}}</th>
|
||||
<th class="text-center" style="width: 50px;">{{__("Icon")}}</th>
|
||||
<th>{{__("Module Info")}}</th>
|
||||
<th>{{__("Slug / Parent")}}</th>
|
||||
<th class="text-center" style="width: 80px;">{{__("Order")}}</th>
|
||||
<th>{{__("Permissions")}}</th>
|
||||
<th class="text-center" style="width: 120px;"><i class="fa fa-cog"></i></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="types">
|
||||
@foreach($types as $a)
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<select name="enabled" table="types" data-id="{{$a->id}}"
|
||||
class="form-control form-control-sm edit custom-select-status">
|
||||
<option value="1" @if($a->enabled) selected @endif>✅ Active</option>
|
||||
<option value="0" @if(!$a->enabled) selected @endif>🟥 Passive</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<select name="is_mobile" table="types" data-id="{{$a->id}}"
|
||||
class="form-control form-control-sm edit custom-select-status">
|
||||
<option value="1" @if($a->is_mobile) selected @endif>✅ Yes</option>
|
||||
<option value="0" @if(!$a->is_mobile) selected @endif>🟥 No</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="icon-wrapper bg-light p-1 rounded">
|
||||
<img src="{{url("assets/icons/" . $a->icon . ".png")}}" width="24" alt=""
|
||||
class="img-fluid">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column">
|
||||
<input type="text" name="title" value="{{$a->title}}" table="types"
|
||||
data-id="{{$a->id}}"
|
||||
class="form-control form-control-sm edit mb-1 font-w600 title{{$a->id}}"
|
||||
placeholder="Title" />
|
||||
<div class="input-group input-group-sm">
|
||||
<div class="input-group-prepend">
|
||||
<span
|
||||
class="input-group-text border-right-0 bg-transparent text-muted"><i
|
||||
class="fa fa-image"></i></span>
|
||||
</div>
|
||||
<input type="text" name="icon" value="{{$a->icon}}" table="types"
|
||||
data-id="{{$a->id}}"
|
||||
class="form-control form-control-sm edit border-left-0"
|
||||
placeholder="Icon name" />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column">
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<div class="input-group-prepend">
|
||||
<button class="btn btn-alt-primary"
|
||||
onclick="$.get('{{url('admin-ajax/slug?title=')}}'+$('.title{{$a->id}}').val(),function(d){ $('.slug{{$a->id}}').val(d).blur(); })">
|
||||
<i class="si si-refresh"></i>
|
||||
</button>
|
||||
</div>
|
||||
<input type="text" name="slug" value="{{$a->slug}}" table="types"
|
||||
data-id="{{$a->id}}" class="slug{{$a->id}} form-control edit"
|
||||
placeholder="Slug" />
|
||||
</div>
|
||||
<input type="text" name="kid" value="{{$a->kid}}" table="types"
|
||||
data-id="{{$a->id}}" class="form-control form-control-sm edit"
|
||||
placeholder="Parent ID" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="number" name="s" value="{{$a->s}}" table="types" data-id="{{$a->id}}"
|
||||
class="form-control form-control-sm edit text-center" />
|
||||
</td>
|
||||
<td>
|
||||
<div class="permissions-container d-flex flex-wrap gap-2">
|
||||
@php
|
||||
$permTypes = [
|
||||
['key' => 'full_control', 'label' => 'Full', 'icon' => 'fa-lock'],
|
||||
['key' => 'write', 'label' => 'Write', 'icon' => 'fa-pencil-alt'],
|
||||
['key' => 'read', 'label' => 'Read', 'icon' => 'fa-eye'],
|
||||
['key' => 'modify', 'label' => 'Mod', 'icon' => 'fa-edit'],
|
||||
];
|
||||
@endphp
|
||||
@foreach($permTypes as $pt)
|
||||
<div class="perm-item mb-2 mr-2">
|
||||
<small class="text-muted d-block mb-1 font-w600"><i
|
||||
class="fa {{ $pt['icon'] }} mr-1"></i>{{ $pt['label'] }}</small>
|
||||
<div class="level-input mini">
|
||||
<div class="level-tags">
|
||||
@foreach(explode(',', $a->{$pt['key']}) as $level)
|
||||
@if($level !== '')
|
||||
<span
|
||||
class="badge badge-pill badge-{{ levelColor(trim($level)) }}">{{ trim($level) }}</span>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
<button class="btn btn-xs btn-alt-primary edit-levels"
|
||||
data-target="{{ $pt['key'] }}_{{ $a->id }}"
|
||||
data-type="{{ $pt['key'] }}">
|
||||
<i class="fa fa-cog"></i>
|
||||
</button>
|
||||
<input type="hidden" name="{{ $pt['key'] }}" table="types"
|
||||
data-id="{{ $a->id }}" value="{{ $a->{$pt['key']} }}" class="edit"
|
||||
id="{{ $pt['key'] }}_{{ $a->id }}">
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="btn-group">
|
||||
<a href="{{ url('admin/types/' . $a->slug) }}" class="btn btn-sm btn-alt-primary"
|
||||
title="Go to Link">
|
||||
<i class="fa fa-link"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-alt-info open-specific-permissions"
|
||||
data-id="{{ $a->id }}" data-title="{{ $a->title }}"
|
||||
data-slug="{{ $a->slug }}"
|
||||
data-permissions='{{ setting("type_{$a->id}_specific_permissions") }}'
|
||||
data-mobile-columns='{{ setting("type_{$a->id}_mobile_columns") }}'
|
||||
title="Specific Permissions">
|
||||
<i class="fa fa-cogs"></i>
|
||||
</button>
|
||||
<a href="{{ url('admin/types/' . $a->id . '/delete') }}"
|
||||
class="btn btn-sm btn-alt-danger delete-confirm" title="Delete">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td style="display: none;" class="module-data">
|
||||
<?php echo json_encode_tr([
|
||||
'icon' => $a->icon,
|
||||
'title' => $a->title
|
||||
]); ?>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="types-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Levels Tab -->
|
||||
@@ -268,13 +110,289 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("#search").on("keyup", function () {
|
||||
var value = $(this).val().toLowerCase();
|
||||
$("#types tr").filter(function () {
|
||||
$(this).toggle($(this).text().toLowerCase().indexOf(value) > -1)
|
||||
});
|
||||
$(function () {
|
||||
const levelsData = @json(levels());
|
||||
const idToName = {};
|
||||
Object.entries(levelsData).forEach(([name, data]) => {
|
||||
const id = data[0].toString();
|
||||
if (!idToName[id]) idToName[id] = [];
|
||||
idToName[id].push(name);
|
||||
});
|
||||
|
||||
const typesData = [
|
||||
@foreach($types as $a)
|
||||
{
|
||||
id: {{ $a->id }},
|
||||
enabled: {{ $a->enabled ? 1 : 0 }},
|
||||
is_mobile: {{ $a->is_mobile ? 1 : 0 }},
|
||||
icon: "{{ $a->icon }}",
|
||||
title: "{{ $a->title }}",
|
||||
slug: "{{ $a->slug }}",
|
||||
kid: "{{ $a->kid }}",
|
||||
s: {{ (int)$a->s }},
|
||||
full_control: "{{ $a->full_control }}",
|
||||
write: "{{ $a->write }}",
|
||||
read: "{{ $a->read }}",
|
||||
modify: "{{ $a->modify }}",
|
||||
specific_permissions: '{!! addslashes(setting("type_{$a->id}_specific_permissions")) !!}',
|
||||
mobile_columns: '{!! addslashes(setting("type_{$a->id}_mobile_columns")) !!}'
|
||||
},
|
||||
@endforeach
|
||||
];
|
||||
|
||||
// Helper to open level modal directly
|
||||
window.openLevelEdit = function(id, type, currentVal, title, icon) {
|
||||
currentTarget = `target_${type}_${id}`;
|
||||
currentType = type;
|
||||
|
||||
// Ensure target input exists
|
||||
if ($(`#${currentTarget}`).length === 0) {
|
||||
$("<input>").attr("type", "hidden").attr("id", currentTarget).val(currentVal).appendTo("body");
|
||||
|
||||
// Listen for changes
|
||||
$(`#${currentTarget}`).on('change', function() {
|
||||
const newVal = $(this).val();
|
||||
const rowIndex = grid.dxDataGrid("instance").getRowIndexByKey(id);
|
||||
if (rowIndex > -1) {
|
||||
grid.dxDataGrid("instance").cellValue(rowIndex, type, newVal);
|
||||
grid.dxDataGrid("instance").saveEditData();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$(`#${currentTarget}`).val(currentVal);
|
||||
}
|
||||
|
||||
// Setup modal data
|
||||
$('.modal-icon').attr('src', `{{url("assets/icons")}}/${icon}.png`);
|
||||
$('.modal-module-title').text(title);
|
||||
$('.modal-permission-title').text(getPermissionTitle(type));
|
||||
|
||||
// Check checkboxes
|
||||
const currentLevels = currentVal ? currentVal.toString().split(',').filter(l => l) : [];
|
||||
$('.level-checkbox').prop('checked', false);
|
||||
currentLevels.forEach(level => {
|
||||
$('#level_' + parseInt(level)).prop('checked', true);
|
||||
});
|
||||
|
||||
$('#levelModal').modal('show');
|
||||
};
|
||||
|
||||
const grid = $("#types-grid").dxDataGrid({
|
||||
dataSource: typesData,
|
||||
keyExpr: "id",
|
||||
showBorders: true,
|
||||
columnAutoWidth: true,
|
||||
allowColumnResizing: true,
|
||||
filterRow: { visible: true },
|
||||
headerFilter: { visible: true },
|
||||
searchPanel: { visible: true, width: 240, placeholder: "{{e2('Search Module...')}}" },
|
||||
editing: {
|
||||
mode: "cell",
|
||||
allowUpdating: true,
|
||||
startEditAction: "click"
|
||||
},
|
||||
onRowUpdating: function(e) {
|
||||
const id = e.key;
|
||||
const values = e.newData;
|
||||
Object.entries(values).forEach(([name, value]) => {
|
||||
$.post('{{url("admin-ajax/input-edit")}}', {
|
||||
table: "types",
|
||||
value: value,
|
||||
id: id,
|
||||
name: name,
|
||||
_token: '{{ csrf_token() }}'
|
||||
});
|
||||
});
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
dataField: "enabled",
|
||||
caption: "{{__('Status')}}",
|
||||
width: 100,
|
||||
alignment: "center",
|
||||
lookup: {
|
||||
dataSource: [
|
||||
{ id: 1, text: "✅ Active" },
|
||||
{ id: 0, text: "🟥 Passive" }
|
||||
],
|
||||
valueExpr: "id",
|
||||
displayExpr: "text"
|
||||
}
|
||||
},
|
||||
{
|
||||
dataField: "is_mobile",
|
||||
caption: "{{__('Mobile')}}",
|
||||
width: 80,
|
||||
alignment: "center",
|
||||
lookup: {
|
||||
dataSource: [
|
||||
{ id: 1, text: "✅ Yes" },
|
||||
{ id: 0, text: "🟥 No" }
|
||||
],
|
||||
valueExpr: "id",
|
||||
displayExpr: "text"
|
||||
}
|
||||
},
|
||||
{
|
||||
dataField: "icon",
|
||||
caption: "{{__('Icon')}}",
|
||||
width: 60,
|
||||
alignment: "center",
|
||||
allowEditing: true,
|
||||
cellTemplate: function(container, options) {
|
||||
const icon = options.value || "default";
|
||||
$("<div>")
|
||||
.addClass("icon-wrapper bg-light p-1 rounded")
|
||||
.append($("<img>").attr("src", `{{url('assets/icons')}}/${icon}.png`).attr("width", 24))
|
||||
.appendTo(container);
|
||||
}
|
||||
},
|
||||
{
|
||||
dataField: "title",
|
||||
caption: "{{__('Title')}}",
|
||||
cssClass: "font-w600"
|
||||
},
|
||||
{
|
||||
dataField: "slug",
|
||||
caption: "{{__('Slug')}}",
|
||||
cellTemplate: function(container, options) {
|
||||
const $group = $("<div>").addClass("input-group input-group-sm");
|
||||
const $btn = $("<button>")
|
||||
.addClass("btn btn-alt-primary")
|
||||
.append($("<i>").addClass("si si-refresh"))
|
||||
.on("click", function() {
|
||||
$.get('{{url("admin-ajax/slug?title=")}}' + options.data.title, function(d) {
|
||||
grid.dxDataGrid("instance").cellValue(options.rowIndex, "slug", d);
|
||||
grid.dxDataGrid("instance").saveEditData();
|
||||
});
|
||||
});
|
||||
|
||||
$("<div>").addClass("input-group-prepend").append($btn).appendTo($group);
|
||||
$("<input>")
|
||||
.addClass("form-control form-control-sm")
|
||||
.val(options.value)
|
||||
.on("change", function() {
|
||||
grid.dxDataGrid("instance").cellValue(options.rowIndex, "slug", $(this).val());
|
||||
grid.dxDataGrid("instance").saveEditData();
|
||||
})
|
||||
.appendTo($group);
|
||||
$group.appendTo(container);
|
||||
}
|
||||
},
|
||||
{
|
||||
dataField: "kid",
|
||||
caption: "{{__('Parent')}}",
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
dataField: "s",
|
||||
caption: "{{__('Order')}}",
|
||||
width: 60,
|
||||
alignment: "center"
|
||||
},
|
||||
{
|
||||
caption: "{{__('Permissions')}}",
|
||||
columns: [
|
||||
{ dataField: "full_control", caption: "Full", cellTemplate: renderPermissionCell },
|
||||
{ dataField: "write", caption: "Write", cellTemplate: renderPermissionCell },
|
||||
{ dataField: "read", caption: "Read", cellTemplate: renderPermissionCell },
|
||||
{ dataField: "modify", caption: "Mod", cellTemplate: renderPermissionCell }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "buttons",
|
||||
width: 150,
|
||||
buttons: [
|
||||
{
|
||||
icon: "link",
|
||||
hint: "Go to Link",
|
||||
onClick: function(e) {
|
||||
window.open("{{ url('admin/types') }}/" + e.row.data.slug, "_blank");
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: "key",
|
||||
hint: "General Permissions",
|
||||
onClick: function(e) {
|
||||
const data = e.row.data;
|
||||
// Default to Full Control modal as a starting point, or we could add a multi-edit modal
|
||||
window.openLevelEdit(data.id, 'full_control', data.full_control, data.title, data.icon);
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: "preferences",
|
||||
hint: "Specific Permissions",
|
||||
onClick: function(e) {
|
||||
const data = e.row.data;
|
||||
const $btn = $("<button>")
|
||||
.data('id', data.id)
|
||||
.data('title', data.title)
|
||||
.data('slug', data.slug)
|
||||
.data('permissions', data.specific_permissions)
|
||||
.data('mobile-columns', data.mobile_columns)
|
||||
.addClass('open-specific-permissions')
|
||||
.hide()
|
||||
.appendTo('body');
|
||||
$btn.trigger('click');
|
||||
$btn.remove();
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: "trash",
|
||||
hint: "Delete",
|
||||
onClick: function(e) {
|
||||
Swal.fire({
|
||||
title: '{{__("Are you sure?")}}',
|
||||
text: '{{__("This module and all its settings will be deleted!")}}',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#3085d6',
|
||||
confirmButtonText: '{{__("Yes, delete it!")}}',
|
||||
cancelButtonText: '{{__("Cancel")}}'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = "{{ url('admin/types') }}/" + e.row.data.id + "/delete";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
function renderPermissionCell(container, options) {
|
||||
const val = options.value || "";
|
||||
const ids = val.split(',').filter(x => x);
|
||||
const $wrapper = $("<div>").addClass("d-flex flex-wrap gap-1 align-items-center");
|
||||
|
||||
if (ids.length === 0) {
|
||||
$wrapper.append($("<small>").addClass("text-muted").text("-"));
|
||||
} else {
|
||||
ids.forEach(id => {
|
||||
const name = idToName[id] ? idToName[id][0] : id;
|
||||
const color = levelColor(id);
|
||||
$("<span>")
|
||||
.addClass(`badge badge-pill badge-${color}`)
|
||||
.css({ "cursor": "help", "font-size": "11px" })
|
||||
.attr("title", name)
|
||||
.text(id) // Show ID (Number)
|
||||
.appendTo($wrapper);
|
||||
});
|
||||
}
|
||||
|
||||
$("<button>")
|
||||
.addClass("btn btn-xs btn-alt-danger ml-auto")
|
||||
.css({ "border-radius": "50%", "width": "24px", "height": "24px", "padding": "0" })
|
||||
.on("click", function() {
|
||||
window.openLevelEdit(options.data.id, options.column.dataField, val, options.data.title, options.data.icon);
|
||||
})
|
||||
.append($("<i>").addClass("fa fa-cog"))
|
||||
.appendTo($wrapper);
|
||||
|
||||
$wrapper.appendTo(container);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -416,6 +534,14 @@
|
||||
);
|
||||
});
|
||||
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: '{{__("Saved")}}',
|
||||
text: '{{__("Permissions updated successfully")}}',
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
});
|
||||
|
||||
$('#levelModal').modal('hide');
|
||||
});
|
||||
|
||||
@@ -857,18 +983,34 @@
|
||||
try {
|
||||
var res = typeof response === 'string' ? JSON.parse(response) : response;
|
||||
if (res.status === 'success') {
|
||||
$('#specificPermissionsModal').modal('hide');
|
||||
location.reload();
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: '{{__("Saved")}}',
|
||||
text: '{{__("Specific permissions updated successfully")}}',
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
}).then(() => {
|
||||
$('#specificPermissionsModal').modal('hide');
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
alert('Error: ' + (res.message || 'Unknown error'));
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: '{{e2("Error")}}',
|
||||
text: res.message || 'Unknown error'
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Response parse error", e);
|
||||
location.reload(); // Assume success if parse fails but request worked
|
||||
location.reload();
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
alert('An error occurred while saving.');
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: '{{e2("Error")}}',
|
||||
text: 'An error occurred while saving.'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user