diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php new file mode 100644 index 0000000..f894014 --- /dev/null +++ b/app/Http/Controllers/AuthController.php @@ -0,0 +1,37 @@ +validate([ + 'namauser' => 'required', + 'password' => 'required' + ]); + $user = User::where('namauser', $request->namauser)->first(); + + if ($user && $user->passcode === sha1($request->password)) { + auth()->login($user); + $request->session()->regenerate(); + return redirect()->intended('/'); + } + return back()->with(['error' => 'Gagal Login! Password / Username Salah']); + + } + + public function logout(){ + Auth::logout(); + request()->session()->invalidate(); + request()->session()->regenerateToken(); + return redirect('/login'); + } +} diff --git a/app/Http/Controllers/KaryawanController.php b/app/Http/Controllers/KaryawanController.php index e1c75b9..b743809 100644 --- a/app/Http/Controllers/KaryawanController.php +++ b/app/Http/Controllers/KaryawanController.php @@ -3,54 +3,96 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; use App\Models\Karyawan; +use App\Models\KaryawanLuar; class KaryawanController extends Controller { public function listData(Request $request){ $search = trim($request->input('search')); - $page = (int) $request->input('page', 1); - $perPage = (int) $request->input('per_page', 10); - - if ($page < 1) $page = 1; - if ($perPage < 1) $perPage = 10; - if ($perPage > 50) $perPage = 50; + $limit = (int) $request->input('limit', 50); + if ($limit < 1) $limit = 50; + if ($limit > 100) $limit = 100; $validator = Validator::make( - ['search' => $search, 'page' => $page, 'per_page' => $perPage], - ['search' => 'required|min:2', 'page' => 'integer|min:1', 'per_page' => 'integer|min:1|max:50'] + ['search' => $search, 'limit' => $limit], + ['search' => 'required|min:2', 'limit' => 'integer|min:1|max:100'] ); if ($validator->fails()) { - return response()->json(['error' => 0, 'data' => [], 'meta' => [ - 'page' => $page, - 'per_page' => $perPage, - 'total' => 0, - 'last_page' => 1, - ]]); + return response()->json(['error' => 0, 'data' => []]); } - $paginator = Karyawan::where('statusenabled', true)->where('kedudukanfk', 1) - ->when($search, function($q) use ($search) { - $q->where(function($q2) use ($search){ - $q2->where('namalengkap', 'ILIKE', "%$search%") - ->orWhere('nip_pns', 'ILIKE', "%$search%"); - }); - }) - ->select('id', 'namalengkap') - ->orderBy('namalengkap') - ->paginate($perPage, ['*'], 'page', $page); + $rows = DB::connection('pgsql') + ->table('public.pegawai_m as pg') + ->leftJoin('public.mappegawaijabatantounitkerja_m as mp', function ($join) { + $join->on('mp.objectpegawaifk', '=', 'pg.id') + ->where('mp.statusenabled', true) + ->where('mp.isprimary', true); + }) + ->leftJoin('public.unitkerjapegawai_m as ukp', function ($join) { + $join->on('ukp.id', '=', 'mp.objectunitkerjapegawaifk') + ->where('ukp.statusenabled', true); + }) + ->where('pg.statusenabled', true) + ->where('pg.kedudukanfk', 1) + ->where(function ($q) use ($search) { + $q->where('pg.namalengkap', 'ILIKE', "%$search%") + ->orWhere('pg.nip_pns', 'ILIKE', "%$search%"); + }) + ->select([ + 'pg.id', + DB::raw("coalesce(pg.namalengkap, '-') as namalengkap"), + DB::raw("coalesce(pg.nip_pns, '-') as nip_pns"), + DB::raw("coalesce(ukp.name, '-') as unit_name"), + ]) + ->orderBy('pg.namalengkap') + ->limit($limit) + ->get(); return response()->json([ 'error' => 0, - 'data' => $paginator->items(), - 'meta' => [ - 'page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], + 'data' => $rows, ]); } + + public function listDataKaryawanLuar(Request $request) + { + $search = trim((string) $request->input('search')); + $limit = (int) $request->input('limit', 50); + if ($limit < 1) $limit = 50; + if ($limit > 100) $limit = 100; + + $validator = Validator::make( + ['search' => $search, 'limit' => $limit], + ['search' => 'required|min:2', 'limit' => 'integer|min:1|max:100'] + ); + + if ($validator->fails()) { + return response()->json(['error' => 0, 'data' => []]); + } + + $data = KaryawanLuar::query() + ->from('public.pegawai_luar_pl as pl') + ->where(function($q) use($search){ + $q->where('pl.nama', 'ILIKE', "%$search%") + ->orWhere('pl.nik', 'ILIKE', "%$search%"); + }) + ->select([ + 'pl.id as id', + DB::raw("coalesce(pl.nama, '-') as namalengkap"), + DB::raw("coalesce(pl.nik, '-') as nip_pns"), + DB::raw("coalesce(nullif(pl.tipe, ''), '-') as unit_name"), + ]) + ->orderBy('pl.nama') + ->limit($limit) + ->get(); + return response()->json([ + 'error' => 0, + 'data' => $data + ]); + + } } diff --git a/app/Http/Controllers/MasterPitStopController.php b/app/Http/Controllers/MasterPitStopController.php new file mode 100644 index 0000000..c02e3cc --- /dev/null +++ b/app/Http/Controllers/MasterPitStopController.php @@ -0,0 +1,145 @@ + 'Master PitStop', + ]); + } + + public function data(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', '')); + + $baseNoSearch = MasterPitStopPraAkre::query() + ->select(['id', 'nama', 'statusenabled']) + ->orderBy('id'); + + $recordsTotal = (clone $baseNoSearch)->count(); + + $base = clone $baseNoSearch; + if ($search !== '') { + $base->where('nama', 'ILIKE', '%' . $search . '%'); + } + + $recordsFiltered = $search === '' ? $recordsTotal : (clone $base)->count(); + + $rows = $base + ->orderBy('nama') + ->offset($start) + ->limit($length) + ->get(); + + $data = $rows->map(function ($r) { + return [ + 'id' => (int) $r->id, + 'nama' => (string) ($r->nama ?? '-'), + 'statusenabled' => (bool) $r->statusenabled, + ]; + })->values(); + + return response()->json([ + 'draw' => $draw, + 'recordsTotal' => $recordsTotal, + 'recordsFiltered' => $recordsFiltered, + 'data' => $data, + ]); + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'nama' => 'required|string|max:255', + 'statusenabled' => 'nullable|boolean', + ]); + + if ($validator->fails()) { + return response()->json([ + 'error' => 1, + 'message' => 'Validasi gagal.', + 'errors' => $validator->errors(), + ], 422); + } + + $payload = $validator->validated(); + + $row = MasterPitStopPraAkre::create([ + 'nama' => trim($payload['nama']), + 'statusenabled' => array_key_exists('statusenabled', $payload) ? (bool) $payload['statusenabled'] : true, + ]); + + return response()->json([ + 'error' => 0, + 'message' => 'Berhasil menambah master pitstop.', + 'data' => [ + 'id' => $row->id, + ], + ]); + } + + public function update(Request $request, $id) + { + $row = MasterPitStopPraAkre::findOrFail($id); + + $validator = Validator::make($request->all(), [ + 'nama' => 'required|string|max:255', + 'statusenabled' => 'nullable' + ]); + + if ($validator->fails()) { + return response()->json([ + 'error' => 1, + 'message' => 'Validasi gagal.', + 'errors' => $validator->errors(), + ], 422); + } + + $payload = $validator->validated(); + + $row->update([ + 'nama' => trim($payload['nama']), + 'statusenabled' => trim($payload['statusenabled']) + ]); + + return response()->json([ + 'error' => 0, + 'message' => 'Berhasil mengubah master pitstop.', + ]); + } + + public function toggle(Request $request, $id) + { + $row = MasterPitStopPraAkre::findOrFail($id); + $row->statusenabled = !$row->statusenabled; + $row->save(); + + return response()->json([ + 'error' => 0, + 'message' => $row->statusenabled ? 'Berhasil mengaktifkan.' : 'Berhasil menonaktifkan.', + 'data' => [ + 'statusenabled' => (bool) $row->statusenabled, + ], + ]); + } +} + diff --git a/app/Http/Controllers/PitStopController.php b/app/Http/Controllers/PitStopController.php index 920d4ae..606352e 100644 --- a/app/Http/Controllers/PitStopController.php +++ b/app/Http/Controllers/PitStopController.php @@ -5,11 +5,37 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; +use Symfony\Component\HttpFoundation\StreamedResponse; +use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx; use App\Models\MasterPitStopPraAkre; use App\Models\PraAkre; +use App\Models\KaryawanLuar; +use Barryvdh\DomPDF\Facade\Pdf; class PitStopController extends Controller { + private function dtInt($value, $default = 0) + { + $n = is_numeric($value) ? (int) $value : (int) $default; + return $n < 0 ? (int) $default : $n; + } + + private function streamXlsx(string $filename, callable $buildSpreadsheet): StreamedResponse + { + $response = new StreamedResponse(function () use ($buildSpreadsheet) { + $spreadsheet = new Spreadsheet(); + $buildSpreadsheet($spreadsheet); + $writer = new Xlsx($spreadsheet); + $writer->save('php://output'); + }); + + $response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + $response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"'); + return $response; + } + public function pitstop(){ $masterPitStop = MasterPitStopPraAkre::where('statusenabled', true)->get(); $data = [ @@ -19,77 +45,1223 @@ class PitStopController extends Controller return view('pitstop.index', $data); } - public function progress(Request $request) + public function progressUnit() { - $q = trim((string) $request->query('q', '')); - $perPage = (int) $request->query('per_page', 20); - if ($perPage < 1) $perPage = 20; - if ($perPage > 100) $perPage = 100; + $totalSteps = (int) MasterPitStopPraAkre::where('statusenabled', true)->count(); + + return view('pitstop.progress_unit', [ + 'title' => 'Monitoring Pra Akreditasi', + 'totalSteps' => $totalSteps, + ]); + } + + public function progressUnitDetail(Request $request, $unit_id) + { + $unitId = (int) $unit_id; + $unit = DB::connection('pgsql') + ->table('public.unitkerjapegawai_m as ukp') + ->where('ukp.statusenabled', true) + ->where('ukp.id', $unitId) + ->select(['ukp.id', 'ukp.name']) + ->first(); + + abort_if(!$unit, 404); $totalSteps = (int) MasterPitStopPraAkre::where('statusenabled', true)->count(); - $baseQuery = DB::connection('pgsql') - ->table('public.pegawai_m as pg') + return view('pitstop.progress_unit_detail', [ + 'title' => 'Monitoring Pra Akreditasi - ' . ($unit->name ?? 'Unit'), + 'unit' => $unit, + 'totalSteps' => $totalSteps, + ]); + } + + public function progressExternalDetail(Request $request, $tipe) + { + $tipe = urldecode((string) $tipe); + $tipe = trim($tipe); + abort_if($tipe === '', 404); + + $totalSteps = (int) MasterPitStopPraAkre::where('statusenabled', true)->count(); + $masterPitStop = MasterPitStopPraAkre::where('statusenabled', true)->get(); + + return view('pitstop.progress_external_detail', [ + 'title' => 'Monitoring Karyawan Luar - ' . $tipe, + 'tipe' => $tipe, + 'totalSteps' => $totalSteps, + 'masterPitStop' => $masterPitStop, + ]); + } + + public function dataProgress(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(); + + $perPegawai = 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) - ->when($q !== '', function ($query) use ($q) { - $query->where('pg.namalengkap', 'ILIKE', "%{$q}%"); - }) + ->where('ukp.statusenabled', true) ->select([ - 'pg.id', - DB::raw("coalesce(pg.namalengkap, '-') as nama"), + 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"), ]) - ->groupBy('pg.id', 'pg.namalengkap') - ->orderByDesc('lulus_count') - ->orderBy('pg.namalengkap'); + ->groupBy('ukp.id', 'ukp.name', 'pg.id'); - $users = $baseQuery->paginate($perPage)->appends(['q' => $q, 'per_page' => $perPage]); + $unitAgg = DB::connection('pgsql') + ->query() + ->fromSub($perPegawai, 't') + ->select([ + 't.unit_id', + 't.unit_name', + DB::raw('count(*) as total_pegawai'), + DB::raw('sum(case when t.lulus_count >= ' . $totalSteps . ' then 1 else 0 end) as pegawai_selesai'), + DB::raw('sum(t.lulus_count) as total_step_lulus'), + DB::raw('avg(t.lulus_count) as avg_step_lulus'), + ]) + ->groupBy('t.unit_id', 't.unit_name'); - $totalUsers = $users->total(); - $totalSelesai = DB::connection('pgsql') - ->table('public.praakre as p') + $recordsTotal = DB::connection('pgsql') + ->query() + ->fromSub($unitAgg, 'u') + ->count(); + + $filteredQuery = DB::connection('pgsql')->query()->fromSub($unitAgg, 'u'); + if ($search !== '') { + $filteredQuery->where('u.unit_name', 'ILIKE', '%' . $search . '%'); + } + + $recordsFiltered = (clone $filteredQuery)->count(); + + $rows = $filteredQuery + ->orderByDesc('pegawai_selesai') + ->orderByDesc('avg_step_lulus') + ->orderBy('unit_name') + ->offset($start) + ->limit($length) + ->get(); + + $data = $rows->map(function ($r) use ($totalSteps) { + $totalPegawai = (int) ($r->total_pegawai ?? 0); + $pegawaiSelesai = (int) ($r->pegawai_selesai ?? 0); + $selesaiPct = $totalPegawai > 0 ? round(($pegawaiSelesai / $totalPegawai) * 100, 1) : 0; + + return [ + 'unit_id' => (int) $r->unit_id, + 'unit_name' => (string) ($r->unit_name ?? '-'), + 'total_pegawai' => $totalPegawai, + 'pegawai_selesai' => $pegawaiSelesai, + 'selesai_pct' => $selesaiPct, + 'total_step_lulus' => (int) ($r->total_step_lulus ?? 0), + ]; + })->values(); + + return response()->json([ + 'draw' => $draw, + 'recordsTotal' => $recordsTotal, + 'recordsFiltered' => $recordsFiltered, + 'data' => $data, + ]); + } + + public function dataProgressExternal(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(); + + $perPegawai = 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); }) - ->where('p.status', 'lulus') - ->whereNotNull('m.id') - ->select(DB::raw('count(distinct (p.pegawai_id, m.id)) as c')) - ->value('c'); + ->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"), + ]) + ->groupBy(DB::raw("coalesce(nullif(pl.tipe, ''), '-')"), 'pl.id'); - if ($request->boolean('ajax')) { - return response()->json([ - 'error' => 0, - 'data' => [ - 'totalUsers' => $totalUsers, - 'table_html' => view('pitstop.partials.progress_rows', [ - 'users' => $users, - 'totalSteps' => $totalSteps, - ])->render(), - 'pager_html' => view('pitstop.partials.progress_pager', [ - 'users' => $users, - ])->render(), - ], - ]); + $tipeAgg = DB::connection('pgsql') + ->query() + ->fromSub($perPegawai, 't') + ->select([ + 't.tipe', + DB::raw('count(*) as total_pegawai'), + DB::raw('sum(case when t.lulus_count >= ' . $totalSteps . ' then 1 else 0 end) as pegawai_selesai'), + DB::raw('sum(t.lulus_count) as total_step_lulus'), + DB::raw('avg(t.lulus_count) as avg_step_lulus'), + ]) + ->groupBy('t.tipe'); + + $recordsTotal = DB::connection('pgsql') + ->query() + ->fromSub($tipeAgg, 'u') + ->count(); + + $filteredQuery = DB::connection('pgsql')->query()->fromSub($tipeAgg, 'u'); + if ($search !== '') { + $filteredQuery->where('u.tipe', 'ILIKE', '%' . $search . '%'); } - return view('pitstop.progress', [ - 'title' => 'Dashboard Progress', - 'q' => $q, - 'perPage' => $perPage, - 'totalSteps' => $totalSteps, - 'totalUsers' => $totalUsers, - 'totalSelesai' => $totalSelesai, - 'users' => $users, + $recordsFiltered = (clone $filteredQuery)->count(); + + $rows = $filteredQuery + ->orderByDesc('pegawai_selesai') + ->orderByDesc('avg_step_lulus') + ->orderBy('tipe') + ->offset($start) + ->limit($length) + ->get(); + + $data = $rows->map(function ($r) use ($totalSteps) { + $totalPegawai = (int) ($r->total_pegawai ?? 0); + $pegawaiSelesai = (int) ($r->pegawai_selesai ?? 0); + $selesaiPct = $totalPegawai > 0 ? round(($pegawaiSelesai / $totalPegawai) * 100, 1) : 0; + + return [ + 'tipe' => (string) ($r->tipe ?? '-'), + 'total_pegawai' => $totalPegawai, + 'pegawai_selesai' => $pegawaiSelesai, + 'selesai_pct' => $selesaiPct, + 'total_step_lulus' => (int) ($r->total_step_lulus ?? 0), + 'avg_step_lulus' => $totalSteps > 0 ? round((float) ($r->avg_step_lulus ?? 0), 2) : 0, + ]; + })->values(); + + return response()->json([ + 'draw' => $draw, + 'recordsTotal' => $recordsTotal, + 'recordsFiltered' => $recordsFiltered, + 'data' => $data, ]); } + public function dataProgressExternalDetail(Request $request) + { + $validator = Validator::make($request->all(), [ + 'tipe' => 'required|string|max:100', + 'limit' => 'nullable|integer|min:1|max:500', + ]); + + if ($validator->fails()) { + return response()->json([ + 'error' => 1, + 'message' => 'Validasi gagal.', + 'errors' => $validator->errors(), + ], 422); + } + + $tipe = trim((string) $validator->validated()['tipe']); + $limit = (int) ($validator->validated()['limit'] ?? 200); + + $totalSteps = (int) MasterPitStopPraAkre::where('statusenabled', true)->count(); + + $rows = 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); + }) + ->where(DB::raw("coalesce(nullif(pl.tipe, ''), '-')"), '=', $tipe) + ->select([ + 'pl.id', + 'pl.nama', + 'pl.nik', + DB::raw("coalesce(nullif(pl.tipe, ''), '-') as tipe"), + DB::raw("count(distinct m.id) filter (where p.status='lulus') as lulus_count"), + ]) + ->groupBy('pl.id', 'pl.nama', 'pl.nik', DB::raw("coalesce(nullif(pl.tipe, ''), '-')")) + ->orderBy('pl.nama') + ->limit($limit) + ->get(); + + $data = $rows->map(function ($r) use ($totalSteps) { + $lulus = (int) ($r->lulus_count ?? 0); + $selesai = $totalSteps > 0 ? ($lulus >= $totalSteps ? 1 : 0) : 0; + $pct = $totalSteps > 0 ? round(($lulus / $totalSteps) * 100, 1) : 0; + return [ + 'id' => (int) $r->id, + 'nama' => (string) ($r->nama ?? '-'), + 'nik' => (string) ($r->nik ?? '-'), + 'tipe' => (string) ($r->tipe ?? '-'), + 'lulus_count' => $lulus, + 'pct' => $pct, + 'selesai' => $selesai, + ]; + })->values(); + + return response()->json([ + 'error' => 0, + 'data' => $data, + 'meta' => [ + 'tipe' => $tipe, + 'total_steps' => $totalSteps, + 'count' => $data->count(), + ], + ]); + } + + public function monitoringPdf(Request $request) + { + $unitId = $request->query('unit_id'); + $unitId = is_null($unitId) || $unitId === '' ? null : (int) $unitId; + + $totalSteps = (int) MasterPitStopPraAkre::where('statusenabled', true)->count(); + $generatedAt = now(); + + // Default: ringkasan per unit (tanpa list pegawai) supaya tidak memakan memory besar. + if (is_null($unitId)) { + $perPegawai = 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('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"), + ]) + ->groupBy('ukp.id', 'ukp.name', 'pg.id'); + + $unitAgg = DB::connection('pgsql') + ->query() + ->fromSub($perPegawai, 't') + ->select([ + 't.unit_id', + 't.unit_name', + DB::raw('count(*) as total_pegawai'), + DB::raw('sum(case when t.lulus_count >= ' . $totalSteps . ' then 1 else 0 end) as pegawai_selesai'), + ]) + ->groupBy('t.unit_id', 't.unit_name'); + + $rows = DB::connection('pgsql') + ->query() + ->fromSub($unitAgg, 'u') + ->orderByDesc('pegawai_selesai') + ->orderByDesc('total_pegawai') + ->orderBy('unit_name') + ->get(); + + $units = $rows->map(function ($r) { + $totalPegawai = (int) ($r->total_pegawai ?? 0); + $pegawaiSelesai = (int) ($r->pegawai_selesai ?? 0); + $pct = $totalPegawai > 0 ? round(($pegawaiSelesai / $totalPegawai) * 100, 1) : 0; + return (object) [ + 'unit_id' => (int) $r->unit_id, + 'unit_name' => (string) ($r->unit_name ?? '-'), + 'total_pegawai' => $totalPegawai, + 'pegawai_selesai' => $pegawaiSelesai, + 'pct' => $pct, + 'pegawai' => collect(), + ]; + }); + + $pdf = Pdf::loadView('pitstop.monitoring_pdf', [ + 'title' => 'Monitoring Pra Akreditasi', + 'generatedAt' => $generatedAt, + 'totalSteps' => $totalSteps, + 'units' => $units, + 'isSummaryOnly' => true, + ])->setPaper('a4', 'landscape'); + + $filename = 'monitoring-pra-akreditasi-ringkasan-' . $generatedAt->format('Ymd-Hi') . '.pdf'; + return $pdf->download($filename); + } + + // Mode detail: PDF per unit (lebih aman untuk memory). + $unit = DB::connection('pgsql') + ->table('public.unitkerjapegawai_m as ukp') + ->where('ukp.statusenabled', true) + ->where('ukp.id', $unitId) + ->select(['ukp.id', 'ukp.name']) + ->first(); + + abort_if(!$unit, 404); + + $perPegawai = 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) + ->where('ukp.id', $unitId) + ->select([ + DB::raw('ukp.id as unit_id'), + DB::raw("coalesce(ukp.name, '-') as unit_name"), + 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("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 + ), '') as belum_selesai_steps"), + ]) + ->groupBy('ukp.id', 'ukp.name', 'pg.id', 'pg.namalengkap', 'pg.nip_pns'); + + $rows = DB::connection('pgsql') + ->query() + ->fromSub($perPegawai, 't') + ->orderBy('t.nama') + ->get(); + + $totalPegawai = $rows->count(); + $pegawaiSelesai = $rows->filter(function ($r) use ($totalSteps) { + return (int) ($r->lulus_count ?? 0) >= $totalSteps && $totalSteps > 0; + })->count(); + $pct = $totalPegawai > 0 ? round(($pegawaiSelesai / $totalPegawai) * 100, 1) : 0; + + $pegawai = $rows->map(function ($r) use ($totalSteps) { + $lulus = (int) ($r->lulus_count ?? 0); + $pctPegawai = $totalSteps > 0 ? round(($lulus / $totalSteps) * 100, 1) : 0; + $belum = trim((string) ($r->belum_selesai_steps ?? '')); + return (object) [ + 'pegawai_id' => (int) $r->pegawai_id, + 'nama' => (string) ($r->nama ?? '-'), + 'nip_pns' => (string) ($r->nip_pns ?? '-'), + 'total_steps' => $totalSteps, + 'lulus_steps' => $lulus, + 'pct' => $pctPegawai, + 'belum_selesai_steps' => $belum, + ]; + })->values(); + + $units = collect([ + (object) [ + 'unit_id' => (int) $unit->id, + 'unit_name' => (string) ($unit->name ?? '-'), + 'total_pegawai' => $totalPegawai, + 'pegawai_selesai' => $pegawaiSelesai, + 'pct' => $pct, + 'pegawai' => $pegawai, + ], + ]); + + $pdf = Pdf::loadView('pitstop.monitoring_pdf', [ + 'title' => 'Monitoring Pra Akreditasi', + 'generatedAt' => $generatedAt, + 'totalSteps' => $totalSteps, + 'units' => $units, + 'isSummaryOnly' => false, + ])->setPaper('a4', 'landscape'); + + $filename = 'monitoring-pra-akreditasi-unit-' . $unitId . '-' . $generatedAt->format('Ymd-Hi') . '.pdf'; + return $pdf->download($filename); + } + + public function monitoringPdfExternal(Request $request) + { + $tipe = $request->query('tipe'); + $tipe = is_null($tipe) ? null : trim((string) $tipe); + if ($tipe === '') $tipe = null; + + $totalSteps = (int) MasterPitStopPraAkre::where('statusenabled', true)->count(); + $generatedAt = now(); + + // Summary: per tipe (tanpa list pegawai) + if (is_null($tipe)) { + $perPegawai = 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("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"), + ]) + ->groupBy(DB::raw("coalesce(nullif(pl.tipe, ''), '-')"), 'pl.id'); + + $tipeAgg = DB::connection('pgsql') + ->query() + ->fromSub($perPegawai, 't') + ->select([ + 't.tipe', + DB::raw('count(*) as total_pegawai'), + DB::raw('sum(case when t.lulus_count >= ' . $totalSteps . ' then 1 else 0 end) as pegawai_selesai'), + ]) + ->groupBy('t.tipe') + ->orderByDesc('pegawai_selesai') + ->orderByDesc('total_pegawai') + ->orderBy('tipe') + ->get(); + + $types = $tipeAgg->map(function ($r) { + return (object) [ + 'tipe' => (string) ($r->tipe ?? '-'), + 'total_pegawai' => (int) ($r->total_pegawai ?? 0), + 'pegawai_selesai' => (int) ($r->pegawai_selesai ?? 0), + 'pegawai' => collect(), + ]; + }); + + $pdf = Pdf::loadView('pitstop.monitoring_pdf_external', [ + 'title' => 'Monitoring Karyawan Luar', + 'generatedAt' => $generatedAt, + 'totalSteps' => $totalSteps, + 'types' => $types, + 'isSummaryOnly' => true, + ])->setPaper('a4', 'landscape'); + + $filename = 'monitoring-karyawan-luar-ringkasan-' . $generatedAt->format('Ymd-Hi') . '.pdf'; + return $pdf->download($filename); + } + + // Detail per tipe + $perPegawai = 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); + }) + ->where(DB::raw("coalesce(nullif(pl.tipe, ''), '-')"), '=', $tipe) + ->select([ + '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("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 + ), '') as belum_steps"), + ]) + ->groupBy('pl.id', 'pl.nama', 'pl.nik') + ->orderBy('pl.nama') + ->get(); + + $pegawai = $perPegawai->map(function ($r) use ($totalSteps) { + return (object) [ + 'nama' => (string) ($r->nama ?? '-'), + 'nik' => (string) ($r->nik ?? '-'), + 'lulus_steps' => (int) ($r->lulus_count ?? 0), + 'total_steps' => $totalSteps, + 'belum_selesai_steps' => (string) ($r->belum_steps ?? ''), + ]; + }); + + $totalPegawai = (int) $pegawai->count(); + $pegawaiSelesai = $pegawai->filter(function ($p) { + return ((int) ($p->total_steps ?? 0)) > 0 && ((int) ($p->lulus_steps ?? 0)) >= ((int) ($p->total_steps ?? 0)); + })->count(); + + $types = collect([ + (object) [ + 'tipe' => $tipe, + 'total_pegawai' => $totalPegawai, + 'pegawai_selesai' => $pegawaiSelesai, + 'pegawai' => $pegawai, + ], + ]); + + $pdf = Pdf::loadView('pitstop.monitoring_pdf_external', [ + 'title' => 'Monitoring Karyawan Luar - ' . $tipe, + 'generatedAt' => $generatedAt, + 'totalSteps' => $totalSteps, + 'types' => $types, + 'isSummaryOnly' => false, + ])->setPaper('a4', 'landscape'); + + $filename = 'monitoring-karyawan-luar-' . preg_replace('/[^a-zA-Z0-9_-]+/', '-', $tipe) . '-' . $generatedAt->format('Ymd-Hi') . '.pdf'; + return $pdf->download($filename); + } + + public function monitoringExcel(Request $request) + { + $unitId = $request->query('unit_id'); + $unitId = is_null($unitId) || $unitId === '' ? null : (int) $unitId; + + $totalSteps = (int) MasterPitStopPraAkre::where('statusenabled', true)->count(); + $generatedAt = now(); + + // Summary per unit + if (is_null($unitId)) { + $perPegawai = 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('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"), + ]) + ->groupBy('ukp.id', 'ukp.name', 'pg.id'); + + $unitAgg = DB::connection('pgsql') + ->query() + ->fromSub($perPegawai, 't') + ->select([ + 't.unit_id', + 't.unit_name', + DB::raw('count(*) as total_pegawai'), + DB::raw('sum(case when t.lulus_count >= ' . $totalSteps . ' then 1 else 0 end) as pegawai_selesai'), + ]) + ->groupBy('t.unit_id', 't.unit_name') + ->orderBy('t.unit_name') + ->get(); + + $filename = 'monitoring-pra-akreditasi-ringkasan-' . $generatedAt->format('Ymd-Hi') . '.xlsx'; + return $this->streamXlsx($filename, function (Spreadsheet $spreadsheet) use ($unitAgg, $totalSteps) { + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setTitle('Internal (Summary)'); + + $headers = ['Nama Unit', 'Total Pitstop', 'Total Karyawan', 'Karyawan Selesai']; + foreach ($headers as $i => $h) { + $sheet->setCellValueByColumnAndRow($i + 1, 1, $h); + } + $sheet->getStyle('A1:E1')->getFont()->setBold(true); + + $row = 2; + foreach ($unitAgg as $r) { + $sheet->setCellValueByColumnAndRow(1, $row, (string) ($r->unit_name ?? '-')); + $sheet->setCellValueByColumnAndRow(2, $row, (int) $totalSteps); + $sheet->setCellValueByColumnAndRow(3, $row, (int) ($r->total_pegawai ?? 0)); + $sheet->setCellValueByColumnAndRow(4, $row, (int) ($r->pegawai_selesai ?? 0)); + $row++; + } + foreach(range('A', 'E') as $col){ + $sheet->getColumnDimension($col)->setAutoSize(true); + } + }); + } + + // Detail per unit + $unit = DB::connection('pgsql') + ->table('public.unitkerjapegawai_m as ukp') + ->where('ukp.statusenabled', true) + ->where('ukp.id', $unitId) + ->select(['ukp.id', 'ukp.name']) + ->first(); + + abort_if(!$unit, 404); + + $rows = 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) + ->where('ukp.id', $unitId) + ->select([ + '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("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 + ), '') as belum_steps"), + ]) + ->groupBy('pg.id', 'pg.namalengkap', 'pg.nip_pns') + ->orderBy('pg.namalengkap') + ->get(); + + $filename = 'monitoring-pra-akreditasi-unit-' . (int) $unitId . '-' . $generatedAt->format('Ymd-Hi') . '.xlsx'; + return $this->streamXlsx($filename, function (Spreadsheet $spreadsheet) use ($rows, $unit, $totalSteps) { + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setTitle('Internal (Detail)'); + + $headers = ['Nama Unit', 'Nama Karyawan', 'NIP', 'Lulus Pitstop', 'Total Pitstop', 'Belum Dikerjakan']; + foreach ($headers as $i => $h) { + $sheet->setCellValueByColumnAndRow($i + 1, 1, $h); + } + $sheet->getStyle('A1:I1')->getFont()->setBold(true); + + $row = 2; + foreach ($rows as $r) { + $lulus = (int) ($r->lulus_count ?? 0); + $selesai = $totalSteps > 0 && $lulus >= $totalSteps ? 1 : 0; + + $sheet->setCellValueByColumnAndRow(1, $row, (string) ($unit->name ?? '-')); + $sheet->setCellValueByColumnAndRow(2, $row, (string) ($r->nama ?? '-')); + $sheet->setCellValueExplicitByColumnAndRow(3, $row, (string) ($r->nip_pns ?? '-'), DataType::TYPE_STRING); + $sheet->setCellValueByColumnAndRow(4, $row, $lulus); + $sheet->setCellValueByColumnAndRow(5, $row, (int) $totalSteps); + $sheet->setCellValueByColumnAndRow(6, $row, (string) ($r->belum_steps ?? '')); + $row++; + } + foreach(range('A', 'I') as $col){ + $sheet->getColumnDimension($col)->setAutoSize(true); + } + }); + } + + public function monitoringExcelExternal(Request $request) + { + $tipe = $request->query('tipe'); + $tipe = is_null($tipe) ? null : trim((string) $tipe); + if ($tipe === '') $tipe = null; + + $totalSteps = (int) MasterPitStopPraAkre::where('statusenabled', true)->count(); + $generatedAt = now(); + + // Summary per tipe + if (is_null($tipe)) { + $perPegawai = 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("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"), + ]) + ->groupBy(DB::raw("coalesce(nullif(pl.tipe, ''), '-')"), 'pl.id'); + + $tipeAgg = DB::connection('pgsql') + ->query() + ->fromSub($perPegawai, 't') + ->select([ + 't.tipe', + DB::raw('count(*) as total_pegawai'), + DB::raw('sum(case when t.lulus_count >= ' . $totalSteps . ' then 1 else 0 end) as pegawai_selesai'), + ]) + ->groupBy('t.tipe') + ->orderBy('t.tipe') + ->get(); + + $filename = 'monitoring-karyawan-luar-ringkasan-' . $generatedAt->format('Ymd-Hi') . '.xlsx'; + return $this->streamXlsx($filename, function (Spreadsheet $spreadsheet) use ($tipeAgg, $totalSteps) { + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setTitle('External (Summary)'); + + $headers = ['Unit', 'Total Pitstop', 'Total Karyawan External', 'Karyawan External Selesai']; + foreach ($headers as $i => $h) { + $sheet->setCellValueByColumnAndRow($i + 1, 1, $h); + } + $sheet->getStyle('A1:D1')->getFont()->setBold(true); + $row = 2; + foreach ($tipeAgg as $r) { + $sheet->setCellValueByColumnAndRow(1, $row, (string) ($r->tipe ?? '-')); + $sheet->setCellValueByColumnAndRow(2, $row, (int) $totalSteps); + $sheet->setCellValueByColumnAndRow(3, $row, (int) ($r->total_pegawai ?? 0)); + $sheet->setCellValueByColumnAndRow(4, $row, (int) ($r->pegawai_selesai ?? 0)); + $row++; + } + + foreach(range('A', 'D') as $col){ + $sheet->getColumnDimension($col)->setAutoSize(true); + } + }); + } + + // Detail per tipe + $rows = 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); + }) + ->where(DB::raw("coalesce(nullif(pl.tipe, ''), '-')"), '=', $tipe) + ->select([ + '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("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 + ), '') as belum_steps"), + ]) + ->groupBy('pl.id', 'pl.nama', 'pl.nik') + ->orderBy('pl.nama') + ->get(); + + $filename = 'monitoring-karyawan-luar-' . preg_replace('/[^a-zA-Z0-9_-]+/', '-', (string) $tipe) . '-' . $generatedAt->format('Ymd-Hi') . '.xlsx'; + return $this->streamXlsx($filename, function (Spreadsheet $spreadsheet) use ($rows, $tipe, $totalSteps) { + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setTitle('External (Detail)'); + + $headers = ['Unit', 'Nama', 'NIK', 'Lulus Pitstop', 'Total Pitstop', 'Belum Dikerjakan']; + foreach ($headers as $i => $h) { + $sheet->setCellValueByColumnAndRow($i + 1, 1, $h); + } + $sheet->getStyle('A1:H1')->getFont()->setBold(true); + + $row = 2; + foreach ($rows as $r) { + $lulus = (int) ($r->lulus_count ?? 0); + $selesai = $totalSteps > 0 && $lulus >= $totalSteps ? 1 : 0; + + $sheet->setCellValueByColumnAndRow(1, $row, (string) $tipe); + $sheet->setCellValueByColumnAndRow(2, $row, (string) ($r->nama ?? '-')); + $sheet->setCellValueExplicitByColumnAndRow(3, $row, (string) ($r->nik ?? '-'), DataType::TYPE_STRING); + $sheet->setCellValueByColumnAndRow(4, $row, $lulus); + $sheet->setCellValueByColumnAndRow(5, $row, (int) $totalSteps); + $sheet->setCellValueByColumnAndRow(6, $row, (string) ($r->belum_steps ?? '')); + $row++; + } + + foreach(range('A', 'H') as $col){ + $sheet->getColumnDimension($col)->setAutoSize(true); + } + }); + } + + public function dataProgressUnit(Request $request, $unit_id) + { + $unitId = (int) $unit_id; + + $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(); + + $baseNoSearch = 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) + ->where('ukp.id', $unitId) + ->select([ + '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("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 + ), 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.status = 'lulus' + 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'); + + $base = clone $baseNoSearch; + if ($search !== '') { + $base->where('pg.namalengkap', '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('pg.namalengkap') + ->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 [ + 'id' => (int) $r->id, + 'nama' => (string) ($r->nama ?? '-'), + 'nip_pns' => (string) ($r->nip_pns ?? '-'), + '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, + ]); + } + + public function dataProgressExternalByTipe(Request $request, $tipe) + { + $tipe = urldecode((string) $tipe); + $tipe = trim($tipe); + abort_if($tipe === '', 404); + + $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(); + + $baseNoSearch = 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); + }) + ->where(DB::raw("coalesce(nullif(pl.tipe, ''), '-')"), '=', $tipe) + ->select([ + '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("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 + ), 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.status = 'lulus' + 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'); + + $base = clone $baseNoSearch; + if ($search !== '') { + $base->where(function ($q) use ($search) { + $q->where('pl.nama', 'ILIKE', '%' . $search . '%') + ->orWhere('pl.nik', '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('pl.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 [ + 'id' => (int) $r->id, + 'nama' => (string) ($r->nama ?? '-'), + 'nik' => (string) ($r->nik ?? '-'), + '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, + ]); + } + + public function dataProgressAllKaryawan(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(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("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 + ), 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.status = 'lulus' + 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(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("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 + ), 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.status = 'lulus' + 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.identitas', '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 ?? '-'), + 'identitas' => (string) ($r->identitas ?? '-'), + '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, + ]); + } + + + public function progressDetail(Request $request) { $validator = Validator::make($request->all(), [ @@ -113,13 +1285,57 @@ class PitStopController extends Controller ->where('m.statusenabled', true); }) ->where('p.pegawai_id', $pegawaiId) + ->where('p.status', 'lulus') ->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, 'YYYY-MM-DD HH24:MI:SS') as created_at"), + 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 progressDetailExternal(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) @@ -160,13 +1376,47 @@ class PitStopController extends Controller ]); } - public function submit(Request $request) + public function pegawaiStepsExternal(Request $request) { $validator = Validator::make($request->all(), [ - 'karyawan_id' => 'required|integer|exists:pgsql.public.pegawai_m,id', + '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']; + + $lockedSteps = PraAkre::where('pegawai_id', $pegawaiId) + ->where('tipe_karyawan', 'luar') + ->where('status', 'lulus') + ->distinct() + ->pluck('masterpitstop_id'); + + return response()->json([ + 'error' => 0, + 'data' => [ + 'locked_steps' => $lockedSteps, + ], + ]); + } + + public function submit(Request $request) + { + $tipeKaryawan = (string) $request->input('tipe_karyawan', 'internal'); + $tipeKaryawan = $tipeKaryawan === 'luar' ? 'luar' : 'internal'; + + $validator = Validator::make($request->all(), [ + 'karyawan_id' => ['required', 'integer'], 'step' => 'required', 'status' => 'required|in:lulus,tidak_lulus', 'unit_id' => 'nullable|string|max:15', + 'tipe_karyawan' => 'nullable|in:internal,luar', ]); if ($validator->fails()) { @@ -178,11 +1428,37 @@ class PitStopController extends Controller } $payload = $validator->validated(); + $payload['tipe_karyawan'] = $tipeKaryawan; $pegawaiId = (int) $payload['karyawan_id']; $masterPitstopId = (string) $payload['step']; + if ($tipeKaryawan === 'luar') { + $exists = DB::connection('pgsql') + ->table('public.pegawai_luar_pl') + ->where('id', $pegawaiId) + ->exists(); + if (!$exists) { + return response()->json([ + 'error' => 1, + 'message' => 'Karyawan luar tidak ditemukan.', + ], 422); + } + } else { + $exists = DB::connection('pgsql') + ->table('public.pegawai_m') + ->where('id', $pegawaiId) + ->exists(); + if (!$exists) { + return response()->json([ + 'error' => 1, + 'message' => 'Karyawan tidak ditemukan.', + ], 422); + } + } + $alreadyPassed = PraAkre::where('pegawai_id', $pegawaiId) + ->where('tipe_karyawan', $tipeKaryawan) ->where('masterpitstop_id', $masterPitstopId) ->where('status', 'lulus') ->exists(); @@ -197,8 +1473,11 @@ class PitStopController extends Controller $row = PraAkre::create([ 'pegawai_id' => $pegawaiId, 'masterpitstop_id' => $masterPitstopId, - 'unit_id' => $payload['unit_id'] ?? null, + 'unit_id' => $tipeKaryawan === 'luar' ? null : ($payload['unit_id'] ?? null), 'status' => $payload['status'], + 'tipe_karyawan' => $payload['tipe_karyawan'], + 'action_at' => auth()->user()->objectpegawaifk + , 'created_at' => now(), ]); diff --git a/app/Models/KaryawanLuar.php b/app/Models/KaryawanLuar.php new file mode 100644 index 0000000..70ba174 --- /dev/null +++ b/app/Models/KaryawanLuar.php @@ -0,0 +1,16 @@ + */ - protected $fillable = [ - 'name', - 'email', - 'password', + protected $connection = 'pgsql'; + protected $table = 'public.loginuser_s'; + public $timestamps = false; + protected $primaryKey = "id"; + protected $guarded = [ + 'id', ]; - - /** - * The attributes that should be hidden for serialization. - * - * @var array - */ - protected $hidden = [ - 'password', - 'remember_token', - ]; - - /** - * Get the attributes that should be cast. - * - * @return array - */ - protected function casts(): array - { - return [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - ]; - } } diff --git a/composer.json b/composer.json index 023adcf..435c964 100644 --- a/composer.json +++ b/composer.json @@ -6,8 +6,10 @@ "license": "MIT", "require": { "php": "^8.2", + "barryvdh/laravel-dompdf": "2.2", "laravel/framework": "^11.0", - "laravel/tinker": "^2.9" + "laravel/tinker": "^2.9", + "phpoffice/phpspreadsheet": "1.29" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index af8cd63..d21767b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,85 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e5fbbe6137dc77bcd5b6646b5dd75b52", + "content-hash": "c59ab79e8eb552439286f7df9c02a9c5", "packages": [ + { + "name": "barryvdh/laravel-dompdf", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-dompdf.git", + "reference": "c96f90c97666cebec154ca1ffb67afed372114d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/c96f90c97666cebec154ca1ffb67afed372114d8", + "reference": "c96f90c97666cebec154ca1ffb67afed372114d8", + "shasum": "" + }, + "require": { + "dompdf/dompdf": "^2.0.7", + "illuminate/support": "^6|^7|^8|^9|^10|^11", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "larastan/larastan": "^1.0|^2.7.0", + "orchestra/testbench": "^4|^5|^6|^7|^8|^9", + "phpro/grumphp": "^1 || ^2.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "PDF": "Barryvdh\\DomPDF\\Facade\\Pdf", + "Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf" + }, + "providers": [ + "Barryvdh\\DomPDF\\ServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\DomPDF\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "A DOMPDF Wrapper for Laravel", + "keywords": [ + "dompdf", + "laravel", + "pdf" + ], + "support": { + "issues": "https://github.com/barryvdh/laravel-dompdf/issues", + "source": "https://github.com/barryvdh/laravel-dompdf/tree/v2.2.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2024-04-25T13:16:04+00:00" + }, { "name": "brick/math", "version": "0.14.8", @@ -377,6 +454,68 @@ ], "time": "2024-02-05T11:56:58+00:00" }, + { + "name": "dompdf/dompdf", + "version": "v2.0.8", + "source": { + "type": "git", + "url": "https://github.com/dompdf/dompdf.git", + "reference": "c20247574601700e1f7c8dab39310fca1964dc52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/c20247574601700e1f7c8dab39310fca1964dc52", + "reference": "c20247574601700e1f7c8dab39310fca1964dc52", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "masterminds/html5": "^2.0", + "phenx/php-font-lib": ">=0.5.4 <1.0.0", + "phenx/php-svg-lib": ">=0.5.2 <1.0.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "ext-json": "*", + "ext-zip": "*", + "mockery/mockery": "^1.3", + "phpunit/phpunit": "^7.5 || ^8 || ^9", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "ext-gd": "Needed to process images", + "ext-gmagick": "Improves image processing performance", + "ext-imagick": "Improves image processing performance", + "ext-zlib": "Needed for pdf stream compression" + }, + "type": "library", + "autoload": { + "psr-4": { + "Dompdf\\": "src/" + }, + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "The Dompdf Community", + "homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md" + } + ], + "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", + "homepage": "https://github.com/dompdf/dompdf", + "support": { + "issues": "https://github.com/dompdf/dompdf/issues", + "source": "https://github.com/dompdf/dompdf/tree/v2.0.8" + }, + "time": "2024-04-29T13:06:17+00:00" + }, { "name": "dragonmantank/cron-expression", "version": "v3.6.0", @@ -508,6 +647,67 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.19.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", + "shasum": "" + }, + "require": { + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" + }, + "type": "library", + "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0" + }, + "time": "2025-10-17T16:34:55+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.4.0", @@ -2013,6 +2213,258 @@ ], "time": "2026-03-08T20:05:35+00:00" }, + { + "name": "maennchen/zipstream-php", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f", + "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.2" + }, + "require-dev": { + "brianium/paratest": "^7.7", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.16", + "guzzlehttp/guzzle": "^7.5", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^11.0", + "vimeo/psalm": "^6.0" + }, + "suggest": { + "guzzlehttp/psr7": "^2.4", + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + } + ], + "time": "2025-01-27T12:07:53+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, + { + "name": "masterminds/html5", + "version": "2.10.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "fcf91eb64359852f00d921887b219479b4f21251" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", + "reference": "fcf91eb64359852f00d921887b219479b4f21251", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" + }, + "time": "2025-07-25T09:04:22+00:00" + }, { "name": "monolog/monolog", "version": "3.10.0", @@ -2524,6 +2976,201 @@ ], "time": "2026-02-16T23:10:27+00:00" }, + { + "name": "phenx/php-font-lib", + "version": "0.5.6", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-font-lib.git", + "reference": "a1681e9793040740a405ac5b189275059e2a9863" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a1681e9793040740a405ac5b189275059e2a9863", + "reference": "a1681e9793040740a405ac5b189275059e2a9863", + "shasum": "" + }, + "require": { + "ext-mbstring": "*" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "FontLib\\": "src/FontLib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Fabien Ménager", + "email": "fabien.menager@gmail.com" + } + ], + "description": "A library to read, parse, export and make subsets of different types of font files.", + "homepage": "https://github.com/PhenX/php-font-lib", + "support": { + "issues": "https://github.com/dompdf/php-font-lib/issues", + "source": "https://github.com/dompdf/php-font-lib/tree/0.5.6" + }, + "time": "2024-01-29T14:45:26+00:00" + }, + { + "name": "phenx/php-svg-lib", + "version": "0.5.4", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-svg-lib.git", + "reference": "46b25da81613a9cf43c83b2a8c2c1bdab27df691" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/46b25da81613a9cf43c83b2a8c2c1bdab27df691", + "reference": "46b25da81613a9cf43c83b2a8c2c1bdab27df691", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabberworm/php-css-parser": "^8.4" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Svg\\": "src/Svg" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Fabien Ménager", + "email": "fabien.menager@gmail.com" + } + ], + "description": "A library to read, parse and export to PDF SVG files.", + "homepage": "https://github.com/PhenX/php-svg-lib", + "support": { + "issues": "https://github.com/dompdf/php-svg-lib/issues", + "source": "https://github.com/dompdf/php-svg-lib/tree/0.5.4" + }, + "time": "2024-04-08T12:52:34+00:00" + }, + { + "name": "phpoffice/phpspreadsheet", + "version": "1.29.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fde2ccf55eaef7e86021ff1acce26479160a0fa0", + "reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "ezyang/htmlpurifier": "^4.15", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^7.4 || ^8.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^1.0 || ^2.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.3", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.0 || ^10.0", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.0" + }, + "time": "2023-06-14T22:48:31+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.5", @@ -3288,6 +3935,72 @@ }, "time": "2025-12-14T04:43:48+00:00" }, + { + "name": "sabberworm/php-css-parser", + "version": "v8.9.0", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d8e916507b88e389e26d4ab03c904a082aa66bb9", + "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41", + "rawr/cross-data-providers": "^2.0.0" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sabberworm\\CSS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Raphael Schweikert" + }, + { + "name": "Oliver Klee", + "email": "github@oliverklee.de" + }, + { + "name": "Jake Hotson", + "email": "jake.github@qzdesign.co.uk" + } + ], + "description": "Parser for CSS Files written in PHP", + "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "keywords": [ + "css", + "parser", + "stylesheet" + ], + "support": { + "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.9.0" + }, + "time": "2025-07-11T13:20:48+00:00" + }, { "name": "symfony/clock", "version": "v7.4.8", diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php new file mode 100644 index 0000000..c1d4c59 --- /dev/null +++ b/resources/views/auth/login.blade.php @@ -0,0 +1,155 @@ +@extends('partials.main_auth') + +@section('custom_css') + +@endsection + +@section('content') +
+
+
+
+ +
+ +
+
+
+
+
+
+ +

Login Admin Pra Akreditasi

+
+
+ @csrf +
+ + +
+
+ +
+ + +
+
+ +
+
+
+
+
+@endsection + +@section('custom_js') + +@endsection diff --git a/resources/views/master_pitstop/index.blade.php b/resources/views/master_pitstop/index.blade.php new file mode 100644 index 0000000..15e2421 --- /dev/null +++ b/resources/views/master_pitstop/index.blade.php @@ -0,0 +1,222 @@ +@extends('partials.main') + +@section('content') +
+
+
+

Master PitStop

+
+
+
+
+ + + + +
+ +
+
+
+ +
+
+ + + + + + + + + + +
IDNamaStatusAksi
+
+
+
+ + +@endsection + +@section('custom_js') + +@endsection + diff --git a/resources/views/partials/main_auth.blade.php b/resources/views/partials/main_auth.blade.php new file mode 100644 index 0000000..9669d16 --- /dev/null +++ b/resources/views/partials/main_auth.blade.php @@ -0,0 +1,82 @@ + + + + + Pra Akreditasi RSAB Harapan Kita + + + + + + + + + + + + + + + + + + + + @yield('custom_css') + + + + + + + + + +
+ @yield('content') + + +
+ + + + + + + + + + + + + + + + + + + + + + @yield('custom_js') + + + diff --git a/resources/views/partials/section/sidenav.blade.php b/resources/views/partials/section/sidenav.blade.php index c8f2be1..acedf7c 100644 --- a/resources/views/partials/section/sidenav.blade.php +++ b/resources/views/partials/section/sidenav.blade.php @@ -13,6 +13,17 @@ @@ -46,16 +59,17 @@ diff --git a/resources/views/pitstop/index.blade.php b/resources/views/pitstop/index.blade.php index d20df2e..6d269b9 100644 --- a/resources/views/pitstop/index.blade.php +++ b/resources/views/pitstop/index.blade.php @@ -7,6 +7,16 @@ overflow-y: auto; } + .pitstop-type-sticky { + position: sticky; + top: 16px; + z-index: 2; + } + + #resultList .list-group-item-action:hover { + background-color: var(--bs-gray-100); + } + /* Catatan: styling