Initial commit BIJ CI4

This commit is contained in:
BIJ Dev
2026-04-21 05:49:17 +07:00
commit fa38ac6b24
13170 changed files with 866701 additions and 0 deletions

View File

@@ -0,0 +1,977 @@
<?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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,263 @@
<?php
declare(strict_types=1);
namespace App\Services\Admin;
use App\Services\Mobile\MobileJsonService;
use CodeIgniter\Database\BaseConnection;
use Config\Database;
/**
* Login admin memakai Ion Auth CI3: `admin_users` + relasi `admin_users_groups` / `admin_groups`.
* Password: bcrypt ($2a$/$2y$) atau legacy MD5 (32 hex).
* Setelah sukses, token API ditulis ke baris pegawai "proxy" (sama seperti sebelumnya).
*/
class AdminUsersLoginService
{
protected BaseConnection $db;
public function __construct(?BaseConnection $db = null)
{
$this->db = $db ?? Database::connect();
}
/**
* @return array{
* ok: true,
* token: string,
* username: string,
* admin_user_id: int,
* group_names: list<string>,
* group_ids: list<int>
* }|array{ok: false, reason: 'skip'|'invalid'|'no_group'|'no_proxy'}
*/
/**
* Cari id_pegawai dari identifier login (username pegawai atau NIP).
*/
public function resolvePegawaiIdFromCredentials(string $username): ?int
{
$username = trim($username);
if ($username === '') {
return null;
}
$p = $this->db->table('pegawai')->select('id_pegawai')->where('username', $username)->get()->getRowArray();
if ($p !== null && ! empty($p['id_pegawai'])) {
return (int) $p['id_pegawai'];
}
$p = $this->db->table('pegawai')->select('id_pegawai')->where('nip', $username)->get()->getRowArray();
if ($p !== null && ! empty($p['id_pegawai'])) {
return (int) $p['id_pegawai'];
}
return null;
}
/**
* Pegawai yang di baris `admin_users` punya `id_pegawai` sama + aktif + punya grup Ion
* → panel memakai RBAC grup (supervisor, HRD, dll.), bukan mode pegawai panel tipis.
*
* @return array{admin_user_id: int, username: string, group_names: list<string>}|null
*/
public function findLinkedAdminForPegawaiId(int $pegawaiId): ?array
{
if ($pegawaiId <= 0 || ! $this->db->tableExists('admin_users')) {
return null;
}
if (! $this->db->fieldExists('id_pegawai', 'admin_users')) {
return null;
}
$row = $this->db->table('admin_users')
->where('id_pegawai', $pegawaiId)
->where('active', 1)
->orderBy('id', 'ASC')
->get()
->getRowArray();
if ($row === null) {
return null;
}
$userId = (int) ($row['id'] ?? 0);
if ($userId <= 0) {
return null;
}
$groups = $this->loadAdminGroupsForUser($userId);
if ($this->db->tableExists('admin_users_groups') && $groups['names'] === []) {
return null;
}
return [
'admin_user_id' => $userId,
'username' => (string) ($row['username'] ?? ''),
'group_names' => $groups['names'],
];
}
public function tryLogin(string $username, string $password): array
{
if ($username === '' || $password === '') {
return ['ok' => false, 'reason' => 'skip'];
}
if (! $this->db->tableExists('admin_users')) {
return ['ok' => false, 'reason' => 'skip'];
}
$row = $this->db->table('admin_users')
->where('username', $username)
->where('active', 1)
->get()
->getRowArray();
if ($row === null || empty($row['password'])) {
return ['ok' => false, 'reason' => 'invalid'];
}
$hash = (string) $row['password'];
if (! $this->verifyAdminPassword($password, $hash)) {
return ['ok' => false, 'reason' => 'invalid'];
}
$userId = (int) $row['id'];
$groups = $this->loadAdminGroupsForUser($userId);
if ($this->db->tableExists('admin_users_groups') && $groups === []) {
return ['ok' => false, 'reason' => 'no_group'];
}
$proxyId = $this->resolveProxyPegawaiId($groups, $row);
if ($proxyId === null) {
return ['ok' => false, 'reason' => 'no_proxy'];
}
$mobile = new MobileJsonService($this->db);
$token = $mobile->issueTokenForPegawaiId($proxyId);
if ($token === null) {
return ['ok' => false, 'reason' => 'no_proxy'];
}
$this->db->table('admin_users')->where('id', $userId)->update([
'last_login' => time(),
]);
return [
'ok' => true,
'token' => $token,
'username' => $username,
'admin_user_id' => $userId,
'group_names' => $groups['names'],
'group_ids' => $groups['ids'],
];
}
/**
* @return array{names: list<string>, ids: list<int>}
*/
private function loadAdminGroupsForUser(int $userId): array
{
if (! $this->db->tableExists('admin_users_groups') || ! $this->db->tableExists('admin_groups')) {
return ['names' => [], 'ids' => []];
}
$rows = $this->db->table('admin_users_groups ug')
->select('g.id, g.name')
->join('admin_groups g', 'g.id = ug.group_id', 'inner')
->where('ug.user_id', $userId)
->orderBy('g.id', 'ASC')
->get()
->getResultArray();
$names = [];
$ids = [];
foreach ($rows as $r) {
$ids[] = (int) ($r['id'] ?? 0);
$names[] = (string) ($r['name'] ?? '');
}
return ['names' => $names, 'ids' => $ids];
}
/**
* Proxy pegawai untuk token API. Bisa diarahkan per grup (mis. HRD) lewat .env.
*
* @param array{names: list<string>, ids: list<int>} $groups
* @param array<string, mixed>|null $adminRow Baris admin_users (untuk id_pegawai per-akun).
*/
private function resolveProxyPegawaiId(array $groups, ?array $adminRow = null): ?int
{
if ($adminRow !== null && $this->db->fieldExists('id_pegawai', 'admin_users')) {
$assigned = (int) ($adminRow['id_pegawai'] ?? 0);
if ($assigned > 0 && $this->pegawaiExists($assigned)) {
return $assigned;
}
}
$fromEnv = env('ADMIN_LOGIN_PROXY_PEGAWAI_ID');
if (is_string($fromEnv) && $fromEnv !== '') {
$id = (int) $fromEnv;
if ($id > 0 && $this->pegawaiExists($id)) {
return $id;
}
}
$hrdOnly = in_array('hrd', array_map('strtolower', $groups['names']), true)
&& ! in_array('webmaster', array_map('strtolower', $groups['names']), true);
if ($hrdOnly) {
$hrdEnv = env('ADMIN_LOGIN_PROXY_PEGAWAI_ID_HRD');
if (is_string($hrdEnv) && $hrdEnv !== '') {
$hid = (int) $hrdEnv;
if ($hid > 0 && $this->pegawaiExists($hid)) {
return $hid;
}
}
}
$super = $this->db->table('pegawai')
->select('id_pegawai')
->where('super_akses', 'true')
->orderBy('id_pegawai', 'ASC')
->limit(1)
->get()
->getRowArray();
if ($super !== null && ! empty($super['id_pegawai'])) {
return (int) $super['id_pegawai'];
}
$any = $this->db->table('pegawai')
->select('id_pegawai')
->orderBy('id_pegawai', 'ASC')
->limit(1)
->get()
->getRowArray();
if ($any !== null && ! empty($any['id_pegawai'])) {
return (int) $any['id_pegawai'];
}
return null;
}
private function verifyAdminPassword(string $plain, string $hash): bool
{
if (str_starts_with($hash, '$2y$') || str_starts_with($hash, '$2a$') || str_starts_with($hash, '$2b$')) {
return password_verify($plain, $hash);
}
if (strlen($hash) === 32 && ctype_xdigit($hash)) {
return md5($plain) === $hash;
}
return password_verify($plain, $hash);
}
private function pegawaiExists(int $id): bool
{
return $this->db->table('pegawai')->where('id_pegawai', $id)->countAllResults() > 0;
}
}

View File

@@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace App\Services;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\HTTP\RequestInterface;
use Config\Database;
use Config\Services;
use Throwable;
/**
* Audit trail untuk aksi admin (API `/api/admin/*` dan keamanan).
*/
class AdminAuditService
{
private const MAX_PAYLOAD_CHARS = 65535;
private BaseConnection $db;
public function __construct(?BaseConnection $db = null)
{
$this->db = $db ?? Database::connect();
}
/**
* Catat satu baris audit. `payload` disimpan sebagai JSON (disederhanakan & dibatasi panjang).
* Konteks sesi (auth_source, grup Ion) diambil dari session saat dipanggil dari request web.
*
* @param array<string, mixed> $payload
*/
public function log(string $action, array $payload = []): void
{
try {
if (! $this->db->tableExists('admin_activity_logs')) {
log_message('debug', 'AdminAuditService: tabel admin_activity_logs belum ada — jalankan migrasi.');
return;
}
$req = Services::request();
$sess = session();
$outcome = (string) ($payload['__outcome'] ?? 'success');
$body = $payload;
unset($body['__outcome']);
$row = [
'admin_user' => $this->resolveAdminUserLabel($sess, $body),
'action' => $this->truncate($action, 128),
'endpoint' => $this->truncate($req->getUri()->getPath(), 512),
'payload' => $this->encodePayload($this->enrichPayload($req, $sess, $body)),
'ip_address' => $this->truncate($req->getIPAddress(), 45),
'user_agent' => $this->truncate($req->getUserAgent()->__toString(), 512),
'auth_source' => $this->truncate((string) ($sess->get('admin_auth_source') ?? ''), 32) ?: null,
'roles_json' => $this->encodeRolesJson($sess),
'outcome' => $this->truncate($outcome, 32),
'created_at' => date('Y-m-d H:i:s'),
];
$this->db->table('admin_activity_logs')->insert($row);
} catch (Throwable $e) {
log_message('error', 'AdminAuditService::log gagal: ' . $e->getMessage());
}
}
/**
* @param array<string, mixed> $sessPayload
*/
private function resolveAdminUserLabel($sess, array $sessPayload): string
{
$u = $sess->get('admin_username');
if (is_string($u) && $u !== '') {
return $this->truncate($u, 191);
}
$actor = $sessPayload['actor'] ?? null;
if (is_array($actor)) {
$nip = (string) ($actor['nip'] ?? '');
$id = (string) ($actor['id_pegawai'] ?? '');
if ($nip !== '') {
return $this->truncate('pegawai:' . $nip, 191);
}
if ($id !== '') {
return $this->truncate('pegawai:id:' . $id, 191);
}
}
return 'unknown';
}
/**
* @param array<string, mixed> $payload
*
* @return array<string, mixed>
*/
private function enrichPayload(RequestInterface $req, $sess, array $payload): array
{
$out = $this->sanitizeForStorage($payload);
$out['_http'] = [
'method' => $req->getMethod(),
'uri' => (string) $req->getUri(),
];
$out['_session_panel'] = [
'has_admin_token' => is_string($sess->get('admin_mobile_token')) && $sess->get('admin_mobile_token') !== '',
'auth_source' => $sess->get('admin_auth_source'),
'ion_groups' => $sess->get('admin_ion_groups'),
];
return $out;
}
/**
* @param array<string, mixed> $data
*
* @return array<string, mixed>
*/
private function sanitizeForStorage(array $data): array
{
$redactKeys = ['password', 'token', 'admin_mobile_token'];
return $this->stripSensitiveRecursive($data, $redactKeys);
}
/**
* @param array<string, mixed> $data
* @param list<string> $redactKeys
*
* @return array<string, mixed>
*/
private function stripSensitiveRecursive(array $data, array $redactKeys): array
{
$out = [];
foreach ($data as $k => $v) {
$key = (string) $k;
if (in_array(strtolower($key), $redactKeys, true)) {
$out[$key] = '[redacted]';
continue;
}
if (is_array($v)) {
$out[$key] = $this->stripSensitiveRecursive($v, $redactKeys);
} elseif (is_scalar($v) || $v === null) {
$out[$key] = $v;
}
}
return $out;
}
/**
* @param array<string, mixed> $payload
*/
private function encodePayload(array $payload): ?string
{
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
if ($json === false) {
return null;
}
if (strlen($json) > self::MAX_PAYLOAD_CHARS) {
$json = substr($json, 0, self::MAX_PAYLOAD_CHARS - 40) . '…[truncated]';
}
return $json;
}
private function encodeRolesJson($sess): ?string
{
$g = $sess->get('admin_ion_groups');
if (! is_array($g)) {
return null;
}
$enc = json_encode(array_values($g), JSON_UNESCAPED_UNICODE);
if ($enc === false) {
return null;
}
return strlen($enc) > 16000 ? substr($enc, 0, 16000) : $enc;
}
private function truncate(string $s, int $max): string
{
if (strlen($s) <= $max) {
return $s;
}
return substr($s, 0, max(0, $max - 3)) . '...';
}
}

219
app/Services/ApiClient.php Normal file
View File

@@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
namespace App\Services;
use CodeIgniter\HTTP\CURLRequest;
use Config\Services;
use Throwable;
/**
* Klien HTTP ke API internal (`/api/mobile/*` dan `/api/admin/*`).
* Controller admin web tidak mengakses DB; hanya memanggil API lewat kelas ini.
*/
class ApiClient
{
private CURLRequest $client;
public function __construct(?CURLRequest $client = null)
{
$this->client = $client ?? Services::curlrequest([
'timeout' => 30,
'http_errors' => false,
]);
}
/**
* @param array<string, scalar|null> $formData
*
* @return array{transport_ok: bool, http_code: int, json: array<string, mixed>|null, error: string|null, raw: string}
*/
public function postMobile(string $method, array $formData = []): array
{
helper(['url']);
return $this->post('api/mobile/' . ltrim($method, '/'), $formData);
}
/**
* @param array<string, scalar|null> $formData
*
* @return array{transport_ok: bool, http_code: int, json: array<string, mixed>|null, error: string|null, raw: string}
*/
public function postMobileWithToken(string $method, string $token, array $formData = []): array
{
$formData['token'] = $token;
return $this->postMobile($method, $formData);
}
/**
* GET ke `/api/admin/{path}` — token ditambahkan ke query string.
*
* @param array<string, scalar|null> $query
*
* @return array{transport_ok: bool, http_code: int, json: array<string, mixed>|null, error: string|null, raw: string}
*/
public function getAdmin(string $path, ?string $token, array $query = []): array
{
helper(['url']);
if ($token !== null && $token !== '') {
$query['token'] = $token;
}
$forward = $this->adminInternalHeaders();
if (($forward['Cookie'] ?? '') !== '' && session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
return $this->get('api/admin/' . ltrim($path, '/'), $query, $forward);
}
/**
* POST form ke `/api/admin/{path}`.
*
* @param array<string, scalar|null> $formData
*
* @return array{transport_ok: bool, http_code: int, json: array<string, mixed>|null, error: string|null, raw: string}
*/
public function postAdmin(string $path, ?string $token, array $formData = []): array
{
helper(['url']);
if ($token !== null && $token !== '') {
$formData['token'] = $token;
}
$forward = $this->adminInternalHeaders();
if (($forward['Cookie'] ?? '') !== '' && session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
return $this->post('api/admin/' . ltrim($path, '/'), $formData, $forward);
}
/**
* GET generik (relatif terhadap root site, mis. `api/admin/pegawai`).
*
* @param array<string, scalar|null> $query
* @param array<string, string> $extraHeaders
*
* @return array{transport_ok: bool, http_code: int, json: array<string, mixed>|null, error: string|null, raw: string}
*/
public function get(string $path, array $query = [], array $extraHeaders = []): array
{
helper(['url']);
$url = base_url(ltrim($path, '/'));
$base = $this->emptyResult();
$headers = array_merge(
['Accept' => 'application/json'],
$extraHeaders
);
try {
$response = $this->client->get($url, [
'query' => $query,
'headers' => $headers,
]);
} catch (Throwable $e) {
$base['error'] = $e->getMessage();
return $base;
}
return $this->fillFromResponse($base, $response->getStatusCode(), $response->getBody());
}
/**
* POST `application/x-www-form-urlencoded`.
*
* @param array<string, scalar|null> $formData
* @param array<string, string> $extraHeaders
*
* @return array{transport_ok: bool, http_code: int, json: array<string, mixed>|null, error: string|null, raw: string}
*/
public function post(string $path, array $formData = [], array $extraHeaders = []): array
{
helper(['url']);
$url = base_url(ltrim($path, '/'));
$base = $this->emptyResult();
$headers = array_merge(
['Accept' => 'application/json'],
$extraHeaders
);
try {
$response = $this->client->post($url, [
'form_params' => $formData,
'headers' => $headers,
]);
} catch (Throwable $e) {
$base['error'] = $e->getMessage();
return $base;
}
return $this->fillFromResponse($base, $response->getStatusCode(), $response->getBody());
}
/**
* Teruskan cookie browser ke hit internal `/api/admin/*` agar sesi panel sama.
*
* @return array<string, string>
*/
private function adminInternalHeaders(): array
{
$cookie = Services::request()->getHeaderLine('Cookie');
return $cookie !== '' ? ['Cookie' => $cookie] : [];
}
/**
* @param array<string, mixed>|null $json
*/
public static function isSuccess(?array $json): bool
{
return is_array($json) && (int) ($json['status'] ?? 0) === 1;
}
/**
* @return array{transport_ok: bool, http_code: int, json: array<string, mixed>|null, error: string|null, raw: string}
*/
private function emptyResult(): array
{
return [
'transport_ok' => false,
'http_code' => 0,
'json' => null,
'error' => null,
'raw' => '',
];
}
/**
* @param array{transport_ok: bool, http_code: int, json: array<string, mixed>|null, error: string|null, raw: string} $base
*
* @return array{transport_ok: bool, http_code: int, json: array<string, mixed>|null, error: string|null, raw: string}
*/
private function fillFromResponse(array $base, int $statusCode, string $body): array
{
$base['http_code'] = $statusCode;
$base['raw'] = $body;
$base['transport_ok'] = $statusCode === 200;
$decoded = json_decode($body, true);
if (! is_array($decoded)) {
$base['error'] = 'Respons bukan JSON objek';
return $base;
}
$base['json'] = $decoded;
return $base;
}
}

View File

@@ -0,0 +1,786 @@
<?php
namespace App\Services\Mobile;
use CodeIgniter\Database\BaseConnection;
use Config\Database;
use Exception;
/**
* Port logika bisnis dari CI3 `application/controllers/Json.php`.
* Respons struktur disamakan dengan legacy untuk kompatibilitas aplikasi mobile.
*
* TODO (fase berikutnya): pecah per agregat (PresensiService, CutiService), tambahkan transaksi,
* perbaiki keamanan (MD5 → password_hash, token → JWT) setelah cutover klien.
*/
class MobileJsonService
{
protected BaseConnection $db;
/** @var array<int, string> */
private array $hariIndo = [
1 => 'Senin',
2 => 'Selasa',
3 => 'Rabu',
4 => 'Kamis',
5 => 'Jumat',
6 => 'Sabtu',
7 => 'Minggu',
];
public function __construct(?BaseConnection $db = null)
{
$this->db = $db ?? Database::connect();
}
public function loginWToken(string $token): array
{
$data = [
'status' => 0,
'pesan' => '',
];
$pegawai = $this->db->table('pegawai')->where('token', $token)->get()->getRow();
if ($pegawai) {
unset($pegawai->username, $pegawai->password, $pegawai->token);
$this->db->table('pegawai')->where('id_pegawai', $pegawai->id_pegawai)->update([
'last_login' => date('Y-m-d H:i:s'),
]);
$data['status'] = 1;
}
return $data;
}
public function login(string $user, string $pass): array
{
$data = [
'status' => 0,
'pesan' => 'Username atau Password tidak sesuai',
];
$pegawai = $this->db->table('pegawai')
->where('username', $user)
->where('password', md5($pass))
->get()
->getRow();
if ($pegawai) {
unset($pegawai->username, $pegawai->password, $pegawai->token);
$token = md5((string) $pegawai->id_pegawai) . $this->generateRandomString(15);
$this->db->table('pegawai')->where('id_pegawai', $pegawai->id_pegawai)->update([
'token' => $token,
'last_login' => date('Y-m-d H:i:s'),
]);
$data['status'] = 1;
$data['pesan'] = 'Selamat datang';
$data['token'] = $token;
}
return $data;
}
/**
* Terbitkan token API mobile untuk pegawai (id_pegawai), seperti setelah login sukses.
* Dipakai saat login admin lewat tabel admin_users (proxy token ke pegawai tertentu).
*/
public function issueTokenForPegawaiId(int $id): ?string
{
$pegawai = $this->db->table('pegawai')->where('id_pegawai', $id)->get()->getRow();
if ($pegawai === null) {
return null;
}
$token = md5((string) $pegawai->id_pegawai) . $this->generateRandomString(15);
$this->db->table('pegawai')->where('id_pegawai', $id)->update([
'token' => $token,
'last_login' => date('Y-m-d H:i:s'),
]);
return $token;
}
public function profil(string $token): array
{
$data = [
'status' => 0,
'pesan' => '',
];
$pegawai = $this->db->table('pegawai')
->select('*, COALESCE(tanggal_lahir, \'0000-00-00\') as tanggal_lahir', false)
->where('token', $token)
->limit(1)
->get()
->getRow();
if (! $pegawai) {
return $data;
}
unset($pegawai->username, $pegawai->password, $pegawai->token);
$pegawai->kantor = $this->db->table('kantor')->where('id_kantor', $pegawai->kantor)->limit(1)->get()->getRow();
$pegawai->jabatan = $this->db->table('jabatan')->where('id_jabatan', $pegawai->jabatan)->limit(1)->get()->getRow();
$pegawai->unit_kerja = $this->db->table('unit_kerja')->where('id_unit_kerja', $pegawai->unit_kerja)->limit(1)->get()->getRow();
$pegawai->lembur = $this->db->table('lembur')->where('pegawai', $pegawai->id_pegawai)->where('tanggal_lembur', date('Y-m-d'))->limit(1)->get()->getRow();
$dilapangan = $this->db->table('dilapangan')->where('pegawai', $pegawai->id_pegawai)->limit(1)->get()->getRow();
$pegawai->dilapangan = $dilapangan ? true : false;
$hari = (int) date('N');
$pre_in = $hari . '_in';
$pre_out = $hari . '_out';
$jadwal_ar = [
'hari' => $this->hariIndo[$hari],
'masuk' => '',
'pulang' => '',
'istirahat' => '',
'toleransi_masuk' => '0',
'toleransi_pulang' => '0',
'libur' => true,
];
$libur_perusahaan = $this->db->table('libur')->where('tanggal_libur', date('Y-m-d'))->limit(1)->get()->getRow();
if ($libur_perusahaan) {
$jadwal_ar['ket_libur'] = 'Libur: ' . $libur_perusahaan->keterangan_libur;
} else {
$cuti = $this->db->table('cuti')
->where('pegawai', $pegawai->id_pegawai)
->where('tanggal_cuti', date('Y-m-d'))
->where('status_cuti', 'Approve')
->limit(1)
->get()
->getRow();
if ($cuti) {
$jadwal_ar['ket_libur'] = 'Cuti: ' . $cuti->alasan_cuti;
} else {
$jadwal = $this->db->table('jadwal')->where('id_jadwal', $pegawai->jadwal)->limit(1)->get()->getRow();
if ($jadwal) {
$jadwal_ar['masuk'] = $jadwal->{$pre_in};
$jadwal_ar['pulang'] = $jadwal->{$pre_out};
$jadwal_ar['istirahat'] = '12:00';
$jadwal_ar['toleransi_masuk'] = $jadwal->toleransi_terlambat;
$jadwal_ar['toleransi_pulang'] = $jadwal->toleransi_pulang_cepat;
$jadwal_ar['libur'] = false;
$jadwal_ar['ket_libur'] = '';
} else {
$jadwal_ar['ket_libur'] = 'Tidak ada jadwal. Hubungi Petugas';
}
}
}
$pegawai->jadwal = $jadwal_ar;
$data['status'] = 1;
$data['pegawai'] = $pegawai;
return $data;
}
public function saveCuti(string $token, string $nama_photo, string $img, string $tanggal, string $alasan, string $tipe): array
{
$data = [
'status' => 0,
'pesan' => '',
];
$pegawai = $this->db->table('pegawai')->where('token', $token)->limit(1)->get()->getRow();
if (! $pegawai) {
return $data;
}
$dir = $this->ensureUploadDir('dokcuti');
$image = base64_decode($img);
$image_name = uniqid((string) mt_rand(), true);
$filename = $image_name . '-' . $nama_photo;
if ($image === false || $image === '' || file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $image) === false) {
$data['pesan'] = 'Photo Dokumen GAGAL upload';
return $data;
}
$this->db->table('cuti')->insert([
'pegawai' => $pegawai->id_pegawai,
'tanggal_cuti' => $tanggal,
'tipe_cuti' => $tipe,
'alasan_cuti' => $alasan,
'status_cuti' => 'Waiting',
'alasan_tolak' => '',
]);
$id = (int) $this->db->insertID();
$this->db->table('cuti_dokumen')->insert([
'cuti' => $id,
'dokumen' => $filename,
]);
$data['status'] = 1;
$data['pesan'] = 'Photo Dokumen berhasil di upload';
return $data;
}
/**
* @param int|string|null $id
*/
public function batalkanCuti(string $token, $id): array
{
$data = [
'status' => 0,
'pesan' => '',
];
$pegawai = $this->db->table('pegawai')->where('token', $token)->limit(1)->get()->getRow();
if (! $pegawai) {
return $data;
}
$this->db->table('cuti')->where('pegawai', $pegawai->id_pegawai)->where('id_cuti', $id)->update([
'status_cuti' => 'Cancelled',
]);
$data['status'] = 1;
$data['pesan'] = "Ajuan di batalkan {$id} - {$pegawai->id_pegawai}";
return $data;
}
public function saveAktifitas(string $token, string $nama_photo, string $img, string $tanggal, string $deksripsi): array
{
$data = [
'status' => 0,
'pesan' => '',
];
$pegawai = $this->db->table('pegawai')->where('token', $token)->limit(1)->get()->getRow();
if (! $pegawai) {
return $data;
}
$dir = $this->ensureUploadDir('aktifitas');
$image = base64_decode($img);
$image_name = uniqid((string) mt_rand(), true);
$filename = $image_name . '-' . $nama_photo;
if ($image === false || $image === '' || file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $image) === false) {
$data['pesan'] = 'Photo Kegiatan GAGAL upload';
return $data;
}
$this->db->table('aktifitas_harian')->insert([
'pegawai' => $pegawai->id_pegawai,
'image' => $filename,
'deskripsi' => $deksripsi,
'waktu_aktifitas' => $tanggal,
]);
$data['status'] = 1;
$data['pesan'] = 'Photo Kegiatan berhasil di upload';
return $data;
}
public function saveMasuk(string $token, string $nama_photo, string $img, string $lat, string $lng, string $jarak): array
{
$data = [
'status' => 0,
'pesan' => 'Tidak ada jadwal kerja',
];
$pegawai = $this->db->table('pegawai')->where('token', $token)->limit(1)->get()->getRow();
if (! $pegawai) {
return $data;
}
$jadwal = $this->db->table('jadwal')->where('id_jadwal', $pegawai->jadwal)->limit(1)->get()->getRow();
if (! $jadwal) {
return $data;
}
$dir = $this->ensureUploadDir('absen' . DIRECTORY_SEPARATOR . 'masuk');
$image = base64_decode($img);
$image_name = uniqid((string) mt_rand(), true);
$filename = $image_name . '-' . $nama_photo;
if ($image === false || $image === '' || file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $image) === false) {
$data['pesan'] = 'Photo Kehadiran GAGAL upload';
return $data;
}
$hari = (int) date('N');
$pre_in = $hari . '_in';
$masuk_jadwal = date('Y-m-d') . ' ' . $jadwal->{$pre_in};
$jam_masuk = strtotime($masuk_jadwal);
$jam_toleransi = strtotime($masuk_jadwal) + ((int) $jadwal->toleransi_terlambat * 60);
$jam_sekarang = time();
if ($jam_sekarang < $jam_masuk) {
$ket_masuk = 'Sesuai jadwal';
} elseif ($jam_sekarang >= $jam_masuk && $jam_sekarang <= $jam_toleransi) {
$ket_masuk = 'Terlambat';
} else {
$ket_masuk = 'Sangat terlambat';
}
$this->db->table('presensi')->where('tanggal', date('Y-m-d'))->where('pegawai', $pegawai->id_pegawai)->update([
'jam_masuk' => date('Y-m-d H:i:s'),
'ket_masuk' => $ket_masuk,
'photo_masuk' => $filename,
'lat_masuk' => $lat,
'lng_masuk' => $lng,
'jarak_masuk' => $jarak,
]);
$data['status'] = 1;
$data['pesan'] = 'Photo Kehadiran berhasil di upload';
return $data;
}
public function savePulang(string $token, string $nama_photo, string $img, string $lat, string $lng, string $jarak): array
{
$data = [
'status' => 0,
'pesan' => 'Tidak ada jadwal kerja',
];
$pegawai = $this->db->table('pegawai')->where('token', $token)->limit(1)->get()->getRow();
if (! $pegawai) {
return $data;
}
$jadwal = $this->db->table('jadwal')->where('id_jadwal', $pegawai->jadwal)->limit(1)->get()->getRow();
if (! $jadwal) {
return $data;
}
$dir = $this->ensureUploadDir('absen' . DIRECTORY_SEPARATOR . 'pulang');
$image = base64_decode($img);
$image_name = uniqid((string) mt_rand(), true);
$filename = $image_name . '-' . $nama_photo;
if ($image === false || $image === '' || file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $image) === false) {
$data['pesan'] = 'Photo Kehadiran GAGAL upload';
return $data;
}
$wibNow = new \DateTimeImmutable('now', new \DateTimeZone('Asia/Jakarta'));
$hari = (int) $wibNow->format('N');
$pre_out = $hari . '_out';
$jadwal_pulang = $wibNow->format('Y-m-d') . ' ' . $jadwal->{$pre_out};
$jam_pulang = strtotime($jadwal_pulang);
$jam_toleransi = strtotime($jadwal_pulang) - ((int) $jadwal->toleransi_terlambat * 60);
$jam_sekarang = $wibNow->getTimestamp();
if ($jam_sekarang < $jam_toleransi) {
$data['pesan'] = 'Belum waktunya Pulang, waktu pulang anda adalah pukul ' . $jadwal->{$pre_out} . '. Jika anda memerlukan untuk pulang cepat, silahkan hubungi Operator';
return $data;
}
if ($jam_sekarang >= $jam_toleransi && $jam_sekarang <= $jam_pulang) {
$ket_pulang = 'Pulang Cepat';
} else {
$ket_pulang = 'Sesuai jadwal';
}
$this->db->table('presensi')->where('tanggal', $wibNow->format('Y-m-d'))->where('pegawai', $pegawai->id_pegawai)->update([
'jam_pulang' => $wibNow->format('Y-m-d H:i:s'),
'ket_pulang' => $ket_pulang,
'photo_pulang' => $filename,
'lat_pulang' => $lat,
'lng_pulang' => $lng,
'jarak_pulang' => $jarak,
]);
$data['status'] = 1;
$data['pesan'] = 'Photo Kehadiran berhasil di upload';
return $data;
}
public function saveIstirahat(string $token, string $mulai, string $selesai): array
{
$data = [
'status' => 0,
'pesan' => 'Tidak ada jadwal kerja',
];
$pegawai = $this->db->table('pegawai')->where('token', $token)->limit(1)->get()->getRow();
if (! $pegawai) {
return $data;
}
$jadwal = $this->db->table('jadwal')->where('id_jadwal', $pegawai->jadwal)->limit(1)->get()->getRow();
if (! $jadwal) {
return $data;
}
$ist = null;
if ($mulai !== '') {
$ist = [
'mulai_istirahat' => $mulai,
'beres_istirahat' => $mulai,
];
}
if ($selesai !== '') {
$ist = [
'beres_istirahat' => $selesai,
'is_istirahat' => 1,
];
}
if ($ist === null) {
return $data;
}
$this->db->table('presensi')->where('tanggal', date('Y-m-d'))->where('pegawai', $pegawai->id_pegawai)->update($ist);
$data['status'] = 1;
$data['pesan'] = 'berhasil disimpan';
return $data;
}
public function presensiToday(string $token): array
{
$data = [
'status' => 0,
'pesan' => '',
];
$pegawai = $this->db->table('pegawai')->select('id_pegawai, jadwal')->where('token', $token)->limit(1)->get()->getRow();
if (! $pegawai) {
return $data;
}
$presensi = $this->db->table('presensi')->where('tanggal', date('Y-m-d'))->where('pegawai', $pegawai->id_pegawai)->limit(1)->get()->getRow();
if (! $presensi) {
$jadwal = $this->db->table('jadwal')->where('id_jadwal', $pegawai->jadwal)->get()->getRow();
$ins = [
'pegawai' => $pegawai->id_pegawai,
'tanggal' => date('Y-m-d'),
'jadwal' => serialize($jadwal),
'jam_masuk' => null,
'ket_masuk' => '',
'photo_masuk' => '',
'jam_pulang' => null,
'ket_pulang' => '',
'photo_pulang' => '',
'mulai_istirahat' => null,
'beres_istirahat' => null,
'is_istirahat' => '0',
'lat_masuk' => '0',
'lng_masuk' => '0',
'lat_pulang' => '0',
'lng_pulang' => '0',
'jarak_masuk' => '0',
'jarak_pulang' => '0',
];
$this->db->table('presensi')->insert($ins);
$id = (int) $this->db->insertID();
$ins['id_presensi'] = $id . '.';
$presensi = (object) $ins;
}
$data['status'] = 1;
$data['data'] = $presensi;
return $data;
}
public function presensi(string $token): array
{
$data = [
'status' => 0,
'pesan' => '',
];
$pegawai = $this->db->table('pegawai')->select('id_pegawai, jadwal')->where('token', $token)->limit(1)->get()->getRow();
if (! $pegawai) {
return $data;
}
$presensi = $this->db->table('presensi')->where('pegawai', $pegawai->id_pegawai)->orderBy('tanggal', 'DESC')->limit(20)->get()->getResult();
if ($presensi) {
$data['status'] = 1;
$data['data'] = $presensi;
}
return $data;
}
public function daftarToday(string $token): array
{
$data = [
'status' => 0,
'pesan' => '',
];
$pegawai = $this->db->table('pegawai')->select('id_pegawai, jadwal')->where('token', $token)->limit(1)->get()->getRow();
if (! $pegawai) {
return $data;
}
$today = date('Y-m-d');
$presensi = $this->db->table('pegawai')
->join('jabatan', 'id_jabatan=jabatan', 'left')
->join('presensi', 'id_pegawai=pegawai AND tanggal = ' . $this->db->escape($today), 'left', false)
->where('id_pegawai !=', $pegawai->id_pegawai)
->where('id_presensi IS NOT NULL', null, false)
->orderBy('jam_masuk', 'DESC')
->orderBy('nama_lengkap')
->get()
->getResult();
if ($presensi) {
$data['status'] = 1;
$data['data'] = $presensi;
}
return $data;
}
public function berita(string $token, $dari, $jumlah): array
{
if ($dari === '' || $dari === null) {
$dari = 0;
}
if ($jumlah === '' || $jumlah === null) {
$jumlah = 10;
}
$dari = (int) $dari;
$jumlah = (int) $jumlah;
$data = [
'status' => 0,
'pesan' => '',
];
$pegawai = $this->db->table('pegawai')->select('id_pegawai')->where('token', $token)->limit(1)->get()->getRow();
if (! $pegawai) {
return $data;
}
$berita = $this->db->table('berita')->orderBy('tanggal', 'DESC')->limit($jumlah, $dari)->get()->getResult();
$data['status'] = 1;
$data['data'] = $berita;
return $data;
}
public function cuti(string $token, $dari, $jumlah): array
{
if ($dari === '' || $dari === null) {
$dari = 0;
}
if ($jumlah === '' || $jumlah === null) {
$jumlah = 25;
}
$dari = (int) $dari;
$jumlah = (int) $jumlah;
$data = [
'status' => 0,
'pesan' => '',
];
$pegawai = $this->db->table('pegawai')->select('id_pegawai')->where('token', $token)->limit(1)->get()->getRow();
if (! $pegawai) {
return $data;
}
$cuti = $this->db->table('cuti')->where('pegawai', $pegawai->id_pegawai)->orderBy('tanggal_cuti', 'DESC')->limit($jumlah, $dari)->get()->getResult();
if ($cuti) {
foreach ($cuti as $v) {
$v->dokumen = $this->db->table('cuti_dokumen')->select('dokumen')->where('cuti', $v->id_cuti)->get()->getResult();
}
}
$data['status'] = 1;
$data['data'] = $cuti;
return $data;
}
public function lembur(string $token, $dari, $jumlah): array
{
if ($dari === '' || $dari === null) {
$dari = 0;
}
if ($jumlah === '' || $jumlah === null) {
$jumlah = 25;
}
$dari = (int) $dari;
$jumlah = (int) $jumlah;
$data = [
'status' => 0,
'pesan' => '',
];
$pegawai = $this->db->table('pegawai')->select('id_pegawai')->where('token', $token)->limit(1)->get()->getRow();
if (! $pegawai) {
return $data;
}
$rows = $this->db->table('lembur')->where('pegawai', $pegawai->id_pegawai)->orderBy('tanggal_lembur', 'DESC')->limit($jumlah, $dari)->get()->getResult();
$data['status'] = 1;
$data['data'] = $rows;
return $data;
}
public function libur(string $token): array
{
$data = [
'status' => 0,
'pesan' => '',
];
$pegawai = $this->db->table('pegawai')->select('id_pegawai')->where('token', $token)->limit(1)->get()->getRow();
if (! $pegawai) {
return $data;
}
$rows = $this->db->table('libur')->orderBy('tanggal_libur', 'DESC')->get()->getResult();
$data['status'] = 1;
$data['data'] = $rows;
return $data;
}
public function aktifitas(string $token, $dari, $jumlah): array
{
if ($dari === '' || $dari === null) {
$dari = 0;
}
if ($jumlah === '' || $jumlah === null) {
$jumlah = 25;
}
$dari = (int) $dari;
$jumlah = (int) $jumlah;
$data = [
'status' => 0,
'pesan' => '',
];
$pegawai = $this->db->table('pegawai')->select('id_pegawai')->where('token', $token)->limit(1)->get()->getRow();
if (! $pegawai) {
return $data;
}
$rows = $this->db->table('aktifitas_harian')->where('pegawai', $pegawai->id_pegawai)->orderBy('waktu_aktifitas', 'DESC')->limit($jumlah, $dari)->get()->getResult();
$data['status'] = 1;
$data['data'] = $rows;
return $data;
}
public function savePp(string $token, string $nama_photo, string $img): array
{
$data = [
'status' => 0,
'pesan' => 'Tidak ada jadwal kerja',
];
$pegawai = $this->db->table('pegawai')->where('token', $token)->limit(1)->get()->getRow();
if (! $pegawai) {
return $data;
}
$dir = $this->ensureUploadDir('pengguna');
$image = base64_decode($img);
$image_name = uniqid((string) mt_rand(), true);
$filename = $image_name . '-' . $nama_photo;
if ($image === false || $image === '' || file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $image) === false) {
$data['pesan'] = 'Photo Kehadiran GAGAL upload';
return $data;
}
if ($pegawai->photo !== '' && $pegawai->photo !== null) {
try {
$photoPath = $dir . DIRECTORY_SEPARATOR . $pegawai->photo;
if (is_file($photoPath)) {
unlink($photoPath);
}
} catch (Exception $e) {
}
}
$this->db->table('pegawai')->where('id_pegawai', $pegawai->id_pegawai)->update([
'photo' => $filename,
]);
$data['status'] = 1;
$data['pesan'] = 'Photo Profil berhasil di upload';
return $data;
}
public function savePassword(string $token, string $passlama, string $passbaru): array
{
$data = [
'status' => 0,
'pesan' => '-',
];
$pegawai = $this->db->table('pegawai')->where('token', $token)->limit(1)->get()->getRow();
if (! $pegawai) {
return $data;
}
if (md5($passlama) == $pegawai->password) {
$this->db->table('pegawai')->where('id_pegawai', $pegawai->id_pegawai)->update([
'password' => md5($passbaru),
]);
$data['status'] = 1;
$data['pesan'] = 'Password berhasil di ubah';
} else {
$data['pesan'] = 'Password lama tidak sesuai, silahkan coba lagi';
}
return $data;
}
private function generateRandomString(int $length = 10): string
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = '';
for ($i = 0; $i < $length; $i++) {
// Sengaja memakai rand() seperti CI3 agar pola token kompatibel.
$randomString .= $characters[rand(0, strlen($characters) - 1)];
}
return $randomString;
}
private function ensureUploadDir(string $relativeUnderUploads): string
{
$base = FCPATH . 'assets' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $relativeUnderUploads);
if (! is_dir($base)) {
mkdir($base, 0755, true);
}
return $base;
}
}