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,42 @@
<?= $this->extend('layouts/auth') ?>
<?= $this->section('title') ?>Login Admin<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="rounded-2xl border border-gray-200 bg-white p-8 shadow-md shadow-gray-200/50 sm:p-10">
<div class="mb-6 text-center">
<h1 class="text-xl font-semibold text-gray-900">Masuk ke admin</h1>
<p class="mt-2 text-sm text-gray-500">Gunakan akun pegawai (sama seperti aplikasi mobile) atau akun panel administrator. Sesi menyimpan token API.</p>
</div>
<?php if (session()->getFlashdata('message')) : ?>
<div class="mb-4 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800 shadow-sm" role="status">
<?= esc(session()->getFlashdata('message')) ?>
</div>
<?php endif ?>
<?php if (session()->getFlashdata('error')) : ?>
<div class="mb-4 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800 shadow-sm" role="alert">
<?= esc(session()->getFlashdata('error')) ?>
</div>
<?php endif ?>
<form action="<?= site_url('admin/login') ?>" method="post" class="space-y-5">
<?= csrf_field() ?>
<div>
<label for="username" class="mb-1.5 block text-sm font-medium text-gray-700">Username</label>
<input type="text" name="username" id="username" value="<?= esc(old('username')) ?>"
class="h-11 w-full rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm text-gray-800 shadow-sm placeholder:text-gray-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
autocomplete="username" required>
</div>
<div>
<label for="password" class="mb-1.5 block text-sm font-medium text-gray-700">Password</label>
<input type="password" name="password" id="password"
class="h-11 w-full rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm text-gray-800 shadow-sm placeholder:text-gray-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
autocomplete="current-password" required>
</div>
<button type="submit" class="flex w-full items-center justify-center rounded-lg bg-blue-600 px-4 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-blue-700">
Login
</button>
</form>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,110 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Detail Cuti<?= $this->endSection() ?>
<?php
$cuti = is_array($bundle) ? ($bundle['cuti'] ?? null) : null;
$dok = is_array($bundle) ? ($bundle['dokumen'] ?? []) : [];
$cuti = is_array($cuti) ? $cuti : [];
$st = (string) ($cuti['status_cuti'] ?? '');
$badgeClass = match ($st) {
'Approve' => 'bg-emerald-50 text-emerald-800 ring-1 ring-emerald-100',
'Rejected' => 'bg-red-50 text-red-800 ring-1 ring-red-100',
'Waiting' => 'bg-amber-50 text-amber-900 ring-1 ring-amber-100',
'Cancelled' => 'bg-gray-100 text-gray-600 ring-1 ring-gray-200',
default => 'bg-gray-100 text-gray-700 ring-1 ring-gray-200',
};
?>
<?= $this->section('content') ?>
<div class="mx-auto max-w-3xl space-y-6">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 class="text-2xl font-semibold tracking-tight text-gray-900">Detail cuti</h1>
<?php if ($cuti !== []) : ?>
<p class="mt-2"><span class="inline-flex rounded-full px-3 py-1 text-xs font-semibold <?= $badgeClass ?>"><?= esc($st !== '' ? $st : '—') ?></span></p>
<?php endif ?>
</div>
<a href="<?= site_url('admin/cuti') ?>" class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-sm font-medium text-gray-800 shadow-sm hover:bg-gray-50">
<i class="fa-solid fa-arrow-left text-xs text-gray-500"></i> Kembali ke daftar
</a>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<?php if ($cuti !== []) : ?>
<section class="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<h2 class="text-xs font-semibold uppercase tracking-wide text-gray-500">Ringkasan</h2>
<dl class="mt-4 grid gap-4 text-sm sm:grid-cols-2">
<div class="sm:col-span-2">
<dt class="text-xs font-medium text-gray-500">Pegawai</dt>
<dd class="mt-1 font-semibold text-gray-900"><?= esc((string) ($cuti['nama_lengkap'] ?? '')) ?> <span class="font-normal text-gray-500">· NIP <?= esc((string) ($cuti['nip'] ?? '')) ?></span></dd>
</div>
<div>
<dt class="text-xs font-medium text-gray-500">Tanggal cuti</dt>
<dd class="mt-1 font-medium text-gray-900"><?= esc(cuti_tanggal_label_from_row($cuti)) ?></dd>
</div>
<div>
<dt class="text-xs font-medium text-gray-500">Tipe</dt>
<dd class="mt-1 text-gray-800"><?= esc((string) ($cuti['tipe_cuti'] ?? '')) ?></dd>
</div>
<div class="sm:col-span-2">
<dt class="text-xs font-medium text-gray-500">Alasan</dt>
<dd class="mt-1 text-gray-800"><?= esc((string) ($cuti['alasan_cuti'] ?? '')) ?></dd>
</div>
<?php if ($st === 'Rejected' && ! empty($cuti['alasan_tolak'])) : ?>
<div class="sm:col-span-2 rounded-xl border border-red-100 bg-red-50/50 p-3">
<dt class="text-xs font-semibold text-red-800">Alasan tolak</dt>
<dd class="mt-1 text-sm text-red-900"><?= esc((string) $cuti['alasan_tolak']) ?></dd>
</div>
<?php endif ?>
</dl>
</section>
<?php if ($dok !== []) : ?>
<section class="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<h2 class="text-sm font-semibold text-gray-900">Dokumen</h2>
<ul class="mt-3 space-y-2 text-sm">
<?php foreach ($dok as $d) : ?>
<?php if (! is_array($d)) {
continue;
} ?>
<?php $fn = (string) ($d['dokumen'] ?? ''); ?>
<?php if ($fn !== '') : ?>
<li>
<a href="<?= esc(site_url('admin/cuti/dokumen/' . rawurlencode($fn))) ?>" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-2 font-medium text-blue-600 hover:text-blue-800">
<i class="fa-regular fa-file text-gray-400"></i> <?= esc($fn) ?>
</a>
</li>
<?php endif ?>
<?php endforeach ?>
</ul>
</section>
<?php endif ?>
<?php if ($st === 'Waiting') : ?>
<section class="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm space-y-5">
<h2 class="text-sm font-semibold text-gray-900">Persetujuan</h2>
<form method="post" action="<?= site_url('admin/cuti/approve/' . $id) ?>" class="flex flex-wrap gap-2">
<?= csrf_field() ?>
<button type="submit" class="inline-flex rounded-lg bg-emerald-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-emerald-700">Setujui (Approve)</button>
</form>
<form method="post" action="<?= site_url('admin/cuti/reject/' . $id) ?>" class="space-y-3 border-t border-gray-100 pt-5">
<?= csrf_field() ?>
<label class="block text-xs font-medium text-gray-600">Tolak — alasan wajib</label>
<textarea name="alasan_tolak" rows="3" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" placeholder="Jelaskan alasan penolakan"><?= esc(old('alasan_tolak') ?? '') ?></textarea>
<button type="submit" class="inline-flex rounded-lg bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-700">Tolak (Reject)</button>
</form>
</section>
<?php endif ?>
<?php else : ?>
<div class="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<?= view('layouts/partials/empty_state', [
'icon' => 'fa-circle-question',
'title' => 'Cuti tidak ditemukan',
'hint' => 'ID tidak valid atau data sudah tidak tersedia.',
]) ?>
</div>
<?php endif ?>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,117 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Data Cuti<?= $this->endSection() ?>
<?php
$rows = is_array($payload) ? ($payload['rows'] ?? []) : [];
$total = is_array($payload) ? (int) ($payload['total'] ?? 0) : 0;
$totalPage = is_array($payload) ? max(1, (int) ($payload['total_page'] ?? 1)) : 1;
$curPage = is_array($payload) ? (int) ($payload['page'] ?? $page) : $page;
$statusOpts = ['' => 'Semua', 'Waiting' => 'Waiting', 'Approve' => 'Approve', 'Rejected' => 'Rejected', 'Cancelled' => 'Cancelled'];
?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-semibold tracking-tight text-gray-900">Data cuti pegawai</h1>
<p class="mt-1 max-w-2xl text-sm text-gray-500">Filter menurut status dan lakukan persetujuan atau penolakan dari sini.</p>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<form method="get" action="<?= site_url('admin/cuti') ?>" class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<div class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<label class="block text-xs font-medium text-gray-600">Status</label>
<select name="status" class="mt-1 min-w-[12rem] rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
<?php foreach ($statusOpts as $val => $lab) : ?>
<option value="<?= esc($val) ?>" <?= ($status === $val) ? 'selected' : '' ?>><?= esc($lab) ?></option>
<?php endforeach ?>
</select>
</div>
<div class="flex flex-wrap items-center gap-2">
<button type="submit" class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Terapkan</button>
<a href="<?= site_url('admin/cuti') ?>" class="inline-flex items-center justify-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-800 shadow-sm hover:bg-gray-50">Semua status</a>
</div>
</div>
</form>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="border-b border-gray-200 px-5 py-4">
<h2 class="text-base font-semibold text-gray-900">Daftar permohonan</h2>
<p class="mt-0.5 text-xs text-gray-500">Hijau = disetujui · kuning = menunggu · merah = ditolak.</p>
</div>
<?php if ($rows === []) : ?>
<div class="p-6">
<?= view('layouts/partials/empty_state', [
'icon' => 'fa-plane-departure',
'title' => 'Tidak ada permohonan cuti',
'hint' => 'Pilih status lain atau kembali ke Semua status untuk melihat seluruh riwayat.',
]) ?>
</div>
<?php else : ?>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50 text-left text-xs font-semibold uppercase tracking-wide text-gray-500">
<tr>
<th class="px-4 py-3">Pegawai</th>
<th class="px-4 py-3">Tanggal</th>
<th class="px-4 py-3">Tipe</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php foreach ($rows as $c) : ?>
<?php if (! is_array($c)) {
continue;
} ?>
<?php
$st = (string) ($c['status_cuti'] ?? '');
$badge = match ($st) {
'Approve' => 'bg-emerald-50 text-emerald-800 ring-1 ring-emerald-100',
'Rejected' => 'bg-red-50 text-red-800 ring-1 ring-red-100',
'Waiting' => 'bg-amber-50 text-amber-900 ring-1 ring-amber-100',
'Cancelled' => 'bg-gray-100 text-gray-600 ring-1 ring-gray-200',
default => 'bg-gray-100 text-gray-700 ring-1 ring-gray-200',
};
?>
<tr class="transition-colors hover:bg-gray-50">
<td class="px-4 py-2.5">
<div class="font-medium text-gray-900"><?= esc((string) ($c['nama_lengkap'] ?? '')) ?></div>
<div class="text-xs text-gray-500"><?= esc((string) ($c['nip'] ?? '')) ?></div>
</td>
<td class="whitespace-nowrap px-4 py-2.5 text-gray-800"><?= esc(cuti_tanggal_label_from_row($c)) ?></td>
<td class="px-4 py-2.5 text-gray-700"><?= esc((string) ($c['tipe_cuti'] ?? '')) ?></td>
<td class="px-4 py-2.5">
<span class="inline-flex rounded-full px-2.5 py-0.5 text-xs font-semibold <?= $badge ?>"><?= esc($st) ?></span>
</td>
<td class="whitespace-nowrap px-4 py-2.5">
<a href="<?= site_url('admin/cuti/detail/' . (int) ($c['id_cuti'] ?? 0)) ?>" class="mr-2 text-xs font-semibold text-blue-600 hover:text-blue-800">Detail</a>
<?php if ($st === 'Waiting') : ?>
<form method="post" action="<?= site_url('admin/cuti/approve/' . (int) ($c['id_cuti'] ?? 0)) ?>" class="inline" onsubmit="return confirm('Setujui permohonan ini?');">
<?= csrf_field() ?>
<button type="submit" class="text-xs font-semibold text-emerald-700 hover:text-emerald-900 hover:underline">Approve</button>
</form>
<?php endif ?>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
<?php endif ?>
<div class="flex flex-col gap-3 border-t border-gray-200 bg-gray-50/80 px-5 py-3 text-xs text-gray-600 sm:flex-row sm:items-center sm:justify-between">
<span>Total: <strong class="text-gray-900"><?= number_format($total, 0, ',', '.') ?></strong></span>
<div class="flex flex-wrap gap-2">
<?php if ($curPage > 1) : ?>
<a class="inline-flex rounded-lg border border-gray-200 bg-white px-3 py-1.5 font-medium text-gray-800 shadow-sm hover:bg-gray-50" href="<?= site_url('admin/cuti?' . http_build_query(['status' => $status, 'page' => $curPage - 1])) ?>">Sebelumnya</a>
<?php endif ?>
<?php if ($curPage < $totalPage) : ?>
<a class="inline-flex rounded-lg border border-gray-200 bg-white px-3 py-1.5 font-medium text-gray-800 shadow-sm hover:bg-gray-50" href="<?= site_url('admin/cuti?' . http_build_query(['status' => $status, 'page' => $curPage + 1])) ?>">Berikutnya</a>
<?php endif ?>
</div>
</div>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,218 @@
<?php
declare(strict_types=1);
/** @var array<string, mixed>|object|null $profil */
$p = $profil ?? null;
if (is_object($p)) {
$json = json_encode($p);
$p = is_string($json) ? json_decode($json, true) : [];
}
$p = is_array($p) ? $p : [];
/**
* @param array<string, mixed> $row
*/
$nestedNama = static function (mixed $row, string ...$keys): string {
if (! is_array($row)) {
return '';
}
foreach ($keys as $k) {
if (isset($row[$k]) && is_string($row[$k]) && $row[$k] !== '') {
return $row[$k];
}
}
return '';
};
$fmtDate = static function (mixed $v): string {
if ($v === null || $v === '') {
return '—';
}
$s = trim((string) $v);
if ($s === '' || str_starts_with($s, '0000-00-00')) {
return '—';
}
if (preg_match('/^(\d{4}-\d{1,2}-\d{1,2})/', $s, $m)) {
$t = strtotime($m[1]);
return $t !== false ? date('d/m/Y', $t) : '—';
}
return '—';
};
$fmtDateTime = static function (mixed $v): string {
if ($v === null || $v === '') {
return '—';
}
$s = trim((string) $v);
$t = strtotime($s);
return $t !== false ? date('d/m/Y H:i', $t) : $s;
};
$fmtBool = static function (mixed $v): string {
if ($v === null || $v === '') {
return '—';
}
if (is_bool($v)) {
return $v ? 'Ya' : 'Tidak';
}
$f = filter_var($v, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
return $f === null ? (string) $v : ($f ? 'Ya' : 'Tidak');
};
$scalarOrDash = static function (mixed $v): string {
if ($v === null) {
return '—';
}
if (is_bool($v)) {
return $v ? 'Ya' : 'Tidak';
}
if (is_scalar($v)) {
$s = trim((string) $v);
return $s === '' ? '—' : $s;
}
return '—';
};
$lemburLabel = static function (mixed $v): string {
if ($v === null || $v === false || $v === '') {
return 'Tidak ada';
}
if (is_string($v) || is_int($v) || is_float($v)) {
$s = trim((string) $v);
return $s === '' ? 'Tidak ada' : $s;
}
if (is_array($v)) {
return $v !== [] ? 'Ada (hari ini)' : 'Tidak ada';
}
if (is_object($v)) {
$a = (array) $v;
return $a !== [] ? 'Ada (hari ini)' : 'Tidak ada';
}
return '—';
};
$rows = [
['key' => 'nip', 'label' => 'NIP', 'fmt' => 'scalar'],
['key' => 'jenis_kelamin', 'label' => 'Jenis kelamin', 'fmt' => 'scalar'],
['key' => 'tempat_lahir', 'label' => 'Tempat lahir', 'fmt' => 'scalar'],
['key' => 'tanggal_lahir', 'label' => 'Tanggal lahir', 'fmt' => 'date'],
['key' => 'email', 'label' => 'Email', 'fmt' => 'scalar'],
['key' => 'kantor', 'label' => 'Kantor', 'fmt' => 'nested', 'nested' => ['nama_kantor', 'nama']],
['key' => 'jabatan', 'label' => 'Jabatan', 'fmt' => 'nested', 'nested' => ['nama_jabatan', 'nama']],
['key' => 'unit_kerja', 'label' => 'Unit kerja', 'fmt' => 'nested', 'nested' => ['nama_unit_kerja', 'nama']],
['key' => 'golongan_pekerjaan', 'label' => 'Golongan (ID)', 'fmt' => 'scalar'],
['key' => 'status_kepegawaian', 'label' => 'Status kepegawaian', 'fmt' => 'scalar'],
['key' => 'tanggal_bergabung', 'label' => 'Tanggal bergabung', 'fmt' => 'date'],
['key' => 'super_akses', 'label' => 'Super akses', 'fmt' => 'bool'],
['key' => 'last_login', 'label' => 'Terakhir login', 'fmt' => 'dt'],
['key' => 'lembur', 'label' => 'Lembur hari ini', 'fmt' => 'lembur'],
['key' => 'dilapangan', 'label' => 'Tugas luar / lapangan', 'fmt' => 'bool'],
];
$nama = (string) ($p['nama_lengkap'] ?? '');
$nip = (string) ($p['nip'] ?? '');
$idPeg = isset($p['id_pegawai']) ? (string) $p['id_pegawai'] : '';
$photo = trim((string) ($p['photo'] ?? ''));
$jadwal = $p['jadwal'] ?? null;
?>
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<h2 class="text-xs font-semibold uppercase tracking-wide text-gray-500">Profil pegawai</h2>
<?php if ($p === []) : ?>
<div class="mt-4">
<?= view('layouts/partials/empty_state', [
'icon' => 'fa-user',
'title' => 'Belum ada data profil',
'hint' => 'Setelah login pegawai valid, ringkasan profil akan tampil di sini.',
]) ?>
</div>
<?php else : ?>
<div class="mt-4 flex flex-col gap-4 border-b border-gray-100 pb-4 sm:flex-row sm:items-start sm:gap-5">
<div class="flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-2xl border border-gray-200 bg-gray-50 text-gray-400">
<?php if ($photo !== '') : ?>
<img src="<?= esc($photo) ?>" alt="" class="h-full w-full object-cover" loading="lazy" decoding="async" onerror="this.style.display='none'; this.nextElementSibling?.classList.remove('hidden');">
<i class="fa-solid fa-user hidden text-2xl"></i>
<?php else : ?>
<i class="fa-solid fa-user text-2xl"></i>
<?php endif ?>
</div>
<div class="min-w-0 flex-1">
<p class="text-lg font-semibold tracking-tight text-gray-900"><?= esc($nama !== '' ? $nama : '—') ?></p>
<?php if ($nip !== '') : ?>
<p class="mt-0.5 font-mono text-sm text-gray-600"><?= esc($nip) ?></p>
<?php endif ?>
<?php if ($idPeg !== '') : ?>
<p class="mt-1 text-xs text-gray-400">ID pegawai <?= esc($idPeg) ?></p>
<?php endif ?>
</div>
</div>
<dl class="mt-4 grid gap-x-6 gap-y-3 text-sm sm:grid-cols-2">
<?php foreach ($rows as $spec) : ?>
<?php
$key = $spec['key'];
$raw = $p[$key] ?? null;
$label = $spec['label'];
$fmt = $spec['fmt'];
$out = '—';
if ($fmt === 'scalar') {
$out = $scalarOrDash($raw);
} elseif ($fmt === 'date') {
$out = $fmtDate($raw);
} elseif ($fmt === 'dt') {
$out = $fmtDateTime($raw);
} elseif ($fmt === 'bool') {
$out = $fmtBool($raw);
} elseif ($fmt === 'lembur') {
$out = $lemburLabel($raw);
} elseif ($fmt === 'nested' && isset($spec['nested'])) {
$out = $nestedNama(is_array($raw) ? $raw : (is_object($raw) ? (array) $raw : []), ...$spec['nested']);
$out = $out !== '' ? $out : '—';
}
if ($key === 'email' && $out === '—') {
continue;
}
?>
<div class="flex flex-col gap-0.5 border-b border-gray-50 pb-3 sm:border-0 sm:pb-0">
<dt class="text-xs font-medium text-gray-500"><?= esc($label) ?></dt>
<dd class="font-medium text-gray-900"><?= esc((string) $out) ?></dd>
</div>
<?php endforeach ?>
</dl>
<?php if (is_array($jadwal) && $jadwal !== []) : ?>
<div class="mt-5 rounded-xl border border-gray-100 bg-gray-50/80 px-4 py-3">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">Jadwal hari ini</p>
<p class="mt-1 text-sm text-gray-800">
<?php
$hari = (string) ($jadwal['hari'] ?? '');
$masuk = (string) ($jadwal['masuk'] ?? '');
$pulang = (string) ($jadwal['pulang'] ?? '');
$ket = (string) ($jadwal['ket_libur'] ?? '');
$libur = ! empty($jadwal['libur']);
?>
<?php if ($hari !== '') : ?><span class="font-medium"><?= esc($hari) ?></span><?php endif ?>
<?php if (! $libur && ($masuk !== '' || $pulang !== '')) : ?>
<span class="text-gray-600"> · Masuk <?= esc($masuk !== '' ? $masuk : '—') ?>, pulang <?= esc($pulang !== '' ? $pulang : '—') ?></span>
<?php endif ?>
<?php if ($ket !== '') : ?>
<span class="mt-1 block text-xs text-amber-800"><?= esc($ket) ?></span>
<?php elseif ($libur && $ket === '') : ?>
<span class="mt-1 block text-xs text-gray-600">Hari libur / tidak ada jadwal masuk</span>
<?php endif ?>
</p>
</div>
<?php endif ?>
<?php endif ?>
</section>

View File

@@ -0,0 +1,208 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Dashboard Presensi<?= $this->endSection() ?>
<?= $this->section('content') ?>
<?php
$s = is_array($summary ?? null) ? $summary : [];
$belumList = is_array($s['belum_rekam_pegawai'] ?? null) ? $s['belum_rekam_pegawai'] : [];
?>
<div class="space-y-6">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<h1 class="text-2xl font-semibold tracking-tight text-gray-900">Dashboard Presensi</h1>
<p class="mt-1 max-w-2xl text-sm text-gray-500">Statistik harian dan antrian pengajuan cuti.</p>
</div>
<span class="inline-flex w-fit shrink-0 items-center rounded-full border border-gray-200 bg-white px-3 py-1.5 text-xs font-semibold text-gray-700 shadow-sm"><?= esc(date('d F Y, H:i')) ?></span>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<?php if (! empty($errors) && empty($token ?? null)) : ?>
<div class="rounded-2xl border border-gray-200 bg-white p-4 text-sm text-gray-700 shadow-sm">
<a href="<?= site_url('admin/login') ?>" class="inline-flex items-center rounded-lg bg-blue-600 px-4 py-2 text-xs font-semibold text-white shadow-sm hover:bg-blue-700">Login ulang</a>
</div>
<?php endif ?>
<?php if ($summary !== null) : ?>
<div class="grid gap-4 md:grid-cols-3">
<div class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">Total Pegawai</p>
<p class="mt-2 text-3xl font-bold text-gray-900"><?= number_format((int) ($s['total_pegawai'] ?? 0), 0, ',', '.') ?></p>
<p class="mt-2 text-xs text-gray-600">
Pria: <?= number_format((int) ($s['pegawai_laki'] ?? 0), 0, ',', '.') ?>
<span class="text-gray-400">(<?= esc((string) ($s['persen_laki'] ?? '0')) ?>%)</span>
· Wanita: <?= number_format((int) ($s['pegawai_perempuan'] ?? 0), 0, ',', '.') ?>
<span class="text-gray-400">(<?= esc((string) ($s['persen_perempuan'] ?? '0')) ?>%)</span>
</p>
</div>
<div class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">Presensi hari ini</p>
<p class="mt-2 text-3xl font-bold text-blue-600"><?= number_format((int) ($s['presensi_hari_ini'] ?? 0), 0, ',', '.') ?></p>
<p class="mt-2 text-xs text-gray-600">Kehadiran <?= esc((string) ($s['persen_presensi'] ?? '0')) ?>%</p>
<p class="mt-1 text-xs text-amber-700"><?= number_format((int) ($s['belum_rekam'] ?? 0), 0, ',', '.') ?> belum rekam presensi</p>
</div>
<div class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">Cuti hari ini (Approve)</p>
<p class="mt-2 text-3xl font-bold text-emerald-600"><?= number_format((int) ($s['cuti_hari_ini'] ?? 0), 0, ',', '.') ?></p>
<p class="mt-2 text-xs text-gray-600">Pegawai cuti <?= esc((string) ($s['persen_cuti'] ?? '0')) ?>% · status disetujui</p>
</div>
</div>
<div class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<h2 class="text-sm font-semibold text-gray-900">Ringkasan kehadiran hari ini</h2>
<p class="mt-0.5 text-xs text-gray-500">Proporsi hadir, cuti disetujui, dan belum rekam.</p>
<?php
$hadir = (int) ($s['presensi_hari_ini'] ?? 0);
$cuti = (int) ($s['cuti_hari_ini'] ?? 0);
$blm = (int) ($s['belum_rekam'] ?? 0);
$tot = max(1, (int) ($s['total_pegawai'] ?? 1));
$pH = round(($hadir / $tot) * 100, 1);
$pC = round(($cuti / $tot) * 100, 1);
$pB = round(($blm / $tot) * 100, 1);
?>
<div class="mt-4 flex h-11 w-full overflow-hidden rounded-xl text-xs font-semibold text-white shadow-inner">
<span class="flex min-w-0 items-center justify-center bg-emerald-500 px-1" style="width: <?= esc((string) max(0, min(100, $pH))) ?>%"><?= $hadir > 0 ? 'Hadir ' . $hadir : '' ?></span>
<span class="flex min-w-0 items-center justify-center bg-amber-500 px-1" style="width: <?= esc((string) max(0, min(100, $pC))) ?>%"><?= $cuti > 0 ? 'Cuti ' . $cuti : '' ?></span>
<span class="flex min-w-0 items-center justify-center bg-gray-400 px-1" style="width: <?= esc((string) max(0, min(100, $pB))) ?>%"><?= $blm > 0 ? 'Belum ' . $blm : '' ?></span>
</div>
<div class="mt-4 flex flex-wrap items-baseline gap-x-6 gap-y-2 text-xs text-gray-600">
<span><strong class="text-gray-900">Hadir</strong> <?= $hadir ?> (<?= $pH ?>%)</span>
<span><strong class="text-gray-900">Cuti</strong> <?= $cuti ?> (<?= $pC ?>%)</span>
<?php if ($blm > 0) : ?>
<details class="relative max-w-full [&_summary::-webkit-details-marker]:hidden">
<summary class="flex cursor-pointer list-none flex-wrap items-baseline gap-x-1 text-gray-600 underline decoration-dotted decoration-gray-400 underline-offset-2 hover:text-blue-700">
<strong class="text-gray-900">Belum rekam</strong>
<span><?= $blm ?> (<?= $pB ?>%)</span>
<span class="text-[10px] font-normal text-blue-600">klik</span>
</summary>
<div class="absolute left-0 z-20 mt-2 max-h-64 w-[min(calc(100vw-2rem),20rem)] overflow-y-auto rounded-lg border border-gray-200 bg-white p-3 text-left shadow-lg">
<p class="mb-2 font-semibold text-gray-800">Yang belum rekam presensi hari ini</p>
<p class="mb-2 text-[11px] leading-snug text-gray-500">Hanya informasi; tidak termasuk yang cuti disetujui.</p>
<?php if ($belumList === []) : ?>
<p class="text-gray-500">Daftar nama tidak tersedia. Coba muat ulang halaman setelah pembaruan server.</p>
<?php else : ?>
<ol class="list-decimal space-y-1.5 pl-4 text-gray-700">
<?php foreach ($belumList as $bp) : ?>
<?php if (! is_array($bp)) {
continue;
} ?>
<li class="leading-snug">
<span class="font-medium text-gray-900"><?= esc((string) ($bp['nama_lengkap'] ?? '')) ?></span>
<?php if (($bp['nip'] ?? '') !== '') : ?>
<span class="text-gray-500"> · NIP <?= esc((string) $bp['nip']) ?></span>
<?php endif ?>
</li>
<?php endforeach ?>
</ol>
<?php endif ?>
</div>
</details>
<?php else : ?>
<span><strong class="text-gray-900">Belum rekam</strong> <?= $blm ?> (<?= $pB ?>%)</span>
<?php endif ?>
<span><strong class="text-gray-900">Total</strong> <?= (int) ($s['total_pegawai'] ?? 0) ?></span>
</div>
</div>
<?php
$pending = is_array($s['permohonan_cuti'] ?? null) ? $s['permohonan_cuti'] : [];
$nPending = (int) ($s['total_permohonan_cuti'] ?? 0);
?>
<section class="overflow-hidden rounded-2xl border border-amber-200/80 bg-white shadow-sm">
<div class="border-b border-amber-100 bg-amber-50 px-5 py-4">
<h2 class="text-sm font-semibold text-amber-950">Permohonan cuti menunggu persetujuan</h2>
<p class="mt-0.5 text-xs text-amber-900/80"><?= number_format($nPending, 0, ',', '.') ?> permohonan menunggu, seluruhnya pengajuan lihat di Data Cuti.</p>
</div>
<?php if ($pending === []) : ?>
<div class="p-6">
<?= view('layouts/partials/empty_state', [
'icon' => 'fa-clipboard-check',
'title' => 'Tidak ada antrian Waiting',
'hint' => 'Permohonan baru akan muncul di sini. Kelola semua cuti lewat menu Pegawai → Data Cuti.',
]) ?>
</div>
<?php else : ?>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50 text-left text-xs font-semibold uppercase tracking-wide text-gray-500">
<tr>
<th class="px-5 py-3">Pegawai</th>
<th class="px-5 py-3">Tanggal cuti</th>
<th class="px-5 py-3">Tipe</th>
<th class="px-5 py-3">Alasan</th>
<?php if (canAccess('cuti')) : ?>
<th class="px-5 py-3">Aksi</th>
<?php endif ?>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php foreach ($pending as $row) : ?>
<?php if (! is_array($row)) {
continue;
} ?>
<tr class="transition-colors hover:bg-gray-50">
<td class="px-5 py-3">
<div class="font-medium text-gray-900"><?= esc((string) ($row['nama_lengkap'] ?? '')) ?></div>
<div class="text-xs text-gray-500">NIP: <?= esc((string) ($row['nip'] ?? '')) ?></div>
</td>
<td class="whitespace-nowrap px-5 py-3 text-gray-800"><?= esc(cuti_tanggal_label_from_row($row)) ?></td>
<td class="px-5 py-3 text-gray-700"><?= esc((string) ($row['tipe_cuti'] ?? '')) ?></td>
<td class="max-w-xs truncate px-5 py-3 text-gray-600" title="<?= esc((string) ($row['alasan_cuti'] ?? '')) ?>"><?= esc((string) ($row['alasan_cuti'] ?? '')) ?></td>
<?php if (canAccess('cuti')) : ?>
<td class="whitespace-nowrap px-5 py-3">
<div class="flex flex-wrap gap-2">
<form method="post" action="<?= site_url('admin/cuti/approve/' . (int) ($row['id_cuti'] ?? 0)) ?>" class="inline">
<?= csrf_field() ?>
<button type="submit" class="inline-flex rounded-lg bg-emerald-600 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-emerald-700">Approve</button>
</form>
<a href="<?= site_url('admin/cuti/detail/' . (int) ($row['id_cuti'] ?? 0)) ?>" class="inline-flex rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-xs font-semibold text-gray-800 shadow-sm hover:bg-gray-50">Tolak…</a>
</div>
</td>
<?php endif ?>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
<?php endif ?>
<?php if (canAccess('cuti')) : ?>
<div class="border-t border-gray-100 bg-gray-50/80 px-5 py-3">
<a href="<?= site_url('admin/cuti') ?>" class="text-sm font-semibold text-blue-600 hover:text-blue-800">Kelola semua cuti →</a>
</div>
<?php endif ?>
</section>
<?php endif ?>
<div class="grid gap-4 md:grid-cols-2">
<?= view('admin/dashboard/_profil_ringkas', ['profil' => $profil ?? null]) ?>
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<h2 class="text-xs font-semibold uppercase tracking-wide text-gray-500">Berita terbaru</h2>
<?php if (! empty($berita) && is_array($berita)) : ?>
<ul class="mt-4 divide-y divide-gray-100">
<?php foreach ($berita as $row) : ?>
<li class="py-3 text-sm">
<?php if (is_array($row)) : ?>
<span class="font-medium text-gray-900"><?= esc((string) ($row['judul'] ?? $row['title'] ?? 'Item')) ?></span>
<?php if (! empty($row['tanggal'] ?? $row['tgl'] ?? null)) : ?>
<span class="mt-0.5 block text-xs text-gray-400"><?= esc((string) ($row['tanggal'] ?? $row['tgl'])) ?></span>
<?php endif ?>
<?php else : ?>
<?= esc((string) $row) ?>
<?php endif ?>
</li>
<?php endforeach ?>
</ul>
<?php else : ?>
<div class="mt-4">
<?= view('layouts/partials/empty_state', [
'icon' => 'fa-newspaper',
'title' => 'Tidak ada berita',
'hint' => 'Pengumuman dari sistem akan tampil di blok ini bila tersedia.',
]) ?>
</div>
<?php endif ?>
</section>
</div>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,95 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Laporan cuti<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<h1 class="text-2xl font-semibold tracking-tight text-gray-900">Laporan cuti pegawai</h1>
<p class="mt-1 max-w-2xl text-sm text-gray-500">Filter menurut tanggal mulai cuti. Untuk mencetak halaman ini, gunakan <kbd class="rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 text-[10px] font-sans">Ctrl</kbd>+<kbd class="rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 text-[10px] font-sans">P</kbd> di browser.</p>
</div>
<button type="button" onclick="window.print()" class="hidden h-10 shrink-0 items-center justify-center rounded-lg border border-gray-200 bg-white px-4 text-xs font-semibold text-gray-800 shadow-sm hover:bg-gray-50 print:hidden sm:inline-flex">
<i class="fa-solid fa-print mr-2 text-gray-500"></i> Cetak
</button>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<form method="get" action="<?= site_url('admin/laporan/cuti') ?>" class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div class="flex flex-wrap items-end gap-3">
<div>
<label class="block text-xs font-medium text-gray-600">Dari</label>
<input type="date" name="dari" value="<?= esc($dari) ?>" class="mt-1 rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Sampai</label>
<input type="date" name="sampai" value="<?= esc($sampai) ?>" class="mt-1 rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<button type="submit" class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Tampilkan</button>
<a href="<?= site_url('admin/laporan') ?>" class="inline-flex items-center justify-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-800 shadow-sm hover:bg-gray-50">Ringkasan</a>
</div>
</div>
</form>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="border-b border-gray-200 px-5 py-4">
<h2 class="text-base font-semibold text-gray-900">Daftar cuti</h2>
<p class="mt-0.5 text-xs text-gray-500"><?= count($rows) ?> baris · <?= esc($dari) ?> s/d <?= esc($sampai) ?></p>
</div>
<?php if ($rows === []) : ?>
<div class="p-6">
<?= view('layouts/partials/empty_state', [
'icon' => 'fa-file-lines',
'title' => 'Tidak ada cuti pada rentang ini',
'hint' => 'Perlebar rentang tanggal atau kembali ke ringkasan laporan.',
]) ?>
</div>
<?php else : ?>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50 text-left text-xs font-semibold uppercase tracking-wide text-gray-500">
<tr>
<th class="px-4 py-3">Tanggal</th>
<th class="px-4 py-3">Nama</th>
<th class="px-4 py-3">NIP</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3">Jenis / keterangan</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php foreach ($rows as $r) : ?>
<?php
$st = (string) ($r['status'] ?? '');
$badgeClass = 'bg-gray-100 text-gray-700 ring-1 ring-gray-200';
if ($st === 'Approve') {
$badgeClass = 'bg-emerald-50 text-emerald-800 ring-1 ring-emerald-100';
} elseif ($st === 'Waiting') {
$badgeClass = 'bg-amber-50 text-amber-900 ring-1 ring-amber-100';
} elseif ($st === 'Rejected') {
$badgeClass = 'bg-red-50 text-red-800 ring-1 ring-red-100';
} elseif ($st === 'Cancelled') {
$badgeClass = 'bg-gray-100 text-gray-600 ring-1 ring-gray-200';
}
$jenis = (string) ($r['jenis_cuti'] ?? $r['jenis'] ?? $r['keterangan'] ?? '');
?>
<tr class="transition-colors hover:bg-gray-50">
<td class="whitespace-nowrap px-4 py-2.5 text-gray-800"><?= esc((string) ($r['tanggal_cuti'] ?? '')) ?></td>
<td class="px-4 py-2.5 font-medium text-gray-900"><?= esc((string) ($r['nama_lengkap'] ?? '')) ?></td>
<td class="whitespace-nowrap px-4 py-2.5 font-mono text-xs text-gray-600"><?= esc((string) ($r['nip'] ?? '')) ?></td>
<td class="px-4 py-2.5">
<span class="inline-flex rounded-full px-2.5 py-0.5 text-xs font-semibold <?= $badgeClass ?>"><?= esc($st !== '' ? $st : '—') ?></span>
</td>
<td class="max-w-xs truncate px-4 py-2.5 text-gray-600" title="<?= esc($jenis) ?>"><?= esc($jenis !== '' ? $jenis : '—') ?></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
<?php endif ?>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,86 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Laporan<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-semibold tracking-tight text-gray-900">Ringkasan laporan</h1>
<p class="mt-1 max-w-2xl text-sm text-gray-500">Ringkasan angka untuk rentang tanggal yang dipilih. Untuk daftar cuti per periode, buka <strong>Laporan → Cuti pegawai</strong>.</p>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<form method="get" action="<?= site_url('admin/laporan') ?>" class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div class="flex flex-wrap items-end gap-3">
<div>
<label class="block text-xs font-medium text-gray-600">Dari</label>
<input type="date" name="dari" value="<?= esc($dari) ?>" class="mt-1 rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Sampai</label>
<input type="date" name="sampai" value="<?= esc($sampai) ?>" class="mt-1 rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<button type="submit" class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Tampilkan</button>
<a href="<?= site_url('admin/laporan/cuti') ?>?dari=<?= esc(urlencode($dari)) ?>&sampai=<?= esc(urlencode($sampai)) ?>" class="inline-flex items-center justify-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-800 shadow-sm hover:bg-gray-50">Laporan cuti</a>
</div>
</div>
</form>
<?php if (is_array($summary)) : ?>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">Total pegawai</p>
<p class="mt-2 text-3xl font-bold text-gray-900"><?= number_format((int) ($summary['total_pegawai'] ?? 0), 0, ',', '.') ?></p>
</div>
<div class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">Presensi (rekam) rentang</p>
<p class="mt-2 text-3xl font-bold text-emerald-700"><?= number_format((int) ($summary['presensi_rekam'] ?? 0), 0, ',', '.') ?></p>
</div>
<div class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">Cuti disetujui rentang</p>
<p class="mt-2 text-3xl font-bold text-sky-700"><?= number_format((int) ($summary['cuti_approve'] ?? 0), 0, ',', '.') ?></p>
</div>
<div class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">Presensi hari ini</p>
<p class="mt-2 text-2xl font-bold text-gray-900"><?= number_format((int) ($summary['presensi_hari_ini'] ?? 0), 0, ',', '.') ?></p>
<p class="text-xs text-gray-500"><?= esc((string) ($summary['hari_ini'] ?? '')) ?></p>
</div>
<div class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">Cuti (Approve) hari ini</p>
<p class="mt-2 text-2xl font-bold text-gray-900"><?= number_format((int) ($summary['cuti_hari_ini'] ?? 0), 0, ',', '.') ?></p>
</div>
<div class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">Belum rekam (estimasi)</p>
<p class="mt-2 text-2xl font-bold text-amber-700"><?= number_format((int) ($summary['belum_rekam_hari_ini'] ?? 0), 0, ',', '.') ?></p>
<p class="text-xs text-gray-500">Total presensi hari ini cuti approve hari ini</p>
</div>
</div>
<section class="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">Laporan terkait</p>
<p class="mt-1 text-sm font-semibold text-gray-900">Cuti pegawai (rentang tanggal)</p>
<p class="mt-1 text-xs text-gray-500">Tabel detail per pegawai; cetak dari browser (Ctrl+P) di halaman laporan cuti.</p>
</div>
<a href="<?= site_url('admin/laporan/cuti') ?>?dari=<?= esc(urlencode($dari)) ?>&sampai=<?= esc(urlencode($sampai)) ?>" class="inline-flex shrink-0 items-center justify-center rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">
Buka laporan cuti
</a>
</div>
<p class="mt-5 border-t border-gray-100 pt-4 text-xs text-gray-500">Integrasi: <code class="rounded bg-gray-100 px-1">GET /api/admin/laporan</code> · <code class="rounded bg-gray-100 px-1">GET /api/admin/laporan/cuti</code></p>
</section>
<?php elseif (empty($errors)) : ?>
<div class="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<?= view('layouts/partials/empty_state', [
'icon' => 'fa-chart-simple',
'title' => 'Belum ada ringkasan',
'hint' => 'Pilih tanggal lalu klik Tampilkan untuk memuat angka agregat.',
]) ?>
</div>
<?php endif ?>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,38 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Tambah grup admin<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Tambah grup</h1>
<p class="mt-1 text-sm text-gray-500">Nama unik, huruf/angka/garis bawah, maks. 20 karakter (disimpan huruf kecil). Untuk pembatasan satu cabang, gunakan nama <code class="rounded bg-gray-100 px-1 text-xs">supervisor</code> dan pastikan pengguna punya assign pegawai dengan <code class="rounded bg-gray-100 px-1 text-xs">kantor</code> benar.</p>
</div>
<a href="<?= site_url('admin/panel/groups') ?>" class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-800 shadow-sm hover:bg-gray-50">
<i class="fa-solid fa-arrow-left mr-2 text-gray-500"></i> Kembali
</a>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<section class="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<form method="post" action="<?= site_url('admin/panel/groups/store') ?>" class="grid max-w-xl gap-4">
<?= csrf_field() ?>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Nama grup *</label>
<input name="name" value="<?= esc(old('name', '')) ?>" required maxlength="20" pattern="[a-zA-Z0-9_]+" autocomplete="off" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm" placeholder="supervisor">
<p class="mt-1 text-xs text-gray-500">Hanya a-z, 0-9, underscore. Tidak boleh <code class="rounded bg-gray-100 px-1">webmaster</code>.</p>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Deskripsi *</label>
<input name="description" value="<?= esc(old('description', '')) ?>" required maxlength="100" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm" placeholder="Supervisor cabang">
</div>
<div class="flex flex-wrap gap-3 pt-2">
<button type="submit" class="rounded-lg bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Simpan</button>
<a href="<?= site_url('admin/panel/groups') ?>" class="inline-flex items-center rounded-lg border border-gray-200 px-5 py-2.5 text-sm font-medium text-gray-800 hover:bg-gray-50">Batal</a>
</div>
</form>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,43 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Ubah grup admin<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Ubah grup</h1>
<p class="mt-1 text-sm text-gray-500">ID <?= (int) ($id ?? 0) ?>. Nama webmaster tidak boleh diganti.</p>
</div>
<a href="<?= site_url('admin/panel/groups') ?>" class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-800 shadow-sm hover:bg-gray-50">
<i class="fa-solid fa-arrow-left mr-2 text-gray-500"></i> Kembali
</a>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<?php if (($row ?? null) !== null && is_array($row)) : ?>
<?php $gname = strtolower((string) ($row['name'] ?? '')); ?>
<section class="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<form method="post" action="<?= site_url('admin/panel/groups/update/' . (int) ($row['id'] ?? 0)) ?>" class="grid max-w-xl gap-4">
<?= csrf_field() ?>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Nama grup *</label>
<input name="name" value="<?= esc(old('name', (string) ($row['name'] ?? ''))) ?>" required maxlength="20" pattern="[a-zA-Z0-9_]+" <?= $gname === 'webmaster' ? 'readonly class="w-full rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-600"' : 'class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm"' ?>>
<?php if ($gname === 'webmaster') : ?>
<p class="mt-1 text-xs text-amber-700">Nama webmaster dikunci.</p>
<?php endif ?>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Deskripsi *</label>
<input name="description" value="<?= esc(old('description', (string) ($row['description'] ?? ''))) ?>" required maxlength="100" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div class="flex flex-wrap gap-3 pt-2">
<button type="submit" class="rounded-lg bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Simpan</button>
<a href="<?= site_url('admin/panel/groups') ?>" class="inline-flex items-center rounded-lg border border-gray-200 px-5 py-2.5 text-sm font-medium text-gray-800 hover:bg-gray-50">Batal</a>
</div>
</form>
</section>
<?php endif ?>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,83 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Grup akses admin<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div class="flex flex-col justify-between gap-3 sm:flex-row sm:items-center">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Grup akses</h1>
<p class="mt-1 text-sm text-gray-500">Grup menentukan izin menu di panel. Contoh: <code class="rounded bg-gray-100 px-1 text-xs">hrd</code>, <code class="rounded bg-gray-100 px-1 text-xs">supervisor</code> (beberapa grup hanya melihat data sesuai kantor pegawai yang sedang login).</p>
</div>
<div class="flex flex-wrap gap-2">
<a href="<?= site_url('admin/panel/users') ?>" class="inline-flex items-center justify-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-800 shadow-sm hover:bg-gray-50">
<i class="fa-solid fa-users mr-2 text-gray-500"></i> Pengguna
</a>
<a href="<?= site_url('admin/panel/groups/create') ?>" class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">
<i class="fa-solid fa-plus mr-2"></i> Tambah grup
</a>
</div>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="border-b border-gray-200 px-5 py-4">
<h2 class="text-base font-semibold text-gray-900">Daftar grup</h2>
<p class="text-xs text-gray-500">Grup <strong>webmaster</strong> tidak bisa dihapus. Tombol <strong>Hapus</strong> nonaktif jika grup masih dipakai pengguna — pindahkan pengguna ke grup lain dulu.</p>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="border-y border-gray-200 bg-gray-50 text-left text-xs font-semibold uppercase tracking-wide text-gray-500">
<tr>
<th class="px-4 py-3">ID</th>
<th class="px-4 py-3">Nama</th>
<th class="px-4 py-3">Deskripsi</th>
<th class="px-4 py-3">Pengguna</th>
<th class="px-4 py-3 w-40">Tindakan</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php if (($rows ?? []) === []) : ?>
<tr>
<td colspan="5" class="px-4 py-10 text-center text-gray-500">Tidak ada grup atau gagal memuat.</td>
</tr>
<?php else : ?>
<?php foreach ($rows as $r) : ?>
<?php
$gid = (int) ($r['id'] ?? 0);
$gname = strtolower((string) ($r['name'] ?? ''));
$isWm = $gname === 'webmaster';
$uc = (int) ($r['user_count'] ?? 0);
?>
<tr class="hover:bg-gray-50/80">
<td class="whitespace-nowrap px-4 py-2 font-mono text-xs text-gray-600"><?= $gid ?></td>
<td class="whitespace-nowrap px-4 py-2 font-medium text-gray-900"><?= esc((string) ($r['name'] ?? '')) ?></td>
<td class="px-4 py-2 text-gray-600"><?= esc((string) ($r['description'] ?? '')) ?></td>
<td class="px-4 py-2 text-gray-600"><?= $uc ?></td>
<td class="px-4 py-2">
<div class="flex flex-wrap gap-1">
<a href="<?= site_url('admin/panel/groups/edit/' . $gid) ?>" class="inline-flex rounded-lg border border-gray-200 bg-white px-2 py-1 text-xs font-semibold text-gray-800 shadow-sm hover:bg-gray-50">Ubah</a>
<?php if (! $isWm) : ?>
<form method="post" action="<?= site_url('admin/panel/groups/delete/' . $gid) ?>" class="inline" onsubmit="<?= $uc > 0 ? 'return false;' : "return confirm('Hapus grup ini?');" ?>">
<?= csrf_field() ?>
<button
type="submit"
class="inline-flex rounded-lg border px-2 py-1 text-xs font-semibold shadow-sm <?= $uc > 0 ? 'cursor-not-allowed border-gray-200 bg-gray-100 text-gray-400' : 'border-red-200 bg-red-50 text-red-800 hover:bg-red-100' ?>"
<?= $uc > 0 ? 'disabled title="Grup masih dipakai ' . $uc . ' pengguna. Ubah pengguna lewat menu Pengguna → hapus dari grup ini dulu."' : 'title="Hapus grup ini"' ?>
>Hapus</button>
</form>
<?php else : ?>
<span class="text-xs text-gray-400" title="Grup sistem">—</span>
<?php endif ?>
</div>
</td>
</tr>
<?php endforeach ?>
<?php endif ?>
</tbody>
</table>
</div>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,99 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Tambah pengguna admin<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Tambah pengguna admin</h1>
<p class="mt-1 text-sm text-gray-500">Grup webmaster tidak bisa dipilih di sini. Akun bertipe pegawai menghubungkan login admin ke data pegawai tertentu.</p>
</div>
<a href="<?= site_url('admin/panel/users') ?>" class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-800 shadow-sm hover:bg-gray-50">
<i class="fa-solid fa-arrow-left mr-2 text-gray-500"></i> Kembali ke daftar
</a>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<?php
$groupChoices = 0;
foreach ($groups as $g) {
if (strtolower((string) ($g['name'] ?? '')) !== 'webmaster') {
$groupChoices++;
}
}
?>
<?php if ($groupChoices === 0) : ?>
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">
Tidak ada grup yang bisa dipilih (semua grup mungkin webmaster, atau belum ada grup). Tambah grup lewat <a href="<?= site_url('admin/panel/groups/create') ?>" class="font-medium text-amber-950 underline">Tambah grup</a> atau hubungi webmaster.
</div>
<?php endif ?>
<section class="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<h2 class="text-base font-semibold text-gray-900">Form pengguna</h2>
<form method="post" action="<?= site_url('admin/panel/users/store') ?>" class="mt-6 grid gap-4 sm:grid-cols-2">
<?= csrf_field() ?>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Username *</label>
<input name="username" value="<?= esc(old('username', '')) ?>" required autocomplete="username" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Password *</label>
<input type="password" name="password" required autocomplete="new-password" minlength="6" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Nama lengkap *</label>
<input name="nama_lengkap" value="<?= esc(old('nama_lengkap', '')) ?>" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Email</label>
<input type="email" name="email" value="<?= esc(old('email', '')) ?>" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">No. telepon</label>
<input name="no_telepon" value="<?= esc(old('no_telepon', '')) ?>" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div class="sm:col-span-2">
<label class="mb-1 block text-xs font-medium text-gray-600">Pegawai untuk token API (opsional)</label>
<select name="id_pegawai" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
<option value="">— Default (urutan .env / super_akses) —</option>
<?php foreach ($pegawai_rows ?? [] as $p) : ?>
<?php
$pid = (int) ($p['id_pegawai'] ?? 0);
if ($pid <= 0) {
continue;
}
$label = trim((string) ($p['nama_lengkap'] ?? '')) . ' — NIP ' . (string) ($p['nip'] ?? '');
?>
<option value="<?= $pid ?>" <?= (string) old('id_pegawai') === (string) $pid ? 'selected' : '' ?>><?= esc($label) ?></option>
<?php endforeach ?>
</select>
<p class="mt-1 text-xs text-gray-500">Kosongkan jika satu akun admin boleh memakai aturan fallback lama. Isi agar login panel memakai pegawai ini sebagai proxy.</p>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Grup *</label>
<select name="group_id" required class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
<option value="">— Pilih grup —</option>
<?php foreach ($groups as $g) : ?>
<?php
$gid = (int) ($g['id'] ?? 0);
$gname = strtolower((string) ($g['name'] ?? ''));
if ($gname === 'webmaster') {
continue;
}
?>
<option value="<?= $gid ?>" <?= (string) old('group_id') === (string) $gid ? 'selected' : '' ?>>
<?= esc((string) ($g['name'] ?? '')) ?><?= isset($g['description']) && (string) $g['description'] !== '' ? ' — ' . esc((string) $g['description']) : '' ?>
</option>
<?php endforeach ?>
</select>
</div>
<div class="sm:col-span-2 flex flex-wrap gap-3 pt-2">
<button type="submit" <?= $groupChoices === 0 ? 'disabled' : '' ?> class="rounded-lg bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50">Simpan</button>
<a href="<?= site_url('admin/panel/users') ?>" class="inline-flex items-center rounded-lg border border-gray-200 px-5 py-2.5 text-sm font-medium text-gray-800 hover:bg-gray-50">Batal</a>
</div>
</form>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,139 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Ubah pengguna admin<?= $this->endSection() ?>
<?= $this->section('content') ?>
<?php
$user = $user ?? null;
$isWm = false;
if (is_array($user)) {
$gnames = $user['groups'] ?? [];
$gnames = is_array($gnames) ? $gnames : [];
foreach ($gnames as $gn) {
if (strtolower((string) $gn) === 'webmaster') {
$isWm = true;
break;
}
}
}
?>
<div class="space-y-6">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Ubah pengguna admin</h1>
<p class="mt-1 text-sm text-gray-500">ID <?= (int) ($id ?? 0) ?>. Password diubah lewat <strong>Reset password</strong>. Akun webmaster: grup tetap webmaster.</p>
</div>
<a href="<?= site_url('admin/panel/users') ?>" class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-800 shadow-sm hover:bg-gray-50">
<i class="fa-solid fa-arrow-left mr-2 text-gray-500"></i> Kembali ke daftar
</a>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<?php if (is_array($user)) : ?>
<?php
$groupChoices = 0;
foreach ($groups as $g) {
$gname = strtolower((string) ($g['name'] ?? ''));
if ($isWm) {
if ($gname === 'webmaster') {
$groupChoices++;
}
} elseif ($gname !== 'webmaster') {
$groupChoices++;
}
}
$gidCurrent = (int) ($user['group_id'] ?? 0);
if ($isWm && $gidCurrent <= 0) {
foreach ($groups as $gw) {
if (strtolower((string) ($gw['name'] ?? '')) === 'webmaster') {
$gidCurrent = (int) ($gw['id'] ?? 0);
break;
}
}
}
$pegawaiKey = array_key_exists('id_pegawai', $user);
$pidCurrent = $pegawaiKey ? (int) ($user['id_pegawai'] ?? 0) : 0;
$actCurrent = (int) ($user['active'] ?? 1) === 1 ? '1' : '0';
?>
<section class="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<form method="post" action="<?= site_url('admin/panel/users/update/' . (int) ($user['id'] ?? 0)) ?>" class="mt-0 grid gap-4 sm:grid-cols-2">
<?= csrf_field() ?>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Username *</label>
<input name="username" value="<?= esc(old('username', (string) ($user['username'] ?? ''))) ?>" required autocomplete="username" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Status *</label>
<select name="active" required class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
<option value="1" <?= (string) old('active', $actCurrent) === '1' ? 'selected' : '' ?>>Aktif</option>
<option value="0" <?= (string) old('active', $actCurrent) === '0' ? 'selected' : '' ?>>Nonaktif</option>
</select>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Nama lengkap *</label>
<input name="nama_lengkap" value="<?= esc(old('nama_lengkap', (string) ($user['nama_lengkap'] ?? ''))) ?>" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Email</label>
<input type="email" name="email" value="<?= esc(old('email', (string) ($user['email'] ?? ''))) ?>" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">No. telepon</label>
<input name="no_telepon" value="<?= esc(old('no_telepon', (string) ($user['no_telepon'] ?? ''))) ?>" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<?php if ($pegawaiKey) : ?>
<div class="sm:col-span-2">
<label class="mb-1 block text-xs font-medium text-gray-600">Pegawai untuk token API (opsional)</label>
<select name="id_pegawai" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
<option value="">— Default (urutan .env / super_akses) —</option>
<?php foreach ($pegawai_rows ?? [] as $p) : ?>
<?php
$pid = (int) ($p['id_pegawai'] ?? 0);
if ($pid <= 0) {
continue;
}
$label = trim((string) ($p['nama_lengkap'] ?? '')) . ' — NIP ' . (string) ($p['nip'] ?? '');
$sel = (string) old('id_pegawai', (string) ($pidCurrent > 0 ? $pidCurrent : '')) === (string) $pid;
?>
<option value="<?= $pid ?>" <?= $sel ? 'selected' : '' ?>><?= esc($label) ?></option>
<?php endforeach ?>
</select>
</div>
<?php endif ?>
<div class="<?= $pegawaiKey ? '' : 'sm:col-span-2' ?>">
<label class="mb-1 block text-xs font-medium text-gray-600">Grup *</label>
<select name="group_id" required class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm" <?= $isWm ? 'disabled' : '' ?>>
<?php foreach ($groups as $g) : ?>
<?php
$gid = (int) ($g['id'] ?? 0);
$gname = strtolower((string) ($g['name'] ?? ''));
if ($isWm) {
if ($gname !== 'webmaster') {
continue;
}
} elseif ($gname === 'webmaster') {
continue;
}
$sel = (string) old('group_id', (string) $gidCurrent) === (string) $gid;
?>
<option value="<?= $gid ?>" <?= $sel ? 'selected' : '' ?>>
<?= esc((string) ($g['name'] ?? '')) ?><?= isset($g['description']) && (string) $g['description'] !== '' ? ' — ' . esc((string) $g['description']) : '' ?>
</option>
<?php endforeach ?>
</select>
<?php if ($isWm) : ?>
<input type="hidden" name="group_id" value="<?= $gidCurrent > 0 ? $gidCurrent : '' ?>">
<p class="mt-1 text-xs text-amber-800">Grup webmaster tidak bisa diganti dari sini.</p>
<?php endif ?>
</div>
<div class="sm:col-span-2 flex flex-wrap gap-3 pt-2">
<button type="submit" <?= $groupChoices === 0 ? 'disabled' : '' ?> class="rounded-lg bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50">Simpan</button>
<a href="<?= site_url('admin/panel/users/reset/' . (int) ($user['id'] ?? 0)) ?>" class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-800 shadow-sm hover:bg-gray-50">Reset password</a>
<a href="<?= site_url('admin/panel/users') ?>" class="inline-flex items-center rounded-lg border border-gray-200 px-5 py-2.5 text-sm font-medium text-gray-800 hover:bg-gray-50">Batal</a>
</div>
</form>
</section>
<?php endif ?>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,54 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Reset password admin<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Reset password</h1>
<p class="mt-1 text-sm text-gray-500">Atur ulang kata sandi untuk pengguna ID <span class="font-mono font-semibold text-gray-800"><?= (int) $id ?></span>.</p>
</div>
<a href="<?= site_url('admin/panel/users') ?>" class="inline-flex items-center justify-center rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-sm font-medium text-gray-800 shadow-sm hover:bg-gray-50">
<i class="fa-solid fa-arrow-left mr-2 text-gray-500"></i> Kembali
</a>
</div>
<?php $flashErr = session()->getFlashdata('error'); ?>
<?php if (is_string($flashErr) && $flashErr !== '') : ?>
<?= view('layouts/partials/admin_alert', ['errors' => [$flashErr]]) ?>
<?php endif ?>
<section class="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<h2 class="text-base font-semibold text-gray-900">Password baru</h2>
<form method="post" action="<?= site_url('admin/panel/users/reset_password/' . (int) $id) ?>" class="mt-6 max-w-md space-y-4" id="form-reset-admin">
<?= csrf_field() ?>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Password baru *</label>
<input type="password" id="pw1" name="password" required minlength="6" autocomplete="new-password" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Ulangi password *</label>
<input type="password" id="pw2" required minlength="6" autocomplete="new-password" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div class="flex flex-wrap gap-3 pt-2">
<button type="submit" class="rounded-lg bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Simpan password</button>
</div>
</form>
</section>
</div>
<script>
(function () {
var f = document.getElementById('form-reset-admin');
if (!f) return;
f.addEventListener('submit', function (e) {
var a = document.getElementById('pw1');
var b = document.getElementById('pw2');
if (!a || !b || a.value !== b.value) {
e.preventDefault();
alert('Kedua password harus sama.');
}
});
})();
</script>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,102 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Pengguna admin<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div class="flex flex-col justify-between gap-3 sm:flex-row sm:items-center">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Daftar pengguna admin</h1>
<p class="mt-1 text-sm text-gray-500">Daftar akun yang dapat masuk ke panel admin.</p>
</div>
<div class="flex flex-wrap gap-2">
<a href="<?= site_url('admin/panel/groups') ?>" class="inline-flex items-center justify-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-800 shadow-sm hover:bg-gray-50">
<i class="fa-solid fa-layer-group mr-2 text-gray-500"></i> Kelola grup
</a>
<a href="<?= site_url('admin/panel/users/create') ?>" class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">
<i class="fa-solid fa-user-plus mr-2"></i> Tambah pengguna
</a>
</div>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="border-b border-gray-200 px-5 py-4">
<h2 class="text-base font-semibold text-gray-900">Pengguna</h2>
<p class="text-xs text-gray-500">Reset password untuk akun yang sudah ada. Pembuatan akun tidak mengizinkan grup webmaster.</p>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="border-y border-gray-200 bg-gray-50 text-left text-xs font-semibold uppercase tracking-wide text-gray-500">
<tr>
<th class="px-4 py-3">ID</th>
<th class="px-4 py-3">Username</th>
<th class="px-4 py-3">Nama</th>
<th class="px-4 py-3">Email</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3">Grup</th>
<th class="px-4 py-3">Pegawai (API)</th>
<th class="px-4 py-3 w-52">Tindakan</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php if ($rows === []) : ?>
<tr>
<td colspan="8" class="px-4 py-10 text-center text-gray-500">Tidak ada pengguna atau tabel belum tersedia.</td>
</tr>
<?php else : ?>
<?php foreach ($rows as $r) : ?>
<?php
$uid = (int) ($r['id'] ?? 0);
$act = (int) ($r['active'] ?? 0) === 1;
$grups = $r['groups'] ?? [];
$glist = is_array($grups) ? $grups : [];
?>
<tr class="hover:bg-gray-50/80">
<td class="whitespace-nowrap px-4 py-2 font-mono text-xs text-gray-600"><?= $uid ?></td>
<td class="whitespace-nowrap px-4 py-2 font-medium text-gray-900"><?= esc((string) ($r['username'] ?? '')) ?></td>
<td class="px-4 py-2"><?= esc((string) ($r['nama_lengkap'] ?? '')) ?></td>
<td class="px-4 py-2 text-gray-600"><?= esc((string) ($r['email'] ?? '')) ?></td>
<td class="px-4 py-2">
<?php if ($act) : ?>
<span class="inline-flex rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-semibold text-emerald-800">Aktif</span>
<?php else : ?>
<span class="inline-flex rounded-full bg-gray-100 px-2 py-0.5 text-xs font-semibold text-gray-600">Nonaktif</span>
<?php endif ?>
</td>
<td class="px-4 py-2">
<div class="flex flex-wrap gap-1">
<?php foreach ($glist as $gn) : ?>
<span class="inline-flex rounded-md bg-sky-50 px-2 py-0.5 text-xs font-medium text-sky-800"><?= esc((string) $gn) ?></span>
<?php endforeach ?>
<?php if ($glist === []) : ?>
<span class="text-xs text-gray-400">—</span>
<?php endif ?>
</div>
</td>
<td class="max-w-xs px-4 py-2 text-xs text-gray-700">
<?php
$pp = $r['pegawai_proxy'] ?? null;
?>
<?php if (is_string($pp) && $pp !== '') : ?>
<?= esc($pp) ?>
<?php else : ?>
<span class="text-gray-400">Default</span>
<?php endif ?>
</td>
<td class="px-4 py-2">
<div class="flex flex-col gap-1 sm:flex-row sm:flex-wrap">
<a href="<?= site_url('admin/panel/users/edit/' . $uid) ?>" class="inline-flex justify-center rounded-lg border border-gray-200 bg-white px-2 py-1 text-xs font-semibold text-gray-800 shadow-sm hover:bg-gray-50">Ubah</a>
<a href="<?= site_url('admin/panel/users/reset/' . $uid) ?>" class="inline-flex justify-center rounded-lg border border-gray-200 bg-white px-2 py-1 text-xs font-semibold text-gray-800 shadow-sm hover:bg-gray-50">Reset password</a>
</div>
</td>
</tr>
<?php endforeach ?>
<?php endif ?>
</tbody>
</table>
</div>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,175 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?><?= $mode === 'create' ? 'Tambah' : 'Edit' ?> Pegawai<?= $this->endSection() ?>
<?php
/** @var string $mode */
/** @var array<string,mixed>|null $row */
/** @var array<string,mixed>|null $refs */
$r = $row ?? [];
$jab = $refs['jabatan'] ?? [];
$uk = $refs['unit_kerja'] ?? [];
$gol = $refs['golongan'] ?? [];
$kan = $refs['kantor'] ?? [];
$jad = $refs['jadwal'] ?? [];
$ph = (string) ($r['photo'] ?? '');
$phDisp = $ph !== '' && $ph !== '-';
?>
<?= $this->section('content') ?>
<div class="mx-auto max-w-3xl space-y-6">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 class="text-2xl font-semibold tracking-tight text-gray-900"><?= $mode === 'create' ? 'Tambah pegawai' : 'Edit pegawai' ?></h1>
<p class="mt-1 text-sm text-gray-500"><?= $mode === 'create' ? 'Isi data pegawai baru lalu simpan.' : 'Perbarui data pegawai lalu simpan.' ?></p>
</div>
<a href="<?= site_url('admin/pegawai') ?>" class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-sm font-medium text-gray-800 shadow-sm hover:bg-gray-50">
<i class="fa-solid fa-arrow-left text-xs text-gray-500"></i> Kembali
</a>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<?php if ($refs === null) : ?>
<div class="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<?= view('layouts/partials/empty_state', [
'icon' => 'fa-triangle-exclamation',
'title' => 'Referensi form tidak dimuat',
'hint' => 'Periksa koneksi API atau login ulang. Tanpa jabatan/unit/kantor, form tidak dapat digunakan.',
]) ?>
</div>
<?php else : ?>
<?php
$action = $mode === 'create' ? site_url('admin/pegawai/store') : site_url('admin/pegawai/update/' . (int) ($r['id_pegawai'] ?? 0));
?>
<form action="<?= $action ?>" method="post" enctype="multipart/form-data" class="space-y-4 rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<?= csrf_field() ?>
<?php if ($mode === 'edit') : ?>
<input type="hidden" name="photo_existing" value="<?= esc(old('photo_existing', $ph), 'attr') ?>">
<?php endif ?>
<div class="grid gap-4 sm:grid-cols-2">
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-gray-600">NIP / NIK *</label>
<input name="nip" required value="<?= esc(old('nip', (string) ($r['nip'] ?? ''))) ?>" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm">
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-gray-600">Nama lengkap *</label>
<input name="nama_lengkap" required value="<?= esc(old('nama_lengkap', (string) ($r['nama_lengkap'] ?? ''))) ?>" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Jenis kelamin *</label>
<select name="jenis_kelamin" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm">
<?php foreach (['Pria', 'Wanita'] as $jk) : ?>
<option value="<?= esc($jk) ?>" <?= old('jenis_kelamin', (string) ($r['jenis_kelamin'] ?? 'Pria')) === $jk ? 'selected' : '' ?>><?= esc($jk) ?></option>
<?php endforeach ?>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Status kepegawaian *</label>
<select name="status_kepegawaian" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm">
<?php foreach (['Pegawai Tetap', 'Kontrak'] as $st) : ?>
<option value="<?= esc($st) ?>" <?= old('status_kepegawaian', (string) ($r['status_kepegawaian'] ?? 'Pegawai Tetap')) === $st ? 'selected' : '' ?>><?= esc($st) ?></option>
<?php endforeach ?>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Tempat lahir</label>
<input name="tempat_lahir" value="<?= esc(old('tempat_lahir', (string) ($r['tempat_lahir'] ?? ''))) ?>" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Tanggal lahir</label>
<input type="date" name="tanggal_lahir" value="<?= esc(old('tanggal_lahir', ($r['tanggal_lahir'] ?? '') && ($r['tanggal_lahir'] ?? '') !== '0000-00-00' ? (string) $r['tanggal_lahir'] : '')) ?>" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm">
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-gray-600">Email</label>
<input name="email" type="email" value="<?= esc(old('email', (string) ($r['email'] ?? ''))) ?>" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Jabatan *</label>
<select name="jabatan" required class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm">
<?php foreach ($jab as $j) : ?>
<option value="<?= (int) ($j['id_jabatan'] ?? 0) ?>" <?= (int) old('jabatan', (string) ($r['jabatan'] ?? '0')) === (int) ($j['id_jabatan'] ?? 0) ? 'selected' : '' ?>><?= esc((string) ($j['nama_jabatan'] ?? '')) ?></option>
<?php endforeach ?>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Unit kerja *</label>
<select name="unit_kerja" required class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm">
<?php foreach ($uk as $u) : ?>
<option value="<?= (int) ($u['id_unit_kerja'] ?? 0) ?>" <?= (int) old('unit_kerja', (string) ($r['unit_kerja'] ?? '0')) === (int) ($u['id_unit_kerja'] ?? 0) ? 'selected' : '' ?>><?= esc((string) ($u['nama_unit_kerja'] ?? '')) ?></option>
<?php endforeach ?>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Golongan *</label>
<select name="golongan_pekerjaan" required class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm">
<?php foreach ($gol as $g) : ?>
<option value="<?= (int) ($g['id_golongan'] ?? 0) ?>" <?= (int) old('golongan_pekerjaan', (string) ($r['golongan_pekerjaan'] ?? '0')) === (int) ($g['id_golongan'] ?? 0) ? 'selected' : '' ?>><?= esc((string) ($g['nama_golongan'] ?? '')) ?></option>
<?php endforeach ?>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Kantor *</label>
<select name="kantor" required class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm">
<?php foreach ($kan as $k) : ?>
<option value="<?= (int) ($k['id_kantor'] ?? 0) ?>" <?= (int) old('kantor', (string) ($r['kantor'] ?? '0')) === (int) ($k['id_kantor'] ?? 0) ? 'selected' : '' ?>><?= esc((string) ($k['nama_kantor'] ?? '')) ?></option>
<?php endforeach ?>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Jadwal *</label>
<select name="jadwal" required class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm">
<?php foreach ($jad as $jd) : ?>
<option value="<?= (int) ($jd['id_jadwal'] ?? 0) ?>" <?= (int) old('jadwal', (string) ($r['jadwal'] ?? '1')) === (int) ($jd['id_jadwal'] ?? 0) ? 'selected' : '' ?>><?= esc((string) ($jd['nama_jadwal'] ?? '')) ?></option>
<?php endforeach ?>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Tanggal bergabung *</label>
<input type="date" name="tanggal_bergabung" required value="<?= esc(old('tanggal_bergabung', ($r['tanggal_bergabung'] ?? '') && ($r['tanggal_bergabung'] ?? '') !== '0000-00-00' ? (string) $r['tanggal_bergabung'] : date('Y-m-d'))) ?>" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Super akses</label>
<select name="super_akses" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm">
<?php foreach (['false', 'true'] as $sa) : ?>
<option value="<?= esc($sa) ?>" <?= old('super_akses', (string) ($r['super_akses'] ?? 'false')) === $sa ? 'selected' : '' ?>><?= esc($sa) ?></option>
<?php endforeach ?>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Username *</label>
<input name="username" required value="<?= esc(old('username', (string) ($r['username'] ?? ''))) ?>" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm" autocomplete="username">
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-gray-600">Password <?= $mode === 'edit' ? '(kosongkan jika tidak diubah)' : '(opsional, default = MD5(NIP))' ?></label>
<input type="password" name="password" value="<?= esc(old('password', '')) ?>" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm" autocomplete="new-password">
</div>
<div class="sm:col-span-2 rounded-xl border border-gray-100 bg-gray-50/80 p-4">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">Foto profil</p>
<p class="mt-1 text-xs text-gray-500">Unggah gambar (maks. 2 MB) atau isi nama file yang sudah ada di folder <code class="rounded bg-white px-1">assets/uploads/pengguna/</code>. Unggah file menggantikan isian nama file.</p>
<?php if ($phDisp) : ?>
<div class="mt-3 flex items-center gap-4">
<img src="<?= esc(base_url('assets/uploads/pengguna/' . rawurlencode(basename(str_replace('\\', '/', $ph))))) ?>" alt="Foto pegawai" class="h-20 w-20 shrink-0 rounded-lg border border-gray-200 object-cover shadow-sm">
<span class="text-xs text-gray-600">File saat ini: <strong class="font-mono text-gray-800"><?= esc(basename(str_replace('\\', '/', $ph))) ?></strong></span>
</div>
<?php endif ?>
<div class="mt-3 grid gap-3 sm:grid-cols-2">
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-gray-600">Unggah foto</label>
<input type="file" name="photo_file" accept="image/jpeg,image/png,image/gif,image/webp" class="mt-1 block w-full text-sm text-gray-600 file:mr-3 file:rounded-lg file:border-0 file:bg-blue-50 file:px-3 file:py-2 file:text-sm file:font-semibold file:text-blue-700 hover:file:bg-blue-100">
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-gray-600">Nama file di server (opsional)</label>
<input type="text" name="photo" value="<?= esc(old('photo', $ph)) ?>" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm font-mono" placeholder="contoh: 67a1b2c3d4e5f-photo.jpg" autocomplete="off">
</div>
</div>
</div>
</div>
<div class="flex flex-col-reverse gap-2 border-t border-gray-100 pt-4 sm:flex-row sm:justify-end">
<a href="<?= site_url('admin/pegawai') ?>" class="inline-flex items-center justify-center rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-sm font-medium text-gray-800 shadow-sm hover:bg-gray-50">Batal</a>
<button type="submit" class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Simpan</button>
</div>
</form>
<?php endif ?>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,110 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Data Pegawai<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div class="flex flex-col justify-between gap-4 sm:flex-row sm:items-start">
<div>
<h1 class="text-2xl font-semibold tracking-tight text-gray-900">Data Pegawai</h1>
<p class="mt-1 max-w-2xl text-sm text-gray-500">Daftar pegawai. Gunakan pencarian untuk memfilter nama atau NIP.</p>
</div>
<?php if (canAccess('pegawai_tambah')) : ?>
<a href="<?= site_url('admin/pegawai/create') ?>" class="inline-flex shrink-0 items-center justify-center rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">
<i class="fa-solid fa-plus mr-2"></i> Tambah pegawai
</a>
<?php endif ?>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<form method="get" action="<?= site_url('admin/pegawai') ?>" class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<div class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div class="min-w-0 flex-1">
<label class="block text-xs font-medium text-gray-600">Cari (nama / NIP / username)</label>
<input type="search" name="q" value="<?= esc($q) ?>" placeholder="Ketik lalu cari…" class="mt-1 w-full max-w-xl rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
</div>
<div class="flex shrink-0 gap-2">
<button type="submit" class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Cari</button>
</div>
</div>
</form>
<?php
$rows = is_array($payload) ? ($payload['rows'] ?? []) : [];
$total = is_array($payload) ? (int) ($payload['total'] ?? 0) : 0;
$totalPage = is_array($payload) ? max(1, (int) ($payload['total_page'] ?? 1)) : 1;
$curPage = is_array($payload) ? (int) ($payload['page'] ?? $page) : $page;
?>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="border-b border-gray-200 px-5 py-4">
<h2 class="text-base font-semibold text-gray-900">Daftar pegawai</h2>
<p class="mt-0.5 text-xs text-gray-500">Edit, reset password (NIP MD5), atau hapus baris.</p>
</div>
<?php if ($rows === []) : ?>
<div class="p-6">
<?= view('layouts/partials/empty_state', [
'icon' => 'fa-users',
'title' => 'Tidak ada pegawai',
'hint' => 'Sesuaikan kata kunci pencarian atau tambah pegawai baru dengan tombol di atas.',
]) ?>
</div>
<?php else : ?>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50 text-left text-xs font-semibold uppercase tracking-wide text-gray-500">
<tr>
<th class="px-4 py-3">NIP</th>
<th class="px-4 py-3">Nama</th>
<th class="px-4 py-3">JK</th>
<th class="px-4 py-3">Jabatan</th>
<th class="px-4 py-3">Unit</th>
<th class="px-4 py-3">Golongan</th>
<th class="px-4 py-3">Kantor</th>
<th class="px-4 py-3 w-44">Tindakan</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php foreach ($rows as $p) : ?>
<tr class="transition-colors hover:bg-gray-50">
<td class="whitespace-nowrap px-4 py-2.5 font-mono text-xs text-gray-800"><?= esc((string) ($p['nip'] ?? '')) ?></td>
<td class="px-4 py-2.5 font-medium text-gray-900"><?= esc((string) ($p['nama_lengkap'] ?? '')) ?></td>
<td class="px-4 py-2.5 text-gray-700"><?= esc((string) ($p['jenis_kelamin'] ?? '')) ?></td>
<td class="px-4 py-2.5 text-gray-700"><?= esc((string) ($p['nama_jabatan'] ?? '')) ?></td>
<td class="px-4 py-2.5 text-gray-700"><?= esc((string) ($p['nama_unit_kerja'] ?? '')) ?></td>
<td class="px-4 py-2.5 text-gray-700"><?= esc((string) ($p['nama_golongan'] ?? '')) ?></td>
<td class="px-4 py-2.5 text-gray-700"><?= esc((string) ($p['nama_kantor'] ?? '')) ?></td>
<td class="px-4 py-2.5">
<div class="flex flex-wrap gap-1.5">
<a href="<?= site_url('admin/pegawai/edit/' . (int) ($p['id_pegawai'] ?? 0)) ?>" class="inline-flex rounded-lg border border-gray-200 bg-white px-2 py-1 text-xs font-semibold text-gray-800 shadow-sm hover:bg-gray-50">Edit</a>
<form action="<?= site_url('admin/pegawai/reset/' . (int) ($p['id_pegawai'] ?? 0)) ?>" method="post" class="inline" onsubmit="return confirm('Reset password ke NIP (MD5) dan kosongkan token?');">
<?= csrf_field() ?>
<button type="submit" class="inline-flex rounded-lg border border-amber-200 bg-amber-50 px-2 py-1 text-xs font-semibold text-amber-900 hover:bg-amber-100">Reset</button>
</form>
<form action="<?= site_url('admin/pegawai/delete/' . (int) ($p['id_pegawai'] ?? 0)) ?>" method="post" class="inline" onsubmit="return confirm('Hapus pegawai ini?');">
<?= csrf_field() ?>
<button type="submit" class="inline-flex rounded-lg border border-red-200 bg-red-50 px-2 py-1 text-xs font-semibold text-red-800 hover:bg-red-100">Hapus</button>
</form>
</div>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
<?php endif ?>
<div class="flex flex-col gap-3 border-t border-gray-200 bg-gray-50/80 px-5 py-3 text-xs text-gray-600 sm:flex-row sm:items-center sm:justify-between">
<span>Total <strong class="text-gray-900"><?= number_format($total, 0, ',', '.') ?></strong> pegawai</span>
<div class="flex flex-wrap gap-2">
<?php if ($curPage > 1) : ?>
<a class="inline-flex rounded-lg border border-gray-200 bg-white px-3 py-1.5 font-medium text-gray-800 shadow-sm hover:bg-gray-50" href="<?= site_url('admin/pegawai?' . http_build_query(['page' => $curPage - 1, 'q' => $q])) ?>">Sebelumnya</a>
<?php endif ?>
<?php if ($curPage < $totalPage) : ?>
<a class="inline-flex rounded-lg border border-gray-200 bg-white px-3 py-1.5 font-medium text-gray-800 shadow-sm hover:bg-gray-50" href="<?= site_url('admin/pegawai?' . http_build_query(['page' => $curPage + 1, 'q' => $q])) ?>">Berikutnya</a>
<?php endif ?>
</div>
</div>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,146 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Berita<?= $this->endSection() ?>
<?php $rows = is_array($payload) ? ($payload['rows'] ?? []) : []; ?>
<?php
$beritaPhotoUrl = static function (string $photo): ?string {
$p = trim($photo);
if ($p === '' || $p === '-') {
return null;
}
$base = basename(str_replace('\\', '/', $p));
return base_url('assets/uploads/berita/' . rawurlencode($base));
};
?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Berita / pengumuman</h1>
<p class="mt-1 text-sm text-gray-500">Foto disimpan di <code class="rounded bg-gray-100 px-1 text-xs">assets/uploads/berita/</code> (sama lokasi dengan aplikasi mobile). Bisa unggah file atau isi nama file yang sudah ada.</p>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<form method="post" action="<?= site_url('admin/perusahaan/berita/save') ?>" enctype="multipart/form-data" class="grid gap-4">
<?= csrf_field() ?>
<input type="hidden" name="id_berita" id="f_id" value="">
<input type="hidden" name="photo_existing" id="f_photo_existing" value="">
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Tanggal *</label>
<input type="date" name="tanggal" id="f_tgl" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div class="sm:col-span-2 rounded-xl border border-gray-100 bg-gray-50/80 p-4">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">Gambar berita</p>
<div id="f_preview_wrap" class="mt-2 hidden">
<img id="f_preview_img" src="" alt="Pratinjau" class="h-20 w-20 rounded-lg border border-gray-200 object-cover shadow-sm">
</div>
<div class="mt-3 grid gap-3 sm:grid-cols-2">
<div class="sm:col-span-2">
<label class="mb-1 block text-xs font-medium text-gray-600">Unggah foto</label>
<input type="file" name="photo_file" id="f_photo_file" accept="image/jpeg,image/png,image/gif,image/webp" class="block w-full text-sm text-gray-600 file:mr-3 file:rounded-lg file:border-0 file:bg-blue-50 file:px-3 file:py-2 file:text-sm file:font-semibold file:text-blue-700 hover:file:bg-blue-100">
</div>
<div class="sm:col-span-2">
<label class="mb-1 block text-xs font-medium text-gray-600">Nama file di server (opsional)</label>
<input name="photo" id="f_photo" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm font-mono shadow-sm" placeholder="-" autocomplete="off">
</div>
</div>
</div>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Judul *</label>
<input name="judul" id="f_judul" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Isi *</label>
<textarea name="isi" id="f_isi" rows="5" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm"></textarea>
</div>
<div class="flex gap-2">
<button type="submit" class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Simpan</button>
<button type="button" id="f_clear" class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50">Bersihkan</button>
</div>
</form>
</section>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="border-y border-gray-200 bg-gray-50 text-left text-xs font-semibold uppercase text-gray-500">
<tr><th class="px-5 py-3">Tanggal</th><th class="px-5 py-3 w-16">Foto</th><th class="px-5 py-3">Judul</th><th class="px-5 py-3"></th></tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php foreach ($rows as $r) : ?>
<?php if (! is_array($r)) {
continue;
} ?>
<?php
$purl = $beritaPhotoUrl((string) ($r['photo'] ?? ''));
?>
<tr class="hover:bg-gray-50">
<td class="whitespace-nowrap px-5 py-3"><?= esc((string) ($r['tanggal'] ?? '')) ?></td>
<td class="px-5 py-3">
<?php if ($purl !== null) : ?>
<img src="<?= esc($purl) ?>" alt="" class="h-10 w-10 rounded-md border border-gray-100 object-cover">
<?php else : ?>
<span class="text-xs text-gray-400">—</span>
<?php endif ?>
</td>
<td class="px-5 py-3 font-medium text-gray-900"><?= esc((string) ($r['judul'] ?? '')) ?></td>
<td class="whitespace-nowrap px-5 py-3">
<button type="button" class="rounded-lg border border-gray-200 px-2 py-1 text-xs font-semibold btn-edit-berita" data-row="<?= esc(json_encode($r, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT), 'attr') ?>">Edit</button>
<form method="post" action="<?= site_url('admin/perusahaan/berita/delete/' . (int) ($r['id_berita'] ?? 0)) ?>" class="inline" onsubmit="return confirm('Hapus?');"><?= csrf_field() ?><button type="submit" class="ml-1 rounded-lg bg-red-50 px-2 py-1 text-xs font-semibold text-red-700">Hapus</button></form>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
</section>
</div>
<script>
(function () {
var base = <?= json_encode(base_url('assets/uploads/berita/')) ?>;
function showPreview(filename) {
var wrap = document.getElementById('f_preview_wrap');
var img = document.getElementById('f_preview_img');
if (!wrap || !img) return;
if (!filename || filename === '-') {
wrap.classList.add('hidden');
img.removeAttribute('src');
return;
}
img.src = base + encodeURIComponent(filename.split(/[/\\]/).pop());
wrap.classList.remove('hidden');
}
document.querySelectorAll('.btn-edit-berita').forEach(function (btn) {
btn.addEventListener('click', function () {
var o = JSON.parse(this.getAttribute('data-row'));
document.getElementById('f_id').value = o.id_berita || '';
document.getElementById('f_tgl').value = o.tanggal || '';
document.getElementById('f_photo').value = (o.photo && o.photo !== '-') ? o.photo : '';
document.getElementById('f_photo_existing').value = o.photo || '';
document.getElementById('f_judul').value = o.judul || '';
document.getElementById('f_isi').value = o.isi || '';
var pf = document.getElementById('f_photo_file');
if (pf) pf.value = '';
showPreview((o.photo && o.photo !== '-') ? o.photo : '');
});
});
document.getElementById('f_clear').addEventListener('click', function () {
document.getElementById('f_id').value = '';
document.getElementById('f_tgl').value = '';
document.getElementById('f_photo').value = '';
document.getElementById('f_photo_existing').value = '';
document.getElementById('f_judul').value = '';
document.getElementById('f_isi').value = '';
var pf = document.getElementById('f_photo_file');
if (pf) pf.value = '';
showPreview('');
});
document.getElementById('f_photo').addEventListener('input', function () {
var v = this.value.trim();
showPreview(v && v !== '-' ? v : '');
});
})();
</script>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,48 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Golongan<?= $this->endSection() ?>
<?php $rows = is_array($payload) ? ($payload['rows'] ?? []) : []; ?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Golongan</h1>
<p class="mt-1 text-sm text-gray-500">Master golongan.</p>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<form method="post" action="<?= site_url('admin/perusahaan/golongan/save') ?>" class="flex flex-wrap items-end gap-3">
<?= csrf_field() ?>
<input type="hidden" name="id_golongan" id="f_id" value="">
<div class="min-w-[16rem] flex-1">
<label class="mb-1 block text-xs font-medium text-gray-600">Nama golongan</label>
<input name="nama_golongan" id="f_nama" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<button type="submit" class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Simpan</button>
</form>
</section>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="border-y border-gray-200 bg-gray-50 text-left text-xs font-semibold uppercase text-gray-500">
<tr><th class="px-5 py-3">Nama</th><th class="px-5 py-3"></th></tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php foreach ($rows as $r) : ?>
<?php if (! is_array($r)) {
continue;
} ?>
<tr class="hover:bg-gray-50">
<td class="px-5 py-3"><?= esc((string) ($r['nama_golongan'] ?? '')) ?></td>
<td class="whitespace-nowrap px-5 py-3">
<button type="button" class="rounded-lg border border-gray-200 px-2 py-1 text-xs font-semibold" data-row="<?= esc(json_encode($r, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT), 'attr') ?>" onclick="var o=JSON.parse(this.dataset.row);document.getElementById('f_id').value=o.id_golongan||'';document.getElementById('f_nama').value=o.nama_golongan||'';">Edit</button>
<form method="post" action="<?= site_url('admin/perusahaan/golongan/delete/' . (int) ($r['id_golongan'] ?? 0)) ?>" class="inline" onsubmit="return confirm('Hapus?');"><?= csrf_field() ?><button type="submit" class="ml-1 rounded-lg bg-red-50 px-2 py-1 text-xs font-semibold text-red-700">Hapus</button></form>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,48 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Jabatan<?= $this->endSection() ?>
<?php $rows = is_array($payload) ? ($payload['rows'] ?? []) : []; ?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Jabatan</h1>
<p class="mt-1 text-sm text-gray-500">Master jabatan.</p>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<form method="post" action="<?= site_url('admin/perusahaan/jabatan/save') ?>" class="flex flex-wrap items-end gap-3">
<?= csrf_field() ?>
<input type="hidden" name="id_jabatan" id="f_id" value="">
<div class="min-w-[16rem] flex-1">
<label class="mb-1 block text-xs font-medium text-gray-600">Nama jabatan *</label>
<input name="nama_jabatan" id="f_nama" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<button type="submit" class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Simpan</button>
</form>
</section>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="border-y border-gray-200 bg-gray-50 text-left text-xs font-semibold uppercase text-gray-500">
<tr><th class="px-5 py-3">Nama</th><th class="px-5 py-3"></th></tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php foreach ($rows as $r) : ?>
<?php if (! is_array($r)) {
continue;
} ?>
<tr class="hover:bg-gray-50">
<td class="px-5 py-3 font-medium text-gray-900"><?= esc((string) ($r['nama_jabatan'] ?? '')) ?></td>
<td class="whitespace-nowrap px-5 py-3">
<button type="button" class="rounded-lg border border-gray-200 px-2 py-1 text-xs font-semibold" data-row="<?= esc(json_encode($r, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT), 'attr') ?>" onclick="var o=JSON.parse(this.dataset.row);document.getElementById('f_id').value=o.id_jabatan||'';document.getElementById('f_nama').value=o.nama_jabatan||'';">Edit</button>
<form method="post" action="<?= site_url('admin/perusahaan/jabatan/delete/' . (int) ($r['id_jabatan'] ?? 0)) ?>" class="inline" onsubmit="return confirm('Hapus?');"><?= csrf_field() ?><button type="submit" class="ml-1 rounded-lg bg-red-50 px-2 py-1 text-xs font-semibold text-red-700">Hapus</button></form>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,121 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Lokasi kerja<?= $this->endSection() ?>
<?php
$rows = is_array($payload) ? ($payload['rows'] ?? []) : [];
?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Lokasi kerja (Kantor)</h1>
<p class="mt-1 text-sm text-gray-500">Kelola data kantor atau cabang perusahaan.</p>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<h2 class="text-base font-semibold text-gray-900">Tambah / ubah</h2>
<form method="post" action="<?= site_url('admin/perusahaan/kantor/save') ?>" class="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<?= csrf_field() ?>
<input type="hidden" name="id_kantor" id="f_id_kantor" value="">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Kode kantor *</label>
<input name="kode_kantor" id="f_kode" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Nama *</label>
<input name="nama_kantor" id="f_nama" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Tipe *</label>
<input name="tipe" id="f_tipe" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div class="sm:col-span-2">
<label class="mb-1 block text-xs font-medium text-gray-600">Alamat *</label>
<input name="alamat_kantor" id="f_alamat" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Latitude *</label>
<input name="lat" id="f_lat" required step="any" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Longitude *</label>
<input name="lng" id="f_lng" required step="any" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Jarak rekam (m)</label>
<input type="number" name="jarak_rekam_presensi" id="f_jarak" value="30" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div class="flex items-end gap-2">
<button type="submit" class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Simpan</button>
<button type="button" onclick="document.getElementById('f_id_kantor').value='';this.form.reset();" class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50">Bersihkan</button>
</div>
</form>
</section>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="border-b border-gray-200 px-5 py-4">
<h2 class="text-base font-semibold text-gray-900">Daftar kantor</h2>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="border-y border-gray-200 bg-gray-50 text-left text-xs font-semibold uppercase text-gray-500">
<tr>
<th class="px-5 py-3">Kode</th>
<th class="px-5 py-3">Nama</th>
<th class="px-5 py-3">Tipe</th>
<th class="px-5 py-3">Peta</th>
<th class="px-5 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php if ($rows === []) : ?>
<tr><td colspan="5" class="px-5 py-10 text-center text-gray-500">Belum ada data.</td></tr>
<?php else : ?>
<?php foreach ($rows as $k) : ?>
<?php if (! is_array($k)) {
continue;
} ?>
<?php
$lat = (float) ($k['lat'] ?? 0);
$lng = (float) ($k['lng'] ?? 0);
$map = ($lat != 0.0 || $lng != 0.0) ? 'https://www.google.com/maps/place/' . rawurlencode((string) $lat) . ',' . rawurlencode((string) $lng) : '';
?>
<tr class="hover:bg-gray-50">
<td class="px-5 py-3 font-mono text-xs"><?= esc((string) ($k['kode_kantor'] ?? '')) ?></td>
<td class="px-5 py-3 font-medium text-gray-900"><?= esc((string) ($k['nama_kantor'] ?? '')) ?></td>
<td class="px-5 py-3"><?= esc((string) ($k['tipe'] ?? '')) ?></td>
<td class="px-5 py-3"><?= $map !== '' ? '<a href="' . esc($map) . '" target="_blank" rel="noopener" class="text-blue-600 hover:underline">Lihat</a>' : '—' ?></td>
<td class="whitespace-nowrap px-5 py-3">
<button type="button" class="edit-kantor rounded-lg border border-gray-200 bg-white px-2 py-1 text-xs font-semibold text-gray-800 shadow-sm hover:bg-gray-50" data-row="<?= esc(json_encode($k, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT), 'attr') ?>">Edit</button>
<form method="post" action="<?= site_url('admin/perusahaan/kantor/delete/' . (int) ($k['id_kantor'] ?? 0)) ?>" class="inline" onsubmit="return confirm('Hapus kantor ini?');">
<?= csrf_field() ?>
<button type="submit" class="ml-1 rounded-lg bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100">Hapus</button>
</form>
</td>
</tr>
<?php endforeach ?>
<?php endif ?>
</tbody>
</table>
</div>
</section>
</div>
<script>
document.querySelectorAll('.edit-kantor').forEach(function (btn) {
btn.addEventListener('click', function () {
var row = JSON.parse(this.getAttribute('data-row'));
document.getElementById('f_id_kantor').value = row.id_kantor || '';
document.getElementById('f_kode').value = row.kode_kantor || '';
document.getElementById('f_nama').value = row.nama_kantor || '';
document.getElementById('f_tipe').value = row.tipe || '';
document.getElementById('f_alamat').value = row.alamat_kantor || '';
document.getElementById('f_lat').value = row.lat || '';
document.getElementById('f_lng').value = row.lng || '';
document.getElementById('f_jarak').value = row.jarak_rekam_presensi || 30;
window.scrollTo({ top: 0, behavior: 'smooth' });
});
});
</script>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,54 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Unit kerja<?= $this->endSection() ?>
<?php $rows = is_array($payload) ? ($payload['rows'] ?? []) : []; ?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Unit kerja</h1>
<p class="mt-1 text-sm text-gray-500">Master unit kerja pegawai.</p>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<h2 class="text-base font-semibold text-gray-900">Tambah / ubah</h2>
<form method="post" action="<?= site_url('admin/perusahaan/unit_kerja/save') ?>" class="mt-4 flex flex-wrap items-end gap-3">
<?= csrf_field() ?>
<input type="hidden" name="id_unit_kerja" id="f_id" value="">
<div class="min-w-[16rem] flex-1">
<label class="mb-1 block text-xs font-medium text-gray-600">Nama unit *</label>
<input name="nama_unit_kerja" id="f_nama" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<button type="submit" class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Simpan</button>
<button type="button" onclick="document.getElementById('f_id').value='';document.getElementById('f_nama').value='';" class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50">Bersihkan</button>
</form>
</section>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="border-y border-gray-200 bg-gray-50 text-left text-xs font-semibold uppercase text-gray-500">
<tr><th class="px-5 py-3">Nama</th><th class="px-5 py-3"></th></tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php if ($rows === []) : ?>
<tr><td colspan="2" class="px-5 py-10 text-center text-gray-500">Kosong.</td></tr>
<?php else : ?>
<?php foreach ($rows as $r) : ?>
<?php if (! is_array($r)) {
continue;
} ?>
<tr class="hover:bg-gray-50">
<td class="px-5 py-3 font-medium text-gray-900"><?= esc((string) ($r['nama_unit_kerja'] ?? '')) ?></td>
<td class="whitespace-nowrap px-5 py-3">
<button type="button" class="rounded-lg border border-gray-200 bg-white px-2 py-1 text-xs font-semibold shadow-sm hover:bg-gray-50" data-row="<?= esc(json_encode($r, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT), 'attr') ?>" onclick="var o=JSON.parse(this.dataset.row);document.getElementById('f_id').value=o.id_unit_kerja||'';document.getElementById('f_nama').value=o.nama_unit_kerja||'';">Edit</button>
<form method="post" action="<?= site_url('admin/perusahaan/unit_kerja/delete/' . (int) ($r['id_unit_kerja'] ?? 0)) ?>" class="inline" onsubmit="return confirm('Hapus?');"><?= csrf_field() ?><button type="submit" class="ml-1 rounded-lg bg-red-50 px-2 py-1 text-xs font-semibold text-red-700">Hapus</button></form>
</td>
</tr>
<?php endforeach ?>
<?php endif ?>
</tbody>
</table>
</div>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,76 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Aktivitas harian<?= $this->endSection() ?>
<?php
$rows = is_array($payload) ? ($payload['rows'] ?? []) : [];
$total = is_array($payload) ? (int) ($payload['total'] ?? 0) : 0;
$totalPage = is_array($payload) ? max(1, (int) ($payload['total_page'] ?? 1)) : 1;
$curPage = is_array($payload) ? (int) ($payload['page'] ?? $page) : $page;
?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Rekaman aktivitas harian</h1>
<p class="mt-1 text-sm text-gray-500">Log aktivitas dari aplikasi pegawai. Hanya dapat dilihat atau dihapus di sini.</p>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="flex items-center justify-between border-b border-gray-200 px-5 py-4">
<h2 class="text-base font-semibold text-gray-900">Daftar</h2>
<span class="text-xs text-gray-500">Total <?= number_format($total, 0, ',', '.') ?></span>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="border-y border-gray-200 bg-gray-50 text-left text-xs font-semibold uppercase text-gray-500">
<tr>
<th class="px-5 py-3">Waktu</th>
<th class="px-5 py-3">Pegawai</th>
<th class="px-5 py-3">Deskripsi</th>
<th class="px-5 py-3">Gambar</th>
<th class="px-5 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php if ($rows === []) : ?>
<tr><td colspan="5" class="px-5 py-10 text-center text-gray-500">Tidak ada data.</td></tr>
<?php else : ?>
<?php foreach ($rows as $x) : ?>
<?php if (! is_array($x)) {
continue;
} ?>
<tr class="hover:bg-gray-50">
<td class="whitespace-nowrap px-5 py-3 text-xs"><?= esc((string) ($x['waktu_aktifitas'] ?? '')) ?></td>
<td class="px-5 py-3">
<div class="font-medium text-gray-900"><?= esc((string) ($x['nama_lengkap'] ?? '')) ?></div>
<div class="text-xs text-gray-500"><?= esc((string) ($x['nip'] ?? '')) ?></div>
</td>
<td class="max-w-xs truncate px-5 py-3" title="<?= esc((string) ($x['deskripsi'] ?? '')) ?>"><?= esc((string) ($x['deskripsi'] ?? '')) ?></td>
<td class="px-5 py-3 text-xs"><?= esc((string) ($x['image'] ?? '')) ?></td>
<td class="whitespace-nowrap px-5 py-3">
<form method="post" action="<?= site_url('admin/presensi/aktivitas/delete/' . (int) ($x['id_aktifitas_harian'] ?? 0)) ?>" class="inline" onsubmit="return confirm('Hapus rekaman ini?');">
<?= csrf_field() ?>
<input type="hidden" name="page" value="<?= (int) $curPage ?>">
<button type="submit" class="rounded-lg bg-red-50 px-2 py-1 text-xs font-semibold text-red-700 hover:bg-red-100">Hapus</button>
</form>
</td>
</tr>
<?php endforeach ?>
<?php endif ?>
</tbody>
</table>
</div>
<div class="flex items-center justify-between border-t border-gray-200 bg-gray-50/80 px-5 py-3 text-xs text-gray-600">
<span>Halaman <?= (int) $curPage ?> / <?= (int) $totalPage ?></span>
<div class="flex gap-2">
<?php if ($curPage > 1) : ?>
<a class="inline-flex rounded-lg border border-gray-200 bg-white px-3 py-1.5 font-medium shadow-sm hover:bg-gray-50" href="<?= site_url('admin/presensi/aktivitas?' . http_build_query(['page' => $curPage - 1])) ?>">Sebelumnya</a>
<?php endif ?>
<?php if ($curPage < $totalPage) : ?>
<a class="inline-flex rounded-lg border border-gray-200 bg-white px-3 py-1.5 font-medium shadow-sm hover:bg-gray-50" href="<?= site_url('admin/presensi/aktivitas?' . http_build_query(['page' => $curPage + 1])) ?>">Berikutnya</a>
<?php endif ?>
</div>
</div>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,65 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Detail presensi<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="mx-auto max-w-3xl space-y-6">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 class="text-2xl font-semibold tracking-tight text-gray-900">Detail presensi</h1>
<p class="mt-1 text-sm text-gray-500">Rekap satu baris presensi.</p>
</div>
<a href="<?= site_url('admin/presensi') ?>" class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-sm font-medium text-gray-800 shadow-sm hover:bg-gray-50">
<i class="fa-solid fa-arrow-left text-xs text-gray-500"></i> Kembali ke daftar
</a>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<?php if (is_array($row)) : ?>
<dl class="grid gap-4 rounded-2xl border border-gray-200 bg-white p-6 text-sm shadow-sm sm:grid-cols-2">
<?php foreach ($row as $k => $v) : ?>
<?php if ($k === 'jadwal') : ?>
<div class="sm:col-span-2">
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500"><?= esc((string) $k) ?></dt>
<dd class="mt-1 break-all rounded-xl bg-gray-900 p-3 font-mono text-xs text-gray-100"><?= esc(is_scalar($v) ? (string) $v : json_encode($v, JSON_UNESCAPED_UNICODE)) ?></dd>
</div>
<?php else : ?>
<div>
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500"><?= esc((string) $k) ?></dt>
<dd class="mt-1 font-medium text-gray-900"><?= esc(is_scalar($v) || $v === null ? (string) ($v ?? '') : json_encode($v, JSON_UNESCAPED_UNICODE)) ?></dd>
</div>
<?php endif ?>
<?php endforeach ?>
</dl>
<?php
$photoM = $row['photo_masuk'] ?? '';
$photoP = $row['photo_pulang'] ?? '';
?>
<?php if ($photoM !== '' || $photoP !== '') : ?>
<div class="grid gap-4 sm:grid-cols-2">
<?php if ($photoM !== '') : ?>
<div class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<h2 class="text-sm font-semibold text-gray-900">Foto masuk</h2>
<img src="<?= esc(site_url('admin/presensi/foto/masuk/' . rawurlencode($photoM))) ?>" alt="Foto masuk" class="mt-3 max-h-64 rounded-xl border border-gray-100 object-contain">
</div>
<?php endif ?>
<?php if ($photoP !== '') : ?>
<div class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<h2 class="text-sm font-semibold text-gray-900">Foto pulang</h2>
<img src="<?= esc(site_url('admin/presensi/foto/pulang/' . rawurlencode($photoP))) ?>" alt="Foto pulang" class="mt-3 max-h-64 rounded-xl border border-gray-100 object-contain">
</div>
<?php endif ?>
</div>
<?php endif ?>
<?php else : ?>
<div class="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<?= view('layouts/partials/empty_state', [
'icon' => 'fa-circle-question',
'title' => 'Data tidak ditemukan',
'hint' => 'Presensi mungkin sudah dihapus atau ID tidak valid.',
]) ?>
</div>
<?php endif ?>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,153 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Presensi<?= $this->endSection() ?>
<?php
$rows = is_array($payload) ? ($payload['rows'] ?? []) : [];
$total = is_array($payload) ? (int) ($payload['total'] ?? 0) : 0;
$totalPage = is_array($payload) ? max(1, (int) ($payload['total_page'] ?? 1)) : 1;
$curPage = is_array($payload) ? (int) ($payload['page'] ?? $page) : $page;
?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-semibold tracking-tight text-gray-900">Data presensi pegawai</h1>
<p class="mt-1 max-w-2xl text-sm text-gray-500">Filter rentang tanggal (default hari ini) dan pencarian nama atau NIP.</p>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<form method="get" action="<?= site_url('admin/presensi') ?>" class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div class="flex flex-wrap items-end gap-3">
<div>
<label class="block text-xs font-medium text-gray-600">Tanggal dari</label>
<input type="date" name="tanggal_dari" value="<?= esc($dari) ?>" class="mt-1 rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Tanggal sampai</label>
<input type="date" name="tanggal_sampai" value="<?= esc($sampai) ?>" class="mt-1 rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
</div>
<div class="min-w-[min(100%,14rem)] flex-1 sm:min-w-[16rem]">
<label class="block text-xs font-medium text-gray-600">Cari nama / NIP</label>
<input type="search" name="q" value="<?= esc($q ?? '') ?>" placeholder="Opsional" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
</div>
</div>
<div class="flex flex-shrink-0 flex-wrap items-center gap-2">
<button type="submit" class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Terapkan</button>
<a href="<?= site_url('admin/presensi?' . http_build_query(['tanggal_dari' => date('Y-m-d'), 'tanggal_sampai' => date('Y-m-d')])) ?>" class="inline-flex items-center justify-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-800 shadow-sm hover:bg-gray-50">Hari ini</a>
<?php $exportQs = array_filter(['tanggal_dari' => $dari, 'tanggal_sampai' => $sampai, 'q' => $q ?? '']); ?>
<a href="<?= site_url('admin/presensi/export-xlsx?' . http_build_query($exportQs)) ?>" class="inline-flex items-center justify-center rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-2 text-sm font-semibold text-emerald-900 shadow-sm hover:bg-emerald-100">
<i class="fa-solid fa-file-excel mr-2"></i> Export Excel
</a>
<a href="<?= site_url('admin/presensi/export?' . http_build_query($exportQs)) ?>" class="inline-flex items-center justify-center rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-medium text-gray-600 shadow-sm hover:bg-gray-50" title="Format CSV untuk aplikasi lain">CSV</a>
</div>
</div>
</form>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="border-b border-gray-200 px-5 py-4">
<h2 class="text-base font-semibold text-gray-900">Rekaman presensi</h2>
<p class="mt-0.5 text-xs text-gray-500">Hanya menampilkan pegawai yang sudah punya minimal satu rekam (masuk / pulang / istirahat atau foto). Baris kosong dari aplikasi (buka app tanpa absen) tidak ditampilkan. Status: lengkap (hijau), sebagian (kuning), belum rekam (abu) untuk baris yang tampil. <strong>Export Excel</strong> (dan CSV) memakai filter tanggal dan pencarian yang sama (maks. 20.000 baris); file .xlsx sudah berheader, filter, dan lebar kolom.</p>
</div>
<?php if ($rows === []) : ?>
<div class="p-6">
<?= view('layouts/partials/empty_state', [
'icon' => 'fa-calendar-xmark',
'title' => 'Tidak ada data presensi',
'hint' => 'Ubah rentang tanggal atau kata kunci pencarian, lalu klik Terapkan.',
]) ?>
</div>
<?php else : ?>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50 text-left text-xs font-semibold uppercase tracking-wide text-gray-500">
<tr>
<th class="px-5 py-3">Tanggal</th>
<th class="px-5 py-3">Pegawai</th>
<th class="px-5 py-3">Status</th>
<th class="px-5 py-3">Masuk</th>
<th class="px-5 py-3">Pulang</th>
<th class="px-5 py-3">Istirahat</th>
<th class="px-5 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php foreach ($rows as $pr) : ?>
<?php
$jm = $pr['jam_masuk'] ?? null;
$jp = $pr['jam_pulang'] ?? null;
$mi = $pr['mulai_istirahat'] ?? null;
$bi = $pr['beres_istirahat'] ?? null;
$hadirMasuk = ! empty($jm);
$hadirPulang = ! empty($jp);
if ($hadirMasuk && $hadirPulang) {
$stLabel = 'Lengkap';
$stClass = 'bg-emerald-50 text-emerald-800 ring-1 ring-emerald-100';
} elseif ($hadirMasuk || $hadirPulang) {
$stLabel = 'Sebagian';
$stClass = 'bg-amber-50 text-amber-900 ring-1 ring-amber-100';
} else {
$stLabel = 'Belum rekam';
$stClass = 'bg-gray-100 text-gray-600 ring-1 ring-gray-200';
}
?>
<tr class="transition-colors hover:bg-gray-50">
<td class="whitespace-nowrap px-5 py-3 text-gray-800"><?= esc((string) ($pr['tanggal'] ?? '')) ?></td>
<td class="px-5 py-3">
<div class="font-medium text-gray-900"><?= esc((string) ($pr['nama_lengkap'] ?? '')) ?></div>
<div class="text-xs text-gray-500"><?= esc((string) ($pr['nip'] ?? '')) ?></div>
</td>
<td class="whitespace-nowrap px-5 py-3">
<span class="inline-flex rounded-full px-2.5 py-0.5 text-xs font-semibold <?= $stClass ?>"><?= esc($stLabel) ?></span>
</td>
<td class="px-5 py-3">
<?php if (! empty($jm)) : ?>
<span class="font-mono text-xs text-gray-900"><?= esc(date('H:i', strtotime((string) $jm))) ?></span>
<div class="text-xs text-gray-500"><?= esc((string) ($pr['ket_masuk'] ?? '')) ?></div>
<?php else : ?>
<span class="text-gray-400">—</span>
<?php endif ?>
</td>
<td class="px-5 py-3">
<?php if (! empty($jp)) : ?>
<span class="font-mono text-xs text-gray-900"><?= esc(date('H:i', strtotime((string) $jp))) ?></span>
<div class="text-xs text-gray-500"><?= esc((string) ($pr['ket_pulang'] ?? '')) ?></div>
<?php else : ?>
<span class="text-gray-400">—</span>
<?php endif ?>
</td>
<td class="px-5 py-3 text-xs text-gray-700">
<?php if (! empty($mi) || ! empty($bi)) : ?>
<?php
$s = (empty($mi) ? '—' : date('H:i', strtotime((string) $mi))) . ' → ' . (empty($bi) ? '—' : date('H:i', strtotime((string) $bi)));
?>
<?= esc($s) ?>
<?php else : ?>
<span class="text-gray-400">—</span>
<?php endif ?>
</td>
<td class="whitespace-nowrap px-5 py-3">
<a href="<?= site_url('admin/presensi/detail/' . (int) ($pr['id_presensi'] ?? 0)) ?>" class="inline-flex rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-xs font-semibold text-gray-800 shadow-sm hover:bg-gray-50">Detail</a>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
<?php endif ?>
<div class="flex flex-col gap-3 border-t border-gray-200 bg-gray-50/80 px-5 py-3 text-xs text-gray-600 sm:flex-row sm:items-center sm:justify-between">
<span>Total baris: <strong class="text-gray-900"><?= number_format($total, 0, ',', '.') ?></strong></span>
<div class="flex flex-wrap gap-2">
<?php if ($curPage > 1) : ?>
<a class="inline-flex rounded-lg border border-gray-200 bg-white px-3 py-1.5 font-medium text-gray-800 shadow-sm hover:bg-gray-50" href="<?= site_url('admin/presensi?' . http_build_query(array_filter(['tanggal_dari' => $dari, 'tanggal_sampai' => $sampai, 'q' => $q ?? '', 'page' => $curPage - 1]))) ?>">Sebelumnya</a>
<?php endif ?>
<?php if ($curPage < $totalPage) : ?>
<a class="inline-flex rounded-lg border border-gray-200 bg-white px-3 py-1.5 font-medium text-gray-800 shadow-sm hover:bg-gray-50" href="<?= site_url('admin/presensi?' . http_build_query(array_filter(['tanggal_dari' => $dari, 'tanggal_sampai' => $sampai, 'q' => $q ?? '', 'page' => $curPage + 1]))) ?>">Berikutnya</a>
<?php endif ?>
</div>
</div>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,79 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Jadwal presensi<?= $this->endSection() ?>
<?php $rows = is_array($payload) ? ($payload['rows'] ?? []) : []; ?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Management jadwal</h1>
<p class="mt-1 text-sm text-gray-500">Atur jam masuk dan pulang standar per hari dalam minggu.</p>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<form method="post" action="<?= site_url('admin/presensi/jadwal/save') ?>" class="space-y-4">
<?= csrf_field() ?>
<input type="hidden" name="id_jadwal" id="f_id" value="">
<div class="grid gap-4 sm:grid-cols-3">
<div class="sm:col-span-2">
<label class="mb-1 block text-xs font-medium text-gray-600">Nama jadwal *</label>
<input name="nama_jadwal" id="f_nama" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Toleransi terlambat (mnt) *</label>
<input type="number" name="toleransi_terlambat" id="f_tol_t" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Toleransi pulang cepat *</label>
<input type="number" name="toleransi_pulang_cepat" id="f_tol_p" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
</div>
<?php
$days = [1 => 'Senin', 2 => 'Selasa', 3 => 'Rabu', 4 => 'Kamis', 5 => 'Jumat', 6 => 'Sabtu', 7 => 'Minggu'];
?>
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<?php foreach ($days as $d => $label) : ?>
<div class="rounded-xl border border-gray-100 bg-gray-50/80 p-3">
<p class="mb-2 text-xs font-semibold text-gray-700"><?= esc($label) ?></p>
<div class="flex gap-2">
<div class="flex-1">
<label class="text-[10px] text-gray-500">Masuk</label>
<input name="<?= $d ?>_in" id="f_<?= $d ?>_in" class="w-full rounded border border-gray-300 px-2 py-1 text-xs" placeholder="08:00">
</div>
<div class="flex-1">
<label class="text-[10px] text-gray-500">Pulang</label>
<input name="<?= $d ?>_out" id="f_<?= $d ?>_out" class="w-full rounded border border-gray-300 px-2 py-1 text-xs" placeholder="17:00">
</div>
</div>
</div>
<?php endforeach ?>
</div>
<button type="submit" class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Simpan jadwal</button>
</form>
</section>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="border-y border-gray-200 bg-gray-50 text-left text-xs font-semibold uppercase text-gray-500">
<tr><th class="px-5 py-3">Nama</th><th class="px-5 py-3">SenMin (masuk)</th><th class="px-5 py-3"></th></tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php foreach ($rows as $x) : ?>
<?php if (! is_array($x)) {
continue;
} ?>
<tr class="hover:bg-gray-50">
<td class="px-5 py-3 font-medium text-gray-900"><?= esc((string) ($x['nama_jadwal'] ?? '')) ?></td>
<td class="px-5 py-3 font-mono text-xs text-gray-600"><?= esc((string) ($x['1_in'] ?? '')) ?> … <?= esc((string) ($x['5_out'] ?? '')) ?></td>
<td class="whitespace-nowrap px-5 py-3">
<button type="button" class="rounded-lg border border-gray-200 px-2 py-1 text-xs font-semibold" data-row="<?= esc(json_encode($x, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT), 'attr') ?>" onclick="(function(o){document.getElementById('f_id').value=o.id_jadwal||'';document.getElementById('f_nama').value=o.nama_jadwal||'';document.getElementById('f_tol_t').value=o.toleransi_terlambat||0;document.getElementById('f_tol_p').value=o.toleransi_pulang_cepat||0;for(var d=1;d<=7;d++){document.getElementById('f_'+d+'_in').value=o[d+'_in']||'';document.getElementById('f_'+d+'_out').value=o[d+'_out']||'';}})(JSON.parse(this.dataset.row))">Edit</button>
<form method="post" action="<?= site_url('admin/presensi/jadwal/delete/' . (int) ($x['id_jadwal'] ?? 0)) ?>" class="inline" onsubmit="return confirm('Hapus jadwal? Pastikan tidak dipakai pegawai.');"><?= csrf_field() ?><button type="submit" class="ml-1 rounded-lg bg-red-50 px-2 py-1 text-xs font-semibold text-red-700">Hapus</button></form>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,68 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Tugas luar / lapangan<?= $this->endSection() ?>
<?php $rows = is_array($payload) ? ($payload['rows'] ?? []) : []; ?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Pegawai dilapangan</h1>
<p class="mt-1 text-sm text-gray-500">Catat pegawai yang bertugas di luar kantor.</p>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<h2 class="text-base font-semibold text-gray-900">Tambah / ubah</h2>
<form method="post" action="<?= site_url('admin/presensi/lapangan/save') ?>" class="mt-4 grid gap-4 sm:grid-cols-2">
<?= csrf_field() ?>
<input type="hidden" name="id_dilapangan" id="f_id" value="">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Pegawai *</label>
<select name="pegawai" id="f_pegawai" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
<option value="">— pilih —</option>
<?php foreach ($pegawai as $pg) : ?>
<?php if (! is_array($pg)) {
continue;
} ?>
<option value="<?= (int) ($pg['id_pegawai'] ?? 0) ?>"><?= esc((string) ($pg['nama_lengkap'] ?? '')) ?> (<?= esc((string) ($pg['nip'] ?? '')) ?>)</option>
<?php endforeach ?>
</select>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Alasan *</label>
<input name="alasan" id="f_alasan" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div class="sm:col-span-2 flex gap-2">
<button type="submit" class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Simpan</button>
<button type="button" onclick="document.getElementById('f_id').value='';document.getElementById('f_pegawai').value='';document.getElementById('f_alasan').value='';" class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50">Bersihkan</button>
</div>
</form>
</section>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="border-y border-gray-200 bg-gray-50 text-left text-xs font-semibold uppercase text-gray-500">
<tr><th class="px-5 py-3">Pegawai</th><th class="px-5 py-3">Alasan</th><th class="px-5 py-3"></th></tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php foreach ($rows as $x) : ?>
<?php if (! is_array($x)) {
continue;
} ?>
<tr class="hover:bg-gray-50">
<td class="px-5 py-3">
<div class="font-medium text-gray-900"><?= esc((string) ($x['nama_lengkap'] ?? '')) ?></div>
<div class="text-xs text-gray-500"><?= esc((string) ($x['nip'] ?? '')) ?></div>
</td>
<td class="px-5 py-3"><?= esc((string) ($x['alasan'] ?? '')) ?></td>
<td class="whitespace-nowrap px-5 py-3">
<button type="button" class="rounded-lg border border-gray-200 px-2 py-1 text-xs font-semibold" data-row="<?= esc(json_encode($x, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT), 'attr') ?>" onclick="var o=JSON.parse(this.dataset.row);document.getElementById('f_id').value=o.id_dilapangan||'';document.getElementById('f_pegawai').value=String(o.pegawai||'');document.getElementById('f_alasan').value=o.alasan||'';">Edit</button>
<form method="post" action="<?= site_url('admin/presensi/lapangan/delete/' . (int) ($x['id_dilapangan'] ?? 0)) ?>" class="inline" onsubmit="return confirm('Hapus?');"><?= csrf_field() ?><button type="submit" class="ml-1 rounded-lg bg-red-50 px-2 py-1 text-xs font-semibold text-red-700">Hapus</button></form>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,77 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Jadwal lembur<?= $this->endSection() ?>
<?php $rows = is_array($payload) ? ($payload['rows'] ?? []) : []; ?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Jadwal lembur</h1>
<p class="mt-1 text-sm text-gray-500">Kelola pencatatan lembur pegawai.</p>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<form method="post" action="<?= site_url('admin/presensi/lembur/save') ?>" class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<?= csrf_field() ?>
<input type="hidden" name="id_lembur" id="f_id" value="">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Tanggal *</label>
<input type="date" name="tanggal_lembur" id="f_tgl" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Pegawai *</label>
<select name="pegawai" id="f_pegawai" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
<option value="">—</option>
<?php foreach ($pegawai as $pg) : ?>
<?php if (! is_array($pg)) {
continue;
} ?>
<option value="<?= (int) ($pg['id_pegawai'] ?? 0) ?>"><?= esc((string) ($pg['nama_lengkap'] ?? '')) ?></option>
<?php endforeach ?>
</select>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Masuk (HH:MM) *</label>
<input name="masuk" id="f_masuk" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm" placeholder="18:00">
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Beres (HH:MM) *</label>
<input name="beres" id="f_beres" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm" placeholder="22:00">
</div>
<div class="sm:col-span-2">
<label class="mb-1 block text-xs font-medium text-gray-600">Keterangan *</label>
<input name="keterangan" id="f_ket" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div class="flex items-end gap-2">
<button type="submit" class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Simpan</button>
</div>
</form>
</section>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="border-y border-gray-200 bg-gray-50 text-left text-xs font-semibold uppercase text-gray-500">
<tr><th class="px-5 py-3">Tanggal</th><th class="px-5 py-3">Pegawai</th><th class="px-5 py-3">Jam</th><th class="px-5 py-3">Ket</th><th class="px-5 py-3"></th></tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php foreach ($rows as $x) : ?>
<?php if (! is_array($x)) {
continue;
} ?>
<tr class="hover:bg-gray-50">
<td class="whitespace-nowrap px-5 py-3"><?= esc((string) ($x['tanggal_lembur'] ?? '')) ?></td>
<td class="px-5 py-3"><?= esc((string) ($x['nama_lengkap'] ?? '')) ?></td>
<td class="px-5 py-3 font-mono text-xs"><?= esc((string) ($x['masuk'] ?? '')) ?><?= esc((string) ($x['beres'] ?? '')) ?></td>
<td class="px-5 py-3"><?= esc((string) ($x['keterangan'] ?? '')) ?></td>
<td class="whitespace-nowrap px-5 py-3">
<button type="button" class="rounded-lg border border-gray-200 px-2 py-1 text-xs font-semibold" data-row="<?= esc(json_encode($x, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT), 'attr') ?>" onclick="var o=JSON.parse(this.dataset.row);document.getElementById('f_id').value=o.id_lembur||'';document.getElementById('f_tgl').value=o.tanggal_lembur||'';document.getElementById('f_pegawai').value=String(o.pegawai||'');document.getElementById('f_masuk').value=o.masuk||'';document.getElementById('f_beres').value=o.beres||'';document.getElementById('f_ket').value=o.keterangan||'';">Edit</button>
<form method="post" action="<?= site_url('admin/presensi/lembur/delete/' . (int) ($x['id_lembur'] ?? 0)) ?>" class="inline" onsubmit="return confirm('Hapus?');"><?= csrf_field() ?><button type="submit" class="ml-1 rounded-lg bg-red-50 px-2 py-1 text-xs font-semibold text-red-700">Hapus</button></form>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,53 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Hari libur<?= $this->endSection() ?>
<?php $rows = is_array($payload) ? ($payload['rows'] ?? []) : []; ?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Hari libur perusahaan</h1>
<p class="mt-1 text-sm text-gray-500">Atur hari libur perusahaan yang mempengaruhi presensi.</p>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
<form method="post" action="<?= site_url('admin/presensi/libur/save') ?>" class="grid gap-4 sm:grid-cols-2">
<?= csrf_field() ?>
<input type="hidden" name="id_libur" id="f_id" value="">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Tanggal libur *</label>
<input type="date" name="tanggal_libur" id="f_tgl" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<div class="sm:col-span-2">
<label class="mb-1 block text-xs font-medium text-gray-600">Keterangan *</label>
<input name="keterangan_libur" id="f_ket" required class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm">
</div>
<button type="submit" class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Simpan</button>
</form>
</section>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="border-y border-gray-200 bg-gray-50 text-left text-xs font-semibold uppercase text-gray-500">
<tr><th class="px-5 py-3">Tanggal</th><th class="px-5 py-3">Keterangan</th><th class="px-5 py-3"></th></tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php foreach ($rows as $x) : ?>
<?php if (! is_array($x)) {
continue;
} ?>
<tr class="hover:bg-gray-50">
<td class="whitespace-nowrap px-5 py-3"><?= esc((string) ($x['tanggal_libur'] ?? '')) ?></td>
<td class="px-5 py-3"><?= esc((string) ($x['keterangan_libur'] ?? '')) ?></td>
<td class="whitespace-nowrap px-5 py-3">
<button type="button" class="rounded-lg border border-gray-200 px-2 py-1 text-xs font-semibold" data-row="<?= esc(json_encode($x, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT), 'attr') ?>" onclick="var o=JSON.parse(this.dataset.row);document.getElementById('f_id').value=o.id_libur||'';document.getElementById('f_tgl').value=o.tanggal_libur||'';document.getElementById('f_ket').value=o.keterangan_libur||'';">Edit</button>
<form method="post" action="<?= site_url('admin/presensi/libur/delete/' . (int) ($x['id_libur'] ?? 0)) ?>" class="inline" onsubmit="return confirm('Hapus?');"><?= csrf_field() ?><button type="submit" class="ml-1 rounded-lg bg-red-50 px-2 py-1 text-xs font-semibold text-red-700">Hapus</button></form>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,87 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('title') ?>Backup database<?= $this->endSection() ?>
<?php
$files = is_array($payload) && isset($payload['files']) && is_array($payload['files']) ? $payload['files'] : [];
$dir = is_array($payload) ? (string) ($payload['directory'] ?? '') : '';
?>
<?= $this->section('content') ?>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Backup database</h1>
<p class="mt-1 text-sm text-gray-500">File cadangan basis data disimpan di folder aplikasi (writable). Unduh atau hapus dari daftar di bawah.</p>
<?php if ($dir !== '') : ?>
<p class="mt-1 font-mono text-xs text-gray-400 break-all"><?= esc($dir) ?></p>
<?php endif ?>
</div>
<?= view('layouts/partials/admin_alert', ['errors' => $errors ?? []]) ?>
<section class="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<h2 class="text-base font-semibold text-gray-900">Buat backup baru</h2>
<p class="mt-1 text-xs text-gray-500">Cadangan basis data dalam format SQL. Proses bisa memakan waktu beberapa detik.</p>
<form method="post" action="<?= site_url('admin/util/backup/run') ?>" class="mt-4 flex flex-wrap items-center gap-4">
<?= csrf_field() ?>
<label class="inline-flex items-center gap-2 text-sm text-gray-700">
<input type="checkbox" name="save_latest" value="1" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
Juga tulis salinan <code class="rounded bg-gray-100 px-1 text-xs">latest.sql</code>
</label>
<button type="submit" class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700" onclick="return confirm('Jalankan backup sekarang?');">
<i class="fa-solid fa-database mr-2"></i> Jalankan backup
</button>
</form>
</section>
<section class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="border-b border-gray-200 px-5 py-4">
<h2 class="text-base font-semibold text-gray-900">File backup</h2>
<p class="text-xs text-gray-500">Unduh untuk arsip; hapus jika sudah tidak diperlukan.</p>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="border-y border-gray-200 bg-gray-50 text-left text-xs font-semibold uppercase tracking-wide text-gray-500">
<tr>
<th class="px-4 py-3">Nama file</th>
<th class="px-4 py-3">Ukuran</th>
<th class="px-4 py-3">Diubah</th>
<th class="px-4 py-3 w-48">Tindakan</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php if ($files === []) : ?>
<tr>
<td colspan="4" class="px-4 py-10 text-center text-gray-500">Belum ada file backup.</td>
</tr>
<?php else : ?>
<?php foreach ($files as $f) : ?>
<?php
$name = (string) ($f['name'] ?? '');
if ($name === '') {
continue;
}
$enc = rawurlencode($name);
?>
<tr class="hover:bg-gray-50/80">
<td class="px-4 py-2 font-mono text-xs text-gray-900"><?= esc($name) ?></td>
<td class="whitespace-nowrap px-4 py-2 text-gray-600"><?= number_format((int) ($f['size'] ?? 0), 0, ',', '.') ?> B</td>
<td class="whitespace-nowrap px-4 py-2 text-gray-600"><?= esc((string) ($f['mtime'] ?? '')) ?></td>
<td class="px-4 py-2">
<div class="flex flex-wrap gap-2">
<a href="<?= site_url('admin/util/backup/download/' . $enc) ?>" class="inline-flex rounded-lg border border-gray-200 bg-white px-2 py-1 text-xs font-semibold text-gray-800 shadow-sm hover:bg-gray-50">Unduh</a>
<form method="post" action="<?= site_url('admin/util/backup/delete/' . $enc) ?>" class="inline" onsubmit="return confirm('Hapus file backup ini?');">
<?= csrf_field() ?>
<button type="submit" class="inline-flex rounded-lg bg-red-50 px-2 py-1 text-xs font-semibold text-red-800 hover:bg-red-100">Hapus</button>
</form>
</div>
</td>
</tr>
<?php endforeach ?>
<?php endif ?>
</tbody>
</table>
</div>
</section>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,7 @@
<?php
use CodeIgniter\CLI\CLI;
CLI::error('ERROR: ' . $code);
CLI::write($message);
CLI::newLine();

View File

@@ -0,0 +1,65 @@
<?php
use CodeIgniter\CLI\CLI;
// The main Exception
CLI::write('[' . $exception::class . ']', 'light_gray', 'red');
CLI::write($message);
CLI::write('at ' . CLI::color(clean_path($exception->getFile()) . ':' . $exception->getLine(), 'green'));
CLI::newLine();
$last = $exception;
while ($prevException = $last->getPrevious()) {
$last = $prevException;
CLI::write(' Caused by:');
CLI::write(' [' . $prevException::class . ']', 'red');
CLI::write(' ' . $prevException->getMessage());
CLI::write(' at ' . CLI::color(clean_path($prevException->getFile()) . ':' . $prevException->getLine(), 'green'));
CLI::newLine();
}
// The backtrace
if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) {
$backtraces = $last->getTrace();
if ($backtraces) {
CLI::write('Backtrace:', 'green');
}
foreach ($backtraces as $i => $error) {
$padFile = ' '; // 4 spaces
$padClass = ' '; // 7 spaces
$c = str_pad($i + 1, 3, ' ', STR_PAD_LEFT);
if (isset($error['file'])) {
$filepath = clean_path($error['file']) . ':' . $error['line'];
CLI::write($c . $padFile . CLI::color($filepath, 'yellow'));
} else {
CLI::write($c . $padFile . CLI::color('[internal function]', 'yellow'));
}
$function = '';
if (isset($error['class'])) {
$type = ($error['type'] === '->') ? '()' . $error['type'] : $error['type'];
$function .= $padClass . $error['class'] . $type . $error['function'];
} elseif (! isset($error['class']) && isset($error['function'])) {
$function .= $padClass . $error['function'];
}
$args = implode(', ', array_map(static fn ($value): string => match (true) {
is_object($value) => 'Object(' . $value::class . ')',
is_array($value) => $value !== [] ? '[...]' : '[]',
$value === null => 'null', // return the lowercased version
default => var_export($value, true),
}, array_values($error['args'] ?? [])));
$function .= '(' . $args . ')';
CLI::write($function);
CLI::newLine();
}
}

View File

@@ -0,0 +1,5 @@
<?php
// On the CLI, we still want errors in productions
// so just use the exception template.
include __DIR__ . '/error_exception.php';

View File

@@ -0,0 +1,194 @@
:root {
--main-bg-color: #fff;
--main-text-color: #555;
--dark-text-color: #222;
--light-text-color: #c7c7c7;
--brand-primary-color: #DC4814;
--light-bg-color: #ededee;
--dark-bg-color: #404040;
}
body {
height: 100%;
background: var(--main-bg-color);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
color: var(--main-text-color);
font-weight: 300;
margin: 0;
padding: 0;
}
h1 {
font-weight: lighter;
font-size: 3rem;
color: var(--dark-text-color);
margin: 0;
}
h1.headline {
margin-top: 20%;
font-size: 5rem;
}
.text-center {
text-align: center;
}
p.lead {
font-size: 1.6rem;
}
.container {
max-width: 75rem;
margin: 0 auto;
padding: 1rem;
}
.header {
background: var(--light-bg-color);
color: var(--dark-text-color);
margin-top: 2.17rem;
}
.header .container {
padding: 1rem;
}
.header h1 {
font-size: 2.5rem;
font-weight: 500;
}
.header p {
font-size: 1.2rem;
margin: 0;
line-height: 2.5;
}
.header a {
color: var(--brand-primary-color);
margin-left: 2rem;
display: none;
text-decoration: none;
}
.header:hover a {
display: inline;
}
.environment {
background: var(--brand-primary-color);
color: var(--main-bg-color);
text-align: center;
padding: calc(4px + 0.2083vw);
width: 100%;
top: 0;
position: fixed;
}
.source {
background: #343434;
color: var(--light-text-color);
padding: 0.5em 1em;
border-radius: 5px;
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
font-size: 0.85rem;
margin: 0;
overflow-x: scroll;
}
.source span.line {
line-height: 1.4;
}
.source span.line .number {
color: #666;
}
.source .line .highlight {
display: block;
background: var(--dark-text-color);
color: var(--light-text-color);
}
.source span.highlight .number {
color: #fff;
}
.tabs {
list-style: none;
list-style-position: inside;
margin: 0;
padding: 0;
margin-bottom: -1px;
}
.tabs li {
display: inline;
}
.tabs a:link,
.tabs a:visited {
padding: 0 1rem;
line-height: 2.7;
text-decoration: none;
color: var(--dark-text-color);
background: var(--light-bg-color);
border: 1px solid rgba(0,0,0,0.15);
border-bottom: 0;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
display: inline-block;
}
.tabs a:hover {
background: var(--light-bg-color);
border-color: rgba(0,0,0,0.15);
}
.tabs a.active {
background: var(--main-bg-color);
color: var(--main-text-color);
}
.tab-content {
background: var(--main-bg-color);
border: 1px solid rgba(0,0,0,0.15);
}
.content {
padding: 1rem;
}
.hide {
display: none;
}
.alert {
margin-top: 2rem;
display: block;
text-align: center;
line-height: 3.0;
background: #d9edf7;
border: 1px solid #bcdff1;
border-radius: 5px;
color: #31708f;
}
table {
width: 100%;
overflow: hidden;
}
th {
text-align: left;
border-bottom: 1px solid #e7e7e7;
padding-bottom: 0.5rem;
}
td {
padding: 0.2rem 0.5rem 0.2rem 0;
}
tr:hover td {
background: #f1f1f1;
}
td pre {
white-space: pre-wrap;
}
.trace a {
color: inherit;
}
.trace table {
width: auto;
}
.trace tr td:first-child {
min-width: 5em;
font-weight: bold;
}
.trace td {
background: var(--light-bg-color);
padding: 0 1rem;
}
.trace td pre {
margin: 0;
}
.args {
display: none;
}

View File

@@ -0,0 +1,116 @@
var tabLinks = new Array();
var contentDivs = new Array();
function init()
{
// Grab the tab links and content divs from the page
var tabListItems = document.getElementById('tabs').childNodes;
console.log(tabListItems);
for (var i = 0; i < tabListItems.length; i ++)
{
if (tabListItems[i].nodeName == "LI")
{
var tabLink = getFirstChildWithTagName(tabListItems[i], 'A');
var id = getHash(tabLink.getAttribute('href'));
tabLinks[id] = tabLink;
contentDivs[id] = document.getElementById(id);
}
}
// Assign onclick events to the tab links, and
// highlight the first tab
var i = 0;
for (var id in tabLinks)
{
tabLinks[id].onclick = showTab;
tabLinks[id].onfocus = function () {
this.blur()
};
if (i == 0)
{
tabLinks[id].className = 'active';
}
i ++;
}
// Hide all content divs except the first
var i = 0;
for (var id in contentDivs)
{
if (i != 0)
{
console.log(contentDivs[id]);
contentDivs[id].className = 'content hide';
}
i ++;
}
}
function showTab()
{
var selectedId = getHash(this.getAttribute('href'));
// Highlight the selected tab, and dim all others.
// Also show the selected content div, and hide all others.
for (var id in contentDivs)
{
if (id == selectedId)
{
tabLinks[id].className = 'active';
contentDivs[id].className = 'content';
}
else
{
tabLinks[id].className = '';
contentDivs[id].className = 'content hide';
}
}
// Stop the browser following the link
return false;
}
function getFirstChildWithTagName(element, tagName)
{
for (var i = 0; i < element.childNodes.length; i ++)
{
if (element.childNodes[i].nodeName == tagName)
{
return element.childNodes[i];
}
}
}
function getHash(url)
{
var hashPos = url.lastIndexOf('#');
return url.substring(hashPos + 1);
}
function toggle(elem)
{
elem = document.getElementById(elem);
if (elem.style && elem.style['display'])
{
// Only works with the "style" attr
var disp = elem.style['display'];
}
else if (elem.currentStyle)
{
// For MSIE, naturally
var disp = elem.currentStyle['display'];
}
else if (window.getComputedStyle)
{
// For most other browsers
var disp = document.defaultView.getComputedStyle(elem, null).getPropertyValue('display');
}
// Toggle the state of the "display" style
elem.style.display = disp == 'block' ? 'none' : 'block';
return false;
}

View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><?= lang('Errors.badRequest') ?></title>
<style>
div.logo {
height: 200px;
width: 155px;
display: inline-block;
opacity: 0.08;
position: absolute;
top: 2rem;
left: 50%;
margin-left: -73px;
}
body {
height: 100%;
background: #fafafa;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #777;
font-weight: 300;
}
h1 {
font-weight: lighter;
letter-spacing: normal;
font-size: 3rem;
margin-top: 0;
margin-bottom: 0;
color: #222;
}
.wrap {
max-width: 1024px;
margin: 5rem auto;
padding: 2rem;
background: #fff;
text-align: center;
border: 1px solid #efefef;
border-radius: 0.5rem;
position: relative;
}
pre {
white-space: normal;
margin-top: 1.5rem;
}
code {
background: #fafafa;
border: 1px solid #efefef;
padding: 0.5rem 1rem;
border-radius: 5px;
display: block;
}
p {
margin-top: 1.5rem;
}
.footer {
margin-top: 2rem;
border-top: 1px solid #efefef;
padding: 1em 2em 0 2em;
font-size: 85%;
color: #999;
}
a:active,
a:link,
a:visited {
color: #dd4814;
}
</style>
</head>
<body>
<div class="wrap">
<h1>400</h1>
<p>
<?php if (ENVIRONMENT !== 'production') : ?>
<?= nl2br(esc($message)) ?>
<?php else : ?>
<?= lang('Errors.sorryBadRequest') ?>
<?php endif; ?>
</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><?= lang('Errors.pageNotFound') ?></title>
<style>
div.logo {
height: 200px;
width: 155px;
display: inline-block;
opacity: 0.08;
position: absolute;
top: 2rem;
left: 50%;
margin-left: -73px;
}
body {
height: 100%;
background: #fafafa;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #777;
font-weight: 300;
}
h1 {
font-weight: lighter;
letter-spacing: normal;
font-size: 3rem;
margin-top: 0;
margin-bottom: 0;
color: #222;
}
.wrap {
max-width: 1024px;
margin: 5rem auto;
padding: 2rem;
background: #fff;
text-align: center;
border: 1px solid #efefef;
border-radius: 0.5rem;
position: relative;
}
pre {
white-space: normal;
margin-top: 1.5rem;
}
code {
background: #fafafa;
border: 1px solid #efefef;
padding: 0.5rem 1rem;
border-radius: 5px;
display: block;
}
p {
margin-top: 1.5rem;
}
.footer {
margin-top: 2rem;
border-top: 1px solid #efefef;
padding: 1em 2em 0 2em;
font-size: 85%;
color: #999;
}
a:active,
a:link,
a:visited {
color: #dd4814;
}
</style>
</head>
<body>
<div class="wrap">
<h1>404</h1>
<p>
<?php if (ENVIRONMENT !== 'production') : ?>
<?= nl2br(esc($message)) ?>
<?php else : ?>
<?= lang('Errors.sorryCannotFind') ?>
<?php endif; ?>
</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,429 @@
<?php
use CodeIgniter\HTTP\Header;
use CodeIgniter\CodeIgniter;
$errorId = uniqid('error', true);
?>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<title><?= esc($title) ?></title>
<style>
<?= preg_replace('#[\r\n\t ]+#', ' ', file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.css')) ?>
</style>
<script>
<?= file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.js') ?>
</script>
</head>
<body onload="init()">
<!-- Header -->
<div class="header">
<div class="environment">
Displayed at <?= esc(date('H:i:s')) ?> &mdash;
PHP: <?= esc(PHP_VERSION) ?> &mdash;
CodeIgniter: <?= esc(CodeIgniter::CI_VERSION) ?> --
Environment: <?= ENVIRONMENT ?>
</div>
<div class="container">
<h1><?= esc($title), esc($exception->getCode() ? ' #' . $exception->getCode() : '') ?></h1>
<p>
<?= nl2br(esc($exception->getMessage())) ?>
<a href="https://www.duckduckgo.com/?q=<?= urlencode($title . ' ' . preg_replace('#\'.*\'|".*"#Us', '', $exception->getMessage())) ?>"
rel="noreferrer" target="_blank">search &rarr;</a>
</p>
</div>
</div>
<!-- Source -->
<div class="container">
<p><b><?= esc(clean_path($file)) ?></b> at line <b><?= esc($line) ?></b></p>
<?php if (is_file($file)) : ?>
<div class="source">
<?= static::highlightFile($file, $line, 15); ?>
</div>
<?php endif; ?>
</div>
<div class="container">
<?php
$last = $exception;
while ($prevException = $last->getPrevious()) {
$last = $prevException;
?>
<pre>
Caused by:
<?= esc($prevException::class), esc($prevException->getCode() ? ' #' . $prevException->getCode() : '') ?>
<?= nl2br(esc($prevException->getMessage())) ?>
<a href="https://www.duckduckgo.com/?q=<?= urlencode($prevException::class . ' ' . preg_replace('#\'.*\'|".*"#Us', '', $prevException->getMessage())) ?>"
rel="noreferrer" target="_blank">search &rarr;</a>
<?= esc(clean_path($prevException->getFile()) . ':' . $prevException->getLine()) ?>
</pre>
<?php
}
?>
</div>
<?php if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) : ?>
<div class="container">
<ul class="tabs" id="tabs">
<li><a href="#backtrace">Backtrace</a></li>
<li><a href="#server">Server</a></li>
<li><a href="#request">Request</a></li>
<li><a href="#response">Response</a></li>
<li><a href="#files">Files</a></li>
<li><a href="#memory">Memory</a></li>
</ul>
<div class="tab-content">
<!-- Backtrace -->
<div class="content" id="backtrace">
<ol class="trace">
<?php foreach ($trace as $index => $row) : ?>
<li>
<p>
<!-- Trace info -->
<?php if (isset($row['file']) && is_file($row['file'])) : ?>
<?php
if (isset($row['function']) && in_array($row['function'], ['include', 'include_once', 'require', 'require_once'], true)) {
echo esc($row['function'] . ' ' . clean_path($row['file']));
} else {
echo esc(clean_path($row['file']) . ' : ' . $row['line']);
}
?>
<?php else: ?>
{PHP internal code}
<?php endif; ?>
<!-- Class/Method -->
<?php if (isset($row['class'])) : ?>
&nbsp;&nbsp;&mdash;&nbsp;&nbsp;<?= esc($row['class'] . $row['type'] . $row['function']) ?>
<?php if (! empty($row['args'])) : ?>
<?php $argsId = $errorId . 'args' . $index ?>
( <a href="#" onclick="return toggle('<?= esc($argsId, 'attr') ?>');">arguments</a> )
<div class="args" id="<?= esc($argsId, 'attr') ?>">
<table cellspacing="0">
<?php
$params = null;
// Reflection by name is not available for closure function
if (! str_ends_with($row['function'], '}')) {
$mirror = isset($row['class']) ? new ReflectionMethod($row['class'], $row['function']) : new ReflectionFunction($row['function']);
$params = $mirror->getParameters();
}
foreach ($row['args'] as $key => $value) : ?>
<tr>
<td><code><?= esc(isset($params[$key]) ? '$' . $params[$key]->name : "#{$key}") ?></code></td>
<td><pre><?= esc(print_r($value, true)) ?></pre></td>
</tr>
<?php endforeach ?>
</table>
</div>
<?php else : ?>
()
<?php endif; ?>
<?php endif; ?>
<?php if (! isset($row['class']) && isset($row['function'])) : ?>
&nbsp;&nbsp;&mdash;&nbsp;&nbsp; <?= esc($row['function']) ?>()
<?php endif; ?>
</p>
<!-- Source? -->
<?php if (isset($row['file']) && is_file($row['file']) && isset($row['class'])) : ?>
<div class="source">
<?= static::highlightFile($row['file'], $row['line']) ?>
</div>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ol>
</div>
<!-- Server -->
<div class="content" id="server">
<?php foreach (['_SERVER', '_SESSION'] as $var) : ?>
<?php
if (empty($GLOBALS[$var]) || ! is_array($GLOBALS[$var])) {
continue;
} ?>
<h3>$<?= esc($var) ?></h3>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($GLOBALS[$var] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach ?>
<!-- Constants -->
<?php $constants = get_defined_constants(true); ?>
<?php if (! empty($constants['user'])) : ?>
<h3>Constants</h3>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($constants['user'] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Request -->
<div class="content" id="request">
<?php $request = service('request'); ?>
<table>
<tbody>
<tr>
<td style="width: 10em">Path</td>
<td><?= esc($request->getUri()) ?></td>
</tr>
<tr>
<td>HTTP Method</td>
<td><?= esc($request->getMethod()) ?></td>
</tr>
<tr>
<td>IP Address</td>
<td><?= esc($request->getIPAddress()) ?></td>
</tr>
<tr>
<td style="width: 10em">Is AJAX Request?</td>
<td><?= $request->isAJAX() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>Is CLI Request?</td>
<td><?= $request->isCLI() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>Is Secure Request?</td>
<td><?= $request->isSecure() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>User Agent</td>
<td><?= esc($request->getUserAgent()->getAgentString()) ?></td>
</tr>
</tbody>
</table>
<?php $empty = true; ?>
<?php foreach (['_GET', '_POST', '_COOKIE'] as $var) : ?>
<?php
if (empty($GLOBALS[$var]) || ! is_array($GLOBALS[$var])) {
continue;
} ?>
<?php $empty = false; ?>
<h3>$<?= esc($var) ?></h3>
<table style="width: 100%">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($GLOBALS[$var] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach ?>
<?php if ($empty) : ?>
<div class="alert">
No $_GET, $_POST, or $_COOKIE Information to show.
</div>
<?php endif; ?>
<?php $headers = $request->headers(); ?>
<?php if (! empty($headers)) : ?>
<h3>Headers</h3>
<table>
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($headers as $name => $value) : ?>
<tr>
<td><?= esc($name, 'html') ?></td>
<td>
<?php
if ($value instanceof Header) {
echo esc($value->getValueLine(), 'html');
} else {
foreach ($value as $i => $header) {
echo ' ('. $i+1 . ') ' . esc($header->getValueLine(), 'html');
}
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Response -->
<?php
$response = service('response');
$response->setStatusCode(http_response_code());
?>
<div class="content" id="response">
<table>
<tr>
<td style="width: 15em">Response Status</td>
<td><?= esc($response->getStatusCode() . ' - ' . $response->getReasonPhrase()) ?></td>
</tr>
</table>
<?php $headers = $response->headers(); ?>
<?php if (! empty($headers)) : ?>
<h3>Headers</h3>
<table>
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($headers as $name => $value) : ?>
<tr>
<td><?= esc($name, 'html') ?></td>
<td>
<?php
if ($value instanceof Header) {
echo esc($response->getHeaderLine($name), 'html');
} else {
foreach ($value as $i => $header) {
echo ' ('. $i+1 . ') ' . esc($header->getValueLine(), 'html');
}
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Files -->
<div class="content" id="files">
<?php $files = get_included_files(); ?>
<ol>
<?php foreach ($files as $file) :?>
<li><?= esc(clean_path($file)) ?></li>
<?php endforeach ?>
</ol>
</div>
<!-- Memory -->
<div class="content" id="memory">
<table>
<tbody>
<tr>
<td>Memory Usage</td>
<td><?= esc(static::describeMemory(memory_get_usage(true))) ?></td>
</tr>
<tr>
<td style="width: 12em">Peak Memory Usage:</td>
<td><?= esc(static::describeMemory(memory_get_peak_usage(true))) ?></td>
</tr>
<tr>
<td>Memory Limit:</td>
<td><?= esc(ini_get('memory_limit')) ?></td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /tab-content -->
</div> <!-- /container -->
<?php endif; ?>
</body>
</html>

View File

@@ -0,0 +1,25 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<title><?= lang('Errors.whoops') ?></title>
<style>
<?= preg_replace('#[\r\n\t ]+#', ' ', file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.css')) ?>
</style>
</head>
<body>
<div class="container text-center">
<h1 class="headline"><?= lang('Errors.whoops') ?></h1>
<p class="lead"><?= lang('Errors.weHitASnag') ?></p>
</div>
</body>
</html>

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= esc($this->renderSection('title') ?: 'Login') ?> — BIJ Admin</title>
<link rel="icon" type="image/png" href="<?= base_url('assets/images/bij_logo.png') ?>">
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script>
<link rel="stylesheet" href="<?= base_url('assets/tailadmin/fa-7.1.0-web/css/all.min.css') ?>">
</head>
<body class="min-h-screen bg-gradient-to-br from-gray-100 via-gray-50 to-gray-200 text-gray-900 antialiased">
<div class="flex min-h-screen flex-col items-center justify-center px-4 py-10">
<div class="mb-8 flex flex-col items-center text-center">
<img src="<?= base_url('assets/images/bpr-logo.png') ?>" alt="Logo BPR" class="h-12 w-auto max-h-16 max-w-[220px] object-contain drop-shadow-sm">
<p class="mt-3 text-sm font-medium text-gray-600">Panel administrasi BIJ</p>
</div>
<div class="w-full max-w-md">
<?= $this->renderSection('content') ?>
</div>
<p class="mt-10 text-center text-xs text-gray-500">CodeIgniter 4 · Presensi &amp; kepegawaian</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,3 @@
<footer class="mt-auto border-t border-gray-200 bg-gray-50/80 px-4 py-3 text-center text-[11px] text-gray-500 md:px-6 2xl:px-8">
&copy; <?= date('Y') ?> bank BIJ — PT. Wira Pratama Indonesia.
</footer>

View File

@@ -0,0 +1,31 @@
<header class="sticky top-0 z-30 flex w-full flex-col gap-2 border-b border-gray-200 bg-white/95 px-4 py-3 shadow-sm backdrop-blur-md supports-[backdrop-filter]:bg-white/90 md:h-16 md:flex-row md:items-center md:justify-between md:gap-3 md:px-6 md:py-0 2xl:px-8">
<div class="flex min-w-0 items-center gap-3">
<button type="button" id="btn-mobile-sidebar" class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-gray-200 bg-white text-gray-700 shadow-sm hover:bg-gray-50 lg:hidden" aria-label="Buka menu">
<i class="fa-solid fa-bars text-lg"></i>
</button>
<button type="button" id="btn-desktop-sidebar-toggle" class="hidden h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-gray-200 bg-white text-gray-700 shadow-sm hover:bg-gray-50 lg:inline-flex" aria-label="Ciutkan sidebar">
<i class="fa-solid fa-table-columns text-sm"></i>
</button>
<div class="min-w-0 leading-tight">
<p class="truncate text-sm font-semibold tracking-tight text-gray-900">Panel administrasi</p>
<?php if (session()->get('admin_auth_source') === 'admin_users' && is_array(session()->get('admin_ion_groups'))) : ?>
<p class="truncate text-xs text-gray-500">
<?= esc(session('admin_username') ?? '') ?>
<span class="text-gray-300">·</span>
<?= esc(implode(', ', session('admin_ion_groups'))) ?>
</p>
<?php elseif (session()->get('admin_auth_source') === 'pegawai') : ?>
<p class="truncate text-xs text-gray-500"><?= esc(session('admin_username') ?? '') ?> (pegawai)</p>
<?php endif ?>
</div>
</div>
<div class="flex flex-wrap items-center justify-end gap-2 md:max-w-[50%]">
<?php if (session()->getFlashdata('message')) : ?>
<span class="inline-flex max-w-full items-center rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-800"><?= esc(session()->getFlashdata('message')) ?></span>
<?php endif ?>
<?php if (session()->getFlashdata('error')) : ?>
<span class="inline-flex max-w-full items-center rounded-full border border-red-200 bg-red-50 px-3 py-1.5 text-xs font-medium text-red-800"><?= esc(session()->getFlashdata('error')) ?></span>
<?php endif ?>
<span class="hidden shrink-0 text-[11px] font-medium uppercase tracking-wide text-gray-400 sm:inline">Presensi Admin</span>
</div>
</header>

128
app/Views/layouts/main.php Normal file
View File

@@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= esc($this->renderSection('title') ?: 'Admin') ?> — BIJ</title>
<link rel="icon" type="image/png" href="<?= base_url('assets/images/bij_logo.png') ?>">
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script>
<link rel="stylesheet" href="<?= base_url('assets/tailadmin/fa-7.1.0-web/css/all.min.css') ?>">
<style>
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #d1d5db transparent;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #d1d5db;
border-radius: 9999px;
}
/* Shell: mobile drawer + desktop sidebar width (TailAdmin-style) */
#admin-sidebar {
transition: transform 0.2s ease, width 0.2s ease;
}
@media (max-width: 1023px) {
#admin-sidebar {
transform: translateX(-100%);
}
#admin-shell.mobile-sidebar-open #admin-sidebar {
transform: translateX(0);
}
}
@media (min-width: 1024px) {
#admin-sidebar {
transform: none !important;
}
#sidebar-overlay {
display: none !important;
}
}
@media (min-width: 1024px) {
#admin-shell.sidebar-narrow #admin-sidebar {
width: 90px !important;
min-width: 90px;
}
#admin-shell.sidebar-narrow #admin-sidebar .sidebar-text,
#admin-shell.sidebar-narrow #admin-sidebar .nav-section-label {
display: none !important;
}
#admin-shell.sidebar-narrow #admin-sidebar details > summary .fa-chevron-down {
display: none;
}
#admin-shell.sidebar-narrow #admin-sidebar .sidebar-brand {
justify-content: center;
}
#admin-shell.sidebar-narrow #admin-sidebar .sidebar-logo-expanded {
display: none !important;
}
#admin-shell.sidebar-narrow #admin-sidebar .sidebar-logo-collapsed {
display: block !important;
background: none !important;
background-color: transparent !important;
box-shadow: none !important;
border: 0 !important;
border-radius: 0 !important;
outline: none !important;
-webkit-box-shadow: none !important;
}
}
</style>
</head>
<body class="min-h-screen bg-gray-50 font-sans text-gray-900 antialiased">
<?php helper('rbac'); ?>
<div id="admin-shell" class="flex h-screen w-full overflow-hidden">
<div id="sidebar-overlay" class="fixed inset-0 z-40 hidden bg-black/50 lg:hidden" aria-hidden="true"></div>
<?= $this->include('layouts/sidebar') ?>
<div class="flex min-w-0 flex-1 flex-col overflow-hidden bg-gray-50">
<?= $this->include('layouts/header') ?>
<main class="custom-scrollbar relative flex flex-1 flex-col overflow-x-hidden overflow-y-auto">
<div class="mx-auto w-full max-w-screen-2xl flex-1 p-4 pb-10 md:p-6 md:pb-12 2xl:p-8 2xl:pb-14">
<?= $this->renderSection('content') ?>
</div>
<?= $this->include('layouts/footer') ?>
</main>
</div>
</div>
<script>
(function () {
var shell = document.getElementById('admin-shell');
var overlay = document.getElementById('sidebar-overlay');
var btnMobile = document.getElementById('btn-mobile-sidebar');
var btnDesktop = document.getElementById('btn-desktop-sidebar-toggle');
function openMobile() {
if (!shell) return;
shell.classList.add('mobile-sidebar-open');
if (overlay) overlay.classList.remove('hidden');
}
function closeMobile() {
if (!shell) return;
shell.classList.remove('mobile-sidebar-open');
if (overlay) overlay.classList.add('hidden');
}
if (btnMobile) btnMobile.addEventListener('click', function () {
if (shell && shell.classList.contains('mobile-sidebar-open')) closeMobile(); else openMobile();
});
if (overlay) overlay.addEventListener('click', closeMobile);
window.addEventListener('resize', function () {
if (window.matchMedia('(min-width: 1024px)').matches) closeMobile();
});
if (btnDesktop && shell) {
btnDesktop.addEventListener('click', function () {
shell.classList.toggle('sidebar-narrow');
try {
localStorage.setItem('adminSidebarNarrow', shell.classList.contains('sidebar-narrow') ? '1' : '0');
} catch (e) {}
});
try {
if (localStorage.getItem('adminSidebarNarrow') === '1') {
shell.classList.add('sidebar-narrow');
}
} catch (e) {}
}
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,15 @@
<?php
/** @var list<string>|array<int, string>|null $errors */
$errors = is_array($errors ?? null) ? $errors : [];
if ($errors === []) {
return;
}
?>
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900 shadow-sm" role="alert">
<p class="font-medium text-amber-950">Perlu perhatian</p>
<ul class="mt-2 list-inside list-disc space-y-1">
<?php foreach ($errors as $e) : ?>
<li><?= esc((string) $e) ?></li>
<?php endforeach ?>
</ul>
</div>

View File

@@ -0,0 +1,14 @@
<?php
$icon = isset($icon) && is_string($icon) ? $icon : 'fa-inbox';
$title = isset($title) && is_string($title) ? $title : 'Tidak ada data';
$hint = isset($hint) && is_string($hint) ? $hint : '';
?>
<div class="flex flex-col items-center justify-center rounded-2xl border border-dashed border-gray-200 bg-gray-50 px-6 py-12 text-center">
<span class="flex h-14 w-14 items-center justify-center rounded-full bg-white text-2xl text-gray-400 shadow-sm ring-1 ring-gray-100">
<i class="fa-solid <?= esc($icon) ?>"></i>
</span>
<p class="mt-4 text-sm font-semibold text-gray-900"><?= esc($title) ?></p>
<?php if ($hint !== '') : ?>
<p class="mt-2 max-w-md text-sm text-gray-500"><?= esc($hint) ?></p>
<?php endif ?>
</div>

View File

@@ -0,0 +1,179 @@
<?php
/**
* Menu panel admin + visibilitas menurut AdminAccess::canAccess().
*/
$u = uri_string();
$is = static function (string $prefix) use ($u): bool {
return $u === $prefix || str_starts_with($u, $prefix . '/');
};
$exact = static function (string $path) use ($u): bool {
return $u === $path || $u === $path . '/';
};
$dashActive = $exact('admin');
$linkBase = 'flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-colors';
$linkIdle = 'text-gray-700 hover:bg-gray-100';
$linkActive = 'bg-gray-100 text-gray-900';
$subWrap = 'mt-1 space-y-0.5 border-l border-gray-200 py-1 pl-3 ml-3';
$subLink = 'block rounded-md px-2 py-2 text-xs font-medium transition-colors';
$subIdle = 'text-gray-600 hover:bg-gray-50 hover:text-gray-900';
$subActive = 'bg-gray-100 text-gray-900';
$presensiOpen = $is('admin/presensi');
$presensiData = $exact('admin/presensi') || str_starts_with($u, 'admin/presensi/detail');
$presLapangan = str_starts_with($u, 'admin/presensi/lapangan');
$presLembur = str_starts_with($u, 'admin/presensi/lembur');
$presJadwal = str_starts_with($u, 'admin/presensi/jadwal');
$presLibur = str_starts_with($u, 'admin/presensi/libur');
$presAktiv = str_starts_with($u, 'admin/presensi/aktivitas');
$perusahaanOpen = $is('admin/perusahaan');
$laporanOpen = $is('admin/laporan');
$laporanRingkas = $exact('admin/laporan');
$laporanCuti = str_starts_with($u, 'admin/laporan/cuti');
$panelOpen = $is('admin/panel');
$panelUserList = $exact('admin/panel/users') || str_starts_with($u, 'admin/panel/users/reset') || str_starts_with($u, 'admin/panel/users/edit');
$panelUserCreate = str_starts_with($u, 'admin/panel/users/create');
$panelGroupsIndex = $exact('admin/panel/groups');
$panelGroupCreate = str_starts_with($u, 'admin/panel/groups/create');
$panelGroupEdit = str_starts_with($u, 'admin/panel/groups/edit');
$utilOpen = $is('admin/util');
?>
<aside id="admin-sidebar" class="fixed inset-y-0 left-0 z-50 flex h-screen w-[290px] shrink-0 flex-col border-r border-gray-200 bg-white shadow-sm lg:static lg:shadow-none">
<div class="sidebar-brand flex h-16 shrink-0 items-center gap-3 border-b border-gray-200 px-5">
<img src="<?= base_url('assets/images/bpr-logo.png') ?>" alt="Logo BPR" width="144" height="36" decoding="async" class="sidebar-logo-expanded h-9 w-auto max-w-[9rem] shrink-0 object-contain object-left">
<img src="<?= base_url('assets/images/bij_logo.png') ?>" alt="BIJ" width="36" height="36" decoding="async" class="sidebar-logo-collapsed hidden h-9 w-9 shrink-0 rounded-none border-0 bg-transparent object-contain shadow-none ring-0 outline-none">
</div>
<nav class="custom-scrollbar flex-1 overflow-y-auto overflow-x-hidden px-3 py-5 text-sm">
<p class="nav-section-label mb-2 px-3 text-[11px] font-semibold uppercase tracking-wider text-gray-400">Menu</p>
<a href="<?= site_url('admin') ?>" title="Beranda" class="<?= $linkBase ?> <?= $dashActive ? $linkActive : $linkIdle ?>">
<i class="fa-solid fa-house w-5 shrink-0 text-center text-gray-500"></i>
<span class="sidebar-text">Beranda</span>
</a>
<?php if (canAccess('presensi')) : ?>
<details class="group mb-1" <?= $presensiOpen ? 'open' : '' ?>>
<summary class="<?= $linkBase ?> cursor-pointer list-none text-gray-700 marker:hidden hover:bg-gray-100 [&::-webkit-details-marker]:hidden">
<i class="fa-solid fa-clock w-5 shrink-0 text-center text-gray-500"></i>
<span class="sidebar-text flex-1 text-left">Presensi</span>
<i class="fa-solid fa-chevron-down text-[10px] text-gray-400 transition group-open:rotate-180"></i>
</summary>
<div class="<?= $subWrap ?>">
<a href="<?= site_url('admin/presensi') ?>" title="Data Presensi" class="<?= $subLink ?> <?= $presensiData ? $subActive : $subIdle ?>">Data Presensi</a>
<a href="<?= site_url('admin/presensi/lapangan') ?>" title="Tugas luar / lapangan" class="<?= $subLink ?> <?= $presLapangan ? $subActive : $subIdle ?>">Tugas Luar / Lapangan</a>
<a href="<?= site_url('admin/presensi/lembur') ?>" title="Jadwal lembur" class="<?= $subLink ?> <?= $presLembur ? $subActive : $subIdle ?>">Jadwal Lembur</a>
<?php if (canAccess('presensi_jadwal')) : ?>
<a href="<?= site_url('admin/presensi/jadwal') ?>" title="Management jadwal" class="<?= $subLink ?> <?= $presJadwal ? $subActive : $subIdle ?>">Management Jadwal</a>
<?php endif ?>
<?php if (canAccess('presensi_libur')) : ?>
<a href="<?= site_url('admin/presensi/libur') ?>" title="Hari libur" class="<?= $subLink ?> <?= $presLibur ? $subActive : $subIdle ?>">Hari Libur</a>
<?php endif ?>
<a href="<?= site_url('admin/presensi/aktivitas') ?>" title="Rekaman aktivitas" class="<?= $subLink ?> <?= $presAktiv ? $subActive : $subIdle ?>">Rekaman Aktivitas</a>
</div>
</details>
<?php endif ?>
<?php if (canAccess('perusahaan')) : ?>
<details class="group mb-1" <?= $perusahaanOpen ? 'open' : '' ?>>
<summary class="<?= $linkBase ?> cursor-pointer list-none text-gray-700 marker:hidden hover:bg-gray-100 [&::-webkit-details-marker]:hidden">
<i class="fa-solid fa-building w-5 shrink-0 text-center text-gray-500"></i>
<span class="sidebar-text flex-1 text-left">Perusahaan</span>
<i class="fa-solid fa-chevron-down text-[10px] text-gray-400 transition group-open:rotate-180"></i>
</summary>
<div class="<?= $subWrap ?>">
<a href="<?= site_url('admin/perusahaan/kantor') ?>" title="Lokasi kerja" class="<?= $subLink ?> <?= str_starts_with($u, 'admin/perusahaan/kantor') ? $subActive : $subIdle ?>">Lokasi Kerja</a>
<a href="<?= site_url('admin/perusahaan/unit_kerja') ?>" title="Unit kerja" class="<?= $subLink ?> <?= str_starts_with($u, 'admin/perusahaan/unit_kerja') ? $subActive : $subIdle ?>">Unit Kerja</a>
<a href="<?= site_url('admin/perusahaan/jabatan') ?>" title="Jabatan" class="<?= $subLink ?> <?= str_starts_with($u, 'admin/perusahaan/jabatan') ? $subActive : $subIdle ?>">Jabatan</a>
<a href="<?= site_url('admin/perusahaan/golongan') ?>" title="Golongan" class="<?= $subLink ?> <?= str_starts_with($u, 'admin/perusahaan/golongan') ? $subActive : $subIdle ?>">Golongan</a>
<a href="<?= site_url('admin/perusahaan/berita') ?>" title="Berita dan pengumuman" class="<?= $subLink ?> <?= str_starts_with($u, 'admin/perusahaan/berita') ? $subActive : $subIdle ?>">Berita / Pengumuman</a>
</div>
</details>
<?php endif ?>
<?php if (canAccess('pegawai')) : ?>
<details class="group mb-1" <?= ($is('admin/pegawai') || $is('admin/cuti')) ? 'open' : '' ?>>
<summary class="<?= $linkBase ?> cursor-pointer list-none text-gray-700 marker:hidden hover:bg-gray-100 [&::-webkit-details-marker]:hidden">
<i class="fa-solid fa-users w-5 shrink-0 text-center text-gray-500"></i>
<span class="sidebar-text flex-1 text-left">Pegawai</span>
<i class="fa-solid fa-chevron-down text-[10px] text-gray-400 transition group-open:rotate-180"></i>
</summary>
<div class="<?= $subWrap ?>">
<a href="<?= site_url('admin/pegawai') ?>" title="Data pegawai" class="<?= $subLink ?> <?= $is('admin/pegawai') && ! $is('admin/cuti') ? $subActive : $subIdle ?>">Data Pegawai</a>
<?php if (canAccess('cuti')) : ?>
<a href="<?= site_url('admin/cuti') ?>" title="Data cuti" class="<?= $subLink ?> <?= $is('admin/cuti') ? $subActive : $subIdle ?>">Data Cuti</a>
<?php endif ?>
</div>
</details>
<?php endif ?>
<?php if (canAccess('laporan')) : ?>
<details class="group mb-1" <?= $laporanOpen ? 'open' : '' ?>>
<summary class="<?= $linkBase ?> cursor-pointer list-none text-gray-700 marker:hidden hover:bg-gray-100 [&::-webkit-details-marker]:hidden">
<i class="fa-solid fa-print w-5 shrink-0 text-center text-gray-500"></i>
<span class="sidebar-text flex-1 text-left">Laporan</span>
<i class="fa-solid fa-chevron-down text-[10px] text-gray-400 transition group-open:rotate-180"></i>
</summary>
<div class="<?= $subWrap ?>">
<a href="<?= site_url('admin/laporan') ?>" title="Statistik dan ringkasan" class="<?= $subLink ?> <?= $laporanRingkas ? $subActive : $subIdle ?>">Statistik / Ringkasan</a>
<a href="<?= site_url('admin/laporan/cuti') ?>" title="Laporan cuti pegawai" class="<?= $subLink ?> <?= $laporanCuti ? $subActive : $subIdle ?>">Cuti Pegawai</a>
</div>
</details>
<?php endif ?>
<?php if (canAccess('panel')) : ?>
<details class="group mb-1" <?= $panelOpen ? 'open' : '' ?>>
<summary class="<?= $linkBase ?> cursor-pointer list-none text-gray-700 marker:hidden hover:bg-gray-100 [&::-webkit-details-marker]:hidden">
<i class="fa-solid fa-user-shield w-5 shrink-0 text-center text-gray-500"></i>
<span class="sidebar-text flex-1 text-left">Akses Pengguna</span>
<i class="fa-solid fa-chevron-down text-[10px] text-gray-400 transition group-open:rotate-180"></i>
</summary>
<div class="<?= $subWrap ?>">
<a href="<?= site_url('admin/panel/users') ?>" title="Daftar pengguna admin" class="<?= $subLink ?> <?= $panelUserList && ! $panelUserCreate ? $subActive : $subIdle ?>">Daftar Pengguna</a>
<a href="<?= site_url('admin/panel/users/create') ?>" title="Tambah pengguna admin" class="<?= $subLink ?> <?= $panelUserCreate ? $subActive : $subIdle ?>">Tambah Pengguna</a>
<a href="<?= site_url('admin/panel/groups') ?>" title="Grup akses" class="<?= $subLink ?> <?= ($panelGroupsIndex || $panelGroupCreate || $panelGroupEdit) && ! $panelGroupCreate && ! $panelGroupEdit ? $subActive : $subIdle ?>">Kelola Grup</a>
<a href="<?= site_url('admin/panel/groups/create') ?>" title="Tambah grup" class="<?= $subLink ?> <?= $panelGroupCreate ? $subActive : $subIdle ?>">Tambah Grup</a>
</div>
</details>
<?php endif ?>
<?php if (canAccess('utilitas')) : ?>
<details class="group mb-1" <?= $utilOpen ? 'open' : '' ?>>
<summary class="<?= $linkBase ?> cursor-pointer list-none text-gray-700 marker:hidden hover:bg-gray-100 [&::-webkit-details-marker]:hidden">
<i class="fa-solid fa-gears w-5 shrink-0 text-center text-gray-500"></i>
<span class="sidebar-text flex-1 text-left">Utilitas</span>
<i class="fa-solid fa-chevron-down text-[10px] text-gray-400 transition group-open:rotate-180"></i>
</summary>
<div class="<?= $subWrap ?>">
<a href="<?= site_url('admin/util/backup') ?>" title="Backup database" class="<?= $subLink ?> <?= str_starts_with($u, 'admin/util/backup') ? $subActive : $subIdle ?>">Daftar File Backup</a>
</div>
</details>
<?php endif ?>
<?php
$apk = env('ANDROID_APP_APK_URL', 'https://bij.mwp.co.id/assets/uploads/bij_apps_v_1_1_0.apk');
?>
<div class="mt-4 border-t border-gray-200 pt-4">
<p class="nav-section-label mb-2 px-3 text-[11px] font-semibold uppercase tracking-wider text-gray-400">Tautan</p>
<?php if (is_string($apk) && $apk !== '' && canAccess('apk_link')) : ?>
<a href="<?= esc($apk) ?>" target="_blank" rel="noopener noreferrer" title="Unduh aplikasi Android" class="<?= $linkBase ?> text-emerald-700 hover:bg-emerald-50">
<i class="fa-brands fa-android w-5 shrink-0 text-center"></i>
<span class="sidebar-text">Aplikasi Android</span>
</a>
<?php endif ?>
<?php if (session()->get('admin_mobile_token')) : ?>
<p class="sidebar-text mb-1 truncate px-3 text-xs text-gray-500"><?= esc(session('admin_username') ?? '') ?></p>
<a href="<?= site_url('admin/logout') ?>" title="Keluar dari panel" class="<?= $linkBase ?> text-red-600 hover:bg-red-50">
<i class="fa-solid fa-right-from-bracket w-5 shrink-0 text-center"></i>
<span class="sidebar-text">Sign Out</span>
</a>
<?php else : ?>
<a href="<?= site_url('admin/login') ?>" title="Masuk ke panel admin" class="<?= $linkBase ?> border border-gray-200 bg-gray-50 text-gray-900 hover:bg-white">
<i class="fa-solid fa-key w-5 shrink-0 text-center text-gray-500"></i>
<span class="sidebar-text">Login</span>
</a>
<?php endif ?>
</div>
</nav>
</aside>

View File

@@ -0,0 +1,19 @@
<?php $this->extend('layouts/main'); ?>
<?php $this->section('title'); ?>
Dashboard
<?php $this->endSection(); ?>
<?php $this->section('content'); ?>
<div class="mx-auto max-w-4xl rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
<h1 class="text-2xl font-semibold text-slate-900">Dashboard</h1>
<p class="mt-2 text-slate-600">
Layout admin memakai struktur partial CI4 (<code>extend</code> / <code>section</code>) dan aset TailAdmin di
<code><?= esc(base_url('assets/tailadmin/')) ?></code>.
</p>
<ul class="mt-4 list-inside list-disc text-sm text-slate-600">
<li>API mobile: <code class="rounded bg-slate-100 px-1">POST <?= esc(site_url('api/mobile/login')) ?></code></li>
<li>Endpoint login JSON (opsional): <code class="rounded bg-slate-100 px-1">POST <?= esc(site_url('json/login')) ?></code></li>
</ul>
</div>
<?php $this->endSection(); ?>

File diff suppressed because one or more lines are too long