done data unit & data umum

This commit is contained in:
JokoPrasetio 2026-01-28 16:38:53 +07:00
parent 8c1663f9c3
commit 313cf64724
13 changed files with 1559 additions and 277 deletions

View File

@ -20,29 +20,90 @@ use ZipArchive;
use function PHPUnit\Framework\isEmpty;
class PdfFpdiWithAlpha extends Fpdi
{
protected array $extgstates = [];
public function SetAlpha(float $alpha, string $blendMode = 'Normal'): void
{
$alpha = max(0.0, min(1.0, $alpha));
$gs = $this->AddExtGState([
'ca' => $alpha,
'CA' => $alpha,
'BM' => '/' . $blendMode
]);
$this->SetExtGState($gs);
}
protected function AddExtGState(array $parms): int
{
$n = count($this->extgstates) + 1;
$this->extgstates[$n]['parms'] = $parms;
return $n;
}
protected function SetExtGState(int $gs): void
{
$this->_out(sprintf('/GS%d gs', $gs));
}
protected function _putextgstates(): void
{
foreach ($this->extgstates as $i => $extgstate) {
$this->_newobj();
$this->extgstates[$i]['n'] = $this->n;
$this->_put('<</Type /ExtGState');
foreach ($extgstate['parms'] as $k => $v) {
$this->_put('/' . $k . ' ' . $v);
}
$this->_put('>>');
$this->_put('endobj');
}
}
protected function _putresourcedict(): void
{
parent::_putresourcedict();
if (empty($this->extgstates)) {
return;
}
$this->_put('/ExtGState <<');
foreach ($this->extgstates as $k => $extgstate) {
$this->_put('/GS' . $k . ' ' . $extgstate['n'] . ' 0 R');
}
$this->_put('>>');
}
protected function _putresources(): void
{
$this->_putextgstates();
parent::_putresources();
}
}
class DashboardController extends Controller
{
public function index(){
$katDok = MasterKategori::where('statusenabled', true)->select('master_kategori_directory_id', 'nama_kategori_directory')->get();
$klasifikasiDok = MasterKlasifikasi::where('statusenabled', true)->select('master_klasifikasi_directory_id', 'nama_klasifikasi_directory')->get();
$prefillFilter = session()->pull('dashboard_prefill');
// public function index(){
// $katDok = MasterKategori::where('statusenabled', true)->select('master_kategori_directory_id', 'nama_kategori_directory')->get();
// $klasifikasiDok = MasterKlasifikasi::where('statusenabled', true)->select('master_klasifikasi_directory_id', 'nama_klasifikasi_directory')->get();
// $prefillFilter = session()->pull('dashboard_prefill');
$authMapping = auth()->user()?->dataUser?->mappingUnitKerjaPegawai[0];
$authUnitKerja = $authMapping->objectunitkerjapegawaifk;
$authSubUnitKerja = $authMapping->objectsubunitkerjapegawaifk;
// $authMapping = auth()->user()?->dataUser?->mappingUnitKerjaPegawai[0];
// $authUnitKerja = $authMapping->objectunitkerjapegawaifk;
// $authSubUnitKerja = $authMapping->objectsubunitkerjapegawaifk;
$allAkses = AksesFile::where(['statusenabled' => true, 'pegawai_id' => auth()->user()?->dataUser->id])->first();
$payload = [
'title' => 'Dashboard',
'katDok' => $katDok,
'klasifikasiDok' => $klasifikasiDok,
'authUnitKerja' => $authUnitKerja,
'authSubUnitKerja' => $authSubUnitKerja,
'allAkses' => $allAkses ?? null,
'prefillFilter' => $prefillFilter
];
return view('dashboard.index', $payload);
}
// $allAkses = AksesFile::where(['statusenabled' => true, 'pegawai_id' => auth()->user()?->dataUser->id])->first();
// $payload = [
// 'title' => 'Dashboard',
// 'katDok' => $katDok,
// 'klasifikasiDok' => $klasifikasiDok,
// 'authUnitKerja' => $authUnitKerja,
// 'authSubUnitKerja' => $authSubUnitKerja,
// 'allAkses' => $allAkses ?? null,
// 'prefillFilter' => $prefillFilter
// ];
// return view('dashboard.index', $payload);
// }
public function setDashboardPrefill(Request $request)
{
@ -63,6 +124,55 @@ class DashboardController extends Controller
]);
}
public function index(){
$katDok = MasterKategori::where('statusenabled', true)->select('master_kategori_directory_id', 'nama_kategori_directory')->get();
$authMapping = auth()->user()?->dataUser?->mappingUnitKerjaPegawai[0];
$authUnitKerja = $authMapping->objectunitkerjapegawaifk;
$authSubUnitKerja = $authMapping->objectsubunitkerjapegawaifk;
$data = [
'title' => 'Dashboard',
'katDok' => $katDok,
'authUnitKerja' => $authUnitKerja,
'authSubUnitKerja' => $authSubUnitKerja,
];
return view('dataUnit.index', $data);
}
public function dataUnitInternal(){
$perPage = (int) request('per_page', 10);
$authUnitId = auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]?->objectunitkerjapegawaifk;
$keyword = request('keyword');
$query = FileDirectory::where('statusenabled', true)
->where(function($subQuery){
$subQuery->where('status_action', '!=', 'rejected')->whereNotNull('status_action');
})
->where('id_unit_kerja', $authUnitId)->where('permission_file', false)
->when($keyword, function ($q) use ($keyword) {
$q->where(function ($sub) use ($keyword) {
$sub->where('file', 'ILIKE', "%{$keyword}%")
->orWhere('no_dokumen', 'ILIKE', "%{$keyword}%");
});
});
$data = $query->orderBy('entry_at', 'desc')
->paginate($perPage);
$payload = [
'status' => true,
'message' => 'Berhasil mendapatkan data',
'data' => $data->items(),
'pagination' => [
'current_page' => $data->currentPage(),
'next_page' => $data->hasMorePages() ? $data->currentPage() + 1 : null,
'has_more' => $data->hasMorePages(),
'last_page' => $data->lastPage(),
'per_page' => $data->perPage(),
'total' => $data->total(),
]
];
return response()->json($payload);
}
public function dataUnitKerja(){
$user = auth()->user()?->dataUser;
$entryPegawaiId = auth()->user()?->objectpegawaifk;
@ -459,7 +569,7 @@ class DashboardController extends Controller
}
public function dashboardVersion2(){
public function dataUmum(){
$katDok = MasterKategori::where('statusenabled', true)->select('master_kategori_directory_id', 'nama_kategori_directory')->get();
$authMapping = auth()->user()?->dataUser?->mappingUnitKerjaPegawai[0];
$authUnitKerja = $authMapping->objectunitkerjapegawaifk;
@ -470,7 +580,7 @@ class DashboardController extends Controller
'authUnitKerja' => $authUnitKerja,
'authSubUnitKerja' => $authSubUnitKerja,
];
return view('dashboardV2.index', $data);
return view('dataUmum.index', $data);
}
public function dataDocumentLast(){
@ -643,8 +753,9 @@ class DashboardController extends Controller
private function watermarkCenterAndStream(string $pdfPath, string $stampFile)
{
$pdf = new Fpdi();
$pdf = new PdfFpdiWithAlpha();
$pageCount = $pdf->setSourceFile($pdfPath);
$opacity = $this->getWatermarkOpacity();
for ($pageNo = 1; $pageNo <= $pageCount; $pageNo++) {
$tplId = $pdf->importPage($pageNo);
@ -660,7 +771,9 @@ class DashboardController extends Controller
$x = ($size['width'] - $stampW) / 2;
$y = ($size['height'] - $stampH) / 2;
$pdf->SetAlpha($opacity);
$pdf->Image($stampFile, $x, $y, $stampW, $stampH);
$pdf->SetAlpha(1);
}
$output = $pdf->Output('S');
@ -706,8 +819,9 @@ class DashboardController extends Controller
private function watermarkCenterToFile(string $pdfPath, string $stampFile, string $outputPath): void
{
$pdf = new Fpdi();
$pdf = new PdfFpdiWithAlpha();
$pageCount = $pdf->setSourceFile($pdfPath);
$opacity = $this->getWatermarkOpacity();
for ($pageNo = 1; $pageNo <= $pageCount; $pageNo++) {
$tplId = $pdf->importPage($pageNo);
@ -722,12 +836,19 @@ class DashboardController extends Controller
$x = ($size['width'] - $stampW) / 2;
$y = ($size['height'] - $stampH) / 2;
$pdf->SetAlpha($opacity);
$pdf->Image($stampFile, $x, $y, $stampW, $stampH);
$pdf->SetAlpha(1);
}
$pdf->Output($outputPath, 'F');
}
private function getWatermarkOpacity(): float
{
return 0.2;
}
private function cleanupTempFiles(array $tempFiles): void
{
foreach ($tempFiles as $file) {

View File

@ -280,7 +280,9 @@
});
}
if(e.target.matches('#btn-view-full')){
window.open(`/file-preview/${idDirectory}`, '_blank');
}
});
</script>

View File

@ -13,9 +13,9 @@
<!-- Body -->
<div class="modal-body p-2" style="min-height:250px; max-height:70vh; overflow:auto;">
<div class="d-flex justify-content-end align-items-center sticky-top bg-white z-10">
{{-- <div class="d-flex justify-content-end align-items-center sticky-top bg-white z-10">
<button type="button" class="btn btn-sm btn-outline-danger mb-2 me-2" id="delete-file">🗑 Hapus</button>
</div>
</div> --}}
<div id="file-preview"
class="text-center text-muted d-flex justify-content-center align-items-center"
style="height:100%;">

View File

@ -9,150 +9,12 @@
<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:30%;">Sub Unit</th>
<th style="width:20%;">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>
@include('dataUnit.section.recap', ['showRecapTitle' => false])
</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.subUnit || '-'}</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

@ -0,0 +1,869 @@
@extends('layout.main')
<style>
.file-cell {
max-width: 420px;
}
.file-wrapper {
display: flex;
flex-direction: column;
gap: 2px;
}
.file-title {
display: flex;
align-items: center;
gap: 6px;
font-weight: 600;
color: #1f2937;
}
.file-title i {
font-size: 18px;
color: #6366f1;
}
.file-link {
color: #1f2937;
text-decoration: none;
line-height: 1.3;
}
.file-link:hover {
color: #4f46e5;
text-decoration: underline;
}
.file-meta {
font-size: 12px;
color: #6b7280;
}
.file-meta span {
margin-right: 4px;
}
/* status pending */
.file-pending {
font-style: italic;
color: #9ca3af !important;
}
.dataunit-tabs .nav-link {
border: 0;
color: #475569;
font-weight: 600;
padding: 0.6rem 1rem;
border-radius: 999px;
transition: background-color .15s ease, color .15s ease, box-shadow .15s ease;
position: relative;
overflow: hidden;
}
.dataunit-tabs .nav-link.active {
background: color-mix(in srgb, var(--bs-primary) 12%, #ffffff);
color: var(--bs-primary);
+}
.dataunit-tabs .nav-link:hover {
color: var(--bs-primary);
background: color-mix(in srgb, var(--bs-primary) 8%, #ffffff);
}
.dataunit-tabs .nav-link::after {
content: "";
position: absolute;
left: 18px;
right: 18px;
bottom: 6px;
height: 2px;
border-radius: 999px;
background: var(--bs-primary);
transform: scaleX(0);
transform-origin: center;
transition: transform .18s ease;
opacity: 0;
}
.dataunit-tabs .nav-link:hover::after {
transform: scaleX(1);
opacity: 0.7;
}
.dataunit-tab-wrap {
background: #f8fafc;
border-radius: 999px;
padding: 6px;
display: flex;
gap: 6px;
width: 100%;
}
.dataunit-tabs {
width: 100%;
}
.dataunit-tabs .nav-item {
flex: 1 1 0;
}
.dataunit-tabs .nav-link {
width: 100%;
justify-content: center;
}
</style>
@section('body_main')
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-body p-3">
<div class="tab-content">
<div class="tab-pane fade show active">
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3">
<h4 class="mb-0">Data Unit</h4>
<button type="button" class="btn btn-success ms-md-auto" data-bs-target="#modalCreateFile" data-bs-toggle="modal">Tambah File</button>
</div>
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3">
<div class="input-group input-group-sm flex-grow-1">
<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, No Dokumen atau folder"
oninput="debouncedTableSearch(this.value)">
</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>
<div class="small text-muted ms-md-auto" id="tableSummary">Memuat data...</div>
</div>
<div class="table-responsive" style="max-height: 70vh; overflow-y:auto;">
<table class="table table-sm table-hover align-middle mb-0 table-fixed" id="lastUpdatedTable">
<thead>
<tr>
<th>Nomor Surat</th>
<th>File</th>
<th>Kategori</th>
<th>Unit</th>
<th>Sub Unit </th>
<th>Tanggal Upload</th>
</tr>
</thead>
<tbody id="tableDataUmum">
<!-- 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>
</div>
</div>
@include('dataUmum.modal.create')
@include('dataUmum.modal.view')
<script>
const katDok = @json($katDok);
const authUnitKerja = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]?->unitKerja);
const authSubUnitKerja = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]->sub_unit_kerja);
const mappingUnitKerjaPegawai = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]);
const formCreate = $("#formFile")
const modalCreate = document.getElementById('modalCreateFile')
const tableState = { data: [], page: 1, pageSize: 8, search: '', lastPage: 1, total: 0 };
const tbody = document.getElementById('tableDataUmum');
const paginationEl = document.getElementById('paginationControls');
const summaryEl = document.getElementById('tableSummary');
const pageSizeSelect = document.getElementById('tablePageSize');
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();
}
});
}
function resetCreateForm(){
colCount = 1;
$("#col_add_fileV2").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('');
}
function isPublic(permissionVal){
if(permissionVal === null || permissionVal === undefined) return false;
const val = String(permissionVal).toLowerCase();
return val === '1' || val === 'true' || val === 'iya' || val === 'yes';
}
function buildRow(item){
const parts = (item.file || '').split('/');
const fileName = parts.pop() || '-';
const folderPath = parts.join('/') || '-';
const publicDoc = isPublic(item.permission_file);
const unitName = parts[0] || '';
const subName = parts[1] || '';
const kategoriName = parts[2] || '';
let statusLabel = '';
let statusClass = '';
let badge
if (publicDoc) {
statusLabel = 'Umum';
statusClass = 'bg-success';
} else {
statusLabel = 'Internal Unit';
statusClass = 'bg-secondary';
}
return `
<tr>
<td class="text-nowrap">${item.no_dokumen || '-'}</td>
<td class="file-cell">
<div style="display:flex; flex-direction:column; gap:4px;">
<!-- BARIS ATAS -->
<div style="display:flex; align-items:center; gap:6px;">
<i class="ti ti-file-text" style="font-size:18px; color:#4f46e5;"></i>
<a href="#"
class="file-link ${statusLabel === "Pending" ? 'file-pending' : ''}"
data-file="${item.file}"
data-fileName="${fileName}"
data-id="${item.file_directory_id}"
data-no_dokumen="${item.no_dokumen || '-'}"
data-tanggal_terbit="${item.tanggal_terbit || '-'}"
data-permission_file="${item.permission_file || '-'}"
style="
color:#1f2937;
font-weight:600;
text-decoration:none;
line-height:1.3;
word-break:break-word;
"
>
${fileName}
</a>
</div>
<!-- BARIS BAWAH (LABEL) -->
<div style="padding-left:24px;">
${
item.permission_file
? `
<span style="
display:inline-block;
padding:2px 10px;
font-size:11px;
border-radius:6px;
background:#ecfdf5;
color:#047857;
border:1px solid #a7f3d0;
font-weight:500;
cursor:default;
">
Umum
</span>
`
: `
<span style="
display:inline-block;
padding:2px 10px;
font-size:11px;
border-radius:6px;
background:#eff6ff;
color:#1d4ed8;
border:1px solid #bfdbfe;
font-weight:500;
cursor:default;
">
Internal Unit
</span>
`
}
</div>
</div>
</td>
<td>
${kategoriName}
</td>
<td>
${unitName}
</td>
<td>
${subName}
</td>
<td class="text-nowrap">${formatTanggal(item.entry_at)}</td>
</tr>
`;
}
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' : ''}> Sebelumnya</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' : ''}>Berikutnya </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 yang cocok
</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} dokumen` : 'Tidak ada data';
}
renderPagination(tableState.lastPage || 1);
}
function debouncedTableSearch(value){
clearTimeout(window.tableSearchTimer);
window.tableSearchTimer = setTimeout(() => {
tableState.search = value.trim();
tableState.page = 1;
fetchData();
}, 250);
}
function fetchData(){
if(summaryEl) summaryEl.textContent = 'Memuat data...';
const params = new URLSearchParams({
page: tableState.page,
per_page: tableState.pageSize,
keyword: tableState.search
});
fetch(`/datatable-umum?${params.toString()}`)
.then(response => response.json())
.then(data => {
tableState.data = data?.data || [];
tableState.lastPage = data?.pagination?.last_page || 1;
tableState.total = data?.pagination?.total || 0;
renderTable();
})
.catch(error => {
console.error('Error : ', error);
if(summaryEl) summaryEl.textContent = 'Gagal memuat data';
})
}
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'
});
}
fetchData()
document.addEventListener('click', function (e) {
const btn = e.target.closest('.folder-prefill');
if (!btn) return;
e.preventDefault();
const payload = {
unitId: btn.getAttribute('data-unit_id') || '',
subId: btn.getAttribute('data-sub_id') || '',
kategoriId: btn.getAttribute('data-kategori_id') || '',
unitName: btn.getAttribute('data-unit_name') || '',
subName: btn.getAttribute('data-sub_name') || '',
kategoriName: btn.getAttribute('data-kategori_name') || '',
};
fetch('/dashboard/prefill', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(payload)
})
.then(res => {
if (!res.ok) throw new Error('Gagal set filter');
window.location.href = '/';
})
.catch(() => {
Swal.fire({ icon: 'error', title: 'Gagal', text: 'Tidak bisa buka folder' });
});
});
let colCount = 1;
$(document).ready(function() {
$('.unit_kerja').select2({
placeholder: '--- Pilih Unit Kerja ---',
allowClear: true,
width: '100%',
ajax: {
url : '/select-unit-kerja',
dataType: 'json',
delay: 250,
data: function(params){
let q = '';
if(allAkses){
q = params.term;
}else{
q = authUnitKerja?.name ?? '';
}
return { q };
},
processResults: function(data){
let results = data?.data.map(item => ({
id: item.id,
text: item.name
}));
return { results };
},
cache: true,
},
minimumInputLength: 0,
});
$('.sub_unit_kerja').select2({
placeholder: '-- Pilih Sub Unit Kerja --',
allowClear: true,
width: '100%',
});
// // --- isi default unit kerja ---
// if(authUnitKerja){
// let option = new Option(authUnitKerja.name, authUnitKerja.id, true, true);
// $('.unit_kerja').append(option).trigger('change');
// }
let initialUnit = $('.unit_kerja').val();
if(initialUnit){
loadSubUnitKerja(initialUnit);
}
// jalankan setiap kali unit_kerja berubah
$('.unit_kerja').on('change', function(){
let idUnit = $(this).val();
if(idUnit){
loadSubUnitKerja(idUnit);
}
});
selectOptionUnitKerjaV1(0);
});
function loadSubUnitKerja(unitId){
$('.sub_unit_kerja').empty().append('<option value="" disabled>-- Pilih Sub Unit Kerja --</option>');
$.ajax({
url: `/select-sub-unit-kerja/${unitId}`,
method: 'GET',
success: function(response) {
if (response?.data) {
response.data.forEach(unit => {
let selected = (authSubUnitKerja && unit.id === authSubUnitKerja.objectsubunitkerjapegawaifk);
const option = new Option(unit.name, unit.id, false, selected);
$('.sub_unit_kerja').append(option);
});
$('.sub_unit_kerja').trigger('change');
}
}
});
}
function addFormV2(){
let col = $("#col_add_fileV2");
let html = '';
html += `
<div class="row g-3 align-items-start" id="col-${colCount}">
<hr class="my-3" />
<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-select"
name="data[${colCount}][id_unit_kerja]"
id="select_id_unit_kerja_${colCount}"
required>
<option value="" disabled selected>Pilih Unit</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Sub Unit <span class="text-danger">*</span></label>
<select class="form-select"
name="data[${colCount}][id_sub_unit_kerja]"
id="select_id_sub_unit_kerja_${colCount}"
required>
<option value="" disabled selected>Pilih Sub Unit</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Kategori Dokumen <span class="text-danger">*</span></label>
<select class="form-select"
name="data[${colCount}][master_kategori_directory_id]"
id="select_kategori_${colCount}"
required>
<option value="" disabled selected>Pilih Kategori</option>
@foreach ($katDok as $kat)
<option value="{{ $kat->master_kategori_directory_id }}/{{ $kat->nama_kategori_directory }}">
{{ $kat->nama_kategori_directory }}
</option>
@endforeach
</select>
</div>
<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="form-text text-muted">Opsional, isi jika ada nomor resmi dokumen.</div>
</div>
<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"
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)
selectOptionUnitKerjaV1(colCount)
colCount++;
}
function removeCol(count){
$(`#col-${count}`).remove()
}
function selectOptionUnitKerjaV1(colCount) {
let selectUnit = $(`#select_id_unit_kerja_${colCount}`);
let selectSubUnit = $(`#select_id_sub_unit_kerja_${colCount}`);
// inisialisasi select2 untuk Unit Kerja
selectUnit.select2({
placeholder: '-- Pilih Unit Kerja --',
allowClear:true,
width: '100%',
dropdownParent: selectUnit.parent(),
ajax:{
url : '/select-unit-kerja',
dataType: 'json',
delay: 250,
data: function(params){
return { q: params.term }
},
processResults: function(data){
return {
results : data?.data.map(item => ({
id: item.id+'/'+item.name,
text: item.name,
sub_units: item.sub_unit_kerja // kirim ke front
}))
}
},
cache: true,
},
minimumInputLength: 1,
});
selectSubUnit.select2({
placeholder: '-- Pilih Sub Unit Kerja --',
allowClear: true,
width: '100%',
dropdownParent: selectSubUnit.parent()
});
// event ketika unit kerja dipilih
selectUnit.on('select2:select', function (e) {
let data = e.params.data; // data unit kerja terpilih
selectSubUnit.empty().append('<option value="" disabled selected>-- Pilih Sub Unit Kerja --</option>');
if (data.sub_units && data.sub_units.length > 0) {
data.sub_units.forEach(sub => {
selectSubUnit.append(`<option value="${sub.id}/${sub.name}">${sub.name}</option>`);
});
}
// aktifkan select2 untuk sub unit
});
}
let currentFile = null;
let idDirectory = null;
document.addEventListener('click', function(e){
if(e.target.matches('.file-link')){
e.preventDefault();
let fileUrl = e.target.getAttribute('data-file');
let noDokumen = e.target.getAttribute('data-no_dokumen')
let tanggalTerbit = e.target.getAttribute('data-tanggal_terbit')
let permissionFile = e.target.getAttribute('data-permission_file')
let fileName = e.target.getAttribute('data-fileName')
currentFile = fileUrl;
idDirectory = e.target.getAttribute('data-id');
const titleEl = document.getElementById('confirm_preview_file');
if (titleEl) titleEl.textContent = fileName;
// set footer info
const noEl = document.getElementById('confirm-upload-dokumen');
if (noEl) noEl.textContent = noDokumen;
const tglEl = document.getElementById('confirm-time-dokumen');
if (tglEl) tglEl.textContent = tanggalTerbit;
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');
}
let previewBox = document.getElementById('file-preview');
previewBox.innerHTML = `<iframe src="/file-preview/${idDirectory}" width="100%" height="500px" style="border:none;"></iframe>`;
$("#previewModal").modal('show')
}
if(e.target.id === 'delete-file'){
if(!currentFile){
Swal.fire({
text: "Tidak ada file yang dipilih!",
icon: "warning",
confirmButtonText: "OK"
});
return;
}
Swal.fire({
title: 'Yakin ingin menghapus file ini?',
text: "File akan dihapus",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#6c757d',
confirmButtonText: 'Ya, hapus',
cancelButtonText: 'Batal'
}).then((result) => {
if(result.isConfirmed){
fetch(`/delete-file/${idDirectory}`, {
method: 'DELETE',
headers: {
'Content-Type' : 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ file: currentFile })
})
.then(res => res.json())
.then(data => {
if(data.success){
Swal.fire({
text: 'File berhasil dihapus',
icon: 'success',
timer: 2000,
showConfirmButton: false
});
$("#previewModal").modal("hide");
fetchData()
currentFile = null;
idDirectory = null;
}else{
Swal.fire({
text: 'Gagal menghapus file',
icon: 'error'
});
}
})
.catch(err => {
console.log(err);
Swal.fire({
text: 'Terjadi error saat menghapus file ',
icon: 'error'
});
});
}
})
}
if(e.target.matches('#btn-view-full')){
window.open(`/file-preview/${idDirectory}`, '_blank');
}
})
formCreate.off('submit').on('submit', function(e){
e.preventDefault();
const submitBtn = $(this).find('button[type="submit"]');
submitBtn.prop('disabled', true).text('menyimpan...')
const formData = new FormData(this);
console.log(formData);
fetch(`/uploadv2`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('input[name="_token"]').value,
},
body: formData
}).then(async(res) => {
const responseData = await res.json();
if(responseData.status){
Toastify({
text: responseData.message || 'Berhasil melakukan aksi!',
duration: 3000,
gravity: "top",
position: "right",
style: {
background: "linear-gradient(to right, #00b09b, #96c93d)",
color: "#fff",
}
}).showToast();
resetCreateForm();
fetchData()
submitBtn.prop('disabled', false).text('Simpan')
const modalInstance = bootstrap.Modal.getInstance(modalCreate);
modalInstance?.hide();
} else {
throw new Error(responseData.message || 'Terjadi kesalahan saat menyimpan data.');
}
}).catch(err => {
if (err.message) {
Swal.fire({
icon: 'error',
title: 'Gagal',
text: err.message
});
submitBtn.prop('disabled', false).text('Simpan')
}
});
});
</script>
@endsection

View File

@ -13,9 +13,9 @@
<!-- Body -->
<div class="modal-body p-2" style="min-height:250px; max-height:70vh; overflow:auto;">
<div class="d-flex justify-content-end align-items-center sticky-top bg-white z-10">
{{-- <div class="d-flex justify-content-end align-items-center sticky-top bg-white z-10">
<button type="button" class="btn btn-sm btn-outline-danger mb-2 me-2" id="delete-file">🗑 Hapus</button>
</div>
</div> --}}
<div id="file-preview"
class="text-center text-muted d-flex justify-content-center align-items-center"
style="height:100%;">

View File

@ -1,96 +1,199 @@
@extends('layout.main')
<style>
.table-fixed{ table-layout: fixed; width: 100%; }
.table-fixed th:nth-child(1), .table-fixed td:nth-child(1){ width: 140px; }
.table-fixed th:nth-child(2), .table-fixed td:nth-child(2){ width: 260px; }
.table-fixed th:nth-child(3), .table-fixed td:nth-child(3){ width: 220px; }
.table-fixed th:nth-child(4), .table-fixed td:nth-child(4){ width: 120px; }
.table-fixed th:nth-child(5), .table-fixed td:nth-child(5){ width: 150px; }
.file-cell {
max-width: 420px;
}
.file-cell{
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
}
.file-wrapper {
display: flex;
flex-direction: column;
gap: 2px;
}
.folder-cell{
max-width: 220px;
}
.file-title {
display: flex;
align-items: center;
gap: 6px;
font-weight: 600;
color: #1f2937;
}
.folder-badge{
white-space: normal !important;
word-break: break-word;
overflow-wrap: anywhere;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
max-width: 100%;
text-align: left;
line-height: 1.3;
padding: .35rem .5rem;
}
.file-title i {
font-size: 18px;
color: #6366f1;
}
.file-link {
color: #1f2937;
text-decoration: none;
line-height: 1.3;
}
.file-link:hover {
color: #4f46e5;
text-decoration: underline;
}
.file-meta {
font-size: 12px;
color: #6b7280;
}
.file-meta span {
margin-right: 4px;
}
/* status pending */
.file-pending {
font-style: italic;
color: #9ca3af !important;
}
.dataunit-tabs .nav-link {
border: 0;
color: #475569;
font-weight: 600;
padding: 0.6rem 1rem;
border-radius: 999px;
transition: background-color .15s ease, color .15s ease, box-shadow .15s ease;
position: relative;
overflow: hidden;
}
.dataunit-tabs .nav-link.active {
background: color-mix(in srgb, var(--bs-primary) 12%, #ffffff);
color: var(--bs-primary);
+}
.dataunit-tabs .nav-link:hover {
color: var(--bs-primary);
background: color-mix(in srgb, var(--bs-primary) 8%, #ffffff);
}
.dataunit-tabs .nav-link::after {
content: "";
position: absolute;
left: 18px;
right: 18px;
bottom: 6px;
height: 2px;
border-radius: 999px;
background: var(--bs-primary);
transform: scaleX(0);
transform-origin: center;
transition: transform .18s ease;
opacity: 0;
}
.dataunit-tabs .nav-link:hover::after {
transform: scaleX(1);
opacity: 0.7;
}
.dataunit-tab-wrap {
background: #f8fafc;
border-radius: 999px;
padding: 6px;
display: flex;
gap: 6px;
width: 100%;
}
.dataunit-tabs {
width: 100%;
}
.dataunit-tabs .nav-item {
flex: 1 1 0;
}
.dataunit-tabs .nav-link {
width: 100%;
justify-content: center;
}
.folder-link-sm{
font-size: 13px;
text-decoration: underline;
}
</style>
@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">Data Terakhir</h4>
<button type="button" class="btn btn-success" data-bs-target="#modalCreateFile" data-bs-toggle="modal">Tambah File</button>
<div class="dataunit-tab-wrap">
<ul class="nav dataunit-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active d-flex align-items-center gap-2" data-bs-toggle="tab" href="#tab-data-unit" role="tab" aria-controls="tab-data-unit" aria-selected="true">
<i class="ti ti-folders"></i>
<span>Data Unit</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center gap-2" data-bs-toggle="tab" href="#tab-data-recap" role="tab" aria-controls="tab-data-recap" aria-selected="false">
<i class="ti ti-chart-bar"></i>
<span>Data Recap</span>
</a>
</li>
</ul>
</div>
</div>
<div class="card-body p-3">
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3">
<div class="input-group input-group-sm flex-grow-1">
<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, No Dokumen atau folder"
oninput="debouncedTableSearch(this.value)">
<div class="tab-content">
<div class="tab-pane fade show active" id="tab-data-unit" role="tabpanel">
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3">
<h4 class="mb-0">Data Unit</h4>
<button type="button" class="btn btn-success ms-md-auto" data-bs-target="#modalCreateFile" data-bs-toggle="modal">Tambah File</button>
</div>
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3">
<div class="input-group input-group-sm flex-grow-1">
<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, No Dokumen atau folder"
oninput="debouncedTableSearch(this.value)">
</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>
<div class="small text-muted ms-md-auto" id="tableSummary">Memuat data...</div>
</div>
<div class="table-responsive" style="max-height: 70vh; overflow-y:auto;">
<table class="table table-sm table-hover align-middle mb-0 table-fixed" id="lastUpdatedTable">
<thead>
<tr>
<th>Nomor Surat</th>
<th>File</th>
<th>Kategori</th>
<th>Unit</th>
<th>Sub Unit </th>
<th>Tanggal Upload</th>
</tr>
</thead>
<tbody id="tableDataUnit">
<!-- 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 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 class="tab-pane fade" id="tab-data-recap" role="tabpanel">
@include('dataUnit.section.recap')
</div>
<div class="small text-muted ms-md-auto" id="tableSummary">Memuat data...</div>
</div>
<div class="table-responsive" style="max-height: 70vh; overflow-y:auto;">
<table class="table table-sm table-hover align-middle mb-0 table-fixed" id="lastUpdatedTable">
<thead>
<tr>
<th>Nomor Surat</th>
<th>Nama</th>
<th>File Folder</th>
<th>Akses</th>
<th>Tanggal Modifikasi</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>
@include('dashboardV2.modal.create')
@include('dashboardV2.modal.view')
@include('dataUnit.modal.create')
@include('dataUnit.modal.view')
<script>
const katDok = @json($katDok);
const authUnitKerja = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]?->unitKerja);
@ -99,7 +202,7 @@
const formCreate = $("#formFile")
const modalCreate = document.getElementById('modalCreateFile')
const tableState = { data: [], page: 1, pageSize: 8, search: '', lastPage: 1, total: 0 };
const tbody = document.getElementById('tableFolderLastUpdated');
const tbody = document.getElementById('tableDataUnit');
const paginationEl = document.getElementById('paginationControls');
const summaryEl = document.getElementById('tableSummary');
const pageSizeSelect = document.getElementById('tablePageSize');
@ -143,47 +246,40 @@
let statusLabel = '';
let statusClass = '';
if (!item.status_action) {
statusLabel = 'Pending';
statusClass = 'bg-warning text-dark';
} else if (publicDoc) {
if (publicDoc) {
statusLabel = 'Umum';
statusClass = 'bg-success';
} else {
statusLabel = 'Internal Unit';
statusClass = 'bg-secondary';
}
return `
<tr>
<td class="text-nowrap">${item.no_dokumen || '-'}</td>
<td class="file-cell">
<span class="me-1">📄</span>
<a href="#" class="file-link ${statusLabel === "Pending" ? 'fst-italic' : ''}"
data-file="${item.file}"
data-fileName="${fileName}"
data-id="${item.file_directory_id}"
data-no_dokumen="${item.no_dokumen || '-'}"
data-tanggal_terbit="${item.tanggal_terbit || '-'}"
data-permission_file="${item.permission_file || '-'}">${fileName}</a>
</td>
<td class="folder-cell">
<a href="#"
class="text-dark fw-semibold folder-prefill folder-link-sm ${statusLabel === "Pending" ? 'fst-italic' : ''}"
title="${folderPath}"
data-unit_id="${item.id_unit_kerja || ''}"
data-sub_id="${item.id_sub_unit_kerja || ''}"
data-kategori_id="${item.master_kategori_directory_id || ''}"
data-unit_name="${unitName}"
data-sub_name="${subName}"
data-kategori_name="${kategoriName}">
${folderPath}
</a>
<div class="file-title">
<i class="ti ti-file-text"></i>
<a href="#"
class="file-link ${statusLabel === "Pending" ? 'file-pending' : ''}"
data-file="${item.file}"
data-fileName="${fileName}"
data-id="${item.file_directory_id}"
data-no_dokumen="${item.no_dokumen || '-'}"
data-tanggal_terbit="${item.tanggal_terbit || '-'}"
data-permission_file="${item.permission_file || '-'}"
>
${fileName}
</a>
</div>
</td>
<td>
<span class="badge ${statusClass}">
${statusLabel}
</span>
${kategoriName}
</td>
<td>
${unitName}
</td>
<td>
${subName}
</td>
<td class="text-nowrap">${formatTanggal(item.entry_at)}</td>
</tr>
@ -273,7 +369,7 @@
per_page: tableState.pageSize,
keyword: tableState.search
});
fetch(`/last-document?${params.toString()}`)
fetch(`/data-internal?${params.toString()}`)
.then(response => response.json())
.then(data => {
tableState.data = data?.data || [];

View File

@ -0,0 +1,96 @@
<div class="modal fade" id="modalCreateFile" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content">
<!-- Modal Header -->
<div class="modal-header">
<h1 class="modal-title fs-5">Aksi </h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<!-- Modal Form -->
<form id="formFile" action="/uploadv2" enctype="multipart/form-data" method="POST">
@csrf
<div class="modal-body">
<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-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 selected>Select Choose</option>
</select>
</div>
<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)
<option value="{{ $kat->master_kategori_directory_id }}/{{ $kat->nama_kategori_directory }}">{{ $kat->nama_kategori_directory }}</option>
@endforeach
</select>
</div>
<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]">
</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 (PDF)</label>
<div class="border rounded-3 p-3 bg-white shadow-sm">
<input class="form-control" 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">
Bisa upload lebih dari 1 file. Format yang didukung: <b>PDF</b>.
</div>
</div>
<div id="col_add_fileV2" class="col-12"></div>
</div>
<button type="button" class="btn btn-outline-primary btn-sm mt-3" onclick="addFormV2()">
+ Tambah Form
</button>
</div>
</div>
<!-- Modal Footer -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Tutup</button>
<button type="submit" class="btn btn-primary">Simpan</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,61 @@
<!-- Modal Preview -->
<div class="modal fade" id="previewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<!-- Header -->
<div class="modal-header">
<h6 class="modal-title" id="previewModalLabel">
📄 Preview <strong id="confirm_preview_file"></strong>
</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<!-- Body -->
<div class="modal-body p-2" style="min-height:250px; max-height:70vh; overflow:auto;">
{{-- <div class="d-flex justify-content-end align-items-center sticky-top bg-white z-10">
<button type="button" class="btn btn-sm btn-outline-danger mb-2 me-2" id="delete-file">🗑 Hapus</button>
</div> --}}
<div id="file-preview"
class="text-center text-muted d-flex justify-content-center align-items-center"
style="height:100%;">
<p>📂 Pilih file untuk melihat preview</p>
</div>
</div>
<!-- Footer -->
<div class="modal-footer d-flex justify-content-between align-items-center">
<div class="small text-muted">
<span class="me-3">
Nomor Dokumen:
<strong id="confirm-upload-dokumen" class="text-dark">-</strong>
</span>
<span class="me-3">
Tanggal Terbit:
<strong id="confirm-time-dokumen" class="text-dark">-</strong>
</span>
<span>
Akses Dokumen:
<span id="confirm-permission" class="badge bg-secondary">-</span>
</span>
</div>
<!-- Button -->
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-primary" id="btn-view-full">
Lihat Full
</button>
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">
Tutup
</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,147 @@
@php($showRecapTitle = $showRecapTitle ?? true)
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3">
@if ($showRecapTitle)
<div>
<h4 class="mb-0">Rekap Dokumen</h4>
<small class="text-muted">Ringkasan jumlah file per Unit dan Folder</small>
</div>
@endif
<div class="{{ $showRecapTitle ? '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 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:30%;">Sub Unit</th>
<th style="width:20%;">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>
<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');
if(!tbody) return;
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.subUnit || '-'}</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

@ -22,17 +22,23 @@
{{-- HOME --}}
<li class="nav-small-cap"><span class="hide-menu">Home</span></li>
<li class="sidebar-item">
{{-- <li class="sidebar-item">
<a class="sidebar-link" href="{{ url('/') }}" aria-expanded="false">
<i class="ti ti-dashboard"></i>
<span class="hide-menu">Dashboard</span>
</a>
</li> --}}
<li class="sidebar-item">
<a class="sidebar-link" href="{{ url('/') }}" aria-expanded="false">
<i class="ti ti-dashboard"></i>
<span class="hide-menu">Data Unit</span>
</a>
</li>
<li class="sidebar-item">
<a class="sidebar-link" href="{{ url('/new') }}" aria-expanded="false">
<a class="sidebar-link" href="{{ url('/data-umum') }}" aria-expanded="false">
<i class="ti ti-layout-dashboard"></i>
<span class="hide-menu">Dashboard V2</span>
<span class="hide-menu">Data Umum</span>
</a>
</li>
@ -75,7 +81,7 @@
</li>
@endif
{{-- RECAP --}}
<li class="nav-small-cap"><span class="hide-menu">Recap</span></li>
{{-- <li class="nav-small-cap"><span class="hide-menu">Recap</span></li>
<li class="sidebar-item">
<a class="sidebar-link d-flex align-items-center justify-content-between"
@ -85,7 +91,7 @@
<span class="hide-menu">Data Rekap</span>
</div>
</a>
</li>
</li> --}}
{{-- MASTER --}}
@if(auth()->user()->akses && auth()->user()->akses->master_akses)
@ -138,6 +144,28 @@
.sidebar-submenu { padding-left: 2.7rem; }
.sidebar-submenu .sidebar-link { padding: 6px 0; font-size: 13px; opacity: .9; }
.sidebar-item .sidebar-link {
border-radius: 10px;
transition: background-color .15s ease, color .15s ease, box-shadow .15s ease;
}
.sidebar-item .sidebar-link:hover,
.sidebar-item .sidebar-link:focus {
background: #eef2ff;
color: #1e40af;
box-shadow: 0 8px 18px rgba(30, 64, 175, 0.12);
}
.sidebar-item .sidebar-link:focus-visible {
outline: 2px solid rgba(59, 130, 246, 0.6);
outline-offset: 2px;
}
.sidebar-item .sidebar-link:hover i,
.sidebar-item .sidebar-link:focus i {
color: #1e40af;
}
.sidebar-item.has-sub .sidebar-arrow { transition: transform .2s ease; }
.sidebar-item.has-sub.open .sidebar-arrow { transform: rotate(90deg); }
</style>
@ -185,4 +213,3 @@
});
}
</script>

View File

@ -12,8 +12,9 @@ use Illuminate\Support\Facades\Route;
Route::middleware(['auth'])->group(function(){
Route::get('/', [DashboardController::class, 'index']);
Route::get('/new', [DashboardController::class, 'dashboardVersion2']);
Route::get('/last-document', [DashboardController::class, 'dataDocumentLast']);
Route::get('/data-internal', [DashboardController::class, 'dataUnitInternal']);
Route::get('/data-umum', [DashboardController::class, 'dataUmum']);
Route::get('/datatable-umum', [DashboardController::class, 'dataDocumentLast']);
Route::post('/uploadv2', [DashboardController::class, 'storeVersion2']);
Route::get('/file-preview/{id}', [DashboardController::class, 'dataPdf']);
Route::post('/upload', [DashboardController::class, 'store']);