membuat code pengembangan, merubah status menjadi penilaian dan menambahkan akun batasan untuk login

This commit is contained in:
JokoPrasetio 2026-05-11 14:42:14 +07:00
parent 4c335c9481
commit 76876f94ef
10 changed files with 1022 additions and 230 deletions

View File

@ -5,11 +5,15 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use App\Models\MasterPitStopPraAkre;
class AuthController extends Controller
{
public function login(){
return view('auth.login');
$totalSteps = (int) MasterPitStopPraAkre::where('statusenabled', true)->count();
return view('auth.login', [
'totalSteps' => $totalSteps,
]);
}
public function submitLogin(Request $request){
@ -17,8 +21,25 @@ class AuthController extends Controller
'namauser' => 'required',
'password' => 'required'
]);
$user = User::where('namauser', $request->namauser)->first();
$allowedIds = [
727,
1755,
2184,
2549,
993,
3053,
2319,
1995,
2011,
2145,
1113,
2998
];
$user = User::where('namauser', $request->namauser)->first();
if (!in_array($user->id, $allowedIds)) {
return back()->with(['error' => 'Akun Anda tidak diizinkan login']);
}
if ($user && $user->passcode === sha1($request->password)) {
auth()->login($user);
$request->session()->regenerate();
@ -34,4 +55,21 @@ class AuthController extends Controller
request()->session()->regenerateToken();
return redirect('/login');
}
// query dibawah digunakan untuk mencari data akun user login
// select ls.id, ls.namauser, ls.objectpegawaifk, pm.nama
// FROM pegawai_m pm
// left join loginuser_s ls on pm.id = ls.objectpegawaifk
// WHERE nama ILIKE ANY (ARRAY[
// '%Sarvita Dewi%',
// '%Milwiyandia%',
// '%Zulkarnaen%',
// '%Ripka perdija surbakti%',
// '%Arum Budiarti%',
// '%Ghufran Haning Putra%',
// '%Ifah Kisyafah%',
// '%Putri rishki Roma Dani%',
// '%Putri Milenia Ramadhanti%',
// '%Nurul susilowati%',
// '%Joko Prasetio%'
// ]);
}

View File

@ -39,7 +39,7 @@ class PitStopController extends Controller
public function pitstop(){
$masterPitStop = MasterPitStopPraAkre::where('statusenabled', true)->get();
$data = [
'title' => 'Pit Stop',
'title' => 'PitStop',
'masterPitStop' => $masterPitStop
];
return view('pitstop.index', $data);
@ -122,7 +122,7 @@ class PitStopController extends Controller
DB::raw('ukp.id as unit_id'),
DB::raw("coalesce(ukp.name, '-') as unit_name"),
DB::raw('pg.id as pegawai_id'),
DB::raw("count(distinct m.id) filter (where p.status='lulus') as lulus_count"),
DB::raw("count(distinct m.id) as lulus_count"),
])
->groupBy('ukp.id', 'ukp.name', 'pg.id');
@ -206,7 +206,7 @@ class PitStopController extends Controller
->select([
DB::raw("coalesce(nullif(pl.tipe, ''), '-') as tipe"),
DB::raw('pl.id as pegawai_id'),
DB::raw("count(distinct m.id) filter (where p.status='lulus') as lulus_count"),
DB::raw("count(distinct m.id) as lulus_count"),
])
->groupBy(DB::raw("coalesce(nullif(pl.tipe, ''), '-')"), 'pl.id');
@ -338,7 +338,6 @@ class PitStopController extends Controller
{
$unitId = $request->query('unit_id');
$unitId = is_null($unitId) || $unitId === '' ? null : (int) $unitId;
$totalSteps = (int) MasterPitStopPraAkre::where('statusenabled', true)->count();
$generatedAt = now();
@ -362,7 +361,7 @@ class PitStopController extends Controller
DB::raw('ukp.id as unit_id'),
DB::raw("coalesce(ukp.name, '-') as unit_name"),
DB::raw('pg.id as pegawai_id'),
DB::raw("count(distinct m.id) filter (where p.status='lulus') as lulus_count"),
DB::raw("count(distinct m.id) as lulus_count"),
])
->groupBy('ukp.id', 'ukp.name', 'pg.id');
@ -408,7 +407,7 @@ class PitStopController extends Controller
])->setPaper('a4', 'landscape');
$filename = 'monitoring-pra-akreditasi-ringkasan-' . $generatedAt->format('Ymd-Hi') . '.pdf';
return $pdf->download($filename);
return $pdf->stream($filename);
}
// Mode detail: PDF per unit (lebih aman untuk memory).
@ -442,13 +441,12 @@ class PitStopController extends Controller
DB::raw('pg.id as pegawai_id'),
DB::raw("coalesce(pg.namalengkap, '-') as nama"),
DB::raw("coalesce(pg.nip_pns, '-') as nip_pns"),
DB::raw("count(distinct m.id) filter (where p.status='lulus') as lulus_count"),
DB::raw("count(distinct m.id) as lulus_count"),
DB::raw("coalesce((
select string_agg(ms.nama, ', ' order by ms.id)
from public.masterpitstop ms
left join public.praakre px
on px.pegawai_id = pg.id
and px.status = 'lulus'
and px.masterpitstop_id = ms.id::text
where ms.statusenabled = true
and px.id is null
@ -503,7 +501,7 @@ class PitStopController extends Controller
])->setPaper('a4', 'landscape');
$filename = 'monitoring-pra-akreditasi-unit-' . $unitId . '-' . $generatedAt->format('Ymd-Hi') . '.pdf';
return $pdf->download($filename);
return $pdf->stream($filename);
}
public function monitoringPdfExternal(Request $request)
@ -530,7 +528,7 @@ class PitStopController extends Controller
->select([
DB::raw("coalesce(nullif(pl.tipe, ''), '-') as tipe"),
DB::raw('pl.id as pegawai_id'),
DB::raw("count(distinct m.id) filter (where p.status='lulus') as lulus_count"),
DB::raw("count(distinct m.id) as lulus_count"),
])
->groupBy(DB::raw("coalesce(nullif(pl.tipe, ''), '-')"), 'pl.id');
@ -566,7 +564,7 @@ class PitStopController extends Controller
])->setPaper('a4', 'landscape');
$filename = 'monitoring-karyawan-luar-ringkasan-' . $generatedAt->format('Ymd-Hi') . '.pdf';
return $pdf->download($filename);
return $pdf->stream($filename);
}
// Detail per tipe
@ -585,14 +583,13 @@ class PitStopController extends Controller
'pl.id',
DB::raw("coalesce(pl.nama, '-') as nama"),
DB::raw("coalesce(pl.nik, '-') as nik"),
DB::raw("count(distinct m.id) filter (where p.status='lulus') as lulus_count"),
DB::raw("count(distinct m.id) as lulus_count"),
DB::raw("coalesce((
select string_agg(ms.nama, ', ' order by ms.id)
from public.masterpitstop ms
left join public.praakre px
on px.pegawai_id = pl.id
and px.tipe_karyawan = 'luar'
and px.status = 'lulus'
and px.masterpitstop_id = ms.id::text
where ms.statusenabled = true
and px.id is null
@ -666,7 +663,7 @@ class PitStopController extends Controller
DB::raw('ukp.id as unit_id'),
DB::raw("coalesce(ukp.name, '-') as unit_name"),
DB::raw('pg.id as pegawai_id'),
DB::raw("count(distinct m.id) filter (where p.status='lulus') as lulus_count"),
DB::raw("count(distinct m.id) as lulus_count"),
])
->groupBy('ukp.id', 'ukp.name', 'pg.id');
@ -737,13 +734,12 @@ class PitStopController extends Controller
'pg.id',
DB::raw("coalesce(pg.namalengkap, '-') as nama"),
DB::raw("coalesce(pg.nip_pns, '-') as nip_pns"),
DB::raw("count(distinct m.id) filter (where p.status='lulus') as lulus_count"),
DB::raw("count(distinct m.id) as lulus_count"),
DB::raw("coalesce((
select string_agg(ms.nama, ', ' order by ms.id)
from public.masterpitstop ms
left join public.praakre px
on px.pegawai_id = pg.id
and px.status = 'lulus'
and px.masterpitstop_id = ms.id::text
where ms.statusenabled = true
and px.id is null
@ -807,7 +803,7 @@ class PitStopController extends Controller
->select([
DB::raw("coalesce(nullif(pl.tipe, ''), '-') as tipe"),
DB::raw('pl.id as pegawai_id'),
DB::raw("count(distinct m.id) filter (where p.status='lulus') as lulus_count"),
DB::raw("count(distinct m.id) as lulus_count"),
])
->groupBy(DB::raw("coalesce(nullif(pl.tipe, ''), '-')"), 'pl.id');
@ -864,14 +860,13 @@ class PitStopController extends Controller
'pl.id',
DB::raw("coalesce(pl.nama, '-') as nama"),
DB::raw("coalesce(pl.nik, '-') as nik"),
DB::raw("count(distinct m.id) filter (where p.status='lulus') as lulus_count"),
DB::raw("count(distinct m.id) as lulus_count"),
DB::raw("coalesce((
select string_agg(ms.nama, ', ' order by ms.id)
from public.masterpitstop ms
left join public.praakre px
on px.pegawai_id = pl.id
and px.tipe_karyawan = 'luar'
and px.status = 'lulus'
and px.masterpitstop_id = ms.id::text
where ms.statusenabled = true
and px.id is null
@ -945,13 +940,12 @@ class PitStopController extends Controller
'pg.id',
DB::raw("coalesce(pg.namalengkap, '-') as nama"),
DB::raw("coalesce(pg.nip_pns, '-') as nip_pns"),
DB::raw("count(distinct m.id) filter (where p.status='lulus') as lulus_count"),
DB::raw("count(distinct m.id) as lulus_count"),
DB::raw("coalesce((
select count(*)
from public.masterpitstop ms
left join public.praakre px
on px.pegawai_id = pg.id
and px.status = 'lulus'
and px.masterpitstop_id = ms.id::text
where ms.statusenabled = true
and px.id is null
@ -961,7 +955,6 @@ class PitStopController extends Controller
from public.masterpitstop ms
left join public.praakre px
on px.pegawai_id = pg.id
and px.status = 'lulus'
and px.masterpitstop_id = ms.id::text
where ms.statusenabled = true
and px.id is null
@ -1040,14 +1033,13 @@ class PitStopController extends Controller
'pl.id',
DB::raw("coalesce(pl.nama, '-') as nama"),
DB::raw("coalesce(pl.nik, '-') as nik"),
DB::raw("count(distinct m.id) filter (where p.status='lulus') as lulus_count"),
DB::raw("count(distinct m.id) as lulus_count"),
DB::raw("coalesce((
select count(*)
from public.masterpitstop ms
left join public.praakre px
on px.pegawai_id = pl.id
and px.tipe_karyawan = 'luar'
and px.status = 'lulus'
and px.masterpitstop_id = ms.id::text
where ms.statusenabled = true
and px.id is null
@ -1058,7 +1050,6 @@ class PitStopController extends Controller
left join public.praakre px
on px.pegawai_id = pl.id
and px.tipe_karyawan = 'luar'
and px.status = 'lulus'
and px.masterpitstop_id = ms.id::text
where ms.statusenabled = true
and px.id is null
@ -1142,13 +1133,12 @@ class PitStopController extends Controller
DB::raw("coalesce(pg.namalengkap, '-') as nama"),
DB::raw("coalesce(pg.nip_pns, '-') as identitas"),
DB::raw("coalesce(ukp.name, '-') as unit_name"),
DB::raw("count(distinct m.id) filter (where p.status='lulus') as lulus_count"),
DB::raw("count(distinct m.id) as lulus_count"),
DB::raw("coalesce((
select count(*)
from public.masterpitstop ms
left join public.praakre px
on px.pegawai_id = pg.id
and px.status = 'lulus'
and px.masterpitstop_id = ms.id::text
where ms.statusenabled = true
and px.id is null
@ -1158,7 +1148,6 @@ class PitStopController extends Controller
from public.masterpitstop ms
left join public.praakre px
on px.pegawai_id = pg.id
and px.status = 'lulus'
and px.masterpitstop_id = ms.id::text
where ms.statusenabled = true
and px.id is null
@ -1183,14 +1172,13 @@ class PitStopController extends Controller
DB::raw("coalesce(pl.nama, '-') as nama"),
DB::raw("coalesce(pl.nik, '-') as identitas"),
DB::raw("coalesce(nullif(pl.tipe, ''), '-') as unit_name"),
DB::raw("count(distinct m.id) filter (where p.status='lulus') as lulus_count"),
DB::raw("count(distinct m.id) as lulus_count"),
DB::raw("coalesce((
select count(*)
from public.masterpitstop ms
left join public.praakre px
on px.pegawai_id = pl.id
and px.tipe_karyawan = 'luar'
and px.status = 'lulus'
and px.masterpitstop_id = ms.id::text
where ms.statusenabled = true
and px.id is null
@ -1201,7 +1189,6 @@ class PitStopController extends Controller
left join public.praakre px
on px.pegawai_id = pl.id
and px.tipe_karyawan = 'luar'
and px.status = 'lulus'
and px.masterpitstop_id = ms.id::text
where ms.statusenabled = true
and px.id is null
@ -1285,13 +1272,13 @@ class PitStopController extends Controller
->where('m.statusenabled', true);
})
->where('p.pegawai_id', $pegawaiId)
->where('p.status', 'lulus')
// ->where('p.status', 'lulus')
->whereNotNull('m.id')
->select([
'p.id',
'p.masterpitstop_id',
DB::raw("coalesce(m.nama, '-') as step_nama"),
'p.status',
'p.nilai',
DB::raw("to_char(p.created_at, 'DD-MM-YYYY HH24:MI') as created_at"),
])
->orderByDesc('p.id')
@ -1327,14 +1314,13 @@ class PitStopController extends Controller
->where('m.statusenabled', true);
})
->where('p.tipe_karyawan', 'luar')
->where('p.status', 'lulus')
->where('p.pegawai_id', $pegawaiId)
->whereNotNull('m.id')
->select([
'p.id',
'p.masterpitstop_id',
DB::raw("coalesce(m.nama, '-') as step_nama"),
'p.status',
'p.nilai',
DB::raw("to_char(p.created_at, 'DD-MM-YYYY HH24:MI') as created_at"),
])
->orderByDesc('p.id')
@ -1364,7 +1350,7 @@ class PitStopController extends Controller
$pegawaiId = (int) $validator->validated()['pegawai_id'];
$lockedSteps = PraAkre::where('pegawai_id', $pegawaiId)
->where('status', 'lulus')
// ->where('status', 'lulus')
->distinct()
->pluck('masterpitstop_id');
@ -1394,7 +1380,7 @@ class PitStopController extends Controller
$lockedSteps = PraAkre::where('pegawai_id', $pegawaiId)
->where('tipe_karyawan', 'luar')
->where('status', 'lulus')
// ->where('status', 'lulus')
->distinct()
->pluck('masterpitstop_id');
@ -1414,9 +1400,10 @@ class PitStopController extends Controller
$validator = Validator::make($request->all(), [
'karyawan_id' => ['required', 'integer'],
'step' => 'required',
'status' => 'required|in:lulus,tidak_lulus',
// 'status' => 'required|in:lulus,tidak_lulus',
'unit_id' => 'nullable|string|max:15',
'tipe_karyawan' => 'nullable|in:internal,luar',
'nilai' => 'required'
]);
if ($validator->fails()) {
@ -1460,13 +1447,13 @@ class PitStopController extends Controller
$alreadyPassed = PraAkre::where('pegawai_id', $pegawaiId)
->where('tipe_karyawan', $tipeKaryawan)
->where('masterpitstop_id', $masterPitstopId)
->where('status', 'lulus')
// ->where('status', 'lulus')
->exists();
if ($alreadyPassed) {
return response()->json([
'error' => 1,
'message' => 'Step ini sudah lulus dan terkunci.',
'message' => 'Pitstop ini sudah terkunci.',
], 409);
}
@ -1474,7 +1461,8 @@ class PitStopController extends Controller
'pegawai_id' => $pegawaiId,
'masterpitstop_id' => $masterPitstopId,
'unit_id' => $tipeKaryawan === 'luar' ? null : ($payload['unit_id'] ?? null),
'status' => $payload['status'],
// 'status' => $payload['status'],
'nilai' => $payload['nilai'],
'tipe_karyawan' => $payload['tipe_karyawan'],
'action_at' => auth()->user()->objectpegawaifk
,
@ -1493,4 +1481,230 @@ class PitStopController extends Controller
]);
}
public function progressDetailGhost(Request $request)
{
$validator = Validator::make($request->all(), [
'pegawai_id' => 'required|integer|exists:pgsql.public.pegawai_m,id',
]);
if ($validator->fails()) {
return response()->json([
'error' => 1,
'message' => 'Validasi gagal.',
'errors' => $validator->errors(),
], 422);
}
$pegawaiId = (int) $validator->validated()['pegawai_id'];
$rows = DB::connection('pgsql')
->table('public.praakre as p')
->leftJoin('public.masterpitstop as m', function ($join) {
$join->on(DB::raw('m.id::text'), '=', 'p.masterpitstop_id')
->where('m.statusenabled', true);
})
->where('p.pegawai_id', $pegawaiId)
->whereNotNull('m.id')
->select([
'p.id',
'p.masterpitstop_id',
DB::raw("coalesce(m.nama, '-') as step_nama"),
'p.nilai',
DB::raw("to_char(p.created_at, 'DD-MM-YYYY HH24:MI') as created_at"),
])
->orderByDesc('p.id')
->limit(500)
->get();
return response()->json([
'error' => 0,
'data' => $rows,
]);
}
public function progressDetailExternalGhost(Request $request)
{
$validator = Validator::make($request->all(), [
'pegawai_id' => 'required|integer|exists:pgsql.public.pegawai_luar_pl,id',
]);
if ($validator->fails()) {
return response()->json([
'error' => 1,
'message' => 'Validasi gagal.',
'errors' => $validator->errors(),
], 422);
}
$pegawaiId = (int) $validator->validated()['pegawai_id'];
$rows = DB::connection('pgsql')
->table('public.praakre as p')
->leftJoin('public.masterpitstop as m', function ($join) {
$join->on(DB::raw('m.id::text'), '=', 'p.masterpitstop_id')
->where('m.statusenabled', true);
})
->where('p.tipe_karyawan', 'luar')
->where('p.status', 'lulus')
->where('p.pegawai_id', $pegawaiId)
->whereNotNull('m.id')
->select([
'p.id',
'p.masterpitstop_id',
DB::raw("coalesce(m.nama, '-') as step_nama"),
'p.status',
DB::raw("to_char(p.created_at, 'DD-MM-YYYY HH24:MI') as created_at"),
])
->orderByDesc('p.id')
->limit(500)
->get();
return response()->json([
'error' => 0,
'data' => $rows,
]);
}
public function dataProgressAllKaryawanGhost(Request $request){
$draw = $this->dtInt($request->input('draw'), 1);
$start = $this->dtInt($request->input('start'), 0);
$length = $this->dtInt($request->input('length'), 10);
if ($length < 1) $length = 10;
if ($length > 200) $length = 200;
$search = trim((string) data_get($request->all(), 'search.value', ''));
$totalSteps = (int) MasterPitStopPraAkre::where('statusenabled', true)->count();
// Internal
$internal = DB::connection('pgsql')
->table('public.mappegawaijabatantounitkerja_m as mp')
->join('public.pegawai_m as pg', 'pg.id', '=', 'mp.objectpegawaifk')
->join('public.unitkerjapegawai_m as ukp', 'ukp.id', '=', 'mp.objectunitkerjapegawaifk')
->leftJoin('public.praakre as p', 'p.pegawai_id', '=', 'pg.id')
->leftJoin('public.masterpitstop as m', function ($join) {
$join->on(DB::raw('m.id::text'), '=', 'p.masterpitstop_id')
->where('m.statusenabled', true);
})
->where('mp.statusenabled', true)
->where('mp.isprimary', true)
->where('pg.statusenabled', true)
->where('pg.kedudukanfk', 1)
->where('ukp.statusenabled', true)
->select([
DB::raw("'internal' as tipe_karyawan"),
'pg.id',
DB::raw("coalesce(pg.namalengkap, '-') as nama"),
DB::raw("coalesce(ukp.name, '-') as unit_name"),
DB::raw("count(distinct m.id) as lulus_count"),
DB::raw("coalesce((
select count(*)
from public.masterpitstop ms
left join public.praakre px
on px.pegawai_id = pg.id
and px.masterpitstop_id = ms.id::text
where ms.statusenabled = true
and px.id is null
), 0) as belum_count"),
DB::raw("coalesce((
select string_agg(ms.nama, ', ' order by ms.id)
from public.masterpitstop ms
left join public.praakre px
on px.pegawai_id = pg.id
and px.masterpitstop_id = ms.id::text
where ms.statusenabled = true
and px.id is null
), '') as belum_steps"),
])
->groupBy('pg.id', 'pg.namalengkap', 'pg.nip_pns', 'ukp.name');
// External
$external = DB::connection('pgsql')
->table('public.pegawai_luar_pl as pl')
->leftJoin('public.praakre as p', function ($join) {
$join->on('p.pegawai_id', '=', 'pl.id')
->where('p.tipe_karyawan', 'luar');
})
->leftJoin('public.masterpitstop as m', function ($join) {
$join->on(DB::raw('m.id::text'), '=', 'p.masterpitstop_id')
->where('m.statusenabled', true);
})
->select([
DB::raw("'luar' as tipe_karyawan"),
'pl.id',
DB::raw("coalesce(pl.nama, '-') as nama"),
DB::raw("coalesce(nullif(pl.tipe, ''), '-') as unit_name"),
DB::raw("count(distinct m.id) as lulus_count"),
DB::raw("coalesce((
select count(*)
from public.masterpitstop ms
left join public.praakre px
on px.pegawai_id = pl.id
and px.tipe_karyawan = 'luar'
and px.masterpitstop_id = ms.id::text
where ms.statusenabled = true
and px.id is null
), 0) as belum_count"),
DB::raw("coalesce((
select string_agg(ms.nama, ', ' order by ms.id)
from public.masterpitstop ms
left join public.praakre px
on px.pegawai_id = pl.id
and px.tipe_karyawan = 'luar'
and px.masterpitstop_id = ms.id::text
where ms.statusenabled = true
and px.id is null
), '') as belum_steps"),
])
->groupBy('pl.id', 'pl.nama', 'pl.nik', DB::raw("coalesce(nullif(pl.tipe, ''), '-')"));
$union = $internal->unionAll($external);
$baseNoSearch = DB::connection('pgsql')->query()->fromSub($union, 'u');
$base = DB::connection('pgsql')->query()->fromSub($union, 'u');
if ($search !== '') {
$base->where(function ($q) use ($search) {
$q->where('u.nama', 'ILIKE', '%' . $search . '%')
->orWhere('u.unit_name', 'ILIKE', '%' . $search . '%')
->orWhere('u.tipe_karyawan', 'ILIKE', '%' . $search . '%');
});
}
$recordsTotal = DB::connection('pgsql')->query()->fromSub($baseNoSearch, 't')->count();
$recordsFiltered = $search === ''
? $recordsTotal
: DB::connection('pgsql')->query()->fromSub($base, 't')->count();
$rows = $base
->orderByDesc('lulus_count')
->orderBy('nama')
->offset($start)
->limit($length)
->get();
$data = $rows->map(function ($r) use ($totalSteps) {
$lulus = (int) ($r->lulus_count ?? 0);
$pct = $totalSteps > 0 ? round(($lulus / $totalSteps) * 100, 1) : 0;
return [
'tipe_karyawan' => (string) ($r->tipe_karyawan ?? 'internal'),
'id' => (int) ($r->id ?? 0),
'nama' => (string) ($r->nama ?? '-'),
'unit_name' => (string) ($r->unit_name ?? '-'),
'lulus_count' => $lulus,
'pct' => $pct,
'selesai' => $totalSteps > 0 ? ($lulus >= $totalSteps ? 1 : 0) : 0,
'belum_count' => (int) ($r->belum_count ?? 0),
'belum_steps' => (string) ($r->belum_steps ?? ''),
];
})->values();
return response()->json([
'draw' => $draw,
'recordsTotal' => $recordsTotal,
'recordsFiltered' => $recordsFiltered,
'data' => $data,
]);
}
}

View File

@ -2,8 +2,49 @@
@section('custom_css')
<style>
.table td, .table th {
vertical-align: middle !important;
}
.progress {
background-color: #e9ecef;
}
.container-fluid {
padding: 0 10px !important;
padding: 0 !important;
}
.left-panel {
background: linear-gradient(135deg, #eef2f7, #f8f9fb);
min-height: 100vh;
padding: 30px;
}
.right-panel {
background: #ffffff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.card-custom {
border-radius: 12px;
box-shadow: 0 8px 25px rgba(0,0,0,0.08);
border: none;
}
.login-card {
width: 100%;
max-width: 400px;
}
.progress {
border-radius: 10px;
overflow: hidden;
}
.table thead th {
font-size: 12px;
letter-spacing: 0.5px;
}
@media (max-width: 575.98px) {
@ -17,15 +58,36 @@
@section('content')
<div class="container-fluid">
<div class="row">
<div class="col-md-8 hide-xs bg-secondary vh-100">
<div class="col-md-8 left-panel d-flex align-items-center justify-content-center">
<div class="d-flex align-items-center justify-content-center h-75 flex-column">
<img src="{{ asset('metronic/assets/media/illustrations/sigma-1/10.png') }}" alt="" srcset="" class="h-75">
<div class="mt-5">
<span class="fs-1 fw-bold" id="title_selamat_datang"></span>
<div class="card h-150 flex-column">
<div class="card-body table-responsive">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0">Monitoring Pra Akreditasi</h4>
<input type="text" id="searchAllKaryawan"
class="form-control w-250px"
placeholder="Cari karyawan...">
</div>
<table id="tblAllKaryawan" class="table align-middle table-row-dashed fs-6 gy-3 w-100">
<thead>
<tr class="text-muted fw-semibold fs-7 text-uppercase gs-0">
<th>Nama</th>
<th>Unit / Tipe</th>
<th style="width: 320px">Progress</th>
<th>Belum dikerjakan</th>
<th class="text-end">Jenis</th>
<th class="text-end">Aksi</th>
</tr>
</thead>
<tbody class="fw-semibold text-gray-800"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-md-4 py-10 px-5">
<div class="col-md-4 right-panel">
<div class="d-flex align-items-center justify-content-center h-100 flex-column">
<center>
<img src="{{ asset('assets/img/logo-fullname.png') }}" alt="" srcset="" class="w-50">
@ -61,95 +123,297 @@
</div>
@endsection
<div class="modal fade" id="modalAllDetail" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Detail Progress</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="text-muted mb-4" id="allDetailNama"></div>
<div class="table-responsive">
<table class="table align-middle table-row-dashed fs-6 gy-3">
<thead>
<tr class="text-muted fw-semibold fs-7 text-uppercase gs-0">
<th>Nama Step</th>
<th>Nilai</th>
<th class="text-end">Waktu</th>
</tr>
</thead>
<tbody class="fw-semibold text-gray-800" id="allDetailTable">
<tr>
<td colspan="4" class="text-center text-muted py-6">Memuat data...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="modalBelum" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">PitStop Belum Dikerjakan</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="text-dark fw-semibold mb-4" id="belumNama"></div>
<ul id="belumList" class="mb-0 ps-5"></ul>
</div>
</div>
</div>
</div>
@section('custom_js')
<script>
@if (session('error'))
toastr.error("{{ session('error') }}");
@endif
function escapeHtml(text) {
return $('<div>').text(text).html();
}
$(document).ready(function () {
$(document).ready(function() {
$('#togglePassword').on('click', function () {
let input = $('#password');
let icon = $(this).find('i');
// =========================
// TOGGLE PASSWORD
// =========================
$('#togglePassword').on('click', function () {
let input = $('#password');
let icon = $(this).find('i');
if (input.attr('type') === 'password') {
input.attr('type', 'text');
icon.removeClass('fa-eye').addClass('fa-eye-slash');
} else {
input.attr('type', 'password');
icon.removeClass('fa-eye-slash').addClass('fa-eye');
if (input.attr('type') === 'password') {
input.attr('type', 'text');
icon.removeClass('fa-eye').addClass('fa-eye-slash');
} else {
input.attr('type', 'password');
icon.removeClass('fa-eye-slash').addClass('fa-eye');
}
});
// =========================
// TOTAL STEP
// =========================
const totalSteps = Number(@json((int) ($totalSteps ?? 0)));
// =========================
// ESCAPE HTML (WAJIB ADA)
// =========================
// =========================
// FORM VALIDATION (FIXED)
// =========================
const form = document.getElementById('form_login');
var validator = FormValidation.formValidation(form, {
fields: {
'namauser': {
validators: {
notEmpty: {
message: 'Username masih kosong'
}
}
},
'password': {
validators: {
notEmpty: {
message: 'Password masih kosong'
}
}
}
},
plugins: {
trigger: new FormValidation.plugins.Trigger(),
bootstrap: new FormValidation.plugins.Bootstrap5({
rowSelector: '.fv-row'
})
}
});
$('#form_permintaan_submit').on('click', function (e) {
e.preventDefault();
validator.validate().then(function (status) {
if (status === 'Valid') {
form.submit();
}
});
});
var typed = new Typed("#title_selamat_datang", {
strings: ["Selamat Datang", "Di Halaman Admin", "Permintaan IT", "Silahkan Masukkan Username Dan Password", "Jika Belum Memiliki Akun", "Silahkan Hubungi Tim IT"],
typeSpeed: 30,
});
// Define form element
const form = document.getElementById('form_login');
const token = $('meta[name="csrf-token"]').attr('content');
// =========================
// DATATABLE INIT (FIXED)
// =========================
const allTable = $('#tblAllKaryawan').DataTable({
processing: true,
serverSide: true,
searchDelay: 350,
dom: 'rtip',
pageLength: 10,
order: [[2, 'desc']],
ajax: {
url: '/data/progress-all-karyawan-ghost',
type: 'GET',
},
columns: [
{
data: null,
render: function (row) {
const nama = escapeHtml(row.nama ?? '-');
// const idt = escapeHtml(row.identitas ?? '-');
// const label = row.tipe_karyawan === 'luar' ? 'NIK' : 'NIP';
// Init form validation rules. For more info check the FormValidation plugin's official documentation:https://formvalidation.io/
var validator = FormValidation.formValidation(
form,
{
fields: {
'username': {
validators: {
notEmpty: {
message: 'Username masih kosong'
}
}
},
'password': {
validators: {
notEmpty: {
message: 'Password masih kosong'
}
}
},
},
plugins: {
trigger: new FormValidation.plugins.Trigger(),
bootstrap: new FormValidation.plugins.Bootstrap5({
rowSelector: '.fv-row',
eleInvalidClass: '',
eleValidClass: ''
})
}
}
);
// Submit button handler
const submitButton = document.getElementById('form_permintaan_submit');
submitButton.addEventListener('click', function (e) {
// Prevent default button action
e.preventDefault();
// Validate form before submit
if (validator) {
validator.validate().then(function (status) {
if (status == 'Valid') {
submitButton.setAttribute('data-kt-indicator', 'on');
submitButton.disabled = true;
setTimeout(function () {
submitButton.removeAttribute('data-kt-indicator');
submitButton.disabled = false;
}, 2000);
if (!form.querySelector('input[name="_token"]')) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = '_token';
input.value = token;
form.appendChild(input);
}
form.submit();
return `
<div>
<div class="fw-bold">${nama}</div>
</div>
`;
}
});
}
},
{
data: 'unit_name',
render: data => `<span>${escapeHtml(data ?? '-')}</span>`
},
{
data: null,
orderable: false,
render: function (row) {
const pct = Number(row.pct ?? 0);
return `
<div>
<div class="d-flex justify-content-between small text-muted">
<span>${row.lulus_count ?? 0} / ${totalSteps}</span>
<span>${pct}%</span>
</div>
<div class="progress mt-1" style="height:6px;">
<div class="progress-bar bg-success" style="width:${pct}%"></div>
</div>
</div>
`;
}
},
{
data: null,
className: 'text-center',
orderable: false,
render: function (data, type, row) {
const c = Number(row.belum_count ?? 0);
if (c <= 0) return '<span class="text-muted">-</span>';
return `<a href="#" class="showBelum" data-nama="${escapeHtml(row.nama)}" data-steps="${escapeHtml(row.belum_steps ?? '')}"><span class="badge badge-warning text-dark">${c} pitstop</span></a>`;
},
},
{
data: 'tipe_karyawan',
className: 'text-end',
render: function (data) {
return data === 'luar'
? '<span class="badge bg-warning text-dark">Eksternal</span>'
: '<span class="badge bg-primary text-white">Internal</span>';
}
},
{
data: null,
className: 'text-end',
orderable: false,
render: function (row) {
return `
<button class="btn btn-sm btn-primary viewDetailAll" data-id="${row.id}" data-nama="${row.nama}">
Detail
</button>
`;
}
}
]
});
// =========================
// SEARCH FIX (DEBOUNCE)
// =========================
let timer = null;
$('#searchAllKaryawan').on('keyup', function () {
const val = $(this).val();
clearTimeout(timer);
timer = setTimeout(() => {
allTable.search(val).draw();
}, 300);
});
$(document).on('click', '.viewDetailAll', function (e) {
e.preventDefault();
const id = Number($(this).data('id'));
const tipe = String($(this).data('tipe') ?? 'internal');
const nama = String($(this).data('nama') ?? '-');
$('#allDetailNama').text(`${nama}`);
$('#allDetailTable').html('<tr><td colspan="4" class="text-center text-muted py-6">Memuat data...</td></tr>');
const url = tipe === 'luar' ? '/pitstop/progress-detail-external-ghost' : '/pitstop/progress-detail-ghost';
$.get(url, { pegawai_id: id })
.done(function (res) {
renderDetailRows(res?.data ?? []);
})
.fail(function () {
$('#allDetailTable').html('<tr><td colspan="4" class="text-center text-muted py-6">Gagal memuat data</td></tr>');
});
new bootstrap.Modal(document.getElementById('modalAllDetail')).show();
});
const renderDetailRows = (rows) => {
if (!rows.length) {
$('#allDetailTable').html('<tr><td colspan="4" class="text-center text-muted py-6">Belum ada data</td></tr>');
return;
}
const html = rows
.map((r) => {
const status = String(r.status ?? '-');
const badge =
status === 'lulus'
? '<span class="badge badge-light-success">Lulus</span>'
: status === 'tidak_lulus'
? '<span class="badge badge-light-danger">Tidak Lulus</span>'
: `<span class="badge badge-light">${escapeHtml(status)}</span>`;
const waktu = r.created_at ? String(r.created_at) : '-';
return `
<tr>
<td>${escapeHtml(r.step_nama ?? '-')}</td>
<td>${r.nilai}</td>
<td class="text-end">${escapeHtml(waktu)}</td>
</tr>`;
})
.join('');
$('#allDetailTable').html(html);
};
});
$(document).on('click', '.showBelum', function (e) {
e.preventDefault();
const nama = $(this).data('nama');
const nip = $(this).data('nip');
const stepsRaw = String($(this).data('steps') ?? '').trim();
$('#belumNama').text(nama + ' (NIP ' + nip +')');
if (!stepsRaw) {
$('#belumList').html('<li class="text-muted">Tidak ada data.</li>');
} else {
const steps = stepsRaw.split(',').map((s) => s.trim()).filter(Boolean);
const html = steps.map((s) => `<li><span class="fw-semibold">${escapeHtml(s)}</span></li>`).join('');
$('#belumList').html(html || '<li class="text-muted">Tidak ada data.</li>');
}
new bootstrap.Modal(document.getElementById('modalBelum')).show();
});
</script>
@endsection

View File

@ -44,11 +44,11 @@
<div class="d-flex flex-wrap gap-6">
<label class="form-check form-check-sm form-check-custom form-check-solid mb-0">
<input class="form-check-input" type="radio" name="karyawan_type" id="karyawanTypeInternal" value="internal" checked />
<span class="form-check-label fw-semibold">Internal</span>
<span class="form-check-label fw-semibold">Karyawan Internal</span>
</label>
<label class="form-check form-check-sm form-check-custom form-check-solid mb-0">
<input class="form-check-input" type="radio" name="karyawan_type" id="karyawanTypeLuar" value="luar" />
<span class="form-check-label fw-semibold">Karyawan Luar</span>
<span class="form-check-label fw-semibold">Karyawan Eksternal</span>
</label>
</div>
<div class="form-text">Mode pencarian akan menyesuaikan tipe karyawan.</div>
@ -97,7 +97,7 @@
<div class="d-flex flex-stack flex-grow-1">
<div class="fw-semibold">
<div class="text-gray-900 fw-bold">Info</div>
<div class="text-gray-700">Step akan terkunci jika sudah berstatus lulus.</div>
<div class="text-gray-700">Data pitstop yang telah diinput tidak akan ditampilkan kembali.</div>
</div>
</div>
</div>
@ -113,8 +113,22 @@
</select>
<div class="form-text" id="stepHint"></div>
</div>
<div class="mb-2">
<div class="mb-4">
<label for="nilai" class="form-label" id="label_nilai">Nilai (0-100)</label>
<input
type="number"
id="nilai"
name="nilai"
class="form-control"
min="0"
max="100"
step="1"
inputmode="numeric"
autocomplete="off"
required
/>
</div>
{{-- <div class="mb-2">
<label class="form-label d-block">Status</label>
<div class="d-flex flex-wrap gap-6">
<label class="form-check form-check-sm form-check-custom form-check-solid">
@ -126,7 +140,7 @@
<span class="form-check-label">Tidak Lulus</span>
</label>
</div>
</div>
</div> --}}
</div>
<div class="modal-footer">
@ -263,7 +277,7 @@
const availableCount = $step.find('option').length;
if (!availableCount) {
$form.find('button[type="submit"]').prop('disabled', true);
$stepHint.html('<span class="text-success fw-semibold">Semua step sudah lulus</span>');
$stepHint.html('<span class="text-success fw-semibold">Semua step sudah selesai dikerjakan</span>');
}
})
.fail(function () {
@ -304,7 +318,7 @@
$('#nip').val($(this).data('nip'));
$('#karyawan_id').val($(this).data('id'));
lockExistingSteps($(this).data('id'));
getKaryawanType() === 'luar' ? $("#label_nip").text('NIK') : $("#label_nip").text('NIP')
getKaryawanType() === 'luar' ? $('#label_nip').text('NIK') : $('#label_nip').text('NIP');
new bootstrap.Modal(document.getElementById('modalPitstop')).show();
});
@ -324,11 +338,27 @@
return;
}
const rawNilai = String($('#nilai').val() ?? '').trim();
const nilai = rawNilai === '' ? NaN : Number(rawNilai);
if (!Number.isFinite(nilai) || nilai < 0 || nilai > 100) {
Swal.fire({
toast: true,
position: 'top-end',
icon: 'warning',
title: 'Nilai harus di antara 0 sampai 100.',
showConfirmButton: false,
timer: 2400,
timerProgressBar: true,
});
return;
}
const payload = {
karyawan_id: $('#karyawan_id').val(),
step: $('#step').val(),
status: $('input[name="status"]:checked').val(),
tipe_karyawan: getKaryawanType()
// status: $('input[name="status"]:checked').val(),
nilai: nilai,
tipe_karyawan: getKaryawanType(),
};
$.ajax({
url: '/pitstop/submit',

View File

@ -4,25 +4,139 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ $title ?? 'Monitoring Pra Akreditasi' }}</title>
<style>
/* * { font-family: DejaVu Sans, sans-serif; } */
body { font-size: 10px; color: #111827; }
.h1 { font-size: 16px; font-weight: 700; margin: 0; }
.meta { margin-top: 4px; color: #6b7280; font-size: 10px; }
/* Jangan paksa 1 unit utuh di 1 halaman (bisa bikin halaman 1 kosong) */
.unit { margin-top: 14px; }
.unit-head { padding: 8px 10px; border: 1px solid #e5e7eb; background: #f9fafb; }
.unit-title { font-size: 12px; font-weight: 700; margin: 0; }
.unit-meta { margin-top: 3px; color: #6b7280; font-size: 10px; }
.table { width: 100%; border-collapse: collapse; margin-top: 8px; }
.table th, .table td { border: 1px solid #e5e7eb; padding: 6px; vertical-align: top; }
.table th { background: #f9fafb; text-align: left; font-weight: 700; }
.text-right { text-align: right; }
.badge { display: inline-block; padding: 2px 6px; border-radius: 10px; font-size: 10px; background: #eef2ff; color: #3730a3; }
.badge-ok { background: #dcfce7; color: #166534; }
.badge-warn { background: #fff7ed; color: #9a3412; }
.muted { color: #6b7280; }
</style>
<style>
/* ===== GLOBAL ===== */
* { box-sizing: border-box; }
body {
margin: 0;
font-family: DejaVu Sans, Arial, sans-serif;
font-size: 12px;
line-height: 1.35;
color: #111827;
font-weight: 400;
}
/* ===== HEADER ===== */
.h1 {
font-size: 16px;
font-weight: 700;
margin: 0;
}
.meta {
margin-top: 4px;
color: #6b7280;
font-size: 10.5px;
}
/* ===== UNIT ===== */
.unit {
margin-top: 16px;
page-break-inside: auto;
}
.unit-head {
padding: 10px 12px;
border: 1px solid #e5e7eb;
background: #f3f4f6;
}
.unit-title {
font-size: 13px;
font-weight: 700;
}
.unit-meta {
margin-top: 4px;
font-size: 11px;
color: #6b7280;
}
/* ===== TABLE ===== */
.table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
margin-top: 10px;
}
.table th,
.table td {
border: 1px solid #e5e7eb;
padding: 7px 9px;
vertical-align: top;
word-wrap: break-word;
overflow-wrap: break-word;
}
.col-no {
font-size: 9px;
padding: 3px 4px;
width: 18px !important;
min-width: 18px !important;
max-width: 18px !important;
}
.table th {
background: #f9fafb;
font-weight: 700;
font-size: 12px;
}
.table td {
font-size: 12px;
font-weight: 400;
}
/* Keep header repeated on page-break (PDF renderers) */
thead { display: table-header-group; }
tfoot { display: table-footer-group; }
/* Avoid making tables "unbreakable" (can push content to next page) */
tr { page-break-inside: auto; }
td, th { page-break-inside: auto; }
.unit-head { page-break-inside: avoid; }
/* Zebra biar enak dibaca */
.table tr:nth-child(even) {
background: #fafafa;
}
/* ===== ALIGN ===== */
.text-right {
text-align: right;
}
.nowrap {
white-space: nowrap;
}
/* ===== BADGE ===== */
.badge {
display: inline-block;
padding: 3px 8px;
border-radius: 12px;
font-size: 10.5px;
font-weight: 600;
}
.badge-ok {
background: #dcfce7;
color: #166534;
}
.badge-warn {
background: #fff7ed;
color: #9a3412;
}
.muted {
color: #6b7280;
}
/* ===== PRINT FIX ===== */
@page {
margin: 20px;
}
</style>
</head>
<body>
<div>
@ -30,32 +144,34 @@
<div class="meta">
Dicetak: {{ optional($generatedAt)->format('Y-m-d H:i') }}
</div>
<div class="meta">
Total PitStop Aktif: {{ (int) ($totalSteps ?? 0) }}
</div>
</div>
@if (($isSummaryOnly ?? false) === true)
<table class="table" style="margin-top: 14px;">
<colgroup>
<col style="width: 18px">
<col>
<col style="width: 110px">
</colgroup>
<thead>
<tr>
<th style="width: 28px">No</th>
<th>Unit</th>
<th class="text-right" style="width: 110px">Karyawan Selesai / Total Karyawan</th>
<th style="width: 5px;" class="nowrap col-no">No</th>
<th style="width: 70%;">Unit</th>
<th class="text-right nowrap" style="width: 25%;">Karyawan Selesai / Total</th>
</tr>
</thead>
<tbody>
@forelse ($units ?? [] as $i => $u)
<tr>
<td class="text-right">{{ $i + 1 }}</td>
<td class="">{{ $i + 1 }}</td>
<td>
<div style="font-weight: 700;">{{ $u->unit_name }}</div>
{{ $u->unit_name }}
</td>
<td class="text-right">{{ number_format((int) $u->pegawai_selesai) }}/{{ number_format((int) $u->total_pegawai) }}</td>
<td class="text-right nowrap">{{ number_format((int) $u->pegawai_selesai) }}/{{ number_format((int) $u->total_pegawai) }}</td>
</tr>
@empty
<tr>
<td colspan="5" class="muted">Tidak ada data.</td>
<td colspan="3" class="muted">Tidak ada data.</td>
</tr>
@endforelse
</tbody>
@ -72,13 +188,20 @@
</div>
<table class="table">
<colgroup>
<col style="width: 18px">
<col style="width: 210px">
<col style="width: 120px">
<col style="width: 120px">
<col>
</colgroup>
<thead>
<tr>
<th style="width: 28px">No</th>
<th style="width: 210px">Nama</th>
<th style="width: 120px">NIP</th>
<th class="text-right" style="width: 120px">Lulus / Total PitStop</th>
<th>PitStop Belum Selesai</th>
<th style="width: 5%;" class="nowrap col-no">No</th>
<th style="width: 25%;">Nama</th>
<th style="width: 15%;" class="nowrap">NIP</th>
<th class="text-right nowrap" style="width: 15%;">Lulus / Total PitStop</th>
<th style="width: 40%;">PitStop Belum Selesai</th>
</tr>
</thead>
<tbody>
@ -87,15 +210,15 @@
$selesai = ((int) ($p->total_steps ?? 0)) > 0 && ((int) ($p->lulus_steps ?? 0)) >= ((int) ($p->total_steps ?? 0));
@endphp
<tr>
<td class="text-right">{{ $i + 1 }}</td>
<td class="text-right nowrap col-no">{{ $i + 1 }}</td>
<td>{{ $p->nama }}</td>
<td>{{ $p->nip_pns }}</td>
<td class="text-right">{{ number_format((int) $p->lulus_steps) }}/{{ number_format((int) $p->total_steps) }}</td>
<td class="nowrap">{{ $p->nip_pns }}</td>
<td class="text-right nowrap">{{ number_format((int) $p->lulus_steps) }}/{{ number_format((int) $p->total_steps) }}</td>
<td>{{ $selesai ? '-' : ($p->belum_selesai_steps !== '' ? $p->belum_selesai_steps : '-') }}</td>
</tr>
@empty
<tr>
<td colspan="7" class="muted">Tidak ada data pegawai.</td>
<td colspan="5" class="muted">Tidak ada data pegawai.</td>
</tr>
@endforelse
</tbody>

View File

@ -1,53 +1,174 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ $title ?? 'Monitoring Karyawan Luar' }}</title>
<style>
* { font-family: DejaVu Sans, sans-serif; }
body { font-size: 10px; color: #111827; }
.h1 { font-size: 16px; font-weight: 700; margin: 0; }
.meta { margin-top: 4px; color: #6b7280; font-size: 10px; }
.section { margin-top: 14px; }
.section-head { padding: 8px 10px; border: 1px solid #e5e7eb; background: #f9fafb; }
.section-title { font-size: 12px; font-weight: 700; margin: 0; }
.section-meta { margin-top: 3px; color: #6b7280; font-size: 10px; }
.table { width: 100%; border-collapse: collapse; margin-top: 8px; }
.table th, .table td { border: 1px solid #e5e7eb; padding: 6px; vertical-align: top; }
.table th { background: #f9fafb; text-align: left; font-weight: 700; }
.text-right { text-align: right; }
.muted { color: #6b7280; }
</style>
<title>{{ $title ?? 'Monitoring Pra Akreditasi' }}</title>
<style>
/* ===== GLOBAL ===== */
* { box-sizing: border-box; }
body {
margin: 0;
font-family: DejaVu Sans, Arial, sans-serif;
font-size: 12px;
line-height: 1.35;
color: #111827;
font-weight: 400;
}
/* ===== HEADER ===== */
.h1 {
font-size: 16px;
font-weight: 700;
margin: 0;
}
.meta {
margin-top: 4px;
color: #6b7280;
font-size: 10.5px;
}
/* ===== UNIT ===== */
.unit {
margin-top: 16px;
page-break-inside: auto;
}
.unit-head {
padding: 10px 12px;
border: 1px solid #e5e7eb;
background: #f3f4f6;
}
.unit-title {
font-size: 13px;
font-weight: 700;
}
.unit-meta {
margin-top: 4px;
font-size: 11px;
color: #6b7280;
}
/* ===== TABLE ===== */
.table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
margin-top: 10px;
}
.table th,
.table td {
border: 1px solid #e5e7eb;
padding: 7px 9px;
vertical-align: top;
word-wrap: break-word;
overflow-wrap: break-word;
}
.col-no {
font-size: 9px;
padding: 3px 4px;
width: 18px !important;
min-width: 18px !important;
max-width: 18px !important;
}
.table th {
background: #f9fafb;
font-weight: 700;
font-size: 12px;
}
.table td {
font-size: 12px;
font-weight: 400;
}
/* Keep header repeated on page-break (PDF renderers) */
thead { display: table-header-group; }
tfoot { display: table-footer-group; }
/* Avoid making tables "unbreakable" (can push content to next page) */
tr { page-break-inside: auto; }
td, th { page-break-inside: auto; }
.unit-head { page-break-inside: avoid; }
/* Zebra biar enak dibaca */
.table tr:nth-child(even) {
background: #fafafa;
}
/* ===== ALIGN ===== */
.text-right {
text-align: right;
}
.nowrap {
white-space: nowrap;
}
/* ===== BADGE ===== */
.badge {
display: inline-block;
padding: 3px 8px;
border-radius: 12px;
font-size: 10.5px;
font-weight: 600;
}
.badge-ok {
background: #dcfce7;
color: #166534;
}
.badge-warn {
background: #fff7ed;
color: #9a3412;
}
.muted {
color: #6b7280;
}
/* ===== PRINT FIX ===== */
@page {
margin: 20px;
}
</style>
</head>
<body>
<div>
<div class="h1">{{ $title ?? 'Monitoring Karyawan Luar' }}</div>
<div class="h1">{{ $title ?? 'Monitoring Pra Akreditasi' }}</div>
<div class="meta">
Dicetak: {{ optional($generatedAt)->format('Y-m-d H:i') }}
</div>
<div class="meta">
Total PitStop Aktif: {{ (int) ($totalSteps ?? 0) }}
</div>
</div>
@if (($isSummaryOnly ?? false) === true)
<table class="table" style="margin-top: 14px;">
<colgroup>
<col style="width: 18px">
<col>
<col style="width: 110px">
</colgroup>
<thead>
<tr>
<th style="width: 28px">No</th>
<th>Tipe</th>
<th class="text-right" style="width: 130px">Selesai / Total</th>
<th style="width: 5%;" class="nowrap col-no">No</th>
<th style="width: 80%;">Tipe</th>
<th class="text-right nowrap" style="width: 15%;">Karyawan Selesai / Total</th>
</tr>
</thead>
<tbody>
@forelse ($types ?? [] as $i => $t)
<tr>
<td class="text-right">{{ $i + 1 }}</td>
<td class="text-right nowrap col-no">{{ $i + 1 }}</td>
<td>
<div style="font-weight: 700;">{{ $t->tipe }}</div>
{{ $t->tipe }}
</td>
<td class="text-right">{{ number_format((int) $t->pegawai_selesai) }}/{{ number_format((int) $t->total_pegawai) }}</td>
<td class="text-right nowrap">{{ number_format((int) $t->pegawai_selesai) }}/{{ number_format((int) $t->total_pegawai) }}</td>
</tr>
@empty
<tr>
@ -58,23 +179,30 @@
</table>
@else
@forelse ($types ?? [] as $t)
<div class="section">
<div class="section-head">
<div class="section-title">{{ $t->tipe }}</div>
<div class="section-meta">
<div class="unit">
<div class="unit-head">
<div class="unit-title">{{ $t->tipe }}</div>
<div class="unit-meta">
Total Karyawan: <b>{{ number_format((int) $t->total_pegawai) }}</b>
&nbsp;|&nbsp; Karyawan Selesai: <b>{{ number_format((int) $t->pegawai_selesai) }}</b>
</div>
</div>
<table class="table">
<colgroup>
<col style="width: 10%;">
<col style="width: 25%;">
<col style="width: 25%;">
<col style="width: 40%;">
<col>
</colgroup>
<thead>
<tr>
<th style="width: 28px">No</th>
<th style="width: 230px">Nama</th>
<th style="width: 140px">NIK</th>
<th class="text-right" style="width: 120px">Lulus / Total PitStop</th>
<th>PitStop Belum Selesai</th>
<th style="width: 5%;" class="">No</th>
<th style="width: 25%;">Nama</th>
<th style="width: 10%;" class="">NIK</th>
<th style="width: 15%;">Lulus / Total PitStop</th>
<th style="width: 45%;">PitStop Belum Selesai</th>
</tr>
</thead>
<tbody>
@ -83,15 +211,15 @@
$selesai = ((int) ($p->total_steps ?? 0)) > 0 && ((int) ($p->lulus_steps ?? 0)) >= ((int) ($p->total_steps ?? 0));
@endphp
<tr>
<td class="text-right">{{ $i + 1 }}</td>
<td class="text-right nowrap col-no">{{ $i + 1 }}</td>
<td>{{ $p->nama }}</td>
<td>{{ $p->nik }}</td>
<td class="text-right">{{ number_format((int) $p->lulus_steps) }}/{{ number_format((int) $p->total_steps) }}</td>
<td class="nowrap">{{ $p->nik }}</td>
<td class="text-right nowrap">{{ number_format((int) $p->lulus_steps) }}/{{ number_format((int) $p->total_steps) }}</td>
<td>{{ $selesai ? '-' : ($p->belum_selesai_steps !== '' ? $p->belum_selesai_steps : '-') }}</td>
</tr>
@empty
<tr>
<td colspan="5" class="muted">Tidak ada data karyawan.</td>
<td colspan="5" class="muted">Tidak ada data pegawai.</td>
</tr>
@endforelse
</tbody>
@ -103,4 +231,3 @@
@endif
</body>
</html>

View File

@ -73,7 +73,7 @@
<thead>
<tr class="text-muted fw-semibold fs-7 text-uppercase gs-0">
<th>Nama Step</th>
<th>Status</th>
<th>Nilai</th>
<th class="text-end">Waktu</th>
</tr>
</thead>
@ -129,19 +129,12 @@
const html = rows
.map((r) => {
const status = String(r.status ?? '-');
const badge =
status === 'lulus'
? '<span class="badge badge-light-success">Lulus</span>'
: status === 'tidak_lulus'
? '<span class="badge badge-light-danger">Tidak Lulus</span>'
: `<span class="badge badge-light">${escapeHtml(status)}</span>`;
const waktu = r.created_at ? String(r.created_at) : '-';
return `
<tr>
<td>${escapeHtml(r.step_nama ?? '-')}</td>
<td>${badge}</td>
<td>${r.nilai}</td>
<td class="text-end">${escapeHtml(waktu)}</td>
</tr>`;
})

View File

@ -20,7 +20,7 @@
<a class="dropdown-item" href="/monitoring-pra-akreditasi/pdf">Internal</a>
</li>
<li>
<a class="dropdown-item" href="/monitoring-pra-akreditasi/pdf-external">External</a>
<a class="dropdown-item" href="/monitoring-pra-akreditasi/pdf-external">Eksternal</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
@ -30,7 +30,7 @@
<a class="dropdown-item" href="/monitoring-pra-akreditasi/excel">Internal</a>
</li>
<li>
<a class="dropdown-item" href="/monitoring-pra-akreditasi/excel-external">External</a>
<a class="dropdown-item" href="/monitoring-pra-akreditasi/excel-external">Eksternal</a>
</li>
</ul>
</div>
@ -157,7 +157,7 @@
<thead>
<tr class="text-muted fw-semibold fs-7 text-uppercase gs-0">
<th>Nama Step</th>
<th>Status</th>
<th>Nilai</th>
<th class="text-end">Waktu</th>
</tr>
</thead>
@ -222,7 +222,7 @@
return `
<tr>
<td>${escapeHtml(r.step_nama ?? '-')}</td>
<td>${badge}</td>
<td>${r.nilai}</td>
<td class="text-end">${escapeHtml(waktu)}</td>
</tr>`;
})
@ -422,7 +422,7 @@
render: function (data) {
const tipe = String(data ?? 'internal');
return tipe === 'luar'
? '<span class="badge badge-light-warning text-dark">External</span>'
? '<span class="badge badge-light-warning text-dark">Eksternal</span>'
: '<span class="badge badge-light-primary">Internal</span>';
},
},

View File

@ -73,7 +73,7 @@
<thead>
<tr class="text-muted fw-semibold fs-7 text-uppercase gs-0">
<th>Nama Step</th>
<th>Status</th>
<th>Nilai</th>
<th class="text-end">Waktu</th>
</tr>
</thead>
@ -139,7 +139,7 @@
return `
<tr>
<td>${escapeHtml(r.step_nama ?? '-')}</td>
<td>${badge}</td>
<td>${r.nilai}</td>
<td class="text-end">${escapeHtml(waktu)}</td>
</tr>`;
})

View File

@ -20,7 +20,7 @@ Route::middleware(['auth'])->group(function(){
Route::get('/monitoring-pra-akreditasi/pdf-external', [PitStopController::class, 'monitoringPdfExternal']);
Route::get('/monitoring-pra-akreditasi/excel', [PitStopController::class, 'monitoringExcel']);
Route::get('/monitoring-pra-akreditasi/excel-external', [PitStopController::class, 'monitoringExcelExternal']);
Route::get('/pitstop/progress-unit/{unit_id}', [PitStopController::class, 'progressUnitDetail']);
Route::get('/pitstop/progress-external/{tipe}', [PitStopController::class, 'progressExternalDetail']);
Route::get('/data/progress-Internal', [PitStopController::class, 'dataProgress']);
@ -48,3 +48,6 @@ Route::middleware(['auth'])->group(function(){
Route::get('/login', [AuthController::class, 'login'])->name('login');
Route::post('/login', [AuthController::class, 'submitLogin']);
Route::get('/data/progress-all-karyawan-ghost', [PitStopController::class, 'dataProgressAllKaryawanGhost']);
Route::get('/pitstop/progress-detail-ghost', [PitStopController::class, 'progressDetailGhost']);
Route::get('/pitstop/progress-detail-external-ghost', [PitStopController::class, 'progressDetailExternal']);