feat: add modules management view with DevExtreme data grid and level permissions interface

This commit is contained in:
Ümit Tunç
2026-04-29 06:21:57 +03:00
parent 963e0accc9
commit c553960ac9
+313 -171
View File
@@ -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.'
});
}
});
});