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>, 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 $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 $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); } }