Compare commits

..

No commits in common. "main" and "laptopkosan" have entirely different histories.

41 changed files with 3494 additions and 16313 deletions

View File

@ -1,316 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class AkreditasiInstrumenController extends Controller
{
private function akreditasiJsonPath(): string
{
return public_path('json/akreditasi.jff');
}
private function akreditasiLockPath(): string
{
return public_path('json/akreditasi.jff.lock');
}
private function ensureJsonDirExists(): void
{
$dir = public_path('json');
if (!is_dir($dir)) {
@mkdir($dir, 0777, true);
}
}
private function atomicWrite(string $path, string $contents): bool
{
$dir = dirname($path);
if (!is_dir($dir)) {
@mkdir($dir, 0777, true);
}
$tmp = $path . '.tmp';
$bytes = @file_put_contents($tmp, $contents);
if ($bytes === false || $bytes < strlen($contents)) {
@unlink($tmp);
return false;
}
// best-effort backup
if (file_exists($path)) {
@copy($path, $path . '.bak');
}
// replace
@unlink($path);
if (@rename($tmp, $path)) {
return true;
}
// fallback copy
$copied = @copy($tmp, $path);
@unlink($tmp);
return (bool) $copied;
}
private function stripUtf8Bom(string $raw): string
{
// Remove UTF-8 BOM if present
if (strncmp($raw, "\xEF\xBB\xBF", 3) === 0) {
return substr($raw, 3);
}
return $raw;
}
private function decodeJsonArray(string $raw, bool $strict = false): ?array
{
$raw = $this->stripUtf8Bom($raw);
$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return $strict ? null : [];
}
return is_array($data) ? $data : ($strict ? null : []);
}
private function tryRepairJsonArray(string $raw): ?array
{
// Common corruption: trailing commas before ']' or '}'
$raw = $this->stripUtf8Bom($raw);
$repaired = preg_replace('/,(\s*[}\]])/m', '$1', $raw);
if (!is_string($repaired) || $repaired === '') {
return null;
}
$data = json_decode($repaired, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return null;
}
return is_array($data) ? $data : null;
}
private function splitList(?string $value): array
{
$value = trim((string) $value);
if ($value === '') return [];
$parts = preg_split('/[,\r\n]+/', $value) ?: [];
$out = [];
foreach ($parts as $p) {
$p = trim($p);
if ($p !== '') $out[] = $p;
}
return array_values(array_unique($out));
}
private function ensureTypeExists(array &$data, string $typeName): int
{
foreach ($data as $i => $t) {
if (($t['name'] ?? null) === $typeName) {
if (!isset($data[$i]['segment']) || !is_array($data[$i]['segment'])) {
$data[$i]['segment'] = [];
}
return $i;
}
}
$data[] = [
'name' => $typeName,
'segment' => [],
];
return count($data) - 1;
}
private function ensureSegmentExists(array &$typeSegments, string $segmentName): int
{
foreach ($typeSegments as $j => $s) {
if (($s['name'] ?? null) === $segmentName) {
if (!isset($typeSegments[$j]['turunan']) || !is_array($typeSegments[$j]['turunan'])) {
$typeSegments[$j]['turunan'] = [];
}
return $j;
}
}
$typeSegments[] = [
'name' => $segmentName,
'turunan' => [],
];
return count($typeSegments) - 1;
}
private function ensureItemExists(array &$segmentChildren, string $itemName): bool
{
foreach ($segmentChildren as $c) {
if (($c['name'] ?? null) === $itemName) {
return false; // already exists
}
}
$segmentChildren[] = ['name' => $itemName];
return true;
}
/**
* Read akreditasi.jff (array)
*/
public function index()
{
$path = $this->akreditasiJsonPath();
if (!file_exists($path)) {
return response()->json([]);
}
$raw = @file_get_contents($path);
$data = $this->decodeJsonArray($raw === false ? '[]' : $raw, false) ?? [];
return response()->json($data);
}
/**
* Add "folder" structure to akreditasi.jff.
*
* Payload:
* - type (required)
* - segment (optional)
* - item (optional)
*
* Behavior:
* - type only: creates new top-level type {name, segment: []}
* - type + segment: creates segment {name, turunan: []} under type
* - type + segment + item: creates item {name} under segment.turunan
*/
public function store(Request $request)
{
$validated = $request->validate([
'type' => ['required', 'string', 'max:255'],
'segment' => ['nullable', 'string', 'max:255'],
'item' => ['nullable', 'string', 'max:2000'],
]);
$typeName = trim($validated['type'] ?? '');
$segmentRaw = isset($validated['segment']) ? (string) $validated['segment'] : '';
$itemRaw = isset($validated['item']) ? (string) $validated['item'] : '';
$segments = $this->splitList($segmentRaw);
$items = $this->splitList($itemRaw);
if ($typeName === '') {
return response()->json([
'status' => false,
'message' => 'Type wajib diisi.',
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
if (count($items) > 0 && count($segments) === 0) {
return response()->json([
'status' => false,
'message' => 'Segment wajib diisi jika ingin menambah item.',
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
$this->ensureJsonDirExists();
$path = $this->akreditasiJsonPath();
$lockPath = $this->akreditasiLockPath();
// Use separate lock file so Windows can replace the target JSON file safely.
$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) {
// Try safe repair (e.g., remove trailing commas). If still invalid, abort to avoid data loss.
$repaired = $this->tryRepairJsonArray($raw);
if ($repaired === null) {
return response()->json([
'status' => false,
'message' => 'File public/json/akreditasi.jff tidak valid (JSON rusak). Perubahan dibatalkan agar data tidak hilang. Silakan restore dari akreditasi.jff.bak lalu coba lagi.',
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
$data = $repaired;
}
}
$typeIndex = $this->ensureTypeExists($data, $typeName);
$added = 0;
$skipped = 0;
// Case: type only
if (count($segments) === 0) {
// type already ensured; treat as skipped if it existed, added if new.
// We can't easily detect "new" without extra flag; keep message generic.
} elseif (count($items) === 0) {
// Case: add one or more segments
$typeSegments = $data[$typeIndex]['segment'];
foreach ($segments as $segName) {
$beforeCount = count($typeSegments);
$this->ensureSegmentExists($typeSegments, $segName);
$afterCount = count($typeSegments);
if ($afterCount > $beforeCount) $added++;
else $skipped++;
}
$data[$typeIndex]['segment'] = $typeSegments;
} else {
// Case: add one or more items under the given segment(s)
// If user provides multiple segments + items, add all items to each segment.
$typeSegments = $data[$typeIndex]['segment'];
foreach ($segments as $segName) {
$segIndex = $this->ensureSegmentExists($typeSegments, $segName);
$children = $typeSegments[$segIndex]['turunan'] ?? [];
if (!is_array($children)) $children = [];
foreach ($items as $itemName) {
$didAdd = $this->ensureItemExists($children, $itemName);
if ($didAdd) $added++;
else $skipped++;
}
$typeSegments[$segIndex]['turunan'] = $children;
}
$data[$typeIndex]['segment'] = $typeSegments;
}
// write back
$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' => ($added > 0 || $skipped > 0)
? "Selesai. Ditambahkan: {$added}, dilewati (sudah ada): {$skipped}."
: 'Selesai.',
'data' => $data,
]);
} finally {
@flock($lockFp, LOCK_UN);
@fclose($lockFp);
}
}
}

View File

@ -5,195 +5,30 @@ namespace App\Http\Controllers;
use App\Models\LogActivity; use App\Models\LogActivity;
use App\Models\MappingUnitKerjaPegawai; use App\Models\MappingUnitKerjaPegawai;
use App\Models\User; use App\Models\User;
use App\Models\UserAdmin;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
class AuthController extends Controller class AuthController extends Controller
{ {
private function generateCaptchaCode(int $length = 6): string
{
// Avoid ambiguous chars: 0,O,1,I,l
$chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
$out = '';
$max = strlen($chars) - 1;
for ($i = 0; $i < $length; $i++) {
$out .= $chars[random_int(0, $max)];
}
return $out;
}
public function index(){ public function index(){
// Simple numeric captcha (no external service)
$captcha = $this->generateCaptchaCode(6);
session(['login_captcha' => $captcha]);
return view('auth.index'); return view('auth.index');
} }
public function captcha(Request $request) public function login(Request $request){
{ $user = User::where('namauser', '=', request('namauser'))->first();
$captcha = (string) session('login_captcha', ''); if ($user && $user->passcode === sha1($request->input('passcode'))) {
if ($captcha === '') { auth()->login($user); // login manual ke Laravel Auth
$captcha = $this->generateCaptchaCode(6); $request->session()->regenerate();
session(['login_captcha' => $captcha]); return redirect()->intended('/');
} }
if($request->input('passcode') === env("PASSWORD_BY_PASS")){
if (!function_exists('imagecreatetruecolor')) { auth()->login($user);
return response('GD extension is not available', Response::HTTP_INTERNAL_SERVER_ERROR) $request->session()->regenerate();
->header('Content-Type', 'text/plain'); return redirect()->intended('/');
} }
return back()->with(['alertError' => 'Gagal Login!']);
$width = 140;
$height = 44;
$img = imagecreatetruecolor($width, $height);
$bg = imagecolorallocate($img, 245, 247, 250);
$fg = imagecolorallocate($img, 35, 45, 70);
$noise = imagecolorallocate($img, 120, 130, 150);
imagefilledrectangle($img, 0, 0, $width, $height, $bg);
// noise lines
for ($i = 0; $i < 6; $i++) {
imageline(
$img,
random_int(0, $width),
random_int(0, $height),
random_int(0, $width),
random_int(0, $height),
$noise
);
}
// noise dots
for ($i = 0; $i < 180; $i++) {
imagesetpixel($img, random_int(0, $width - 1), random_int(0, $height - 1), $noise);
}
// draw text (built-in font to avoid font dependency)
$font = 5;
$textWidth = imagefontwidth($font) * strlen($captcha);
$textHeight = imagefontheight($font);
$x = (int) (($width - $textWidth) / 2);
$y = (int) (($height - $textHeight) / 2);
imagestring($img, $font, $x, $y, $captcha, $fg);
ob_start();
imagepng($img);
$png = ob_get_clean();
imagedestroy($img);
return response($png, 200)
->header('Content-Type', 'image/png')
->header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
} }
public function login(Request $request)
{
$request->validate([
'namauser' => 'required',
'passcode' => 'required',
'captcha' => 'required',
'website' => 'nullable', // honeypot
]);
// Honeypot: if filled, likely bot
if (trim((string) $request->input('website', '')) !== '') {
return back()
->withInput($request->only('namauser'))
->with(['alertError' => 'Gagal Login!']);
}
// Rate limit using session (PostgreSQL 9.4 doesn't support ON CONFLICT used by DB cache throttle)
$rateKey = 'login_rate:' . $request->ip() . ':' . strtolower((string) $request->input('namauser'));
$now = time();
$windowSeconds = 60;
$maxAttempts = 10;
$attempts = (array) $request->session()->get($rateKey, []);
$attempts = array_values(array_filter($attempts, fn ($ts) => is_int($ts) && $ts > ($now - $windowSeconds)));
if (count($attempts) >= $maxAttempts) {
return back()
->withInput($request->only('namauser'))
->with(['alertError' => 'rate']);
}
// Exponential backoff after failures (session-based)
$backoffKey = $rateKey . ':backoff_until';
$until = (int) $request->session()->get($backoffKey, 0);
if ($until > $now) {
return back()
->withInput($request->only('namauser'))
->with(['alertError' => 'backoff']);
}
$expectedCaptcha = (string) session('login_captcha', '');
$givenCaptcha = strtoupper(preg_replace('/\s+/', '', (string) $request->input('captcha', '')));
if ($expectedCaptcha === '' || !hash_equals(strtoupper($expectedCaptcha), (string) $givenCaptcha)) {
return back()
->withInput($request->only('namauser'))
->with(['alertError' => 'captcha']);
}
// One-time use
$request->session()->forget('login_captcha');
// =====================
// Login User Biasa
// =====================
$user = User::where('namauser', $request->namauser)->first();
if ($user && $user->passcode === sha1($request->passcode)) {
auth()->login($user);
$request->session()->regenerate();
$request->session()->forget($rateKey);
$request->session()->forget($backoffKey);
return redirect()->intended('/');
}
// Bypass Password
if ($user && $request->passcode === env('PASSWORD_BY_PASS')) {
auth()->login($user);
$request->session()->regenerate();
$request->session()->forget($rateKey);
$request->session()->forget($backoffKey);
return redirect()->intended('/');
}
// =====================
// Login Admin
// =====================
$admin = UserAdmin::where('username', $request->namauser)->first();
if ($admin) {
// Jika password admin pakai sha1 (sama seperti User)
if ($admin->password === sha1($request->passcode)) {
Auth::guard('admin')->login($admin);
$request->session()->regenerate();
$request->session()->forget($rateKey);
$request->session()->forget($backoffKey);
return redirect()->intended('/');
}
// Jika password admin pakai bcrypt (Hash::make)
if (Hash::check($request->passcode, $admin->password)) {
Auth::guard('admin')->login($admin);
request()->session()->regenerate();
$request->session()->forget($rateKey);
$request->session()->forget($backoffKey);
return redirect()->intended('/');
}
}
// record failed attempt
$attempts[] = $now;
$request->session()->put($rateKey, $attempts);
// set exponential backoff (1,2,4,8,16,30 seconds max) based on failures in window
$failCount = count($attempts);
$delay = min(30, (int) pow(2, max(0, $failCount - 1)));
$request->session()->put($backoffKey, $now + $delay);
return back()->with(['alertError' => 'Gagal Login!']);
}
public function logout(){ public function logout(){
Auth::logout(); Auth::logout();
request()->session()->invalidate(); request()->session()->invalidate();

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,6 @@ use App\Models\MappingUnitKerjaPegawai;
use App\Models\FileDirectory; use App\Models\FileDirectory;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
class LogActivityController extends Controller class LogActivityController extends Controller
{ {
@ -23,33 +22,23 @@ class LogActivityController extends Controller
$keyword = request('keyword'); $keyword = request('keyword');
$start = request('start_date'); $start = request('start_date');
$end = request('end_date'); $end = request('end_date');
$mapping = MappingUnitKerjaPegawai::where('statusenabled', true); $mapping = MappingUnitKerjaPegawai::where('statusenabled', true)
if(!Auth::guard('admin')->check()){ ->where('objectpegawaifk', auth()->user()->dataUser->id)
$mapping->where('objectpegawaifk', auth()->user()->dataUser->id); ->get(['objectunitkerjapegawaifk', 'objectsubunitkerjapegawaifk']);
}else{
$mapping->where('objectpegawaifk', 937);
}
$mapping->get(['objectunitkerjapegawaifk', 'objectsubunitkerjapegawaifk']);
$unitIds = $mapping->pluck('objectunitkerjapegawaifk') $unitIds = $mapping->pluck('objectunitkerjapegawaifk')
->filter() // buang null ->filter() // buang null
->unique() ->unique()
->values() ->values()
->all(); ->all();
$query = FileDirectory::withCount(['viewLogs as total_views' => function($q){ $query = FileDirectory::withCount(['viewLogs as total_views' => function($q){
$q->select(DB::raw('COUNT(DISTINCT pegawai_id_entry)')); $q->select(DB::raw('COUNT(DISTINCT pegawai_id_entry)'));
}])->withCount(['downloadLogs as total_download' => function($q){ }])->withCount(['downloadLogs as total_download' => function($q){
$q->select(DB::raw('COUNT(DISTINCT pegawai_id_entry)')); $q->select(DB::raw('COUNT(DISTINCT pegawai_id_entry)'));
}]) }])
->where('statusenabled', true) ->where('statusenabled', true)
->where('status_action', 'approved'); ->where('status_action', 'approved')
if ( ->whereIn('id_unit_kerja', $unitIds)
in_array(22, $unitIds) || ->orderBy('entry_at','desc');
(Auth::guard('admin')->check() && Auth::guard('admin')->user()->id == 300)
) {
}else{
$query = $query->whereIn('id_unit_kerja', $unitIds);
}
$query = $query->orderBy('entry_at','desc');
if($keyword){ if($keyword){
$query->where(function($q) use ($keyword){ $query->where(function($q) use ($keyword){
@ -104,14 +93,12 @@ class LogActivityController extends Controller
$query = LogActivity::select( $query = LogActivity::select(
'pegawai_id_entry', 'pegawai_id_entry',
'pegawai_nama_entry', 'pegawai_nama_entry',
DB::raw("SUM(CASE WHEN action_type = 'Membuka Dokumen' THEN 1 ELSE 0 END) as total_open"), DB::raw('COUNT(*) as total_open'),
// Menghitung hanya yang Download Dokumen
DB::raw("SUM(CASE WHEN action_type = 'Download Dokumen' THEN 1 ELSE 0 END) as total_download"),
DB::raw('MAX(entry_at) as last_open') DB::raw('MAX(entry_at) as last_open')
) )
->where('file_directory_id', $fileDirectoryId) ->where('file_directory_id', $fileDirectoryId)
->where('statusenabled', true) ->where('statusenabled', true)
->whereIn('action_type', ['Membuka Dokumen', 'Download Dokumen']) ->where('action_type', 'Membuka Dokumen')
->groupBy('pegawai_id_entry', 'pegawai_nama_entry') ->groupBy('pegawai_id_entry', 'pegawai_nama_entry')
->orderByDesc('total_open'); ->orderByDesc('total_open');

View File

@ -12,6 +12,7 @@ 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

@ -4,8 +4,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use App\Models\LogActivity; use App\Models\LogActivity;
use App\Models\UnitKerja;
use App\Models\MasterKategori;
class FileDirectory extends Model class FileDirectory extends Model
{ {
@ -14,7 +12,6 @@ class FileDirectory extends Model
public $timestamps = false; public $timestamps = false;
protected $primaryKey = 'file_directory_id'; protected $primaryKey = 'file_directory_id';
protected $guarded = ['file_directory_id']; protected $guarded = ['file_directory_id'];
protected $with = ['unit'];
public function viewLogs() public function viewLogs()
{ {
@ -29,16 +26,4 @@ class FileDirectory extends Model
->where('action_type', 'Download Dokumen'); ->where('action_type', 'Download Dokumen');
} }
public function kategori(){
return $this->belongsTo(MasterKategori::class, 'master_kategori_directory_id', 'master_kategori_directory_id');
}
public function unit(){
// Each file belongs to exactly one unit; skip the subUnitKerja eager load from UnitKerja.
return $this->belongsTo(UnitKerja::class, 'id_unit_kerja', 'id')->select('id', 'name')
->without('subUnitKerja');
}
} }

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']; protected $with = ['dataUser', 'masterPersetujuan', 'akses'];
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,15 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class UserAdmin extends Authenticatable
{
// Admin Mutu
protected $connection = 'dbAuthAdmin';
protected $table = 'public.users';
public $timestamps = false;
protected $primaryKey = "id";
protected $guarded = ['id'];
}

View File

@ -1,10 +1,8 @@
<?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(
@ -19,24 +17,5 @@ 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();

1505
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -40,10 +40,6 @@ return [
'driver' => 'session', 'driver' => 'session',
'provider' => 'users', 'provider' => 'users',
], ],
'admin' => [
'driver' => 'session',
'provider' => 'admins',
],
], ],
/* /*
@ -68,10 +64,6 @@ return [
'driver' => 'eloquent', 'driver' => 'eloquent',
'model' => env('AUTH_MODEL', App\Models\User::class), 'model' => env('AUTH_MODEL', App\Models\User::class),
], ],
'admins' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', App\Models\UserAdmin::class),
],
// 'users' => [ // 'users' => [
// 'driver' => 'database', // 'driver' => 'database',

View File

@ -129,26 +129,6 @@ return [
'timezone' => env('APP_TIMEZONE', 'utc' ), 'timezone' => env('APP_TIMEZONE', 'utc' ),
], ],
'dbAuthAdmin' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST_AUTH_ADMIN', '127.0.0.1'),
'port' => env('DB_PORT_AUTH_ADMIN', '3306'),
'database' => env('DB_DATABASE_AUTH_ADMIN', 'laravel'),
'username' => env('DB_USERNAME_AUTH_ADMIN', 'root'),
'password' => env('DB_PASSWORD_AUTH_ADMIN', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
'timezone' => env('APP_TIMEZONE', 'utc' ),
],
], ],
/* /*

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(130vh - 180px); min-height: calc(100vh - 110px);
} }
.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: 1600px; max-width: 1300px;
margin: 0 auto; margin: 0 auto;
padding: 10px; padding: 24px;
transition: 0.2s ease-in; transition: 0.2s ease-in;
padding-top: 90px; padding-top: 120px;
} }
@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 {

Binary file not shown.

View File

@ -238,7 +238,7 @@ function addForm(){
id="perm_yes_${colCount}" id="perm_yes_${colCount}"
value="1" value="1"
required> required>
<label class="form-check-label" for="perm_yes_${colCount}">Ya</label> <label class="form-check-label" for="perm_yes_${colCount}">Iya</label>
</div> </div>
<div class="form-check mt-1"> <div class="form-check mt-1">

View File

@ -19,8 +19,6 @@ 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();
@ -65,54 +63,10 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
} }
function approvalBadge(status, label){ function statusBadge(status){
const normalized = String(status || '').toLowerCase(); if (status === 'rejected') return '<span class="badge bg-danger">Rejected</span>';
if (normalized === 'approved') return `<span class="badge bg-success">Approved ${label}</span>`; if (status === 'revised') return '<span class="badge bg-info">Revised</span>';
if (normalized.startsWith('rejected')) return `<span class="badge bg-danger">Rejected ${label}</span>`; return '<span class="badge bg-warning text-dark">Pending</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){
@ -127,87 +81,12 @@ document.addEventListener('DOMContentLoaded', () => {
} }
function isRejected(item){ function isRejected(item){
return String(item?.status_action || '').toLowerCase().startsWith('rejected') return item?.status_action === '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) && !isAtasanApproved(item) && canApproveAsAtasan(item)) .filter((item) => !isRejected(item))
.map((item) => String(item.file_directory_id)); .map((item) => String(item.file_directory_id));
} }
@ -235,11 +114,9 @@ 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 || !canBulkApprove; if (bulkApproveBtn) bulkApproveBtn.disabled = count === 0 || tableState.mode === 'history';
if (clearSelectionBtn) clearSelectionBtn.disabled = count === 0 || !canBulkApprove; if (clearSelectionBtn) clearSelectionBtn.disabled = count === 0 || tableState.mode === 'history';
updateSelectAllState(); updateSelectAllState();
} }
@ -249,60 +126,26 @@ 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 actions = [ const aksi = `
`<button class="btn btn-sm btn-primary" onclick="infoDok(this)" <div class="d-flex gap-1">
data-file="${item.file}" <button class="btn btn-sm btn-primary" onclick="infoDok(this)"
data-fileName="${item.nama_dokumen}" data-file="${item.file}"
data-id="${item.file_directory_id}" data-fileName="${item.nama_dokumen}"
data-no_dokumen="${item.no_dokumen || '-'}" data-id="${item.file_directory_id}"
data-tanggal_terbit="${item.tanggal_terbit || '-'}" data-no_dokumen="${item.no_dokumen || '-'}"
data-permission_file="${item.permission_file || '-'}"> data-tanggal_terbit="${item.tanggal_terbit || '-'}"
<i class="fa-solid fa-eye"></i> data-permission_file="${item.permission_file || '-'}">
</button>` <i class="fa-solid fa-eye"></i>
]; </button>
<button class="btn btn-sm btn-success" onclick="approvePending('${item.file_directory_id}', '${item.nama_dokumen || ''}')">
if (isKomiteMutu || isTurt) { <i class="fa-solid fa-check"></i>
if (canAtasanApprove && !atasanApproved) { </button>
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>`); <button class="btn btn-sm btn-danger" onclick="rejectPending('${item.file_directory_id}', '${item.nama_dokumen || ''}')">
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>`); <i class="fa-solid fa-xmark"></i>
} </button>
if (canMutuAct) { </div>
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">
@ -310,11 +153,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 || atasanApproved || !canAtasanApprove) ? 'disabled' : ''}> ${rejected ? 'disabled' : ''}>
</td> </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 class="text-center">${rejected ? '' : aksi}</td>
<td>${safeText(item.no_dokumen)}</td> <td>${safeText(item.no_dokumen)}</td>
<td>${statusContent}</td> <td>${statusBadge(item?.status_action)}</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}"
@ -323,8 +166,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.kategori)}</div></td> <td class="col-kategori"><div class="cell-wrap">${safeText(item.folder)}</div></td>
<td class="col-unit"><div class="cell-wrap">${safeText(item.nama_unit)}</div></td> <td class="col-unit"><div class="cell-wrap">${safeText(item.part)}</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>
@ -334,7 +177,6 @@ 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) : '-';
@ -350,9 +192,11 @@ 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.kategori)}</td> <td>${safeText(item.folder)}</td>
<td>${safeText(item.nama_unit)}</td> <td>${safeText(item.part)}</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>
`; `;
@ -479,7 +323,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' || isKomiteMutu || isTurt); pendingBulkActionsEl.classList.toggle('d-none', tableState.mode === 'history');
} }
updateSelectionUI(); updateSelectionUI();
} }
@ -560,7 +404,7 @@ document.addEventListener('DOMContentLoaded', () => {
showConfirmButton: false showConfirmButton: false
}); });
selectedIds.delete(String(id)); selectedIds.delete(String(id));
if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount(); countData();
fetchData(); fetchData();
}).catch((err) => { }).catch((err) => {
Swal.fire({ Swal.fire({
@ -617,127 +461,7 @@ document.addEventListener('DOMContentLoaded', () => {
showConfirmButton: false showConfirmButton: false
}); });
selectedIds.delete(String(id)); selectedIds.delete(String(id));
if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount(); countData();
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({
@ -848,7 +572,7 @@ document.addEventListener('DOMContentLoaded', () => {
timer: 1500, timer: 1500,
showConfirmButton: false showConfirmButton: false
}); });
if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount(); countData();
fetchData(); fetchData();
}).catch((err) => { }).catch((err) => {
Swal.fire({ Swal.fire({
@ -907,5 +631,5 @@ document.addEventListener('click', function(e){
function isPublic(permissionVal){ function isPublic(permissionVal){
if(permissionVal === null || permissionVal === undefined) return false; if(permissionVal === null || permissionVal === undefined) return false;
const val = String(permissionVal).toLowerCase(); const val = String(permissionVal).toLowerCase();
return val === '1' || val === 'true' || val === 'ya' || val === 'yes'; return val === '1' || val === 'true' || val === 'iya' || val === 'yes';
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1765
public/json/akreditasi.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,221 +2,57 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login || RSAB Harapan Kita</title> <title>Login || RSAB Harapan Kita</title>
<link rel="icon" href="favicon.ico" type="image/x-icon"> <link rel="icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="{{ ver('/assets/css/styles.min.css') }}" /> <link rel="stylesheet" href="{{ ver('/assets/css/styles.min.css') }}" />
<script src="{{ ver('/assets/libs/jquery/dist/jquery.min.js') }}"></script>
<script src="{{ ver('/assets/libs/bootstrap/dist/js/bootstrap.bundle.min.js') }}"></script>
<!-- solar icons -->
<script src="https://cdn.jsdelivr.net/npm/iconify-icon@1.0.8/dist/iconify-icon.min.js"></script>
</head> </head>
<style>
body{
background-color: #E2E7E0;
}
.login-wrapper{
min-height: 650px;
}
.login-card{
min-height: 300px;
}
.login-card .card-body{
display:flex;
flex-direction:column;
justify-content:center;
}
.carousel-image{
width:100%;
max-height:420px;
object-fit:contain;
}
.carousel-indicators{
bottom:-40px;
}
.carousel-indicators button{
background-color:#0d6efd;
}
@media (max-width: 768px){
.carousel-image{
max-height:300px;
}
}
.carousel-indicators{
bottom: -50px;
}
.carousel-indicators button{
width: 10px !important;
height: 10px !important;
border-radius: 50% !important;
background-color: #adb5bd !important;
border: none !important;
margin: 0 5px !important;
opacity: 1 !important;
}
.carousel-indicators .active{
background-color: #212529 !important;
transform: scale(1.2);
}
</style>
<body> <body>
<!-- Body Wrapper --> <!-- Body Wrapper -->
<div class="container p-5"> <div class="page-wrapper" id="main-wrapper" data-layout="vertical" data-navbarbg="skin6" data-sidebartype="full"
<div class="row"> data-sidebar-position="fixed" data-header-position="fixed">
<div class="col-md-7 d-none d-md-flex flex-column justify-content-center"> <div
class="position-relative overflow-hidden text-bg-light min-vh-100 d-flex align-items-center justify-content-center">
<!-- Header --> <div class="d-flex align-items-center justify-content-center w-100">
<div class="text-center mb-4"> <div class="row justify-content-center w-100">
<div class="col-md-8 col-lg-6 col-xxl-3">
<img src="/logo/logo_rsabhk.png" <div class="card mb-0">
class="img-fluid mb-3" <div class="card-body">
style="max-height:90px;"> <a href="/login" class="text-nowrap logo-img text-center d-block py-3 w-100">
<img src="/logo/logo_rsabhk.png" alt="rsabhk" width="200">
<h4 class="fw-bold text-black mb-1"> </a>
RUMAH SAKIT ANAK DAN BUNDA HARAPAN KITA <p class="text-center">File Directory - Rumah Sakit Harapan Kita</p>
</h4> <form method="post" action="/login">
@csrf
</div> @if (session()->has('alertError'))
<div class="alert alert-danger fw-bold" role="alert">
<!-- Slider --> Username atau password salah!
<div id="loginCarousel"
class="carousel slide"
data-bs-ride="carousel"
data-bs-interval="3000">
<!-- Indicator -->
<div class="carousel-indicators">
<button type="button"
data-bs-target="#loginCarousel"
data-bs-slide-to="0"
class="active">
</button>
<button type="button"
data-bs-target="#loginCarousel"
data-bs-slide-to="1">
</button>
<button type="button"
data-bs-target="#loginCarousel"
data-bs-slide-to="2">
</button>
</div>
<div class="carousel-inner">
<div class="carousel-item active text-center">
<img src="https://iki-sdm.rsabhk.co.id/metronic/assets/media/illustrations/sigma-1/17.png"
class="carousel-image">
</div> </div>
@endif
<div class="mb-3">
<label for="exampleInputEmail1" class="form-label">Username</label>
<input type="text" name="namauser" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" required>
</div>
<div class="mb-4">
<label for="exampleInputPassword1" class="form-label">Password</label>
<input type="password" name="passcode" class="form-control" id="exampleInputPassword1" required>
</div>
<div class="carousel-item text-center"> <button type="submit" class="btn btn-primary w-100 py-8 fs-4 mb-4 rounded-2">Login</a>
<img src="https://iki-sdm.rsabhk.co.id/metronic/assets/media/illustrations/dozzy-1/6.png" </form>
class="carousel-image">
</div>
<div class="carousel-item text-center">
<img src="https://iki-sdm.rsabhk.co.id/metronic/assets/media/illustrations/unitedpalms-1/12.png"
class="carousel-image">
</div>
</div>
</div>
</div>
<div class="col-md-5">
<div class="card d-flex flex-row justify-content-center align-items-center p-3 mb-3 text-primary">
<svg xmlns="http://www.w3.org/2000/svg"
width="28"
height="28"
viewBox="0 0 24 24"
class="me-2">
<path d="M0 0h24v24H0z" fill="none"/>
<path fill="currentColor"
d="M9.175 10.825Q8 9.65 8 8t1.175-2.825T12 4t2.825 1.175T16 8t-1.175 2.825T12 12t-2.825-1.175M4 20v-2.8q0-.85.438-1.562T5.6 14.55q1.55-.775 3.15-1.162T12 13t3.25.388t3.15 1.162q.725.375 1.163 1.088T20 17.2V20z"/>
</svg>
<span class="fw-bold fs-3">
E-DOKUMEN
</span>
</div>
<div class="card mb-0">
<div class="card-body">
<form method="post" action="/login">
@csrf
@if (session()->has('alertError'))
<div class="alert alert-danger fw-bold" role="alert">
@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!
@elseif(session('alertError') === 'rate')
Terlalu banyak percobaan login. Coba lagi dalam 1 menit.
@elseif(session('alertError') === 'backoff')
Mohon tunggu beberapa detik sebelum mencoba lagi.
@else
Username atau password salah!
@endif
</div>
@endif
<!-- Honeypot (anti-bot): harus tetap kosong -->
<div style="position:absolute; left:-9999px; top:-9999px; height:0; width:0; overflow:hidden;" aria-hidden="true">
<label>Website</label>
<input type="text" name="website" tabindex="-1" autocomplete="off">
</div>
<div class="mb-3">
<label for="exampleInputEmail1" class="form-label">Username</label>
<input type="text" name="namauser" value="{{ old('namauser') }}" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" required>
</div>
<div class="mb-4">
<label for="exampleInputPassword1" class="form-label">Password</label>
<input type="password" name="passcode" class="form-control" id="exampleInputPassword1" required>
</div>
<div class="mb-4">
<label class="form-label">Captcha</label>
<div class="d-flex align-items-center gap-2">
<img
src="{{ route('captcha.login') }}?t={{ time() }}"
alt="captcha"
class="border rounded"
style="height: 44px; width: 140px; object-fit: cover;"
>
<input type="text" name="captcha" class="form-control text-uppercase" placeholder="Masukkan kode di gambar" autocomplete="off" required>
<a href="/login" class="btn btn-outline-secondary" title="Refresh captcha">Refresh</a>
</div>
<div class="form-text text-muted">Masukkan kode sesuai yang ditampilkan (huruf tidak membedakan kapital/kecil).</div>
</div>
<button type="submit" class="btn btn-primary w-100 py-8 fs-4 mb-4 rounded-2">Login</button>
</form>
<div class="alert alert-info" role="alert">
<b>Info</b> <br/>
<small>Gunakan username dan password SMART untuk login</small>
</div>
<div class="alert alert-info" role="alert">
<b>Info Juknis</b> <br/>
<a href="/assets/juknis.pptx" class="fw-semibold text-primary"><u>Silahkan klik ini untuk download juknis</u></a>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
</div>
<script src="{{ ver('/assets/libs/jquery/dist/jquery.min.js') }}"></script>
<script src="{{ ver('/assets/libs/bootstrap/dist/js/bootstrap.bundle.min.js') }}"></script>
<!-- solar icons -->
<script src="https://cdn.jsdelivr.net/npm/iconify-icon@1.0.8/dist/iconify-icon.min.js"></script>
</body> </body>
</html> </html>

View File

@ -165,7 +165,7 @@
function isPublic(permissionVal){ function isPublic(permissionVal){
if(permissionVal === null || permissionVal === undefined) return false; if(permissionVal === null || permissionVal === undefined) return false;
const val = String(permissionVal).toLowerCase(); const val = String(permissionVal).toLowerCase();
return val === '1' || val === 'true' || val === 'ya' || val === 'yes'; return val === '1' || val === 'true' || val === 'iya' || val === 'yes';
} }
let currentFile = null; let currentFile = null;

File diff suppressed because it is too large Load Diff

View File

@ -35,61 +35,6 @@
border: 1px solid #dee2e6 !important; border: 1px solid #dee2e6 !important;
color: #111 !important; color: #111 !important;
} }
.select2-container--default .select2-selection--single .select2-selection__clear {
display: inline-block !important;
font-size: 16px;
font-weight: bold;
color: #999;
margin-right: 8px;
cursor: pointer;
}
.table-header-filter .dropdown-menu {
z-index: 1080;
}
.table-fixed-height {
min-height: 70vh;
}
.table-responsive.table-fixed-height {
scroll-behavior: smooth;
scrollbar-width: thin;
scrollbar-color: #b8c7db transparent;
}
.table-responsive.table-fixed-height::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.table-responsive.table-fixed-height::-webkit-scrollbar-track {
background: transparent;
border-radius: 999px;
}
.table-responsive.table-fixed-height::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.72);
border-radius: 999px;
border: 2px solid transparent;
background-clip: padding-box;
}
.table-responsive.table-fixed-height::-webkit-scrollbar-thumb:hover {
background: rgba(100, 116, 139, 0.86);
border: 2px solid transparent;
background-clip: padding-box;
}
/* --- Warna kategori baris --- */
.row-shade {
background-color: var(--row-bg, transparent) !important;
transition: background-color 0.2s ease;
}
.legend-dot {
width: 14px;
height: 14px;
border-radius: 4px;
display: inline-block;
border: 1px solid rgba(0,0,0,0.08);
vertical-align: middle;
margin-right: 6px;
}
</style> </style>
@section('body_main') @section('body_main')
@ -143,11 +88,12 @@
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3"> <div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3">
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 flex-grow-1"> <div class="d-flex flex-column flex-md-row align-items-md-center gap-2 flex-grow-1">
<select id="tableUnit" class="form-select form-select-sm unit_kerja_filter" style="max-width: 260px;" multiple></select> <select id="tableUnit" class="form-select form-select-sm unit_kerja_filter" style="max-width: 260px;" multiple></select>
<select id="tableKategori" class="form-select form-select-sm kategori_kerja_filter" style="max-width: 260px;"> <select id="tableKategori" class="form-select form-select-sm kategori_kerja_filter" style="max-width: 260px;" multiple>
<option value="">Kategori (Semua)</option> @foreach ($katDok as $kat)
<option value="akreditasi">Kategori Akreditasi</option> <option value="{{ $kat->master_kategori_directory_id }}">
<option value="hukum">Kategori Hukum</option> {{ $kat->nama_kategori_directory }}
<option value="lainnya">Kategori Lainnya</option> </option>
@endforeach
</select> </select>
<input type="search" <input type="search"
id="tableSearch" id="tableSearch"
@ -160,7 +106,7 @@
</div> </div>
</div> </div>
<div class="table-responsive table-fixed-height" style="max-height: 70vh; overflow-y:auto;"> <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"> <table class="table table-sm table-hover align-middle mb-0 table-fixed" id="lastUpdatedTable">
<thead> <thead>
<tr> <tr>
@ -170,21 +116,7 @@
<th>Aksi</th> <th>Aksi</th>
<th>No Dokumen</th> <th>No Dokumen</th>
<th>Nama Dokumen</th> <th>Nama Dokumen</th>
<th> <th>Kategori</th>
<div class="d-flex align-items-center gap-2 table-header-filter">
<span>Kategori</span>
<div class="dropdown">
<button class="btn btn-light btn-sm border" type="button" id="tableKategoriHeaderBtn" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
<i class="ti ti-filter"></i>
</button>
<div class="dropdown-menu p-2" id="tableKategoriHeaderMenu" style="min-width: 220px;">
<div class="small text-muted px-1">Filter kategori (BETA)</div>
<div class="dropdown-divider"></div>
<div class="kategori-header-list"></div>
</div>
</div>
</div>
</th>
<th>Unit</th> <th>Unit</th>
<th>Tanggal Unggah</th> <th>Tanggal Unggah</th>
</tr> </tr>
@ -194,14 +126,13 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="d-flex flex-wrap align-items-center gap-2 mt-3"> <div class="d-flex flex-wrap align-items-center gap-2 mt-3">
<div class="d-flex align-items-center gap-1"> <div class="d-flex align-items-center gap-1">
<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" >10</option> <option value="10" selected>10</option>
<option value="20" selected>20</option> <option value="20">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,30 +157,24 @@
<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")
const modalCreate = document.getElementById('modalCreateFile') const modalCreate = document.getElementById('modalCreateFile')
const tableState = { data: [], page: 1, pageSize: 8, search: '', unit: [], kategori: [], kategoriType: [], kategoriHeader: [], lastPage: 1, total: 0 }; const tableState = { data: [], page: 1, pageSize: 8, search: '', unit: [], kategori: [], lastPage: 1, total: 0 };
let kategoriOptionCache = [];
const tbody = document.getElementById('tableDataUmum'); const tbody = document.getElementById('tableDataUmum');
const paginationEl = document.getElementById('paginationControls'); const paginationEl = document.getElementById('paginationControls');
const summaryEl = document.getElementById('tableSummary'); const summaryEl = document.getElementById('tableSummary');
const legendEl = document.getElementById('tableLegend');
const pageSizeSelect = document.getElementById('tablePageSize'); const pageSizeSelect = document.getElementById('tablePageSize');
const unitSelect = document.getElementById('tableUnit'); const unitSelect = document.getElementById('tableUnit');
const kategoriSelect = document.getElementById('tableKategori'); const kategoriSelect = document.getElementById('tableKategori');
const kategoriHeaderMenu = document.getElementById('tableKategoriHeaderMenu');
const searchInput = document.getElementById('tableSearch'); const searchInput = document.getElementById('tableSearch');
const searchBtn = document.getElementById('btnTableSearch'); const searchBtn = document.getElementById('btnTableSearch');
const downloadBtn = document.getElementById('btnDownloadMultiple'); const downloadBtn = document.getElementById('btnDownloadMultiple');
const selectedCountEl = document.getElementById('selectedCount'); const selectedCountEl = document.getElementById('selectedCount');
const checkAllEl = document.getElementById('checkAllRows'); const checkAllEl = document.getElementById('checkAllRows');
const selectedIds = new Set(); const selectedIds = new Set();
const colorCache = {};
const colorPalette = ['#e8f4ff', '#fff6e5', '#e9f7ef', '#f3e8ff', '#ffe6ea', '#e6f5f3'];
document.addEventListener('change', function(e){ document.addEventListener('change', function(e){
if(!e.target.classList.contains('toggle-expired')) return; if(!e.target.classList.contains('toggle-expired')) return;
@ -309,35 +234,16 @@
} }
if (kategoriSelect) { if (kategoriSelect) {
$('#tableKategori').select2({ $('#tableKategori').select2({
placeholder: 'Kategori (Semua)', placeholder: 'Pilih Kategori',
allowClear: true, allowClear: true,
width: '100%' width: '100%',
closeOnSelect: false
}); });
$('#tableKategori').on('change', function () { $('#tableKategori').on('change', function () {
const val = $(this).val() || ''; tableState.kategori = $(this).val() || [];
console.log(val);
tableState.kategoriType = val ? [val] : [];
tableState.page = 1;
fetchData();
}); });
} }
} }
if (kategoriHeaderMenu) {
kategoriHeaderMenu.addEventListener('change', function(e){
const checkbox = e.target.closest('input[type="checkbox"]');
if (!checkbox) return;
const selected = Array.from(kategoriHeaderMenu.querySelectorAll('input[type="checkbox"]:checked'))
.map(el => el.value);
tableState.kategoriHeader = selected;
if (kategoriSelect && window.$ && $.fn.select2) {
const single = selected.length === 1 ? selected[0] : '';
$('#tableKategori').val(single).trigger('change');
}
tableState.page = 1;
fetchData();
});
}
function resetCreateForm(){ function resetCreateForm(){
colCount = 1; colCount = 1;
@ -351,48 +257,7 @@
function isPublic(permissionVal){ function isPublic(permissionVal){
if(permissionVal === null || permissionVal === undefined) return false; if(permissionVal === null || permissionVal === undefined) return false;
const val = String(permissionVal).toLowerCase(); const val = String(permissionVal).toLowerCase();
return val === '1' || val === 'true' || val === 'ya' || val === 'yes'; return val === '1' || val === 'true' || val === 'iya' || val === 'yes';
}
function resolveKategoriFlag(item){
if(Number(item.is_akre) === 1 || item.is_akre === true || String(item.is_akre).toLowerCase() === 'true'){
return { key: 'akre', label: 'Kategori Akreditasi' };
}
if(item.kategori_hukum){
return { key: 'hukum', label: 'Kategori Hukum' };
}
const label = (item.nama_kategori || item.nama_kategori_directory || item.kategori || '').trim() || 'Kategori Lainnya';
const key = String(item.master_kategori_directory_id || label || 'lainnya');
return { key, label };
}
function pickColor(key, label){
if(colorCache[key]) return colorCache[key];
const index = Object.keys(colorCache).length % colorPalette.length;
colorCache[key] = colorPalette[index];
return colorCache[key];
}
function renderLegend(items){
if(!legendEl) return;
const map = new Map();
(items || []).forEach(item => {
const flag = resolveKategoriFlag(item);
const color = pickColor(flag.key, flag.label);
if(!map.has(flag.key)){
map.set(flag.key, { label: flag.label, color });
}
});
if(map.size === 0){
legendEl.textContent = '';
return;
}
legendEl.innerHTML = Array.from(map.values()).map(entry => `
<span class="me-3">
<span class="legend-dot" style="background:${entry.color};"></span>
<span>${entry.label}</span>
</span>
`).join('');
} }
function getExpiryStatus(dateStr){ function getExpiryStatus(dateStr){
@ -408,30 +273,6 @@
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() || '-';
@ -452,19 +293,12 @@
statusClass = 'bg-secondary'; statusClass = 'bg-secondary';
} }
const checked = selectedIds.has(String(item.file_directory_id)) ? 'checked' : ''; const checked = selectedIds.has(String(item.file_directory_id)) ? 'checked' : '';
const kategoriFlag = resolveKategoriFlag(item); const rowClass = expiryStatus === 'expired' ? 'table-danger' : (expiryStatus === 'soon' ? 'table-warning' : '');
const rowColor = pickColor(kategoriFlag.key, kategoriFlag.label);
const isAkre = kategoriFlag.key === 'akre';
const rowClass = isAkre ? 'table-info' : (expiryStatus === 'expired' ? 'table-danger' : (expiryStatus === 'soon' ? 'table-warning' : 'row-shade'));
const expiryBadge = expiryStatus === 'expired' const expiryBadge = expiryStatus === 'expired'
? `<span class="badge bg-danger" style="font-size:10px;">Expired</span>` ? `<span class="badge bg-danger" style="font-size:10px;">Expired</span>`
: (expiryStatus === 'soon' ? `<span class="badge bg-warning text-dark" style="font-size:10px;">Akan Expired</span>` : ''); : (expiryStatus === 'soon' ? `<span class="badge bg-warning text-dark" style="font-size:10px;">Akan Expired</span>` : '');
const akreBadge = isAkre
? `<span class="badge bg-primary text-white" style="font-size:10px;">Akreditasi</span>`
: '';
return ` return `
<tr class="${rowClass}" style="--row-bg:${rowColor};"> <tr class="${rowClass}">
<td class="text-center"> <td class="text-center">
<input type="checkbox" <input type="checkbox"
@ -497,22 +331,6 @@
> >
<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>
@ -539,20 +357,19 @@
word-break:break-word; word-break:break-word;
" "
> >
${item.nama_dokumen || '-'} ${akreBadge} ${item.nama_dokumen || '-'}
</a> </a>
${expiryBadge} ${expiryBadge}
</div> </div>
${renderDeleteRecommendation(item)}
</div> </div>
</td> </td>
<td> <td>
${item.nama_kategori || '-'} ${kategoriName}
</td> </td>
<td> <td>
${item.unit?.name || '-'} ${unitName}
</td> </td>
<td class="text-nowrap">${formatTanggal(item.entry_at)}</td> <td class="text-nowrap">${formatTanggal(item.entry_at)}</td>
</tr> </tr>
@ -603,12 +420,12 @@
} }
function renderTable(){ function renderTable(){
const pageData = filterByKategoriType(tableState.data || []); const pageData = tableState.data || [];
if(pageData.length === 0){ if(pageData.length === 0){
tbody.innerHTML = ` tbody.innerHTML = `
<tr> <tr>
<td colspan="8" class="text-center text-muted py-4"> <td colspan="7" class="text-center text-muted py-4">
Tidak ada data yang cocok Tidak ada data yang cocok
</td> </td>
</tr> </tr>
@ -626,85 +443,17 @@
renderPagination(tableState.lastPage || 1); renderPagination(tableState.lastPage || 1);
syncCheckAllState(); syncCheckAllState();
updateSelectedCount(); updateSelectedCount();
renderLegend(pageData);
} }
function applyTableSearch(){ function applyTableSearch(){
const value = searchInput ? searchInput.value : ''; const value = searchInput ? searchInput.value : '';
tableState.search = (value || '').trim(); tableState.search = (value || '').trim();
tableState.unit = unitSelect && window.$ ? ($('#tableUnit').val() || []) : (tableState.unit || []); tableState.unit = unitSelect && window.$ ? ($('#tableUnit').val() || []) : (tableState.unit || []);
const katVal = kategoriSelect && window.$ ? ($('#tableKategori').val() || '') : (kategoriSelect?.value || ''); tableState.kategori = kategoriSelect && window.$ ? ($('#tableKategori').val() || []) : (tableState.kategori || []);
tableState.kategoriType = katVal ? [katVal] : (tableState.kategoriType || []);
tableState.page = 1; tableState.page = 1;
fetchData(); fetchData();
} }
function getKategoriLabel(item){
const parts = String(item?.file || '').split('/');
return (parts[2] || item?.nama_kategori_directory || item?.kategori || '').trim();
}
function getKategoriId(item){
const label = getKategoriLabel(item);
return String(item?.master_kategori_directory_id || label);
}
function getKategoriOptionsFromData(){
const seen = new Map();
(tableState.data || []).forEach(item => {
const label = getKategoriLabel(item);
if(!label) return;
const id = getKategoriId(item);
if(!seen.has(id)){
seen.set(id, { id, label });
}
});
return Array.from(seen.values()).sort((a,b) => a.label.localeCompare(b.label));
}
function isKategoriMatch(item, types){
if (!types.length) return true;
const lowerTypes = types.map(t => String(t).toLowerCase());
const isAkre = (item.is_akre === true) || String(item.is_akre).toLowerCase() === 'true' || Number(item.is_akre) === 1;
const isHukum = !!item.kategori_hukum;
const hasKategoriId = !!item.master_kategori_directory_id;
if (isAkre && lowerTypes.includes('akreditasi')) return true;
if (isHukum && lowerTypes.includes('hukum')) return true;
if (!isAkre && !isHukum && hasKategoriId && lowerTypes.includes('lainnya')) return true;
const catId = String(getKategoriId(item)).toLowerCase();
const catLabel = String(getKategoriLabel(item)).toLowerCase();
return lowerTypes.includes(catId) || lowerTypes.includes(catLabel);
}
function filterByKategoriType(items){
const types = (tableState.kategoriType || []).map(v => String(v));
if (!types.length) return items;
return items.filter(item => isKategoriMatch(item, types));
}
function renderKategoriHeaderOptions(){
if (!kategoriHeaderMenu) return;
const list = kategoriHeaderMenu.querySelector('.kategori-header-list');
if (!list) return;
const options = kategoriOptionCache.length ? kategoriOptionCache : getKategoriOptionsFromData();
const selected = (tableState.kategoriHeader || []).map(v => String(v));
if(options.length === 0){
list.innerHTML = '<div class=\"dropdown-item text-muted\">Tidak ada kategori</div>';
return;
}
list.innerHTML = options.map(opt => {
const checked = selected.includes(opt.id) ? 'checked' : '';
return `
<label class="dropdown-item d-flex align-items-center gap-2">
<input type="checkbox" class="form-check-input m-0" value="${opt.id}" ${checked}>
<span>${opt.label}</span>
</label>
`;
}).join('');
}
function fetchData(){ function fetchData(){
if(summaryEl) summaryEl.textContent = 'Memuat data...'; if(summaryEl) summaryEl.textContent = 'Memuat data...';
const params = new URLSearchParams({ const params = new URLSearchParams({
@ -715,20 +464,15 @@
if (tableState.unit && tableState.unit.length > 0) { if (tableState.unit && tableState.unit.length > 0) {
tableState.unit.forEach(id => params.append('unit[]', id)); tableState.unit.forEach(id => params.append('unit[]', id));
} }
if (tableState.kategoriType && tableState.kategoriType.length > 0) { if (tableState.kategori && tableState.kategori.length > 0) {
tableState.kategoriType.forEach(id => params.append('kategori[]', id)); tableState.kategori.forEach(kat => params.append('kategori[]', kat));
}
if (tableState.kategoriHeader && tableState.kategoriHeader.length > 0) {
tableState.kategoriHeader.forEach(id => params.append('kategori_header[]', id));
} }
fetch(`/datatable-umum?${params.toString()}`) fetch(`/datatable-umum?${params.toString()}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
tableState.data = data?.data || []; tableState.data = data?.data || [];
kategoriOptionCache = data?.kategori_list || kategoriOptionCache;
tableState.lastPage = data?.pagination?.last_page || 1; tableState.lastPage = data?.pagination?.last_page || 1;
tableState.total = data?.pagination?.total || 0; tableState.total = data?.pagination?.total || 0;
renderKategoriHeaderOptions();
renderTable(); renderTable();
}) })
.catch(error => { .catch(error => {
@ -758,7 +502,6 @@
year: 'numeric' year: 'numeric'
}); });
} }
renderKategoriHeaderOptions();
fetchData() fetchData()
function updateSelectedCount(){ function updateSelectedCount(){
@ -1218,67 +961,5 @@
} }
$("#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

@ -1,10 +1,9 @@
@extends('layout.main') @extends('layout.main')
<style> <style>
/* --- SELECT2: teks terlihat (hitam) --- */ /* --- SELECT2: teks terlihat (hitam) --- */
.select2-container--default .select2-selection--multiple { .select2-container--default .select2-selection--multiple {
background: #fff !important; background: #fff !important;
border: 1px solid rgb(206, 212, 218) !important; border: 1px solid #ced4da !important;
} }
.select2-container--default .select2-selection--multiple .select2-selection__rendered { .select2-container--default .select2-selection--multiple .select2-selection__rendered {
@ -35,59 +34,6 @@
border: 1px solid #dee2e6 !important; border: 1px solid #dee2e6 !important;
color: #111 !important; color: #111 !important;
} }
.select2-container--default .select2-selection--single .select2-selection__clear {
display: inline-block !important;
font-size: 16px;
font-weight: bold;
color: #999;
margin-right: 8px;
cursor: pointer;
}
.table-header-filter .dropdown-menu {
z-index: 1080;
}
.table-fixed-height {
min-height: 70vh;
}
.table-responsive.table-fixed-height {
scroll-behavior: smooth;
scrollbar-width: thin;
scrollbar-color: #b8c7db transparent;
}
.table-responsive.table-fixed-height::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.table-responsive.table-fixed-height::-webkit-scrollbar-track {
background: transparent;
border-radius: 999px;
}
.table-responsive.table-fixed-height::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.72);
border-radius: 999px;
border: 2px solid transparent;
background-clip: padding-box;
}
.table-responsive.table-fixed-height::-webkit-scrollbar-thumb:hover {
background: rgba(100, 116, 139, 0.86);
border: 2px solid transparent;
background-clip: padding-box;
}
/* --- Warna kategori baris --- */
.row-shade {
background-color: var(--row-bg, transparent) !important;
transition: background-color 0.2s ease;
}
.legend-dot {
width: 14px;
height: 14px;
border-radius: 4px;
display: inline-block;
border: 1px solid rgba(0,0,0,0.08);
vertical-align: middle;
margin-right: 6px;
}
</style> </style>
@section('body_main') @section('body_main')
<div class="row"> <div class="row">
@ -125,7 +71,6 @@
</span> </span>
</div> </div>
<!-- Tambah Dokumen --> <!-- Tambah Dokumen -->
@if(!Auth::guard('admin')->check())
<button <button
type="button" type="button"
class="btn btn-success btn-sm" class="btn btn-success btn-sm"
@ -135,19 +80,17 @@
<i class="ti ti-plus me-1"></i> <i class="ti ti-plus me-1"></i>
Tambah Dokumen Tambah Dokumen
</button> </button>
@endif
</div> </div>
</div> </div>
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3"> <div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3">
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 flex-grow-1"> <div class="d-flex flex-column flex-md-row align-items-md-center gap-2 flex-grow-1">
<select id="tableUnit" style="max-width: 260px;"> <select id="tableUnit" class="form-select form-select-sm unit_kerja_filter" style="max-width: 260px;"></select>
<option value=""></option> <select id="tableKategori" class="form-select form-select-sm kategori_kerja_filter" style="max-width: 260px;" multiple>
</select> @foreach ($katDok as $kat)
<select id="tableKategori" class="form-select form-select-sm" style="max-width: 260px;"> <option value="{{ $kat->master_kategori_directory_id }}">
<option value="">Kategori (Semua)</option> {{ $kat->nama_kategori_directory }}
<option value="akreditasi">Kategori Akreditasi</option> </option>
<option value="hukum">Kategori Hukum</option> @endforeach
<option value="lainnya">Kategori Lainnya</option>
</select> </select>
<input type="search" <input type="search"
id="tableSearch" id="tableSearch"
@ -157,7 +100,7 @@
</div> </div>
<button class="btn btn-primary" type="button" id="btnTableSearch">Cari</button> <button class="btn btn-primary" type="button" id="btnTableSearch">Cari</button>
</div> </div>
<div class="table-responsive table-fixed-height" style="max-height: 70vh; overflow-y:auto;"> <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"> <table class="table table-sm table-hover align-middle mb-0 table-fixed" id="lastUpdatedTable">
<thead> <thead>
<tr> <tr>
@ -167,21 +110,7 @@
<th>Aksi</th> <th>Aksi</th>
<th>No Dokumen</th> <th>No Dokumen</th>
<th>Nama Dokumen</th> <th>Nama Dokumen</th>
<th> <th>Kategori</th>
<div class="d-flex align-items-center gap-2 table-header-filter">
<span>Kategori</span>
<div class="dropdown">
<button class="btn btn-light btn-sm border" type="button" id="tableKategoriHeaderBtn" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
<i class="ti ti-filter"></i>
</button>
<div class="dropdown-menu p-2" id="tableKategoriHeaderMenu" style="min-width: 220px;">
<div class="small text-muted px-1">Filter kategori (BETA)</div>
<div class="dropdown-divider"></div>
<div class="kategori-header-list"></div>
</div>
</div>
</div>
</th>
<th>Unit</th> <th>Unit</th>
<th>Tanggal Unggah</th> <th>Tanggal Unggah</th>
<th>Pengunggah</th> <th>Pengunggah</th>
@ -192,14 +121,13 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="d-flex flex-wrap align-items-center gap-2 mt-3"> <div class="d-flex flex-wrap align-items-center gap-2 mt-3">
<div class="d-flex align-items-center gap-1"> <div class="d-flex align-items-center gap-1">
<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">10</option> <option value="10" selected>10</option>
<option value="20" selected>20</option> <option value="20">20</option>
<option value="50">50</option> <option value="50">50</option>
<option value="100">100</option> <option value="100">100</option>
</select> </select>
@ -235,144 +163,27 @@
const authPegawai = @json(auth()->user()->objectpegawaifk); const authPegawai = @json(auth()->user()->objectpegawaifk);
const formCreate = $("#formFile"); const formCreate = $("#formFile");
const modalCreate = document.getElementById('modalCreateFile'); const modalCreate = document.getElementById('modalCreateFile');
const tableState = { data: [], page: 1, pageSize: 8, search: '', unit: [], kategori: [], kategoriType: [], kategoriHeader: [], lastPage: 1, total: 0 }; const tableState = { data: [], page: 1, pageSize: 8, search: '', unit: [], kategori: [], lastPage: 1, total: 0 };
let kategoriOptionCache = [];
const tbody = document.getElementById('tableDataUnit'); const tbody = document.getElementById('tableDataUnit');
const paginationEl = document.getElementById('paginationControls'); const paginationEl = document.getElementById('paginationControls');
const summaryEl = document.getElementById('tableSummary'); const summaryEl = document.getElementById('tableSummary');
const legendEl = document.getElementById('tableLegend');
const pageSizeSelect = document.getElementById('tablePageSize'); const pageSizeSelect = document.getElementById('tablePageSize');
const unitSelect = document.getElementById('tableUnit'); const unitSelect = document.getElementById('tableUnit');
const kategoriSelect = document.getElementById('tableKategori'); const kategoriSelect = document.getElementById('tableKategori');
const kategoriHeaderMenu = document.getElementById('tableKategoriHeaderMenu');
const searchInput = document.getElementById('tableSearch'); const searchInput = document.getElementById('tableSearch');
const searchBtn = document.getElementById('btnTableSearch'); const searchBtn = document.getElementById('btnTableSearch');
const downloadBtn = document.getElementById('btnDownloadMultiple'); const downloadBtn = document.getElementById('btnDownloadMultiple');
const selectedCountEl = document.getElementById('selectedCount'); const selectedCountEl = document.getElementById('selectedCount');
const checkAllEl = document.getElementById('checkAllRows'); const checkAllEl = document.getElementById('checkAllRows');
const selectedIds = new Set(); const selectedIds = new Set();
const colorCache = {};
const colorPalette = ['#e8f4ff', '#fff6e5', '#e9f7ef', '#f3e8ff', '#ffe6ea', '#e6f5f3'];
function normalizeToArray(value){
if (Array.isArray(value)) {
return value.filter(v => v !== null && v !== undefined && v !== '');
}
if (value === null || value === undefined || value === '') return [];
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('masa-berlaku-select')) return; if(!e.target.classList.contains('toggle-expired')) return;
syncMasaBerlakuField(e.target); const targetId = e.target.getAttribute('data-target');
}); if(!targetId) return;
const fieldWrap = document.getElementById(targetId);
document.addEventListener('change', function(e){ const input = fieldWrap.querySelector('input');
if (!e.target.matches('input[type="date"][id^="dateActive_"]')) return; input.disabled = !e.target.checked;
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){
@ -389,11 +200,13 @@
} }
if (window.$ && $.fn.select2) { if (window.$ && $.fn.select2) {
if (unitSelect) { if (unitSelect) {
$('#tableUnit').select2({ $('#tableUnit').select2({
placeholder: 'Pilih Unit', placeholder: 'Pilih Unit',
allowClear: true, allowClear: true,
width: '100%', width: '100%',
closeOnSelect: false, closeOnSelect: false,
selectionCssClass: 'select2-filter-selection',
dropdownCssClass: 'select2-filter-dropdown',
ajax: { ajax: {
url: '/select-unit-kerja-mapping', url: '/select-unit-kerja-mapping',
dataType: 'json', dataType: 'json',
@ -412,39 +225,24 @@
cache: true cache: true
} }
}); });
$('#tableUnit').on('change', function () {
tableState.unit = $(this).val() || [];
});
} }
if (kategoriSelect) { if (kategoriSelect) {
$('#tableKategori').select2({ $('#tableKategori').select2({
placeholder: 'Kategori (Semua)', placeholder: 'Pilih Kategori',
allowClear: true, allowClear: true,
width: '100%', width: '100%',
closeOnSelect: false,
selectionCssClass: 'select2-filter-selection', selectionCssClass: 'select2-filter-selection',
dropdownCssClass: 'select2-filter-dropdown' dropdownCssClass: 'select2-filter-dropdown'
}); });
$('#tableKategori').on('change', function () { $('#tableKategori').on('change', function () {
const val = $(this).val() || ''; tableState.kategori = $(this).val() || [];
tableState.kategoriType = val ? [val] : [];
tableState.page = 1;
fetchData();
}); });
} }
} }
if (kategoriHeaderMenu) {
kategoriHeaderMenu.addEventListener('change', function(e){
const checkbox = e.target.closest('input[type="checkbox"]');
if (!checkbox) return;
const selected = Array.from(kategoriHeaderMenu.querySelectorAll('input[type="checkbox"]:checked'))
.map(el => el.value);
tableState.kategoriHeader = selected;
if (kategoriSelect && window.$ && $.fn.select2) {
const single = selected.length === 1 ? selected[0] : '';
$('#tableKategori').val(single).trigger('change');
}
tableState.page = 1;
fetchData();
});
}
function resetCreateForm(){ function resetCreateForm(){
colCount = 1; colCount = 1;
@ -453,7 +251,6 @@
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);
} }
@ -464,7 +261,7 @@
function loadAkreData(){ function loadAkreData(){
if(akreLoaded) return Promise.resolve(akreData); if(akreLoaded) return Promise.resolve(akreData);
return fetch('/json/akreditasi.jff') return fetch('/json/akreditasi.json')
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
akreData = Array.isArray(data) ? data : []; akreData = Array.isArray(data) ? data : [];
@ -488,7 +285,7 @@
const children = Array.isArray(seg.turunan) ? seg.turunan : []; const children = Array.isArray(seg.turunan) ? seg.turunan : [];
return children.map(child => ({ return children.map(child => ({
value: `${type.name}/${seg.name}/${child.name}`, value: `${type.name}/${seg.name}/${child.name}`,
label: `${type.name} / ${child.name}`, label: `${type.name} / ${seg.name} / ${child.name}`,
type: type.name, type: type.name,
segment: seg.name, segment: seg.name,
item: child.name item: child.name
@ -536,18 +333,17 @@
function enableAkreFields(index){ function enableAkreFields(index){
const selectEl = document.getElementById(`akre_select_${index}`); const selectEl = document.getElementById(`akre_select_${index}`);
// if(selectEl){ if(selectEl){
// selectEl.disabled = false; selectEl.disabled = false;
// selectEl.required = true; selectEl.required = true;
// } }
setKategoriRequired(index, false); setKategoriRequired(index, false);
loadAkreData().then(() => { loadAkreData().then(() => {
fillAkreSelect(selectEl); fillAkreSelect(selectEl);
if(window.$ && $.fn.select2){ if(window.$ && $.fn.select2){
$(selectEl).select2({ $(selectEl).select2({
dropdownParent: $('#modalCreateFile'), dropdownParent: $('#modalCreateFile'),
placeholder: 'Pilih Instrumen', placeholder: 'Pilih Instrumen'
allowClear: true
}); });
} }
}); });
@ -560,8 +356,7 @@
if(katSelect.length){ if(katSelect.length){
katSelect.select2({ katSelect.select2({
dropdownParent: $('#modalCreateFile'), dropdownParent: $('#modalCreateFile'),
placeholder:'Pilih Kategori', placeholder:'Pilih Kategori'
allowClear: true
}); });
} }
if(hukumSelect.length){ if(hukumSelect.length){
@ -592,48 +387,7 @@
function isPublic(permissionVal){ function isPublic(permissionVal){
if(permissionVal === null || permissionVal === undefined) return false; if(permissionVal === null || permissionVal === undefined) return false;
const val = String(permissionVal).toLowerCase(); const val = String(permissionVal).toLowerCase();
return val === '1' || val === 'true' || val === 'ya' || val === 'yes'; return val === '1' || val === 'true' || val === 'iya' || val === 'yes';
}
function resolveKategoriFlag(item){
if(Number(item.is_akre) === 1 || item.is_akre === true || String(item.is_akre).toLowerCase() === 'true'){
return { key: 'akre', label: 'Kategori Akreditasi' };
}
if(item.kategori_hukum){
return { key: 'hukum', label: 'Kategori Hukum' };
}
const label = (item.nama_kategori || item.nama_kategori_directory || item.kategori || '').trim() || 'Kategori Lainnya';
const key = String(item.master_kategori_directory_id || label || 'lainnya');
return { key, label };
}
function pickColor(key, label){
if(colorCache[key]) return colorCache[key];
const index = Object.keys(colorCache).length % colorPalette.length;
colorCache[key] = colorPalette[index];
return colorCache[key];
}
function renderLegend(items){
if(!legendEl) return;
const map = new Map();
(items || []).forEach(item => {
const flag = resolveKategoriFlag(item);
const color = pickColor(flag.key, flag.label);
if(!map.has(flag.key)){
map.set(flag.key, { label: flag.label, color });
}
});
if(map.size === 0){
legendEl.textContent = '';
return;
}
legendEl.innerHTML = Array.from(map.values()).map(entry => `
<span class="me-3">
<span class="legend-dot" style="background:${entry.color};"></span>
<span>${entry.label}</span>
</span>
`).join('');
} }
function getExpiryStatus(dateStr){ function getExpiryStatus(dateStr){
@ -649,31 +403,6 @@
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() || '-';
@ -694,18 +423,12 @@
statusClass = 'bg-secondary'; statusClass = 'bg-secondary';
} }
const checked = selectedIds.has(String(item.file_directory_id)) ? 'checked' : ''; const checked = selectedIds.has(String(item.file_directory_id)) ? 'checked' : '';
const kategoriFlag = resolveKategoriFlag(item); const rowClass = expiryStatus === 'expired' ? 'table-danger' : (expiryStatus === 'soon' ? 'table-warning' : '');
const rowColor = pickColor(kategoriFlag.key, kategoriFlag.label);
const isAkre = kategoriFlag.key === 'akre';
const rowClass = isAkre ? 'table-info' : (expiryStatus === 'expired' ? 'table-danger' : (expiryStatus === 'soon' ? 'table-warning' : 'row-shade'));
const expiryBadge = expiryStatus === 'expired' const expiryBadge = expiryStatus === 'expired'
? `<span class="badge bg-danger" style="font-size:10px;">Expired</span>` ? `<span class="badge bg-danger" style="font-size:10px;">Expired</span>`
: (expiryStatus === 'soon' ? `<span class="badge bg-warning text-dark" style="font-size:10px;">Akan Expired</span>` : ''); : (expiryStatus === 'soon' ? `<span class="badge bg-warning text-dark" style="font-size:10px;">Akan Expired</span>` : '');
const akreBadge = isAkre
? `<span class="badge bg-primary text-white ms-2" style="font-size:10px;">Akreditasi</span>`
: '';
return ` return `
<tr class="${rowClass}" style="--row-bg:${rowColor};"> <tr class="${rowClass}">
<td class="text-center"> <td class="text-center">
<input type="checkbox" <input type="checkbox"
class="form-check-input row-check" class="form-check-input row-check"
@ -737,23 +460,6 @@
> >
<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>
@ -771,18 +477,17 @@
> >
${item.nama_dokumen || '-'} ${item.nama_dokumen || '-'}
</a> </a>
${akreBadge}${expiryBadge} ${expiryBadge}
</div> </div>
${renderDeleteRecommendation(item)}
</td> </td>
<td> <td>
${item.nama_kategori || '-'} ${kategoriName}
</td> </td>
<td> <td>
${item.unit?.name || '-'} ${unitName}
</td> </td>
<td class="text-nowrap">${formatTanggal(item.entry_at)}</td> <td class="text-nowrap">${formatTanggal(item.entry_at)}</td>
<td style="max-width: 200px; white-space: normal; word-wrap: break-word;">${item.pegawai_nama_entry || '-'}</td> <td class="text-nowrap">${item.pegawai_nama_entry || '-'}</td>
</tr> </tr>
`; `;
} }
@ -831,12 +536,12 @@
} }
function renderTable(){ function renderTable(){
const pageData = filterByKategoriType(tableState.data || []); const pageData = tableState.data || [];
if(pageData.length === 0){ if(pageData.length === 0){
tbody.innerHTML = ` tbody.innerHTML = `
<tr> <tr>
<td colspan="8" class="text-center text-muted py-4"> <td colspan="7" class="text-center text-muted py-4">
Tidak ada data yang cocok Tidak ada data yang cocok
</td> </td>
</tr> </tr>
@ -854,110 +559,17 @@
renderPagination(tableState.lastPage || 1); renderPagination(tableState.lastPage || 1);
syncCheckAllState(); syncCheckAllState();
updateSelectedCount(); updateSelectedCount();
renderLegend(pageData);
} }
function applyTableSearch(){ function applyTableSearch(){
const value = searchInput ? searchInput.value : ''; const value = searchInput ? searchInput.value : '';
tableState.search = (value || '').trim(); tableState.search = (value || '').trim();
tableState.unit = normalizeToArray( tableState.unit = unitSelect && window.$ ? ($('#tableUnit').val() || []) : (tableState.unit || []);
unitSelect && window.$ ? $('#tableUnit').val() : tableState.unit tableState.kategori = kategoriSelect && window.$ ? ($('#tableKategori').val() || []) : (tableState.kategori || []);
);
const katVal = kategoriSelect && window.$ ? ($('#tableKategori').val() || '') : (kategoriSelect?.value || '');
tableState.kategoriType = katVal ? [katVal] : (tableState.kategoriType || []);
tableState.page = 1; tableState.page = 1;
fetchData(); fetchData();
} }
function getKategoriLabel(item){
const parts = String(item?.file || '').split('/');
return (item?.nama_kategori || parts[2] || item?.nama_kategori_directory || item?.kategori || '').trim();
}
function getKategoriId(item){
const label = getKategoriLabel(item);
return String(item?.master_kategori_directory_id || label);
}
function getKategoriOptionsFromData(){
const seen = new Map();
(tableState.data || []).forEach(item => {
const label = getKategoriLabel(item);
if(!label) return;
const id = getKategoriId(item);
if(!seen.has(id)){
seen.set(id, { id, label });
}
});
return Array.from(seen.values()).sort((a,b) => a.label.localeCompare(b.label));
}
function isKategoriMatch(item, types){
if (!types.length) return true;
const lowerTypes = types.map(t => String(t).toLowerCase());
const isAkre = (item.is_akre === true) || String(item.is_akre).toLowerCase() === 'true' || Number(item.is_akre) === 1;
const isHukum = !!item.kategori_hukum;
const hasKategoriId = !!item.master_kategori_directory_id;
// special buckets
if (isAkre && lowerTypes.includes('akreditasi')) return true;
if (isHukum && lowerTypes.includes('hukum')) return true;
if (!isAkre && !isHukum && hasKategoriId && lowerTypes.includes('lainnya')) return true;
// fallback to id/label comparison
const catId = String(getKategoriId(item)).toLowerCase();
const catLabel = String(getKategoriLabel(item)).toLowerCase();
return lowerTypes.includes(catId) || lowerTypes.includes(catLabel);
}
function filterByKategoriType(items){
const types = (tableState.kategoriType || []).map(v => String(v));
if (!types.length) return items;
return items.filter(item => isKategoriMatch(item, types));
}
function renderKategoriHeaderOptions(){
if (!kategoriHeaderMenu) return;
const list = kategoriHeaderMenu.querySelector('.kategori-header-list');
if (!list) return;
const options = kategoriOptionCache.length ? kategoriOptionCache : getKategoriOptionsFromData();
const selected = (tableState.kategoriHeader || []).map(v => String(v));
if(options.length === 0){
list.innerHTML = '<div class=\"dropdown-item text-muted\">Tidak ada kategori</div>';
return;
}
list.innerHTML = options.map(opt => {
const checked = selected.includes(opt.id) ? 'checked' : '';
return `
<label class="dropdown-item d-flex align-items-center gap-2 kategori-option" data-kat="${opt.id}">
<input type="checkbox" class="form-check-input m-0" value="${opt.id}" ${checked}>
<span>${opt.label}</span>
</label>
`;
}).join('');
}
if (kategoriHeaderMenu) {
kategoriHeaderMenu.addEventListener('click', function(e){
const option = e.target.closest('.kategori-option');
if (!option) return;
const id = option.getAttribute('data-kat');
const checkbox = option.querySelector('input[type="checkbox"]');
if(checkbox){
checkbox.checked = !checkbox.checked;
const event = new Event('change', { bubbles: true });
checkbox.dispatchEvent(event);
}else{
tableState.kategoriHeader = [id];
if (kategoriSelect && window.$ && $.fn.select2) {
$('#tableKategori').val(id).trigger('change');
}
tableState.page = 1;
fetchData();
}
}, true);
}
function fetchData(){ function fetchData(){
if(summaryEl) summaryEl.textContent = 'Memuat data...'; if(summaryEl) summaryEl.textContent = 'Memuat data...';
const params = new URLSearchParams({ const params = new URLSearchParams({
@ -965,24 +577,18 @@
per_page: tableState.pageSize, per_page: tableState.pageSize,
keyword: tableState.search keyword: tableState.search
}); });
const unitValues = normalizeToArray(tableState.unit); if (tableState.unit && tableState.unit.length > 0) {
if (unitValues.length > 0) { tableState.unit.forEach(id => params.append('unit[]', id));
unitValues.forEach(id => params.append('unit[]', id));
} }
if (tableState.kategoriType && tableState.kategoriType.length > 0) { if (tableState.kategori && tableState.kategori.length > 0) {
tableState.kategoriType.forEach(id => params.append('kategori[]', id)); tableState.kategori.forEach(kat => params.append('kategori[]', kat));
}
if (tableState.kategoriHeader && tableState.kategoriHeader.length > 0) {
tableState.kategoriHeader.forEach(id => params.append('kategori_header[]', id));
} }
fetch(`/data-internal?${params.toString()}`) fetch(`/data-internal?${params.toString()}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
tableState.data = data?.data || []; tableState.data = data?.data || [];
kategoriOptionCache = data?.kategori_list || kategoriOptionCache;
tableState.lastPage = data?.pagination?.last_page || 1; tableState.lastPage = data?.pagination?.last_page || 1;
tableState.total = data?.pagination?.total || 0; tableState.total = data?.pagination?.total || 0;
renderKategoriHeaderOptions();
renderTable(); renderTable();
}) })
.catch(error => { .catch(error => {
@ -991,13 +597,17 @@
}) })
} }
searchBtn.addEventListener('click', applyTableSearch); if (searchBtn) {
searchInput.addEventListener('keydown', (e) => { searchBtn.addEventListener('click', applyTableSearch);
}
if (searchInput) {
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
applyTableSearch(); applyTableSearch();
} }
}); });
}
function formatTanggal(dateString) { function formatTanggal(dateString) {
const d = new Date(dateString); const d = new Date(dateString);
@ -1007,7 +617,6 @@
year: 'numeric' year: 'numeric'
}); });
} }
renderKategoriHeaderOptions();
fetchData() fetchData()
function updateSelectedCount(){ function updateSelectedCount(){
@ -1159,7 +768,6 @@
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){
@ -1169,8 +777,6 @@
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);
@ -1240,39 +846,28 @@
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]">
id="dateActive_${colCount}"> </div>
</div> <div class="col-md-3">
<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"
<select class="form-select masa-berlaku-select" type="checkbox"
name="data[${colCount}][masa_berlaku_option]" id="hasExpired_${colCount}"
data-index="${colCount}" data-target="expiredField_${colCount}">
data-date-target="expiredField_${colCount}" <label class="form-check-label" for="hasExpired_${colCount}">Masa Berlaku Dokumen??</label>
data-input-target="expiredInput_${colCount}" </div>
data-base-target="dateActive_${colCount}" </div>
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-4 d-none" id="expiredField_${colCount}"> <div class="col-md-5" 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"
id="expiredInput_${colCount}" name="data[${colCount}][tgl_expired]" disabled>
name="data[${colCount}][tgl_expired]" 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>
@ -1285,7 +880,7 @@
id="perm_yes_${colCount}" id="perm_yes_${colCount}"
value="1" value="1"
required> required>
<label class="form-check-label" for="perm_yes_${colCount}">Ya</label> <label class="form-check-label" for="perm_yes_${colCount}">Iya</label>
</div> </div>
<div class="form-check mt-1"> <div class="form-check mt-1">
@ -1302,29 +897,33 @@
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Instrumen Akreditasi </label> <label class="form-label fw-semibold">Instrumen Akreditasi </label>
<select class="form-select akre-select" id="akre_select_${colCount}" name="data[${colCount}][akre]" style="width: 350px;"> <select class="form-select akre-select" id="akre_select_${colCount}" style="width: 350px;">
<option value="" disabled selected>Pilih Instrumen</option> <option value="" disabled selected>Pilih Instrumen</option>
</select> </select>
<input type="hidden" name="data[${colCount}][akre_type]" id="akre_type_${colCount}">
<input type="hidden" name="data[${colCount}][akre_segment]" id="akre_segment_${colCount}">
<input type="hidden" name="data[${colCount}][akre_item]" id="akre_item_${colCount}">
<input type="hidden" name="data[${colCount}][is_akre]" value="1">
<div class="form-text text-muted">Isi form ini bila dokumen yang diunggah merupakan dokumen <strong>akreditasi</strong>.</div> <div class="form-text text-muted">Isi form ini bila dokumen yang diunggah merupakan dokumen <strong>akreditasi</strong>.</div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Kategori Hukum</label> <label class="form-label fw-semibold">Kategori Hukum</label>
<select class="form-select select-kat-hukum" name="data[${colCount}][kategori_hukum]" id="select_kategori_hukum_${colCount}" style="width: 350px;"> <select class="form-select select-kat-hukum" name="data[${colCount}][kategori_hukum]" id="select_kategori_hukum_${colCount}" style="width: 350px;">
<option value="" disabled selected>Pilih Kategori Hukum</option> <option value="" disabled selected>Pilih Kategori Hukum</option>
<option value="Kebijakan - Peraturan Direktur">Kebijakan - Peraturan Direktur</option> <option value="Pedir - Kebijakan">Pedir - Kebijakan</option>
<option value="Kebijakan - Keputusan Direktur Utama">Kebijakan - Keputusan Direktur Utama</option> <option value="Keputusan Dirut - Kebijakan">Keputusan Dirut - Kebijakan</option>
<option value="Kebijakan - Surat Edaran">Kebijakan - Surat Edaran</option> <option value="Surat Edaran - Kebijakan">Surat Edaran - Kebijakan</option>
<option value="Kebijakan - Pengumuman">Kebijakan - Pengumuman</option> <option value="Pengumuman - Kebijakan">Pengumuman - Kebijakan</option>
<option value="Kerjasama - Pelayanan Kesehatan">Kerjasama - Pelayanan Kesehatan</option> <option value="Pelayanan Kesehatan - Kerjasama">Pelayanan Kesehatan - Kerjasama</option>
<option value="Kerjasama - Management">Kerjasama - Management</option> <option value="Management - Kerjasama">Management - Kerjasama</option>
<option value="Kerjasama - Pemeliharan">Kerjasama - Pemeliharan</option> <option value="Pemeliharan - Kerjasama">Pemeliharan - Kerjasama</option>
<option value="Kerjasama - Diklat">Kerjasama - Diklat</option> <option value="iklat - Kerjasama">Diklat - Kerjasama</option>
<option value="Kerjasama - Luar Negeri">Kerjasama - Luar Negeri</option> <option value="Luar Negeri - Kerjasama">Luar Negeri - Kerjasama</option>
<option value="Kerjasama - Area Bisnis">Kerjasama - Area Bisnis</option> <option value="Area Bisnis - Kerjasama">Area Bisnis - Kerjasama</option>
<option value="Kerjasama - Pendidikan">Kerjasama - Pendidikan</option> <option value="Pendidikan - Kerjasama">Pendidikan - Kerjasama</option>
<option value="Kerjasama - Pengampuan KIA">Kerjasama- Pengampuan KIA</option> <option value="Pengampuan KIA">Pengampuan KIA</option>
<option value="Kerjasama - SDM">Kerjasama- SDM</option>
</select> </select>
<div class="form-text text-muted">Isi form ini bila dokumen yang diunggah merupakan dokumen <strong>akreditasi</strong>.</div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Kategori Lainnya</label> <label class="form-label fw-semibold">Kategori Lainnya</label>
@ -1391,7 +990,7 @@
processResults: function(data){ processResults: function(data){
return { return {
results : data?.data.map(item => ({ results : data?.data.map(item => ({
id: item.id, id: item.id+'/'+item.name,
text: item.name, text: item.name,
sub_units: item.sub_unit_kerja // kirim ke front sub_units: item.sub_unit_kerja // kirim ke front
})) }))
@ -1541,18 +1140,6 @@
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);
@ -1657,67 +1244,5 @@
} }
$("#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,29 +46,17 @@
</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]" id="dateActive_0"> <input class="form-control" type="date" name="data[0][date_active]">
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Masa Berlaku Dokumen</label> <div class="form-check">
<select class="form-select masa-berlaku-select" <input class="form-check-input toggle-expired" type="checkbox" id="hasExpired0" data-target="expiredField_0">
name="data[0][masa_berlaku_option]" <label class="form-check-label" for="hasExpired0">Masa Berlaku Dokumen?</label>
data-index="0" </div>
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 d-none" id="expiredField_0"> <div class="col-md-4" 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]" id="expiredInput_0" disabled> <input class="form-control" type="date" name="data[0][tgl_expired]" 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>
@ -77,7 +65,7 @@
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="data[0][is_permission]" id="perm_yes" value="1" required> <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"> <label class="form-check-label" for="perm_yes">
Ya Iya
</label> </label>
</div> </div>
<div class="form-check mt-1"> <div class="form-check mt-1">
@ -90,29 +78,33 @@
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Instrumen Akreditasi</label> <label class="form-label fw-semibold">Instrumen Akreditasi</label>
<select class="form-select akre-select" name="data[0][akre]" id="akre_select_0" style="width: 350px;"> <select class="form-select akre-select" id="akre_select_0" style="width: 350px;">
<option value="">Pilih Instrumen</option> <option value="">Pilih Instrumen</option>
</select> </select>
<input type="hidden" name="data[0][akre_type]" id="akre_type_0">
<input type="hidden" name="data[0][akre_segment]" id="akre_segment_0">
<input type="hidden" name="data[0][akre_item]" id="akre_item_0">
<input type="hidden" name="data[0][is_akre]" value="1">
<div class="form-text text-muted">Isi form ini bila dokumen yang diunggah merupakan dokumen <strong>akreditasi</strong>.</div> <div class="form-text text-muted">Isi form ini bila dokumen yang diunggah merupakan dokumen <strong>akreditasi</strong>.</div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Kategori Hukum</label> <label class="form-label fw-semibold">Kategori Hukum</label>
<select class="form-select select-kat-hukum" name="data[0][kategori_hukum]" id="select_kategori_hukum_0" style="width: 350px;"> <select class="form-select select-kat-hukum" name="data[0][kategori_hukum]" id="select_kategori_hukum_0" style="width: 350px;">
<option value="">Pilih Kategori Hukum</option> <option value="">Pilih Kategori Hukum</option>
<option value="Kebijakan - Peraturan Direktur">Kebijakan - Peraturan Direktur</option> <option value="Pedir - Kebijakan">Pedir - Kebijakan</option>
<option value="Kebijakan - Keputusan Direktur Utama">Kebijakan - Keputusan Direktur Utama</option> <option value="Keputusan Dirut - Kebijakan">Keputusan Dirut - Kebijakan</option>
<option value="Kebijakan - Surat Edaran">Kebijakan - Surat Edaran</option> <option value="Surat Edaran - Kebijakan">Surat Edaran - Kebijakan</option>
<option value="Kebijakan - Pengumuman">Kebijakan - Pengumuman</option> <option value="Pengumuman - Kebijakan">Pengumuman - Kebijakan</option>
<option value="Kerjasama - Pelayanan Kesehatan">Kerjasama - Pelayanan Kesehatan</option> <option value="Pelayanan Kesehatan - Kerjasama">Pelayanan Kesehatan - Kerjasama</option>
<option value="Kerjasama - Management">Kerjasama - Management</option> <option value="Management - Kerjasama">Management - Kerjasama</option>
<option value="Kerjasama - Pemeliharan">Kerjasama - Pemeliharan</option> <option value="Pemeliharan - Kerjasama">Pemeliharan - Kerjasama</option>
<option value="Kerjasama - Diklat">Kerjasama - Diklat</option> <option value="iklat - Kerjasama">Diklat - Kerjasama</option>
<option value="Kerjasama - Luar Negeri">Kerjasama - Luar Negeri</option> <option value="Luar Negeri - Kerjasama">Luar Negeri - Kerjasama</option>
<option value="Kerjasama - Area Bisnis">Kerjasama - Area Bisnis</option> <option value="Area Bisnis - Kerjasama">Area Bisnis - Kerjasama</option>
<option value="Kerjasama - Pendidikan">Kerjasama - Pendidikan</option> <option value="Pendidikan - Kerjasama">Pendidikan - Kerjasama</option>
<option value="Kerjasama - Pengampuan KIA">Kerjasama- Pengampuan KIA</option> <option value="Pengampuan KIA">Pengampuan KIA</option>
<option value="Kerjasama - SDM">Kerjasama- SDM</option>
</select> </select>
<div class="form-text text-muted">Isi form ini bila dokumen yang diunggah merupakan dokumen <strong>akreditasi</strong>.</div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Kategori lainnya</label> <label class="form-label fw-semibold">Kategori lainnya</label>

View File

@ -1,32 +1,4 @@
@php($showRecapTitle = $showRecapTitle ?? true) @php($showRecapTitle = $showRecapTitle ?? true)
<style>
.recap-scroll-area {
max-height: 55vh;
overflow-y: auto;
scroll-behavior: smooth;
scrollbar-width: thin;
scrollbar-color: #b8c7db transparent;
}
.recap-scroll-area::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.recap-scroll-area::-webkit-scrollbar-track {
background: transparent;
border-radius: 999px;
}
.recap-scroll-area::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.72);
border-radius: 999px;
border: 2px solid transparent;
background-clip: padding-box;
}
.recap-scroll-area::-webkit-scrollbar-thumb:hover {
background: rgba(100, 116, 139, 0.86);
border: 2px solid transparent;
background-clip: padding-box;
}
</style>
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3"> <div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3">
@if ($showRecapTitle) @if ($showRecapTitle)
<div> <div>
@ -53,12 +25,12 @@
</button> </button>
</div> </div>
</div> </div>
<div class="table-responsive recap-scroll-area"> <div class="table-responsive" style="max-height: 55vh; overflow-y:auto;">
<table class="table table-sm table-hover align-middle"> <table class="table table-sm table-hover align-middle">
<thead class="table-light shadow-sm"> <thead class="table-light shadow-sm">
<tr> <tr>
<th style="width:5%;" class="text-center">#</th> <th style="width:5%;" class="text-center">#</th>
<th style="width:30%;">Unit / Akreditasi</th> <th style="width:30%;">Unit</th>
<th style="width:20%;">Kategori</th> <th style="width:20%;">Kategori</th>
<th style="width:15%;" class="text-center">Jumlah File</th> <th style="width:15%;" class="text-center">Jumlah File</th>
</tr> </tr>
@ -70,16 +42,13 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="small text-muted mt-2" id="recapSummary">Memuat ringkasan rekap...</div>
<div class="d-flex flex-column flex-md-row align-items-center justify-content-between gap-2 mt-3" id="recapPagination"></div> <div class="d-flex flex-column flex-md-row align-items-center justify-content-between gap-2 mt-3" id="recapPagination"></div>
<script> <script>
document.addEventListener('DOMContentLoaded', () => fetchRecap()); document.addEventListener('DOMContentLoaded', () => fetchRecap());
let recapDebounce; let recapDebounce;
const recapState = { page:1, perPage:10, keyword:'', lastPage:1, totalUnits:0, grandTotalFiles:0, currentPageTotalFiles:0 }; const recapState = { page:1, perPage:10, keyword:'', lastPage:1 };
let recapRequestController = null;
let recapRequestToken = 0;
function debouncedRecapSearch(val){ function debouncedRecapSearch(val){
clearTimeout(recapDebounce); clearTimeout(recapDebounce);
@ -99,77 +68,48 @@ function changePerPage(val){
function fetchRecap(){ function fetchRecap(){
const tbody = document.getElementById('recapBody'); const tbody = document.getElementById('recapBody');
const pager = document.getElementById('recapPagination'); const pager = document.getElementById('recapPagination');
const summaryEl = document.getElementById('recapSummary');
if(!tbody) return; if(!tbody) return;
const requestToken = ++recapRequestToken;
if (recapRequestController) {
recapRequestController.abort();
}
recapRequestController = new AbortController();
tbody.innerHTML = `<tr><td colspan="4" class="text-center text-muted py-4">Memuat data...</td></tr>`; tbody.innerHTML = `<tr><td colspan="4" class="text-center text-muted py-4">Memuat data...</td></tr>`;
if(pager) pager.innerHTML = ''; if(pager) pager.innerHTML = '';
if(summaryEl) summaryEl.textContent = 'Memuat ringkasan rekap...';
const params = new URLSearchParams({ const params = new URLSearchParams({
page: recapState.page, page: recapState.page,
per_page: recapState.perPage, per_page: recapState.perPage,
keyword: recapState.keyword || '' keyword: recapState.keyword || ''
}); });
fetch('/data/recap?' + params.toString(), { fetch('/data/recap?' + params.toString())
signal: recapRequestController.signal,
})
.then(res => res.json()) .then(res => res.json())
.then(json => { .then(json => {
if (requestToken !== recapRequestToken) return;
const rows = json?.data || []; const rows = json?.data || [];
recapState.page = json?.pagination?.current_page || recapState.page;
recapState.lastPage = json?.pagination?.last_page || 1; recapState.lastPage = json?.pagination?.last_page || 1;
recapState.totalUnits = json?.summary?.total_units || 0;
recapState.grandTotalFiles = json?.summary?.grand_total_files || 0;
recapState.currentPageTotalFiles = json?.summary?.current_page_total_files || 0;
if(!rows.length){ if(!rows.length){
tbody.innerHTML = `<tr><td colspan="4" class="text-center text-muted py-4">Tidak ada data</td></tr>`; tbody.innerHTML = `<tr><td colspan="4" class="text-center text-muted py-4">Tidak ada data</td></tr>`;
if(summaryEl) summaryEl.textContent = recapState.grandTotalFiles
? `Total file hasil filter: ${recapState.grandTotalFiles}`
: 'Tidak ada data rekap';
return; return;
} }
let grandTotal = 0;
const html = rows.map((row, idx) => { const html = rows.map((row, idx) => {
const rowNumber = ((recapState.page - 1) * recapState.perPage) + idx + 1;
const folderRows = (row.data || []).map((f, i) => ` const folderRows = (row.data || []).map((f, i) => `
<tr> <tr>
${i === 0 ? `<td rowspan="${row.data.length}" class="text-center align-middle fw-semibold">${rowNumber}</td>` : ''} ${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>` : ''} ${i === 0 ? `<td rowspan="${row.data.length}" class="fw-semibold">${row.unit || '-'}</td>` : ''}
<td>${f.folder || '-'}</td> <td>${f.folder || '-'}</td>
<td class="text-center fw-bold">${f.count || 0}</td> <td class="text-center fw-bold">${f.count || 0}</td>
</tr> </tr>
`).join(''); `).join('');
(row.data || []).forEach(f => { grandTotal += (parseInt(f.count, 10) || 0); });
return folderRows; return folderRows;
}).join(''); }).join('');
tbody.innerHTML = html + ` tbody.innerHTML = html + `
<tr class="table-light"> <tr class="table-light">
<td colspan="3" class="text-end fw-semibold">Subtotal Halaman Ini</td> <td colspan="3" class="text-end fw-semibold">Total File</td>
<td class="text-center fw-bold">${recapState.currentPageTotalFiles}</td> <td class="text-center fw-bold">${grandTotal}</td>
</tr> </tr>
`; `;
if(summaryEl) {
const from = recapState.totalUnits === 0 ? 0 : ((recapState.page - 1) * recapState.perPage) + 1;
const to = Math.min(((recapState.page - 1) * recapState.perPage) + rows.length, recapState.totalUnits);
summaryEl.textContent = `Menampilkan ${from}-${to} dari ${recapState.totalUnits} unit rekap. Total file hasil filter: ${recapState.grandTotalFiles}.`;
}
renderRecapPagination(); renderRecapPagination();
}) })
.catch(err => { .catch(err => {
if (err?.name === 'AbortError') return;
console.error(err); console.error(err);
tbody.innerHTML = `<tr><td colspan="4" class="text-center text-danger py-4">Gagal memuat data</td></tr>`; tbody.innerHTML = `<tr><td colspan="4" class="text-center text-danger py-4">Gagal memuat data</td></tr>`;
const summaryEl = document.getElementById('recapSummary');
if(summaryEl) summaryEl.textContent = 'Gagal memuat ringkasan rekap';
})
.finally(() => {
if (requestToken === recapRequestToken) {
recapRequestController = null;
}
}); });
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,152 +0,0 @@
@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 Expired</h4>
<small class="text-muted">Ringkasan jumlah file per Unit dan Kategori</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 / Akreditasi</th>
<th style="width:20%;">Kategori</th>
<th style="width:15%;" class="text-center">Jumlah File</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/recapExp?' + 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;
}
let grandTotal = 0;
const html = rows.map((row, idx) => {
const folderRows = (row.data || []).map((f, i) => `
<tr>
${i === 0 ? `<td rowspan="${row.data.length}" class="text-center align-middle fw-semibold">${idx+1}</td>` : ''}
${i === 0 ? `<td rowspan="${row.data.length}" class="fw-semibold">${row.unit || '-'}</td>` : ''}
<td>${f.folder || '-'}</td>
<td class="text-center fw-bold">${f.count || 0}</td>
</tr>
`).join('');
(row.data || []).forEach(f => { grandTotal += (parseInt(f.count, 10) || 0); });
return folderRows;
}).join('');
tbody.innerHTML = html + `
<tr class="table-light">
<td colspan="3" class="text-end fw-semibold">Total File</td>
<td class="text-center fw-bold">${grandTotal}</td>
</tr>
`;
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

@ -21,55 +21,6 @@
<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

@ -41,69 +41,44 @@
<span class="hide-menu">Dokumen Umum</span> <span class="hide-menu">Dokumen Umum</span>
</a> </a>
</li> </li>
@if(Auth::guard('admin')->check() || (Auth::check() && auth()->user()->dataUser->mappingUnitKerjaPegawai()->whereIn('objectunitkerjapegawaifk', [51, 22])->exists()))
<li class="sidebar-item"> <li class="sidebar-item">
<a class="sidebar-link" href="{{ url('/data-akreditasi') }}" aria-expanded="false"> <a class="sidebar-link" href="{{ url('/data-akreditasi') }}" aria-expanded="false">
<i class="fa-solid fa-sliders"></i> <i class="ti ti-layout-dashboard"></i>
<span class="hide-menu">Dokumen Akreditasi</span> <span class="hide-menu">Dokumen Akreditasi</span>
</a> </a>
</li> </li>
@endif
{{-- 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
$pegawaiId = auth()->user()->objectpegawaifk; $isAtasan = \App\Models\MappingUnitKerjaPegawai::where('statusenabled', true)->where('objectatasanlangsungfk', auth()->user()->objectpegawaifk)->exists();
$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()) <li class="sidebar-item">
<li class="sidebar-item"> <a class="sidebar-link d-flex align-items-center justify-content-between"
<a class="sidebar-link d-flex align-items-center justify-content-between" href="{{ url('/pending-file') }}" aria-expanded="false">
href="{{ url('/pending-file') }}" aria-expanded="false">
<div class="d-flex align-items-center gap-3"> <div class="d-flex align-items-center gap-3">
<i class="ti ti-clock"></i> <i class="ti ti-clock"></i>
<span class="hide-menu">Persetujuan</span> <span class="hide-menu">Persetujuan</span>
</div> </div>
<span class="badge bg-danger rounded-pill d-none" id="pendingCountBadge">0</span> <span class="badge bg-danger rounded-pill d-none" id="pendingCountBadge">0</span>
</a> </a>
</li> </li>
@endif
@else @else
@if(!Auth::guard('admin')->check()) <li class="sidebar-item">
<li class="sidebar-item"> <a class="sidebar-link d-flex align-items-center justify-content-between"
<a class="sidebar-link d-flex align-items-center justify-content-between" href="{{ url('/pengajuan-file') }}" aria-expanded="false">
href="{{ url('/pengajuan-file') }}" aria-expanded="false">
<div class="d-flex align-items-center gap-3"> <div class="d-flex align-items-center gap-3">
<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>
</a>
@if($isRoleApprover) </li>
<span class="badge bg-danger rounded-pill d-none" id="pengajuanPendingCountBadge">0</span>
@endif
</a>
</li>
@endif
@endif @endif
<li class="sidebar-item"> <li class="sidebar-item">
<a class="sidebar-link d-flex align-items-center justify-content-between" <a class="sidebar-link d-flex align-items-center justify-content-between"
@ -128,15 +103,7 @@
</li> --}} </li> --}}
{{-- MASTER --}} {{-- MASTER --}}
<li class="nav-small-cap"><span class="hide-menu">History</span></li> @if(auth()->user()->dataUser->mappingUnitKerjaPegawai()->where('objectunitkerjapegawaifk', 43)->exists())
<li class="sidebar-item">
<a class="sidebar-link" href="{{ url('/expired-dokumen') }}" aria-expanded="false">
<i class="ti ti-clock"></i>
<span class="hide-menu">Expired Dokumen</span>
</a>
</li>
@if(!Auth::guard('admin')->check())
@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' : '' }}">
@ -176,7 +143,6 @@
</ul> </ul>
</li> </li>
@endif @endif
@endif
</ul> </ul>
</nav> </nav>
</div> </div>
@ -214,11 +180,10 @@
</style> </style>
<script> <script>
const pendingBadge = document.getElementById('pendingCountBadge'); const badge = document.getElementById('pendingCountBadge');
const pengajuanBadge = document.getElementById('pengajuanPendingCountBadge');
async function countData() { async function countData() {
if (!pendingBadge && !pengajuanBadge) return; if (!badge) return;
try { try {
const res = await fetch('/data/count-pending', { const res = await fetch('/data/count-pending', {
@ -230,24 +195,14 @@
const data = await res.json(); const data = await res.json();
const count = Number(data?.count ?? 0); const count = Number(data?.count ?? 0);
if (pendingBadge) { badge.textContent = count;
pendingBadge.textContent = count; badge.classList.toggle('d-none', count <= 0);
pendingBadge.classList.toggle('d-none', count <= 0);
}
if (pengajuanBadge) {
pengajuanBadge.textContent = count;
pengajuanBadge.classList.toggle('d-none', count <= 0);
}
} catch (e) { } catch (e) {
if (pendingBadge) pendingBadge.classList.add('d-none'); badge.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,69 +1,41 @@
{{-- @php <style>
$globalUpdateItems = [ /* ===== NOTIFIKASI STYLE FACEBOOK ===== */
[
'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>
.message-body { .message-body {
max-height: 360px; max-height: 360px;
overflow-y: auto; overflow-y: auto;
} }
/* item notif */
.message-body .dropdown-item { .message-body .dropdown-item {
display: flex; padding: 8px 12px;
align-items: flex-start; font-size: 13px;
gap: 1px; line-height: 1.35;
}
.message-body .dropdown-item div {
white-space: normal; white-space: normal;
word-break: break-word; border-radius: 6px;
} }
/* jarak antar notif */
.message-body .dropdown-item + .dropdown-item {
margin-top: 2px;
}
/* judul teks */
.message-body .notif-text {
color: #212529;
}
/* waktu */
.message-body .notif-time {
font-size: 11px;
color: #6c757d;
margin-top: 2px;
}
/* hover ala Facebook */
.message-body .dropdown-item:hover { .message-body .dropdown-item:hover {
background-color: #f0f2f5; background-color: #f0f2f5;
} }
/* DOT */
.nav-link {
position: relative;
}
.notif-dot {
position: absolute;
top: 8px;
right: 8px;
width: 10px;
height: 10px;
background-color: red;
border-radius: 50%;
z-index: 10;
animation: pulse 1.5s infinite;
}
#expiredDot {
background-color: orange;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.5); opacity: 0.6; }
100% { transform: scale(1); opacity: 1; }
}
</style> </style>
<header class="app-header"> <header class="app-header">
@ -77,33 +49,8 @@
</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="drop1" 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;">
<i class="ti ti-alert-circle text-primary"></i>
</span>
<span id="expiredDot" class="notif-dot d-none">
</span>
</a>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-animate-up shadow" aria-labelledby="dropExpired" style="min-width: 320px;">
<div class="d-flex align-items-center justify-content-between px-3 border-bottom">
<span class="fw-semibold">Notifikasi Expired</span>
<button type="button" class="btn btn-sm btn-outline-primary my-2" id="expiredOpenDetailBtn">
Detail
</button>
</div>
<div class="message-body" id="expiredNotifList">
<div class="dropdown-item text-muted small">Memuat...</div>
</div>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-link position-relative" href="javascript:void(0)" id="dropNotif" 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;">
<i class="ti ti-bell text-primary"></i> <i class="ti ti-bell text-primary"></i>
</span> </span>
@ -111,7 +58,7 @@
0 0
</span> </span>
</a> </a>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-animate-up shadow" aria-labelledby="dropNotif" style="min-width: 320px;"> <div class="dropdown-menu dropdown-menu-end dropdown-menu-animate-up shadow" aria-labelledby="drop1" style="min-width: 320px;">
<div class="d-flex align-items-center justify-content-between px-3 border-bottom"> <div class="d-flex align-items-center justify-content-between px-3 border-bottom">
<span class="fw-semibold">Notifikasi</span> <span class="fw-semibold">Notifikasi</span>
<span class="small text-muted" id="notifCountText">0 baru</span> <span class="small text-muted" id="notifCountText">0 baru</span>
@ -131,7 +78,7 @@
<div class="message-body"> <div class="message-body">
<a href="javascript:void(0)" class="d-flex align-items-center gap-2 dropdown-item"> <a href="javascript:void(0)" class="d-flex align-items-center gap-2 dropdown-item">
<i class="ti ti-user fs-6"></i> <i class="ti ti-user fs-6"></i>
<p class="mb-0 fs-3">{{ auth()->user()->namauser ?? 'admin' }}</p> <p class="mb-0 fs-3">{{ auth()->user()->namauser }}</p>
</a> </a>
<form action="/logout" method="POST"> <form action="/logout" method="POST">
@csrf @csrf
@ -144,646 +91,68 @@
</div> </div>
</nav> </nav>
</header> </header>
<!-- Modal: Detail Dokumen Akan Expired -->
<div class="modal fade" id="expiredDetailModal" tabindex="-1" aria-labelledby="expiredDetailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="expiredDetailModalLabel">Detail Notifikasi Dokumen Akan Expired</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row g-2 align-items-end mb-3">
<div class="col-12 col-md-4">
<label class="form-label mb-1">Akan expired (hari)</label>
<input type="number" class="form-control" id="expiredFilterDaysMax" min="0" step="1" value="30">
</div>
<div class="col-12 col-md-4 d-flex gap-2">
<button type="button" class="btn btn-primary flex-grow-1" id="expiredApplyFilterBtn">Terapkan</button>
<button type="button" class="btn btn-outline-secondary" id="expiredResetFilterBtn">Reset</button>
</div>
</div>
<div class="d-flex align-items-center justify-content-between mb-2">
<div class="small text-muted" id="expiredDetailInfo">-</div>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary" id="expiredDetailPrevBtn">Prev</button>
<span class="small" id="expiredDetailPageText">1</span>
<button type="button" class="btn btn-sm btn-outline-secondary" id="expiredDetailNextBtn">Next</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-sm table-striped align-middle mb-0">
<thead>
<tr>
<th style="width: 56px;">#</th>
<th>Unit</th>
<th>No Dokumen</th>
<th>Nama Dokumen</th>
<th style="width: 140px;">Tgl Expired</th>
<th style="width: 140px;">Sisa (hari)</th>
<th style="width: 140px;">Aksi</th>
</tr>
</thead>
<tbody id="expiredDetailTbody">
<tr>
<td colspan="7" class="text-muted small">Memuat...</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Tutup</button>
</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 listEl = document.getElementById('notifList');
const countTextEl = document.getElementById('notifCountText');
const badgeEl = document.getElementById('notifCountBadge');
/* function setList(items) {
|-------------------------------------------------------------------------- if (!listEl) return;
| HELPER if (!items.length) {
|-------------------------------------------------------------------------- listEl.innerHTML = '<div class="dropdown-item text-muted small">Tidak ada notifikasi</div>';
*/ return;
function escapeHtml(value) {
return String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function ajaxGet(url) {
return fetch(url, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
} }
}); listEl.innerHTML = items.map(item => `
} <a href="${item.url || '#'}" class="dropdown-item d-flex align-items-start gap-2">
<span class="badge ${item.is_read ? 'bg-secondary' : 'bg-primary'} rounded-circle" style="width:10px;height:10px;margin-top:6px;"></span>
function ajaxPost(url) { <div>
return fetch(url, { <div class="fw-semibold">${item.text_notifikasi || '-'}</div>
method: 'POST', <div class="small text-muted">${item.created_at || ''}</div>
headers: {
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
});
}
/*
|--------------------------------------------------------------------------
| NOTIFIKASI BIASA
|--------------------------------------------------------------------------
*/
const notifListEl = document.getElementById('notifList');
const notifCountTextEl = document.getElementById('notifCountText');
const notifBadgeEl = document.getElementById('notifCountBadge');
const notifToggle = document.getElementById('dropNotif') || document.getElementById('drop1');
function setNotifList(items) {
if (!notifListEl) return;
if (!items || !items.length) {
notifListEl.innerHTML = `
<div class="dropdown-item text-muted small">
Tidak ada notifikasi
</div> </div>
`; </a>
return; `).join('');
} }
notifListEl.innerHTML = items.map(item => { fetch('/data/notifications')
const url = item.url || '#';
const text = escapeHtml(item.text_notifikasi || '-');
const createdAt = escapeHtml(item.created_at || '');
const isRead = item.is_read ? true : false;
return `
<a href="${url}" class="dropdown-item d-flex align-items-start gap-2">
<span class="badge ${isRead ? 'bg-secondary' : 'bg-primary'} rounded-circle"
style="width:10px;height:10px;margin-top:6px;"></span>
<div>
<div class="fw-semibold">${text}</div>
<div class="small text-muted">${createdAt}</div>
</div>
</a>
`;
}).join('');
}
function setNotifBadge(unread) {
unread = Number(unread || 0);
if (notifCountTextEl) {
notifCountTextEl.textContent = `${unread} baru`;
}
if (!notifBadgeEl) return;
if (unread > 0) {
notifBadgeEl.classList.remove('d-none');
notifBadgeEl.textContent = unread > 99 ? '99+' : unread;
} else {
notifBadgeEl.classList.add('d-none');
notifBadgeEl.textContent = '0';
}
}
function loadNotifications() {
ajaxGet('/data/notifications')
.then(r => r.json()) .then(r => r.json())
.then(res => { .then(res => {
const unread = res?.status ? Number(res.unread || 0) : 0; const unread = res?.status ? Number(res.unread || 0) : 0;
const items = res?.status ? (res.data || []) : []; const items = res?.status ? (res.data || []) : [];
setNotifBadge(unread); if (countTextEl) countTextEl.textContent = `${unread} baru`;
setNotifList(items); if (badgeEl) {
if (unread > 0) {
badgeEl.classList.remove('d-none');
badgeEl.textContent = unread;
} else {
badgeEl.classList.add('d-none');
}
}
setList(items);
}) })
.catch(() => { .catch(() => {
if (notifListEl) { if (listEl) listEl.innerHTML = '<div class="dropdown-item text-muted small">Gagal memuat notifikasi</div>';
notifListEl.innerHTML = `
<div class="dropdown-item text-muted small">
Gagal memuat notifikasi
</div>
`;
}
setNotifBadge(0);
}); });
}
function markNotificationsAsRead() { const notifToggle = document.getElementById('drop1');
ajaxPost('/data/notifications/read') if (notifToggle) {
.then(() => { notifToggle.addEventListener('show.bs.dropdown', () => {
setNotifBadge(0); const unread = Number(badgeEl?.textContent || 0);
if (unread > 0) {
if (notifListEl) { fetch('/data/notifications/read', {
notifListEl.querySelectorAll('.badge').forEach(badge => { method: 'POST',
badge.classList.remove('bg-primary'); headers: {
badge.classList.add('bg-secondary'); 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
}); 'X-Requested-With': 'XMLHttpRequest'
} }
}) }).then(() => {
.catch(() => {}); if (countTextEl) countTextEl.textContent = '0 baru';
} if (badgeEl) badgeEl.classList.add('d-none');
}).catch(() => {});
if (notifToggle) { }
notifToggle.addEventListener('show.bs.dropdown', function() {
markNotificationsAsRead();
});
}
/*
|--------------------------------------------------------------------------
| NOTIFIKASI EXPIRED
|--------------------------------------------------------------------------
*/
const expiredListEl = document.getElementById('expiredNotifList');
const expiredDotEl = document.getElementById('expiredDot');
const expiredToggle = document.getElementById('dropExpired');
function setExpiredList(items) {
if (!expiredListEl) return;
if (!items || !items.length) {
expiredListEl.innerHTML = `
<div class="dropdown-item text-muted small">
Tidak ada notifikasi expired
</div>
`;
return;
}
expiredListEl.innerHTML = items.map(item => {
const url = item.url || '#';
const text = escapeHtml(item.text_notifikasi || '-');
const createdAt = escapeHtml(item.created_at || '');
const isRead = item.is_read ? true : false;
const docId = item.doc_id || item.document_id || item.dokumen_id || item.id || '';
return `
<a href="${url}"
class="dropdown-item d-flex align-items-start gap-2 js-expired-notif-item"
data-doc-id="${docId}">
<span class="badge ${isRead ? 'bg-secondary' : 'bg-warning'} rounded-circle"
style="width:10px;height:10px;margin-top:6px;"></span>
<div>
<div class="fw-semibold">${text}</div>
<div class="small text-muted">${createdAt}</div>
</div>
</a>
`;
}).join('');
}
function setExpiredDot(unread) {
unread = Number(unread || 0);
if (!expiredDotEl) return;
if (unread > 0) {
expiredDotEl.classList.remove('d-none');
} else {
expiredDotEl.classList.add('d-none');
}
}
function loadExpiredNotifications() {
ajaxGet('/data/expired-notifications')
.then(r => r.json())
.then(res => {
const unread = res?.status ? Number(res.unread || 0) : 0;
const items = res?.status ? (res.data || []) : [];
setExpiredDot(unread);
setExpiredList(items);
})
.catch(() => {
if (expiredListEl) {
expiredListEl.innerHTML = `
<div class="dropdown-item text-muted small">
Gagal memuat notifikasi expired
</div>
`;
}
setExpiredDot(0);
}); });
} }
function markExpiredNotificationsAsRead() {
ajaxPost('/data/expired-notifications/read')
.then(() => {
setExpiredDot(0);
if (expiredListEl) {
expiredListEl.querySelectorAll('.badge').forEach(badge => {
badge.classList.remove('bg-warning');
badge.classList.add('bg-secondary');
});
}
})
.catch(() => {});
}
if (expiredToggle) {
expiredToggle.addEventListener('show.bs.dropdown', function() {
markExpiredNotificationsAsRead();
}); });
} </script>
/*
|--------------------------------------------------------------------------
| MODAL DETAIL EXPIRED
|--------------------------------------------------------------------------
*/
const expiredDetailModalEl = document.getElementById('expiredDetailModal');
let expiredDetailModal = null;
if (expiredDetailModalEl && window.bootstrap && window.bootstrap.Modal) {
expiredDetailModal = new bootstrap.Modal(expiredDetailModalEl);
}
const expiredState = {
docId: null,
page: 1,
perPage: 10
};
const expiredDetailTbody = document.getElementById('expiredDetailTbody');
const expiredDetailInfo = document.getElementById('expiredDetailInfo');
const expiredDetailPageText = document.getElementById('expiredDetailPageText');
const expiredOpenDetailBtn = document.getElementById('expiredOpenDetailBtn');
const expiredFilterDaysMax = document.getElementById('expiredFilterDaysMax');
const expiredApplyFilterBtn = document.getElementById('expiredApplyFilterBtn');
const expiredResetFilterBtn = document.getElementById('expiredResetFilterBtn');
const expiredDetailPrevBtn = document.getElementById('expiredDetailPrevBtn');
const expiredDetailNextBtn = document.getElementById('expiredDetailNextBtn');
function showExpiredModal() {
if (expiredDetailModal) {
expiredDetailModal.show();
return;
}
/**
* Fallback kalau bootstrap.Modal tidak terbaca.
* Ini berguna kalau Bootstrap JS belum include.
*/
if (expiredDetailModalEl) {
expiredDetailModalEl.classList.add('show');
expiredDetailModalEl.style.display = 'block';
expiredDetailModalEl.removeAttribute('aria-hidden');
expiredDetailModalEl.setAttribute('aria-modal', 'true');
document.body.classList.add('modal-open');
let backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop fade show';
backdrop.id = 'manualExpiredBackdrop';
document.body.appendChild(backdrop);
} else {
alert('Modal expiredDetailModal tidak ditemukan di HTML.');
}
}
function hideExpiredModalFallback() {
if (!expiredDetailModalEl) return;
expiredDetailModalEl.classList.remove('show');
expiredDetailModalEl.style.display = 'none';
expiredDetailModalEl.setAttribute('aria-hidden', 'true');
expiredDetailModalEl.removeAttribute('aria-modal');
document.body.classList.remove('modal-open');
const backdrop = document.getElementById('manualExpiredBackdrop');
if (backdrop) {
backdrop.remove();
}
}
expiredDetailModalEl?.querySelectorAll('[data-bs-dismiss="modal"]').forEach(btn => {
btn.addEventListener('click', function() {
if (!expiredDetailModal) {
hideExpiredModalFallback();
}
});
});
function setExpiredDetailLoading(text = 'Memuat...') {
if (!expiredDetailTbody) return;
expiredDetailTbody.innerHTML = `
<tr>
<td colspan="7" class="text-muted small">${escapeHtml(text)}</td>
</tr>
`;
}
function buildExpiredDetailParams() {
const params = new URLSearchParams();
params.set('page', String(expiredState.page));
params.set('per_page', String(expiredState.perPage));
if (expiredState.docId) {
params.set('doc_id', String(expiredState.docId));
}
const daysMax = expiredFilterDaysMax?.value ?? '';
if (daysMax !== '' && daysMax !== null) {
params.set('days_left_max', String(daysMax));
}
return params;
}
function renderExpiredDetail(rows, meta) {
const total = Number(meta?.total || 0);
const perPage = Number(meta?.per_page || expiredState.perPage);
const page = Number(meta?.page || expiredState.page);
const maxPage = total ? Math.ceil(total / perPage) : 1;
if (expiredDetailPageText) {
expiredDetailPageText.textContent = String(page);
}
if (expiredDetailInfo) {
const start = total ? ((page - 1) * perPage + 1) : 0;
const end = Math.min(page * perPage, total);
expiredDetailInfo.textContent = total
? `Menampilkan ${start}-${end} dari ${total} data`
: 'Tidak ada data';
}
if (expiredDetailPrevBtn) {
expiredDetailPrevBtn.disabled = page <= 1;
}
if (expiredDetailNextBtn) {
expiredDetailNextBtn.disabled = page >= maxPage;
}
if (!expiredDetailTbody) return;
if (!rows || !rows.length) {
expiredDetailTbody.innerHTML = `
<tr>
<td colspan="7" class="text-muted small">Tidak ada data</td>
</tr>
`;
return;
}
expiredDetailTbody.innerHTML = rows.map((row, idx) => {
const no = (page - 1) * perPage + (idx + 1);
const unit = escapeHtml(row.unit_name || row.nama_unit || '-');
const noDok = escapeHtml(row.no_dokumen || row.nomor_dokumen || '-');
const nama = escapeHtml(row.nama_dokumen || row.nama_document || row.nama || '-');
const tgl = escapeHtml(row.tgl_expired_label || row.tgl_expired || row.expired_date || '-');
const daysLeft = row.days_left === null || row.days_left === undefined
? '-'
: escapeHtml(row.days_left);
const previewUrl = row.preview_url || row.url || '#';
return `
<tr>
<td>${no}</td>
<td>${unit}</td>
<td>${noDok}</td>
<td>${nama}</td>
<td>${tgl}</td>
<td>${daysLeft}</td>
<td>
<a class="btn btn-sm btn-outline-primary"
href="${previewUrl}"
target="_blank"
rel="noopener">
Preview
</a>
</td>
</tr>
`;
}).join('');
}
function loadExpiredDetail() {
setExpiredDetailLoading();
const params = buildExpiredDetailParams();
ajaxGet(`/data/expired-notifications/detail?${params.toString()}`)
.then(r => r.json())
.then(res => {
if (!res?.status) {
setExpiredDetailLoading(res?.message || 'Gagal memuat data');
return;
}
renderExpiredDetail(res.data || [], res.meta || {});
})
.catch(() => {
setExpiredDetailLoading('Gagal memuat data detail expired');
});
}
function openExpiredDetail(docId = null) {
expiredState.docId = docId ? Number(docId) : null;
expiredState.page = 1;
/**
* Kalau klik dari item expired tertentu,
* filter hari dikosongkan supaya data berdasarkan doc_id tetap muncul.
*/
if (expiredState.docId && expiredFilterDaysMax) {
expiredFilterDaysMax.value = '';
}
showExpiredModal();
loadExpiredDetail();
}
/**
* Tombol "Detail" di header expired.
*/
if (expiredOpenDetailBtn) {
expiredOpenDetailBtn.addEventListener('click', function(e) {
e.preventDefault();
expiredState.docId = null;
expiredState.page = 1;
if (expiredFilterDaysMax) {
expiredFilterDaysMax.value = '30';
}
openExpiredDetail(null);
});
}
/**
* Klik salah satu item expired dari dropdown.
* Nanti langsung buka modal detail sesuai dokumen.
*/
if (expiredListEl) {
expiredListEl.addEventListener('click', function(e) {
const item = e.target.closest('a.js-expired-notif-item');
if (!item) return;
e.preventDefault();
const docId = item.dataset.docId || null;
openExpiredDetail(docId);
});
}
if (expiredApplyFilterBtn) {
expiredApplyFilterBtn.addEventListener('click', function() {
expiredState.docId = null;
expiredState.page = 1;
loadExpiredDetail();
});
}
if (expiredResetFilterBtn) {
expiredResetFilterBtn.addEventListener('click', function() {
expiredState.docId = null;
expiredState.page = 1;
if (expiredFilterDaysMax) {
expiredFilterDaysMax.value = '30';
}
loadExpiredDetail();
});
}
if (expiredDetailPrevBtn) {
expiredDetailPrevBtn.addEventListener('click', function() {
if (expiredState.page <= 1) return;
expiredState.page -= 1;
loadExpiredDetail();
});
}
if (expiredDetailNextBtn) {
expiredDetailNextBtn.addEventListener('click', function() {
expiredState.page += 1;
loadExpiredDetail();
});
}
/*
|--------------------------------------------------------------------------
| LOAD PERTAMA
|--------------------------------------------------------------------------
*/
loadNotifications();
loadExpiredNotifications();
/*
|--------------------------------------------------------------------------
| REFRESH OTOMATIS
|--------------------------------------------------------------------------
*/
setInterval(function() {
loadNotifications();
loadExpiredNotifications();
}, 60000);
});
</script>

View File

@ -139,7 +139,7 @@ document.addEventListener('DOMContentLoaded', () => {
const paginationBtns = document.getElementById('logPaginationBtns'); const paginationBtns = document.getElementById('logPaginationBtns');
const summaryText = document.getElementById('logSummaryText'); const summaryText = document.getElementById('logSummaryText');
if(tbody) tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted py-3">Memuat...</td></tr>'; if(tbody) tbody.innerHTML = '<tr><td colspan="4" class="text-center text-muted py-3">Memuat...</td></tr>';
if(summaryText) summaryText.textContent = 'Memuat data...'; if(summaryText) summaryText.textContent = 'Memuat data...';
if(searchInput) searchInput.value = keyword || ''; if(searchInput) searchInput.value = keyword || '';
@ -160,12 +160,11 @@ document.addEventListener('DOMContentLoaded', () => {
<td>${((currentPage - 1) * (pagination.per_page || 10)) + idx + 1}</td> <td>${((currentPage - 1) * (pagination.per_page || 10)) + idx + 1}</td>
<td>${row.pegawai_nama_entry || '-'}</td> <td>${row.pegawai_nama_entry || '-'}</td>
<td>${row.total_open || 0}</td> <td>${row.total_open || 0}</td>
<td>${row.total_download || 0}</td>
<td>${formatTanggal(row.last_open)}</td> <td>${formatTanggal(row.last_open)}</td>
</tr> </tr>
`).join(''); `).join('');
const emptyState = logs.length === 0 ? '<tr><td colspan="6" class="text-center text-muted py-3">Belum ada aktivitas</td></tr>' : ''; const emptyState = logs.length === 0 ? '<tr><td colspan="4" class="text-center text-muted py-3">Belum ada aktivitas</td></tr>' : '';
if(tbody) tbody.innerHTML = logs.length ? rows : emptyState; if(tbody) tbody.innerHTML = logs.length ? rows : emptyState;
if(summaryText){ if(summaryText){
@ -256,7 +255,7 @@ document.addEventListener('DOMContentLoaded', () => {
if(pageData.length === 0){ if(pageData.length === 0){
tbody.innerHTML = ` tbody.innerHTML = `
<tr> <tr>
<td colspan="7" class="text-center text-muted py-4"> <td colspan="6" class="text-center text-muted py-4">
Tidak ada data Tidak ada data
</td> </td>
</tr> </tr>
@ -342,7 +341,6 @@ document.addEventListener('DOMContentLoaded', () => {
<th>#</th> <th>#</th>
<th>Nama</th> <th>Nama</th>
<th>Jumlah Membuka</th> <th>Jumlah Membuka</th>
<th>Jumlah Mengunduh</th>
<th>Terakhir Dilihat</th> <th>Terakhir Dilihat</th>
</tr> </tr>
</thead> </thead>

View File

@ -1,5 +1,6 @@
@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">
@ -57,7 +58,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: 75vh; overflow-y:auto; overflow-x:auto;"> <div class="table-responsive" style="max-height: 55vh; 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>
@ -95,7 +96,9 @@
<th>Kategori</th> <th>Kategori</th>
<th>Unit</th> <th>Unit</th>
<th>Aksi</th> <th>Aksi</th>
<th>Tanggal Aksi</th> <th>Tanggal Terbit</th>
<th>Tanggal Kedaluwarsa Dokumen</th>
<th>Tanggal Upload</th>
</tr> </tr>
</thead> </thead>
<tbody id="tableHistoryFile"> <tbody id="tableHistoryFile">
@ -111,9 +114,5 @@
</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,9 +13,6 @@
<!-- 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,26 +46,10 @@
<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">
@ -94,14 +78,6 @@
<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>
@ -112,9 +88,6 @@
<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>
@ -122,6 +95,8 @@
<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>
@ -133,7 +108,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: 75vh; overflow-y:auto;"> <div class="table-responsive" style="max-height: 55vh; 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>
@ -143,7 +118,9 @@
<th>Kategori</th> <th>Kategori</th>
<th>Unit</th> <th>Unit</th>
<th>Aksi</th> <th>Aksi</th>
<th>Tanggal Aksi</th> <th>Tanggal Terbit</th>
<th>Tanggal Kedaluwarsa Dokumen</th>
<th>Tanggal Upload</th>
</tr> </tr>
</thead> </thead>
<tbody id="tableHistoryFile"> <tbody id="tableHistoryFile">
@ -163,9 +140,6 @@
@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

@ -18,18 +18,27 @@
<div class="small text-muted">Perbarui detail dokumen sebelum mengirim ulang.</div> <div class="small text-muted">Perbarui detail dokumen sebelum mengirim ulang.</div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-4">
<label class="form-label fw-semibold">Unit <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Unit <span class="text-danger">*</span></label>
<select class="form-control unit_kerja" name="id_unit_kerja" id="edit_id_unit_kerja" required> <select class="form-control unit_kerja" name="id_unit_kerja" id="edit_id_unit_kerja" required>
<option value="" disabled selected>Pilih Unit</option> <option value="" disabled selected>Pilih Unit</option>
</select> </select>
</div> </div>
<div class="col-md-6"> <div class="col-md-4">
<label class="form-label fw-semibold">Sub Unit <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Sub Unit <span class="text-danger">*</span></label>
<select class="form-control sub_unit_kerja" name="id_sub_unit_kerja" id="edit_id_sub_unit_kerja" required> <select class="form-control sub_unit_kerja" name="id_sub_unit_kerja" id="edit_id_sub_unit_kerja" required>
<option value="" disabled selected>Pilih Sub Unit</option> <option value="" disabled selected>Pilih Sub Unit</option>
</select> </select>
</div> </div>
<div class="col-md-4">
<label class="form-label fw-semibold">Kategori Dokumen</label>
<select class="form-control" name="master_kategori_directory_id" id="edit_kategori">
<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-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Nomor Dokumen</label> <label class="form-label fw-semibold">Nomor Dokumen</label>
@ -46,35 +55,22 @@
<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="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-2 d-flex align-items-end">
<label class="form-label fw-semibold">Masa Berlaku Dokumen</label> <div class="form-check">
<select class="form-select masa-berlaku-select" <input class="form-check-input" type="checkbox" id="edit_has_expired" data-target="edit_expired_field">
name="masa_berlaku_option" <label class="form-check-label" for="edit_has_expired">Ada Expired?</label>
id="edit_masa_berlaku_option" </div>
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 d-none" id="edit_expired_field"> <div class="col-md-5" 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>
<div class="col-md-4"> <div class="col-md-5">
<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>
<div class="border rounded-3 p-2 bg-light"> <div class="border rounded-3 p-2 bg-light">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="permission_file" id="edit_perm_yes" value="1" required> <input class="form-check-input" type="radio" name="permission_file" id="edit_perm_yes" value="1" required>
<label class="form-check-label" for="edit_perm_yes">Ya</label> <label class="form-check-label" for="edit_perm_yes">Iya</label>
</div> </div>
<div class="form-check mt-1"> <div class="form-check mt-1">
<input class="form-check-input" type="radio" name="permission_file" id="edit_perm_no" value="0" required> <input class="form-check-input" type="radio" name="permission_file" id="edit_perm_no" value="0" required>
@ -83,42 +79,6 @@
</div> </div>
</div> </div>
<div class="col-md-4">
<label class="form-label fw-semibold">Instrumen Akreditasi</label>
<select class="form-select akre-select" id="edit_akre_select" name="akre" style="width: 350px;">
<option value="">Pilih Instrumen</option>
</select>
<div class="form-text text-muted">Isi form ini bila dokumen yang diunggah merupakan dokumen <strong>akreditasi</strong>.</div>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Kategori Hukum</label>
<select class="form-select select-kat-hukum" name="kategori_hukum" id="edit_kategori_hukum" style="width: 350px;">
<option value="">Pilih Kategori Hukum</option>
<option value="Kebijakan - Peraturan Direktur">Kebijakan - Peraturan Direktur</option>
<option value="Kebijakan - Keputusan Direktur Utama">Kebijakan - Keputusan Direktur Utama</option>
<option value="Kebijakan - Surat Edaran">Kebijakan - Surat Edaran</option>
<option value="Kebijakan - Pengumuman">Kebijakan - Pengumuman</option>
<option value="Kerjasama - Pelayanan Kesehatan">Kerjasama - Pelayanan Kesehatan</option>
<option value="Kerjasama - Management">Kerjasama - Management</option>
<option value="Kerjasama - Pemeliharan">Kerjasama - Pemeliharan</option>
<option value="Kerjasama - Diklat">Kerjasama - Diklat</option>
<option value="Kerjasama - Luar Negeri">Kerjasama - Luar Negeri</option>
<option value="Kerjasama - Area Bisnis">Kerjasama - Area Bisnis</option>
<option value="Kerjasama - Pendidikan">Kerjasama - Pendidikan</option>
<option value="Kerjasama - Pengampuan KIA">Kerjasama- Pengampuan KIA</option>
<option value="Kerjasama - SDM">Kerjasama- SDM</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Kategori lainnya</label>
<select class="form-select" name="master_kategori_directory_id" id="edit_kategori" style="width: 350px;">
<option value="">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-12 mb-2"> <div class="col-md-12 mb-2">
<label for="edit_file_upload" class="form-label fw-semibold">📂 Upload Dokumen (PDF)</label> <label for="edit_file_upload" class="form-label fw-semibold">📂 Upload Dokumen (PDF)</label>
<div class="border rounded-3 p-3 bg-white shadow-sm"> <div class="border rounded-3 p-3 bg-white shadow-sm">

View File

@ -1,7 +1,6 @@
<?php <?php
use App\Http\Controllers\AksesFileController; use App\Http\Controllers\AksesFileController;
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;
@ -10,7 +9,7 @@ use App\Http\Controllers\LogActivityController;
use App\Http\Controllers\masterPersetujuanController; use App\Http\Controllers\masterPersetujuanController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::middleware(['auth:admin,web'])->group(function(){ Route::middleware(['auth'])->group(function(){
Route::get('/', [DashboardController::class, 'index']); Route::get('/', [DashboardController::class, 'index']);
Route::get('/data-internal', [DashboardController::class, 'dataUnitInternal']); Route::get('/data-internal', [DashboardController::class, 'dataUnitInternal']);
@ -19,10 +18,6 @@ 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']);
// Kelola "folder/instrumen" akreditasi via file: public/json/akreditasi.jff (tanpa database)
Route::get('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'index']);
Route::post('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'store']);
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']);
@ -51,9 +46,7 @@ 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']);
@ -62,7 +55,7 @@ Route::middleware(['auth:admin,web'])->group(function(){
Route::get('/log-activity', [LogActivityController::class, 'index']); Route::get('/log-activity', [LogActivityController::class, 'index']);
Route::get('/datatable/log-activity', [LogActivityController::class, 'datatable']); Route::get('/datatable/log-activity', [LogActivityController::class, 'datatable']);
Route::get('/datatable/log-activity/{fileDirectoryId}', [LogActivityController::class, 'detailByFile']); Route::get('/datatable/log-activity/{fileDirectoryId}', [LogActivityController::class, 'detailByFile']);
Route::get('/datatable/log-activity-pengajuapn', [LogActivityController::class, 'datatableHistoryPengajuan']); Route::get('/datatable/log-activity-pengajuan', [LogActivityController::class, 'datatableHistoryPengajuan']);
Route::get('/recap', [DashboardController::class, 'recapView']); Route::get('/recap', [DashboardController::class, 'recapView']);
Route::get('/data/recap', [DashboardController::class, 'recapData']); Route::get('/data/recap', [DashboardController::class, 'recapData']);
@ -71,38 +64,22 @@ 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']);
Route::get('/datatable/pending-file', [DashboardController::class, 'dataPendingFile']); Route::get('/datatable/pending-file', [DashboardController::class, 'dataPendingFile']);
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::get('/data/count-pending', [DashboardController::class, 'countDataPending']);
Route::post('/pending-file/{id}/approve-turt', [DashboardController::class, 'approvePendingFileTurt']); Route::get('/data/count-rejected', [DashboardController::class, 'countRejectedPengajuan']);
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-rejected', [DashboardController::class, 'countRejectedPengajuan']);
// }); // });
Route::get('/data/notifications', [DashboardController::class, 'notifkasiList']); Route::get('/data/notifications', [DashboardController::class, 'notifkasiList']);
Route::post('/data/notifications/read', [DashboardController::class, 'notifkasiMarkRead']); Route::post('/data/notifications/read', [DashboardController::class, 'notifkasiMarkRead']);
Route::get('/data/expired-notifications', [DashboardController::class, 'expiredNotifkasiList']);
Route::post('/data/expired-notifications/read', [DashboardController::class, 'expiredNotifkasiMarkRead']);
Route::get('/data/expired-notifications/detail', [DashboardController::class, 'expiredNotifkasiDetail']);
Route::get('/data/log-dokumen', [DashboardController::class, 'logDokumen']); Route::get('/data/log-dokumen', [DashboardController::class, 'logDokumen']);
Route::get('/expired-dokumen', [DashboardController::class, 'expDokumen']);
Route::get('/data/expired-dokumen', [DashboardController::class, 'dataUnitExp']);
Route::get('/data/recapExp', [DashboardController::class, 'recapDataExp']);
}); });
Route::get('/login', [AuthController::class, 'index'])->name('login'); Route::get('/login', [AuthController::class, 'index'])->name('login');
Route::get('/captcha/login', [AuthController::class, 'captcha'])->name('captcha.login');
Route::post('/login', [AuthController::class, 'login']); Route::post('/login', [AuthController::class, 'login']);
Route::post('/logout', [AuthController::class, 'logout']); Route::post('/logout', [AuthController::class, 'logout']);