Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e3b7ad4f12 | |||
| e45deae90e | |||
| b27e8040eb | |||
| f6031e32fe | |||
| 5786369973 | |||
| 8c0aecd4aa | |||
| 1b07699476 | |||
| a0b32672b4 | |||
| 99a34e8e59 | |||
| 3be01956b3 | |||
| 444426c8e5 | |||
| a48ac75f86 | |||
| e08a0b4622 | |||
| cce18f65a1 | |||
| de2c087d93 | |||
| f19a414f22 | |||
| ba857bea05 | |||
| ac86078d96 | |||
| 6fa55f083b | |||
| cf427020d9 | |||
| ce8848a2b8 | |||
| 2ded2e8ae8 | |||
| 7e01ccbb90 | |||
| 7dd11a23c7 | |||
| 96289f8a55 | |||
| 63cca475d9 | |||
| 641617a8de |
316
app/Http/Controllers/AkreditasiInstrumenController.php
Normal file
316
app/Http/Controllers/AkreditasiInstrumenController.php
Normal file
@ -0,0 +1,316 @@
|
||||
<?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,30 +5,195 @@ 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 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 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)
|
||||
{
|
||||
$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,6 +7,7 @@ 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
|
||||
{
|
||||
@ -22,9 +23,13 @@ class LogActivityController extends Controller
|
||||
$keyword = request('keyword');
|
||||
$start = request('start_date');
|
||||
$end = request('end_date');
|
||||
$mapping = MappingUnitKerjaPegawai::where('statusenabled', true)
|
||||
->where('objectpegawaifk', auth()->user()->dataUser->id)
|
||||
->get(['objectunitkerjapegawaifk', 'objectsubunitkerjapegawaifk']);
|
||||
$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']);
|
||||
$unitIds = $mapping->pluck('objectunitkerjapegawaifk')
|
||||
->filter() // buang null
|
||||
->unique()
|
||||
@ -36,9 +41,15 @@ class LogActivityController extends Controller
|
||||
$q->select(DB::raw('COUNT(DISTINCT pegawai_id_entry)'));
|
||||
}])
|
||||
->where('statusenabled', true)
|
||||
->where('status_action', 'approved')
|
||||
->whereIn('id_unit_kerja', $unitIds)
|
||||
->orderBy('entry_at','desc');
|
||||
->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');
|
||||
|
||||
if($keyword){
|
||||
$query->where(function($q) use ($keyword){
|
||||
@ -93,12 +104,14 @@ class LogActivityController extends Controller
|
||||
$query = LogActivity::select(
|
||||
'pegawai_id_entry',
|
||||
'pegawai_nama_entry',
|
||||
DB::raw('COUNT(*) as total_open'),
|
||||
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('MAX(entry_at) as last_open')
|
||||
)
|
||||
->where('file_directory_id', $fileDirectoryId)
|
||||
->where('statusenabled', true)
|
||||
->where('action_type', 'Membuka Dokumen')
|
||||
->whereIn('action_type', ['Membuka Dokumen', 'Download Dokumen'])
|
||||
->groupBy('pegawai_id_entry', 'pegawai_nama_entry')
|
||||
->orderByDesc('total_open');
|
||||
|
||||
|
||||
@ -4,6 +4,8 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\LogActivity;
|
||||
use App\Models\UnitKerja;
|
||||
use App\Models\MasterKategori;
|
||||
|
||||
class FileDirectory extends Model
|
||||
{
|
||||
@ -12,6 +14,7 @@ class FileDirectory extends Model
|
||||
public $timestamps = false;
|
||||
protected $primaryKey = 'file_directory_id';
|
||||
protected $guarded = ['file_directory_id'];
|
||||
protected $with = ['unit'];
|
||||
|
||||
public function viewLogs()
|
||||
{
|
||||
@ -26,4 +29,16 @@ 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');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
15
app/Models/UserAdmin.php
Normal file
15
app/Models/UserAdmin.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?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'];
|
||||
}
|
||||
@ -40,6 +40,10 @@ return [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
'admin' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'admins',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
@ -64,6 +68,10 @@ 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,6 +129,26 @@ 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' ),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
@ -177,7 +177,7 @@ function addForm(){
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Unit <span class="text-danger">*</span></label>
|
||||
<select class="form-control"
|
||||
name="data[${colCount}][id_unit_kerja]"
|
||||
@ -186,7 +186,7 @@ function addForm(){
|
||||
<option value="" disable>Select Choose</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Sub Unit <span class="text-danger">*</span></label>
|
||||
<select class="form-control"
|
||||
name="data[${colCount}][id_sub_unit_kerja]"
|
||||
@ -196,7 +196,7 @@ function addForm(){
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Kategori Dokumen <span class="text-danger">*</span></label>
|
||||
<label class="form-label fw-semibold">Kategori Dokumen</label>
|
||||
<select class="form-control"
|
||||
name="data[${colCount}][master_kategori_directory_id]"
|
||||
id="select_kategori_${colCount}"
|
||||
@ -238,7 +238,7 @@ function addForm(){
|
||||
id="perm_yes_${colCount}"
|
||||
value="1"
|
||||
required>
|
||||
<label class="form-check-label" for="perm_yes_${colCount}">Iya</label>
|
||||
<label class="form-check-label" for="perm_yes_${colCount}">Ya</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-1">
|
||||
|
||||
@ -631,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 === 'iya' || val === 'yes';
|
||||
return val === '1' || val === 'true' || val === 'ya' || val === 'yes';
|
||||
}
|
||||
|
||||
@ -116,8 +116,13 @@ 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>${item.folder || '-'}</td>
|
||||
<td>${item.part || '-'}</td>
|
||||
${item.is_akre ? `<td colspan="2" class="text-center">Dokumen akreditasi</td>` :
|
||||
`
|
||||
<td>${item.name_kategori || '-'}</td>
|
||||
<td>${item.name_unit || '-'}</td>
|
||||
`
|
||||
}
|
||||
|
||||
<td class="text-nowrap">${tanggalTerbit}</td>
|
||||
<td class="text-nowrap">${tanggalExp}</td>
|
||||
<td class="text-nowrap">${tanggal}</td>
|
||||
@ -363,6 +368,51 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
}
|
||||
|
||||
function byId(id){
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function setInputValue(el, value){
|
||||
if (!el) return;
|
||||
el.value = value ?? '';
|
||||
}
|
||||
|
||||
function setTextValue(el, value){
|
||||
if (!el) return;
|
||||
el.textContent = value || '';
|
||||
}
|
||||
|
||||
function setChecked(el, value){
|
||||
if (!el) return;
|
||||
el.checked = !!value;
|
||||
}
|
||||
|
||||
function setSelectValue(selectEl, value, label){
|
||||
if (!selectEl) return;
|
||||
if (value === undefined || value === null || value === '') {
|
||||
selectEl.value = '';
|
||||
return;
|
||||
}
|
||||
const exists = Array.from(selectEl.options).some(opt => opt.value === value);
|
||||
if (!exists) {
|
||||
selectEl.append(new Option(label || value, value, true, true));
|
||||
}
|
||||
selectEl.value = value;
|
||||
}
|
||||
|
||||
function triggerSelect2(selectEl){
|
||||
if (window.$ && $.fn.select2) $(selectEl).trigger('change');
|
||||
}
|
||||
|
||||
function arrayFilterEmpty(arr){
|
||||
return (arr || []).filter((val) => val !== null && val !== undefined && String(val).trim() !== '');
|
||||
}
|
||||
|
||||
function initSelect2($el, options){
|
||||
if (!$el || !$el.length || !window.$ || !$.fn.select2) return;
|
||||
$el.select2(options);
|
||||
}
|
||||
|
||||
function initEditSelects(){
|
||||
if (editUnitSelect.length) {
|
||||
editUnitSelect.select2({
|
||||
@ -380,7 +430,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
processResults: function (data) {
|
||||
return {
|
||||
results: (data?.data || []).map(item => ({
|
||||
id: `${item.id}/${item.name}`,
|
||||
id: String(item.id),
|
||||
text: item.name
|
||||
}))
|
||||
};
|
||||
@ -403,8 +453,38 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
editUnitSelect.on('change', function(){
|
||||
const val = $(this).val();
|
||||
if (!val) return;
|
||||
const unitId = String(val).split('/')[0];
|
||||
loadEditSubUnit(unitId, null, null);
|
||||
loadEditSubUnit(String(val), null, null);
|
||||
});
|
||||
}
|
||||
|
||||
let akreData = [];
|
||||
let akreLoaded = false;
|
||||
let akreFlat = [];
|
||||
|
||||
function initEditExtraSelects(){
|
||||
const akreSelect = byId('edit_akre_select');
|
||||
if (akreSelect) {
|
||||
loadAkreData().then(() => {
|
||||
fillAkreSelect(akreSelect);
|
||||
initSelect2($(akreSelect), {
|
||||
dropdownParent: $('#modalEditPengajuanFile'),
|
||||
placeholder: 'Pilih Instrumen',
|
||||
allowClear: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const editKat = $('#edit_kategori');
|
||||
const editHukum = $('#edit_kategori_hukum');
|
||||
initSelect2(editKat, {
|
||||
dropdownParent: $('#modalEditPengajuanFile'),
|
||||
placeholder: 'Pilih Kategori',
|
||||
allowClear: true
|
||||
});
|
||||
initSelect2(editHukum, {
|
||||
dropdownParent: $('#modalEditPengajuanFile'),
|
||||
placeholder: 'Pilih Kategori Hukum',
|
||||
allowClear: true
|
||||
});
|
||||
}
|
||||
|
||||
@ -417,13 +497,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
success: function(response) {
|
||||
if (response?.data) {
|
||||
response.data.forEach(unit => {
|
||||
const optVal = `${unit.id}/${unit.name}`;
|
||||
const optVal = String(unit.id);
|
||||
const isSelected = selectedSubId && String(unit.id) === String(selectedSubId);
|
||||
const option = new Option(unit.name, optVal, false, isSelected);
|
||||
editSubUnitSelect.append(option);
|
||||
});
|
||||
if (selectedSubId && selectedSubName && editSubUnitSelect.find(`option[value="${selectedSubId}/${selectedSubName}"]`).length === 0) {
|
||||
editSubUnitSelect.append(new Option(selectedSubName, `${selectedSubId}/${selectedSubName}`, true, true));
|
||||
if (selectedSubId && selectedSubName && editSubUnitSelect.find(`option[value="${selectedSubId}"]`).length === 0) {
|
||||
editSubUnitSelect.append(new Option(selectedSubName, String(selectedSubId), true, true));
|
||||
}
|
||||
editSubUnitSelect.trigger('change');
|
||||
}
|
||||
@ -431,70 +511,86 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
}
|
||||
|
||||
function setEditAkreValue(value){
|
||||
const akreEl = byId('edit_akre_select');
|
||||
if (!akreEl) return;
|
||||
loadAkreData().then(() => {
|
||||
fillAkreSelect(akreEl);
|
||||
if (value && akreValueExists(value)) {
|
||||
setSelectValue(akreEl, value, value);
|
||||
} else {
|
||||
akreEl.value = '';
|
||||
}
|
||||
triggerSelect2(akreEl);
|
||||
});
|
||||
}
|
||||
|
||||
function setEditKategoriHukum(value){
|
||||
const hukumEl = byId('edit_kategori_hukum');
|
||||
if (!hukumEl) return;
|
||||
setSelectValue(hukumEl, value, value);
|
||||
triggerSelect2(hukumEl);
|
||||
}
|
||||
|
||||
function setEditKategoriDir(item, kategoriName){
|
||||
const katEl = byId('edit_kategori');
|
||||
const katId = item?.master_kategori_directory_id;
|
||||
if (!katEl || !katId) return;
|
||||
const match = Array.from(katEl.options).find((opt) => String(opt.value).startsWith(`${katId}/`));
|
||||
if (match) {
|
||||
katEl.value = match.value;
|
||||
} else {
|
||||
const label = kategoriName || 'Kategori';
|
||||
const katVal = `${katId}/${label}`;
|
||||
setSelectValue(katEl, katVal, label);
|
||||
}
|
||||
triggerSelect2(katEl);
|
||||
}
|
||||
|
||||
window.editFileReject = function(id){
|
||||
const item = getItemById(id);
|
||||
if (!item) {
|
||||
Swal.fire({ icon: 'error', title: 'Gagal', text: 'Data tidak ditemukan.' });
|
||||
return;
|
||||
}
|
||||
const idEl = document.getElementById('edit_file_directory_id');
|
||||
const noEl = document.getElementById('edit_no_dokumen');
|
||||
const namaEl = document.getElementById('edit_nama_dokumen');
|
||||
const tglEl = document.getElementById('edit_tanggal_terbit');
|
||||
const tglExpiredEl = document.getElementById('edit_tgl_expired');
|
||||
const hasExpiredEl = document.getElementById('edit_has_expired');
|
||||
const currentFileEl = document.getElementById('edit_current_file');
|
||||
const permYes = document.getElementById('edit_perm_yes');
|
||||
const permNo = document.getElementById('edit_perm_no');
|
||||
const katEl = document.getElementById('edit_kategori');
|
||||
setInputValue(byId('edit_file_directory_id'), item.file_directory_id);
|
||||
setInputValue(byId('edit_no_dokumen'), item.no_dokumen);
|
||||
setInputValue(byId('edit_nama_dokumen'), item.nama_dokumen);
|
||||
setInputValue(byId('edit_tanggal_terbit'), item.tanggal_terbit);
|
||||
setInputValue(byId('edit_tgl_expired'), item.tgl_expired);
|
||||
|
||||
if (idEl) idEl.value = item.file_directory_id || '';
|
||||
if (noEl) noEl.value = item.no_dokumen || '';
|
||||
if (namaEl) namaEl.value = item.nama_dokumen || '';
|
||||
if (tglEl) tglEl.value = item.tanggal_terbit || '';
|
||||
if (tglExpiredEl) tglExpiredEl.value = item.tgl_expired || '';
|
||||
if (permYes && permNo) {
|
||||
const isPublic = item.permission_file === true || item.permission_file === 1 || item.permission_file === '1';
|
||||
permYes.checked = isPublic;
|
||||
permNo.checked = !isPublic;
|
||||
}
|
||||
if (hasExpiredEl && tglExpiredEl) {
|
||||
setChecked(byId('edit_perm_yes'), isPublic);
|
||||
setChecked(byId('edit_perm_no'), !isPublic);
|
||||
|
||||
const hasExpired = !!item.tgl_expired;
|
||||
hasExpiredEl.checked = hasExpired;
|
||||
setChecked(byId('edit_has_expired'), hasExpired);
|
||||
syncEditExpiredField();
|
||||
}
|
||||
if (currentFileEl) {
|
||||
|
||||
const displayName = item.fileName || (item.file ? String(item.file).split('/').pop() : '');
|
||||
currentFileEl.textContent = displayName ? `File saat ini: ${displayName}` : '';
|
||||
}
|
||||
setTextValue(byId('edit_current_file'), displayName ? `File saat ini: ${displayName}` : '');
|
||||
|
||||
const parts = (item.file || '').split('/');
|
||||
const unitName = parts[0] || '';
|
||||
const subName = parts[1] || '';
|
||||
const parts = arrayFilterEmpty((item.file || '').split('/'));
|
||||
const unitNameFromPath = parts[0] || '';
|
||||
const subNameFromPath = parts[1] || '';
|
||||
const kategoriName = parts[2] || '';
|
||||
|
||||
const unitName = item.unit_kerja_name || item.name_unit || item.nama_unit_kerja || item.unit_name || item.unit_kerja || '';
|
||||
if (editUnitSelect.length && item.id_unit_kerja) {
|
||||
const unitVal = `${item.id_unit_kerja}/${unitName}`;
|
||||
editUnitSelect.append(new Option(unitName || 'Unit', unitVal, true, true)).trigger('change');
|
||||
const unitId = String(item.id_unit_kerja);
|
||||
loadEditSubUnit(unitId, item.id_sub_unit_kerja, subName);
|
||||
const unitLabel = unitName || String(item.id_unit_kerja);
|
||||
const unitVal = String(item.id_unit_kerja);
|
||||
setSelectValue(editUnitSelect[0], unitVal, unitLabel);
|
||||
triggerSelect2(editUnitSelect[0]);
|
||||
const subLabel = item.sub_unit_kerja_name || item.nama_sub_unit_kerja || item.sub_unit_name || item.sub_unit_kerja || String(item.id_sub_unit_kerja || '');
|
||||
loadEditSubUnit(String(item.id_unit_kerja), item.id_sub_unit_kerja, subLabel || null);
|
||||
}
|
||||
|
||||
if (katEl && item.master_kategori_directory_id) {
|
||||
const katVal = `${item.master_kategori_directory_id}/${kategoriName}`;
|
||||
if (katEl.querySelector(`option[value="${katVal}"]`)) {
|
||||
katEl.value = katVal;
|
||||
} else {
|
||||
katEl.append(new Option(kategoriName || 'Kategori', katVal, true, true));
|
||||
katEl.value = katVal;
|
||||
}
|
||||
}
|
||||
setEditKategoriDir(item, kategoriName);
|
||||
if (item.kategori_hukum) setEditKategoriHukum(item.kategori_hukum);
|
||||
const akreFromFile = parts.length > 1 ? parts.slice(0, -1).join('/') : '';
|
||||
setEditAkreValue(item.akre || akreFromFile || '');
|
||||
|
||||
const modalEl = document.getElementById('modalEditPengajuanFile');
|
||||
if (modalEl) {
|
||||
$("#modalEditPengajuanFile").modal('show');
|
||||
}
|
||||
}
|
||||
|
||||
function syncEditExpiredField() {
|
||||
const editHasExpired = document.getElementById('edit_has_expired');
|
||||
@ -552,6 +648,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
initEditSelects();
|
||||
initEditExtraSelects();
|
||||
updateTabUI();
|
||||
fetchData();
|
||||
|
||||
@ -575,6 +672,124 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
$(formCreate).find('input[type="file"]').val('');
|
||||
$(formCreate).find('.file-name').addClass('d-none').text('');
|
||||
}
|
||||
resetAkreFields(0);
|
||||
enableAkreFields(0);
|
||||
}
|
||||
|
||||
function loadAkreData(){
|
||||
if(akreLoaded) return Promise.resolve(akreData);
|
||||
return fetch('/json/akreditasi.jff')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
akreData = Array.isArray(data) ? data : [];
|
||||
akreFlat = [];
|
||||
akreLoaded = true;
|
||||
return akreData;
|
||||
})
|
||||
.catch(() => {
|
||||
akreData = [];
|
||||
akreFlat = [];
|
||||
akreLoaded = true;
|
||||
return akreData;
|
||||
});
|
||||
}
|
||||
|
||||
function getAkreFlat(){
|
||||
if(akreFlat.length) return akreFlat;
|
||||
akreFlat = (akreData || []).flatMap(type => {
|
||||
const segments = Array.isArray(type.segment) ? type.segment : [];
|
||||
return segments.flatMap(seg => {
|
||||
const children = Array.isArray(seg.turunan) ? seg.turunan : [];
|
||||
return children.map(child => ({
|
||||
value: `${type.name}/${seg.name}/${child.name}`,
|
||||
label: `${type.name} / ${child.name}`,
|
||||
type: type.name,
|
||||
segment: seg.name,
|
||||
item: child.name
|
||||
}));
|
||||
});
|
||||
});
|
||||
return akreFlat;
|
||||
}
|
||||
|
||||
function akreValueExists(value){
|
||||
if (!value) return false;
|
||||
return getAkreFlat().some(opt => String(opt.value) === String(value));
|
||||
}
|
||||
|
||||
function fillAkreSelect(selectEl){
|
||||
if(!selectEl) return;
|
||||
selectEl.innerHTML = '<option value="">Pilih Instrumen</option>';
|
||||
getAkreFlat().forEach(optData => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = optData.value;
|
||||
opt.textContent = optData.label;
|
||||
selectEl.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function setKategoriRequired(index, isRequired){
|
||||
const katSelect = document.getElementById(`select_kategori_${index}`);
|
||||
if (!katSelect) return;
|
||||
if (isRequired) {
|
||||
katSelect.setAttribute('required', 'required');
|
||||
} else {
|
||||
katSelect.removeAttribute('required');
|
||||
}
|
||||
}
|
||||
|
||||
function resetAkreFields(index){
|
||||
const selectEl = document.getElementById(`akre_select_${index}`);
|
||||
const typeInput = document.getElementById(`akre_type_${index}`);
|
||||
const segmentInput = document.getElementById(`akre_segment_${index}`);
|
||||
const itemInput = document.getElementById(`akre_item_${index}`);
|
||||
if(selectEl){
|
||||
selectEl.value = '';
|
||||
if(window.$ && $.fn.select2) $(selectEl).val(null).trigger('change');
|
||||
}
|
||||
if(typeInput) typeInput.value = '';
|
||||
if(segmentInput) segmentInput.value = '';
|
||||
if(itemInput) itemInput.value = '';
|
||||
setKategoriRequired(index, false);
|
||||
}
|
||||
|
||||
function enableAkreFields(index){
|
||||
const selectEl = document.getElementById(`akre_select_${index}`);
|
||||
// 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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initKategoriSelect2(index){
|
||||
if(!window.$ || !$.fn.select2) return;
|
||||
const katSelect = $(`#select_kategori_${index}`);
|
||||
const hukumSelect = $(`#select_kategori_hukum_${index}`);
|
||||
if(katSelect.length){
|
||||
katSelect.select2({
|
||||
dropdownParent: $('#modalCreateFile'),
|
||||
placeholder:'Pilih Kategori',
|
||||
allowClear:true
|
||||
});
|
||||
}
|
||||
if(hukumSelect.length){
|
||||
hukumSelect.select2({
|
||||
dropdownParent: $('#modalCreateFile'),
|
||||
placeholder:'Pilih Kategori Hukum',
|
||||
allowClear:true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function selectOptionUnitKerjaV1(localCol){
|
||||
@ -642,7 +857,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Unit <span class="text-danger">*</span></label>
|
||||
<select class="form-select"
|
||||
name="data[${colCount}][id_unit_kerja]"
|
||||
@ -652,7 +867,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Sub Unit <span class="text-danger">*</span></label>
|
||||
<select class="form-select"
|
||||
name="data[${colCount}][id_sub_unit_kerja]"
|
||||
@ -662,16 +877,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Kategori Dokumen <span class="text-danger">*</span></label>
|
||||
<select class="form-select"
|
||||
name="data[${colCount}][master_kategori_directory_id]"
|
||||
id="select_kategori_${colCount}"
|
||||
required>
|
||||
<option value="" disabled selected>Pilih Kategori</option>
|
||||
${katOptions}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Nomor Dokumen</label>
|
||||
@ -697,7 +903,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
type="date"
|
||||
name="data[${colCount}][date_active]">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input toggle-expired"
|
||||
type="checkbox"
|
||||
@ -714,7 +920,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
name="data[${colCount}][tgl_expired]" disabled>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
<div class="col-md-4">
|
||||
<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">
|
||||
@ -724,7 +930,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
id="perm_yes_${colCount}"
|
||||
value="1"
|
||||
required>
|
||||
<label class="form-check-label" for="perm_yes_${colCount}">Iya</label>
|
||||
<label class="form-check-label" for="perm_yes_${colCount}">Ya</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-1">
|
||||
@ -739,6 +945,40 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
</div>
|
||||
</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;">
|
||||
<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="data[${colCount}][kategori_hukum]" id="select_kategori_hukum_${colCount}" style="width: 350px;">
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Kategori Lainnya</label>
|
||||
<select class="form-select"
|
||||
name="data[${colCount}][master_kategori_directory_id]"
|
||||
id="select_kategori_${colCount}" style="width: 350px;">
|
||||
<option value="">Pilih Kategori</option>
|
||||
${katOptions}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label for="fileUpload_${colCount}" class="form-label fw-semibold">📂 Upload Dokumen (PDF)</label>
|
||||
<div class="border rounded-3 p-3 bg-white shadow-sm">
|
||||
@ -754,6 +994,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
</div>`;
|
||||
col.insertAdjacentHTML('beforeend', html);
|
||||
selectOptionUnitKerjaV1(colCount);
|
||||
initKategoriSelect2(colCount);
|
||||
enableAkreFields(colCount);
|
||||
setKategoriRequired(colCount, false);
|
||||
colCount++;
|
||||
}
|
||||
|
||||
@ -765,6 +1008,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (formCreate) {
|
||||
const select0 = $('#select_id_unit_kerja_0');
|
||||
if (select0.length) selectOptionUnitKerjaV1(0);
|
||||
initKategoriSelect2(0);
|
||||
enableAkreFields(0);
|
||||
|
||||
formCreate.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
@ -816,6 +1061,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('change', function(e){
|
||||
if(e.target.classList.contains('akre-select')){
|
||||
const id = e.target.id || '';
|
||||
const idx = id.split('_').pop();
|
||||
const [typeVal = '', segmentVal = '', itemVal = ''] = (e.target.value || '').split('/');
|
||||
const typeInput = document.getElementById(`akre_type_${idx}`);
|
||||
const segmentInput = document.getElementById(`akre_segment_${idx}`);
|
||||
const itemInput = document.getElementById(`akre_item_${idx}`);
|
||||
if(typeInput) typeInput.value = typeVal;
|
||||
if(segmentInput) segmentInput.value = segmentVal;
|
||||
if(itemInput) itemInput.value = itemVal;
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
document.addEventListener('click', function(e){
|
||||
if(e.target.matches('.file-link')){
|
||||
@ -863,5 +1123,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 === 'iya' || val === 'yes';
|
||||
return val === '1' || val === 'true' || val === 'ya' || val === 'yes';
|
||||
}
|
||||
|
||||
3181
public/json/akreditasi.jff
Normal file
3181
public/json/akreditasi.jff
Normal file
File diff suppressed because it is too large
Load Diff
3181
public/json/akreditasi.jff.bak
Normal file
3181
public/json/akreditasi.jff.bak
Normal file
File diff suppressed because it is too large
Load Diff
0
public/json/akreditasi.jff.lock
Normal file
0
public/json/akreditasi.jff.lock
Normal file
@ -28,9 +28,22 @@
|
||||
@csrf
|
||||
@if (session()->has('alertError'))
|
||||
<div class="alert alert-danger fw-bold" role="alert">
|
||||
@if(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" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" required>
|
||||
@ -39,6 +52,20 @@
|
||||
<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</a>
|
||||
</form>
|
||||
|
||||
@ -154,7 +154,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@include('dashboard.modal.create')
|
||||
@include('dashboard.modal.view')
|
||||
<script>
|
||||
const klasifikasiDok = @json($klasifikasiDok);
|
||||
@ -166,7 +165,7 @@
|
||||
function isPublic(permissionVal){
|
||||
if(permissionVal === null || permissionVal === undefined) return false;
|
||||
const val = String(permissionVal).toLowerCase();
|
||||
return val === '1' || val === 'true' || val === 'iya' || val === 'yes';
|
||||
return val === '1' || val === 'true' || val === 'ya' || val === 'yes';
|
||||
}
|
||||
|
||||
let currentFile = null;
|
||||
|
||||
@ -1,96 +0,0 @@
|
||||
<div class="modal fade" id="modalCreateFile" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5">Aksi </h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Form -->
|
||||
<form id="formFile" action="/uploadv2" enctype="multipart/form-data" method="POST" >
|
||||
@csrf
|
||||
<div class="modal-body">
|
||||
<div class="container" style="max-height: 70vh; overflow-y:auto;">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Unit <span class="text-danger">*</span></label>
|
||||
<select class="form-control unit_kerja" name="data[0][id_unit_kerja]" id="select_id_unit_kerja_0" required>
|
||||
<option value="" disable>Select Choose</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Sub Unit <span class="text-danger">*</span></label>
|
||||
<select class="form-control" name="data[0][id_sub_unit_kerja]" id="select_id_sub_unit_kerja_0" required>
|
||||
<option value="" disable selected>Select Choose</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Kategori Dokumen <span class="text-danger">*</span></label>
|
||||
<select class="form-control" name="data[0][master_kategori_directory_id]" id="select_kategori_0" required>
|
||||
<option value="" disable>Select Choose</option>
|
||||
@foreach ($katDok as $kat)
|
||||
<option value="{{ $kat->master_kategori_directory_id }}/{{ $kat->nama_kategori_directory }}">{{ $kat->nama_kategori_directory }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Nomor Dokumen</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">#</span>
|
||||
<input type="text" class="form-control" name="data[0][no_dokumen]" placeholder="Contoh: 001/RS/IT/I/2026">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-semibold">Tanggal Terbit</label>
|
||||
<input class="form-control" type="date" name="data[0][date_active]">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-semibold">Boleh dilihat unit lain? <span class="text-danger">*</span></label>
|
||||
|
||||
<div class="border rounded-3 p-2 bg-light">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="data[0][is_permission]" id="perm_yes" value="1" required>
|
||||
<label class="form-check-label" for="perm_yes">
|
||||
Iya
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-1">
|
||||
<input class="form-check-input" type="radio" name="data[0][is_permission]" id="perm_no" value="2" required>
|
||||
<label class="form-check-label" for="perm_no">
|
||||
Tidak
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mb-2">
|
||||
<label for="fileUpload0" class="form-label fw-semibold">📂 Upload Dokumen (PDF)</label>
|
||||
<div class="border rounded-3 p-3 bg-white shadow-sm">
|
||||
<input class="form-control file-input" type="file" id="fileUpload0" accept=".pdf" name="data[0][file]">
|
||||
<div class="mt-2 text-success fw-semibold d-none file-name"></div>
|
||||
</div>
|
||||
<div class="form-text text-muted">
|
||||
Format yang didukung: <b>PDF</b> Maksimal <b>10mb</b>.
|
||||
</div>
|
||||
</div>
|
||||
<div id="col_add_file" class="col-12"></div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm mt-3" onclick="addForm()">
|
||||
+ Tambah Form
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Tutup</button>
|
||||
<button type="submit" class="btn btn-primary">Simpan</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
1234
resources/views/dataAkreditasi/index.blade.php
Normal file
1234
resources/views/dataAkreditasi/index.blade.php
Normal file
File diff suppressed because it is too large
Load Diff
@ -35,6 +35,37 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/* --- 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')
|
||||
@ -88,12 +119,11 @@
|
||||
<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;" multiple>
|
||||
@foreach ($katDok as $kat)
|
||||
<option value="{{ $kat->master_kategori_directory_id }}">
|
||||
{{ $kat->nama_kategori_directory }}
|
||||
</option>
|
||||
@endforeach
|
||||
<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>
|
||||
<input type="search"
|
||||
id="tableSearch"
|
||||
@ -106,7 +136,7 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive" style="max-height: 70vh; overflow-y:auto;">
|
||||
<div class="table-responsive table-fixed-height" style="max-height: 70vh; overflow-y:auto;">
|
||||
<table class="table table-sm table-hover align-middle mb-0 table-fixed" id="lastUpdatedTable">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -116,7 +146,21 @@
|
||||
<th>Aksi</th>
|
||||
<th>No Dokumen</th>
|
||||
<th>Nama Dokumen</th>
|
||||
<th>Kategori</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>Unit</th>
|
||||
<th>Tanggal Unggah</th>
|
||||
</tr>
|
||||
@ -126,6 +170,7 @@
|
||||
</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>
|
||||
@ -153,7 +198,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@include('dataUmum.modal.create')
|
||||
@include('dataUmum.modal.view')
|
||||
<script>
|
||||
const katDok = @json($katDok);
|
||||
@ -163,19 +207,24 @@
|
||||
const authPegawai = @json(auth()->user()->objectpegawaifk);
|
||||
const formCreate = $("#formFile")
|
||||
const modalCreate = document.getElementById('modalCreateFile')
|
||||
const tableState = { data: [], page: 1, pageSize: 8, search: '', unit: [], kategori: [], lastPage: 1, total: 0 };
|
||||
const tableState = { data: [], page: 1, pageSize: 8, search: '', unit: [], kategori: [], kategoriType: [], kategoriHeader: [], lastPage: 1, total: 0 };
|
||||
let kategoriOptionCache = [];
|
||||
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;
|
||||
@ -235,16 +284,35 @@
|
||||
}
|
||||
if (kategoriSelect) {
|
||||
$('#tableKategori').select2({
|
||||
placeholder: 'Pilih Kategori',
|
||||
placeholder: 'Kategori (Semua)',
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
closeOnSelect: false
|
||||
width: '100%'
|
||||
});
|
||||
$('#tableKategori').on('change', function () {
|
||||
tableState.kategori = $(this).val() || [];
|
||||
const val = $(this).val() || '';
|
||||
console.log(val);
|
||||
|
||||
tableState.kategoriType = val ? [val] : [];
|
||||
tableState.page = 1;
|
||||
fetchData();
|
||||
});
|
||||
}
|
||||
}
|
||||
if (kategoriHeaderMenu) {
|
||||
kategoriHeaderMenu.addEventListener('change', function(e){
|
||||
const checkbox = e.target.closest('input[type="checkbox"]');
|
||||
if (!checkbox) return;
|
||||
const selected = Array.from(kategoriHeaderMenu.querySelectorAll('input[type="checkbox"]:checked'))
|
||||
.map(el => el.value);
|
||||
tableState.kategoriHeader = selected;
|
||||
if (kategoriSelect && window.$ && $.fn.select2) {
|
||||
const single = selected.length === 1 ? selected[0] : '';
|
||||
$('#tableKategori').val(single).trigger('change');
|
||||
}
|
||||
tableState.page = 1;
|
||||
fetchData();
|
||||
});
|
||||
}
|
||||
|
||||
function resetCreateForm(){
|
||||
colCount = 1;
|
||||
@ -258,7 +326,48 @@
|
||||
function isPublic(permissionVal){
|
||||
if(permissionVal === null || permissionVal === undefined) return false;
|
||||
const val = String(permissionVal).toLowerCase();
|
||||
return val === '1' || val === 'true' || val === 'iya' || val === 'yes';
|
||||
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('');
|
||||
}
|
||||
|
||||
function getExpiryStatus(dateStr){
|
||||
@ -294,12 +403,19 @@
|
||||
statusClass = 'bg-secondary';
|
||||
}
|
||||
const checked = selectedIds.has(String(item.file_directory_id)) ? 'checked' : '';
|
||||
const rowClass = expiryStatus === 'expired' ? 'table-danger' : (expiryStatus === 'soon' ? 'table-warning' : '');
|
||||
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 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}">
|
||||
<tr class="${rowClass}" style="--row-bg:${rowColor};">
|
||||
|
||||
<td class="text-center">
|
||||
<input type="checkbox"
|
||||
@ -358,7 +474,7 @@
|
||||
word-break:break-word;
|
||||
"
|
||||
>
|
||||
${item.nama_dokumen || '-'}
|
||||
${item.nama_dokumen || '-'} ${akreBadge}
|
||||
</a>
|
||||
${expiryBadge}
|
||||
|
||||
@ -367,10 +483,10 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
${kategoriName}
|
||||
${item.nama_kategori || '-'}
|
||||
</td>
|
||||
<td>
|
||||
${unitName}
|
||||
${item.unit?.name || '-'}
|
||||
</td>
|
||||
<td class="text-nowrap">${formatTanggal(item.entry_at)}</td>
|
||||
</tr>
|
||||
@ -421,12 +537,12 @@
|
||||
}
|
||||
|
||||
function renderTable(){
|
||||
const pageData = tableState.data || [];
|
||||
const pageData = filterByKategoriType(tableState.data || []);
|
||||
|
||||
if(pageData.length === 0){
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted py-4">
|
||||
<td colspan="8" class="text-center text-muted py-4">
|
||||
Tidak ada data yang cocok
|
||||
</td>
|
||||
</tr>
|
||||
@ -444,17 +560,85 @@
|
||||
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 || []);
|
||||
tableState.kategori = kategoriSelect && window.$ ? ($('#tableKategori').val() || []) : (tableState.kategori || []);
|
||||
const katVal = kategoriSelect && window.$ ? ($('#tableKategori').val() || '') : (kategoriSelect?.value || '');
|
||||
tableState.kategoriType = katVal ? [katVal] : (tableState.kategoriType || []);
|
||||
tableState.page = 1;
|
||||
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({
|
||||
@ -465,15 +649,20 @@
|
||||
if (tableState.unit && tableState.unit.length > 0) {
|
||||
tableState.unit.forEach(id => params.append('unit[]', id));
|
||||
}
|
||||
if (tableState.kategori && tableState.kategori.length > 0) {
|
||||
tableState.kategori.forEach(kat => params.append('kategori[]', kat));
|
||||
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));
|
||||
}
|
||||
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 => {
|
||||
@ -503,6 +692,7 @@
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
renderKategoriHeaderOptions();
|
||||
fetchData()
|
||||
|
||||
function updateSelectedCount(){
|
||||
@ -679,144 +869,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function addFormV2(){
|
||||
let col = $("#col_add_fileV2");
|
||||
let html = '';
|
||||
|
||||
html += `
|
||||
<div class="row g-3 align-items-start" id="col-${colCount}">
|
||||
<hr class="my-3" />
|
||||
<div class="col-12 d-flex justify-content-end">
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-danger"
|
||||
onclick="removeCol(${colCount})">
|
||||
<i class="fa-solid fa-trash"></i> Hapus
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Unit <span class="text-danger">*</span></label>
|
||||
<select class="form-select"
|
||||
name="data[${colCount}][id_unit_kerja]"
|
||||
id="select_id_unit_kerja_${colCount}"
|
||||
required>
|
||||
<option value="" disabled selected>Pilih Unit</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Sub Unit <span class="text-danger">*</span></label>
|
||||
<select class="form-select"
|
||||
name="data[${colCount}][id_sub_unit_kerja]"
|
||||
id="select_id_sub_unit_kerja_${colCount}"
|
||||
required>
|
||||
<option value="" disabled selected>Pilih Sub Unit</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Kategori Dokumen <span class="text-danger">*</span></label>
|
||||
<select class="form-select"
|
||||
name="data[${colCount}][master_kategori_directory_id]"
|
||||
id="select_kategori_${colCount}"
|
||||
required>
|
||||
<option value="" disabled selected>Pilih Kategori</option>
|
||||
@foreach ($katDok as $kat)
|
||||
<option value="{{ $kat->master_kategori_directory_id }}/{{ $kat->nama_kategori_directory }}">
|
||||
{{ $kat->nama_kategori_directory }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Nomor Dokumen</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">#</span>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="data[${colCount}][no_dokumen]"
|
||||
placeholder="Contoh: 001/RS/IT/I/2026">
|
||||
</div>
|
||||
<div class="form-text text-muted"></div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Nama Dokumen<span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control"
|
||||
name="data[${colCount}][nama_dokumen]"
|
||||
placeholder="Contoh: 001/RS/IT/I/2026" 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]">
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<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}">Ada Expired?</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5 d-none" id="expiredField_${colCount}">
|
||||
<label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label>
|
||||
<input class="form-control" type="date" name="data[${colCount}][tgl_expired]">
|
||||
</div>
|
||||
<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="data[${colCount}][is_permission]"
|
||||
id="perm_yes_${colCount}"
|
||||
value="1"
|
||||
required>
|
||||
<label class="form-check-label" for="perm_yes_${colCount}">Iya</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-1">
|
||||
<input class="form-check-input"
|
||||
type="radio"
|
||||
name="data[${colCount}][is_permission]"
|
||||
id="perm_no_${colCount}"
|
||||
value="2"
|
||||
required>
|
||||
<label class="form-check-label" for="perm_no_${colCount}">Tidak</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label for="fileUpload_${colCount}" class="form-label fw-semibold">📂 Upload Dokumen (PDF)</label>
|
||||
<div class="border rounded-3 p-3 bg-white shadow-sm">
|
||||
<input class="form-control"
|
||||
type="file"
|
||||
id="fileUpload_${colCount}"
|
||||
accept=".pdf"
|
||||
name="data[${colCount}][file]">
|
||||
<div class="mt-2 text-success fw-semibold d-none file-name" id="fileName_${colCount}"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-text text-muted">
|
||||
Format yang didukung: <b>PDF</b>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`;
|
||||
col.append(html)
|
||||
selectOptionUnitKerjaV1(colCount)
|
||||
colCount++;
|
||||
}
|
||||
|
||||
function removeCol(count){
|
||||
$(`#col-${count}`).remove()
|
||||
}
|
||||
|
||||
@ -1,119 +0,0 @@
|
||||
<div class="modal fade" id="modalCreateFile" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5">Aksi </h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Form -->
|
||||
<form id="formFile" action="/uploadv2" enctype="multipart/form-data" method="POST">
|
||||
@csrf
|
||||
<div class="modal-body">
|
||||
<div class="container" style="max-height: 70vh; overflow-y:auto;">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<div class="p-2 rounded-3 bg-light border">
|
||||
<span class="fw-semibold">Informasi Dokumen</span>
|
||||
<div class="small text-muted">Lengkapi detail dokumen sebelum upload.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Unit <span class="text-danger">*</span></label>
|
||||
<select class="form-control unit_kerja" name="data[0][id_unit_kerja]" id="select_id_unit_kerja_0" required>
|
||||
<option value="" disable>Pilih Unit</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Sub Unit <span class="text-danger">*</span></label>
|
||||
<select class="form-control" name="data[0][id_sub_unit_kerja]" id="select_id_sub_unit_kerja_0" required>
|
||||
<option value="" disable selected>Pilih Sub Unit</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Kategori Dokumen <span class="text-danger">*</span></label>
|
||||
<select class="form-control" name="data[0][master_kategori_directory_id]" id="select_kategori_0" required>
|
||||
<option value="" disable>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>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">#</span>
|
||||
<input type="text" class="form-control" name="data[0][no_dokumen]" placeholder="Contoh: 001/RS/IT/I/2026">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Nama Dokumen<span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="data[0][nama_dokumen]" 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[0][date_active]">
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<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">Ada Expired?</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5 d-none" id="expiredField_0">
|
||||
<label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label>
|
||||
<input class="form-control" type="date" name="data[0][tgl_expired]">
|
||||
|
||||
</div>
|
||||
<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="data[0][is_permission]" id="perm_yes" value="1" required>
|
||||
<label class="form-check-label" for="perm_yes">
|
||||
Iya
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-1">
|
||||
<input class="form-check-input" type="radio" name="data[0][is_permission]" id="perm_no" value="2" required>
|
||||
<label class="form-check-label" for="perm_no">
|
||||
Tidak
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mb-2">
|
||||
<label for="fileUpload0" class="form-label fw-semibold">📂 Upload Dokumen (PDF)</label>
|
||||
<div class="border rounded-3 p-3 bg-white shadow-sm">
|
||||
<input class="form-control" type="file" id="fileUpload0" accept=".pdf" name="data[0][file]">
|
||||
<div class="mt-2 text-success fw-semibold d-none file-name"></div>
|
||||
</div>
|
||||
<div class="form-text text-muted">
|
||||
Format yang didukung: <b>PDF</b> Maksimal <b>10mb</b>.
|
||||
</div>
|
||||
</div>
|
||||
<div id="col_add_fileV2" class="col-12"></div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm mt-3" onclick="addFormV2()">
|
||||
+ Tambah Form
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Tutup</button>
|
||||
<button type="submit" class="btn btn-primary">Simpan</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,10 +1,10 @@
|
||||
@extends('layout.main')
|
||||
<style>
|
||||
/* --- SELECT2: teks terlihat (hitam) --- */
|
||||
|
||||
.select2-container--default .select2-selection--multiple {
|
||||
background: #fff !important;
|
||||
border: 1px solid #ced4da !important;
|
||||
min-height: 31px; /* cocok form-select-sm */
|
||||
border: 1px solid rgb(206, 212, 218) !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
|
||||
@ -35,6 +35,35 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/* --- 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">
|
||||
@ -72,6 +101,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<!-- Tambah Dokumen -->
|
||||
@if(!Auth::guard('admin')->check())
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success btn-sm"
|
||||
@ -81,17 +111,19 @@
|
||||
<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" 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;" multiple>
|
||||
@foreach ($katDok as $kat)
|
||||
<option value="{{ $kat->master_kategori_directory_id }}">
|
||||
{{ $kat->nama_kategori_directory }}
|
||||
</option>
|
||||
@endforeach
|
||||
<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>
|
||||
<input type="search"
|
||||
id="tableSearch"
|
||||
@ -101,7 +133,7 @@
|
||||
</div>
|
||||
<button class="btn btn-primary" type="button" id="btnTableSearch">Cari</button>
|
||||
</div>
|
||||
<div class="table-responsive" style="max-height: 70vh; overflow-y:auto;">
|
||||
<div class="table-responsive table-fixed-height" style="max-height: 70vh; overflow-y:auto;">
|
||||
<table class="table table-sm table-hover align-middle mb-0 table-fixed" id="lastUpdatedTable">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -111,7 +143,21 @@
|
||||
<th>Aksi</th>
|
||||
<th>No Dokumen</th>
|
||||
<th>Nama Dokumen</th>
|
||||
<th>Kategori</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>Unit</th>
|
||||
<th>Tanggal Unggah</th>
|
||||
<th>Pengunggah</th>
|
||||
@ -122,6 +168,7 @@
|
||||
</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>
|
||||
@ -164,19 +211,32 @@
|
||||
const authPegawai = @json(auth()->user()->objectpegawaifk);
|
||||
const formCreate = $("#formFile");
|
||||
const modalCreate = document.getElementById('modalCreateFile');
|
||||
const tableState = { data: [], page: 1, pageSize: 8, search: '', unit: [], kategori: [], lastPage: 1, total: 0 };
|
||||
const tableState = { data: [], page: 1, pageSize: 8, search: '', unit: [], kategori: [], kategoriType: [], kategoriHeader: [], lastPage: 1, total: 0 };
|
||||
let kategoriOptionCache = [];
|
||||
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];
|
||||
}
|
||||
|
||||
document.addEventListener('change', function(e){
|
||||
if(!e.target.classList.contains('toggle-expired')) return;
|
||||
@ -206,8 +266,6 @@
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
closeOnSelect: false,
|
||||
selectionCssClass: 'select2-filter-selection',
|
||||
dropdownCssClass: 'select2-filter-dropdown',
|
||||
ajax: {
|
||||
url: '/select-unit-kerja-mapping',
|
||||
dataType: 'json',
|
||||
@ -226,24 +284,39 @@
|
||||
cache: true
|
||||
}
|
||||
});
|
||||
$('#tableUnit').on('change', function () {
|
||||
tableState.unit = $(this).val() || [];
|
||||
});
|
||||
|
||||
}
|
||||
if (kategoriSelect) {
|
||||
$('#tableKategori').select2({
|
||||
placeholder: 'Pilih Kategori',
|
||||
placeholder: 'Kategori (Semua)',
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
closeOnSelect: false,
|
||||
selectionCssClass: 'select2-filter-selection',
|
||||
dropdownCssClass: 'select2-filter-dropdown'
|
||||
});
|
||||
$('#tableKategori').on('change', function () {
|
||||
tableState.kategori = $(this).val() || [];
|
||||
const val = $(this).val() || '';
|
||||
tableState.kategoriType = val ? [val] : [];
|
||||
tableState.page = 1;
|
||||
fetchData();
|
||||
});
|
||||
}
|
||||
}
|
||||
if (kategoriHeaderMenu) {
|
||||
kategoriHeaderMenu.addEventListener('change', function(e){
|
||||
const checkbox = e.target.closest('input[type="checkbox"]');
|
||||
if (!checkbox) return;
|
||||
const selected = Array.from(kategoriHeaderMenu.querySelectorAll('input[type="checkbox"]:checked'))
|
||||
.map(el => el.value);
|
||||
tableState.kategoriHeader = selected;
|
||||
if (kategoriSelect && window.$ && $.fn.select2) {
|
||||
const single = selected.length === 1 ? selected[0] : '';
|
||||
$('#tableKategori').val(single).trigger('change');
|
||||
}
|
||||
tableState.page = 1;
|
||||
fetchData();
|
||||
});
|
||||
}
|
||||
|
||||
function resetCreateForm(){
|
||||
colCount = 1;
|
||||
@ -252,12 +325,186 @@
|
||||
formCreate.find('select').val(null).trigger('change');
|
||||
formCreate.find('input[type="file"]').val('');
|
||||
formCreate.find('.file-name').addClass('d-none').text('');
|
||||
resetAkreFields(0);
|
||||
enableAkreFields(0);
|
||||
}
|
||||
|
||||
let akreData = [];
|
||||
let akreLoaded = false;
|
||||
let akreFlat = [];
|
||||
|
||||
function loadAkreData(){
|
||||
if(akreLoaded) return Promise.resolve(akreData);
|
||||
return fetch('/json/akreditasi.jff')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
akreData = Array.isArray(data) ? data : [];
|
||||
akreFlat = [];
|
||||
akreLoaded = true;
|
||||
return akreData;
|
||||
})
|
||||
.catch(() => {
|
||||
akreData = [];
|
||||
akreFlat = [];
|
||||
akreLoaded = true;
|
||||
return akreData;
|
||||
});
|
||||
}
|
||||
|
||||
function getAkreFlat(){
|
||||
if(akreFlat.length) return akreFlat;
|
||||
akreFlat = (akreData || []).flatMap(type => {
|
||||
const segments = Array.isArray(type.segment) ? type.segment : [];
|
||||
return segments.flatMap(seg => {
|
||||
const children = Array.isArray(seg.turunan) ? seg.turunan : [];
|
||||
return children.map(child => ({
|
||||
value: `${type.name}/${seg.name}/${child.name}`,
|
||||
label: `${type.name} / ${child.name}`,
|
||||
type: type.name,
|
||||
segment: seg.name,
|
||||
item: child.name
|
||||
}));
|
||||
});
|
||||
});
|
||||
return akreFlat;
|
||||
}
|
||||
|
||||
function fillAkreSelect(selectEl){
|
||||
if(!selectEl) return;
|
||||
selectEl.innerHTML = '<option value="" disabled selected>Pilih Instrumen</option>';
|
||||
getAkreFlat().forEach(optData => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = optData.value;
|
||||
opt.textContent = optData.label;
|
||||
selectEl.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function setKategoriRequired(index, isRequired){
|
||||
const katSelect = document.getElementById(`select_kategori_${index}`);
|
||||
if (!katSelect) return;
|
||||
if (isRequired) {
|
||||
katSelect.setAttribute('required', 'required');
|
||||
} else {
|
||||
katSelect.removeAttribute('required');
|
||||
}
|
||||
}
|
||||
|
||||
function resetAkreFields(index){
|
||||
const selectEl = document.getElementById(`akre_select_${index}`);
|
||||
const typeInput = document.getElementById(`akre_type_${index}`);
|
||||
const segmentInput = document.getElementById(`akre_segment_${index}`);
|
||||
const itemInput = document.getElementById(`akre_item_${index}`);
|
||||
if(selectEl){
|
||||
selectEl.value = '';
|
||||
if(window.$ && $.fn.select2) $(selectEl).val(null).trigger('change');
|
||||
}
|
||||
if(typeInput) typeInput.value = '';
|
||||
if(segmentInput) segmentInput.value = '';
|
||||
if(itemInput) itemInput.value = '';
|
||||
setKategoriRequired(index, false);
|
||||
}
|
||||
|
||||
function enableAkreFields(index){
|
||||
const selectEl = document.getElementById(`akre_select_${index}`);
|
||||
// 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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initKategoriSelect2(index){
|
||||
if(!window.$ || !$.fn.select2) return;
|
||||
const katSelect = $(`#select_kategori_${index}`);
|
||||
const hukumSelect = $(`#select_kategori_hukum_${index}`);
|
||||
if(katSelect.length){
|
||||
katSelect.select2({
|
||||
dropdownParent: $('#modalCreateFile'),
|
||||
placeholder:'Pilih Kategori',
|
||||
allowClear: true
|
||||
});
|
||||
}
|
||||
if(hukumSelect.length){
|
||||
hukumSelect.select2({
|
||||
dropdownParent: $('#modalCreateFile'),
|
||||
placeholder:'Pilih Kategori Hukum',
|
||||
allowClear:true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('change', function(e){
|
||||
if(e.target.classList.contains('akre-select')){
|
||||
const id = e.target.id || '';
|
||||
const idx = id.split('_').pop();
|
||||
const [typeVal = '', segmentVal = '', itemVal = ''] = (e.target.value || '').split('/');
|
||||
const typeInput = document.getElementById(`akre_type_${idx}`);
|
||||
const segmentInput = document.getElementById(`akre_segment_${idx}`);
|
||||
const itemInput = document.getElementById(`akre_item_${idx}`);
|
||||
if(typeInput) typeInput.value = typeVal;
|
||||
if(segmentInput) segmentInput.value = segmentVal;
|
||||
if(itemInput) itemInput.value = itemVal;
|
||||
return;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function isPublic(permissionVal){
|
||||
if(permissionVal === null || permissionVal === undefined) return false;
|
||||
const val = String(permissionVal).toLowerCase();
|
||||
return val === '1' || val === 'true' || val === 'iya' || val === 'yes';
|
||||
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('');
|
||||
}
|
||||
|
||||
function getExpiryStatus(dateStr){
|
||||
@ -293,12 +540,18 @@
|
||||
statusClass = 'bg-secondary';
|
||||
}
|
||||
const checked = selectedIds.has(String(item.file_directory_id)) ? 'checked' : '';
|
||||
const rowClass = expiryStatus === 'expired' ? 'table-danger' : (expiryStatus === 'soon' ? 'table-warning' : '');
|
||||
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 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}">
|
||||
<tr class="${rowClass}" style="--row-bg:${rowColor};">
|
||||
<td class="text-center">
|
||||
<input type="checkbox"
|
||||
class="form-check-input row-check"
|
||||
@ -347,17 +600,17 @@
|
||||
>
|
||||
${item.nama_dokumen || '-'}
|
||||
</a>
|
||||
${expiryBadge}
|
||||
${akreBadge}${expiryBadge}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
${kategoriName}
|
||||
${item.nama_kategori || '-'}
|
||||
</td>
|
||||
<td>
|
||||
${unitName}
|
||||
${item.unit?.name || '-'}
|
||||
</td>
|
||||
<td class="text-nowrap">${formatTanggal(item.entry_at)}</td>
|
||||
<td class="text-nowrap">${item.pegawai_nama_entry || '-'}</td>
|
||||
<td style="max-width: 200px; white-space: normal; word-wrap: break-word;">${item.pegawai_nama_entry || '-'}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
@ -406,12 +659,12 @@
|
||||
}
|
||||
|
||||
function renderTable(){
|
||||
const pageData = tableState.data || [];
|
||||
const pageData = filterByKategoriType(tableState.data || []);
|
||||
|
||||
if(pageData.length === 0){
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted py-4">
|
||||
<td colspan="8" class="text-center text-muted py-4">
|
||||
Tidak ada data yang cocok
|
||||
</td>
|
||||
</tr>
|
||||
@ -429,17 +682,110 @@
|
||||
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 || []);
|
||||
tableState.kategori = kategoriSelect && window.$ ? ($('#tableKategori').val() || []) : (tableState.kategori || []);
|
||||
tableState.unit = normalizeToArray(
|
||||
unitSelect && window.$ ? $('#tableUnit').val() : tableState.unit
|
||||
);
|
||||
const katVal = kategoriSelect && window.$ ? ($('#tableKategori').val() || '') : (kategoriSelect?.value || '');
|
||||
tableState.kategoriType = katVal ? [katVal] : (tableState.kategoriType || []);
|
||||
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({
|
||||
@ -447,18 +793,24 @@
|
||||
per_page: tableState.pageSize,
|
||||
keyword: tableState.search
|
||||
});
|
||||
if (tableState.unit && tableState.unit.length > 0) {
|
||||
tableState.unit.forEach(id => params.append('unit[]', id));
|
||||
const unitValues = normalizeToArray(tableState.unit);
|
||||
if (unitValues.length > 0) {
|
||||
unitValues.forEach(id => params.append('unit[]', id));
|
||||
}
|
||||
if (tableState.kategori && tableState.kategori.length > 0) {
|
||||
tableState.kategori.forEach(kat => params.append('kategori[]', kat));
|
||||
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));
|
||||
}
|
||||
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 => {
|
||||
@ -467,17 +819,13 @@
|
||||
})
|
||||
}
|
||||
|
||||
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);
|
||||
@ -487,6 +835,7 @@
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
renderKategoriHeaderOptions();
|
||||
fetchData()
|
||||
|
||||
function updateSelectedCount(){
|
||||
@ -636,6 +985,8 @@
|
||||
});
|
||||
|
||||
selectOptionUnitKerjaV1(0);
|
||||
initKategoriSelect2(0);
|
||||
enableAkreFields(0);
|
||||
});
|
||||
|
||||
function loadSubUnitKerja(unitId){
|
||||
@ -673,7 +1024,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Unit <span class="text-danger">*</span></label>
|
||||
<select class="form-select"
|
||||
name="data[${colCount}][id_unit_kerja]"
|
||||
@ -683,7 +1034,7 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Sub Unit <span class="text-danger">*</span></label>
|
||||
<select class="form-select"
|
||||
name="data[${colCount}][id_sub_unit_kerja]"
|
||||
@ -693,20 +1044,7 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Kategori Dokumen <span class="text-danger">*</span></label>
|
||||
<select class="form-select"
|
||||
name="data[${colCount}][master_kategori_directory_id]"
|
||||
id="select_kategori_${colCount}"
|
||||
required>
|
||||
<option value="" disabled selected>Pilih Kategori</option>
|
||||
@foreach ($katDok as $kat)
|
||||
<option value="{{ $kat->master_kategori_directory_id }}/{{ $kat->nama_kategori_directory }}">
|
||||
{{ $kat->nama_kategori_directory }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Nomor Dokumen</label>
|
||||
@ -733,7 +1071,7 @@
|
||||
type="date"
|
||||
name="data[${colCount}][date_active]">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input toggle-expired"
|
||||
type="checkbox"
|
||||
@ -750,7 +1088,7 @@
|
||||
name="data[${colCount}][tgl_expired]" disabled>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
<div class="col-md-4">
|
||||
<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">
|
||||
@ -761,7 +1099,7 @@
|
||||
id="perm_yes_${colCount}"
|
||||
value="1"
|
||||
required>
|
||||
<label class="form-check-label" for="perm_yes_${colCount}">Iya</label>
|
||||
<label class="form-check-label" for="perm_yes_${colCount}">Ya</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-1">
|
||||
@ -776,6 +1114,44 @@
|
||||
</div>
|
||||
|
||||
</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;">
|
||||
<option value="" disabled selected>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="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>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Kategori Lainnya</label>
|
||||
<select class="form-select"
|
||||
name="data[${colCount}][master_kategori_directory_id]"
|
||||
id="select_kategori_${colCount}" style="width: 350px;">
|
||||
<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-12">
|
||||
<label for="fileUpload_${colCount}" class="form-label fw-semibold">📂 Upload Dokumen (PDF)</label>
|
||||
@ -798,8 +1174,11 @@
|
||||
`;
|
||||
col.append(html)
|
||||
selectOptionUnitKerjaV1(colCount)
|
||||
initKategoriSelect2(colCount)
|
||||
enableAkreFields(colCount)
|
||||
setKategoriRequired(colCount, false)
|
||||
colCount++;
|
||||
}
|
||||
}
|
||||
|
||||
function removeCol(count){
|
||||
$(`#col-${count}`).remove()
|
||||
|
||||
@ -20,28 +20,19 @@
|
||||
<div class="small text-muted">Lengkapi detail dokumen sebelum upload.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Unit <span class="text-danger">*</span></label>
|
||||
<select class="form-control unit_kerja" name="data[0][id_unit_kerja]" id="select_id_unit_kerja_0" required>
|
||||
<option value="" disable>Pilih Unit</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Sub Unit <span class="text-danger">*</span></label>
|
||||
<select class="form-control" name="data[0][id_sub_unit_kerja]" id="select_id_sub_unit_kerja_0" required>
|
||||
<option value="" disable selected>Pilih Sub Unit</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Kategori Dokumen <span class="text-danger">*</span></label>
|
||||
<select class="form-control" name="data[0][master_kategori_directory_id]" id="select_kategori_0" required>
|
||||
<option value="" disable>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>
|
||||
<div class="input-group">
|
||||
@ -57,27 +48,26 @@
|
||||
<label class="form-label fw-semibold">Tanggal Terbit</label>
|
||||
<input class="form-control" type="date" name="data[0][date_active]">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-4">
|
||||
<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-5" 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]" disabled>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Boleh dilihat unit lain? <span class="text-danger">*</span></label>
|
||||
|
||||
<div class="border rounded-3 p-2 bg-light">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="data[0][is_permission]" id="perm_yes" value="1" required>
|
||||
<label class="form-check-label" for="perm_yes">
|
||||
Iya
|
||||
Ya
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-1">
|
||||
<input class="form-check-input" type="radio" name="data[0][is_permission]" id="perm_no" value="2" required>
|
||||
<label class="form-check-label" for="perm_no">
|
||||
@ -85,9 +75,41 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</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;">
|
||||
<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="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>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Kategori lainnya</label>
|
||||
<select class="form-select select-kat-lain" name="data[0][master_kategori_directory_id]" id="select_kategori_0" 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="fileUpload0" class="form-label fw-semibold">📂 Upload Dokumen (PDF)</label>
|
||||
<div class="border rounded-3 p-3 bg-white shadow-sm">
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
<thead class="table-light shadow-sm">
|
||||
<tr>
|
||||
<th style="width:5%;" class="text-center">#</th>
|
||||
<th style="width:30%;">Unit</th>
|
||||
<th style="width:30%;">Unit / Akreditasi</th>
|
||||
<th style="width:20%;">Kategori</th>
|
||||
<th style="width:15%;" class="text-center">Jumlah File</th>
|
||||
</tr>
|
||||
|
||||
1450
resources/views/expDokumen/index.blade.php
Normal file
1450
resources/views/expDokumen/index.blade.php
Normal file
File diff suppressed because it is too large
Load Diff
152
resources/views/expDokumen/section/recap.blade.php
Normal file
152
resources/views/expDokumen/section/recap.blade.php
Normal file
@ -0,0 +1,152 @@
|
||||
@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>
|
||||
@ -41,15 +41,21 @@
|
||||
<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>
|
||||
<span class="hide-menu">Dokumen Akreditasi</span>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
{{-- AKTIVITAS --}}
|
||||
<li class="nav-small-cap"><span class="hide-menu">Aktivitas</span></li>
|
||||
|
||||
|
||||
@php
|
||||
$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">
|
||||
@ -62,7 +68,9 @@
|
||||
<span class="badge bg-danger rounded-pill d-none" id="pendingCountBadge">0</span>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@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">
|
||||
@ -74,6 +82,7 @@
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@endif
|
||||
<li class="sidebar-item">
|
||||
<a class="sidebar-link d-flex align-items-center justify-content-between"
|
||||
href="{{ url('/log-activity') }}" aria-expanded="false">
|
||||
@ -97,6 +106,14 @@
|
||||
</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()->where('objectunitkerjapegawaifk', 43)->exists())
|
||||
<li class="nav-small-cap"><span class="hide-menu">Master</span></li>
|
||||
|
||||
@ -137,6 +154,7 @@
|
||||
</ul>
|
||||
</li>
|
||||
@endif
|
||||
@endif
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@ -1,41 +1,50 @@
|
||||
<style>
|
||||
/* ===== NOTIFIKASI STYLE FACEBOOK ===== */
|
||||
<style>
|
||||
|
||||
.message-body {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* item notif */
|
||||
|
||||
.message-body .dropdown-item {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1px;
|
||||
}
|
||||
.message-body .dropdown-item div {
|
||||
white-space: normal;
|
||||
border-radius: 6px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* 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">
|
||||
@ -50,7 +59,27 @@
|
||||
<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 dropdown">
|
||||
<a class="nav-link position-relative" href="javascript:void(0)" id="drop1" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<a class="nav-link position-relative" href="javascript:void(0)" id="dropExpired" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="d-inline-flex align-items-center justify-content-center rounded-circle bg-light" style="width:46px;height:46px;">
|
||||
<i class="ti ti-alert-circle text-primary"></i>
|
||||
</span>
|
||||
<span id="expiredDot" class="notif-dot d-none">
|
||||
</span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-animate-up shadow" aria-labelledby="dropExpired" style="min-width: 320px;">
|
||||
<div class="d-flex align-items-center justify-content-between px-3 border-bottom">
|
||||
<span class="fw-semibold">Notifikasi Expired</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary my-2" id="expiredOpenDetailBtn">
|
||||
Detail
|
||||
</button>
|
||||
</div>
|
||||
<div class="message-body" id="expiredNotifList">
|
||||
<div class="dropdown-item text-muted small">Memuat...</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link position-relative" href="javascript:void(0)" id="dropNotif" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="d-inline-flex align-items-center justify-content-center rounded-circle bg-light" style="width:46px;height:46px;">
|
||||
<i class="ti ti-bell text-primary"></i>
|
||||
</span>
|
||||
@ -58,7 +87,7 @@
|
||||
0
|
||||
</span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-animate-up shadow" aria-labelledby="drop1" style="min-width: 320px;">
|
||||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-animate-up shadow" aria-labelledby="dropNotif" 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>
|
||||
@ -78,7 +107,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 }}</p>
|
||||
<p class="mb-0 fs-3">{{ auth()->user()->namauser ?? 'admin' }}</p>
|
||||
</a>
|
||||
<form action="/logout" method="POST">
|
||||
@csrf
|
||||
@ -91,68 +120,619 @@
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const listEl = document.getElementById('notifList');
|
||||
const countTextEl = document.getElementById('notifCountText');
|
||||
const badgeEl = document.getElementById('notifCountBadge');
|
||||
|
||||
function setList(items) {
|
||||
if (!listEl) return;
|
||||
if (!items.length) {
|
||||
listEl.innerHTML = '<div class="dropdown-item text-muted small">Tidak ada notifikasi</div>';
|
||||
<!-- 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>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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 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
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
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>
|
||||
|
||||
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">${item.text_notifikasi || '-'}</div>
|
||||
<div class="small text-muted">${item.created_at || ''}</div>
|
||||
<div class="fw-semibold">${text}</div>
|
||||
<div class="small text-muted">${createdAt}</div>
|
||||
</div>
|
||||
</a>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
fetch('/data/notifications')
|
||||
function setNotifBadge(unread) {
|
||||
unread = Number(unread || 0);
|
||||
|
||||
if (notifCountTextEl) {
|
||||
notifCountTextEl.textContent = `${unread} baru`;
|
||||
}
|
||||
|
||||
if (!notifBadgeEl) return;
|
||||
|
||||
if (unread > 0) {
|
||||
notifBadgeEl.classList.remove('d-none');
|
||||
notifBadgeEl.textContent = unread > 99 ? '99+' : unread;
|
||||
} else {
|
||||
notifBadgeEl.classList.add('d-none');
|
||||
notifBadgeEl.textContent = '0';
|
||||
}
|
||||
}
|
||||
|
||||
function loadNotifications() {
|
||||
ajaxGet('/data/notifications')
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
const unread = res?.status ? Number(res.unread || 0) : 0;
|
||||
const items = res?.status ? (res.data || []) : [];
|
||||
|
||||
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);
|
||||
setNotifBadge(unread);
|
||||
setNotifList(items);
|
||||
})
|
||||
.catch(() => {
|
||||
if (listEl) listEl.innerHTML = '<div class="dropdown-item text-muted small">Gagal memuat notifikasi</div>';
|
||||
if (notifListEl) {
|
||||
notifListEl.innerHTML = `
|
||||
<div class="dropdown-item text-muted small">
|
||||
Gagal memuat notifikasi
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setNotifBadge(0);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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'
|
||||
function setExpiredDetailLoading(text = 'Memuat...') {
|
||||
if (!expiredDetailTbody) return;
|
||||
|
||||
expiredDetailTbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7" class="text-muted small">${escapeHtml(text)}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}).then(() => {
|
||||
if (countTextEl) countTextEl.textContent = '0 baru';
|
||||
if (badgeEl) badgeEl.classList.add('d-none');
|
||||
}).catch(() => {});
|
||||
|
||||
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);
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
|
||||
@ -139,7 +139,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const paginationBtns = document.getElementById('logPaginationBtns');
|
||||
const summaryText = document.getElementById('logSummaryText');
|
||||
|
||||
if(tbody) tbody.innerHTML = '<tr><td colspan="4" class="text-center text-muted py-3">Memuat...</td></tr>';
|
||||
if(tbody) tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted py-3">Memuat...</td></tr>';
|
||||
if(summaryText) summaryText.textContent = 'Memuat data...';
|
||||
if(searchInput) searchInput.value = keyword || '';
|
||||
|
||||
@ -160,11 +160,12 @@ 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="4" class="text-center text-muted py-3">Belum ada aktivitas</td></tr>' : '';
|
||||
const emptyState = logs.length === 0 ? '<tr><td colspan="6" class="text-center text-muted py-3">Belum ada aktivitas</td></tr>' : '';
|
||||
if(tbody) tbody.innerHTML = logs.length ? rows : emptyState;
|
||||
|
||||
if(summaryText){
|
||||
@ -255,7 +256,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if(pageData.length === 0){
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted py-4">
|
||||
<td colspan="7" class="text-center text-muted py-4">
|
||||
Tidak ada data
|
||||
</td>
|
||||
</tr>
|
||||
@ -341,6 +342,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<th>#</th>
|
||||
<th>Nama</th>
|
||||
<th>Jumlah Membuka</th>
|
||||
<th>Jumlah Mengunduh</th>
|
||||
<th>Terakhir Dilihat</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@ -18,27 +18,18 @@
|
||||
<div class="small text-muted">Perbarui detail dokumen sebelum mengirim ulang.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-6">
|
||||
<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-4">
|
||||
<div class="col-md-6">
|
||||
<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 <span class="text-danger">*</span></label>
|
||||
<select class="form-control" name="master_kategori_directory_id" id="edit_kategori" required>
|
||||
<option value="" disabled selected>Pilih Kategori</option>
|
||||
@foreach ($katDok as $kat)
|
||||
<option value="{{ $kat->master_kategori_directory_id }}/{{ $kat->nama_kategori_directory }}">{{ $kat->nama_kategori_directory }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Nomor Dokumen</label>
|
||||
@ -55,22 +46,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-2 d-flex align-items-end">
|
||||
<div class="col-md-4">
|
||||
<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>
|
||||
<input class="form-check-input toggle-expired" type="checkbox" id="edit_has_expired" data-target="edit_expired_field">
|
||||
<label class="form-check-label" for="edit_has_expired">Masa Berlaku Dokumen?</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5" id="edit_expired_field">
|
||||
<div class="col-md-4" 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-5">
|
||||
<div class="col-md-4">
|
||||
<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">Iya</label>
|
||||
<label class="form-check-label" for="edit_perm_yes">Ya</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>
|
||||
@ -79,6 +70,41 @@
|
||||
</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>
|
||||
</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,6 +1,7 @@
|
||||
<?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;
|
||||
@ -9,13 +10,19 @@ use App\Http\Controllers\LogActivityController;
|
||||
use App\Http\Controllers\masterPersetujuanController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware(['auth'])->group(function(){
|
||||
Route::middleware(['auth:admin,web'])->group(function(){
|
||||
|
||||
Route::get('/', [DashboardController::class, 'index']);
|
||||
Route::get('/data-internal', [DashboardController::class, 'dataUnitInternal']);
|
||||
Route::get('/download-excel/data-unit', [DashboardController::class, 'downloadDataUnitExcel']);
|
||||
Route::get('/data-umum', [DashboardController::class, 'dataUmum']);
|
||||
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']);
|
||||
@ -53,7 +60,7 @@ Route::middleware(['auth'])->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-pengajuan', [LogActivityController::class, 'datatableHistoryPengajuan']);
|
||||
Route::get('/datatable/log-activity-pengajuapn', [LogActivityController::class, 'datatableHistoryPengajuan']);
|
||||
|
||||
Route::get('/recap', [DashboardController::class, 'recapView']);
|
||||
Route::get('/data/recap', [DashboardController::class, 'recapData']);
|
||||
@ -74,10 +81,18 @@ Route::middleware(['auth'])->group(function(){
|
||||
// });
|
||||
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