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

646 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace App\Controllers\Admin;
use App\Services\ApiClient;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\ResponseInterface;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
/**
* Daftar & detail presensi lewat `/api/admin/presensi*`.
*/
class Presensi extends BaseAdminController
{
public function index(): ResponseInterface|string
{
if (($deny = $this->enforceAccess('presensi')) !== null) {
return $deny;
}
$today = date('Y-m-d');
$dari = (string) ($this->request->getGet('tanggal_dari') ?? $today);
$sampai = (string) ($this->request->getGet('tanggal_sampai') ?? $today);
$page = max(1, (int) ($this->request->getGet('page') ?? 1));
$q = (string) ($this->request->getGet('q') ?? '');
$errors = [];
$payload = null;
$r = $this->apiAdminGet('presensi', [
'tanggal_dari' => $dari,
'tanggal_sampai' => $sampai,
'page' => (string) $page,
'per_page' => '30',
'q' => $q,
]);
if ($r['transport_ok'] && ApiClient::isSuccess($r['json'])) {
$payload = $r['json']['data'] ?? null;
} else {
$errors[] = $r['error'] ?? (is_array($r['json']) ? (string) ($r['json']['pesan'] ?? 'Gagal memuat presensi') : 'Gagal memuat presensi');
}
return view('admin/presensi/index', [
'payload' => $payload,
'errors' => $errors,
'dari' => $dari,
'sampai' => $sampai,
'page' => $page,
'q' => $q,
]);
}
/**
* Ambil semua baris presensi untuk ekspor (sama dengan filter list; maks. 100 halaman × 200 baris).
*
* @return array{ok: true, rows: list<array<string, mixed>>, dari: string, sampai: string, q: string}|array{ok: false, error: string, dari: string, sampai: string, q: string}
*/
private function fetchPresensiExportRows(): array
{
$today = date('Y-m-d');
$dari = (string) ($this->request->getGet('tanggal_dari') ?? $today);
$sampai = (string) ($this->request->getGet('tanggal_sampai') ?? $today);
$q = (string) ($this->request->getGet('q') ?? '');
$per = 200;
$maxPg = 100;
$allRows = [];
$totalPage = 1;
for ($page = 1; $page <= $totalPage && $page <= $maxPg; $page++) {
$r = $this->apiAdminGet('presensi', [
'tanggal_dari' => $dari,
'tanggal_sampai' => $sampai,
'page' => (string) $page,
'per_page' => (string) $per,
'q' => $q,
]);
if (! $r['transport_ok'] || ! ApiClient::isSuccess($r['json'])) {
$msg = $r['error'] ?? (is_array($r['json']) ? (string) ($r['json']['pesan'] ?? 'Gagal mengekspor') : 'Gagal mengekspor');
return ['ok' => false, 'error' => $msg, 'dari' => $dari, 'sampai' => $sampai, 'q' => $q];
}
$payload = $r['json']['data'] ?? [];
$rows = is_array($payload['rows'] ?? null) ? $payload['rows'] : [];
if ($page === 1) {
$totalPage = max(1, min($maxPg, (int) ($payload['total_page'] ?? 1)));
}
$allRows = array_merge($allRows, $rows);
}
return ['ok' => true, 'rows' => $allRows, 'dari' => $dari, 'sampai' => $sampai, 'q' => $q];
}
/**
* Unduh CSV semua baris presensi untuk filter yang sama (paginasi digabung, maks. 100 halaman × 200 baris).
*/
public function exportCsv(): ResponseInterface
{
if (($deny = $this->enforceAccess('presensi')) !== null) {
return $deny;
}
$fetched = $this->fetchPresensiExportRows();
if (! $fetched['ok']) {
return redirect()->to(site_url('admin/presensi?' . http_build_query(array_filter([
'tanggal_dari' => $fetched['dari'],
'tanggal_sampai' => $fetched['sampai'],
'q' => $fetched['q'],
]))))->with('error', $fetched['error']);
}
$allRows = $fetched['rows'];
$dari = $fetched['dari'];
$sampai = $fetched['sampai'];
$fh = fopen('php://memory', 'r+b');
if ($fh === false) {
return redirect()->to(site_url('admin/presensi'))->with('error', 'Gagal membuat file CSV.');
}
fwrite($fh, "\xEF\xBB\xBF");
fputcsv($fh, [
'id_presensi',
'tanggal',
'nama_lengkap',
'nip',
'status',
'jam_masuk',
'ket_masuk',
'jam_pulang',
'ket_pulang',
'istirahat',
]);
foreach ($allRows as $pr) {
if (! is_array($pr)) {
continue;
}
$jm = $pr['jam_masuk'] ?? null;
$jp = $pr['jam_pulang'] ?? null;
$mi = $pr['mulai_istirahat'] ?? null;
$bi = $pr['beres_istirahat'] ?? null;
$ist = '';
if (! empty($mi) || ! empty($bi)) {
$ist = (empty($mi) ? '—' : self::formatTimeForCsv((string) $mi)) . ' → ' . (empty($bi) ? '—' : self::formatTimeForCsv((string) $bi));
}
fputcsv($fh, [
(string) ($pr['id_presensi'] ?? ''),
(string) ($pr['tanggal'] ?? ''),
(string) ($pr['nama_lengkap'] ?? ''),
(string) ($pr['nip'] ?? ''),
self::presensiStatusLabelForCsv($pr),
self::formatTimeForCsv($jm !== null ? (string) $jm : ''),
(string) ($pr['ket_masuk'] ?? ''),
self::formatTimeForCsv($jp !== null ? (string) $jp : ''),
(string) ($pr['ket_pulang'] ?? ''),
$ist,
]);
}
rewind($fh);
$csv = stream_get_contents($fh) ?: '';
fclose($fh);
$fn = 'presensi_' . preg_replace('/[^0-9-]/', '', $dari) . '_' . preg_replace('/[^0-9-]/', '', $sampai) . '_' . date('His') . '.csv';
return $this->response
->setHeader('Content-Type', 'text/csv; charset=UTF-8')
->setHeader('Content-Disposition', 'attachment; filename="' . str_replace(['"', "\r", "\n"], '', $fn) . '"')
->setBody($csv);
}
/**
* Unduh Excel (.xlsx) dengan kolom rapi: header tebal, filter, lebar kolom, teks ket. dibungkus.
*/
public function exportXlsx(): ResponseInterface
{
if (($deny = $this->enforceAccess('presensi')) !== null) {
return $deny;
}
$fetched = $this->fetchPresensiExportRows();
if (! $fetched['ok']) {
return redirect()->to(site_url('admin/presensi?' . http_build_query(array_filter([
'tanggal_dari' => $fetched['dari'],
'tanggal_sampai' => $fetched['sampai'],
'q' => $fetched['q'],
]))))->with('error', $fetched['error']);
}
$allRows = $fetched['rows'];
$dari = $fetched['dari'];
$sampai = $fetched['sampai'];
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('Presensi');
$headers = [
'ID presensi',
'Tanggal',
'Nama lengkap',
'NIP',
'Status',
'Jam masuk',
'Ket. masuk',
'Jam pulang',
'Ket. pulang',
'Istirahat',
];
foreach ($headers as $i => $h) {
$sheet->setCellValue(Coordinate::stringFromColumnIndex($i + 1) . '1', $h);
}
$headerStyle = [
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
'fill' => [
'fillType' => Fill::FILL_SOLID,
'startColor' => ['rgb' => '1F4E79'],
],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
'vertical' => Alignment::VERTICAL_CENTER,
],
'borders' => [
'allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => 'CCCCCC']],
],
];
$sheet->getStyle('A1:J1')->applyFromArray($headerStyle);
$sheet->getRowDimension(1)->setRowHeight(22);
$rowNum = 2;
foreach ($allRows as $pr) {
if (! is_array($pr)) {
continue;
}
$jm = $pr['jam_masuk'] ?? null;
$jp = $pr['jam_pulang'] ?? null;
$mi = $pr['mulai_istirahat'] ?? null;
$bi = $pr['beres_istirahat'] ?? null;
$ist = '';
if (! empty($mi) || ! empty($bi)) {
$ist = (empty($mi) ? '—' : self::formatTimeForCsv((string) $mi)) . ' → ' . (empty($bi) ? '—' : self::formatTimeForCsv((string) $bi));
}
$sheet->setCellValue('A' . $rowNum, (string) ($pr['id_presensi'] ?? ''));
$sheet->setCellValue('B' . $rowNum, (string) ($pr['tanggal'] ?? ''));
$sheet->setCellValue('C' . $rowNum, (string) ($pr['nama_lengkap'] ?? ''));
$sheet->setCellValue('D' . $rowNum, (string) ($pr['nip'] ?? ''));
$sheet->setCellValue('E' . $rowNum, self::presensiStatusLabelForCsv($pr));
$sheet->setCellValue('F' . $rowNum, self::formatTimeForCsv($jm !== null ? (string) $jm : ''));
$sheet->setCellValue('G' . $rowNum, (string) ($pr['ket_masuk'] ?? ''));
$sheet->setCellValue('H' . $rowNum, self::formatTimeForCsv($jp !== null ? (string) $jp : ''));
$sheet->setCellValue('I' . $rowNum, (string) ($pr['ket_pulang'] ?? ''));
$sheet->setCellValue('J' . $rowNum, $ist);
$rowNum++;
}
$lastRow = max(2, $rowNum - 1);
if ($lastRow >= 2) {
$sheet->getStyle('A2:J' . $lastRow)->applyFromArray([
'borders' => [
'allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => 'DDDDDD']],
],
'alignment' => [
'vertical' => Alignment::VERTICAL_TOP,
],
]);
$sheet->getStyle('G2:G' . $lastRow)->getAlignment()->setWrapText(true);
$sheet->getStyle('I2:I' . $lastRow)->getAlignment()->setWrapText(true);
$sheet->getStyle('A2:A' . $lastRow)->getNumberFormat()->setFormatCode('@');
$sheet->getStyle('D2:D' . $lastRow)->getNumberFormat()->setFormatCode('@');
}
$sheet->freezePane('A2');
$sheet->setAutoFilter('A1:J1');
$sheet->getColumnDimension('A')->setWidth(14);
$sheet->getColumnDimension('B')->setWidth(12);
$sheet->getColumnDimension('C')->setWidth(28);
$sheet->getColumnDimension('D')->setWidth(18);
$sheet->getColumnDimension('E')->setWidth(14);
$sheet->getColumnDimension('F')->setWidth(11);
$sheet->getColumnDimension('G')->setWidth(36);
$sheet->getColumnDimension('H')->setWidth(11);
$sheet->getColumnDimension('I')->setWidth(36);
$sheet->getColumnDimension('J')->setWidth(22);
$tmp = tempnam(sys_get_temp_dir(), 'prxlsx');
if ($tmp === false) {
return redirect()->to(site_url('admin/presensi'))->with('error', 'Gagal membuat file Excel.');
}
try {
(new Xlsx($spreadsheet))->save($tmp);
} catch (\Throwable $e) {
@unlink($tmp);
log_message('error', 'exportXlsx: {message}', ['message' => $e->getMessage()]);
return redirect()->to(site_url('admin/presensi?' . http_build_query(array_filter([
'tanggal_dari' => $dari,
'tanggal_sampai' => $sampai,
'q' => $fetched['q'],
]))))->with('error', 'Gagal menulis file Excel. Pastikan ekstensi PHP zip aktif.');
}
$binary = (string) file_get_contents($tmp);
@unlink($tmp);
$spreadsheet->disconnectWorksheets();
$fn = 'presensi_' . preg_replace('/[^0-9-]/', '', $dari) . '_' . preg_replace('/[^0-9-]/', '', $sampai) . '_' . date('His') . '.xlsx';
return $this->response
->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
->setHeader('Content-Disposition', 'attachment; filename="' . str_replace(['"', "\r", "\n"], '', $fn) . '"')
->setBody($binary);
}
/**
* @param array<string, mixed> $pr
*/
private static function presensiStatusLabelForCsv(array $pr): string
{
$jm = $pr['jam_masuk'] ?? null;
$jp = $pr['jam_pulang'] ?? null;
$hadirMasuk = $jm !== null && $jm !== '';
$hadirPulang = $jp !== null && $jp !== '';
if ($hadirMasuk && $hadirPulang) {
return 'Lengkap';
}
if ($hadirMasuk || $hadirPulang) {
return 'Sebagian';
}
return 'Belum rekam';
}
private static function formatTimeForCsv(string $t): string
{
if ($t === '') {
return '';
}
$ts = strtotime($t);
return $ts ? date('H:i', $ts) : $t;
}
/**
* Layani foto absen masuk/pulang dari disk (public/assets/uploads/absen/…).
*/
public function foto(string $jenis, string $file): ResponseInterface
{
if (($deny = $this->enforceAccess('presensi')) !== null) {
return $deny;
}
$jenis = strtolower($jenis);
if ($jenis !== 'masuk' && $jenis !== 'pulang') {
throw PageNotFoundException::forPageNotFound();
}
$safe = basename(rawurldecode($file));
if ($safe === '' || $safe === '.' || $safe === '..') {
throw PageNotFoundException::forPageNotFound();
}
if (! preg_match('/^[A-Za-z0-9._-]+$/', $safe)) {
throw PageNotFoundException::forPageNotFound();
}
$path = FCPATH . 'assets' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'absen' . DIRECTORY_SEPARATOR . $jenis . DIRECTORY_SEPARATOR . $safe;
if (! is_file($path)) {
throw PageNotFoundException::forPageNotFound(
'Foto tidak ada di server. Salin folder absen/masuk dan absen/pulang dari CI3 ke: public/assets/uploads/absen/',
);
}
$ext = strtolower((string) pathinfo($safe, PATHINFO_EXTENSION));
$mime = match ($ext) {
'jpg', 'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
default => 'application/octet-stream',
};
$this->response->setHeader('Content-Type', $mime);
$this->response->setHeader('Content-Disposition', 'inline; filename="' . addcslashes($safe, '"\\') . '"');
return $this->response->setBody((string) file_get_contents($path));
}
public function detail(int $id): ResponseInterface|string
{
if (($deny = $this->enforceAccess('presensi')) !== null) {
return $deny;
}
$errors = [];
$row = null;
$r = $this->apiAdminGet('presensi/' . $id);
if ($r['transport_ok'] && ApiClient::isSuccess($r['json'])) {
$row = $r['json']['data'] ?? null;
} else {
$errors[] = $r['error'] ?? (is_array($r['json']) ? (string) ($r['json']['pesan'] ?? 'Gagal memuat detail') : 'Gagal memuat detail');
}
return view('admin/presensi/detail', [
'row' => is_array($row) ? $row : null,
'errors' => $errors,
]);
}
public function lapangan(): ResponseInterface|string
{
if (($deny = $this->enforceAccess('presensi')) !== null) {
return $deny;
}
$errors = [];
$payload = null;
$pegawai = [];
$r = $this->apiAdminGet('presensi/dilapangan');
if ($r['transport_ok'] && ApiClient::isSuccess($r['json'])) {
$payload = $r['json']['data'] ?? null;
} else {
$errors[] = $r['error'] ?? (is_array($r['json']) ? (string) ($r['json']['pesan'] ?? 'Gagal') : 'Gagal');
}
$p = $this->apiAdminGet('pegawai', ['page' => '1', 'per_page' => '500', 'q' => '']);
if ($p['transport_ok'] && ApiClient::isSuccess($p['json'])) {
$d = $p['json']['data'] ?? [];
$pegawai = is_array($d['rows'] ?? null) ? $d['rows'] : [];
}
return view('admin/presensi/lapangan', [
'payload' => is_array($payload) ? $payload : null,
'pegawai' => $pegawai,
'errors' => $errors,
]);
}
public function lapanganSave(): ResponseInterface
{
if (($deny = $this->enforceAccess('presensi')) !== null) {
return $deny;
}
return $this->toolPost('presensi/dilapangan/save', 'admin/presensi/lapangan');
}
public function lapanganDelete(int $id): ResponseInterface
{
if (($deny = $this->enforceAccess('presensi')) !== null) {
return $deny;
}
return $this->toolPost('presensi/dilapangan/delete/' . $id, 'admin/presensi/lapangan', []);
}
public function lembur(): ResponseInterface|string
{
if (($deny = $this->enforceAccess('presensi')) !== null) {
return $deny;
}
$errors = [];
$payload = null;
$pegawai = [];
$r = $this->apiAdminGet('presensi/lembur');
if ($r['transport_ok'] && ApiClient::isSuccess($r['json'])) {
$payload = $r['json']['data'] ?? null;
} else {
$errors[] = $r['error'] ?? (is_array($r['json']) ? (string) ($r['json']['pesan'] ?? 'Gagal') : 'Gagal');
}
$p = $this->apiAdminGet('pegawai', ['page' => '1', 'per_page' => '500', 'q' => '']);
if ($p['transport_ok'] && ApiClient::isSuccess($p['json'])) {
$d = $p['json']['data'] ?? [];
$pegawai = is_array($d['rows'] ?? null) ? $d['rows'] : [];
}
return view('admin/presensi/lembur', [
'payload' => is_array($payload) ? $payload : null,
'pegawai' => $pegawai,
'errors' => $errors,
]);
}
public function lemburSave(): ResponseInterface
{
if (($deny = $this->enforceAccess('presensi')) !== null) {
return $deny;
}
return $this->toolPost('presensi/lembur/save', 'admin/presensi/lembur');
}
public function lemburDelete(int $id): ResponseInterface
{
if (($deny = $this->enforceAccess('presensi')) !== null) {
return $deny;
}
return $this->toolPost('presensi/lembur/delete/' . $id, 'admin/presensi/lembur', []);
}
public function libur(): ResponseInterface|string
{
if (($deny = $this->enforceAccess('presensi_libur')) !== null) {
return $deny;
}
return $this->toolPage('presensi/libur', 'admin/presensi/libur', false);
}
public function liburSave(): ResponseInterface
{
if (($deny = $this->enforceAccess('presensi_libur')) !== null) {
return $deny;
}
return $this->toolPost('presensi/libur/save', 'admin/presensi/libur');
}
public function liburDelete(int $id): ResponseInterface
{
if (($deny = $this->enforceAccess('presensi_libur')) !== null) {
return $deny;
}
return $this->toolPost('presensi/libur/delete/' . $id, 'admin/presensi/libur', []);
}
public function jadwal(): ResponseInterface|string
{
if (($deny = $this->enforceAccess('presensi_jadwal')) !== null) {
return $deny;
}
return $this->toolPage('presensi/jadwal', 'admin/presensi/jadwal', false);
}
public function jadwalSave(): ResponseInterface
{
if (($deny = $this->enforceAccess('presensi_jadwal')) !== null) {
return $deny;
}
return $this->toolPost('presensi/jadwal/save', 'admin/presensi/jadwal');
}
public function jadwalDelete(int $id): ResponseInterface
{
if (($deny = $this->enforceAccess('presensi_jadwal')) !== null) {
return $deny;
}
return $this->toolPost('presensi/jadwal/delete/' . $id, 'admin/presensi/jadwal', []);
}
public function aktivitas(): ResponseInterface|string
{
if (($deny = $this->enforceAccess('presensi')) !== null) {
return $deny;
}
$page = max(1, (int) ($this->request->getGet('page') ?? 1));
$errors = [];
$payload = null;
$r = $this->apiAdminGet('presensi/aktivitas', ['page' => (string) $page, 'per_page' => '20']);
if ($r['transport_ok'] && ApiClient::isSuccess($r['json'])) {
$payload = $r['json']['data'] ?? null;
} else {
$errors[] = $r['error'] ?? (is_array($r['json']) ? (string) ($r['json']['pesan'] ?? 'Gagal') : 'Gagal');
}
return view('admin/presensi/aktivitas', [
'payload' => is_array($payload) ? $payload : null,
'errors' => $errors,
'page' => $page,
]);
}
public function aktivitasDelete(int $id): ResponseInterface
{
if (($deny = $this->enforceAccess('presensi')) !== null) {
return $deny;
}
return $this->toolPost('presensi/aktivitas/delete/' . $id, 'admin/presensi/aktivitas', []);
}
/**
* @return ResponseInterface|string
*/
private function toolPage(string $apiPath, string $view, bool $loadRefs)
{
$errors = [];
$payload = null;
$refs = null;
$r = $this->apiAdminGet($apiPath);
if ($r['transport_ok'] && ApiClient::isSuccess($r['json'])) {
$payload = $r['json']['data'] ?? null;
} else {
$errors[] = $r['error'] ?? (is_array($r['json']) ? (string) ($r['json']['pesan'] ?? 'Gagal memuat data') : 'Gagal memuat data');
}
if ($loadRefs) {
$ref = $this->apiAdminGet('references');
if ($ref['transport_ok'] && ApiClient::isSuccess($ref['json'])) {
$refs = $ref['json']['data'] ?? null;
}
}
return view($view, [
'payload' => is_array($payload) ? $payload : null,
'refs' => is_array($refs) ? $refs : null,
'errors' => $errors,
]);
}
/**
* @param array<string, scalar|null> $merge
*/
private function toolPost(string $apiPath, string $redirectPath, array $merge = []): ResponseInterface
{
$post = array_merge($this->request->getPost(), $merge);
$r = $this->apiAdminPost($apiPath, $post);
if ($r['transport_ok'] && ApiClient::isSuccess($r['json'])) {
return redirect()->to(site_url($redirectPath))->with('message', (string) ($r['json']['pesan'] ?? 'OK'));
}
$msg = $r['error'] ?? (is_array($r['json']) ? (string) ($r['json']['pesan'] ?? 'Gagal') : 'Gagal');
return redirect()->to(site_url($redirectPath))->with('error', $msg);
}
}