db = $db ?? Database::connect(); } /** * @return array|null baris pegawai (tanpa password) atau null */ public function actorFromToken(string $token): ?array { if ($token === '') { return null; } $row = $this->db->table('pegawai')->where('token', $token)->get()->getRowArray(); if ($row === null) { return null; } unset($row['password']); return $row; } /** * @return array{status: int, pesan: string, data?: mixed} */ public function references(?int $cabangKantorId = null): array { $kantor = $this->db->table('kantor')->orderBy('nama_kantor')->get()->getResultArray(); if ($cabangKantorId !== null && $cabangKantorId > 0) { $kantor = array_values(array_filter( $kantor, static fn ($r) => (int) ($r['id_kantor'] ?? 0) === $cabangKantorId )); } return [ 'status' => 1, 'pesan' => 'OK', 'data' => [ 'jabatan' => $this->db->table('jabatan')->orderBy('nama_jabatan')->get()->getResultArray(), 'unit_kerja' => $this->db->table('unit_kerja')->orderBy('nama_unit_kerja')->get()->getResultArray(), 'golongan' => $this->db->table('golongan')->orderBy('nama_golongan')->get()->getResultArray(), 'kantor' => $kantor, 'jadwal' => $this->db->table('jadwal')->orderBy('nama_jadwal')->get()->getResultArray(), ], ]; } /** * @return array{status: int, pesan: string, data?: mixed} */ public function pegawaiList(int $page, int $perPage, string $search = '', ?int $cabangKantorId = null): array { $perPage = max(5, min(500, $perPage)); $page = max(1, $page); $offset = ($page - 1) * $perPage; $countB = $this->db->table('pegawai p') ->join('jabatan j', 'j.id_jabatan = p.jabatan', 'left') ->join('unit_kerja u', 'u.id_unit_kerja = p.unit_kerja', 'left') ->join('golongan g', 'g.id_golongan = p.golongan_pekerjaan', 'left') ->join('kantor k', 'k.id_kantor = p.kantor', 'left') ->join('jadwal jd', 'jd.id_jadwal = p.jadwal', 'left'); $this->applyCabangPegawaiAlias($countB, 'p', $cabangKantorId); $this->applyPegawaiSearchFilter($countB, $search); $total = (int) $countB->countAllResults(); $b = $this->db->table('pegawai p') ->select('p.id_pegawai, p.nip, p.nama_lengkap, p.jenis_kelamin, p.photo, p.email, p.jabatan, p.unit_kerja, p.golongan_pekerjaan, p.kantor, p.jadwal, p.status_kepegawaian, p.super_akses') ->select('j.nama_jabatan, u.nama_unit_kerja, g.nama_golongan, k.nama_kantor, jd.nama_jadwal') ->join('jabatan j', 'j.id_jabatan = p.jabatan', 'left') ->join('unit_kerja u', 'u.id_unit_kerja = p.unit_kerja', 'left') ->join('golongan g', 'g.id_golongan = p.golongan_pekerjaan', 'left') ->join('kantor k', 'k.id_kantor = p.kantor', 'left') ->join('jadwal jd', 'jd.id_jadwal = p.jadwal', 'left'); $this->applyCabangPegawaiAlias($b, 'p', $cabangKantorId); $this->applyPegawaiSearchFilter($b, $search); $rows = $b->orderBy('p.nama_lengkap', 'ASC') ->limit($perPage, $offset) ->get() ->getResultArray(); return [ 'status' => 1, 'pesan' => 'OK', 'data' => [ 'rows' => $rows, 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'total_page' => (int) ceil($total / $perPage), ], ]; } /** * @return array{status: int, pesan: string, data?: mixed} */ public function pegawaiShow(int $id, ?int $cabangKantorId = null): array { $row = $this->fetchPegawaiDetail($id); if ($row === null) { return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan']; } if (! $this->pegawaiRowInCabang($row, $cabangKantorId)) { return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan']; } return ['status' => 1, 'pesan' => 'OK', 'data' => $row]; } /** * @param array $input * * @return array{status: int, pesan: string, data?: mixed} */ public function pegawaiCreate(array $input, ?int $cabangKantorId = null): array { $validation = Services::validation(); $validation->setRules([ 'nip' => 'required|max_length[50]|is_unique[pegawai.nip]', 'nama_lengkap' => 'required|max_length[50]', 'jenis_kelamin' => 'required|in_list[Pria,Wanita]', 'tempat_lahir' => 'permit_empty|max_length[100]', 'tanggal_lahir' => 'permit_empty|valid_date', 'email' => 'permit_empty|max_length[50]', 'jabatan' => 'required|integer', 'unit_kerja' => 'required|integer', 'golongan_pekerjaan' => 'required|integer', 'kantor' => 'required|integer', 'status_kepegawaian' => 'required|in_list[Kontrak,Pegawai Tetap]', 'tanggal_bergabung' => 'required|valid_date', 'jadwal' => 'required|integer', 'super_akses' => 'permit_empty|in_list[false,true]', 'photo' => 'permit_empty|max_length[255]', 'username' => 'required|max_length[50]|is_unique[pegawai.username]', 'password' => 'permit_empty|max_length[50]', ]); if (! $validation->run($input)) { return ['status' => 0, 'pesan' => implode(' ', $validation->getErrors())]; } if ($cabangKantorId !== null && $cabangKantorId > 0 && (int) $input['kantor'] !== $cabangKantorId) { return ['status' => 0, 'pesan' => 'Cabang pegawai harus sama dengan cabang Anda.']; } $nip = (string) $input['nip']; $password = isset($input['password']) && (string) $input['password'] !== '' ? md5((string) $input['password']) : md5($nip); $model = new PegawaiModel(); $kantorVal = $cabangKantorId !== null && $cabangKantorId > 0 ? $cabangKantorId : (int) $input['kantor']; $photoIn = trim((string) ($input['photo'] ?? '')); $data = [ 'nip' => $nip, 'nama_lengkap' => (string) $input['nama_lengkap'], 'jenis_kelamin' => (string) $input['jenis_kelamin'], 'tempat_lahir' => (string) ($input['tempat_lahir'] ?? ''), 'tanggal_lahir' => $this->normalizeDate($input['tanggal_lahir'] ?? null), 'photo' => $photoIn === '' || $photoIn === '-' ? '' : substr($photoIn, 0, 255), 'email' => (string) ($input['email'] ?? ''), 'jabatan' => (int) $input['jabatan'], 'unit_kerja' => (int) $input['unit_kerja'], 'golongan_pekerjaan' => (int) $input['golongan_pekerjaan'], 'kantor' => $kantorVal, 'status_kepegawaian' => (string) $input['status_kepegawaian'], 'tanggal_bergabung' => (string) $input['tanggal_bergabung'], 'jadwal' => (int) $input['jadwal'], 'super_akses' => (string) ($input['super_akses'] ?? 'false'), 'username' => (string) $input['username'], 'password' => $password, 'token' => '', 'last_login' => null, ]; if ($model->insert($data, true) === false) { return ['status' => 0, 'pesan' => implode(' ', $model->errors())]; } $id = (int) $model->getInsertID(); return ['status' => 1, 'pesan' => 'Data berhasil disimpan', 'data' => ['id_pegawai' => $id]]; } /** * @param array $input * * @return array{status: int, pesan: string, data?: mixed} */ public function pegawaiUpdate(int $id, array $input, ?int $cabangKantorId = null): array { $model = new PegawaiModel(); if ($model->find($id) === null) { return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan']; } if (! $this->pegawaiIdAllowedForCabang($id, $cabangKantorId)) { return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan']; } $validation = Services::validation(); $validation->setRules([ 'nip' => "required|max_length[50]|is_unique[pegawai.nip,id_pegawai,{$id}]", 'nama_lengkap' => 'required|max_length[50]', 'jenis_kelamin' => 'required|in_list[Pria,Wanita]', 'tempat_lahir' => 'permit_empty|max_length[100]', 'tanggal_lahir' => 'permit_empty|valid_date', 'email' => 'permit_empty|max_length[50]', 'jabatan' => 'required|integer', 'unit_kerja' => 'required|integer', 'golongan_pekerjaan' => 'required|integer', 'kantor' => 'required|integer', 'status_kepegawaian' => 'required|in_list[Kontrak,Pegawai Tetap]', 'tanggal_bergabung' => 'required|valid_date', 'jadwal' => 'required|integer', 'super_akses' => 'permit_empty|in_list[false,true]', 'photo' => 'permit_empty|max_length[255]', 'username' => "required|max_length[50]|is_unique[pegawai.username,id_pegawai,{$id}]", 'password' => 'permit_empty|max_length[50]', ]); if (! $validation->run($input)) { return ['status' => 0, 'pesan' => implode(' ', $validation->getErrors())]; } if ($cabangKantorId !== null && $cabangKantorId > 0 && (int) $input['kantor'] !== $cabangKantorId) { return ['status' => 0, 'pesan' => 'Cabang pegawai harus sama dengan cabang Anda.']; } $kantorVal = $cabangKantorId !== null && $cabangKantorId > 0 ? $cabangKantorId : (int) $input['kantor']; $data = [ 'nip' => (string) $input['nip'], 'nama_lengkap' => (string) $input['nama_lengkap'], 'jenis_kelamin' => (string) $input['jenis_kelamin'], 'tempat_lahir' => (string) ($input['tempat_lahir'] ?? ''), 'tanggal_lahir' => $this->normalizeDate($input['tanggal_lahir'] ?? null), 'email' => (string) ($input['email'] ?? ''), 'jabatan' => (int) $input['jabatan'], 'unit_kerja' => (int) $input['unit_kerja'], 'golongan_pekerjaan' => (int) $input['golongan_pekerjaan'], 'kantor' => $kantorVal, 'status_kepegawaian' => (string) $input['status_kepegawaian'], 'tanggal_bergabung' => (string) $input['tanggal_bergabung'], 'jadwal' => (int) $input['jadwal'], 'super_akses' => (string) ($input['super_akses'] ?? 'false'), 'username' => (string) $input['username'], ]; if (array_key_exists('photo', $input)) { $p = trim((string) $input['photo']); $data['photo'] = $p === '' || $p === '-' ? '' : substr($p, 0, 255); } if (isset($input['password']) && (string) $input['password'] !== '') { $data['password'] = md5((string) $input['password']); } if ($model->update($id, $data) === false) { return ['status' => 0, 'pesan' => implode(' ', $model->errors())]; } return ['status' => 1, 'pesan' => 'Data berhasil diperbarui']; } /** * @return array{status: int, pesan: string} */ public function pegawaiDelete(int $id, ?int $cabangKantorId = null): array { $model = new PegawaiModel(); if ($model->find($id) === null) { return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan']; } if (! $this->pegawaiIdAllowedForCabang($id, $cabangKantorId)) { return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan']; } try { $model->delete($id, true); } catch (\Throwable $e) { return ['status' => 0, 'pesan' => 'Tidak dapat menghapus pegawai (kemungkinan masih direferensikan data lain).']; } return ['status' => 1, 'pesan' => 'Data pegawai telah dihapus']; } /** * Setel ulang password ke md5(NIP) dan kosongkan token (seperti CI3 admin). * * @return array{status: int, pesan: string} */ public function pegawaiResetPassword(int $id, ?int $cabangKantorId = null): array { $row = $this->db->table('pegawai')->select('id_pegawai, nip')->where('id_pegawai', $id)->get()->getRowArray(); if ($row === null) { return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan']; } if (! $this->pegawaiIdAllowedForCabang($id, $cabangKantorId)) { return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan']; } $this->db->table('pegawai')->where('id_pegawai', $id)->update([ 'password' => md5((string) $row['nip']), 'token' => '', ]); return ['status' => 1, 'pesan' => 'Password direset ke NIP (hash MD5) dan token dikosongkan.']; } /** * Batasi query ke baris presensi yang sudah ada minimal satu rekam nyata. * Baris "kosong" dari API mobile presensi_today (pegawai buka app → insert stub) tidak ikut. */ protected function applyPresensiHasMinimalRekam(BaseBuilder $builder): void { $builder->where( '( (pr.jam_masuk IS NOT NULL AND TRIM(CAST(pr.jam_masuk AS CHAR)) NOT IN (\'\',\'00:00\',\'00:00:00\')) OR (pr.jam_pulang IS NOT NULL AND TRIM(CAST(pr.jam_pulang AS CHAR)) NOT IN (\'\',\'00:00\',\'00:00:00\')) OR (pr.mulai_istirahat IS NOT NULL AND TRIM(CAST(pr.mulai_istirahat AS CHAR)) NOT IN (\'\',\'00:00\',\'00:00:00\')) OR (pr.beres_istirahat IS NOT NULL AND TRIM(CAST(pr.beres_istirahat AS CHAR)) NOT IN (\'\',\'00:00\',\'00:00:00\')) OR (pr.photo_masuk IS NOT NULL AND TRIM(CAST(pr.photo_masuk AS CHAR)) <> \'\') OR (pr.photo_pulang IS NOT NULL AND TRIM(CAST(pr.photo_pulang AS CHAR)) <> \'\') )', null, false ); } /** * Daftar pegawai (sesuai cabang) yang hari ini belum presensi minimal dan bukan cuti Approve. * * @return list */ private function belumRekamPegawaiHariIni(string $tanggalHariIni, ?int $cabangKantorId): array { $hadPresensi = $this->db->table('presensi pr') ->select('pr.pegawai') ->distinct() ->join('pegawai pg', 'pg.id_pegawai = pr.pegawai', 'inner') ->where('pr.tanggal', $tanggalHariIni); $this->applyPresensiHasMinimalRekam($hadPresensi); $this->applyCabangPegawaiAlias($hadPresensi, 'pg', $cabangKantorId); $idsP = []; foreach ($hadPresensi->get()->getResultArray() as $r) { $id = (int) ($r['pegawai'] ?? 0); if ($id > 0) { $idsP[] = $id; } } $hadCuti = $this->db->table('cuti c') ->select('c.pegawai') ->distinct() ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'inner') ->where('c.tanggal_cuti', $tanggalHariIni) ->where('c.status_cuti', 'Approve'); $this->applyCabangPegawaiAlias($hadCuti, 'p', $cabangKantorId); $idsC = []; foreach ($hadCuti->get()->getResultArray() as $r) { $id = (int) ($r['pegawai'] ?? 0); if ($id > 0) { $idsC[] = $id; } } $exclude = array_values(array_unique(array_merge($idsP, $idsC))); $qb = $this->db->table('pegawai p') ->select('p.id_pegawai, p.nama_lengkap, p.nip') ->orderBy('p.nama_lengkap', 'ASC'); $this->applyCabangPegawaiAlias($qb, 'p', $cabangKantorId); if ($exclude !== []) { $qb->whereNotIn('p.id_pegawai', $exclude); } $out = []; foreach ($qb->get()->getResultArray() as $row) { if (! is_array($row)) { continue; } $out[] = [ 'id_pegawai' => (int) ($row['id_pegawai'] ?? 0), 'nama_lengkap' => (string) ($row['nama_lengkap'] ?? ''), 'nip' => (string) ($row['nip'] ?? ''), ]; } return $out; } /** * @return array{status: int, pesan: string, data?: mixed} */ public function presensiList(string $tanggalDari, string $tanggalSampai, int $page, int $perPage, string $search = '', ?int $cabangKantorId = null): array { $perPage = max(5, min(200, $perPage)); $page = max(1, $page); $offset = ($page - 1) * $perPage; $search = trim($search); $countB = $this->db->table('presensi pr') ->join('pegawai pg', 'pg.id_pegawai = pr.pegawai', 'left') ->where('pr.tanggal >=', $tanggalDari) ->where('pr.tanggal <=', $tanggalSampai); $this->applyPresensiHasMinimalRekam($countB); $this->applyCabangPegawaiAlias($countB, 'pg', $cabangKantorId); if ($search !== '') { $countB->groupStart() ->like('pg.nama_lengkap', $search) ->orLike('pg.nip', $search) ->groupEnd(); } $total = (int) $countB->countAllResults(); $b = $this->db->table('presensi pr') ->select('pr.*, pg.nama_lengkap, pg.nip') ->join('pegawai pg', 'pg.id_pegawai = pr.pegawai', 'left') ->where('pr.tanggal >=', $tanggalDari) ->where('pr.tanggal <=', $tanggalSampai); $this->applyPresensiHasMinimalRekam($b); $this->applyCabangPegawaiAlias($b, 'pg', $cabangKantorId); if ($search !== '') { $b->groupStart() ->like('pg.nama_lengkap', $search) ->orLike('pg.nip', $search) ->groupEnd(); } $rows = $b->orderBy('pr.tanggal', 'DESC') ->orderBy('pg.nama_lengkap', 'ASC') ->limit($perPage, $offset) ->get() ->getResultArray(); return [ 'status' => 1, 'pesan' => 'OK', 'data' => [ 'rows' => $rows, 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'total_page' => (int) ceil($total / $perPage), 'tanggal_dari' => $tanggalDari, 'tanggal_sampai' => $tanggalSampai, 'q' => $search, ], ]; } /** * @return array{status: int, pesan: string, data?: mixed} */ public function presensiShow(int $id, ?int $cabangKantorId = null): array { $row = $this->db->table('presensi pr') ->select('pr.*, pg.nama_lengkap, pg.nip, pg.email, pg.kantor as pegawai_kantor') ->join('pegawai pg', 'pg.id_pegawai = pr.pegawai', 'left') ->where('pr.id_presensi', $id) ->get() ->getRowArray(); if ($row === null) { return ['status' => 0, 'pesan' => 'Data presensi tidak ditemukan']; } if (! $this->pegawaiRowInCabang(['kantor' => $row['pegawai_kantor'] ?? null], $cabangKantorId)) { return ['status' => 0, 'pesan' => 'Data presensi tidak ditemukan']; } unset($row['pegawai_kantor']); return ['status' => 1, 'pesan' => 'OK', 'data' => $row]; } /** * Ringkasan seperti dashboard admin CI3 (Home): pegawai, presensi & cuti per rentang tanggal. * * @return array{status: int, pesan: string, data?: mixed} */ public function laporanSummary(string $tanggalDari, string $tanggalSampai, ?int $cabangKantorId = null): array { $pegawaiBase = $this->db->table('pegawai'); $this->applyCabangPegawaiAlias($pegawaiBase, 'pegawai', $cabangKantorId); $totalPegawai = (int) $pegawaiBase->countAllResults(); $presensiQb = $this->db->table('presensi pr') ->join('pegawai pg', 'pg.id_pegawai = pr.pegawai', 'inner') ->where('pr.tanggal >=', $tanggalDari) ->where('pr.tanggal <=', $tanggalSampai); $this->applyPresensiHasMinimalRekam($presensiQb); $this->applyCabangPegawaiAlias($presensiQb, 'pg', $cabangKantorId); $presensiQ = (int) $presensiQb->countAllResults(); $cutiQb = $this->db->table('cuti c') ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'inner') ->where('c.tanggal_cuti >=', $tanggalDari) ->where('c.tanggal_cuti <=', $tanggalSampai) ->where('c.status_cuti', 'Approve'); $this->applyCabangPegawaiAlias($cutiQb, 'p', $cabangKantorId); $cutiQ = (int) $cutiQb->countAllResults(); $hariIni = date('Y-m-d'); $ph = $this->db->table('presensi pr') ->join('pegawai pg', 'pg.id_pegawai = pr.pegawai', 'inner') ->where('pr.tanggal', $hariIni); $this->applyPresensiHasMinimalRekam($ph); $this->applyCabangPegawaiAlias($ph, 'pg', $cabangKantorId); $presensiHariIni = (int) $ph->countAllResults(); $ch = $this->db->table('cuti c') ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'inner') ->where('c.tanggal_cuti', $hariIni) ->where('c.status_cuti', 'Approve'); $this->applyCabangPegawaiAlias($ch, 'p', $cabangKantorId); $cutiHariIni = (int) $ch->countAllResults(); $belumRekam = max(0, $totalPegawai - $presensiHariIni - $cutiHariIni); return [ 'status' => 1, 'pesan' => 'OK', 'data' => [ 'total_pegawai' => $totalPegawai, 'rentang' => ['dari' => $tanggalDari, 'sampai' => $tanggalSampai], 'presensi_rekam' => $presensiQ, 'cuti_approve' => $cutiQ, 'hari_ini' => $hariIni, 'presensi_hari_ini' => $presensiHariIni, 'cuti_hari_ini' => $cutiHariIni, 'belum_rekam_hari_ini' => $belumRekam, ], ]; } /** * Dashboard beranda admin — selaras `modules/admin/controllers/Home.php` CI3. * * @param int|null $soloPegawaiId Bila diisi (login panel sebagai pegawai saja), angka & cuti * hanya untuk pegawai tersebut — bukan agregat seluruh perusahaan. * * @return array{status: int, pesan: string, data?: mixed} */ public function dashboardHome(?int $cabangKantorId = null, ?int $soloPegawaiId = null): array { $tanggalHariIni = date('Y-m-d'); if ($soloPegawaiId !== null && $soloPegawaiId > 0) { return $this->dashboardHomeSoloPegawai($tanggalHariIni, $soloPegawaiId); } $pb = $this->db->table('pegawai'); $this->applyCabangPegawaiAlias($pb, 'pegawai', $cabangKantorId); $totalPegawai = (int) $pb->countAllResults(); $pbL = $this->db->table('pegawai')->where('jenis_kelamin', 'Pria'); $this->applyCabangPegawaiAlias($pbL, 'pegawai', $cabangKantorId); $pegawaiLaki = (int) $pbL->countAllResults(); $pbP = $this->db->table('pegawai')->where('jenis_kelamin', 'Wanita'); $this->applyCabangPegawaiAlias($pbP, 'pegawai', $cabangKantorId); $pegawaiPerempuan = (int) $pbP->countAllResults(); $ph = $this->db->table('presensi pr') ->join('pegawai pg', 'pg.id_pegawai = pr.pegawai', 'inner') ->where('pr.tanggal', $tanggalHariIni); $this->applyPresensiHasMinimalRekam($ph); $this->applyCabangPegawaiAlias($ph, 'pg', $cabangKantorId); $presensiHariIni = (int) $ph->countAllResults(); $ch = $this->db->table('cuti c') ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'inner') ->where('c.tanggal_cuti', $tanggalHariIni) ->where('c.status_cuti', 'Approve'); $this->applyCabangPegawaiAlias($ch, 'p', $cabangKantorId); $cutiHariIni = (int) $ch->countAllResults(); $belumRekam = max(0, $totalPegawai - $presensiHariIni - $cutiHariIni); $belumRekamPegawai = $this->belumRekamPegawaiHariIni($tanggalHariIni, $cabangKantorId); $persenLaki = $totalPegawai > 0 ? round(($pegawaiLaki / $totalPegawai) * 100, 1) : 0.0; $persenPerempuan = $totalPegawai > 0 ? round(($pegawaiPerempuan / $totalPegawai) * 100, 1) : 0.0; $persenPresensi = $totalPegawai > 0 ? round(($presensiHariIni / $totalPegawai) * 100, 1) : 0.0; $persenCuti = $totalPegawai > 0 ? round(($cutiHariIni / $totalPegawai) * 100, 1) : 0.0; $persenBelumRekam = $totalPegawai > 0 ? round(($belumRekam / $totalPegawai) * 100, 1) : 0.0; $permohonanCutiB = $this->db->table('cuti c') ->select('c.id_cuti, c.pegawai, DATE_FORMAT(c.tanggal_cuti, \'%Y-%m-%d\') AS tanggal_cuti, c.tipe_cuti, c.alasan_cuti, c.status_cuti, p.nama_lengkap, p.nip', false) ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'left') ->where('c.status_cuti', 'Waiting'); $this->applyCabangPegawaiAlias($permohonanCutiB, 'p', $cabangKantorId); $permohonanCuti = $permohonanCutiB->orderBy('c.tanggal_cuti', 'ASC') ->limit(5) ->get() ->getResultArray(); $permohonanCuti = array_map( fn ($r): array => is_array($r) ? $this->normalizeCutiRowArrayForApi($r) : [], $permohonanCuti, ); $tpc = $this->db->table('cuti c') ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'inner') ->where('c.status_cuti', 'Waiting'); $this->applyCabangPegawaiAlias($tpc, 'p', $cabangKantorId); $totalPermohonanCuti = (int) $tpc->countAllResults(); return [ 'status' => 1, 'pesan' => 'OK', 'data' => [ 'tanggal_hari_ini' => $tanggalHariIni, 'total_pegawai' => $totalPegawai, 'pegawai_laki' => $pegawaiLaki, 'pegawai_perempuan' => $pegawaiPerempuan, 'presensi_hari_ini' => $presensiHariIni, 'cuti_hari_ini' => $cutiHariIni, 'belum_rekam' => $belumRekam, 'persen_laki' => $persenLaki, 'persen_perempuan' => $persenPerempuan, 'persen_presensi' => $persenPresensi, 'persen_cuti' => $persenCuti, 'persen_belum_rekam' => $persenBelumRekam, 'permohonan_cuti' => $permohonanCuti, 'total_permohonan_cuti' => $totalPermohonanCuti, 'belum_rekam_pegawai' => $belumRekamPegawai, ], ]; } /** * Ringkasan beranda untuk satu pegawai (sesi login mobile / non-Ion). * * @return array{status: int, pesan: string, data?: mixed} */ private function dashboardHomeSoloPegawai(string $tanggalHariIni, int $pegawaiId): array { $row = $this->db->table('pegawai')->select('id_pegawai, jenis_kelamin')->where('id_pegawai', $pegawaiId)->get()->getRowArray(); if ($row === null) { return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan']; } $jk = (string) ($row['jenis_kelamin'] ?? ''); $laki = $jk === 'Pria' ? 1 : 0; $wanit = $jk === 'Wanita' ? 1 : 0; $ph = $this->db->table('presensi pr') ->where('pr.pegawai', $pegawaiId) ->where('pr.tanggal', $tanggalHariIni); $this->applyPresensiHasMinimalRekam($ph); $presensiHariIni = (int) $ph->countAllResults(); $ch = $this->db->table('cuti c') ->where('c.pegawai', $pegawaiId) ->where('c.tanggal_cuti', $tanggalHariIni) ->where('c.status_cuti', 'Approve'); $cutiHariIni = (int) $ch->countAllResults(); $belumRekam = max(0, 1 - $presensiHariIni - $cutiHariIni); $permohonanCutiB = $this->db->table('cuti c') ->select('c.id_cuti, c.pegawai, DATE_FORMAT(c.tanggal_cuti, \'%Y-%m-%d\') AS tanggal_cuti, c.tipe_cuti, c.alasan_cuti, c.status_cuti, p.nama_lengkap, p.nip', false) ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'left') ->where('c.pegawai', $pegawaiId) ->where('c.status_cuti', 'Waiting'); $permohonanCuti = $permohonanCutiB->orderBy('c.tanggal_cuti', 'ASC') ->limit(5) ->get() ->getResultArray(); $permohonanCuti = array_map( fn ($r): array => is_array($r) ? $this->normalizeCutiRowArrayForApi($r) : [], $permohonanCuti, ); $tpc = $this->db->table('cuti c') ->where('c.pegawai', $pegawaiId) ->where('c.status_cuti', 'Waiting'); $totalPermohonanCuti = (int) $tpc->countAllResults(); $totalPegawai = 1; $persenLaki = $laki === 1 ? 100.0 : 0.0; $persenWanita = $wanit === 1 ? 100.0 : 0.0; if ($laki === 0 && $wanit === 0) { $persenLaki = 0.0; $persenWanita = 0.0; } $persenPresensi = $presensiHariIni >= 1 ? 100.0 : 0.0; $persenCuti = $cutiHariIni >= 1 ? 100.0 : 0.0; $persenBelumRekam = $belumRekam >= 1 ? 100.0 : 0.0; return [ 'status' => 1, 'pesan' => 'OK', 'data' => [ 'tanggal_hari_ini' => $tanggalHariIni, 'total_pegawai' => $totalPegawai, 'pegawai_laki' => $laki, 'pegawai_perempuan' => $wanit, 'presensi_hari_ini' => $presensiHariIni, 'cuti_hari_ini' => $cutiHariIni, 'belum_rekam' => $belumRekam, 'persen_laki' => $persenLaki, 'persen_perempuan' => $persenWanita, 'persen_presensi' => $persenPresensi, 'persen_cuti' => $persenCuti, 'persen_belum_rekam' => $persenBelumRekam, 'permohonan_cuti' => $permohonanCuti, 'total_permohonan_cuti' => $totalPermohonanCuti, 'belum_rekam_pegawai' => [], ], ]; } /** * @return array{status: int, pesan: string, data?: mixed} */ public function cutiList(string $status, int $page, int $perPage, ?int $cabangKantorId = null): array { $allowed = ['', 'Waiting', 'Approve', 'Rejected', 'Cancelled']; if (! in_array($status, $allowed, true)) { $status = 'Waiting'; } $perPage = max(5, min(200, $perPage)); $page = max(1, $page); $offset = ($page - 1) * $perPage; $countB = $this->db->table('cuti c')->join('pegawai p', 'p.id_pegawai = c.pegawai', 'left'); $this->applyCabangPegawaiAlias($countB, 'p', $cabangKantorId); if ($status !== '') { $countB->where('c.status_cuti', $status); } $total = (int) $countB->countAllResults(); $b = $this->db->table('cuti c') ->select('c.id_cuti, c.pegawai, DATE_FORMAT(c.tanggal_cuti, \'%Y-%m-%d\') AS tanggal_cuti, c.tipe_cuti, c.alasan_cuti, c.status_cuti, c.alasan_tolak, p.nama_lengkap, p.nip', false) ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'left'); $this->applyCabangPegawaiAlias($b, 'p', $cabangKantorId); if ($status !== '') { $b->where('c.status_cuti', $status); } $rows = $b->orderBy('c.tanggal_cuti', 'DESC') ->orderBy('c.id_cuti', 'DESC') ->limit($perPage, $offset) ->get() ->getResultArray(); $rows = array_map( fn ($r): array => is_array($r) ? $this->normalizeCutiRowArrayForApi($r) : [], $rows, ); return [ 'status' => 1, 'pesan' => 'OK', 'data' => [ 'rows' => $rows, 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'total_page' => (int) ceil($total / $perPage), 'status_filter'=> $status, ], ]; } /** * @return array{status: int, pesan: string, data?: mixed} */ public function cutiShow(int $id, ?int $cabangKantorId = null): array { $row = $this->db->table('cuti c') ->select('c.id_cuti, c.pegawai, DATE_FORMAT(c.tanggal_cuti, \'%Y-%m-%d\') AS tanggal_cuti, c.tipe_cuti, c.alasan_cuti, c.status_cuti, c.alasan_tolak, p.nama_lengkap, p.nip, p.email, p.kantor as pegawai_kantor', false) ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'left') ->where('c.id_cuti', $id) ->get() ->getRowArray(); if ($row === null) { return ['status' => 0, 'pesan' => 'Data cuti tidak ditemukan']; } $row = $this->normalizeCutiRowArrayForApi($row); if (! $this->pegawaiRowInCabang(['kantor' => $row['pegawai_kantor'] ?? null], $cabangKantorId)) { return ['status' => 0, 'pesan' => 'Data cuti tidak ditemukan']; } unset($row['pegawai_kantor']); $dok = $this->db->table('cuti_dokumen')->select('dokumen')->where('cuti', $id)->get()->getResultArray(); return [ 'status' => 1, 'pesan' => 'OK', 'data' => [ 'cuti' => $row, 'dokumen' => $dok, ], ]; } /** * @return array{status: int, pesan: string} */ public function cutiApprove(int $id, ?int $cabangKantorId = null): array { $exists = $this->db->table('cuti')->select('id_cuti')->where('id_cuti', $id)->get()->getRowArray(); if ($exists === null) { return ['status' => 0, 'pesan' => 'Data cuti tidak ditemukan']; } if (! $this->cutiAllowedForCabang($id, $cabangKantorId)) { return ['status' => 0, 'pesan' => 'Data cuti tidak ditemukan']; } $this->db->table('cuti')->where('id_cuti', $id)->update([ 'status_cuti' => 'Approve', 'alasan_tolak' => '', ]); return ['status' => 1, 'pesan' => 'Cuti disetujui.']; } /** * @return array{status: int, pesan: string} */ public function cutiReject(int $id, string $alasanTolak, ?int $cabangKantorId = null): array { $exists = $this->db->table('cuti')->select('id_cuti')->where('id_cuti', $id)->get()->getRowArray(); if ($exists === null) { return ['status' => 0, 'pesan' => 'Data cuti tidak ditemukan']; } if (! $this->cutiAllowedForCabang($id, $cabangKantorId)) { return ['status' => 0, 'pesan' => 'Data cuti tidak ditemukan']; } $alasanTolak = trim($alasanTolak); if ($alasanTolak === '') { return ['status' => 0, 'pesan' => 'Alasan penolakan wajib diisi.']; } $this->db->table('cuti')->where('id_cuti', $id)->update([ 'status_cuti' => 'Rejected', 'alasan_tolak' => $alasanTolak, ]); return ['status' => 1, 'pesan' => 'Cuti ditolak.']; } /** * @return array|null */ private function fetchPegawaiDetail(int $id): ?array { $row = $this->db->table('pegawai p') ->select('p.*, j.nama_jabatan, u.nama_unit_kerja, g.nama_golongan, k.nama_kantor, jd.nama_jadwal') ->join('jabatan j', 'j.id_jabatan = p.jabatan', 'left') ->join('unit_kerja u', 'u.id_unit_kerja = p.unit_kerja', 'left') ->join('golongan g', 'g.id_golongan = p.golongan_pekerjaan', 'left') ->join('kantor k', 'k.id_kantor = p.kantor', 'left') ->join('jadwal jd', 'jd.id_jadwal = p.jadwal', 'left') ->where('p.id_pegawai', $id) ->get() ->getRowArray(); if ($row !== null) { unset($row['password'], $row['token']); } return $row; } private function applyCabangPegawaiAlias(BaseBuilder $b, string $alias, ?int $cabangKantorId): void { if ($cabangKantorId !== null && $cabangKantorId > 0) { $b->where("{$alias}.kantor", $cabangKantorId); } } private function pegawaiIdAllowedForCabang(int $pegawaiId, ?int $cabangKantorId): bool { if ($cabangKantorId === null || $cabangKantorId <= 0) { return true; } return $this->db->table('pegawai')->where('id_pegawai', $pegawaiId)->where('kantor', $cabangKantorId)->countAllResults() > 0; } /** * @param array $row baris pegawai (key `kantor` = id_kantor) */ private function pegawaiRowInCabang(array $row, ?int $cabangKantorId): bool { if ($cabangKantorId === null || $cabangKantorId <= 0) { return true; } return (int) ($row['kantor'] ?? 0) === $cabangKantorId; } private function cutiAllowedForCabang(int $cutiId, ?int $cabangKantorId): bool { if ($cabangKantorId === null || $cabangKantorId <= 0) { return true; } $row = $this->db->table('cuti c') ->select('p.kantor') ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'left') ->where('c.id_cuti', $cutiId) ->get() ->getRowArray(); return $row !== null && (int) ($row['kantor'] ?? 0) === $cabangKantorId; } private function applyPegawaiSearchFilter(BaseBuilder $b, string $search): void { if ($search === '') { return; } $b->groupStart() ->like('p.nama_lengkap', $search) ->orLike('p.nip', $search) ->orLike('p.username', $search) ->groupEnd(); } private function normalizeDate(mixed $v): ?string { if ($v === null || $v === '' || $v === '0000-00-00') { return null; } $s = (string) $v; return $s; } /** * Kunci baris cuti ke huruf kecil + rapikan bentuk tanggal_cuti setelah JSON (objek/array). * * @param array $row * * @return array */ private function normalizeCutiRowArrayForApi(array $row): array { $row = array_change_key_case($row, CASE_LOWER); $tc = $row['tanggal_cuti'] ?? null; if (is_array($tc)) { $row['tanggal_cuti'] = $tc['date'] ?? $tc['value'] ?? null; } return $row; } }