done recap

This commit is contained in:
Jokoprasetio 2026-01-23 01:16:48 +07:00
parent b00ebbc277
commit 44c4689f77
13 changed files with 973 additions and 279 deletions

View File

@ -295,18 +295,21 @@ class DashboardController extends Controller
$zipName = 'files_' . time() . '.zip';
$zipPath = public_path('zip/' . $zipName);
$zip = new ZipArchive;
$tempFiles = [];
if($zip->open($zipPath, ZipArchive::CREATE) === TRUE){
foreach ($paths as $path) {
$fullPath = public_path('file/' . $path);
if(file_exists($fullPath)){
$relativePathInZip = $path;
$zip->addFile($fullPath, $relativePathInZip);
}else{
if(!file_exists($fullPath)){
throw new \Exception("File tidak ditemukan: " . $path);
}
$relativePathInZip = $path;
$fileToAdd = $this->prepareFileWithWatermark($fullPath, $tempFiles);
$zip->addFile($fileToAdd, $relativePathInZip);
}
$zip->close();
}
$this->cleanupTempFiles($tempFiles);
return response()->download(public_path('zip/' . $zipName))->deleteFileAfterSend(true);
//code...
} catch (\Throwable $th) {
@ -333,18 +336,21 @@ class DashboardController extends Controller
$zipName = 'files_' . time() . '.zip';
$zipPath = public_path('zip/' . $zipName);
$zip = new ZipArchive;
$tempFiles = [];
if($zip->open($zipPath, ZipArchive::CREATE) === TRUE){
foreach ($data as $path) {
$fullPath = public_path('file/' . $path);
if(file_exists($fullPath)){
$relativePathInZip = $path;
$zip->addFile($fullPath, $relativePathInZip);
}else{
if(!file_exists($fullPath)){
throw new \Exception("File tidak ditemukan: " . $path);
}
$relativePathInZip = $path;
$fileToAdd = $this->prepareFileWithWatermark($fullPath, $tempFiles);
$zip->addFile($fileToAdd, $relativePathInZip);
}
$zip->close();
}
$this->cleanupTempFiles($tempFiles);
return response()->download(public_path('zip/' . $zipName))->deleteFileAfterSend(true);
} catch (\Throwable $th) {
return response()->json([
@ -370,10 +376,27 @@ class DashboardController extends Controller
}
public function dataDocumentLast(){
$perPage = request('per_page', 10);
$perPage = (int) request('per_page', 10);
$authUnitId = auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]?->objectunitkerjapegawaifk;
$keyword = request('keyword');
$data = FileDirectory::where('statusenabled', true)
->where(function ($query) use ($authUnitId) {
$query->where('permission_file', true)
->orWhere(function ($sub) use ($authUnitId) {
$sub->where('permission_file', false)
->where('id_unit_kerja', $authUnitId);
});
})
->when($keyword, function ($q) use ($keyword) {
$q->where(function ($sub) use ($keyword) {
$sub->where('file', 'ILIKE', "%{$keyword}%")
->orWhere('no_dokumen', 'ILIKE', "%{$keyword}%");
});
})
->orderBy('entry_at', 'desc')
->paginate($perPage);
$payload = [
'status' => true,
'message' => 'Berhasil mendapatkan data',
@ -381,7 +404,10 @@ class DashboardController extends Controller
'pagination' => [
'current_page' => $data->currentPage(),
'next_page' => $data->hasMorePages() ? $data->currentPage() + 1 : null,
'has_more' => $data->hasMorePages()
'has_more' => $data->hasMorePages(),
'last_page' => $data->lastPage(),
'per_page' => $data->perPage(),
'total' => $data->total(),
]
];
return response()->json($payload);
@ -391,10 +417,16 @@ class DashboardController extends Controller
DB::connection('dbDirectory')->beginTransaction();
try {
$datas = request('data');
foreach ($datas as $data) {
foreach ($datas as $index => $data) {
list($id_unit_kerja, $nama_unit_kerja) = explode('/', $data['id_unit_kerja'],2);
list($id_sub_unit_kerja, $nama_sub_unit_kerja) = explode('/', $data['id_sub_unit_kerja'],2);
list($master_kategori_directory_id, $nama_kategori) = explode('/', $data['master_kategori_directory_id'],2);
$uploadedFile = request()->file("data.$index.file");
if(!$uploadedFile){
throw new \RuntimeException('File wajib diunggah pada baris ke-' . ($index+1));
}
$payload = [
'id_unit_kerja' => $id_unit_kerja,
'id_sub_unit_kerja' => $id_sub_unit_kerja,
@ -403,16 +435,16 @@ class DashboardController extends Controller
'pegawai_nama_entry' => auth()->user()->dataUser->namalengkap ?? null,
'tanggal_terbit' => $data['date_active'] ?? null,
'no_dokumen' => $data['no_dokumen'] ?? null,
'permission_file' => $data['is_permission'] === "1" ? true : false,
'permission_file' => ($data['is_permission'] ?? null) == "1",
];
if(!empty($data['file'])){
$file = $data['file'];
$imageName = $file->getClientOriginalName();
$imageName = $uploadedFile->getClientOriginalName();
$path = "{$nama_unit_kerja}/{$nama_sub_unit_kerja}/{$nama_kategori}";
$file->storeAs($path, $imageName, 'file_directory');
$uploadedFile->storeAs($path, $imageName, 'file_directory');
$payload['file'] =$path .'/' .$imageName;
}
$fd = FileDirectory::create($payload);
$payloadLog = [
'file_directory_id' => $fd->file_directory_id,
'pegawai_id_entry' => $fd->pegawai_id_entry,
@ -513,6 +545,71 @@ class DashboardController extends Controller
]);
}
/**
* Generate watermarked PDF to temp file (or return original) for zipping.
*/
private function prepareFileWithWatermark(string $fullPath, array &$tempFiles = [])
{
$stampFile = public_path('assets/copy.png');
$ext = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION));
if ($ext !== 'pdf' || !file_exists($stampFile)) {
return $fullPath;
}
$tempDir = storage_path('app/temp');
if (!is_dir($tempDir)) {
@mkdir($tempDir, 0777, true);
}
$tempOut = $tempDir . '/' . uniqid('wm_') . '.pdf';
try {
$this->watermarkCenterToFile($fullPath, $stampFile, $tempOut);
} catch (\Throwable $e) {
// fallback convert then watermark
$converted = $tempDir . '/' . uniqid('conv_') . '.pdf';
$this->convertWithGhostscript($fullPath, $converted);
$this->watermarkCenterToFile($converted, $stampFile, $tempOut);
$tempFiles[] = $converted;
}
$tempFiles[] = $tempOut;
return $tempOut;
}
private function watermarkCenterToFile(string $pdfPath, string $stampFile, string $outputPath): void
{
$pdf = new Fpdi();
$pageCount = $pdf->setSourceFile($pdfPath);
for ($pageNo = 1; $pageNo <= $pageCount; $pageNo++) {
$tplId = $pdf->importPage($pageNo);
$size = $pdf->getTemplateSize($tplId);
$pdf->AddPage($size['orientation'], [$size['width'], $size['height']]);
$pdf->useTemplate($tplId);
$stampW = $size['width'] * 0.60;
$stampH = $stampW * 0.75;
$x = ($size['width'] - $stampW) / 2;
$y = ($size['height'] - $stampH) / 2;
$pdf->Image($stampFile, $x, $y, $stampW, $stampH);
}
$pdf->Output($outputPath, 'F');
}
private function cleanupTempFiles(array $tempFiles): void
{
foreach ($tempFiles as $file) {
if (is_string($file) && file_exists($file)) {
@unlink($file);
}
}
}
private function convertWithGhostscript(string $inputPdf, string $outputPdf): void
{
$gs = config('services.ghostscript.path');
@ -530,4 +627,78 @@ class DashboardController extends Controller
throw new \RuntimeException('Convert Ghostscript gagal (code=' . $code . ')');
}
}
public function recapData(){
try {
$perPage = (int) request('per_page', 10);
$page = max(1, (int) request('page', 1));
$keyword = strtolower(request('keyword', ''));
$rows = FileDirectory::where('statusenabled', true)->pluck('file');
$grouped = [];
foreach ($rows as $path) {
$parts = array_values(array_filter(explode('/', $path)));
if(count($parts) < 4){
continue;
}
$unit = $parts[0];
$folder = $parts[1]. '/' . $parts[2];
if($keyword){
$hit = str_contains(strtolower($unit), $keyword) || str_contains(strtolower($folder), $keyword);
if(!$hit) continue;
}
if(!isset($grouped[$unit])){
$grouped[$unit] = [];
}
if (!isset($grouped[$unit][$folder])) {
$grouped[$unit][$folder] = 0;
}
$grouped[$unit][$folder]++;
}
$result = [];
foreach ($grouped as $unitName => $folders) {
$data = [];
foreach ($folders as $folder => $count) {
$data[] = [
'folder'=> $folder,
'count' => $count,
];
}
usort($data, fn($a, $b) => $b['count'] <=> $a['count']);
$result[] = [
'unit' => $unitName,
'data' => $data,
];
}
// paginate manually
$total = count($result);
$chunks = array_chunk($result, $perPage);
$currentData = $chunks[$page-1] ?? [];
return response()->json([
'status' => true,
'data' => $currentData,
'message' => 'Berhasil mendapatkan data',
'pagination' => [
'current_page' => $page,
'per_page' => $perPage,
'total' => $total,
'last_page' => max(1, ceil($total / $perPage)),
'has_more' => $page < max(1, ceil($total / $perPage)),
]
]);
} catch (\Throwable $th) {
return response()->json([
'status' => false,
'message' => 'Gagal! mendapatkan data'
]);
}
}
public function recapView(){
return view('dashboard.recap', ['title' => 'Rekap Dokumen']);
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers;
use App\Models\LogActivity;
use Illuminate\Http\Request;
class LogActivityController extends Controller
{
public function index(){
$data =[
'title' => 'Log Activity'
];
return view('logActivity.index', $data);
}
public function datatable(){
$perPage = (int) request('per_page', 10);
$keyword = request('keyword');
$start = request('start_date');
$end = request('end_date');
$query = LogActivity::query()
->orderBy('entry_at','desc');
if($keyword){
$query->where(function($q) use ($keyword){
$q->where('pegawai_nama_entry', 'ILIKE', "%{$keyword}%")
->orWhere('action_type', 'ILIKE', "%{$keyword}%")
->orWhere('file', 'ILIKE', "%{$keyword}%")
->orWhere('no_dokumen', 'ILIKE', "%{$keyword}%");
});
}
if($start){
$query->whereDate('entry_at','>=',$start);
}
if($end){
$query->whereDate('entry_at','<=',$end);
}
$paginated = $query->paginate($perPage);
$data = $paginated->getCollection()->map(function($item){
return [
'pegawai_nama_entry' => $item->pegawai_nama_entry,
'action_type' => $item->action_type,
'file' => $item->file,
'no_dokumen' => $item->no_dokumen ?? $item->fileDirectory->no_dokumen ?? '-',
'entry_at' => $item->entry_at,
'unit_name' => $item->unit_name ?? '-',
];
});
return response()->json([
'status' => true,
'data' => $data,
'pagination' => [
'current_page' => $paginated->currentPage(),
'next_page' => $paginated->hasMorePages() ? $paginated->currentPage() + 1 : null,
'has_more' => $paginated->hasMorePages(),
'last_page' => $paginated->lastPage(),
'per_page' => $paginated->perPage(),
'total' => $paginated->total(),
]
]);
}
}

View File

@ -11,4 +11,9 @@ class LogActivity extends Model
public $timestamps = false;
protected $primaryKey = 'id';
protected $guarded = ['id'];
// public function unitKerja(){
// return $this->belongsTo(UnitKerja::class, 'id_unit_kerja', 'id');
// }
}

View File

@ -1,4 +1,3 @@
let allFiles = [];
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".file-input").forEach(input => {
bindFileUpload(input);
@ -6,72 +5,40 @@
});
function bindFileUpload(input) {
const dropArea = input.closest(".file-drop-area");
const fileNameBox = dropArea.querySelector(".file-name");
const inputId = input?.id;
allFiles[inputId] = [];
const fileNameBox = input.closest(".border")?.querySelector(".file-name") || input.closest(".file-drop-area")?.querySelector(".file-name");
input.addEventListener("change", function () {
for (let i = 0; i < this.files.length; i++) {
allFiles[inputId].push(this.files[i]);
if (!fileNameBox) return;
const file = this.files[0];
if (file) {
fileNameBox.textContent = `${file.name}`;
fileNameBox.classList.remove("d-none");
} else {
fileNameBox.textContent = '';
fileNameBox.classList.add("d-none");
}
renderFileList(inputId, fileNameBox);
this.value = ""; // reset agar bisa pilih file lagi
});
}
function renderFileList(inputId, container) {
const files = allFiles[inputId];
if (!files || files.length === 0) {
container.classList.add("d-none");
container.innerHTML = "";
return;
}
let list = "<ul class='list-unstyled mb-0'>";
files.forEach((file, index) => {
list += `
<li class="d-flex justify-content-between align-items-center">
<span> ${file.name}</span>
<button type="button" class="btn btn-sm btn-danger ms-2"
onclick="removeFile('${inputId}', ${index})"></button>
</li>`;
});
list += "</ul>";
container.innerHTML = list;
container.classList.remove("d-none");
}
function removeFile(inputId, index) {
allFiles[inputId].splice(index, 1);
const container = document.querySelector(`#${inputId}`).closest(".file-drop-area").querySelector(".file-name");
renderFileList(inputId, container);
function resetCreateForm(){
colCount = 1;
$("#col_add_file").html('');
formCreate[0]?.reset();
formCreate.find('select').val(null).trigger('change');
formCreate.find('input[type="file"]').val('');
formCreate.find('.file-name').addClass('d-none').text('');
formCreate.find('input[type="radio"]').prop('checked', false);
// rebind initial input
document.querySelectorAll(".file-input").forEach(bindFileUpload);
}
formCreate.off('submit').on('submit', function(e){
e.preventDefault();
const submitBtn = $(this).find('button[type="submit"]');
submitBtn.prop('disabled', true).text('menyimpan...')
let hasFile = Object.keys(allFiles).every(id => {
console.log('fil ' , allFiles[id],' length', allFiles[id].length > 0);
return Array.isArray(allFiles[id]) && allFiles[id].length > 0;
});
if(!hasFile){
Swal.fire({
icon: 'warning',
title: 'Perhatian',
text: 'Silahkan upload minimal 1 file sebelum submit'
});
submitBtn.prop('disabled', false).text('Simpan')
return;
}
const formData = new FormData(this);
for (const inputId in allFiles) {
allFiles[inputId].forEach((file, index) => {
formData.append(`data[${inputId}][file][]`, file); // gunakan inputId = name input file di HTML, misal "files[]"
});
}
fetch(`/upload`, {
fetch(`/uploadv2`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('input[name="_token"]').value,
@ -80,43 +47,29 @@
}).then(async(res) => {
const responseData = await res.json();
if (responseData.status) {
const handler = function () {
Toastify({
text: responseData.message || 'Berhasil melakukan aksi!',
duration: 3000,
gravity: "top", // bisa "bottom"
position: "right", // bisa "left"
gravity: "top",
position: "right",
style: {
background: "linear-gradient(to right, #00b09b, #96c93d)", // hijau gradasi
background: "linear-gradient(to right, #00b09b, #96c93d)",
color: "#fff",
}
}).showToast();
allFiles = [];
formCreate.find('input[type="file"].file-input').each(function () {
const newInput = $(this).clone(); // clone dengan attribute multiple
$(this).replaceWith(newInput); // ganti input lama dengan baru
bindFileUpload(newInput[0])
// console.log(newInput);
});
colCount = 1;
$("#col_add_file").html('')
formCreate.find('select').val(null).trigger('change');
document.querySelectorAll(".file-name").forEach(el => {
el.classList.add("d-none");
el.innerHTML = "";
});
resetCreateForm();
if($("#klasifikasi_dok").val().length === 0 || $("#kategori_dok").val().length === 0 ){
const kategoriVal = $("#kategori_dok").val() || [];
if(kategoriVal.length === 0){
index()
}else{
searchData()
}
submitBtn.prop('disabled', false).text('Simpan')
modalCreate.removeEventListener('hidden.bs.modal', handler);
};
modalCreate.addEventListener('hidden.bs.modal', handler);
bootstrap.Modal.getInstance(modalCreate).hide();
const modalInstance = bootstrap.Modal.getInstance(modalCreate);
modalInstance?.hide();
} else {
throw new Error(responseData.message || 'Terjadi kesalahan saat menyimpan data.');
}
@ -128,9 +81,7 @@
title: 'Gagal',
text: err.message
});
submitBtn.prop('disabled', false).text('Simpan...')
submitBtn.prop('disabled', false).text('Simpan')
}
});
});

View File

@ -55,7 +55,6 @@ $(document).ready(function() {
}
});
$('.klasifikasi_dok').select2();
$('.kategori_dok').select2();
if(allAkses){
@ -91,54 +90,114 @@ function addForm(){
let html = '';
html += `
<div class="row mt-3" id="col-${colCount}">
<hr />
<div class="col-md-6 mb-2">
<label>Unit</label>
<select class="form-control" name="data[${colCount}][id_unit_kerja]" id="select_id_unit_kerja_${colCount}" required>
<div class="row g-3 align-items-start mt-2" id="col-${colCount}">
<hr class="my-2" />
<div class="col-12 d-flex justify-content-end">
<button type="button"
class="btn btn-sm btn-danger"
onclick="removeCol(${colCount})">
<i class="fa-solid fa-trash"></i> Hapus
</button>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Unit <span class="text-danger">*</span></label>
<select class="form-control"
name="data[${colCount}][id_unit_kerja]"
id="select_id_unit_kerja_${colCount}"
required>
<option value="" disable>Select Choose</option>
</select>
</div>
<div class="col-md-6 mb-2">
<label>Sub Unit</label>
<select class="form-control" name="data[${colCount}][id_sub_unit_kerja]" id="select_id_sub_unit_kerja_${colCount}" required>
<div class="col-md-4">
<label class="form-label fw-semibold">Sub Unit <span class="text-danger">*</span></label>
<select class="form-control"
name="data[${colCount}][id_sub_unit_kerja]"
id="select_id_sub_unit_kerja_${colCount}"
required>
<option value="" disable>Select Choose</option>
</select>
</div>
<div class="col-md-6 mb-2">
<label>Kategori Dokumen</label>
<select class="form-control" name="data[${colCount}][master_kategori_directory_id]" id="select_kategori_${colCount}" required>
<div class="col-md-4">
<label class="form-label fw-semibold">Kategori Dokumen <span class="text-danger">*</span></label>
<select class="form-control"
name="data[${colCount}][master_kategori_directory_id]"
id="select_kategori_${colCount}"
required>
<option value="" disable>Select Choose</option>
${katDok.map(dok => `
<option value="${dok?.master_kategori_directory_id}/${dok?.nama_kategori_directory}">${dok?.nama_kategori_directory}</option>
`).join('')}
</select>
</div>
<div class="col-md-6 mb-2">
<label>Klasifikasi Dokumen</label>
<select class="form-select" name="data[${colCount}][klasifikasi]" required>
<option value="" disable >Select Choose</option>
${klasifikasiDok.map(kla => `
<option value="${kla?.master_klasifikasi_directory_id}/${kla?.nama_klasifikasi_directory}">${kla?.nama_klasifikasi_directory}</option>
`).join('')}
</select>
<div class="col-md-6">
<label class="form-label fw-semibold">Nomor Dokumen</label>
<div class="input-group">
<span class="input-group-text">#</span>
<input type="text"
class="form-control"
name="data[${colCount}][no_dokumen]"
placeholder="Contoh: 001/RS/IT/I/2026">
</div>
<div class="col-md-11 mb-2">
<label for="fileUpload${colCount}" class="form-label fw-semibold">📂 Upload Dokumen</label>
<div class="file-drop-area border rounded-3 p-1 shadow-sm">
<input class="file-input" type="file" id="fileUpload${colCount}" accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.xls,.xlsx" multiple>
<div class="mt-2 text-success fw-semibold d-none file-name"></div>
</div>
<div class="form-text text-muted fw-semibold">Format yang didukung: PDF, JPG, PNG, Excel dan Word</div>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger mt-4 me-2" onclick="removeCol(${colCount})"><i class="fa-solid fa-trash"></i></button>
<div class="col-md-3">
<label class="form-label fw-semibold">Tanggal Terbit</label>
<input class="form-control"
type="date"
name="data[${colCount}][date_active]"
required>
</div>
<div class="col-md-3">
<label class="form-label fw-semibold">Boleh dilihat unit lain? <span class="text-danger">*</span></label>
<div class="border rounded-3 p-2 bg-light">
<div class="form-check">
<input class="form-check-input"
type="radio"
name="data[${colCount}][is_permission]"
id="perm_yes_${colCount}"
value="1"
required>
<label class="form-check-label" for="perm_yes_${colCount}">Iya</label>
</div>
<div class="form-check mt-1">
<input class="form-check-input"
type="radio"
name="data[${colCount}][is_permission]"
id="perm_no_${colCount}"
value="2"
required>
<label class="form-check-label" for="perm_no_${colCount}">Tidak</label>
</div>
</div>
</div>
<div class="col-md-12">
<label for="fileUpload_${colCount}" class="form-label fw-semibold">📂 Upload Dokumen (PDF)</label>
<div class="border rounded-3 p-3 bg-white shadow-sm">
<input class="form-control file-input"
type="file"
id="fileUpload_${colCount}"
accept=".pdf"
name="data[${colCount}][file]">
<div class="mt-2 text-success fw-semibold d-none file-name" id="fileName_${colCount}"></div>
</div>
<div class="form-text text-muted">
Format yang didukung: <b>PDF</b>.
</div>
</div>
</div>
`
col.append(html)
let newInput = document.querySelector(`#fileUpload${colCount}`);
let newInput = document.querySelector(`#fileUpload_${colCount}`);
bindFileUpload(newInput);
if(allAkses){
selectOptionUnitKerjaV1(colCount)
@ -263,5 +322,3 @@ function selectOptionUnitKerjaV2(colCount) {
});
}

View File

@ -40,20 +40,23 @@ function renderTree(units, katDok, keyword) {
<ul class="file-tree-ul ms-2">
${files.map(file => {
let fileName = file.file.split('/').pop();
console.log(file);
const isPublic = String(file.permission_file).toLowerCase() === 'true' || file.permission_file === 1 || file.permission_file === '1';
return `
<li class="file-tree-li">
<div class="">
📄
<div class="d-flex align-items-center gap-2 flex-wrap">
<span>📄</span>
<a href="#" class="file-link"
data-file="${file?.file}"
data-fileName="${fileName || '-'}"
data-id="${file.file_directory_id}"
data-no_dokumen="${file.no_dokumen || '-'}"
data-tanggal_terbit="${file.tanggal_terbit || '-'}" data-permission_file="${file.permission_file || '-'}">${fileName}</a>
data-tanggal_terbit="${file.tanggal_terbit || '-'}"
data-permission_file="${file.permission_file || '-'}">${fileName}</a>
<span class="badge ${isPublic ? 'bg-success' : 'bg-secondary'}">
${isPublic ? 'Umum' : 'Internal'}
</span>
</div>
</li>
`;
@ -74,13 +77,12 @@ function renderTree(units, katDok, keyword) {
`;
}
function index(kategori_dok = [], unitKerja = null, subUnitKerja = [], klasifikasi_id = [], keyword = '') {
function index(kategori_dok = [], unitKerja = null, subUnitKerja = [], keyword = '') {
const params = new URLSearchParams();
if (kategori_dok.length) params.append("kategori", kategori_dok);
if (unitKerja) params.append("unitKerja", unitKerja);
if (subUnitKerja.length) params.append("subUnit", subUnitKerja);
if (klasifikasi_id.length) params.append("klasifikasi", klasifikasi_id);
if (keyword) params.append("keyword", keyword);
fetch(`/data-unit-kerja?${params.toString()}`)
@ -126,22 +128,22 @@ function referesh(){
function searchData(){
let klasifikasi_id = $("#klasifikasi_dok").val()
let kategori_dok = $("#kategori_dok").val()
let unitKerja = $("#unit_kerja").val()
let subUnitKerja = $("#sub_unit_kerja").val()
let keyword = ($("#search_file").val() || '').trim();
if(klasifikasi_id.length === 0 ||
kategori_dok.length === 0 ||
subUnitKerja.length === 0){
const hasFilters = kategori_dok.length && subUnitKerja.length;
if(!hasFilters && !keyword){
Swal.fire({
text: 'Lengkapi Isi form pencarian',
text: 'Isi kata kunci atau lengkapkan filter pencarian.',
icon: 'warning'
})
return
}
index(kategori_dok, unitKerja, subUnitKerja, klasifikasi_id);
index(kategori_dok, unitKerja, subUnitKerja, [], keyword);
}
let debounceTimer;
@ -154,11 +156,10 @@ function debounceSearch(input) {
function searchFile(keyword){
let klasifikasi_id = $("#klasifikasi_dok").val()
let kategori_dok = $("#kategori_dok").val()
let unitKerja = $("#unit_kerja").val()
let subUnitKerja = $("#sub_unit_kerja").val()
index(kategori_dok, unitKerja, subUnitKerja, klasifikasi_id, keyword);
index(kategori_dok, unitKerja, subUnitKerja, [], keyword);
}

View File

@ -63,24 +63,22 @@
</div>
{{-- @dd($allAkses) --}}
<div class="card-body p-3">
<div class="row mb-3 mt-3">
<!-- Unit Kerja -->
<div class="col-md-2">
<label for="unit_kerja" class="form-label">Unit Kerja</label>
<div class="bg-light rounded-3 p-3 mb-3 shadow-sm border">
<div class="row g-3 align-items-end">
<div class="col-md-4">
<label for="unit_kerja" class="form-label fw-semibold text-muted small">Unit Kerja</label>
<select class="form-control unit_kerja" id="unit_kerja"{{ ($allAkses && ($allAkses->all_akses || $allAkses->unit_akses)) ? '' : 'disabled' }}>
</select>
</div>
<!-- Sub Unit Kerja -->
<div class="col-md-2">
<label for="sub_unit_kerja" class="form-label">Sub Unit Kerja</label>
<div class="col-md-4">
<label for="sub_unit_kerja" class="form-label fw-semibold text-muted small">Sub Unit Kerja</label>
<select class="form-control sub_unit_kerja" id="sub_unit_kerja" @if($allAkses && ($allAkses->all_akses || $allAkses->unit_akses)) multiple="multiple" @else disabled @endif>
</select>
</div>
<!-- Kategori Dokumen -->
<div class="col-md-2">
<label for="kategori_dok" class="form-label">Kategori Dok.</label>
<div class="col-md-4">
<label for="kategori_dok" class="form-label fw-semibold text-muted small">Kategori Dok.</label>
<select class="form-control kategori_dok" id="kategori_dok" multiple="multiple" placeholder="--Pilih kategori dokumen--">
<option value="" disabled>Select Choose</option>
@foreach ($katDok as $kat)
@ -89,39 +87,47 @@
</select>
</div>
<!-- Klasifikasi Dokumen -->
<div class="col-md-2">
<label for="klasifikasi_dok" class="form-label">Klasifikasi Dok.</label>
<select class="form-control klasifikasi_dok" id="klasifikasi_dok" multiple="multiple" placeholder="--Pilih klasifikasi dokumen--">
<option value="" disabled>Select Choose</option>
@foreach ($klasifikasiDok as $kla)
<option value="{{ $kla->master_klasifikasi_directory_id }}">{{ $kla->nama_klasifikasi_directory }}</option>
@endforeach
</select>
<div class="col-md-6 d-flex gap-2">
<button type="button" class="btn btn-primary flex-grow-1" onclick="searchData()">
<i class="fa fa-search me-1"></i> Cari
</button>
<button class="btn btn-outline-secondary" onclick="referesh()">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<!-- Button Cari -->
<div class="col-md-2 d-flex align-items-end">
<button type="button" class="btn btn-primary w-100" onclick="searchData()">Cari</button>
<div class="col-md-6 d-flex justify-content-md-end">
<button type="button" class="btn btn-success w-100 w-md-auto" data-bs-target="#modalCreateFile" data-bs-toggle="modal">
<i class="fa fa-plus me-1"></i> Tambah File
</button>
</div>
</div>
<div class="d-flex align-items-center gap-2 mt-3 flex-wrap">
<div class="badge bg-light text-secondary border">
Tips: pilih Unit/Sub Unit & Kategori untuk menampilkan data.
</div>
<div class="ms-auto small text-muted d-flex align-items-center gap-2">
<i class="fa fa-filter"></i> Filter aktif siap digunakan
</div>
<!-- Button Tambah File -->
<div class="col-md-2 d-flex align-items-end">
<button type="button" class="btn btn-success w-100" data-bs-target="#modalCreateFile" data-bs-toggle="modal">
<i data-feather="plus" class="me-1"></i>
Tambah File</button>
</div>
</div>
<div class="row">
<div id="tree-wrapper" class="col-md-12 mb-3">
<div class="d-flex gap-2 mb-3 flex-wrap">
<button class="btn btn-sm btn-primary" onclick="referesh()">
<div class="d-flex gap-2 mb-3 flex-wrap align-items-center">
<button class="btn btn-sm btn-outline-primary" onclick="referesh()">
<i class="fas fa-sync-alt"></i> Refresh
</button>
<button type="button" id="btn-download-multi" class="btn btn-sm btn-primary" onclick="downloadMultiple()" disabled>
<i class="fas fa-download"></i> Download Multiple (<span id="multi-count">0</span>)
</button>
<input type="text" id="search_file" oninput="debounceSearch(this)" class="form-control form-control-sm" placeholder="Search" style="max-width:220px;">
<div class="input-group input-group-sm" style="max-width:260px;">
<span class="input-group-text bg-white"><i class="fa fa-search text-muted"></i></span>
<input type="text" id="search_file" oninput="debounceSearch(this)" class="form-control" placeholder="Cari nama/no dokumen">
</div>
<div class="d-flex align-items-center gap-2 ms-auto">
<span class="badge bg-success">Umum</span>
<span class="badge bg-secondary">Internal</span>
</div>
</div>
<div id="file_tree"></div>
</div>
@ -141,6 +147,11 @@
const allAkses = @json($allAkses);
const authUnitKerja = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]?->unitKerja);
const authSubUnitKerja = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]);
function isPublic(permissionVal){
if(permissionVal === null || permissionVal === undefined) return false;
const val = String(permissionVal).toLowerCase();
return val === '1' || val === 'true' || val === 'iya' || val === 'yes';
}
let currentFile = null;
let idDirectory = null;
@ -149,16 +160,22 @@
e.preventDefault();
let fileUrl = e.target.getAttribute('data-file');
let fileName = e.target.getAttribute('data-name_file');
let upload = e.target.getAttribute('data-upload');
let time = e.target.getAttribute('data-time');
let klasifikasiView = e.target.getAttribute('data-klasifikasi');
console.log(fileUrl);
let fileName = e.target.getAttribute('data-fileName') || '-';
let noDokumen = e.target.getAttribute('data-no_dokumen') || '-';
let tanggalTerbit = e.target.getAttribute('data-tanggal_terbit') || '-';
let klasifikasiView = e.target.getAttribute('data-klasifikasi') || '-';
let permissionFile = e.target.getAttribute('data-permission_file');
$("#confirm-upload-dokumen").html(upload);
$("#confirm-time-dokumen").html(time);
$("#confirm-upload-klasifikasi").html(klasifikasiView);
$("#confirm_preview_file").html(fileName);
$("#confirm-upload-dokumen").text(noDokumen);
$("#confirm-time-dokumen").text(tanggalTerbit);
$("#confirm-upload-klasifikasi").text(klasifikasiView);
$("#confirm_preview_file").text(fileName);
const permEl = document.getElementById('confirm-permission');
if (permEl) {
const publicDoc = isPublic(permissionFile);
permEl.textContent = publicDoc ? 'Bisa dilihat unit lain' : 'Hanya unit ini';
permEl.className = 'badge ' + (publicDoc ? 'bg-success' : 'bg-secondary');
}
currentFile = fileUrl;
idDirectory = e.target.getAttribute('data-id');
@ -203,6 +220,8 @@
})
.then(res => res.json())
.then(data => {
console.log(data);
if (data.success) {
Swal.fire({
text: 'File berhasil dihapus',
@ -218,13 +237,6 @@
li?.remove();
}
// Reset modal preview
document.getElementById('file-preview').innerHTML =
`<p>📂 Pilih file untuk melihat preview</p>`;
document.getElementById('confirm_preview_file').innerHTML = "";
document.getElementById('confirm-upload-dokumen').innerHTML = "";
document.getElementById('confirm-time-dokumen').innerHTML = "";
document.getElementById('confirm-upload-klasifikasi').innerHTML = "";
// Tutup modal otomatis setelah hapus
let modalEl = document.getElementById('previewModal');

View File

@ -9,25 +9,25 @@
</div>
<!-- Modal Form -->
<form id="formFile" action="/upload" enctype="multipart/form-data" method="POST" >
<form id="formFile" action="/uploadv2" enctype="multipart/form-data" method="POST" >
@csrf
<div class="modal-body">
<div class="container" id="container_create">
<div class="row mb-3">
<div class="col-md-6 mb-2">
<label>Unit</label>
<select class="form-control" name="data[0][id_unit_kerja]" id="select_id_unit_kerja_0" required>
<div class="container" style="max-height: 70vh; overflow-y:auto;">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label fw-semibold">Unit <span class="text-danger">*</span></label>
<select class="form-control unit_kerja" name="data[0][id_unit_kerja]" id="select_id_unit_kerja_0" required>
<option value="" disable>Select Choose</option>
</select>
</div>
<div class="col-md-6 mb-2">
<label>Sub Unit</label>
<div class="col-md-4">
<label class="form-label fw-semibold">Sub Unit <span class="text-danger">*</span></label>
<select class="form-control" name="data[0][id_sub_unit_kerja]" id="select_id_sub_unit_kerja_0" required>
<option value="" disable>Select Choose</option>
<option value="" disable selected>Select Choose</option>
</select>
</div>
<div class="col-md-6 mb-2">
<label>Kategori Dokumen</label>
<div class="col-md-4">
<label class="form-label fw-semibold">Kategori Dokumen <span class="text-danger">*</span></label>
<select class="form-control" name="data[0][master_kategori_directory_id]" id="select_kategori_0" required>
<option value="" disable>Select Choose</option>
@foreach ($katDok as $kat)
@ -35,28 +35,48 @@
@endforeach
</select>
</div>
<div class="col-md-6 mb-2">
<label>Klasifikasi File</label>
<select class="form-select" name="data[0][klasifikasi]" required>
<option value="" disable>Select Choose</option>
@foreach ($klasifikasiDok as $klasifikasi)
<option value="{{ $klasifikasi->master_klasifikasi_directory_id }}/{{ $klasifikasi->nama_klasifikasi_directory }}">
{{ $klasifikasi->nama_klasifikasi_directory }}
</option>
@endforeach
</select>
<div class="col-md-6">
<label class="form-label fw-semibold">Nomor Dokumen</label>
<div class="input-group">
<span class="input-group-text">#</span>
<input type="text" class="form-control" name="data[0][no_dokumen]" placeholder="Contoh: 001/RS/IT/I/2026">
</div>
</div>
<div class="col-md-3">
<label class="form-label fw-semibold">Tanggal Terbit</label>
<input class="form-control" type="date" name="data[0][date_active]" required>
</div>
<div class="col-md-3">
<label class="form-label fw-semibold">Boleh dilihat unit lain? <span class="text-danger">*</span></label>
<div class="border rounded-3 p-2 bg-light">
<div class="form-check">
<input class="form-check-input" type="radio" name="data[0][is_permission]" id="perm_yes" value="1" required>
<label class="form-check-label" for="perm_yes">
Iya
</label>
</div>
<div class="form-check mt-1">
<input class="form-check-input" type="radio" name="data[0][is_permission]" id="perm_no" value="2" required>
<label class="form-check-label" for="perm_no">
Tidak
</label>
</div>
</div>
</div>
<div class="col-md-12 mb-2">
<label for="fileUpload0" class="form-label fw-semibold">📂 Upload Dokumen</label>
<div class="file-drop-area border rounded-2 p-1 shadow-sm">
<input class="file-input" type="file" id="fileUpload0" accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.xls,.xlsx" multiple>
<label for="fileUpload0" class="form-label fw-semibold">📂 Upload Dokumen (PDF)</label>
<div class="border rounded-3 p-3 bg-white shadow-sm">
<input class="form-control file-input" type="file" id="fileUpload0" accept=".pdf" name="data[0][file]">
<div class="mt-2 text-success fw-semibold d-none file-name"></div>
</div>
<div class="form-text text-muted fw-semibold">
Form bersifat multiple dan format yang didukung: JPG, JPEG, PDF, PNG, PPT Excel dan Word
<div class="form-text text-muted">
Bisa upload lebih dari 1 file. Format yang didukung: <b>PDF</b>.
</div>
</div>
<div id="col_add_file"></div>
<div id="col_add_file" class="col-12"></div>
</div>
<button type="button" class="btn btn-outline-primary btn-sm mt-3" onclick="addForm()">
+ Tambah Form

View File

@ -0,0 +1,156 @@
@extends('layout.main')
@section('body_main')
<div class="row">
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-header bg-white border-0">
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2">
<div>
<h4 class="mb-0">Rekap Dokumen</h4>
<small class="text-muted">Ringkasan jumlah file per Unit dan Folder</small>
</div>
<div class="ms-md-auto d-flex gap-2 align-items-center">
<div class="input-group input-group-sm" style="max-width:320px;">
<span class="input-group-text bg-white border-end-0">
<i class="fa fa-search text-muted"></i>
</span>
<input type="search" id="recapSearch" class="form-control border-start-0" placeholder="Cari unit atau folder" oninput="debouncedRecapSearch(this.value)">
</div>
<select id="recapPerPage" class="form-select form-select-sm" style="width:auto;" onchange="changePerPage(this.value)">
<option value="5">5</option>
<option value="10" selected>10</option>
<option value="20">20</option>
<option value="50">50</option>
</select>
<button class="btn btn-outline-secondary btn-sm d-flex align-items-center gap-1" onclick="fetchRecap()">
<i class="fa fa-rotate"></i>
<span>Refresh</span>
</button>
</div>
</div>
</div>
<div class="card-body pb-2">
<div class="table-responsive" style="max-height: 55vh; overflow-y:auto;">
<table class="table table-sm table-hover align-middle">
<thead class="table-light shadow-sm">
<tr>
<th style="width:5%;" class="text-center">#</th>
<th style="width:30%;">Unit</th>
<th style="width:50%;">Folder</th>
<th style="width:15%;" class="text-center">Jumlah</th>
</tr>
</thead>
<tbody id="recapBody">
<tr>
<td colspan="4" class="text-center text-muted py-4">Memuat data...</td>
</tr>
</tbody>
</table>
</div>
<div class="d-flex flex-column flex-md-row align-items-center justify-content-between gap-2 mt-3" id="recapPagination"></div>
</div>
</div>
</div>
</div>
@endsection
<script>
document.addEventListener('DOMContentLoaded', () => fetchRecap());
let recapDebounce;
const recapState = { page:1, perPage:10, keyword:'', lastPage:1 };
function debouncedRecapSearch(val){
clearTimeout(recapDebounce);
recapDebounce = setTimeout(() => {
recapState.keyword = val;
recapState.page = 1;
fetchRecap();
}, 250);
}
function changePerPage(val){
recapState.perPage = parseInt(val) || 10;
recapState.page = 1;
fetchRecap();
}
function fetchRecap(){
const tbody = document.getElementById('recapBody');
const pager = document.getElementById('recapPagination');
tbody.innerHTML = `<tr><td colspan="4" class="text-center text-muted py-4">Memuat data...</td></tr>`;
if(pager) pager.innerHTML = '';
const params = new URLSearchParams({
page: recapState.page,
per_page: recapState.perPage,
keyword: recapState.keyword || ''
});
fetch('/data/recap?' + params.toString())
.then(res => res.json())
.then(json => {
const rows = json?.data || [];
recapState.lastPage = json?.pagination?.last_page || 1;
if(!rows.length){
tbody.innerHTML = `<tr><td colspan="4" class="text-center text-muted py-4">Tidak ada data</td></tr>`;
return;
}
const html = rows.map((row, idx) => {
const folderRows = (row.data || []).map((f, i) => `
<tr>
${i === 0 ? `<td rowspan="${row.data.length}" class="text-center align-middle fw-semibold">${idx+1}</td>` : ''}
${i === 0 ? `<td rowspan="${row.data.length}" class="fw-semibold">${row.unit || '-'}</td>` : ''}
<td>${f.folder || '-'}</td>
<td class="text-center fw-bold">${f.count || 0}</td>
</tr>
`).join('');
return folderRows;
}).join('');
tbody.innerHTML = html;
renderRecapPagination();
})
.catch(err => {
console.error(err);
tbody.innerHTML = `<tr><td colspan="4" class="text-center text-danger py-4">Gagal memuat data</td></tr>`;
});
}
function renderRecapPagination(){
const pager = document.getElementById('recapPagination');
if(!pager) return;
if(recapState.lastPage <= 1){
pager.innerHTML = '';
return;
}
const maxButtons = 5;
let start = Math.max(1, recapState.page - Math.floor(maxButtons/2));
let end = Math.min(recapState.lastPage, start + maxButtons - 1);
start = Math.max(1, end - maxButtons + 1);
let buttons = '';
buttons += `<button class="btn btn-outline-secondary btn-sm" data-page="prev" ${recapState.page === 1 ? 'disabled' : ''}></button>`;
for(let i=start; i<=end; i++){
buttons += `<button class="btn btn-sm ${i === recapState.page ? 'btn-primary' : 'btn-outline-secondary'}" data-page="${i}">${i}</button>`;
}
buttons += `<button class="btn btn-outline-secondary btn-sm" data-page="next" ${recapState.page === recapState.lastPage ? 'disabled' : ''}></button>`;
pager.innerHTML = `
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="btn-group" role="group">${buttons}</div>
<span class="small text-muted">Halaman ${recapState.page} dari ${recapState.lastPage}</span>
</div>
`;
pager.querySelectorAll('button[data-page]').forEach(btn => {
btn.addEventListener('click', () => {
const page = btn.getAttribute('data-page');
if(page === 'prev' && recapState.page > 1) recapState.page--;
else if(page === 'next' && recapState.page < recapState.lastPage) recapState.page++;
else if(!isNaN(parseInt(page))) recapState.page = parseInt(page);
fetchRecap();
});
});
}
</script>

View File

@ -16,7 +16,7 @@
<input type="search"
id="tableSearch"
class="form-control border-start-0"
placeholder="Cari nama file atau folder"
placeholder="Cari nama file, No Dokumen atau folder"
oninput="debouncedTableSearch(this.value)">
</div>
<div class="d-flex align-items-center gap-2">
@ -61,7 +61,7 @@
const mappingUnitKerjaPegawai = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]);
const formCreate = $("#formFile")
const modalCreate = document.getElementById('modalCreateFile')
const tableState = { data: [], filtered: [], page: 1, pageSize: 8, search: '' };
const tableState = { data: [], page: 1, pageSize: 8, search: '', lastPage: 1, total: 0 };
const tbody = document.getElementById('tableFolderLastUpdated');
const paginationEl = document.getElementById('paginationControls');
const summaryEl = document.getElementById('tableSummary');
@ -75,7 +75,7 @@
if(!isNaN(val) && val > 0){
tableState.pageSize = val;
tableState.page = 1;
renderTable();
fetchData();
}
});
}
@ -159,29 +159,16 @@
if(!page) return;
if(page === 'prev' && tableState.page > 1) tableState.page--;
else if(page === 'next'){
const totalPages = Math.max(1, Math.ceil(tableState.filtered.length / tableState.pageSize));
if(tableState.page < totalPages) tableState.page++;
if(tableState.page < tableState.lastPage) tableState.page++;
}else{
tableState.page = parseInt(page);
}
renderTable();
fetchData();
});
}
function renderTable(){
const term = tableState.search.toLowerCase();
tableState.filtered = tableState.data.filter(item => {
const fileName = (item.file || '').split('/').pop().toLowerCase();
const folderPath = (item.file || '').toLowerCase();
return fileName.includes(term) || folderPath.includes(term);
});
const total = tableState.filtered.length;
const totalPages = Math.max(1, Math.ceil(total / tableState.pageSize));
if(tableState.page > totalPages) tableState.page = totalPages;
const startIdx = (tableState.page - 1) * tableState.pageSize;
const pageData = tableState.filtered.slice(startIdx, startIdx + tableState.pageSize);
const pageData = tableState.data || [];
if(pageData.length === 0){
tbody.innerHTML = `
@ -195,13 +182,13 @@
tbody.innerHTML = pageData.map(buildRow).join('');
}
const from = total === 0 ? 0 : startIdx + 1;
const to = Math.min(startIdx + pageData.length, total);
const from = tableState.total === 0 ? 0 : ((tableState.page -1) * tableState.pageSize) + 1;
const to = Math.min(((tableState.page -1) * tableState.pageSize) + pageData.length, tableState.total);
if(summaryEl){
summaryEl.textContent = total ? `Menampilkan ${from} - ${to} dari ${total} dokumen` : 'Tidak ada data';
summaryEl.textContent = tableState.total ? `Menampilkan ${from} - ${to} dari ${tableState.total} dokumen` : 'Tidak ada data';
}
renderPagination(totalPages);
renderPagination(tableState.lastPage || 1);
}
function debouncedTableSearch(value){
@ -209,17 +196,23 @@
window.tableSearchTimer = setTimeout(() => {
tableState.search = value.trim();
tableState.page = 1;
renderTable();
fetchData();
}, 250);
}
function fetchData(){
if(summaryEl) summaryEl.textContent = 'Memuat data...';
fetch(`/last-document`)
const params = new URLSearchParams({
page: tableState.page,
per_page: tableState.pageSize,
keyword: tableState.search
});
fetch(`/last-document?${params.toString()}`)
.then(response => response.json())
.then(data => {
tableState.data = data?.data || [];
tableState.page = 1;
tableState.lastPage = data?.pagination?.last_page || 1;
tableState.total = data?.pagination?.total || 0;
renderTable();
})
.catch(error => {

View File

@ -66,6 +66,29 @@
</a>
</li>
<li class="sidebar-item">
<a class="sidebar-link justify-content-between"
href="/log-activity" aria-expanded="false">
<div class="d-flex align-items-center gap-3">
<span class="d-flex">
<i class="ti ti-layout-grid"></i>
</span>
<span class="hide-menu">Log Aktivity</span>
</div>
</a>
</li>
<li class="sidebar-item">
<a class="sidebar-link justify-content-between"
href="/recap" aria-expanded="false">
<div class="d-flex align-items-center gap-3">
<span class="d-flex">
<i class="ti ti-layout-grid"></i>
</span>
<span class="hide-menu">Data Rekap</span>
</div>
</a>
</li>
</ul>
</nav>
<!-- End Sidebar navigation -->

View File

@ -0,0 +1,231 @@
@extends('layout.main')
@section('body_main')
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header d-flex align-items-center justify-content-between">
<h4 class="mb-0">Log Activity</h4>
</div>
<div class="card-body p-3">
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3 flex-wrap">
<div class="input-group input-group-sm flex-grow-1" style="max-width:320px;">
<span class="input-group-text bg-white border-end-0">
<i class="fa fa-search text-muted"></i>
</span>
<input type="search"
id="tableSearch"
class="form-control border-start-0"
placeholder="Cari nama file / nomor dokumen / aksi"
oninput="debouncedTableSearch(this.value)">
</div>
<div class="d-flex align-items-center gap-2">
<label class="small mb-0 text-muted">Mulai</label>
<input type="date" id="startDate" class="form-control form-control-sm" onchange="applyDateFilter()">
</div>
<div class="d-flex align-items-center gap-2">
<label class="small mb-0 text-muted">Selesai</label>
<input type="date" id="endDate" class="form-control form-control-sm" onchange="applyDateFilter()">
</div>
<div class="d-flex align-items-center gap-2">
<select id="tablePageSize" class="form-select form-select-sm" style="width: auto;">
<option value="5">5</option>
<option value="10"selected>10</option>
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
<button class="btn btn-outline-secondary btn-sm" onclick="refreshLog()">
<i class="fa fa-rotate"></i> Refresh
</button>
<div class="small text-muted ms-md-auto" id="tableSummary"></div>
</div>
<div class="table-responsive" style="max-height: 55vh; overflow-y:auto;">
<table class="table table-sm table-hover align-middle mb-0" id="lastUpdatedTable">
<thead>
<tr>
<th>Nama</th>
<th>Unit</th>
<th>Aktivitas</th>
<th>No Dokumen</th>
<th>Tanggal</th>
</tr>
</thead>
<tbody id="tableFolderLastUpdated">
<!-- data dari fetch masuk sini -->
</tbody>
</table>
</div>
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2 mt-3" id="paginationControls"></div>
</div>
</div>
</div>
</div>
@endsection
<script>
document.addEventListener('DOMContentLoaded', () => {
const tableState = { data: [], page: 1, pageSize: 10, search: '', lastPage: 1, total: 0, startDate: '', endDate: '' };
const tbody = document.getElementById('tableFolderLastUpdated');
const paginationEl = document.getElementById('paginationControls');
const summaryEl = document.getElementById('tableSummary');
const pageSizeSelect = document.getElementById('tablePageSize');
const startDateInput = document.getElementById('startDate');
const endDateInput = document.getElementById('endDate');
if(pageSizeSelect){
const initialSize = parseInt(pageSizeSelect.value);
if(!isNaN(initialSize)) tableState.pageSize = initialSize;
pageSizeSelect.addEventListener('change', (e) => {
const val = parseInt(e.target.value);
if(!isNaN(val) && val > 0){
tableState.pageSize = val;
tableState.page = 1;
fetchData();
}
});
}
window.applyDateFilter = function(){
tableState.startDate = startDateInput.value || '';
tableState.endDate = endDateInput.value || '';
tableState.page = 1;
fetchData();
}
function buildRow(item){
let unitKerja = item.file ? item.file.split('/')[0] : '-';
let tanggal = item.entry_at ? formatTanggal(item.entry_at) : '-'
return `
<tr>
<td>${item.pegawai_nama_entry || '-'}</td>
<td>${unitKerja}</td>
<td>${item.action_type || '-'}</td>
<td>${item.no_dokumen || '-'}</td>
<td class="text-nowrap">${tanggal}</td>
</tr>
`;
}
function formatTanggal(dateString) {
const d = new Date(dateString);
return d.toLocaleDateString('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
function renderPagination(totalPages){
if(!paginationEl) return;
if(totalPages <= 1){
paginationEl.innerHTML = '';
return;
}
const maxButtons = 5;
let start = Math.max(1, tableState.page - Math.floor(maxButtons/2));
let end = Math.min(totalPages, start + maxButtons - 1);
start = Math.max(1, end - maxButtons + 1);
let buttons = '';
buttons += `<button class="btn btn-outline-secondary btn-sm" data-page="prev" ${tableState.page === 1 ? 'disabled' : ''}></button>`;
for(let i=start; i<=end; i++){
buttons += `<button class="btn btn-sm ${i === tableState.page ? 'btn-primary' : 'btn-outline-secondary'}" data-page="${i}">${i}</button>`;
}
buttons += `<button class="btn btn-outline-secondary btn-sm" data-page="next" ${tableState.page === totalPages ? 'disabled' : ''}></button>`;
paginationEl.innerHTML = `
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="btn-group" role="group">${buttons}</div>
<span class="small text-muted">Halaman ${tableState.page} dari ${totalPages}</span>
</div>
`;
}
if(paginationEl){
paginationEl.addEventListener('click', (e) => {
const page = e.target.getAttribute('data-page');
if(!page) return;
if(page === 'prev' && tableState.page > 1) tableState.page--;
else if(page === 'next'){
if(tableState.page < tableState.lastPage) tableState.page++;
}else{
tableState.page = parseInt(page);
}
fetchData();
});
}
function renderTable(){
const pageData = tableState.data || [];
if(pageData.length === 0){
tbody.innerHTML = `
<tr>
<td colspan="5" class="text-center text-muted py-4">
Tidak ada data
</td>
</tr>
`;
}else{
tbody.innerHTML = pageData.map(buildRow).join('');
}
const from = tableState.total === 0 ? 0 : ((tableState.page -1) * tableState.pageSize) + 1;
const to = Math.min(((tableState.page -1) * tableState.pageSize) + pageData.length, tableState.total);
if(summaryEl){
summaryEl.textContent = tableState.total ? `Menampilkan ${from} - ${to} dari ${tableState.total} aktivitas` : 'Tidak ada data';
}
renderPagination(tableState.lastPage || 1);
}
let searchDebounce;
window.debouncedTableSearch = function(value){
clearTimeout(searchDebounce);
searchDebounce = setTimeout(() => {
tableState.search = value.trim();
tableState.page = 1;
fetchData();
}, 250);
}
window.refreshLog = function(){
tableState.search = '';
tableState.startDate = '';
tableState.endDate = '';
document.getElementById('tableSearch').value = '';
startDateInput.value = '';
endDateInput.value = '';
tableState.page = 1;
fetchData();
}
function fetchData(){
if(summaryEl) summaryEl.textContent = 'Memuat data...';
const params = new URLSearchParams({
page: tableState.page,
per_page: tableState.pageSize,
keyword: tableState.search || '',
start_date: tableState.startDate || '',
end_date: tableState.endDate || ''
});
fetch(`/datatable/log-activity?${params.toString()}`)
.then(res => res.json())
.then(data => {
tableState.data = data?.data || [];
tableState.lastPage = data?.pagination?.last_page || 1;
tableState.total = data?.pagination?.total || 0;
renderTable();
})
.catch(err => {
console.error(err);
if(summaryEl) summaryEl.textContent = 'Gagal memuat data';
});
}
fetchData();
});
</script>

View File

@ -5,6 +5,7 @@ use App\Http\Controllers\AuthController;
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\MasterKategoriController;
use App\Http\Controllers\MasterKlasifikasiController;
use App\Http\Controllers\LogActivityController;
use Illuminate\Support\Facades\Route;
Route::middleware(['auth'])->group(function(){
@ -33,6 +34,12 @@ Route::middleware(['auth'])->group(function(){
Route::post('/download-multiple', [DashboardController::class, 'downloadDataMultiple']);
Route::post('/download-byfolder', [DashboardController::class, 'downloadDataFolder']);
Route::get('/log-activity', [LogActivityController::class, 'index']);
Route::get('/datatable/log-activity', [LogActivityController::class, 'datatable']);
Route::get('/recap', [DashboardController::class, 'recapView']);
Route::get('/data/recap', [DashboardController::class, 'recapData']);
});
Route::get('/login', [AuthController::class, 'index'])->name('login');