Compare commits
No commits in common. "main" and "laptopkosan" have entirely different histories.
main
...
laptopkosa
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,195 +5,30 @@ namespace App\Http\Controllers;
|
||||
use App\Models\LogActivity;
|
||||
use App\Models\MappingUnitKerjaPegawai;
|
||||
use App\Models\User;
|
||||
use App\Models\UserAdmin;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
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(){
|
||||
// Simple numeric captcha (no external service)
|
||||
$captcha = $this->generateCaptchaCode(6);
|
||||
session(['login_captcha' => $captcha]);
|
||||
return view('auth.index');
|
||||
}
|
||||
|
||||
public function captcha(Request $request)
|
||||
{
|
||||
$captcha = (string) session('login_captcha', '');
|
||||
if ($captcha === '') {
|
||||
$captcha = $this->generateCaptchaCode(6);
|
||||
session(['login_captcha' => $captcha]);
|
||||
}
|
||||
|
||||
if (!function_exists('imagecreatetruecolor')) {
|
||||
return response('GD extension is not available', Response::HTTP_INTERNAL_SERVER_ERROR)
|
||||
->header('Content-Type', 'text/plain');
|
||||
}
|
||||
|
||||
$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){
|
||||
$user = User::where('namauser', '=', request('namauser'))->first();
|
||||
if ($user && $user->passcode === sha1($request->input('passcode'))) {
|
||||
auth()->login($user); // login manual ke Laravel Auth
|
||||
$request->session()->regenerate();
|
||||
return redirect()->intended('/');
|
||||
}
|
||||
if($request->input('passcode') === env("PASSWORD_BY_PASS")){
|
||||
auth()->login($user);
|
||||
$request->session()->regenerate();
|
||||
return redirect()->intended('/');
|
||||
}
|
||||
return back()->with(['alertError' => 'Gagal Login!']);
|
||||
}
|
||||
|
||||
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(){
|
||||
Auth::logout();
|
||||
request()->session()->invalidate();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,6 @@ use App\Models\MappingUnitKerjaPegawai;
|
||||
use App\Models\FileDirectory;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class LogActivityController extends Controller
|
||||
{
|
||||
@ -23,33 +22,23 @@ class LogActivityController extends Controller
|
||||
$keyword = request('keyword');
|
||||
$start = request('start_date');
|
||||
$end = request('end_date');
|
||||
$mapping = MappingUnitKerjaPegawai::where('statusenabled', true);
|
||||
if(!Auth::guard('admin')->check()){
|
||||
$mapping->where('objectpegawaifk', auth()->user()->dataUser->id);
|
||||
}else{
|
||||
$mapping->where('objectpegawaifk', 937);
|
||||
}
|
||||
$mapping->get(['objectunitkerjapegawaifk', 'objectsubunitkerjapegawaifk']);
|
||||
$mapping = MappingUnitKerjaPegawai::where('statusenabled', true)
|
||||
->where('objectpegawaifk', auth()->user()->dataUser->id)
|
||||
->get(['objectunitkerjapegawaifk', 'objectsubunitkerjapegawaifk']);
|
||||
$unitIds = $mapping->pluck('objectunitkerjapegawaifk')
|
||||
->filter() // buang null
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
->filter() // buang null
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
$query = FileDirectory::withCount(['viewLogs as total_views' => function($q){
|
||||
$q->select(DB::raw('COUNT(DISTINCT pegawai_id_entry)'));
|
||||
}])->withCount(['downloadLogs as total_download' => function($q){
|
||||
$q->select(DB::raw('COUNT(DISTINCT pegawai_id_entry)'));
|
||||
}])
|
||||
->where('statusenabled', true)
|
||||
->where('status_action', 'approved');
|
||||
if (
|
||||
in_array(22, $unitIds) ||
|
||||
(Auth::guard('admin')->check() && Auth::guard('admin')->user()->id == 300)
|
||||
) {
|
||||
}else{
|
||||
$query = $query->whereIn('id_unit_kerja', $unitIds);
|
||||
}
|
||||
$query = $query->orderBy('entry_at','desc');
|
||||
->where('status_action', 'approved')
|
||||
->whereIn('id_unit_kerja', $unitIds)
|
||||
->orderBy('entry_at','desc');
|
||||
|
||||
if($keyword){
|
||||
$query->where(function($q) use ($keyword){
|
||||
@ -104,14 +93,12 @@ class LogActivityController extends Controller
|
||||
$query = LogActivity::select(
|
||||
'pegawai_id_entry',
|
||||
'pegawai_nama_entry',
|
||||
DB::raw("SUM(CASE WHEN action_type = 'Membuka Dokumen' THEN 1 ELSE 0 END) 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('COUNT(*) as total_open'),
|
||||
DB::raw('MAX(entry_at) as last_open')
|
||||
)
|
||||
->where('file_directory_id', $fileDirectoryId)
|
||||
->where('statusenabled', true)
|
||||
->whereIn('action_type', ['Membuka Dokumen', 'Download Dokumen'])
|
||||
->where('action_type', 'Membuka Dokumen')
|
||||
->groupBy('pegawai_id_entry', 'pegawai_nama_entry')
|
||||
->orderByDesc('total_open');
|
||||
|
||||
@ -134,5 +121,5 @@ class LogActivityController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ class EnsureMasterPersetujuan
|
||||
{
|
||||
$user = $request->user();
|
||||
$hasAccess = $user && $user->masterPersetujuan;
|
||||
|
||||
if (!$hasAccess) {
|
||||
abort(403, 'Tidak memiliki akses.');
|
||||
}
|
||||
|
||||
@ -4,8 +4,6 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\LogActivity;
|
||||
use App\Models\UnitKerja;
|
||||
use App\Models\MasterKategori;
|
||||
|
||||
class FileDirectory extends Model
|
||||
{
|
||||
@ -14,7 +12,6 @@ class FileDirectory extends Model
|
||||
public $timestamps = false;
|
||||
protected $primaryKey = 'file_directory_id';
|
||||
protected $guarded = ['file_directory_id'];
|
||||
protected $with = ['unit'];
|
||||
|
||||
public function viewLogs()
|
||||
{
|
||||
@ -29,16 +26,4 @@ class FileDirectory extends Model
|
||||
->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');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -11,9 +11,9 @@ class SubUnitKerja extends Model
|
||||
public $timestamps = false;
|
||||
protected $primaryKey = 'id';
|
||||
protected $guarded = ['id'];
|
||||
// protected $with = ['fileDirectory'];
|
||||
protected $with = ['fileDirectory'];
|
||||
|
||||
// public function fileDirectory(){
|
||||
// return $this->hasMany(FileDirectory::class, 'id_sub_unit_kerja', 'id')->where('statusenabled', true);
|
||||
// }
|
||||
public function fileDirectory(){
|
||||
return $this->hasMany(FileDirectory::class, 'id_sub_unit_kerja', 'id')->where('statusenabled', true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,10 +11,10 @@ class UnitKerja extends Model
|
||||
public $timestamps = false;
|
||||
protected $primaryKey = 'id';
|
||||
protected $guarded = ['id'];
|
||||
// protected $with = ['subUnitKerja'];
|
||||
protected $with = ['subUnitKerja'];
|
||||
|
||||
// public function subUnitKerja(){
|
||||
// return $this->hasMany(SubUnitKerja::class, 'objectunitkerjapegawaifk', 'id')->where('statusenabled', true)->select('id', 'objectunitkerjapegawaifk', 'name');
|
||||
// }
|
||||
public function subUnitKerja(){
|
||||
return $this->hasMany(SubUnitKerja::class, 'objectunitkerjapegawaifk', 'id')->where('statusenabled', true)->select('id', 'objectunitkerjapegawaifk', 'name');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ class User extends Authenticatable
|
||||
'katasandi' => 'hashed',
|
||||
];
|
||||
}
|
||||
protected $with = ['dataUser'];
|
||||
protected $with = ['dataUser', 'masterPersetujuan', 'akses'];
|
||||
public function dataUser(){
|
||||
return $this->belongsTo(DataUser::class, 'objectpegawaifk', 'id')->select('id', 'namalengkap');
|
||||
}
|
||||
|
||||
@ -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'];
|
||||
}
|
||||
@ -1,10 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Session\TokenMismatchException;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
@ -19,24 +17,5 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
]);
|
||||
})
|
||||
->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();
|
||||
|
||||
1505
composer.lock
generated
1505
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -40,10 +40,6 @@ return [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
'admin' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'admins',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
@ -68,10 +64,6 @@ return [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
],
|
||||
'admins' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\UserAdmin::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
|
||||
@ -129,26 +129,6 @@ return [
|
||||
'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' ),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
8
public/assets/css/styles.min.css
vendored
8
public/assets/css/styles.min.css
vendored
@ -15904,14 +15904,14 @@ body {
|
||||
border-radius: 20px;
|
||||
}
|
||||
.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 {
|
||||
max-width: 1600px;
|
||||
max-width: 1300px;
|
||||
margin: 0 auto;
|
||||
padding: 10px;
|
||||
padding: 24px;
|
||||
transition: 0.2s ease-in;
|
||||
padding-top: 90px;
|
||||
padding-top: 120px;
|
||||
}
|
||||
@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 {
|
||||
|
||||
Binary file not shown.
@ -238,7 +238,7 @@ function addForm(){
|
||||
id="perm_yes_${colCount}"
|
||||
value="1"
|
||||
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 class="form-check mt-1">
|
||||
|
||||
@ -19,8 +19,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const titleEl = document.getElementById('pendingTitle');
|
||||
const tabPendingEl = document.getElementById('tabPengajuan');
|
||||
const tabHistoryEl = document.getElementById('tabHistory');
|
||||
const isKomiteMutu = !!window.isKomiteMutu;
|
||||
const isTurt = !!window.isTurt;
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
const selectedIds = new Set();
|
||||
@ -65,54 +63,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
}
|
||||
|
||||
function approvalBadge(status, label){
|
||||
const normalized = String(status || '').toLowerCase();
|
||||
if (normalized === 'approved') return `<span class="badge bg-success">Approved ${label}</span>`;
|
||||
if (normalized.startsWith('rejected')) return `<span class="badge bg-danger">Rejected ${label}</span>`;
|
||||
if (normalized.startsWith('revised')) return `<span class="badge bg-info">Revised ${label}</span>`;
|
||||
return `<span class="badge bg-warning text-dark">Pending ${label}</span>`;
|
||||
}
|
||||
|
||||
function getWorkflowStatusBadges(item){
|
||||
const actionStatus = String(item?.status_action || '').toLowerCase();
|
||||
const mutuStatus = String(item?.status_mutu || '').toLowerCase();
|
||||
const turtStatus = String(item?.status_turt || '').toLowerCase();
|
||||
|
||||
if (actionStatus.startsWith('revised')) {
|
||||
return [`<span class="badge bg-info">Revised</span>`];
|
||||
}
|
||||
|
||||
if (actionStatus.startsWith('rejected')) {
|
||||
return [`<span class="badge bg-danger">Rejected Atasan</span>`];
|
||||
}
|
||||
|
||||
if (actionStatus !== 'approved') {
|
||||
return [`<span class="badge bg-warning text-dark">Pending Atasan</span>`];
|
||||
}
|
||||
|
||||
const statuses = [];
|
||||
|
||||
if (item?.is_akre) {
|
||||
if (mutuStatus.startsWith('rejected')) {
|
||||
statuses.push(`<span class="badge bg-danger">Rejected Mutu</span>`);
|
||||
} else if (mutuStatus !== 'approved') {
|
||||
statuses.push(`<span class="badge bg-warning text-dark">Pending Mutu</span>`);
|
||||
}
|
||||
}
|
||||
|
||||
if (item?.master_kategori_directory_id) {
|
||||
if (turtStatus.startsWith('rejected')) {
|
||||
statuses.push(`<span class="badge bg-danger">Rejected TURT</span>`);
|
||||
} else if (turtStatus !== 'approved') {
|
||||
statuses.push(`<span class="badge bg-warning text-dark">Pending TURT</span>`);
|
||||
}
|
||||
}
|
||||
|
||||
if (statuses.length > 0) {
|
||||
return statuses;
|
||||
}
|
||||
|
||||
return [`<span class="badge bg-success">Approved</span>`];
|
||||
function statusBadge(status){
|
||||
if (status === 'rejected') return '<span class="badge bg-danger">Rejected</span>';
|
||||
if (status === 'revised') return '<span class="badge bg-info">Revised</span>';
|
||||
return '<span class="badge bg-warning text-dark">Pending</span>';
|
||||
}
|
||||
|
||||
function aksesBadge(akses){
|
||||
@ -127,87 +81,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
function isRejected(item){
|
||||
return String(item?.status_action || '').toLowerCase().startsWith('rejected')
|
||||
|| String(item?.status_mutu || '').toLowerCase().startsWith('rejected')
|
||||
|| String(item?.status_turt || '').toLowerCase().startsWith('rejected');
|
||||
}
|
||||
|
||||
function isAtasanApproved(item){
|
||||
return String(item?.status_action || '').toLowerCase() === 'approved';
|
||||
}
|
||||
|
||||
function canProcessRoleStatus(status){
|
||||
const normalized = String(status || '').toLowerCase();
|
||||
return normalized === '' || normalized.startsWith('revised');
|
||||
}
|
||||
|
||||
function canApproveAsAtasan(item){
|
||||
return item?.can_approve_atasan === true || item?.can_approve_atasan === 1 || item?.can_approve_atasan === '1';
|
||||
}
|
||||
|
||||
function canEditSubmission(item){
|
||||
return item?.can_edit_submission === true || item?.can_edit_submission === 1 || item?.can_edit_submission === '1';
|
||||
}
|
||||
|
||||
function hasRevisionInfo(item){
|
||||
return !!(item?.revision || item?.mutu_revision || item?.turt_revision);
|
||||
}
|
||||
|
||||
function collectRevisionNotes(item){
|
||||
const notes = [];
|
||||
const genericRevision = String(item?.revision || '').trim();
|
||||
const mutuRevision = String(item?.mutu_revision || '').trim();
|
||||
const turtRevision = String(item?.turt_revision || '').trim();
|
||||
|
||||
if (genericRevision) {
|
||||
const lowerGeneric = genericRevision.toLowerCase();
|
||||
if (!lowerGeneric.startsWith('komite mutu:') && !lowerGeneric.startsWith('turt:')) {
|
||||
notes.push({ label: 'Atasan', text: genericRevision });
|
||||
}
|
||||
}
|
||||
if (mutuRevision) {
|
||||
notes.push({ label: 'Komite Mutu', text: mutuRevision });
|
||||
}
|
||||
if (turtRevision) {
|
||||
notes.push({ label: 'TURT', text: turtRevision });
|
||||
}
|
||||
|
||||
return notes;
|
||||
}
|
||||
|
||||
function escapeHtml(value){
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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));
|
||||
return item?.status_action === 'rejected';
|
||||
}
|
||||
|
||||
function getSelectableIdsOnPage(){
|
||||
return (tableState.data || [])
|
||||
.filter((item) => !isRejected(item) && !isAtasanApproved(item) && canApproveAsAtasan(item))
|
||||
.filter((item) => !isRejected(item))
|
||||
.map((item) => String(item.file_directory_id));
|
||||
}
|
||||
|
||||
@ -235,11 +114,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
function updateSelectionUI(){
|
||||
const count = selectedIds.size;
|
||||
const selectableIds = getSelectableIdsOnPage();
|
||||
const canBulkApprove = tableState.mode !== 'history' && selectableIds.length > 0;
|
||||
if (selectedCountEl) selectedCountEl.textContent = String(count);
|
||||
if (bulkApproveBtn) bulkApproveBtn.disabled = count === 0 || !canBulkApprove;
|
||||
if (clearSelectionBtn) clearSelectionBtn.disabled = count === 0 || !canBulkApprove;
|
||||
if (bulkApproveBtn) bulkApproveBtn.disabled = count === 0 || tableState.mode === 'history';
|
||||
if (clearSelectionBtn) clearSelectionBtn.disabled = count === 0 || tableState.mode === 'history';
|
||||
updateSelectAllState();
|
||||
}
|
||||
|
||||
@ -249,60 +126,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const tanggalTerbit = item.tanggal_terbit ? formatTanggal(item.tanggal_terbit) : '-';
|
||||
const id = String(item.file_directory_id);
|
||||
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 actions = [
|
||||
`<button class="btn btn-sm btn-primary" onclick="infoDok(this)"
|
||||
data-file="${item.file}"
|
||||
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 || '-'}">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</button>`
|
||||
];
|
||||
|
||||
if (isKomiteMutu || isTurt) {
|
||||
if (canAtasanApprove && !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 (canMutuAct) {
|
||||
actions.push(`<button class="btn btn-sm btn-success" onclick="approvePendingRole('mutu','${item.file_directory_id}','${item.nama_dokumen || ''}')"><i class="fa-solid fa-check"></i></button>`);
|
||||
actions.push(`<button class="btn btn-sm btn-danger" onclick="rejectPendingRole('mutu','${item.file_directory_id}','${item.nama_dokumen || ''}')"><i class="fa-solid fa-xmark"></i></button>`);
|
||||
}
|
||||
if (canTurtAct) {
|
||||
actions.push(`<button class="btn btn-sm btn-success" onclick="approvePendingRole('turt','${item.file_directory_id}','${item.nama_dokumen || ''}')"><i class="fa-solid fa-check"></i></button>`);
|
||||
actions.push(`<button class="btn btn-sm btn-danger" onclick="rejectPendingRole('turt','${item.file_directory_id}','${item.nama_dokumen || ''}')"><i class="fa-solid fa-ban"></i></button>`);
|
||||
}
|
||||
} else {
|
||||
if (!atasanApproved) {
|
||||
actions.push(`<button class="btn btn-sm btn-success" onclick="approvePending('${item.file_directory_id}', '${item.nama_dokumen || ''}')"><i class="fa-solid fa-check"></i></button>`);
|
||||
actions.push(`<button class="btn btn-sm btn-danger" onclick="rejectPending('${item.file_directory_id}', '${item.nama_dokumen || ''}')"><i class="fa-solid fa-xmark"></i></button>`);
|
||||
}
|
||||
}
|
||||
|
||||
if (rejected && atasanRejected && !canMutuAct && !canTurtAct) {
|
||||
actions.length = 1;
|
||||
}
|
||||
|
||||
if (rejected) {
|
||||
if (hasRevisionInfo(item)) {
|
||||
actions.push(`<button class="btn btn-sm btn-info" onclick="infoRejectPending('${item.file_directory_id}')"><i class="fa-solid fa-circle-info"></i></button>`);
|
||||
}
|
||||
if (canEditSubmission(item)) {
|
||||
actions.push(`<button class="btn btn-sm btn-primary" onclick="editRejectedFromPending('${item.file_directory_id}')"><i class="fa-solid fa-pen-to-square"></i></button>`);
|
||||
}
|
||||
}
|
||||
|
||||
const statusContent = (isKomiteMutu || isTurt)
|
||||
? getWorkflowStatusBadges(item).join(' ')
|
||||
: getWorkflowStatusBadges(item).join(' ');
|
||||
const aksi = `
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn btn-sm btn-primary" onclick="infoDok(this)"
|
||||
data-file="${item.file}"
|
||||
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 || '-'}">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</button>
|
||||
<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 || ''}')">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
return `
|
||||
<tr class="${checked ? 'table-active' : ''}">
|
||||
<td class="text-center">
|
||||
@ -310,11 +153,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
class="form-check-input row-select"
|
||||
data-id="${id}"
|
||||
${checked ? 'checked' : ''}
|
||||
${(rejected || atasanApproved || !canAtasanApprove) ? 'disabled' : ''}>
|
||||
${rejected ? 'disabled' : ''}>
|
||||
</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>${statusContent}</td>
|
||||
<td>${statusBadge(item?.status_action)}</td>
|
||||
<td>${aksesBadge(item?.permission_file)}</td>
|
||||
<td><a href="#" class="file-link"
|
||||
data-file="${item.file}"
|
||||
@ -323,8 +166,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
data-no_dokumen="${item.no_dokumen || '-'}"
|
||||
data-tanggal_terbit="${item.tanggal_terbit || '-'}"
|
||||
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-unit"><div class="cell-wrap">${safeText(item.nama_unit)}</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.part)}</div></td>
|
||||
<td class="text-nowrap">${tanggalTerbit}</td>
|
||||
<td class="text-nowrap">${tanggalExp}</td>
|
||||
<td class="text-nowrap">${tanggal}</td>
|
||||
@ -334,7 +177,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
function buildHistoryRow(item){
|
||||
|
||||
const tanggal = item.entry_at ? formatTanggal(item.entry_at) : '-';
|
||||
const tanggalExp = item.tgl_expired ? formatTanggal(item.tgl_expired) : '-';
|
||||
const tanggalTerbit = item.tanggal_terbit ? formatTanggal(item.tanggal_terbit) : '-';
|
||||
@ -350,9 +192,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
data-no_dokumen="${item.no_dokumen || '-'}"
|
||||
data-tanggal_terbit="${item.tanggal_terbit || '-'}"
|
||||
data-permission_file="${item.permission_file || '-'}">${safeText(item.nama_dokumen)}</a></td>
|
||||
<td>${safeText(item.kategori)}</td>
|
||||
<td>${safeText(item.nama_unit)}</td>
|
||||
<td>${safeText(item.folder)}</td>
|
||||
<td>${safeText(item.part)}</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>
|
||||
</tr>
|
||||
`;
|
||||
@ -479,7 +323,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
tabHistoryEl.classList.toggle('d-none', tableState.mode !== 'history');
|
||||
}
|
||||
if (pendingBulkActionsEl) {
|
||||
pendingBulkActionsEl.classList.toggle('d-none', tableState.mode === 'history' || isKomiteMutu || isTurt);
|
||||
pendingBulkActionsEl.classList.toggle('d-none', tableState.mode === 'history');
|
||||
}
|
||||
updateSelectionUI();
|
||||
}
|
||||
@ -560,7 +404,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
showConfirmButton: false
|
||||
});
|
||||
selectedIds.delete(String(id));
|
||||
if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
|
||||
countData();
|
||||
fetchData();
|
||||
}).catch((err) => {
|
||||
Swal.fire({
|
||||
@ -617,127 +461,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
showConfirmButton: false
|
||||
});
|
||||
selectedIds.delete(String(id));
|
||||
if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
|
||||
fetchData();
|
||||
}).catch((err) => {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
text: err.message || 'Terjadi kesalahan.'
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.infoRejectPending = function(id){
|
||||
const item = getItemById(id);
|
||||
Swal.fire({
|
||||
title: 'Catatan Revisi',
|
||||
html: buildRevisionNotesHtml(collectRevisionNotes(item)),
|
||||
icon: 'info',
|
||||
confirmButtonText: 'Tutup'
|
||||
});
|
||||
}
|
||||
|
||||
window.editRejectedFromPending = function(id){
|
||||
const targetUrl = new URL('/pengajuan-file', window.location.origin);
|
||||
targetUrl.searchParams.set('edit', id);
|
||||
window.location.href = targetUrl.toString();
|
||||
}
|
||||
|
||||
window.approvePendingRole = function(type, id, fileName){
|
||||
const endpoint = type === 'mutu'
|
||||
? `/pending-file/${id}/approve-mutu`
|
||||
: `/pending-file/${id}/approve-turt`;
|
||||
const label = type === 'mutu' ? 'Komite Mutu' : 'TURT';
|
||||
|
||||
Swal.fire({
|
||||
title: `Approve ${label}?`,
|
||||
text: fileName || 'Dokumen akan disetujui.',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Approve',
|
||||
cancelButtonText: 'Batal',
|
||||
}).then((result) => {
|
||||
if (!result.isConfirmed) return;
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}).then(async(res) => {
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data?.status) {
|
||||
throw new Error(data?.message || 'Gagal approve.');
|
||||
}
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
text: data.message || 'Dokumen disetujui.',
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
});
|
||||
if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
|
||||
fetchData();
|
||||
}).catch((err) => {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
text: err.message || 'Terjadi kesalahan.'
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.rejectPendingRole = function(type, id, fileName){
|
||||
const endpoint = type === 'mutu'
|
||||
? `/pending-file/${id}/reject-mutu`
|
||||
: `/pending-file/${id}/reject-turt`;
|
||||
const label = type === 'mutu' ? 'Komite Mutu' : 'TURT';
|
||||
|
||||
Swal.fire({
|
||||
title: `Reject ${label}?`,
|
||||
text: fileName || 'Dokumen akan ditolak.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Reject',
|
||||
cancelButtonText: 'Batal',
|
||||
input: 'textarea',
|
||||
inputLabel: 'Catatan',
|
||||
inputPlaceholder: 'Tulis alasan atau catatan revisi...',
|
||||
inputAttributes: {
|
||||
'aria-label': 'Catatan'
|
||||
},
|
||||
preConfirm: (value) => {
|
||||
const revision = (value || '').trim();
|
||||
if (!revision) {
|
||||
Swal.showValidationMessage('Catatan revisi wajib diisi.');
|
||||
}
|
||||
return revision;
|
||||
}
|
||||
}).then((result) => {
|
||||
if (!result.isConfirmed) return;
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ revision: result.value })
|
||||
}).then(async(res) => {
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data?.status) {
|
||||
throw new Error(data?.message || 'Gagal reject.');
|
||||
}
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
text: data.message || 'Dokumen ditolak.',
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
});
|
||||
if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
|
||||
countData();
|
||||
fetchData();
|
||||
}).catch((err) => {
|
||||
Swal.fire({
|
||||
@ -848,7 +572,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
});
|
||||
if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
|
||||
countData();
|
||||
fetchData();
|
||||
}).catch((err) => {
|
||||
Swal.fire({
|
||||
@ -907,5 +631,5 @@ document.addEventListener('click', function(e){
|
||||
function isPublic(permissionVal){
|
||||
if(permissionVal === null || permissionVal === undefined) return false;
|
||||
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
1765
public/json/akreditasi.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,221 +2,57 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Login || RSAB Harapan Kita</title>
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
||||
<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>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Login || RSAB Harapan Kita</title>
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="{{ ver('/assets/css/styles.min.css') }}" />
|
||||
</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 Wrapper -->
|
||||
<div class="container p-5">
|
||||
<div class="row">
|
||||
<div class="col-md-7 d-none d-md-flex flex-column justify-content-center">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-4">
|
||||
|
||||
<img src="/logo/logo_rsabhk.png"
|
||||
class="img-fluid mb-3"
|
||||
style="max-height:90px;">
|
||||
|
||||
<h4 class="fw-bold text-black mb-1">
|
||||
RUMAH SAKIT ANAK DAN BUNDA HARAPAN KITA
|
||||
</h4>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Slider -->
|
||||
<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 class="page-wrapper" id="main-wrapper" data-layout="vertical" data-navbarbg="skin6" data-sidebartype="full"
|
||||
data-sidebar-position="fixed" data-header-position="fixed">
|
||||
<div
|
||||
class="position-relative overflow-hidden text-bg-light min-vh-100 d-flex align-items-center justify-content-center">
|
||||
<div class="d-flex align-items-center justify-content-center w-100">
|
||||
<div class="row justify-content-center w-100">
|
||||
<div class="col-md-8 col-lg-6 col-xxl-3">
|
||||
<div class="card mb-0">
|
||||
<div class="card-body">
|
||||
<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">
|
||||
</a>
|
||||
<p class="text-center">File Directory - Rumah Sakit Harapan Kita</p>
|
||||
<form method="post" action="/login">
|
||||
@csrf
|
||||
@if (session()->has('alertError'))
|
||||
<div class="alert alert-danger fw-bold" role="alert">
|
||||
Username atau password salah!
|
||||
</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">
|
||||
<img src="https://iki-sdm.rsabhk.co.id/metronic/assets/media/illustrations/dozzy-1/6.png"
|
||||
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>
|
||||
<button type="submit" class="btn btn-primary w-100 py-8 fs-4 mb-4 rounded-2">Login</a>
|
||||
</form>
|
||||
</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>
|
||||
|
||||
</html>
|
||||
|
||||
@ -165,7 +165,7 @@
|
||||
function isPublic(permissionVal){
|
||||
if(permissionVal === null || permissionVal === undefined) return false;
|
||||
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;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -35,61 +35,6 @@
|
||||
border: 1px solid #dee2e6 !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>
|
||||
|
||||
@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 flex-grow-1">
|
||||
<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;">
|
||||
<option value="">Kategori (Semua)</option>
|
||||
<option value="akreditasi">Kategori Akreditasi</option>
|
||||
<option value="hukum">Kategori Hukum</option>
|
||||
<option value="lainnya">Kategori Lainnya</option>
|
||||
<select id="tableKategori" class="form-select form-select-sm kategori_kerja_filter" style="max-width: 260px;" multiple>
|
||||
@foreach ($katDok as $kat)
|
||||
<option value="{{ $kat->master_kategori_directory_id }}">
|
||||
{{ $kat->nama_kategori_directory }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="search"
|
||||
id="tableSearch"
|
||||
@ -160,7 +106,7 @@
|
||||
|
||||
</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">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -170,21 +116,7 @@
|
||||
<th>Aksi</th>
|
||||
<th>No Dokumen</th>
|
||||
<th>Nama Dokumen</th>
|
||||
<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>Kategori</th>
|
||||
<th>Unit</th>
|
||||
<th>Tanggal Unggah</th>
|
||||
</tr>
|
||||
@ -194,14 +126,13 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mt-3">
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<span class="small text-muted">Tampilkan</span>
|
||||
<select id="tablePageSize" class="form-select form-select-sm" style="width: 80px;">
|
||||
<option value="5">5</option>
|
||||
<option value="10" >10</option>
|
||||
<option value="20" selected>20</option>
|
||||
<option value="10" selected>10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
@ -226,30 +157,24 @@
|
||||
<script>
|
||||
const katDok = @json($katDok);
|
||||
const authUnitKerja = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]?->unitKerja);
|
||||
|
||||
// const authSubUnitKerja = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]->sub_unit_kerja);
|
||||
const authSubUnitKerja = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]->sub_unit_kerja);
|
||||
const mappingUnitKerjaPegawai = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]);
|
||||
const authPegawai = @json(auth()->user()->objectpegawaifk);
|
||||
const formCreate = $("#formFile")
|
||||
const modalCreate = document.getElementById('modalCreateFile')
|
||||
const tableState = { data: [], page: 1, pageSize: 8, search: '', unit: [], kategori: [], kategoriType: [], kategoriHeader: [], lastPage: 1, total: 0 };
|
||||
let kategoriOptionCache = [];
|
||||
const tableState = { data: [], page: 1, pageSize: 8, search: '', unit: [], kategori: [], lastPage: 1, total: 0 };
|
||||
const tbody = document.getElementById('tableDataUmum');
|
||||
const paginationEl = document.getElementById('paginationControls');
|
||||
const summaryEl = document.getElementById('tableSummary');
|
||||
const legendEl = document.getElementById('tableLegend');
|
||||
const pageSizeSelect = document.getElementById('tablePageSize');
|
||||
const unitSelect = document.getElementById('tableUnit');
|
||||
const kategoriSelect = document.getElementById('tableKategori');
|
||||
const kategoriHeaderMenu = document.getElementById('tableKategoriHeaderMenu');
|
||||
const searchInput = document.getElementById('tableSearch');
|
||||
const searchBtn = document.getElementById('btnTableSearch');
|
||||
const downloadBtn = document.getElementById('btnDownloadMultiple');
|
||||
const selectedCountEl = document.getElementById('selectedCount');
|
||||
const checkAllEl = document.getElementById('checkAllRows');
|
||||
const selectedIds = new Set();
|
||||
const colorCache = {};
|
||||
const colorPalette = ['#e8f4ff', '#fff6e5', '#e9f7ef', '#f3e8ff', '#ffe6ea', '#e6f5f3'];
|
||||
|
||||
document.addEventListener('change', function(e){
|
||||
if(!e.target.classList.contains('toggle-expired')) return;
|
||||
@ -309,35 +234,16 @@
|
||||
}
|
||||
if (kategoriSelect) {
|
||||
$('#tableKategori').select2({
|
||||
placeholder: 'Kategori (Semua)',
|
||||
placeholder: 'Pilih Kategori',
|
||||
allowClear: true,
|
||||
width: '100%'
|
||||
width: '100%',
|
||||
closeOnSelect: false
|
||||
});
|
||||
$('#tableKategori').on('change', function () {
|
||||
const val = $(this).val() || '';
|
||||
console.log(val);
|
||||
|
||||
tableState.kategoriType = val ? [val] : [];
|
||||
tableState.page = 1;
|
||||
fetchData();
|
||||
tableState.kategori = $(this).val() || [];
|
||||
});
|
||||
}
|
||||
}
|
||||
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(){
|
||||
colCount = 1;
|
||||
@ -351,48 +257,7 @@
|
||||
function isPublic(permissionVal){
|
||||
if(permissionVal === null || permissionVal === undefined) return false;
|
||||
const val = String(permissionVal).toLowerCase();
|
||||
return val === '1' || val === 'true' || val === 'ya' || 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('');
|
||||
return val === '1' || val === 'true' || val === 'iya' || val === 'yes';
|
||||
}
|
||||
|
||||
function getExpiryStatus(dateStr){
|
||||
@ -408,30 +273,6 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
function escapeHtml(value){
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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){
|
||||
const parts = (item.file || '').split('/');
|
||||
const fileName = parts.pop() || '-';
|
||||
@ -452,19 +293,12 @@
|
||||
statusClass = 'bg-secondary';
|
||||
}
|
||||
const checked = selectedIds.has(String(item.file_directory_id)) ? 'checked' : '';
|
||||
const kategoriFlag = resolveKategoriFlag(item);
|
||||
|
||||
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 rowClass = expiryStatus === 'expired' ? 'table-danger' : (expiryStatus === 'soon' ? 'table-warning' : '');
|
||||
const expiryBadge = expiryStatus === 'expired'
|
||||
? `<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>` : '');
|
||||
const akreBadge = isAkre
|
||||
? `<span class="badge bg-primary text-white" style="font-size:10px;">Akreditasi</span>`
|
||||
: '';
|
||||
return `
|
||||
<tr class="${rowClass}" style="--row-bg:${rowColor};">
|
||||
<tr class="${rowClass}">
|
||||
|
||||
<td class="text-center">
|
||||
<input type="checkbox"
|
||||
@ -497,22 +331,6 @@
|
||||
>
|
||||
<i class="fa-solid fa-download"></i>
|
||||
</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>
|
||||
</td>
|
||||
|
||||
@ -539,20 +357,19 @@
|
||||
word-break:break-word;
|
||||
"
|
||||
>
|
||||
${item.nama_dokumen || '-'} ${akreBadge}
|
||||
${item.nama_dokumen || '-'}
|
||||
</a>
|
||||
${expiryBadge}
|
||||
|
||||
</div>
|
||||
${renderDeleteRecommendation(item)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
${item.nama_kategori || '-'}
|
||||
${kategoriName}
|
||||
</td>
|
||||
<td>
|
||||
${item.unit?.name || '-'}
|
||||
${unitName}
|
||||
</td>
|
||||
<td class="text-nowrap">${formatTanggal(item.entry_at)}</td>
|
||||
</tr>
|
||||
@ -603,12 +420,12 @@
|
||||
}
|
||||
|
||||
function renderTable(){
|
||||
const pageData = filterByKategoriType(tableState.data || []);
|
||||
const pageData = tableState.data || [];
|
||||
|
||||
if(pageData.length === 0){
|
||||
tbody.innerHTML = `
|
||||
<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
|
||||
</td>
|
||||
</tr>
|
||||
@ -626,85 +443,17 @@
|
||||
renderPagination(tableState.lastPage || 1);
|
||||
syncCheckAllState();
|
||||
updateSelectedCount();
|
||||
renderLegend(pageData);
|
||||
}
|
||||
|
||||
function applyTableSearch(){
|
||||
const value = searchInput ? searchInput.value : '';
|
||||
tableState.search = (value || '').trim();
|
||||
tableState.unit = unitSelect && window.$ ? ($('#tableUnit').val() || []) : (tableState.unit || []);
|
||||
const katVal = kategoriSelect && window.$ ? ($('#tableKategori').val() || '') : (kategoriSelect?.value || '');
|
||||
tableState.kategoriType = katVal ? [katVal] : (tableState.kategoriType || []);
|
||||
tableState.kategori = kategoriSelect && window.$ ? ($('#tableKategori').val() || []) : (tableState.kategori || []);
|
||||
tableState.page = 1;
|
||||
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(){
|
||||
if(summaryEl) summaryEl.textContent = 'Memuat data...';
|
||||
const params = new URLSearchParams({
|
||||
@ -715,20 +464,15 @@
|
||||
if (tableState.unit && tableState.unit.length > 0) {
|
||||
tableState.unit.forEach(id => params.append('unit[]', id));
|
||||
}
|
||||
if (tableState.kategoriType && tableState.kategoriType.length > 0) {
|
||||
tableState.kategoriType.forEach(id => params.append('kategori[]', id));
|
||||
}
|
||||
if (tableState.kategoriHeader && tableState.kategoriHeader.length > 0) {
|
||||
tableState.kategoriHeader.forEach(id => params.append('kategori_header[]', id));
|
||||
if (tableState.kategori && tableState.kategori.length > 0) {
|
||||
tableState.kategori.forEach(kat => params.append('kategori[]', kat));
|
||||
}
|
||||
fetch(`/datatable-umum?${params.toString()}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
tableState.data = data?.data || [];
|
||||
kategoriOptionCache = data?.kategori_list || kategoriOptionCache;
|
||||
tableState.lastPage = data?.pagination?.last_page || 1;
|
||||
tableState.total = data?.pagination?.total || 0;
|
||||
renderKategoriHeaderOptions();
|
||||
renderTable();
|
||||
})
|
||||
.catch(error => {
|
||||
@ -758,7 +502,6 @@
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
renderKategoriHeaderOptions();
|
||||
fetchData()
|
||||
|
||||
function updateSelectedCount(){
|
||||
@ -1218,67 +961,5 @@
|
||||
}
|
||||
$("#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>
|
||||
@endsection
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
@extends('layout.main')
|
||||
<style>
|
||||
/* --- SELECT2: teks terlihat (hitam) --- */
|
||||
|
||||
.select2-container--default .select2-selection--multiple {
|
||||
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 {
|
||||
@ -35,59 +34,6 @@
|
||||
border: 1px solid #dee2e6 !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>
|
||||
@section('body_main')
|
||||
<div class="row">
|
||||
@ -125,7 +71,6 @@
|
||||
</span>
|
||||
</div>
|
||||
<!-- Tambah Dokumen -->
|
||||
@if(!Auth::guard('admin')->check())
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success btn-sm"
|
||||
@ -135,19 +80,17 @@
|
||||
<i class="ti ti-plus me-1"></i>
|
||||
Tambah Dokumen
|
||||
</button>
|
||||
@endif
|
||||
</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 flex-grow-1">
|
||||
<select id="tableUnit" style="max-width: 260px;">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
<select id="tableKategori" class="form-select form-select-sm" style="max-width: 260px;">
|
||||
<option value="">Kategori (Semua)</option>
|
||||
<option value="akreditasi">Kategori Akreditasi</option>
|
||||
<option value="hukum">Kategori Hukum</option>
|
||||
<option value="lainnya">Kategori Lainnya</option>
|
||||
<select id="tableUnit" class="form-select form-select-sm unit_kerja_filter" style="max-width: 260px;"></select>
|
||||
<select id="tableKategori" class="form-select form-select-sm kategori_kerja_filter" style="max-width: 260px;" multiple>
|
||||
@foreach ($katDok as $kat)
|
||||
<option value="{{ $kat->master_kategori_directory_id }}">
|
||||
{{ $kat->nama_kategori_directory }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="search"
|
||||
id="tableSearch"
|
||||
@ -157,7 +100,7 @@
|
||||
</div>
|
||||
<button class="btn btn-primary" type="button" id="btnTableSearch">Cari</button>
|
||||
</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">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -167,21 +110,7 @@
|
||||
<th>Aksi</th>
|
||||
<th>No Dokumen</th>
|
||||
<th>Nama Dokumen</th>
|
||||
<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>Kategori</th>
|
||||
<th>Unit</th>
|
||||
<th>Tanggal Unggah</th>
|
||||
<th>Pengunggah</th>
|
||||
@ -192,14 +121,13 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mt-3">
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<span class="small text-muted">Tampilkan</span>
|
||||
<select id="tablePageSize" class="form-select form-select-sm" style="width: 80px;">
|
||||
<option value="5">5</option>
|
||||
<option value="10">10</option>
|
||||
<option value="20" selected>20</option>
|
||||
<option value="10" selected>10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
@ -235,144 +163,27 @@
|
||||
const authPegawai = @json(auth()->user()->objectpegawaifk);
|
||||
const formCreate = $("#formFile");
|
||||
const modalCreate = document.getElementById('modalCreateFile');
|
||||
const tableState = { data: [], page: 1, pageSize: 8, search: '', unit: [], kategori: [], kategoriType: [], kategoriHeader: [], lastPage: 1, total: 0 };
|
||||
let kategoriOptionCache = [];
|
||||
const tableState = { data: [], page: 1, pageSize: 8, search: '', unit: [], kategori: [], lastPage: 1, total: 0 };
|
||||
const tbody = document.getElementById('tableDataUnit');
|
||||
const paginationEl = document.getElementById('paginationControls');
|
||||
const summaryEl = document.getElementById('tableSummary');
|
||||
const legendEl = document.getElementById('tableLegend');
|
||||
const pageSizeSelect = document.getElementById('tablePageSize');
|
||||
const unitSelect = document.getElementById('tableUnit');
|
||||
const kategoriSelect = document.getElementById('tableKategori');
|
||||
const kategoriHeaderMenu = document.getElementById('tableKategoriHeaderMenu');
|
||||
const searchInput = document.getElementById('tableSearch');
|
||||
const searchBtn = document.getElementById('btnTableSearch');
|
||||
const downloadBtn = document.getElementById('btnDownloadMultiple');
|
||||
const selectedCountEl = document.getElementById('selectedCount');
|
||||
const checkAllEl = document.getElementById('checkAllRows');
|
||||
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){
|
||||
if(!e.target.classList.contains('masa-berlaku-select')) return;
|
||||
syncMasaBerlakuField(e.target);
|
||||
});
|
||||
|
||||
document.addEventListener('change', function(e){
|
||||
if (!e.target.matches('input[type="date"][id^="dateActive_"]')) return;
|
||||
const index = e.target.id.replace('dateActive_', '');
|
||||
const selectEl = document.querySelector(`.masa-berlaku-select[data-index="${index}"]`);
|
||||
if (selectEl) syncMasaBerlakuField(selectEl);
|
||||
if(!e.target.classList.contains('toggle-expired')) return;
|
||||
const targetId = e.target.getAttribute('data-target');
|
||||
if(!targetId) return;
|
||||
const fieldWrap = document.getElementById(targetId);
|
||||
const input = fieldWrap.querySelector('input');
|
||||
input.disabled = !e.target.checked;
|
||||
});
|
||||
|
||||
if(pageSizeSelect){
|
||||
@ -389,11 +200,13 @@
|
||||
}
|
||||
if (window.$ && $.fn.select2) {
|
||||
if (unitSelect) {
|
||||
$('#tableUnit').select2({
|
||||
$('#tableUnit').select2({
|
||||
placeholder: 'Pilih Unit',
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
closeOnSelect: false,
|
||||
selectionCssClass: 'select2-filter-selection',
|
||||
dropdownCssClass: 'select2-filter-dropdown',
|
||||
ajax: {
|
||||
url: '/select-unit-kerja-mapping',
|
||||
dataType: 'json',
|
||||
@ -412,39 +225,24 @@
|
||||
cache: true
|
||||
}
|
||||
});
|
||||
|
||||
$('#tableUnit').on('change', function () {
|
||||
tableState.unit = $(this).val() || [];
|
||||
});
|
||||
}
|
||||
if (kategoriSelect) {
|
||||
$('#tableKategori').select2({
|
||||
placeholder: 'Kategori (Semua)',
|
||||
placeholder: 'Pilih Kategori',
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
closeOnSelect: false,
|
||||
selectionCssClass: 'select2-filter-selection',
|
||||
dropdownCssClass: 'select2-filter-dropdown'
|
||||
});
|
||||
$('#tableKategori').on('change', function () {
|
||||
const val = $(this).val() || '';
|
||||
tableState.kategoriType = val ? [val] : [];
|
||||
tableState.page = 1;
|
||||
fetchData();
|
||||
tableState.kategori = $(this).val() || [];
|
||||
});
|
||||
}
|
||||
}
|
||||
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(){
|
||||
colCount = 1;
|
||||
@ -453,7 +251,6 @@
|
||||
formCreate.find('select').val(null).trigger('change');
|
||||
formCreate.find('input[type="file"]').val('');
|
||||
formCreate.find('.file-name').addClass('d-none').text('');
|
||||
document.querySelectorAll('.masa-berlaku-select').forEach(syncMasaBerlakuField);
|
||||
resetAkreFields(0);
|
||||
enableAkreFields(0);
|
||||
}
|
||||
@ -464,7 +261,7 @@
|
||||
|
||||
function loadAkreData(){
|
||||
if(akreLoaded) return Promise.resolve(akreData);
|
||||
return fetch('/json/akreditasi.jff')
|
||||
return fetch('/json/akreditasi.json')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
akreData = Array.isArray(data) ? data : [];
|
||||
@ -488,7 +285,7 @@
|
||||
const children = Array.isArray(seg.turunan) ? seg.turunan : [];
|
||||
return children.map(child => ({
|
||||
value: `${type.name}/${seg.name}/${child.name}`,
|
||||
label: `${type.name} / ${child.name}`,
|
||||
label: `${type.name} / ${seg.name} / ${child.name}`,
|
||||
type: type.name,
|
||||
segment: seg.name,
|
||||
item: child.name
|
||||
@ -536,18 +333,17 @@
|
||||
|
||||
function enableAkreFields(index){
|
||||
const selectEl = document.getElementById(`akre_select_${index}`);
|
||||
// if(selectEl){
|
||||
// selectEl.disabled = false;
|
||||
// selectEl.required = true;
|
||||
// }
|
||||
if(selectEl){
|
||||
selectEl.disabled = false;
|
||||
selectEl.required = true;
|
||||
}
|
||||
setKategoriRequired(index, false);
|
||||
loadAkreData().then(() => {
|
||||
fillAkreSelect(selectEl);
|
||||
if(window.$ && $.fn.select2){
|
||||
$(selectEl).select2({
|
||||
dropdownParent: $('#modalCreateFile'),
|
||||
placeholder: 'Pilih Instrumen',
|
||||
allowClear: true
|
||||
placeholder: 'Pilih Instrumen'
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -560,8 +356,7 @@
|
||||
if(katSelect.length){
|
||||
katSelect.select2({
|
||||
dropdownParent: $('#modalCreateFile'),
|
||||
placeholder:'Pilih Kategori',
|
||||
allowClear: true
|
||||
placeholder:'Pilih Kategori'
|
||||
});
|
||||
}
|
||||
if(hukumSelect.length){
|
||||
@ -592,48 +387,7 @@
|
||||
function isPublic(permissionVal){
|
||||
if(permissionVal === null || permissionVal === undefined) return false;
|
||||
const val = String(permissionVal).toLowerCase();
|
||||
return val === '1' || val === 'true' || val === 'ya' || 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('');
|
||||
return val === '1' || val === 'true' || val === 'iya' || val === 'yes';
|
||||
}
|
||||
|
||||
function getExpiryStatus(dateStr){
|
||||
@ -649,31 +403,6 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
function escapeHtml(value){
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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){
|
||||
const parts = (item.file || '').split('/');
|
||||
const fileName = parts.pop() || '-';
|
||||
@ -694,18 +423,12 @@
|
||||
statusClass = 'bg-secondary';
|
||||
}
|
||||
const checked = selectedIds.has(String(item.file_directory_id)) ? 'checked' : '';
|
||||
const kategoriFlag = resolveKategoriFlag(item);
|
||||
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 rowClass = expiryStatus === 'expired' ? 'table-danger' : (expiryStatus === 'soon' ? 'table-warning' : '');
|
||||
const expiryBadge = expiryStatus === 'expired'
|
||||
? `<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>` : '');
|
||||
const akreBadge = isAkre
|
||||
? `<span class="badge bg-primary text-white ms-2" style="font-size:10px;">Akreditasi</span>`
|
||||
: '';
|
||||
return `
|
||||
<tr class="${rowClass}" style="--row-bg:${rowColor};">
|
||||
<tr class="${rowClass}">
|
||||
<td class="text-center">
|
||||
<input type="checkbox"
|
||||
class="form-check-input row-check"
|
||||
@ -737,23 +460,6 @@
|
||||
>
|
||||
<i class="fa-solid fa-download"></i>
|
||||
</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>
|
||||
</td>
|
||||
<td class="text-nowrap">${item.no_dokumen || '-'}</td>
|
||||
@ -771,18 +477,17 @@
|
||||
>
|
||||
${item.nama_dokumen || '-'}
|
||||
</a>
|
||||
${akreBadge}${expiryBadge}
|
||||
${expiryBadge}
|
||||
</div>
|
||||
${renderDeleteRecommendation(item)}
|
||||
</td>
|
||||
<td>
|
||||
${item.nama_kategori || '-'}
|
||||
${kategoriName}
|
||||
</td>
|
||||
<td>
|
||||
${item.unit?.name || '-'}
|
||||
${unitName}
|
||||
</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>
|
||||
`;
|
||||
}
|
||||
@ -831,12 +536,12 @@
|
||||
}
|
||||
|
||||
function renderTable(){
|
||||
const pageData = filterByKategoriType(tableState.data || []);
|
||||
const pageData = tableState.data || [];
|
||||
|
||||
if(pageData.length === 0){
|
||||
tbody.innerHTML = `
|
||||
<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
|
||||
</td>
|
||||
</tr>
|
||||
@ -854,110 +559,17 @@
|
||||
renderPagination(tableState.lastPage || 1);
|
||||
syncCheckAllState();
|
||||
updateSelectedCount();
|
||||
renderLegend(pageData);
|
||||
}
|
||||
|
||||
function applyTableSearch(){
|
||||
const value = searchInput ? searchInput.value : '';
|
||||
tableState.search = (value || '').trim();
|
||||
tableState.unit = normalizeToArray(
|
||||
unitSelect && window.$ ? $('#tableUnit').val() : tableState.unit
|
||||
);
|
||||
const katVal = kategoriSelect && window.$ ? ($('#tableKategori').val() || '') : (kategoriSelect?.value || '');
|
||||
tableState.kategoriType = katVal ? [katVal] : (tableState.kategoriType || []);
|
||||
tableState.unit = unitSelect && window.$ ? ($('#tableUnit').val() || []) : (tableState.unit || []);
|
||||
tableState.kategori = kategoriSelect && window.$ ? ($('#tableKategori').val() || []) : (tableState.kategori || []);
|
||||
tableState.page = 1;
|
||||
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(){
|
||||
if(summaryEl) summaryEl.textContent = 'Memuat data...';
|
||||
const params = new URLSearchParams({
|
||||
@ -965,24 +577,18 @@
|
||||
per_page: tableState.pageSize,
|
||||
keyword: tableState.search
|
||||
});
|
||||
const unitValues = normalizeToArray(tableState.unit);
|
||||
if (unitValues.length > 0) {
|
||||
unitValues.forEach(id => params.append('unit[]', id));
|
||||
if (tableState.unit && tableState.unit.length > 0) {
|
||||
tableState.unit.forEach(id => params.append('unit[]', id));
|
||||
}
|
||||
if (tableState.kategoriType && tableState.kategoriType.length > 0) {
|
||||
tableState.kategoriType.forEach(id => params.append('kategori[]', id));
|
||||
}
|
||||
if (tableState.kategoriHeader && tableState.kategoriHeader.length > 0) {
|
||||
tableState.kategoriHeader.forEach(id => params.append('kategori_header[]', id));
|
||||
if (tableState.kategori && tableState.kategori.length > 0) {
|
||||
tableState.kategori.forEach(kat => params.append('kategori[]', kat));
|
||||
}
|
||||
fetch(`/data-internal?${params.toString()}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
tableState.data = data?.data || [];
|
||||
kategoriOptionCache = data?.kategori_list || kategoriOptionCache;
|
||||
tableState.lastPage = data?.pagination?.last_page || 1;
|
||||
tableState.total = data?.pagination?.total || 0;
|
||||
renderKategoriHeaderOptions();
|
||||
renderTable();
|
||||
})
|
||||
.catch(error => {
|
||||
@ -991,13 +597,17 @@
|
||||
})
|
||||
}
|
||||
|
||||
searchBtn.addEventListener('click', applyTableSearch);
|
||||
searchInput.addEventListener('keydown', (e) => {
|
||||
if (searchBtn) {
|
||||
searchBtn.addEventListener('click', applyTableSearch);
|
||||
}
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
applyTableSearch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatTanggal(dateString) {
|
||||
const d = new Date(dateString);
|
||||
@ -1007,7 +617,6 @@
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
renderKategoriHeaderOptions();
|
||||
fetchData()
|
||||
|
||||
function updateSelectedCount(){
|
||||
@ -1159,7 +768,6 @@
|
||||
selectOptionUnitKerjaV1(0);
|
||||
initKategoriSelect2(0);
|
||||
enableAkreFields(0);
|
||||
document.querySelectorAll('.masa-berlaku-select').forEach(syncMasaBerlakuField);
|
||||
});
|
||||
|
||||
function loadSubUnitKerja(unitId){
|
||||
@ -1169,8 +777,6 @@
|
||||
url: `/select-sub-unit-kerja-mapping/${unitId}`,
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
console.log(response);
|
||||
|
||||
if (response?.data) {
|
||||
response.data.forEach(unit => {
|
||||
let selected = (authSubUnitKerja && unit.id === authSubUnitKerja.objectsubunitkerjapegawaifk);
|
||||
@ -1240,39 +846,28 @@
|
||||
placeholder="Contoh: Panduan Mencuci Tangan" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Tanggal Terbit</label>
|
||||
<input class="form-control"
|
||||
type="date"
|
||||
name="data[${colCount}][date_active]"
|
||||
id="dateActive_${colCount}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Masa Berlaku Dokumen</label>
|
||||
<select class="form-select masa-berlaku-select"
|
||||
name="data[${colCount}][masa_berlaku_option]"
|
||||
data-index="${colCount}"
|
||||
data-date-target="expiredField_${colCount}"
|
||||
data-input-target="expiredInput_${colCount}"
|
||||
data-base-target="dateActive_${colCount}"
|
||||
data-preview-target="expiredPreview_${colCount}">
|
||||
<option value="">Selamanya</option>
|
||||
<option value="1">1 Tahun</option>
|
||||
<option value="2">2 Tahun</option>
|
||||
<option value="3">3 Tahun</option>
|
||||
<option value="custom">Lainnya</option>
|
||||
</select>
|
||||
<div class="form-text text-muted">Opsi 1, 2, dan 3 tahun dihitung dari Tanggal Terbit.</div>
|
||||
<div class="form-text text-primary" id="expiredPreview_${colCount}"></div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Tanggal Terbit</label>
|
||||
<input class="form-control"
|
||||
type="date"
|
||||
name="data[${colCount}][date_active]">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input toggle-expired"
|
||||
type="checkbox"
|
||||
id="hasExpired_${colCount}"
|
||||
data-target="expiredField_${colCount}">
|
||||
<label class="form-check-label" for="hasExpired_${colCount}">Masa Berlaku Dokumen??</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 d-none" id="expiredField_${colCount}">
|
||||
<label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label>
|
||||
<input class="form-control"
|
||||
type="date"
|
||||
id="expiredInput_${colCount}"
|
||||
name="data[${colCount}][tgl_expired]" disabled>
|
||||
</div>
|
||||
<div class="col-md-5" id="expiredField_${colCount}">
|
||||
<label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label>
|
||||
<input class="form-control"
|
||||
type="date"
|
||||
name="data[${colCount}][tgl_expired]" disabled>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Boleh dilihat unit lain? <span class="text-danger">*</span></label>
|
||||
@ -1285,7 +880,7 @@
|
||||
id="perm_yes_${colCount}"
|
||||
value="1"
|
||||
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 class="form-check mt-1">
|
||||
@ -1302,29 +897,33 @@
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<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>
|
||||
</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>
|
||||
<div class="col-md-4">
|
||||
<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;">
|
||||
<option value="" disabled selected>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>
|
||||
<option value="Pedir - Kebijakan">Pedir - Kebijakan</option>
|
||||
<option value="Keputusan Dirut - Kebijakan">Keputusan Dirut - Kebijakan</option>
|
||||
<option value="Surat Edaran - Kebijakan">Surat Edaran - Kebijakan</option>
|
||||
<option value="Pengumuman - Kebijakan">Pengumuman - Kebijakan</option>
|
||||
<option value="Pelayanan Kesehatan - Kerjasama">Pelayanan Kesehatan - Kerjasama</option>
|
||||
<option value="Management - Kerjasama">Management - Kerjasama</option>
|
||||
<option value="Pemeliharan - Kerjasama">Pemeliharan - Kerjasama</option>
|
||||
<option value="iklat - Kerjasama">Diklat - Kerjasama</option>
|
||||
<option value="Luar Negeri - Kerjasama">Luar Negeri - Kerjasama</option>
|
||||
<option value="Area Bisnis - Kerjasama">Area Bisnis - Kerjasama</option>
|
||||
<option value="Pendidikan - Kerjasama">Pendidikan - Kerjasama</option>
|
||||
<option value="Pengampuan KIA">Pengampuan KIA</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 Lainnya</label>
|
||||
@ -1391,7 +990,7 @@
|
||||
processResults: function(data){
|
||||
return {
|
||||
results : data?.data.map(item => ({
|
||||
id: item.id,
|
||||
id: item.id+'/'+item.name,
|
||||
text: item.name,
|
||||
sub_units: item.sub_unit_kerja // kirim ke front
|
||||
}))
|
||||
@ -1541,18 +1140,6 @@
|
||||
e.preventDefault();
|
||||
const submitBtn = $(this).find('button[type="submit"]');
|
||||
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);
|
||||
console.log(formData);
|
||||
|
||||
@ -1657,67 +1244,5 @@
|
||||
}
|
||||
$("#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>
|
||||
@endsection
|
||||
|
||||
@ -46,29 +46,17 @@
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<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 class="col-md-4">
|
||||
<label class="form-label fw-semibold">Masa Berlaku Dokumen</label>
|
||||
<select class="form-select masa-berlaku-select"
|
||||
name="data[0][masa_berlaku_option]"
|
||||
data-index="0"
|
||||
data-date-target="expiredField_0"
|
||||
data-input-target="expiredInput_0"
|
||||
data-base-target="dateActive_0"
|
||||
data-preview-target="expiredPreview_0">
|
||||
<option value="">Selamanya</option>
|
||||
<option value="1">1 Tahun</option>
|
||||
<option value="2">2 Tahun</option>
|
||||
<option value="3">3 Tahun</option>
|
||||
<option value="custom">Lainnya</option>
|
||||
</select>
|
||||
<div class="form-text text-muted">Opsi 1, 2, dan 3 tahun dihitung dari Tanggal Terbit.</div>
|
||||
<div class="form-text text-primary" id="expiredPreview_0"></div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input toggle-expired" type="checkbox" id="hasExpired0" data-target="expiredField_0">
|
||||
<label class="form-check-label" for="hasExpired0">Masa Berlaku Dokumen?</label>
|
||||
</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>
|
||||
<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 class="col-md-4">
|
||||
<label class="form-label fw-semibold">Boleh dilihat unit lain? <span class="text-danger">*</span></label>
|
||||
@ -77,7 +65,7 @@
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="data[0][is_permission]" id="perm_yes" value="1" required>
|
||||
<label class="form-check-label" for="perm_yes">
|
||||
Ya
|
||||
Iya
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-1">
|
||||
@ -90,29 +78,33 @@
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<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>
|
||||
</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>
|
||||
<div class="col-md-4">
|
||||
<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;">
|
||||
<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>
|
||||
<option value="Pedir - Kebijakan">Pedir - Kebijakan</option>
|
||||
<option value="Keputusan Dirut - Kebijakan">Keputusan Dirut - Kebijakan</option>
|
||||
<option value="Surat Edaran - Kebijakan">Surat Edaran - Kebijakan</option>
|
||||
<option value="Pengumuman - Kebijakan">Pengumuman - Kebijakan</option>
|
||||
<option value="Pelayanan Kesehatan - Kerjasama">Pelayanan Kesehatan - Kerjasama</option>
|
||||
<option value="Management - Kerjasama">Management - Kerjasama</option>
|
||||
<option value="Pemeliharan - Kerjasama">Pemeliharan - Kerjasama</option>
|
||||
<option value="iklat - Kerjasama">Diklat - Kerjasama</option>
|
||||
<option value="Luar Negeri - Kerjasama">Luar Negeri - Kerjasama</option>
|
||||
<option value="Area Bisnis - Kerjasama">Area Bisnis - Kerjasama</option>
|
||||
<option value="Pendidikan - Kerjasama">Pendidikan - Kerjasama</option>
|
||||
<option value="Pengampuan KIA">Pengampuan KIA</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 lainnya</label>
|
||||
|
||||
@ -1,32 +1,4 @@
|
||||
@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">
|
||||
@if ($showRecapTitle)
|
||||
<div>
|
||||
@ -53,12 +25,12 @@
|
||||
</button>
|
||||
</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">
|
||||
<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:30%;">Unit</th>
|
||||
<th style="width:20%;">Kategori</th>
|
||||
<th style="width:15%;" class="text-center">Jumlah File</th>
|
||||
</tr>
|
||||
@ -70,16 +42,13 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => fetchRecap());
|
||||
|
||||
let recapDebounce;
|
||||
const recapState = { page:1, perPage:10, keyword:'', lastPage:1, totalUnits:0, grandTotalFiles:0, currentPageTotalFiles:0 };
|
||||
let recapRequestController = null;
|
||||
let recapRequestToken = 0;
|
||||
const recapState = { page:1, perPage:10, keyword:'', lastPage:1 };
|
||||
|
||||
function debouncedRecapSearch(val){
|
||||
clearTimeout(recapDebounce);
|
||||
@ -99,77 +68,48 @@ function changePerPage(val){
|
||||
function fetchRecap(){
|
||||
const tbody = document.getElementById('recapBody');
|
||||
const pager = document.getElementById('recapPagination');
|
||||
const summaryEl = document.getElementById('recapSummary');
|
||||
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>`;
|
||||
if(pager) pager.innerHTML = '';
|
||||
if(summaryEl) summaryEl.textContent = 'Memuat ringkasan rekap...';
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: recapState.page,
|
||||
per_page: recapState.perPage,
|
||||
keyword: recapState.keyword || ''
|
||||
});
|
||||
fetch('/data/recap?' + params.toString(), {
|
||||
signal: recapRequestController.signal,
|
||||
})
|
||||
fetch('/data/recap?' + params.toString())
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
if (requestToken !== recapRequestToken) return;
|
||||
const rows = json?.data || [];
|
||||
recapState.page = json?.pagination?.current_page || recapState.page;
|
||||
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){
|
||||
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;
|
||||
}
|
||||
let grandTotal = 0;
|
||||
const html = rows.map((row, idx) => {
|
||||
const rowNumber = ((recapState.page - 1) * recapState.perPage) + idx + 1;
|
||||
const folderRows = (row.data || []).map((f, i) => `
|
||||
<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>` : ''}
|
||||
<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">Subtotal Halaman Ini</td>
|
||||
<td class="text-center fw-bold">${recapState.currentPageTotalFiles}</td>
|
||||
<td colspan="3" class="text-end fw-semibold">Total File</td>
|
||||
<td class="text-center fw-bold">${grandTotal}</td>
|
||||
</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();
|
||||
})
|
||||
.catch(err => {
|
||||
if (err?.name === 'AbortError') return;
|
||||
console.error(err);
|
||||
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
@ -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>
|
||||
@ -21,55 +21,6 @@
|
||||
<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>
|
||||
<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-content,
|
||||
.modal-body,
|
||||
|
||||
@ -41,69 +41,44 @@
|
||||
<span class="hide-menu">Dokumen Umum</span>
|
||||
</a>
|
||||
</li>
|
||||
@if(Auth::guard('admin')->check() || (Auth::check() && auth()->user()->dataUser->mappingUnitKerjaPegawai()->whereIn('objectunitkerjapegawaifk', [51, 22])->exists()))
|
||||
<li class="sidebar-item">
|
||||
<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>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
{{-- AKTIVITAS --}}
|
||||
<li class="nav-small-cap"><span class="hide-menu">Aktivitas</span></li>
|
||||
|
||||
|
||||
@php
|
||||
$pegawaiId = auth()->user()->objectpegawaifk;
|
||||
$userUnitIds = auth()->user()->dataUser?->mappingUnitKerjaPegawai()
|
||||
->where('statusenabled', true)
|
||||
->pluck('objectunitkerjapegawaifk')
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
if($userUnitIds){
|
||||
$isAtasan = \App\Models\MappingUnitKerjaPegawai::where('statusenabled', true)
|
||||
->where(function ($q) use ($pegawaiId) {
|
||||
$q->where('objectatasanlangsungfk', $pegawaiId)
|
||||
->orWhere('objectpejabatpenilaifk', $pegawaiId);
|
||||
})->exists();
|
||||
$isRoleApprover = in_array(51, $userUnitIds, true) || in_array(22, $userUnitIds, true);
|
||||
}else{
|
||||
$isAtasan = null;
|
||||
}
|
||||
|
||||
|
||||
$isAtasan = \App\Models\MappingUnitKerjaPegawai::where('statusenabled', true)->where('objectatasanlangsungfk', auth()->user()->objectpegawaifk)->exists();
|
||||
@endphp
|
||||
@if($isAtasan)
|
||||
@if(!Auth::guard('admin')->check())
|
||||
<li class="sidebar-item">
|
||||
<a class="sidebar-link d-flex align-items-center justify-content-between"
|
||||
href="{{ url('/pending-file') }}" aria-expanded="false">
|
||||
<li class="sidebar-item">
|
||||
<a class="sidebar-link d-flex align-items-center justify-content-between"
|
||||
href="{{ url('/pending-file') }}" aria-expanded="false">
|
||||
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<i class="ti ti-clock"></i>
|
||||
<span class="hide-menu">Persetujuan</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<i class="ti ti-clock"></i>
|
||||
<span class="hide-menu">Persetujuan</span>
|
||||
</div>
|
||||
|
||||
<span class="badge bg-danger rounded-pill d-none" id="pendingCountBadge">0</span>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
<span class="badge bg-danger rounded-pill d-none" id="pendingCountBadge">0</span>
|
||||
</a>
|
||||
</li>
|
||||
@else
|
||||
@if(!Auth::guard('admin')->check())
|
||||
<li class="sidebar-item">
|
||||
<a class="sidebar-link d-flex align-items-center justify-content-between"
|
||||
href="{{ url('/pengajuan-file') }}" aria-expanded="false">
|
||||
<li class="sidebar-item">
|
||||
<a class="sidebar-link d-flex align-items-center justify-content-between"
|
||||
href="{{ url('/pengajuan-file') }}" aria-expanded="false">
|
||||
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<i class="ti ti-clock"></i>
|
||||
<span class="hide-menu">Pengajuan</span>
|
||||
</div>
|
||||
|
||||
@if($isRoleApprover)
|
||||
<span class="badge bg-danger rounded-pill d-none" id="pengajuanPendingCountBadge">0</span>
|
||||
@endif
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<i class="ti ti-clock"></i>
|
||||
<span class="hide-menu">Pengajuan</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
<li class="sidebar-item">
|
||||
<a class="sidebar-link d-flex align-items-center justify-content-between"
|
||||
@ -128,15 +103,7 @@
|
||||
</li> --}}
|
||||
|
||||
{{-- MASTER --}}
|
||||
<li class="nav-small-cap"><span class="hide-menu">History</span></li>
|
||||
<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())
|
||||
@if(auth()->user()->dataUser->mappingUnitKerjaPegawai()->where('objectunitkerjapegawaifk', 43)->exists())
|
||||
<li class="nav-small-cap"><span class="hide-menu">Master</span></li>
|
||||
|
||||
<li class="sidebar-item has-sub {{ $openMaster ? 'open' : '' }}">
|
||||
@ -176,7 +143,6 @@
|
||||
</ul>
|
||||
</li>
|
||||
@endif
|
||||
@endif
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@ -214,11 +180,10 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const pendingBadge = document.getElementById('pendingCountBadge');
|
||||
const pengajuanBadge = document.getElementById('pengajuanPendingCountBadge');
|
||||
const badge = document.getElementById('pendingCountBadge');
|
||||
|
||||
async function countData() {
|
||||
if (!pendingBadge && !pengajuanBadge) return;
|
||||
if (!badge) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/data/count-pending', {
|
||||
@ -230,24 +195,14 @@
|
||||
const data = await res.json();
|
||||
const count = Number(data?.count ?? 0);
|
||||
|
||||
if (pendingBadge) {
|
||||
pendingBadge.textContent = count;
|
||||
pendingBadge.classList.toggle('d-none', count <= 0);
|
||||
}
|
||||
|
||||
if (pengajuanBadge) {
|
||||
pengajuanBadge.textContent = count;
|
||||
pengajuanBadge.classList.toggle('d-none', count <= 0);
|
||||
}
|
||||
badge.textContent = count;
|
||||
badge.classList.toggle('d-none', count <= 0);
|
||||
|
||||
} catch (e) {
|
||||
if (pendingBadge) pendingBadge.classList.add('d-none');
|
||||
if (pengajuanBadge) pengajuanBadge.classList.add('d-none');
|
||||
badge.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
window.refreshPendingCount = countData;
|
||||
|
||||
countData();
|
||||
|
||||
// setInterval(countData, 60000);
|
||||
|
||||
@ -1,69 +1,41 @@
|
||||
{{-- @php
|
||||
$globalUpdateItems = [
|
||||
[
|
||||
'date' => '29 Juni 2026',
|
||||
'title' => 'Metadata preview dokumen diperluas',
|
||||
'description' => 'Modal preview sekarang menampilkan nomor dokumen, tanggal terbit, tanggal kedaluwarsa, tanggal upload, akses dokumen, jenis dokumen akreditasi/non-akreditasi, dan kategori.',
|
||||
],
|
||||
[
|
||||
'date' => '29 Juni 2026',
|
||||
'title' => 'Nama unit ditampilkan lebih jelas',
|
||||
'description' => 'Data persetujuan dan pengajuan file sekarang memakai nama unit langsung dari tabel unit agar lebih mudah dibaca.',
|
||||
],
|
||||
[
|
||||
'date' => '29 Juni 2026',
|
||||
'title' => 'Perbaikan query persetujuan',
|
||||
'description' => 'Query halaman persetujuan dan pengajuan file dirapikan supaya aman saat join tabel dan tidak gagal karena kolom ambigu.',
|
||||
],
|
||||
];
|
||||
@endphp --}}
|
||||
<style>
|
||||
<style>
|
||||
/* ===== NOTIFIKASI STYLE FACEBOOK ===== */
|
||||
|
||||
.message-body {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
/* item notif */
|
||||
.message-body .dropdown-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1px;
|
||||
}
|
||||
.message-body .dropdown-item div {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
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 {
|
||||
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>
|
||||
|
||||
<header class="app-header">
|
||||
@ -76,34 +48,9 @@
|
||||
</li>
|
||||
</ul>
|
||||
<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">
|
||||
{{-- <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> --}}
|
||||
<ul class="navbar-nav flex-row ms-auto align-items-center justify-content-end">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link position-relative" href="javascript:void(0)" id="dropExpired" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="d-inline-flex align-items-center justify-content-center rounded-circle bg-light" style="width:46px;height:46px;">
|
||||
<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">
|
||||
<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-bell text-primary"></i>
|
||||
</span>
|
||||
@ -111,7 +58,7 @@
|
||||
0
|
||||
</span>
|
||||
</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">
|
||||
<span class="fw-semibold">Notifikasi</span>
|
||||
<span class="small text-muted" id="notifCountText">0 baru</span>
|
||||
@ -131,7 +78,7 @@
|
||||
<div class="message-body">
|
||||
<a href="javascript:void(0)" class="d-flex align-items-center gap-2 dropdown-item">
|
||||
<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>
|
||||
<form action="/logout" method="POST">
|
||||
@csrf
|
||||
@ -144,646 +91,68 @@
|
||||
</div>
|
||||
</nav>
|
||||
</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>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const listEl = document.getElementById('notifList');
|
||||
const countTextEl = document.getElementById('notifCountText');
|
||||
const badgeEl = document.getElementById('notifCountBadge');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HELPER
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function ajaxGet(url) {
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
function setList(items) {
|
||||
if (!listEl) return;
|
||||
if (!items.length) {
|
||||
listEl.innerHTML = '<div class="dropdown-item text-muted small">Tidak ada notifikasi</div>';
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ajaxPost(url) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
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
|
||||
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>
|
||||
<div>
|
||||
<div class="fw-semibold">${item.text_notifikasi || '-'}</div>
|
||||
<div class="small text-muted">${item.created_at || ''}</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
</a>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
notifListEl.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;
|
||||
|
||||
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')
|
||||
fetch('/data/notifications')
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
const unread = res?.status ? Number(res.unread || 0) : 0;
|
||||
const items = res?.status ? (res.data || []) : [];
|
||||
const unread = res?.status ? Number(res.unread || 0) : 0;
|
||||
const items = res?.status ? (res.data || []) : [];
|
||||
|
||||
setNotifBadge(unread);
|
||||
setNotifList(items);
|
||||
if (countTextEl) countTextEl.textContent = `${unread} baru`;
|
||||
if (badgeEl) {
|
||||
if (unread > 0) {
|
||||
badgeEl.classList.remove('d-none');
|
||||
badgeEl.textContent = unread;
|
||||
} else {
|
||||
badgeEl.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
setList(items);
|
||||
})
|
||||
.catch(() => {
|
||||
if (notifListEl) {
|
||||
notifListEl.innerHTML = `
|
||||
<div class="dropdown-item text-muted small">
|
||||
Gagal memuat notifikasi
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setNotifBadge(0);
|
||||
if (listEl) listEl.innerHTML = '<div class="dropdown-item text-muted small">Gagal memuat notifikasi</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function markNotificationsAsRead() {
|
||||
ajaxPost('/data/notifications/read')
|
||||
.then(() => {
|
||||
setNotifBadge(0);
|
||||
|
||||
if (notifListEl) {
|
||||
notifListEl.querySelectorAll('.badge').forEach(badge => {
|
||||
badge.classList.remove('bg-primary');
|
||||
badge.classList.add('bg-secondary');
|
||||
});
|
||||
}
|
||||
})
|
||||
.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);
|
||||
const notifToggle = document.getElementById('drop1');
|
||||
if (notifToggle) {
|
||||
notifToggle.addEventListener('show.bs.dropdown', () => {
|
||||
const unread = Number(badgeEl?.textContent || 0);
|
||||
if (unread > 0) {
|
||||
fetch('/data/notifications/read', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
}).then(() => {
|
||||
if (countTextEl) countTextEl.textContent = '0 baru';
|
||||
if (badgeEl) badgeEl.classList.add('d-none');
|
||||
}).catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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>
|
||||
</script>
|
||||
|
||||
@ -139,7 +139,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const paginationBtns = document.getElementById('logPaginationBtns');
|
||||
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(searchInput) searchInput.value = keyword || '';
|
||||
|
||||
@ -160,12 +160,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<td>${((currentPage - 1) * (pagination.per_page || 10)) + idx + 1}</td>
|
||||
<td>${row.pegawai_nama_entry || '-'}</td>
|
||||
<td>${row.total_open || 0}</td>
|
||||
<td>${row.total_download || 0}</td>
|
||||
<td>${formatTanggal(row.last_open)}</td>
|
||||
</tr>
|
||||
`).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(summaryText){
|
||||
@ -256,7 +255,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if(pageData.length === 0){
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted py-4">
|
||||
<td colspan="6" class="text-center text-muted py-4">
|
||||
Tidak ada data
|
||||
</td>
|
||||
</tr>
|
||||
@ -342,7 +341,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<th>#</th>
|
||||
<th>Nama</th>
|
||||
<th>Jumlah Membuka</th>
|
||||
<th>Jumlah Mengunduh</th>
|
||||
<th>Terakhir Dilihat</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
@extends('layout.main')
|
||||
@section('body_main')
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
@ -57,7 +58,7 @@
|
||||
<div class="small text-muted ms-md-auto" id="tableSummary"></div>
|
||||
</div>
|
||||
<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">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -95,7 +96,9 @@
|
||||
<th>Kategori</th>
|
||||
<th>Unit</th>
|
||||
<th>Aksi</th>
|
||||
<th>Tanggal Aksi</th>
|
||||
<th>Tanggal Terbit</th>
|
||||
<th>Tanggal Kedaluwarsa Dokumen</th>
|
||||
<th>Tanggal Upload</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tableHistoryFile">
|
||||
@ -111,9 +114,5 @@
|
||||
</div>
|
||||
</div>
|
||||
@include('pendingFile.modal.view')
|
||||
<script>
|
||||
window.isKomiteMutu = @json($isKomiteMutu);
|
||||
window.isTurt = @json($isTurt);
|
||||
</script>
|
||||
<script src="{{ ver('/js/pendingFile/index.js') }}"></script>
|
||||
@endsection
|
||||
|
||||
@ -13,9 +13,6 @@
|
||||
|
||||
<!-- Body -->
|
||||
<div class="modal-body p-2" style="min-height:250px; max-height:70vh; overflow:auto;">
|
||||
<div class="d-flex justify-content-end align-items-center sticky-top bg-white z-10 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"
|
||||
class="text-center text-muted d-flex justify-content-center align-items-center"
|
||||
style="height:100%;">
|
||||
|
||||
@ -46,26 +46,10 @@
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" type="button" data-mode="pengajuan">Data Pengajuan</button>
|
||||
</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">
|
||||
<button class="nav-link" type="button" data-mode="history">Log History</button>
|
||||
</li>
|
||||
</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="input-group input-group-sm flex-grow-1" style="max-width:320px;">
|
||||
<span class="input-group-text bg-white border-end-0">
|
||||
@ -94,14 +78,6 @@
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</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()">
|
||||
<i class="fa fa-rotate"></i> Refresh
|
||||
</button>
|
||||
@ -112,9 +88,6 @@
|
||||
<table class="table table-sm table-hover align-middle mb-0" id="tablePengajuan">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" class="form-check-input" id="selectAllPengajuan" title="Pilih semua di halaman">
|
||||
</th>
|
||||
<th>Aksi</th>
|
||||
<th>No Dokumen</th>
|
||||
<th>Status</th>
|
||||
@ -122,6 +95,8 @@
|
||||
<th>Nama</th>
|
||||
<th>Kategori</th>
|
||||
<th>Unit</th>
|
||||
<th>Tanggal Terbit</th>
|
||||
<th>Tanggal Kedaluwarsa Dokumen</th>
|
||||
<th>Tanggal Upload</th>
|
||||
</tr>
|
||||
</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>
|
||||
<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">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -143,7 +118,9 @@
|
||||
<th>Kategori</th>
|
||||
<th>Unit</th>
|
||||
<th>Aksi</th>
|
||||
<th>Tanggal Aksi</th>
|
||||
<th>Tanggal Terbit</th>
|
||||
<th>Tanggal Kedaluwarsa Dokumen</th>
|
||||
<th>Tanggal Upload</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tableHistoryFile">
|
||||
@ -163,9 +140,6 @@
|
||||
@include('dataUnit.modal.create')
|
||||
<script>
|
||||
window.katDok = @json($katDok);
|
||||
window.isKomiteMutu = @json($isKomiteMutu);
|
||||
window.isTurt = @json($isTurt);
|
||||
window.isAtasan = @json($isAtasan);
|
||||
</script>
|
||||
<script src="{{ ver('/js/pengajuanFile/index.js') }}"></script>
|
||||
@endsection
|
||||
|
||||
@ -18,18 +18,27 @@
|
||||
<div class="small text-muted">Perbarui detail dokumen sebelum mengirim ulang.</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>
|
||||
<select class="form-control unit_kerja" name="id_unit_kerja" id="edit_id_unit_kerja" required>
|
||||
<option value="" disabled selected>Pilih Unit</option>
|
||||
</select>
|
||||
</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>
|
||||
<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>
|
||||
</select>
|
||||
</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">
|
||||
<label class="form-label fw-semibold">Nomor Dokumen</label>
|
||||
@ -46,35 +55,22 @@
|
||||
<label class="form-label fw-semibold">Tanggal Terbit</label>
|
||||
<input class="form-control" type="date" name="tanggal_terbit" id="edit_tanggal_terbit">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Masa Berlaku Dokumen</label>
|
||||
<select class="form-select masa-berlaku-select"
|
||||
name="masa_berlaku_option"
|
||||
id="edit_masa_berlaku_option"
|
||||
data-index="edit"
|
||||
data-date-target="edit_expired_field"
|
||||
data-input-target="edit_tgl_expired"
|
||||
data-base-target="edit_tanggal_terbit"
|
||||
data-preview-target="edit_expired_preview">
|
||||
<option value="">Selamanya</option>
|
||||
<option value="1">1 Tahun</option>
|
||||
<option value="2">2 Tahun</option>
|
||||
<option value="3">3 Tahun</option>
|
||||
<option value="custom">Lainnya</option>
|
||||
</select>
|
||||
<div class="form-text text-muted">Opsi 1, 2, dan 3 tahun dihitung dari Tanggal Terbit.</div>
|
||||
<div class="form-text text-primary" id="edit_expired_preview"></div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="edit_has_expired" data-target="edit_expired_field">
|
||||
<label class="form-check-label" for="edit_has_expired">Ada Expired?</label>
|
||||
</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>
|
||||
<input class="form-control" type="date" name="tgl_expired" id="edit_tgl_expired" disabled>
|
||||
</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>
|
||||
<div class="border rounded-3 p-2 bg-light">
|
||||
<div class="form-check">
|
||||
<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 class="form-check mt-1">
|
||||
<input class="form-check-input" type="radio" name="permission_file" id="edit_perm_no" value="0" required>
|
||||
@ -83,42 +79,6 @@
|
||||
</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">
|
||||
<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">
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\AksesFileController;
|
||||
use App\Http\Controllers\AkreditasiInstrumenController;
|
||||
use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\DashboardController;
|
||||
use App\Http\Controllers\MasterKategoriController;
|
||||
@ -10,7 +9,7 @@ use App\Http\Controllers\LogActivityController;
|
||||
use App\Http\Controllers\masterPersetujuanController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware(['auth:admin,web'])->group(function(){
|
||||
Route::middleware(['auth'])->group(function(){
|
||||
|
||||
Route::get('/', [DashboardController::class, 'index']);
|
||||
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('/data-akreditasi', [DashboardController::class, 'dataAkreditasi']);
|
||||
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::post('/uploadv2', [DashboardController::class, 'storeVersion2']);
|
||||
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::delete('/delete-file/bulk', [DashboardController::class, 'deleteFiles']);
|
||||
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::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('/datatable/log-activity', [LogActivityController::class, 'datatable']);
|
||||
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('/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('/datatable/pengajuan-file', [DashboardController::class, 'dataPengajuanFile']);
|
||||
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::get('/pending-file', [DashboardController::class, 'pendingFile']);
|
||||
Route::get('/datatable/pending-file', [DashboardController::class, 'dataPendingFile']);
|
||||
Route::post('/pending-file/{id}/approve', [DashboardController::class, 'approvePendingFile']);
|
||||
Route::post('/pending-file/approve-multiple', [DashboardController::class, 'approvePendingFileMultiple']);
|
||||
Route::post('/pending-file/{id}/reject', [DashboardController::class, 'rejectPendingFile']);
|
||||
Route::post('/pending-file/{id}/approve-mutu', [DashboardController::class, 'approvePendingFileMutu']);
|
||||
Route::post('/pending-file/{id}/approve-turt', [DashboardController::class, 'approvePendingFileTurt']);
|
||||
Route::post('/pending-file/{id}/reject-mutu', [DashboardController::class, 'rejectPendingFileMutu']);
|
||||
Route::post('/pending-file/{id}/reject-turt', [DashboardController::class, 'rejectPendingFileTurt']);
|
||||
Route::get('/data/count-pending', [DashboardController::class, 'countDataPending']);
|
||||
Route::get('/data/count-rejected', [DashboardController::class, 'countRejectedPengajuan']);
|
||||
Route::get('/pending-file', [DashboardController::class, 'pendingFile']);
|
||||
Route::get('/datatable/pending-file', [DashboardController::class, 'dataPendingFile']);
|
||||
Route::post('/pending-file/{id}/approve', [DashboardController::class, 'approvePendingFile']);
|
||||
Route::post('/pending-file/approve-multiple', [DashboardController::class, 'approvePendingFileMultiple']);
|
||||
Route::post('/pending-file/{id}/reject', [DashboardController::class, 'rejectPendingFile']);
|
||||
Route::get('/data/count-pending', [DashboardController::class, 'countDataPending']);
|
||||
Route::get('/data/count-rejected', [DashboardController::class, 'countRejectedPengajuan']);
|
||||
// });
|
||||
Route::get('/data/notifications', [DashboardController::class, 'notifkasiList']);
|
||||
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('/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('/captcha/login', [AuthController::class, 'captcha'])->name('captcha.login');
|
||||
Route::post('/login', [AuthController::class, 'login']);
|
||||
Route::post('/logout', [AuthController::class, 'logout']);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user