(usermanagement): tambah fitur export users dengan filter pencarian

- Modifikasi `UsersExport` untuk menerima parameter search
- Tambah filter whereAny (name, email) di `collection()`
- Tambah kolom "Roles" dengan mapping roles (pluck + implode)
- Null safety branch pakai optional chaining
- Update formatting kolom export
- Modifikasi `UsersController@export` untuk terima & teruskan search
- Batasi role berdasarkan role user login
- Konsistensikan pencarian di `index()` pakai whereAny
- Hapus validasi NIK di profile update
- Tambah ID pada tombol export di `index.blade.php`
- Tambah fungsi JS `updateExportUrl()` untuk sinkronisasi search
- Null safety render branch & role di DataTable
- Tambah listener untuk update URL export saat search
- Perbaiki formatting & indentasi kode
This commit is contained in:
Daeng Deni Mardaeni
2025-08-15 08:35:06 +07:00
parent e3c7bf711c
commit c348af2484
3 changed files with 84 additions and 51 deletions

View File

@@ -9,10 +9,21 @@ use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use Maatwebsite\Excel\Concerns\WithColumnFormatting; use Maatwebsite\Excel\Concerns\WithColumnFormatting;
use Modules\Usermanagement\Models\User; use Modules\Usermanagement\Models\User;
class UsersExport implements WithColumnFormatting, WithHeadings, FromCollection, withMapping class UsersExport implements WithColumnFormatting, WithHeadings, FromCollection, WithMapping
{ {
protected $search;
public function __construct($search = null)
{
$this->search = $search;
}
public function collection(){ public function collection(){
return User::all(); return User::query()
->when($this->search, function ($query) {
$query->whereAny(['name','email'],'like','%'.$this->search.'%');
})
->get();
} }
public function map($row): array{ public function map($row): array{
@@ -21,7 +32,8 @@ class UsersExport implements WithColumnFormatting, WithHeadings, FromCollection,
$row->name, $row->name,
$row->email, $row->email,
$row->nik, $row->nik,
$row->branch->name, $row->branch?->name,
$row->roles?->pluck('name')->implode(', '),
$row->created_at $row->created_at
]; ];
} }
@@ -32,6 +44,7 @@ class UsersExport implements WithColumnFormatting, WithHeadings, FromCollection,
'Email', 'Email',
'NIK', 'NIK',
'Branch', 'Branch',
'Roles',
'Created At' 'Created At'
]; ];
} }
@@ -39,7 +52,7 @@ class UsersExport implements WithColumnFormatting, WithHeadings, FromCollection,
public function columnFormats(): array{ public function columnFormats(): array{
return [ return [
'A' => \PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_NUMBER, 'A' => \PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_NUMBER,
'F' => \PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_DATE_DATETIME 'G' => \PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_DATE_DATETIME
]; ];
} }
} }

View File

@@ -82,10 +82,7 @@
// Apply search filter if provided // Apply search filter if provided
if ($request->has('search') && !empty($request->get('search'))) { if ($request->has('search') && !empty($request->get('search'))) {
$search = $request->get('search'); $search = $request->get('search');
$query->where(function ($q) use ($search) { $query->whereAny(['name','email'],'like','%'.$search.'%');
$q->whereRaw('LOWER(name) LIKE ?', ['%' . strtolower($search) . '%'])
->orWhereRaw('LOWER(email) LIKE ?', ['%' . strtolower($search) . '%']);
});
} }
// Apply sorting if provided // Apply sorting if provided
@@ -241,13 +238,16 @@
return view('usermanagement::users.create', compact('roles', 'branches')); return view('usermanagement::users.create', compact('roles', 'branches'));
} }
public function export() public function export(Request $request)
{ {
if (is_null($this->user) || !$this->user->can('usermanagement.export')) { if (is_null($this->user) || !$this->user->can('usermanagement.export')) {
abort(403, 'Sorry! You are not allowed to export users.'); abort(403, 'Sorry! You are not allowed to export users.');
} }
return Excel::download(new UsersExport, 'users.xlsx'); // Get search parameter from request
$search = $request->get('search');
return Excel::download(new UsersExport($search), 'users.xlsx');
} }
public function profile() public function profile()
@@ -263,7 +263,6 @@
$validatedData = $request->validate([ $validatedData = $request->validate([
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users,email,' . $user->id, 'email' => 'required|string|email|max:255|unique:users,email,' . $user->id,
'nik' => 'required|string|max:255|unique:users,nik,' . $user->id,
'sign' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048', 'sign' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
]); ]);

View File

@@ -7,8 +7,9 @@
@section('content') @section('content')
<div class="container-fluid"> <div class="container-fluid">
<div class="grid"> <div class="grid">
<div class="card card-grid min-w-full" data-datatable="false" data-datatable-page-size="5" data-datatable-state-save="true" id="users-table" data-api-url="{{ route('users.datatables') }}"> <div class="min-w-full card card-grid" data-datatable="false" data-datatable-page-size="5"
<div class="card-header py-5 flex-wrap"> data-datatable-state-save="true" id="users-table" data-api-url="{{ route('users.datatables') }}">
<div class="flex-wrap py-5 card-header">
<h3 class="card-title"> <h3 class="card-title">
List of Users List of Users
</h3> </h3>
@@ -21,50 +22,54 @@
</div> </div>
<div class="flex flex-wrap gap-2.5 lg:gap-5"> <div class="flex flex-wrap gap-2.5 lg:gap-5">
<div class="h-[24px] border border-r-gray-200"> </div> <div class="h-[24px] border border-r-gray-200"> </div>
<a class="btn btn-sm btn-light" href="{{ route('users.export') }}"> Export to Excel </a> <a class="btn btn-sm btn-light" id="export-btn" href="{{ route('users.export') }}"> Export to
Excel </a>
<a class="btn btn-sm btn-primary" href="{{ route('users.create') }}"> Add User </a> <a class="btn btn-sm btn-primary" href="{{ route('users.create') }}"> Add User </a>
</div> </div>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="scrollable-x-auto"> <div class="scrollable-x-auto">
<table class="table table-auto table-border align-middle text-gray-700 font-medium text-sm" data-datatable-table="true"> <table class="table text-sm font-medium text-gray-700 align-middle table-auto table-border"
data-datatable-table="true">
<thead> <thead>
<tr> <tr>
<th class="w-14"> <th class="w-14">
<input class="checkbox checkbox-sm" data-datatable-check="true" type="checkbox"/> <input class="checkbox checkbox-sm" data-datatable-check="true" type="checkbox" />
</th> </th>
<th class="min-w-[250px]" data-datatable-column="name"> <th class="min-w-[250px]" data-datatable-column="name">
<span class="sort"> <span class="sort-label"> Name </span> <span class="sort"> <span class="sort-label"> Name </span>
<span class="sort-icon"> </span> </span> <span class="sort-icon"> </span> </span>
</th> </th>
<th class="min-w-[185px]" data-datatable-column="email"> <th class="min-w-[185px]" data-datatable-column="email">
<span class="sort"> <span class="sort-label"> Email </span> <span class="sort"> <span class="sort-label"> Email </span>
<span class="sort-icon"> </span> </span> <span class="sort-icon"> </span> </span>
</th> </th>
<th class="min-w-[185px]" data-datatable-column="nik"> <th class="min-w-[185px]" data-datatable-column="nik">
<span class="sort"> <span class="sort-label"> NIK </span> <span class="sort"> <span class="sort-label"> NIK </span>
<span class="sort-icon"> </span> </span> <span class="sort-icon"> </span> </span>
</th> </th>
<th class="min-w-[185px]" data-datatable-column="branch"> <th class="min-w-[185px]" data-datatable-column="branch">
<span class="sort"> <span class="sort-label"> Branch </span> <span class="sort"> <span class="sort-label"> Branch </span>
<span class="sort-icon"> </span> </span> <span class="sort-icon"> </span> </span>
</th> </th>
<th class="min-w-[185px]" data-datatable-column="role"> <th class="min-w-[185px]" data-datatable-column="role">
<span class="sort"> <span class="sort-label"> Role </span> <span class="sort"> <span class="sort-label"> Role </span>
<span class="sort-icon"> </span> </span> <span class="sort-icon"> </span> </span>
</th> </th>
<th class="min-w-[50px] text-center" data-datatable-column="actions">Action</th> <th class="min-w-[50px] text-center" data-datatable-column="actions">Action</th>
</tr> </tr>
</thead> </thead>
</table> </table>
</div> </div>
<div class="card-footer justify-center md:justify-between flex-col md:flex-row gap-3 text-gray-600 text-2sm font-medium"> <div
<div class="flex items-center gap-2"> class="flex-col gap-3 justify-center font-medium text-gray-600 card-footer md:justify-between md:flex-row text-2sm">
<div class="flex gap-2 items-center">
Show Show
<select class="select select-sm w-16" data-datatable-size="true" name="perpage"> </select> per page <select class="w-16 select select-sm" data-datatable-size="true" name="perpage"> </select> per
page
</div> </div>
<div class="flex items-center gap-4"> <div class="flex gap-4 items-center">
<span data-datatable-info="true"> </span> <span data-datatable-info="true"> </span>
<div class="pagination" data-datatable-pagination="true"> <div class="pagination" data-datatable-pagination="true">
</div> </div>
@@ -82,7 +87,7 @@
function deleteData(data) { function deleteData(data) {
Swal.fire({ Swal.fire({
title: 'Are you sure?', title: 'Are you sure?',
text: "You won't be able to revert this!" , text: "You won't be able to revert this!",
icon: 'warning', icon: 'warning',
showCancelButton: true, showCancelButton: true,
confirmButtonColor: '#3085d6', confirmButtonColor: '#3085d6',
@@ -92,14 +97,14 @@
if (result.isConfirmed) { if (result.isConfirmed) {
$.ajaxSetup({ $.ajaxSetup({
headers: { headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}' 'X-CSRF-TOKEN': '{{ csrf_token() }}'
} }
}); });
$.ajax(`users/${data}`, { $.ajax(`users/${data}`, {
type: 'DELETE' type: 'DELETE'
}).then((response) => { }).then((response) => {
swal.fire('Deleted!', 'User has been deleted.','success').then(() => { swal.fire('Deleted!', 'User has been deleted.', 'success').then(() => {
window.location.reload(); window.location.reload();
}); });
}).catch((error) => { }).catch((error) => {
@@ -113,6 +118,20 @@
<script type="module"> <script type="module">
const element = document.querySelector('#users-table'); const element = document.querySelector('#users-table');
const searchInput = document.getElementById('search'); const searchInput = document.getElementById('search');
const exportBtn = document.getElementById('export-btn');
// Update export URL with filters
function updateExportUrl() {
let url = new URL(exportBtn.href);
if (searchInput.value) {
url.searchParams.set('search', searchInput.value);
} else {
url.searchParams.delete('search');
}
exportBtn.href = url.toString();
};
const apiUrl = element.getAttribute('data-api-url'); const apiUrl = element.getAttribute('data-api-url');
const dataTableOptions = { const dataTableOptions = {
@@ -141,12 +160,13 @@
branch: { branch: {
title: 'Branch', title: 'Branch',
render: (item, data) => { render: (item, data) => {
return data.branch.name; return data.branch?.name || '-';
}, },
}, },
role: { role: {
title: 'Role', title: 'Role',
render: (item, data) => { render: (item, data) => {
console.log(data);
return data.roles.map(role => role.name).join(', '); return data.roles.map(role => role.name).join(', ');
}, },
}, },
@@ -168,10 +188,11 @@
let dataTable = new KTDataTable(element, dataTableOptions); let dataTable = new KTDataTable(element, dataTableOptions);
// Custom search functionality // Custom search functionality
searchInput.addEventListener('input', function () { searchInput.addEventListener('input', function() {
const searchValue = this.value.trim(); const searchValue = this.value.trim();
dataTable.search(searchValue, true); dataTable.search(searchValue, true);
updateExportUrl();
dataTable.goPage(1);
}); });
</script> </script>
@endpush @endpush