Compare commits

..

15 Commits
akrev2 ... main

27 changed files with 3919 additions and 2376 deletions

View File

@ -2,7 +2,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\FileDirectory;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
@ -150,90 +149,6 @@ class AkreditasiInstrumenController extends Controller
return true; return true;
} }
private function hasFilesInPath(string $folderPath): bool
{
$escapedPath = addcslashes($folderPath, '\\%_');
return FileDirectory::query()
->where('statusenabled', true)
->where('is_akre', true)
->where('file', 'ILIKE', $escapedPath . '/%')
->exists();
}
private function removeNodeByPath(array &$data, string $folderPath): bool
{
$parts = array_values(array_filter(explode('/', $folderPath), fn ($value) => trim((string) $value) !== ''));
$depth = count($parts);
if ($depth === 0) {
return false;
}
if ($depth === 1) {
foreach ($data as $index => $type) {
if (($type['name'] ?? null) !== $parts[0]) {
continue;
}
$segments = $type['segment'] ?? [];
if (is_array($segments) && count($segments) > 0) {
return false;
}
array_splice($data, $index, 1);
return true;
}
return false;
}
foreach ($data as $typeIndex => $type) {
if (($type['name'] ?? null) !== $parts[0]) {
continue;
}
$segments = $data[$typeIndex]['segment'] ?? [];
if (!is_array($segments)) {
return false;
}
foreach ($segments as $segmentIndex => $segment) {
if (($segment['name'] ?? null) !== $parts[1]) {
continue;
}
if ($depth === 2) {
$children = $segment['turunan'] ?? [];
if (is_array($children) && count($children) > 0) {
return false;
}
array_splice($segments, $segmentIndex, 1);
$data[$typeIndex]['segment'] = array_values($segments);
return true;
}
$children = $segments[$segmentIndex]['turunan'] ?? [];
if (!is_array($children)) {
return false;
}
foreach ($children as $childIndex => $child) {
if (($child['name'] ?? null) !== $parts[2]) {
continue;
}
array_splice($children, $childIndex, 1);
$segments[$segmentIndex]['turunan'] = array_values($children);
$data[$typeIndex]['segment'] = array_values($segments);
return true;
}
}
}
return false;
}
/** /**
* Read akreditasi.jff (array) * Read akreditasi.jff (array)
*/ */
@ -398,97 +313,4 @@ class AkreditasiInstrumenController extends Controller
@fclose($lockFp); @fclose($lockFp);
} }
} }
public function destroy(Request $request)
{
$validated = $request->validate([
'path' => ['required', 'string', 'max:2000'],
]);
$folderPath = trim((string) ($validated['path'] ?? ''));
if ($folderPath === '') {
return response()->json([
'status' => false,
'message' => 'Path folder wajib diisi.',
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
if ($this->hasFilesInPath($folderPath)) {
return response()->json([
'status' => false,
'message' => 'Folder tidak bisa dihapus karena masih memiliki file di dalamnya.',
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
$this->ensureJsonDirExists();
$path = $this->akreditasiJsonPath();
$lockPath = $this->akreditasiLockPath();
$lockFp = @fopen($lockPath, 'c+');
if (!$lockFp) {
return response()->json([
'status' => false,
'message' => 'Gagal membuka file lock untuk akreditasi.jff.',
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
try {
if (!flock($lockFp, LOCK_EX)) {
return response()->json([
'status' => false,
'message' => 'Gagal mengunci file akreditasi.jff.',
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
$raw = @file_get_contents($path);
if ($raw === false) {
$data = [];
} else {
$data = $this->decodeJsonArray($raw, true);
if ($data === null) {
$repaired = $this->tryRepairJsonArray($raw);
if ($repaired === null) {
return response()->json([
'status' => false,
'message' => 'File public/json/akreditasi.jff tidak valid (JSON rusak).',
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
$data = $repaired;
}
}
$removed = $this->removeNodeByPath($data, $folderPath);
if (!$removed) {
return response()->json([
'status' => false,
'message' => 'Folder tidak ditemukan atau masih memiliki turunan.',
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json === false) {
return response()->json([
'status' => false,
'message' => 'Gagal mengubah data menjadi JSON.',
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
$json .= "\n";
if (!$this->atomicWrite($path, $json)) {
return response()->json([
'status' => false,
'message' => 'Gagal menulis file akreditasi.jff (atomic write).',
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
return response()->json([
'status' => true,
'message' => 'Folder akreditasi berhasil dihapus.',
'data' => $data,
]);
} finally {
@flock($lockFp, LOCK_UN);
@fclose($lockFp);
}
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,6 @@ class EnsureMasterPersetujuan
{ {
$user = $request->user(); $user = $request->user();
$hasAccess = $user && $user->masterPersetujuan; $hasAccess = $user && $user->masterPersetujuan;
if (!$hasAccess) { if (!$hasAccess) {
abort(403, 'Tidak memiliki akses.'); abort(403, 'Tidak memiliki akses.');
} }

View File

@ -11,9 +11,9 @@ class SubUnitKerja extends Model
public $timestamps = false; public $timestamps = false;
protected $primaryKey = 'id'; protected $primaryKey = 'id';
protected $guarded = ['id']; protected $guarded = ['id'];
protected $with = ['fileDirectory']; // protected $with = ['fileDirectory'];
public function fileDirectory(){ // public function fileDirectory(){
return $this->hasMany(FileDirectory::class, 'id_sub_unit_kerja', 'id')->where('statusenabled', true); // return $this->hasMany(FileDirectory::class, 'id_sub_unit_kerja', 'id')->where('statusenabled', true);
} // }
} }

View File

@ -11,10 +11,10 @@ class UnitKerja extends Model
public $timestamps = false; public $timestamps = false;
protected $primaryKey = 'id'; protected $primaryKey = 'id';
protected $guarded = ['id']; protected $guarded = ['id'];
protected $with = ['subUnitKerja']; // protected $with = ['subUnitKerja'];
public function subUnitKerja(){ // public function subUnitKerja(){
return $this->hasMany(SubUnitKerja::class, 'objectunitkerjapegawaifk', 'id')->where('statusenabled', true)->select('id', 'objectunitkerjapegawaifk', 'name'); // return $this->hasMany(SubUnitKerja::class, 'objectunitkerjapegawaifk', 'id')->where('statusenabled', true)->select('id', 'objectunitkerjapegawaifk', 'name');
} // }
} }

View File

@ -45,7 +45,7 @@ class User extends Authenticatable
'katasandi' => 'hashed', 'katasandi' => 'hashed',
]; ];
} }
protected $with = ['dataUser', 'masterPersetujuan', 'akses']; protected $with = ['dataUser'];
public function dataUser(){ public function dataUser(){
return $this->belongsTo(DataUser::class, 'objectpegawaifk', 'id')->select('id', 'namalengkap'); return $this->belongsTo(DataUser::class, 'objectpegawaifk', 'id')->select('id', 'namalengkap');
} }

View File

@ -1,8 +1,10 @@
<?php <?php
use Illuminate\Http\Request;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Session\TokenMismatchException;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
@ -17,5 +19,24 @@ return Application::configure(basePath: dirname(__DIR__))
]); ]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
// $exceptions->render(function (TokenMismatchException $exception, Request $request) {
$message = 'Sesi halaman sudah habis. Silakan login ulang.';
if ($request->is('login')) {
return redirect()
->route('login')
->withInput($request->only('namauser'))
->with('alertError', 'expired');
}
if (!$request->expectsJson()) {
return redirect()
->guest(route('login'))
->with('alertError', 'expired-session');
}
return response()->json([
'message' => $message,
], 419);
});
})->create(); })->create();

78
composer.lock generated
View File

@ -62,16 +62,16 @@
}, },
{ {
"name": "aws/aws-sdk-php", "name": "aws/aws-sdk-php",
"version": "3.384.2", "version": "3.383.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/aws/aws-sdk-php.git", "url": "https://github.com/aws/aws-sdk-php.git",
"reference": "a6c85b3a1f5f8327076291040329214b3a2caf2b" "reference": "11c2de39e4511dc99e44f049c7dfc8087e051867"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a6c85b3a1f5f8327076291040329214b3a2caf2b", "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/11c2de39e4511dc99e44f049c7dfc8087e051867",
"reference": "a6c85b3a1f5f8327076291040329214b3a2caf2b", "reference": "11c2de39e4511dc99e44f049c7dfc8087e051867",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -153,9 +153,9 @@
"support": { "support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions", "forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues", "issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.384.2" "source": "https://github.com/aws/aws-sdk-php/tree/3.383.2"
}, },
"time": "2026-06-03T18:07:33+00:00" "time": "2026-06-01T18:08:21+00:00"
}, },
{ {
"name": "brick/math", "name": "brick/math",
@ -873,26 +873,25 @@
}, },
{ {
"name": "guzzlehttp/guzzle", "name": "guzzlehttp/guzzle",
"version": "7.11.0", "version": "7.10.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/guzzle/guzzle.git", "url": "https://github.com/guzzle/guzzle.git",
"reference": "c987f8ce84b8434fa430795eca0f3430663da72b" "reference": "e7412b3180912c01650cc66647f18c1d1cbe9b94"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/c987f8ce84b8434fa430795eca0f3430663da72b", "url": "https://api.github.com/repos/guzzle/guzzle/zipball/e7412b3180912c01650cc66647f18c1d1cbe9b94",
"reference": "c987f8ce84b8434fa430795eca0f3430663da72b", "reference": "e7412b3180912c01650cc66647f18c1d1cbe9b94",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-json": "*", "ext-json": "*",
"guzzlehttp/promises": "^2.5", "guzzlehttp/promises": "^2.3",
"guzzlehttp/psr7": "^2.11", "guzzlehttp/psr7": "^2.8",
"php": "^7.2.5 || ^8.0", "php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0", "psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.5 || ^3.0", "symfony/deprecation-contracts": "^2.2 || ^3.0"
"symfony/polyfill-php80": "^1.24"
}, },
"provide": { "provide": {
"psr/http-client-implementation": "1.0" "psr/http-client-implementation": "1.0"
@ -981,7 +980,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/guzzle/guzzle/issues", "issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.11.0" "source": "https://github.com/guzzle/guzzle/tree/7.10.6"
}, },
"funding": [ "funding": [
{ {
@ -997,25 +996,24 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-06-02T12:40:51+00:00" "time": "2026-06-01T13:06:22+00:00"
}, },
{ {
"name": "guzzlehttp/promises", "name": "guzzlehttp/promises",
"version": "2.5.0", "version": "2.4.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/guzzle/promises.git", "url": "https://github.com/guzzle/promises.git",
"reference": "4360e982f87f5f258bf872d094647791db2f4c8e" "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", "url": "https://api.github.com/repos/guzzle/promises/zipball/09e8a212562fb1fb6a512c4156ed71525969d6c2",
"reference": "4360e982f87f5f258bf872d094647791db2f4c8e", "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.2.5 || ^8.0", "php": "^7.2.5 || ^8.0"
"symfony/deprecation-contracts": "^2.5 || ^3.0"
}, },
"require-dev": { "require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2", "bamarni/composer-bin-plugin": "^1.8.2",
@ -1065,7 +1063,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/guzzle/promises/issues", "issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/2.5.0" "source": "https://github.com/guzzle/promises/tree/2.4.1"
}, },
"funding": [ "funding": [
{ {
@ -1081,29 +1079,27 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-06-02T12:23:43+00:00" "time": "2026-05-20T22:57:30+00:00"
}, },
{ {
"name": "guzzlehttp/psr7", "name": "guzzlehttp/psr7",
"version": "2.11.0", "version": "2.10.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/guzzle/psr7.git", "url": "https://github.com/guzzle/psr7.git",
"reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f" "reference": "d2a1a094e396da8957e797489fddaf860c340cfc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/bbb5e61349fa5cb822b3e87842b951088b76b81f", "url": "https://api.github.com/repos/guzzle/psr7/zipball/d2a1a094e396da8957e797489fddaf860c340cfc",
"reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f", "reference": "d2a1a094e396da8957e797489fddaf860c340cfc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.2.5 || ^8.0", "php": "^7.2.5 || ^8.0",
"psr/http-factory": "^1.0", "psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0", "psr/http-message": "^1.1 || ^2.0",
"ralouphie/getallheaders": "^3.0", "ralouphie/getallheaders": "^3.0"
"symfony/deprecation-contracts": "^2.5 || ^3.0",
"symfony/polyfill-php80": "^1.24"
}, },
"provide": { "provide": {
"psr/http-factory-implementation": "1.0", "psr/http-factory-implementation": "1.0",
@ -1184,7 +1180,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/guzzle/psr7/issues", "issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.11.0" "source": "https://github.com/guzzle/psr7/tree/2.10.4"
}, },
"funding": [ "funding": [
{ {
@ -1200,7 +1196,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-06-02T12:30:48+00:00" "time": "2026-05-29T12:59:07+00:00"
}, },
{ {
"name": "guzzlehttp/uri-template", "name": "guzzlehttp/uri-template",
@ -7193,16 +7189,16 @@
}, },
{ {
"name": "laravel/sail", "name": "laravel/sail",
"version": "v1.62.0", "version": "v1.61.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/sail.git", "url": "https://github.com/laravel/sail.git",
"reference": "3aaeefc979f8ba6586fbc5b6e0b1b3638058f98e" "reference": "68ef35015630fe510432e63e11e21749006df688"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/sail/zipball/3aaeefc979f8ba6586fbc5b6e0b1b3638058f98e", "url": "https://api.github.com/repos/laravel/sail/zipball/68ef35015630fe510432e63e11e21749006df688",
"reference": "3aaeefc979f8ba6586fbc5b6e0b1b3638058f98e", "reference": "68ef35015630fe510432e63e11e21749006df688",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7252,7 +7248,7 @@
"issues": "https://github.com/laravel/sail/issues", "issues": "https://github.com/laravel/sail/issues",
"source": "https://github.com/laravel/sail" "source": "https://github.com/laravel/sail"
}, },
"time": "2026-05-27T04:02:01+00:00" "time": "2026-05-23T23:33:57+00:00"
}, },
{ {
"name": "mockery/mockery", "name": "mockery/mockery",
@ -9235,12 +9231,12 @@
], ],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": {}, "stability-flags": [],
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": "^8.2" "php": "^8.2"
}, },
"platform-dev": {}, "platform-dev": [],
"plugin-api-version": "2.9.0" "plugin-api-version": "2.2.0"
} }

View File

@ -15904,14 +15904,14 @@ body {
border-radius: 20px; border-radius: 20px;
} }
.body-wrapper .body-wrapper-inner { .body-wrapper .body-wrapper-inner {
min-height: calc(100vh - 110px); min-height: calc(130vh - 180px);
} }
.body-wrapper .container-fluid, .body-wrapper .container-sm, .body-wrapper .container-md, .body-wrapper .container-lg, .body-wrapper .container-xl, .body-wrapper .container-xxl { .body-wrapper .container-fluid, .body-wrapper .container-sm, .body-wrapper .container-md, .body-wrapper .container-lg, .body-wrapper .container-xl, .body-wrapper .container-xxl {
max-width: 1300px; max-width: 1600px;
margin: 0 auto; margin: 0 auto;
padding: 24px; padding: 10px;
transition: 0.2s ease-in; transition: 0.2s ease-in;
padding-top: 120px; padding-top: 90px;
} }
@media (max-width: 991.98px) { @media (max-width: 991.98px) {
.body-wrapper .container-fluid, .body-wrapper .container-sm, .body-wrapper .container-md, .body-wrapper .container-lg, .body-wrapper .container-xl, .body-wrapper .container-xxl { .body-wrapper .container-fluid, .body-wrapper .container-sm, .body-wrapper .container-md, .body-wrapper .container-lg, .body-wrapper .container-xl, .body-wrapper .container-xxl {

View File

@ -19,6 +19,8 @@ document.addEventListener('DOMContentLoaded', () => {
const titleEl = document.getElementById('pendingTitle'); const titleEl = document.getElementById('pendingTitle');
const tabPendingEl = document.getElementById('tabPengajuan'); const tabPendingEl = document.getElementById('tabPengajuan');
const tabHistoryEl = document.getElementById('tabHistory'); const tabHistoryEl = document.getElementById('tabHistory');
const isKomiteMutu = !!window.isKomiteMutu;
const isTurt = !!window.isTurt;
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const selectedIds = new Set(); const selectedIds = new Set();
@ -63,10 +65,54 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
} }
function statusBadge(status){ function approvalBadge(status, label){
if (status === 'rejected') return '<span class="badge bg-danger">Rejected</span>'; const normalized = String(status || '').toLowerCase();
if (status === 'revised') return '<span class="badge bg-info">Revised</span>'; if (normalized === 'approved') return `<span class="badge bg-success">Approved ${label}</span>`;
return '<span class="badge bg-warning text-dark">Pending</span>'; if (normalized.startsWith('rejected')) return `<span class="badge bg-danger">Rejected ${label}</span>`;
if (normalized.startsWith('revised')) return `<span class="badge bg-info">Revised ${label}</span>`;
return `<span class="badge bg-warning text-dark">Pending ${label}</span>`;
}
function getWorkflowStatusBadges(item){
const actionStatus = String(item?.status_action || '').toLowerCase();
const mutuStatus = String(item?.status_mutu || '').toLowerCase();
const turtStatus = String(item?.status_turt || '').toLowerCase();
if (actionStatus.startsWith('revised')) {
return [`<span class="badge bg-info">Revised</span>`];
}
if (actionStatus.startsWith('rejected')) {
return [`<span class="badge bg-danger">Rejected Atasan</span>`];
}
if (actionStatus !== 'approved') {
return [`<span class="badge bg-warning text-dark">Pending Atasan</span>`];
}
const statuses = [];
if (item?.is_akre) {
if (mutuStatus.startsWith('rejected')) {
statuses.push(`<span class="badge bg-danger">Rejected Mutu</span>`);
} else if (mutuStatus !== 'approved') {
statuses.push(`<span class="badge bg-warning text-dark">Pending Mutu</span>`);
}
}
if (item?.master_kategori_directory_id) {
if (turtStatus.startsWith('rejected')) {
statuses.push(`<span class="badge bg-danger">Rejected TURT</span>`);
} else if (turtStatus !== 'approved') {
statuses.push(`<span class="badge bg-warning text-dark">Pending TURT</span>`);
}
}
if (statuses.length > 0) {
return statuses;
}
return [`<span class="badge bg-success">Approved</span>`];
} }
function aksesBadge(akses){ function aksesBadge(akses){
@ -81,12 +127,87 @@ document.addEventListener('DOMContentLoaded', () => {
} }
function isRejected(item){ function isRejected(item){
return item?.status_action === 'rejected'; return String(item?.status_action || '').toLowerCase().startsWith('rejected')
|| String(item?.status_mutu || '').toLowerCase().startsWith('rejected')
|| String(item?.status_turt || '').toLowerCase().startsWith('rejected');
}
function isAtasanApproved(item){
return String(item?.status_action || '').toLowerCase() === 'approved';
}
function canProcessRoleStatus(status){
const normalized = String(status || '').toLowerCase();
return normalized === '' || normalized.startsWith('revised');
}
function canApproveAsAtasan(item){
return item?.can_approve_atasan === true || item?.can_approve_atasan === 1 || item?.can_approve_atasan === '1';
}
function canEditSubmission(item){
return item?.can_edit_submission === true || item?.can_edit_submission === 1 || item?.can_edit_submission === '1';
}
function hasRevisionInfo(item){
return !!(item?.revision || item?.mutu_revision || item?.turt_revision);
}
function collectRevisionNotes(item){
const notes = [];
const genericRevision = String(item?.revision || '').trim();
const mutuRevision = String(item?.mutu_revision || '').trim();
const turtRevision = String(item?.turt_revision || '').trim();
if (genericRevision) {
const lowerGeneric = genericRevision.toLowerCase();
if (!lowerGeneric.startsWith('komite mutu:') && !lowerGeneric.startsWith('turt:')) {
notes.push({ label: 'Atasan', text: genericRevision });
}
}
if (mutuRevision) {
notes.push({ label: 'Komite Mutu', text: mutuRevision });
}
if (turtRevision) {
notes.push({ label: 'TURT', text: turtRevision });
}
return notes;
}
function escapeHtml(value){
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function buildRevisionNotesHtml(notes){
if (!notes.length) {
return '<div class="text-muted">Tidak ada catatan revisi.</div>';
}
return `
<div class="text-start" style="line-height:1.6;">
${notes.map(note => `
<div class="mb-2">
<div><strong>${escapeHtml(note.label)}</strong></div>
<div>${escapeHtml(note.text)}</div>
</div>
`).join('')}
</div>
`;
}
function getItemById(id){
return (tableState.data || []).find((row) => String(row.file_directory_id) === String(id));
} }
function getSelectableIdsOnPage(){ function getSelectableIdsOnPage(){
return (tableState.data || []) return (tableState.data || [])
.filter((item) => !isRejected(item)) .filter((item) => !isRejected(item) && !isAtasanApproved(item) && canApproveAsAtasan(item))
.map((item) => String(item.file_directory_id)); .map((item) => String(item.file_directory_id));
} }
@ -114,9 +235,11 @@ document.addEventListener('DOMContentLoaded', () => {
function updateSelectionUI(){ function updateSelectionUI(){
const count = selectedIds.size; const count = selectedIds.size;
const selectableIds = getSelectableIdsOnPage();
const canBulkApprove = tableState.mode !== 'history' && selectableIds.length > 0;
if (selectedCountEl) selectedCountEl.textContent = String(count); if (selectedCountEl) selectedCountEl.textContent = String(count);
if (bulkApproveBtn) bulkApproveBtn.disabled = count === 0 || tableState.mode === 'history'; if (bulkApproveBtn) bulkApproveBtn.disabled = count === 0 || !canBulkApprove;
if (clearSelectionBtn) clearSelectionBtn.disabled = count === 0 || tableState.mode === 'history'; if (clearSelectionBtn) clearSelectionBtn.disabled = count === 0 || !canBulkApprove;
updateSelectAllState(); updateSelectAllState();
} }
@ -126,26 +249,60 @@ document.addEventListener('DOMContentLoaded', () => {
const tanggalTerbit = item.tanggal_terbit ? formatTanggal(item.tanggal_terbit) : '-'; const tanggalTerbit = item.tanggal_terbit ? formatTanggal(item.tanggal_terbit) : '-';
const id = String(item.file_directory_id); const id = String(item.file_directory_id);
const rejected = isRejected(item); const rejected = isRejected(item);
const atasanApproved = isAtasanApproved(item);
const canAtasanApprove = canApproveAsAtasan(item);
const atasanRejected = String(item?.status_action || '').toLowerCase().startsWith('rejected');
const canMutuAct = atasanApproved && isKomiteMutu && item.is_akre && canProcessRoleStatus(item.status_mutu);
const canTurtAct = atasanApproved && isTurt && item.master_kategori_directory_id && canProcessRoleStatus(item.status_turt);
const checked = selectedIds.has(id); const checked = selectedIds.has(id);
const aksi = ` const actions = [
<div class="d-flex gap-1"> `<button class="btn btn-sm btn-primary" onclick="infoDok(this)"
<button class="btn btn-sm btn-primary" onclick="infoDok(this)" data-file="${item.file}"
data-file="${item.file}" data-fileName="${item.nama_dokumen}"
data-fileName="${item.nama_dokumen}" data-id="${item.file_directory_id}"
data-id="${item.file_directory_id}" data-no_dokumen="${item.no_dokumen || '-'}"
data-no_dokumen="${item.no_dokumen || '-'}" data-tanggal_terbit="${item.tanggal_terbit || '-'}"
data-tanggal_terbit="${item.tanggal_terbit || '-'}" data-permission_file="${item.permission_file || '-'}">
data-permission_file="${item.permission_file || '-'}"> <i class="fa-solid fa-eye"></i>
<i class="fa-solid fa-eye"></i> </button>`
</button> ];
<button class="btn btn-sm btn-success" onclick="approvePending('${item.file_directory_id}', '${item.nama_dokumen || ''}')">
<i class="fa-solid fa-check"></i> if (isKomiteMutu || isTurt) {
</button> if (canAtasanApprove && !atasanApproved) {
<button class="btn btn-sm btn-danger" onclick="rejectPending('${item.file_directory_id}', '${item.nama_dokumen || ''}')"> actions.push(`<button class="btn btn-sm btn-success" onclick="approvePending('${item.file_directory_id}', '${item.nama_dokumen || ''}')"><i class="fa-solid fa-check"></i></button>`);
<i class="fa-solid fa-xmark"></i> actions.push(`<button class="btn btn-sm btn-danger" onclick="rejectPending('${item.file_directory_id}', '${item.nama_dokumen || ''}')"><i class="fa-solid fa-xmark"></i></button>`);
</button> }
</div> if (canMutuAct) {
`; actions.push(`<button class="btn btn-sm btn-success" onclick="approvePendingRole('mutu','${item.file_directory_id}','${item.nama_dokumen || ''}')"><i class="fa-solid fa-check"></i></button>`);
actions.push(`<button class="btn btn-sm btn-danger" onclick="rejectPendingRole('mutu','${item.file_directory_id}','${item.nama_dokumen || ''}')"><i class="fa-solid fa-xmark"></i></button>`);
}
if (canTurtAct) {
actions.push(`<button class="btn btn-sm btn-success" onclick="approvePendingRole('turt','${item.file_directory_id}','${item.nama_dokumen || ''}')"><i class="fa-solid fa-check"></i></button>`);
actions.push(`<button class="btn btn-sm btn-danger" onclick="rejectPendingRole('turt','${item.file_directory_id}','${item.nama_dokumen || ''}')"><i class="fa-solid fa-ban"></i></button>`);
}
} else {
if (!atasanApproved) {
actions.push(`<button class="btn btn-sm btn-success" onclick="approvePending('${item.file_directory_id}', '${item.nama_dokumen || ''}')"><i class="fa-solid fa-check"></i></button>`);
actions.push(`<button class="btn btn-sm btn-danger" onclick="rejectPending('${item.file_directory_id}', '${item.nama_dokumen || ''}')"><i class="fa-solid fa-xmark"></i></button>`);
}
}
if (rejected && atasanRejected && !canMutuAct && !canTurtAct) {
actions.length = 1;
}
if (rejected) {
if (hasRevisionInfo(item)) {
actions.push(`<button class="btn btn-sm btn-info" onclick="infoRejectPending('${item.file_directory_id}')"><i class="fa-solid fa-circle-info"></i></button>`);
}
if (canEditSubmission(item)) {
actions.push(`<button class="btn btn-sm btn-primary" onclick="editRejectedFromPending('${item.file_directory_id}')"><i class="fa-solid fa-pen-to-square"></i></button>`);
}
}
const statusContent = (isKomiteMutu || isTurt)
? getWorkflowStatusBadges(item).join(' ')
: getWorkflowStatusBadges(item).join(' ');
return ` return `
<tr class="${checked ? 'table-active' : ''}"> <tr class="${checked ? 'table-active' : ''}">
<td class="text-center"> <td class="text-center">
@ -153,11 +310,11 @@ document.addEventListener('DOMContentLoaded', () => {
class="form-check-input row-select" class="form-check-input row-select"
data-id="${id}" data-id="${id}"
${checked ? 'checked' : ''} ${checked ? 'checked' : ''}
${rejected ? 'disabled' : ''}> ${(rejected || atasanApproved || !canAtasanApprove) ? 'disabled' : ''}>
</td> </td>
<td class="text-center">${rejected ? '' : aksi}</td> <td class="text-center text-nowrap"><div class="d-flex justify-content-center align-items-center gap-1 flex-nowrap">${actions.join('')}</div></td>
<td>${safeText(item.no_dokumen)}</td> <td>${safeText(item.no_dokumen)}</td>
<td>${statusBadge(item?.status_action)}</td> <td>${statusContent}</td>
<td>${aksesBadge(item?.permission_file)}</td> <td>${aksesBadge(item?.permission_file)}</td>
<td><a href="#" class="file-link" <td><a href="#" class="file-link"
data-file="${item.file}" data-file="${item.file}"
@ -166,8 +323,8 @@ document.addEventListener('DOMContentLoaded', () => {
data-no_dokumen="${item.no_dokumen || '-'}" data-no_dokumen="${item.no_dokumen || '-'}"
data-tanggal_terbit="${item.tanggal_terbit || '-'}" data-tanggal_terbit="${item.tanggal_terbit || '-'}"
data-permission_file="${item.permission_file || '-'}">${item.nama_dokumen}</a></td> data-permission_file="${item.permission_file || '-'}">${item.nama_dokumen}</a></td>
<td class="col-kategori"><div class="cell-wrap">${safeText(item.folder)}</div></td> <td class="col-kategori"><div class="cell-wrap">${safeText(item.kategori)}</div></td>
<td class="col-unit"><div class="cell-wrap">${safeText(item.part)}</div></td> <td class="col-unit"><div class="cell-wrap">${safeText(item.nama_unit)}</div></td>
<td class="text-nowrap">${tanggalTerbit}</td> <td class="text-nowrap">${tanggalTerbit}</td>
<td class="text-nowrap">${tanggalExp}</td> <td class="text-nowrap">${tanggalExp}</td>
<td class="text-nowrap">${tanggal}</td> <td class="text-nowrap">${tanggal}</td>
@ -177,6 +334,7 @@ document.addEventListener('DOMContentLoaded', () => {
} }
function buildHistoryRow(item){ function buildHistoryRow(item){
const tanggal = item.entry_at ? formatTanggal(item.entry_at) : '-'; const tanggal = item.entry_at ? formatTanggal(item.entry_at) : '-';
const tanggalExp = item.tgl_expired ? formatTanggal(item.tgl_expired) : '-'; const tanggalExp = item.tgl_expired ? formatTanggal(item.tgl_expired) : '-';
const tanggalTerbit = item.tanggal_terbit ? formatTanggal(item.tanggal_terbit) : '-'; const tanggalTerbit = item.tanggal_terbit ? formatTanggal(item.tanggal_terbit) : '-';
@ -192,11 +350,9 @@ document.addEventListener('DOMContentLoaded', () => {
data-no_dokumen="${item.no_dokumen || '-'}" data-no_dokumen="${item.no_dokumen || '-'}"
data-tanggal_terbit="${item.tanggal_terbit || '-'}" data-tanggal_terbit="${item.tanggal_terbit || '-'}"
data-permission_file="${item.permission_file || '-'}">${safeText(item.nama_dokumen)}</a></td> data-permission_file="${item.permission_file || '-'}">${safeText(item.nama_dokumen)}</a></td>
<td>${safeText(item.folder)}</td> <td>${safeText(item.kategori)}</td>
<td>${safeText(item.part)}</td> <td>${safeText(item.nama_unit)}</td>
<td><span class="badge bg-info">${actionType}</span></td> <td><span class="badge bg-info">${actionType}</span></td>
<td class="text-nowrap">${tanggalTerbit}</td>
<td class="text-nowrap">${tanggalExp}</td>
<td class="text-nowrap">${tanggal}</td> <td class="text-nowrap">${tanggal}</td>
</tr> </tr>
`; `;
@ -323,7 +479,7 @@ document.addEventListener('DOMContentLoaded', () => {
tabHistoryEl.classList.toggle('d-none', tableState.mode !== 'history'); tabHistoryEl.classList.toggle('d-none', tableState.mode !== 'history');
} }
if (pendingBulkActionsEl) { if (pendingBulkActionsEl) {
pendingBulkActionsEl.classList.toggle('d-none', tableState.mode === 'history'); pendingBulkActionsEl.classList.toggle('d-none', tableState.mode === 'history' || isKomiteMutu || isTurt);
} }
updateSelectionUI(); updateSelectionUI();
} }
@ -404,7 +560,7 @@ document.addEventListener('DOMContentLoaded', () => {
showConfirmButton: false showConfirmButton: false
}); });
selectedIds.delete(String(id)); selectedIds.delete(String(id));
countData(); if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
fetchData(); fetchData();
}).catch((err) => { }).catch((err) => {
Swal.fire({ Swal.fire({
@ -461,7 +617,127 @@ document.addEventListener('DOMContentLoaded', () => {
showConfirmButton: false showConfirmButton: false
}); });
selectedIds.delete(String(id)); selectedIds.delete(String(id));
countData(); if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
fetchData();
}).catch((err) => {
Swal.fire({
icon: 'error',
title: 'Gagal',
text: err.message || 'Terjadi kesalahan.'
});
});
});
}
window.infoRejectPending = function(id){
const item = getItemById(id);
Swal.fire({
title: 'Catatan Revisi',
html: buildRevisionNotesHtml(collectRevisionNotes(item)),
icon: 'info',
confirmButtonText: 'Tutup'
});
}
window.editRejectedFromPending = function(id){
const targetUrl = new URL('/pengajuan-file', window.location.origin);
targetUrl.searchParams.set('edit', id);
window.location.href = targetUrl.toString();
}
window.approvePendingRole = function(type, id, fileName){
const endpoint = type === 'mutu'
? `/pending-file/${id}/approve-mutu`
: `/pending-file/${id}/approve-turt`;
const label = type === 'mutu' ? 'Komite Mutu' : 'TURT';
Swal.fire({
title: `Approve ${label}?`,
text: fileName || 'Dokumen akan disetujui.',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Approve',
cancelButtonText: 'Batal',
}).then((result) => {
if (!result.isConfirmed) return;
fetch(endpoint, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken,
'Content-Type': 'application/json'
},
}).then(async(res) => {
const data = await res.json();
if (!res.ok || !data?.status) {
throw new Error(data?.message || 'Gagal approve.');
}
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: data.message || 'Dokumen disetujui.',
timer: 1500,
showConfirmButton: false
});
if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
fetchData();
}).catch((err) => {
Swal.fire({
icon: 'error',
title: 'Gagal',
text: err.message || 'Terjadi kesalahan.'
});
});
});
}
window.rejectPendingRole = function(type, id, fileName){
const endpoint = type === 'mutu'
? `/pending-file/${id}/reject-mutu`
: `/pending-file/${id}/reject-turt`;
const label = type === 'mutu' ? 'Komite Mutu' : 'TURT';
Swal.fire({
title: `Reject ${label}?`,
text: fileName || 'Dokumen akan ditolak.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Reject',
cancelButtonText: 'Batal',
input: 'textarea',
inputLabel: 'Catatan',
inputPlaceholder: 'Tulis alasan atau catatan revisi...',
inputAttributes: {
'aria-label': 'Catatan'
},
preConfirm: (value) => {
const revision = (value || '').trim();
if (!revision) {
Swal.showValidationMessage('Catatan revisi wajib diisi.');
}
return revision;
}
}).then((result) => {
if (!result.isConfirmed) return;
fetch(endpoint, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({ revision: result.value })
}).then(async(res) => {
const data = await res.json();
if (!res.ok || !data?.status) {
throw new Error(data?.message || 'Gagal reject.');
}
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: data.message || 'Dokumen ditolak.',
timer: 1500,
showConfirmButton: false
});
if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
fetchData(); fetchData();
}).catch((err) => { }).catch((err) => {
Swal.fire({ Swal.fire({
@ -572,7 +848,7 @@ document.addEventListener('DOMContentLoaded', () => {
timer: 1500, timer: 1500,
showConfirmButton: false showConfirmButton: false
}); });
countData(); if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
fetchData(); fetchData();
}).catch((err) => { }).catch((err) => {
Swal.fire({ Swal.fire({

File diff suppressed because it is too large Load Diff

View File

@ -103,188 +103,6 @@
"name": "TKRS 6.f" "name": "TKRS 6.f"
} }
] ]
},
{
"name": "TKRS 7",
"turunan": [
{
"name": "TKRS 7.a"
},
{
"name": "TKRS 7.b"
},
{
"name": "TKRS 7.c"
},
{
"name": "TKRS 7.d"
},
{
"name": "TKRS 7.e"
},
{
"name": "TKRS 7.f"
}
]
},
{
"name": "TKRS 7.1",
"turunan": [
{
"name": "TKRS 7.1.a"
},
{
"name": "TKRS 7.1.b"
},
{
"name": "TKRS 7.1.c"
},
{
"name": "TKRS 7.1.d"
}
]
},
{
"name": "TKRS 8",
"turunan": [
{
"name": "TKRS 8.a"
},
{
"name": "TKRS 8.b"
},
{
"name": "TKRS 8.c"
}
]
},
{
"name": "TKRS 9",
"turunan": [
{
"name": "TKRS 9.a"
},
{
"name": "TKRS 9.b"
},
{
"name": "TKRS 9.c"
},
{
"name": "TKRS 9.d"
},
{
"name": "TKRS 9.e"
}
]
},
{
"name": "TKRS 10",
"turunan": [
{
"name": "TKRS 10.a"
},
{
"name": "TKRS 10.b"
},
{
"name": "TKRS 10.c"
},
{
"name": "TKRS 10.d"
}
]
},
{
"name": "TKRS 11",
"turunan": [
{
"name": "TKRS 11.a"
},
{
"name": "TKRS 11.b"
},
{
"name": "TKRS 11.c"
}
]
},
{
"name": "TKRS 12",
"turunan": [
{
"name": "TKRS 12.a"
},
{
"name": "TKRS 12.b"
},
{
"name": "TKRS 12.c"
},
{
"name": "TKRS 12.d"
}
]
},
{
"name": "TKRS 13",
"turunan": [
{
"name": "TKRS 13.a"
},
{
"name": "TKRS 13.b"
},
{
"name": "TKRS 13.c"
},
{
"name": "TKRS 13.d"
},
{
"name": "TKRS 13.e"
},
{
"name": "TKRS 13.f"
}
]
},
{
"name": "TKRS 14",
"turunan": [
{
"name": "TKRS 14.a"
},
{
"name": "TKRS 14.b"
}
]
},
{
"name": "TKRS 15",
"turunan": [
{
"name": "TKRS 15.a"
},
{
"name": "TKRS 15.b"
},
{
"name": "TKRS 15.c"
},
{
"name": "TKRS 15.d"
},
{
"name": "TKRS 15.e"
},
{
"name": "TKRS 15.f"
},
{
"name": "TKRS 15.g"
}
]
} }
] ]
}, },

View File

@ -103,188 +103,6 @@
"name": "TKRS 6.f" "name": "TKRS 6.f"
} }
] ]
},
{
"name": "TKRS 7",
"turunan": [
{
"name": "TKRS 7.a"
},
{
"name": "TKRS 7.b"
},
{
"name": "TKRS 7.c"
},
{
"name": "TKRS 7.d"
},
{
"name": "TKRS 7.e"
},
{
"name": "TKRS 7.f"
}
]
},
{
"name": "TKRS 7.1",
"turunan": [
{
"name": "TKRS 7.1.a"
},
{
"name": "TKRS 7.1.b"
},
{
"name": "TKRS 7.1.c"
},
{
"name": "TKRS 7.1.d"
}
]
},
{
"name": "TKRS 8",
"turunan": [
{
"name": "TKRS 8.a"
},
{
"name": "TKRS 8.b"
},
{
"name": "TKRS 8.c"
}
]
},
{
"name": "TKRS 9",
"turunan": [
{
"name": "TKRS 9.a"
},
{
"name": "TKRS 9.b"
},
{
"name": "TKRS 9.c"
},
{
"name": "TKRS 9.d"
},
{
"name": "TKRS 9.e"
}
]
},
{
"name": "TKRS 10",
"turunan": [
{
"name": "TKRS 10.a"
},
{
"name": "TKRS 10.b"
},
{
"name": "TKRS 10.c"
},
{
"name": "TKRS 10.d"
}
]
},
{
"name": "TKRS 11",
"turunan": [
{
"name": "TKRS 11.a"
},
{
"name": "TKRS 11.b"
},
{
"name": "TKRS 11.c"
}
]
},
{
"name": "TKRS 12",
"turunan": [
{
"name": "TKRS 12.a"
},
{
"name": "TKRS 12.b"
},
{
"name": "TKRS 12.c"
},
{
"name": "TKRS 12.d"
}
]
},
{
"name": "TKRS 13",
"turunan": [
{
"name": "TKRS 13.a"
},
{
"name": "TKRS 13.b"
},
{
"name": "TKRS 13.c"
},
{
"name": "TKRS 13.d"
},
{
"name": "TKRS 13.e"
},
{
"name": "TKRS 13.f"
}
]
},
{
"name": "TKRS 14",
"turunan": [
{
"name": "TKRS 14.a"
},
{
"name": "TKRS 14.b"
}
]
},
{
"name": "TKRS 15",
"turunan": [
{
"name": "TKRS 15.a"
},
{
"name": "TKRS 15.b"
},
{
"name": "TKRS 15.c"
},
{
"name": "TKRS 15.d"
},
{
"name": "TKRS 15.e"
},
{
"name": "TKRS 15.f"
},
{
"name": "TKRS 15.g"
}
]
} }
] ]
}, },
@ -1480,7 +1298,7 @@
}, },
{ {
"name": "PPI 8.c" "name": "PPI 8.c"
} },
] ]
}, },
{ {
@ -3360,8 +3178,4 @@
} }
] ]
}, },
{
"name": "tst",
"segment": []
}
] ]

View File

@ -159,7 +159,11 @@
@csrf @csrf
@if (session()->has('alertError')) @if (session()->has('alertError'))
<div class="alert alert-danger fw-bold" role="alert"> <div class="alert alert-danger fw-bold" role="alert">
@if(session('alertError') === 'captcha') @if(session('alertError') === 'expired')
Sesi login sudah habis karena halaman terlalu lama terbuka. Silakan coba lagi.
@elseif(session('alertError') === 'expired-session')
Sesi Anda sudah habis. Silakan login kembali.
@elseif(session('alertError') === 'captcha')
Captcha salah! Captcha salah!
@elseif(session('alertError') === 'rate') @elseif(session('alertError') === 'rate')
Terlalu banyak percobaan login. Coba lagi dalam 1 menit. Terlalu banyak percobaan login. Coba lagi dalam 1 menit.
@ -177,7 +181,7 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="exampleInputEmail1" class="form-label">Username</label> <label for="exampleInputEmail1" class="form-label">Username</label>
<input type="text" name="namauser" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" required> <input type="text" name="namauser" value="{{ old('namauser') }}" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" required>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="exampleInputPassword1" class="form-label">Password</label> <label for="exampleInputPassword1" class="form-label">Password</label>

File diff suppressed because it is too large Load Diff

View File

@ -200,8 +200,8 @@
<span class="small text-muted">Tampilkan</span> <span class="small text-muted">Tampilkan</span>
<select id="tablePageSize" class="form-select form-select-sm" style="width: 80px;"> <select id="tablePageSize" class="form-select form-select-sm" style="width: 80px;">
<option value="5">5</option> <option value="5">5</option>
<option value="10" selected>10</option> <option value="10" >10</option>
<option value="20">20</option> <option value="20" selected>20</option>
<option value="50">50</option> <option value="50">50</option>
<option value="100">100</option> <option value="100">100</option>
</select> </select>
@ -226,7 +226,8 @@
<script> <script>
const katDok = @json($katDok); const katDok = @json($katDok);
const authUnitKerja = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]?->unitKerja); const authUnitKerja = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]?->unitKerja);
const authSubUnitKerja = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]->sub_unit_kerja);
// const authSubUnitKerja = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]->sub_unit_kerja);
const mappingUnitKerjaPegawai = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]); const mappingUnitKerjaPegawai = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]);
const authPegawai = @json(auth()->user()->objectpegawaifk); const authPegawai = @json(auth()->user()->objectpegawaifk);
const formCreate = $("#formFile") const formCreate = $("#formFile")
@ -407,6 +408,30 @@
return null; return null;
} }
function escapeHtml(value){
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function renderDeleteRecommendation(item){
const recommendation = item?.delete_recommendation;
if (!recommendation?.visible || !recommendation?.note) return '';
const actorRole = recommendation.actor_role || 'TURT';
const actorName = recommendation.actor_name ? ` (${escapeHtml(recommendation.actor_name)})` : '';
const note = escapeHtml(recommendation.note);
return `
<div class="mt-1 px-2 py-1 rounded border border-warning-subtle bg-warning-subtle text-warning-emphasis" style="font-size:12px; line-height:1.35;">
Dokumen ini direkomendasikan dihapus oleh ${escapeHtml(actorRole)}${actorName}. Catatan: ${note}
</div>
`;
}
function buildRow(item){ function buildRow(item){
const parts = (item.file || '').split('/'); const parts = (item.file || '').split('/');
const fileName = parts.pop() || '-'; const fileName = parts.pop() || '-';
@ -472,6 +497,22 @@
> >
<i class="fa-solid fa-download"></i> <i class="fa-solid fa-download"></i>
</a> </a>
${authUnitKerja?.id === 22 ? `
<button
type="button"
class="btn btn-warning btn-sm"
onclick="rekomendasiDelete(this)"
title="Rekomendasi Hapus"
data-filename="${item.nama_dokumen || '-'}"
data-id="${item.file_directory_id}"
data-no_dokumen="${item.no_dokumen || '-'}"
data-tanggal_terbit="${item.tanggal_terbit || '-'}"
data-permission_file="${item.permission_file || '-'}"
data-pegawai_id_entry="${item.pegawai_id_entry || '-'}"
>
<i class="fa-solid fa-trash-can"></i>
</button>
` : ''}
</div> </div>
</td> </td>
@ -503,6 +544,7 @@
${expiryBadge} ${expiryBadge}
</div> </div>
${renderDeleteRecommendation(item)}
</div> </div>
</td> </td>
@ -1176,5 +1218,67 @@
} }
$("#previewModal").modal('show') $("#previewModal").modal('show')
} }
function rekomendasiDelete(button){
const id = $(button).data('id');
const fileName = $(button).data('filename') || 'dokumen';
const noDokumen = $(button).data('no_dokumen') || '-';
Swal.fire({
title: 'Rekomendasi hapus dokumen?',
text: `${fileName} (${noDokumen})`,
icon: 'warning',
input: 'textarea',
inputLabel: 'Catatan rekomendasi',
inputPlaceholder: 'Tulis alasan kenapa dokumen ini direkomendasikan untuk dihapus...',
inputAttributes: {
'aria-label': 'Catatan rekomendasi'
},
showCancelButton: true,
confirmButtonText: 'Kirim rekomendasi',
cancelButtonText: 'Batal',
preConfirm: (value) => {
const note = (value || '').trim();
if (!note) {
Swal.showValidationMessage('Catatan rekomendasi wajib diisi.');
}
return note;
}
}).then((result) => {
if (!result.isConfirmed) return;
fetch(`/recommend-delete-file/${id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
note: result.value
})
})
.then(res => res.json())
.then(data => {
if (data.status) {
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: data.message || 'Rekomendasi hapus berhasil dikirim.',
timer: 1800,
showConfirmButton: false
});
} else {
throw new Error(data.message || 'Gagal mengirim rekomendasi hapus.');
}
})
.catch(err => {
Swal.fire({
icon: 'error',
title: 'Gagal',
text: err.message || 'Terjadi kesalahan.'
});
});
});
}
</script> </script>
@endsection @endsection

View File

@ -198,8 +198,8 @@
<span class="small text-muted">Tampilkan</span> <span class="small text-muted">Tampilkan</span>
<select id="tablePageSize" class="form-select form-select-sm" style="width: 80px;"> <select id="tablePageSize" class="form-select form-select-sm" style="width: 80px;">
<option value="5">5</option> <option value="5">5</option>
<option value="10" selected>10</option> <option value="10">10</option>
<option value="20">20</option> <option value="20" selected>20</option>
<option value="50">50</option> <option value="50">50</option>
<option value="100">100</option> <option value="100">100</option>
</select> </select>
@ -262,13 +262,117 @@
return [value]; return [value];
} }
function formatDateForInput(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function formatDateDisplay(dateValue) {
if (!dateValue) return '';
const date = new Date(`${dateValue}T00:00:00`);
if (Number.isNaN(date.getTime())) return '';
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}/${month}/${year}`;
}
function addYearsToDate(dateValue, years) {
if (!dateValue || !years) return '';
const baseDate = new Date(`${dateValue}T00:00:00`);
if (Number.isNaN(baseDate.getTime())) return '';
const originalDay = baseDate.getDate();
baseDate.setFullYear(baseDate.getFullYear() + Number(years));
if (baseDate.getDate() !== originalDay) {
baseDate.setDate(0);
}
return formatDateForInput(baseDate);
}
function syncMasaBerlakuField(selectEl) {
if (!selectEl) return;
const fieldWrap = document.getElementById(selectEl.dataset.dateTarget);
const input = document.getElementById(selectEl.dataset.inputTarget);
const preview = document.getElementById(selectEl.dataset.previewTarget);
const baseInput = document.getElementById(selectEl.dataset.baseTarget);
if (!fieldWrap || !input) return;
if (selectEl.value === 'custom') {
fieldWrap.classList.remove('d-none');
input.disabled = false;
if (preview) preview.textContent = '';
return;
}
fieldWrap.classList.add('d-none');
input.disabled = true;
input.value = '';
if (!preview) return;
if (!selectEl.value) {
preview.textContent = '';
return;
}
if (!baseInput || !baseInput.value) {
preview.textContent = 'Pilih Tanggal Terbit untuk melihat hasil masa berlaku.';
return;
}
const expiredDate = addYearsToDate(baseInput.value, selectEl.value);
preview.textContent = expiredDate
? `Tanggal kedaluwarsa: ${formatDateDisplay(expiredDate)}`
: '';
}
function prepareExpiryInputs(formEl) {
const selects = formEl.querySelectorAll('.masa-berlaku-select');
for (const selectEl of selects) {
const option = selectEl.value;
const dateInput = document.getElementById(selectEl.dataset.inputTarget);
const baseInput = document.getElementById(selectEl.dataset.baseTarget);
if (!dateInput) continue;
if (!option) {
dateInput.value = '';
dateInput.disabled = true;
continue;
}
if (option === 'custom') {
if (!dateInput.value) {
return 'Tanggal kedaluwarsa wajib diisi saat memilih opsi Lainnya.';
}
dateInput.disabled = false;
continue;
}
if (!baseInput || !baseInput.value) {
return 'Tanggal Terbit wajib diisi saat memilih masa berlaku 1, 2, atau 3 tahun.';
}
dateInput.value = addYearsToDate(baseInput.value, option);
dateInput.disabled = false;
}
return null;
}
document.addEventListener('change', function(e){ document.addEventListener('change', function(e){
if(!e.target.classList.contains('toggle-expired')) return; if(!e.target.classList.contains('masa-berlaku-select')) return;
const targetId = e.target.getAttribute('data-target'); syncMasaBerlakuField(e.target);
if(!targetId) return; });
const fieldWrap = document.getElementById(targetId);
const input = fieldWrap.querySelector('input'); document.addEventListener('change', function(e){
input.disabled = !e.target.checked; if (!e.target.matches('input[type="date"][id^="dateActive_"]')) return;
const index = e.target.id.replace('dateActive_', '');
const selectEl = document.querySelector(`.masa-berlaku-select[data-index="${index}"]`);
if (selectEl) syncMasaBerlakuField(selectEl);
}); });
if(pageSizeSelect){ if(pageSizeSelect){
@ -308,7 +412,7 @@
cache: true cache: true
} }
}); });
} }
if (kategoriSelect) { if (kategoriSelect) {
$('#tableKategori').select2({ $('#tableKategori').select2({
@ -349,6 +453,7 @@
formCreate.find('select').val(null).trigger('change'); formCreate.find('select').val(null).trigger('change');
formCreate.find('input[type="file"]').val(''); formCreate.find('input[type="file"]').val('');
formCreate.find('.file-name').addClass('d-none').text(''); formCreate.find('.file-name').addClass('d-none').text('');
document.querySelectorAll('.masa-berlaku-select').forEach(syncMasaBerlakuField);
resetAkreFields(0); resetAkreFields(0);
enableAkreFields(0); enableAkreFields(0);
} }
@ -544,6 +649,31 @@
return null; return null;
} }
function escapeHtml(value){
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function renderDeleteRecommendation(item){
const recommendation = item?.delete_recommendation;
if (!recommendation?.visible || !recommendation?.note) return '';
const actorRole = recommendation.actor_role || 'TURT';
const actorName = recommendation.actor_name ? ` (${escapeHtml(recommendation.actor_name)})` : '';
const note = escapeHtml(recommendation.note);
return `
<div class="mt-1 px-2 py-1 rounded border border-warning-subtle bg-warning-subtle text-warning-emphasis" style="font-size:12px; line-height:1.35;">
Dokumen ini direkomendasikan dihapus oleh ${escapeHtml(actorRole)}${actorName}. Catatan: ${note}
</div>
`;
}
function buildRow(item){ function buildRow(item){
const parts = (item.file || '').split('/'); const parts = (item.file || '').split('/');
const fileName = parts.pop() || '-'; const fileName = parts.pop() || '-';
@ -607,6 +737,23 @@
> >
<i class="fa-solid fa-download"></i> <i class="fa-solid fa-download"></i>
</a> </a>
${authUnitKerja?.id === 22 ? `
<button
type="button"
class="btn btn-warning btn-sm"
onclick="rekomendasiDelete(this)"
title="Rekomendasi Hapus"
data-filename="${item.nama_dokumen || '-'}"
data-id="${item.file_directory_id}"
data-no_dokumen="${item.no_dokumen || '-'}"
data-tanggal_terbit="${item.tanggal_terbit || '-'}"
data-permission_file="${item.permission_file || '-'}"
data-pegawai_id_entry="${item.pegawai_id_entry || '-'}"
>
<i class="fa-solid fa-trash-can"></i>
</button>
` : ''}
</div> </div>
</td> </td>
<td class="text-nowrap">${item.no_dokumen || '-'}</td> <td class="text-nowrap">${item.no_dokumen || '-'}</td>
@ -626,6 +773,7 @@
</a> </a>
${akreBadge}${expiryBadge} ${akreBadge}${expiryBadge}
</div> </div>
${renderDeleteRecommendation(item)}
</td> </td>
<td> <td>
${item.nama_kategori || '-'} ${item.nama_kategori || '-'}
@ -1011,6 +1159,7 @@
selectOptionUnitKerjaV1(0); selectOptionUnitKerjaV1(0);
initKategoriSelect2(0); initKategoriSelect2(0);
enableAkreFields(0); enableAkreFields(0);
document.querySelectorAll('.masa-berlaku-select').forEach(syncMasaBerlakuField);
}); });
function loadSubUnitKerja(unitId){ function loadSubUnitKerja(unitId){
@ -1020,6 +1169,8 @@
url: `/select-sub-unit-kerja-mapping/${unitId}`, url: `/select-sub-unit-kerja-mapping/${unitId}`,
method: 'GET', method: 'GET',
success: function(response) { success: function(response) {
console.log(response);
if (response?.data) { if (response?.data) {
response.data.forEach(unit => { response.data.forEach(unit => {
let selected = (authSubUnitKerja && unit.id === authSubUnitKerja.objectsubunitkerjapegawaifk); let selected = (authSubUnitKerja && unit.id === authSubUnitKerja.objectsubunitkerjapegawaifk);
@ -1089,28 +1240,39 @@
placeholder="Contoh: Panduan Mencuci Tangan" required> placeholder="Contoh: Panduan Mencuci Tangan" required>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Tanggal Terbit</label> <label class="form-label fw-semibold">Tanggal Terbit</label>
<input class="form-control" <input class="form-control"
type="date" type="date"
name="data[${colCount}][date_active]"> name="data[${colCount}][date_active]"
</div> id="dateActive_${colCount}">
<div class="col-md-3"> </div>
<div class="form-check"> <div class="col-md-4">
<input class="form-check-input toggle-expired" <label class="form-label fw-semibold">Masa Berlaku Dokumen</label>
type="checkbox" <select class="form-select masa-berlaku-select"
id="hasExpired_${colCount}" name="data[${colCount}][masa_berlaku_option]"
data-target="expiredField_${colCount}"> data-index="${colCount}"
<label class="form-check-label" for="hasExpired_${colCount}">Masa Berlaku Dokumen??</label> data-date-target="expiredField_${colCount}"
</div> data-input-target="expiredInput_${colCount}"
</div> data-base-target="dateActive_${colCount}"
data-preview-target="expiredPreview_${colCount}">
<option value="">Selamanya</option>
<option value="1">1 Tahun</option>
<option value="2">2 Tahun</option>
<option value="3">3 Tahun</option>
<option value="custom">Lainnya</option>
</select>
<div class="form-text text-muted">Opsi 1, 2, dan 3 tahun dihitung dari Tanggal Terbit.</div>
<div class="form-text text-primary" id="expiredPreview_${colCount}"></div>
</div>
<div class="col-md-5" id="expiredField_${colCount}"> <div class="col-md-4 d-none" id="expiredField_${colCount}">
<label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label> <label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label>
<input class="form-control" <input class="form-control"
type="date" type="date"
name="data[${colCount}][tgl_expired]" disabled> id="expiredInput_${colCount}"
</div> name="data[${colCount}][tgl_expired]" disabled>
</div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Boleh dilihat unit lain? <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Boleh dilihat unit lain? <span class="text-danger">*</span></label>
@ -1228,7 +1390,7 @@
processResults: function(data){ processResults: function(data){
return { return {
results : data?.data.map(item => ({ results : data?.data.map(item => ({
id: item.id+'/'+item.name, id: item.id,
text: item.name, text: item.name,
sub_units: item.sub_unit_kerja // kirim ke front sub_units: item.sub_unit_kerja // kirim ke front
})) }))
@ -1378,6 +1540,18 @@
e.preventDefault(); e.preventDefault();
const submitBtn = $(this).find('button[type="submit"]'); const submitBtn = $(this).find('button[type="submit"]');
submitBtn.prop('disabled', true).text('menyimpan...') submitBtn.prop('disabled', true).text('menyimpan...')
const expiryError = prepareExpiryInputs(this);
if (expiryError) {
Swal.fire({
icon: 'warning',
title: 'Validasi form',
text: expiryError
});
submitBtn.prop('disabled', false).text('Simpan');
return;
}
const formData = new FormData(this); const formData = new FormData(this);
console.log(formData); console.log(formData);
@ -1482,5 +1656,67 @@
} }
$("#previewModal").modal('show') $("#previewModal").modal('show')
} }
function rekomendasiDelete(button){
const id = $(button).data('id');
const fileName = $(button).data('filename') || 'dokumen';
const noDokumen = $(button).data('no_dokumen') || '-';
Swal.fire({
title: 'Rekomendasi hapus dokumen?',
text: `${fileName} (${noDokumen})`,
icon: 'warning',
input: 'textarea',
inputLabel: 'Catatan rekomendasi',
inputPlaceholder: 'Tulis alasan kenapa dokumen ini direkomendasikan untuk dihapus...',
inputAttributes: {
'aria-label': 'Catatan rekomendasi'
},
showCancelButton: true,
confirmButtonText: 'Kirim rekomendasi',
cancelButtonText: 'Batal',
preConfirm: (value) => {
const note = (value || '').trim();
if (!note) {
Swal.showValidationMessage('Catatan rekomendasi wajib diisi.');
}
return note;
}
}).then((result) => {
if (!result.isConfirmed) return;
fetch(`/recommend-delete-file/${id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
note: result.value
})
})
.then(res => res.json())
.then(data => {
if (data.status) {
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: data.message || 'Rekomendasi hapus berhasil dikirim.',
timer: 1800,
showConfirmButton: false
});
} else {
throw new Error(data.message || 'Gagal mengirim rekomendasi hapus.');
}
})
.catch(err => {
Swal.fire({
icon: 'error',
title: 'Gagal',
text: err.message || 'Terjadi kesalahan.'
});
});
});
}
</script> </script>
@endsection @endsection

View File

@ -46,17 +46,29 @@
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Tanggal Terbit</label> <label class="form-label fw-semibold">Tanggal Terbit</label>
<input class="form-control" type="date" name="data[0][date_active]"> <input class="form-control" type="date" name="data[0][date_active]" id="dateActive_0">
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-check"> <label class="form-label fw-semibold">Masa Berlaku Dokumen</label>
<input class="form-check-input toggle-expired" type="checkbox" id="hasExpired0" data-target="expiredField_0"> <select class="form-select masa-berlaku-select"
<label class="form-check-label" for="hasExpired0">Masa Berlaku Dokumen?</label> name="data[0][masa_berlaku_option]"
</div> data-index="0"
data-date-target="expiredField_0"
data-input-target="expiredInput_0"
data-base-target="dateActive_0"
data-preview-target="expiredPreview_0">
<option value="">Selamanya</option>
<option value="1">1 Tahun</option>
<option value="2">2 Tahun</option>
<option value="3">3 Tahun</option>
<option value="custom">Lainnya</option>
</select>
<div class="form-text text-muted">Opsi 1, 2, dan 3 tahun dihitung dari Tanggal Terbit.</div>
<div class="form-text text-primary" id="expiredPreview_0"></div>
</div> </div>
<div class="col-md-4" id="expiredField_0"> <div class="col-md-4 d-none" id="expiredField_0">
<label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label> <label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label>
<input class="form-control" type="date" name="data[0][tgl_expired]" disabled> <input class="form-control" type="date" name="data[0][tgl_expired]" id="expiredInput_0" disabled>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Boleh dilihat unit lain? <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Boleh dilihat unit lain? <span class="text-danger">*</span></label>

View File

@ -21,6 +21,55 @@
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
<style> <style>
html,
body,
.body-wrapper,
.body-wrapper-inner,
.container-fluid {
scroll-behavior: smooth;
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.5) transparent;
}
html::-webkit-scrollbar,
body::-webkit-scrollbar,
.body-wrapper::-webkit-scrollbar,
.body-wrapper-inner::-webkit-scrollbar,
.container-fluid::-webkit-scrollbar {
width: 6px;
height: 6px;
}
html::-webkit-scrollbar-track,
body::-webkit-scrollbar-track,
.body-wrapper::-webkit-scrollbar-track,
.body-wrapper-inner::-webkit-scrollbar-track,
.container-fluid::-webkit-scrollbar-track {
background: transparent;
border-radius: 999px;
}
html::-webkit-scrollbar-thumb,
body::-webkit-scrollbar-thumb,
.body-wrapper::-webkit-scrollbar-thumb,
.body-wrapper-inner::-webkit-scrollbar-thumb,
.container-fluid::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.5);
border-radius: 999px;
border: 1px solid transparent;
background-clip: padding-box;
}
html::-webkit-scrollbar-thumb:hover,
body::-webkit-scrollbar-thumb:hover,
.body-wrapper::-webkit-scrollbar-thumb:hover,
.body-wrapper-inner::-webkit-scrollbar-thumb:hover,
.container-fluid::-webkit-scrollbar-thumb:hover {
background: rgba(100, 116, 139, 0.68);
border: 1px solid transparent;
background-clip: padding-box;
}
.modal, .modal,
.modal-content, .modal-content,
.modal-body, .modal-body,

View File

@ -2,5 +2,5 @@
<p class="mb-0 fs-4"> © <p class="mb-0 fs-4"> ©
<script> <script>
document.write(new Date().getFullYear()); document.write(new Date().getFullYear());
</script> made with by <a href="/" target="_blank" >TIM SIRS</a></p> </script> made with by <a href="/" target="_blank" >TIM SIMRS</a></p>
</div> </div>

View File

@ -52,7 +52,25 @@
{{-- AKTIVITAS --}} {{-- AKTIVITAS --}}
<li class="nav-small-cap"><span class="hide-menu">Aktivitas</span></li> <li class="nav-small-cap"><span class="hide-menu">Aktivitas</span></li>
@php @php
$isAtasan = \App\Models\MappingUnitKerjaPegawai::where('statusenabled', true)->where('objectatasanlangsungfk', auth()->user()->objectpegawaifk)->exists(); $pegawaiId = auth()->user()->objectpegawaifk;
$userUnitIds = auth()->user()->dataUser?->mappingUnitKerjaPegawai()
->where('statusenabled', true)
->pluck('objectunitkerjapegawaifk')
->unique()
->values()
->all();
if($userUnitIds){
$isAtasan = \App\Models\MappingUnitKerjaPegawai::where('statusenabled', true)
->where(function ($q) use ($pegawaiId) {
$q->where('objectatasanlangsungfk', $pegawaiId)
->orWhere('objectpejabatpenilaifk', $pegawaiId);
})->exists();
$isRoleApprover = in_array(51, $userUnitIds, true) || in_array(22, $userUnitIds, true);
}else{
$isAtasan = null;
}
@endphp @endphp
@if($isAtasan) @if($isAtasan)
@if(!Auth::guard('admin')->check()) @if(!Auth::guard('admin')->check())
@ -79,6 +97,10 @@
<i class="ti ti-clock"></i> <i class="ti ti-clock"></i>
<span class="hide-menu">Pengajuan</span> <span class="hide-menu">Pengajuan</span>
</div> </div>
@if($isRoleApprover)
<span class="badge bg-danger rounded-pill d-none" id="pengajuanPendingCountBadge">0</span>
@endif
</a> </a>
</li> </li>
@endif @endif
@ -114,7 +136,7 @@
</a> </a>
</li> </li>
@if(!Auth::guard('admin')->check()) @if(!Auth::guard('admin')->check())
@if(auth()->user()->dataUser->mappingUnitKerjaPegawai()->where('objectunitkerjapegawaifk', 43)->exists()) @if(auth()->user()->dataUser->mappingUnitKerjaPegawai()->whereIn('objectunitkerjapegawaifk', [43, 22])->exists())
<li class="nav-small-cap"><span class="hide-menu">Master</span></li> <li class="nav-small-cap"><span class="hide-menu">Master</span></li>
<li class="sidebar-item has-sub {{ $openMaster ? 'open' : '' }}"> <li class="sidebar-item has-sub {{ $openMaster ? 'open' : '' }}">
@ -192,10 +214,11 @@
</style> </style>
<script> <script>
const badge = document.getElementById('pendingCountBadge'); const pendingBadge = document.getElementById('pendingCountBadge');
const pengajuanBadge = document.getElementById('pengajuanPendingCountBadge');
async function countData() { async function countData() {
if (!badge) return; if (!pendingBadge && !pengajuanBadge) return;
try { try {
const res = await fetch('/data/count-pending', { const res = await fetch('/data/count-pending', {
@ -207,14 +230,24 @@
const data = await res.json(); const data = await res.json();
const count = Number(data?.count ?? 0); const count = Number(data?.count ?? 0);
badge.textContent = count; if (pendingBadge) {
badge.classList.toggle('d-none', count <= 0); pendingBadge.textContent = count;
pendingBadge.classList.toggle('d-none', count <= 0);
}
if (pengajuanBadge) {
pengajuanBadge.textContent = count;
pengajuanBadge.classList.toggle('d-none', count <= 0);
}
} catch (e) { } catch (e) {
badge.classList.add('d-none'); if (pendingBadge) pendingBadge.classList.add('d-none');
if (pengajuanBadge) pengajuanBadge.classList.add('d-none');
} }
} }
window.refreshPendingCount = countData;
countData(); countData();
// setInterval(countData, 60000); // setInterval(countData, 60000);

View File

@ -1,3 +1,22 @@
{{-- @php
$globalUpdateItems = [
[
'date' => '29 Juni 2026',
'title' => 'Metadata preview dokumen diperluas',
'description' => 'Modal preview sekarang menampilkan nomor dokumen, tanggal terbit, tanggal kedaluwarsa, tanggal upload, akses dokumen, jenis dokumen akreditasi/non-akreditasi, dan kategori.',
],
[
'date' => '29 Juni 2026',
'title' => 'Nama unit ditampilkan lebih jelas',
'description' => 'Data persetujuan dan pengajuan file sekarang memakai nama unit langsung dari tabel unit agar lebih mudah dibaca.',
],
[
'date' => '29 Juni 2026',
'title' => 'Perbaikan query persetujuan',
'description' => 'Query halaman persetujuan dan pengajuan file dirapikan supaya aman saat join tabel dan tidak gagal karena kolom ambigu.',
],
];
@endphp --}}
<style> <style>
.message-body { .message-body {
@ -57,7 +76,12 @@
</li> </li>
</ul> </ul>
<div class="navbar-collapse justify-content-end px-0" id="navbarNav"> <div class="navbar-collapse justify-content-end px-0" id="navbarNav">
<ul class="navbar-nav flex-row ms-auto align-items-center justify-content-end"> <ul class="navbar-nav flex-row ms-auto align-items-center justify-content-end">
{{-- <li class="nav-item me-2">
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#updatesInfoModal">
<i class="fa-solid fa-bullhorn me-1"></i> Info Update
</button>
</li> --}}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link position-relative" href="javascript:void(0)" id="dropExpired" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link position-relative" href="javascript:void(0)" id="dropExpired" data-bs-toggle="dropdown" aria-expanded="false">
<span class="d-inline-flex align-items-center justify-content-center rounded-circle bg-light" style="width:46px;height:46px;"> <span class="d-inline-flex align-items-center justify-content-center rounded-circle bg-light" style="width:46px;height:46px;">
@ -177,6 +201,33 @@
</div> </div>
</div> </div>
</div> </div>
{{-- <div class="modal fade" id="updatesInfoModal" tabindex="-1" aria-labelledby="updatesInfoModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="updatesInfoModalLabel">Info Update Terbaru</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="d-flex flex-column gap-3">
@foreach ($globalUpdateItems as $update)
<div class="border rounded p-3">
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2 mb-2">
<h6 class="mb-0">{{ $update['title'] }}</h6>
<span class="badge bg-light text-dark border">{{ $update['date'] }}</span>
</div>
<div class="text-muted small">{{ $update['description'] }}</div>
</div>
@endforeach
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Tutup</button>
</div>
</div>
</div>
</div> --}}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ''; const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';

View File

@ -1,6 +1,5 @@
@extends('layout.main') @extends('layout.main')
@section('body_main') @section('body_main')
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div class="card"> <div class="card">
@ -58,7 +57,7 @@
<div class="small text-muted ms-md-auto" id="tableSummary"></div> <div class="small text-muted ms-md-auto" id="tableSummary"></div>
</div> </div>
<div id="tabPengajuan"> <div id="tabPengajuan">
<div class="table-responsive" style="max-height: 55vh; overflow-y:auto; overflow-x:auto;"> <div class="table-responsive" style="max-height: 75vh; overflow-y:auto; overflow-x:auto;">
<table class="table table-sm table-hover table-striped align-middle mb-0 pending-table" id="lastUpdatedTable"> <table class="table table-sm table-hover table-striped align-middle mb-0 pending-table" id="lastUpdatedTable">
<thead> <thead>
<tr> <tr>
@ -96,9 +95,7 @@
<th>Kategori</th> <th>Kategori</th>
<th>Unit</th> <th>Unit</th>
<th>Aksi</th> <th>Aksi</th>
<th>Tanggal Terbit</th> <th>Tanggal Aksi</th>
<th>Tanggal Kedaluwarsa Dokumen</th>
<th>Tanggal Upload</th>
</tr> </tr>
</thead> </thead>
<tbody id="tableHistoryFile"> <tbody id="tableHistoryFile">
@ -114,5 +111,9 @@
</div> </div>
</div> </div>
@include('pendingFile.modal.view') @include('pendingFile.modal.view')
<script>
window.isKomiteMutu = @json($isKomiteMutu);
window.isTurt = @json($isTurt);
</script>
<script src="{{ ver('/js/pendingFile/index.js') }}"></script> <script src="{{ ver('/js/pendingFile/index.js') }}"></script>
@endsection @endsection

View File

@ -13,6 +13,9 @@
<!-- Body --> <!-- Body -->
<div class="modal-body p-2" style="min-height:250px; max-height:70vh; overflow:auto;"> <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 d-none" id="deleteData">
<button type="button" class="btn btn-sm btn-outline-danger mb-2 me-2" id="delete-file">Hapus</button>
</div>
<div id="file-preview" <div id="file-preview"
class="text-center text-muted d-flex justify-content-center align-items-center" class="text-center text-muted d-flex justify-content-center align-items-center"
style="height:100%;"> style="height:100%;">

View File

@ -46,10 +46,26 @@
<li class="nav-item"> <li class="nav-item">
<button class="nav-link active" type="button" data-mode="pengajuan">Data Pengajuan</button> <button class="nav-link active" type="button" data-mode="pengajuan">Data Pengajuan</button>
</li> </li>
@if($isKomiteMutu || $isTurt || $isAtasan)
<li class="nav-item">
<button class="nav-link d-flex align-items-center gap-2" type="button" data-mode="persetujuan">
<span>Data Persetujuan</span>
<span class="badge bg-danger rounded-pill d-none" id="pendingPersetujuanTabBadge">0</span>
</button>
</li>
@endif
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" type="button" data-mode="history">Log History</button> <button class="nav-link" type="button" data-mode="history">Log History</button>
</li> </li>
</ul> </ul>
@if(($isKomiteMutu || $isTurt) && !$isAtasan)
<div class="alert alert-warning d-none align-items-center justify-content-between gap-3 py-2" id="pendingPersetujuanNotice" role="alert">
<div>
Ada <strong id="pendingPersetujuanNoticeCount">0</strong> data yang menunggu persetujuan Anda.
</div>
<a href="{{ url('/pending-file') }}" class="btn btn-sm btn-outline-warning">Lihat Persetujuan</a>
</div>
@endif
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3 flex-wrap"> <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;"> <div class="input-group input-group-sm flex-grow-1" style="max-width:320px;">
<span class="input-group-text bg-white border-end-0"> <span class="input-group-text bg-white border-end-0">
@ -78,6 +94,14 @@
<option value="100">100</option> <option value="100">100</option>
</select> </select>
</div> </div>
<div class="d-flex align-items-center gap-2" id="pengajuanBulkActions">
<button class="btn btn-danger btn-sm" id="bulkDeleteBtn" disabled>
Hapus dipilih (<span id="selectedCount">0</span>)
</button>
<button class="btn btn-outline-secondary btn-sm" id="clearSelectionBtn" disabled>
Reset pilihan
</button>
</div>
<button class="btn btn-outline-secondary btn-sm" onclick="refreshLog()"> <button class="btn btn-outline-secondary btn-sm" onclick="refreshLog()">
<i class="fa fa-rotate"></i> Refresh <i class="fa fa-rotate"></i> Refresh
</button> </button>
@ -88,6 +112,9 @@
<table class="table table-sm table-hover align-middle mb-0" id="tablePengajuan"> <table class="table table-sm table-hover align-middle mb-0" id="tablePengajuan">
<thead> <thead>
<tr> <tr>
<th class="text-center">
<input type="checkbox" class="form-check-input" id="selectAllPengajuan" title="Pilih semua di halaman">
</th>
<th>Aksi</th> <th>Aksi</th>
<th>No Dokumen</th> <th>No Dokumen</th>
<th>Status</th> <th>Status</th>
@ -95,8 +122,6 @@
<th>Nama</th> <th>Nama</th>
<th>Kategori</th> <th>Kategori</th>
<th>Unit</th> <th>Unit</th>
<th>Tanggal Terbit</th>
<th>Tanggal Kedaluwarsa Dokumen</th>
<th>Tanggal Upload</th> <th>Tanggal Upload</th>
</tr> </tr>
</thead> </thead>
@ -108,7 +133,7 @@
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2 mt-3" id="paginationPengajuan"></div> <div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2 mt-3" id="paginationPengajuan"></div>
</div> </div>
<div id="tabHistory" class="d-none"> <div id="tabHistory" class="d-none">
<div class="table-responsive" style="max-height: 55vh; overflow-y:auto;"> <div class="table-responsive" style="max-height: 75vh; overflow-y:auto;">
<table class="table table-sm table-hover align-middle mb-0" id="tableHistory"> <table class="table table-sm table-hover align-middle mb-0" id="tableHistory">
<thead> <thead>
<tr> <tr>
@ -118,9 +143,7 @@
<th>Kategori</th> <th>Kategori</th>
<th>Unit</th> <th>Unit</th>
<th>Aksi</th> <th>Aksi</th>
<th>Tanggal Terbit</th> <th>Tanggal Aksi</th>
<th>Tanggal Kedaluwarsa Dokumen</th>
<th>Tanggal Upload</th>
</tr> </tr>
</thead> </thead>
<tbody id="tableHistoryFile"> <tbody id="tableHistoryFile">
@ -140,6 +163,9 @@
@include('dataUnit.modal.create') @include('dataUnit.modal.create')
<script> <script>
window.katDok = @json($katDok); window.katDok = @json($katDok);
window.isKomiteMutu = @json($isKomiteMutu);
window.isTurt = @json($isTurt);
window.isAtasan = @json($isAtasan);
</script> </script>
<script src="{{ ver('/js/pengajuanFile/index.js') }}"></script> <script src="{{ ver('/js/pengajuanFile/index.js') }}"></script>
@endsection @endsection

View File

@ -47,12 +47,25 @@
<input class="form-control" type="date" name="tanggal_terbit" id="edit_tanggal_terbit"> <input class="form-control" type="date" name="tanggal_terbit" id="edit_tanggal_terbit">
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-check"> <label class="form-label fw-semibold">Masa Berlaku Dokumen</label>
<input class="form-check-input toggle-expired" type="checkbox" id="edit_has_expired" data-target="edit_expired_field"> <select class="form-select masa-berlaku-select"
<label class="form-check-label" for="edit_has_expired">Masa Berlaku Dokumen?</label> name="masa_berlaku_option"
</div> id="edit_masa_berlaku_option"
data-index="edit"
data-date-target="edit_expired_field"
data-input-target="edit_tgl_expired"
data-base-target="edit_tanggal_terbit"
data-preview-target="edit_expired_preview">
<option value="">Selamanya</option>
<option value="1">1 Tahun</option>
<option value="2">2 Tahun</option>
<option value="3">3 Tahun</option>
<option value="custom">Lainnya</option>
</select>
<div class="form-text text-muted">Opsi 1, 2, dan 3 tahun dihitung dari Tanggal Terbit.</div>
<div class="form-text text-primary" id="edit_expired_preview"></div>
</div> </div>
<div class="col-md-4" id="edit_expired_field"> <div class="col-md-4 d-none" id="edit_expired_field">
<label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label> <label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label>
<input class="form-control" type="date" name="tgl_expired" id="edit_tgl_expired" disabled> <input class="form-control" type="date" name="tgl_expired" id="edit_tgl_expired" disabled>
</div> </div>

View File

@ -1,7 +1,7 @@
<?php <?php
use App\Http\Controllers\AksesFileController; use App\Http\Controllers\AksesFileController;
use App\Http\Controllers\AkreditasiInstrumenController; use App\Http\Controllers\AkreditasiInstrumenController;
use App\Http\Controllers\AuthController; use App\Http\Controllers\AuthController;
use App\Http\Controllers\DashboardController; use App\Http\Controllers\DashboardController;
use App\Http\Controllers\MasterKategoriController; use App\Http\Controllers\MasterKategoriController;
@ -19,12 +19,10 @@ Route::middleware(['auth:admin,web'])->group(function(){
Route::get('/datatable-umum', [DashboardController::class, 'datatableDataUmum']); Route::get('/datatable-umum', [DashboardController::class, 'datatableDataUmum']);
Route::get('/data-akreditasi', [DashboardController::class, 'dataAkreditasi']); Route::get('/data-akreditasi', [DashboardController::class, 'dataAkreditasi']);
Route::get('/datatable-akreditasi', [DashboardController::class, 'dataTableAkreditasi']); Route::get('/datatable-akreditasi', [DashboardController::class, 'dataTableAkreditasi']);
Route::get('/data-akreditasi/recap', [DashboardController::class, 'dataAkreditasiRecap']);
// Kelola "folder/instrumen" akreditasi via file: public/json/akreditasi.jff (tanpa database) // Kelola "folder/instrumen" akreditasi via file: public/json/akreditasi.jff (tanpa database)
Route::get('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'index']); Route::get('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'index']);
Route::post('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'store']); Route::post('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'store']);
Route::delete('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'destroy']);
Route::get('/download-excel/data-umum', [DashboardController::class, 'downloadDataUmumExcel']); Route::get('/download-excel/data-umum', [DashboardController::class, 'downloadDataUmumExcel']);
Route::post('/uploadv2', [DashboardController::class, 'storeVersion2']); Route::post('/uploadv2', [DashboardController::class, 'storeVersion2']);
Route::get('/file-preview/{id}', [DashboardController::class, 'dataPdf']); Route::get('/file-preview/{id}', [DashboardController::class, 'dataPdf']);
@ -53,7 +51,9 @@ Route::middleware(['auth:admin,web'])->group(function(){
Route::get('/select-sub-unit-kerja-mapping/{id}', [DashboardController::class, 'optionSubUnitKerjaByMapping']); Route::get('/select-sub-unit-kerja-mapping/{id}', [DashboardController::class, 'optionSubUnitKerjaByMapping']);
Route::delete('/delete-file/bulk', [DashboardController::class, 'deleteFiles']);
Route::delete('/delete-file/{id}', [DashboardController::class, 'deleteFile']); Route::delete('/delete-file/{id}', [DashboardController::class, 'deleteFile']);
Route::post('/recommend-delete-file/{id}', [DashboardController::class, 'recommendDeleteFile']);
// Route::get('/getFile/{id_unit_kerja}/{id_sub_unit_kerja}/{master_kategori_directory_id}', [DashboardController::class, 'getFile']); // Route::get('/getFile/{id_unit_kerja}/{id_sub_unit_kerja}/{master_kategori_directory_id}', [DashboardController::class, 'getFile']);
Route::post('/download-multiple', [DashboardController::class, 'downloadDataMultiple']); Route::post('/download-multiple', [DashboardController::class, 'downloadDataMultiple']);
@ -71,6 +71,10 @@ Route::middleware(['auth:admin,web'])->group(function(){
Route::get('/pengajuan-file', [DashboardController::class, 'pengajuanFile']); Route::get('/pengajuan-file', [DashboardController::class, 'pengajuanFile']);
Route::get('/datatable/pengajuan-file', [DashboardController::class, 'dataPengajuanFile']); Route::get('/datatable/pengajuan-file', [DashboardController::class, 'dataPengajuanFile']);
Route::post('/pengajuan-file/{id}/update', [DashboardController::class, 'updatePengajuanFile']); Route::post('/pengajuan-file/{id}/update', [DashboardController::class, 'updatePengajuanFile']);
Route::post('/pengajuan-file/{id}/approve-mutu', [DashboardController::class, 'approvePendingFileMutu']);
Route::post('/pengajuan-file/{id}/approve-turt', [DashboardController::class, 'approvePendingFileTurt']);
Route::post('/pengajuan-file/{id}/reject-mutu', [DashboardController::class, 'rejectPendingFileMutu']);
Route::post('/pengajuan-file/{id}/reject-turt', [DashboardController::class, 'rejectPendingFileTurt']);
// Route::middleware(['master.persetujuan'])->group(function () { // Route::middleware(['master.persetujuan'])->group(function () {
Route::get('/pending-file', [DashboardController::class, 'pendingFile']); Route::get('/pending-file', [DashboardController::class, 'pendingFile']);
@ -78,6 +82,10 @@ Route::middleware(['auth:admin,web'])->group(function(){
Route::post('/pending-file/{id}/approve', [DashboardController::class, 'approvePendingFile']); Route::post('/pending-file/{id}/approve', [DashboardController::class, 'approvePendingFile']);
Route::post('/pending-file/approve-multiple', [DashboardController::class, 'approvePendingFileMultiple']); Route::post('/pending-file/approve-multiple', [DashboardController::class, 'approvePendingFileMultiple']);
Route::post('/pending-file/{id}/reject', [DashboardController::class, 'rejectPendingFile']); Route::post('/pending-file/{id}/reject', [DashboardController::class, 'rejectPendingFile']);
Route::post('/pending-file/{id}/approve-mutu', [DashboardController::class, 'approvePendingFileMutu']);
Route::post('/pending-file/{id}/approve-turt', [DashboardController::class, 'approvePendingFileTurt']);
Route::post('/pending-file/{id}/reject-mutu', [DashboardController::class, 'rejectPendingFileMutu']);
Route::post('/pending-file/{id}/reject-turt', [DashboardController::class, 'rejectPendingFileTurt']);
Route::get('/data/count-pending', [DashboardController::class, 'countDataPending']); Route::get('/data/count-pending', [DashboardController::class, 'countDataPending']);
Route::get('/data/count-rejected', [DashboardController::class, 'countRejectedPengajuan']); Route::get('/data/count-rejected', [DashboardController::class, 'countRejectedPengajuan']);
// }); // });