Files
bij/app/Services/Admin/AdminApiService.php
2026-04-21 05:59:39 +07:00

978 lines
39 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Admin;
use App\Models\PegawaiModel;
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Database\BaseConnection;
use Config\Database;
use Config\Services;
/**
* Logika API admin (/api/admin/*) — akses DB hanya dari sini, bukan dari controller web admin.
*/
class AdminApiService
{
protected BaseConnection $db;
public function __construct(?BaseConnection $db = null)
{
$this->db = $db ?? Database::connect();
}
/**
* @return array<string, mixed>|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<string, scalar|null> $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<string, scalar|null> $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<array{id_pegawai: int, nama_lengkap: string, nip: string}>
*/
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<string, mixed>|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<string, mixed> $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<string, mixed> $row
*
* @return array<string, mixed>
*/
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;
}
}