Aplikasi kehadiran KTM ambil peti undi (SPR 2026)
Sistem untuk admin DUN merekod waktu KTM (Ketua Tempat Mengundi) hadir mengambil peti undi. Data KTM dibaca daripada DB sppm (spr2026) melalui connection kedua — hanya penempatan berjawatan KTM yang dipaparkan. - Dashboard: senarai KTM ikut pusat mengundi (asc) & saluran (asc), status BELUM HADIR (merah) / HADIR (hijau) + tarikh & masa - Carian nama/no. KP, tapisan pusat mengundi & status - Tanda hadir / batal dengan kawalan DUN (admin terhad ke DUN sendiri) - Eksport Excel (worksheet Hadir & Belum Hadir) guna SimpleXlsx - Peranan: superadmin (urus pengguna, semua DUN) & admin DUN - Docker: nginx + php-fpm + queue + scheduler + webhook deploy, port 127.0.0.1:8009, TZ Asia/Kuala_Lumpur di semua container - Zon waktu aplikasi: Asia/Kuala_Lumpur (APP_TIMEZONE) Berasaskan scaffold spr2026_taklimat. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
28
resources/views/auth/login.blade.php
Normal file
28
resources/views/auth/login.blade.php
Normal file
@@ -0,0 +1,28 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('tajuk', 'Log Masuk')
|
||||
@section('saiz-container', 'container-kecil')
|
||||
|
||||
@section('kandungan')
|
||||
<div class="kad">
|
||||
<h1>Log Masuk</h1>
|
||||
<p class="teks-kecil">Sistem rekod kehadiran KTM mengambil peti undi. Untuk superadmin dan admin DUN sahaja.</p>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="makluman makluman-ralat">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('login.attempt') }}">
|
||||
@csrf
|
||||
<label for="email">Emel</label>
|
||||
<input type="email" id="email" name="email" value="{{ old('email') }}" required autofocus>
|
||||
|
||||
<label for="password">Kata Laluan</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
|
||||
<button type="submit" class="btn btn-penuh">Log Masuk</button>
|
||||
</form>
|
||||
|
||||
<p class="teks-kecil mt-1"><a href="{{ route('password.request') }}">Lupa kata laluan?</a></p>
|
||||
</div>
|
||||
@endsection
|
||||
25
resources/views/auth/lupa-katalaluan.blade.php
Normal file
25
resources/views/auth/lupa-katalaluan.blade.php
Normal file
@@ -0,0 +1,25 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('tajuk', 'Lupa Kata Laluan')
|
||||
@section('saiz-container', 'container-kecil')
|
||||
|
||||
@section('kandungan')
|
||||
<div class="kad">
|
||||
<h1>Lupa Kata Laluan</h1>
|
||||
<p class="teks-kecil">Masukkan e-mel akaun anda. Pautan set semula kata laluan akan dihantar ke e-mel tersebut.</p>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="makluman makluman-ralat">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('password.email') }}">
|
||||
@csrf
|
||||
<label for="email">E-mel</label>
|
||||
<input type="email" id="email" name="email" value="{{ old('email') }}" required autofocus>
|
||||
|
||||
<button type="submit" class="btn btn-penuh">Hantar Pautan Set Semula</button>
|
||||
</form>
|
||||
|
||||
<p class="teks-kecil mt-1"><a href="{{ route('login') }}">Kembali ke Log Masuk</a></p>
|
||||
</div>
|
||||
@endsection
|
||||
30
resources/views/auth/set-kata-laluan.blade.php
Normal file
30
resources/views/auth/set-kata-laluan.blade.php
Normal file
@@ -0,0 +1,30 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('tajuk', 'Set Semula Kata Laluan')
|
||||
@section('saiz-container', 'container-kecil')
|
||||
|
||||
@section('kandungan')
|
||||
<div class="kad">
|
||||
<h1>Set Semula Kata Laluan</h1>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="makluman makluman-ralat">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('password.update') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="token" value="{{ $token }}">
|
||||
|
||||
<label for="email">E-mel</label>
|
||||
<input type="email" id="email" name="email" value="{{ old('email', $email) }}" required autofocus>
|
||||
|
||||
<label for="password">Kata Laluan Baharu</label>
|
||||
<input type="password" id="password" name="password" required minlength="8">
|
||||
|
||||
<label for="password_confirmation">Sahkan Kata Laluan Baharu</label>
|
||||
<input type="password" id="password_confirmation" name="password_confirmation" required minlength="8">
|
||||
|
||||
<button type="submit" class="btn btn-penuh">Set Semula Kata Laluan</button>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
148
resources/views/dashboard.blade.php
Normal file
148
resources/views/dashboard.blade.php
Normal file
@@ -0,0 +1,148 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('tajuk', 'Dashboard Kehadiran KTM')
|
||||
|
||||
@section('kandungan')
|
||||
<div class="stat-grid">
|
||||
<div class="stat">
|
||||
<div class="nilai">{{ $jumlahSemua }}</div>
|
||||
<div class="label">Jumlah KTM</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="nilai" style="color: var(--hijau)">{{ $jumlahHadir }}</div>
|
||||
<div class="label">Hadir</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="nilai" style="color: var(--merah)">{{ $jumlahBelum }}</div>
|
||||
<div class="label">Belum Hadir</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kad">
|
||||
<form method="GET" action="{{ route('dashboard') }}">
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(180px, 1fr)); gap:0 0.75rem;">
|
||||
@if (auth()->user()->isSuperadmin())
|
||||
<div>
|
||||
<label for="dun_id">DUN</label>
|
||||
<select id="dun_id" name="dun_id" onchange="this.form.submit()">
|
||||
<option value="">— Semua DUN —</option>
|
||||
@foreach ($senaraiDun as $dun)
|
||||
<option value="{{ $dun->id }}" @selected($dunId == $dun->id)>
|
||||
{{ $dun->code }} — {{ $dun->nama }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div>
|
||||
<label for="q">Cari Nama / No. KP</label>
|
||||
<input type="text" id="q" name="q" value="{{ request('q') }}" placeholder="Nama KTM atau No. KP">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="pusat_id">Pusat Mengundi</label>
|
||||
<select id="pusat_id" name="pusat_id">
|
||||
<option value="">— Semua Pusat —</option>
|
||||
@foreach ($senaraiPusat as $pusat)
|
||||
<option value="{{ $pusat->id }}" @selected(request('pusat_id') == $pusat->id)>
|
||||
{{ $pusat->nama }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="status">Status</label>
|
||||
<select id="status" name="status">
|
||||
<option value="">— Semua Status —</option>
|
||||
<option value="belum" @selected(request('status') === 'belum')>Belum Hadir</option>
|
||||
<option value="hadir" @selected(request('status') === 'hadir')>Hadir</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="baris-tindakan">
|
||||
<button type="submit" class="btn">Cari / Tapis</button>
|
||||
<a href="{{ route('dashboard') }}" class="btn btn-luar">Set Semula</a>
|
||||
<a href="{{ route('kehadiran.eksport', array_filter(['dun_id' => auth()->user()->isSuperadmin() ? $dunId : null])) }}"
|
||||
class="btn btn-hijau">⬇ Muat Turun Excel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="kad" style="padding:0;">
|
||||
<div class="jadual-kepala">
|
||||
<h2 class="tajuk-jadual mb-0">Senarai KTM (Ketua Tempat Mengundi)</h2>
|
||||
</div>
|
||||
|
||||
<div class="jadual-bekas">
|
||||
<table class="jadual jadual-responsif">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bil</th>
|
||||
<th>Pusat Mengundi</th>
|
||||
<th>Saluran</th>
|
||||
<th>Nama</th>
|
||||
<th>No. KP</th>
|
||||
<th>No. Telefon</th>
|
||||
<th>Jawatan</th>
|
||||
<th>Status</th>
|
||||
<th>Tindakan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($assignments as $i => $a)
|
||||
@php
|
||||
$rekod = $kehadiran->get($a->id);
|
||||
$p = $a->permohonan;
|
||||
@endphp
|
||||
<tr class="baris-data">
|
||||
<td data-label="Bil">{{ $i + 1 }}</td>
|
||||
<td data-label="Pusat Mengundi">{{ $a->pusatMengundi?->nama ?? '-' }}</td>
|
||||
<td data-label="Saluran">{{ $a->saluran ? 'Saluran ' . $a->saluran->nombor_saluran : '-' }}</td>
|
||||
<td data-label="Nama"><strong>{{ $p?->nama_ic ?? '-' }}</strong></td>
|
||||
<td data-label="No. KP">{{ $p?->no_kp ?? '-' }}</td>
|
||||
<td data-label="No. Telefon">{{ $p?->no_telefon ?? '-' }}</td>
|
||||
<td data-label="Jawatan">{{ $a->jawatan?->nama ?? '-' }}</td>
|
||||
<td data-label="Status">
|
||||
@if ($rekod)
|
||||
<span class="lencana lencana-hijau">HADIR</span>
|
||||
<div class="teks-kecil">{{ $rekod->hadir_at->format('d/m/Y h:i A') }}</div>
|
||||
@else
|
||||
<span class="lencana lencana-merah">BELUM HADIR</span>
|
||||
@endif
|
||||
</td>
|
||||
<td data-label="Tindakan">
|
||||
<div class="baris-tindakan">
|
||||
@if ($rekod)
|
||||
<form method="POST" action="{{ route('kehadiran.batal', $a->id) }}"
|
||||
onsubmit="return confirm('Batalkan rekod kehadiran {{ $p?->nama_ic }}?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-kecil btn-luar">Batal</button>
|
||||
</form>
|
||||
@else
|
||||
<form method="POST" action="{{ route('kehadiran.hadir', $a->id) }}"
|
||||
onsubmit="return confirm('Tandakan {{ $p?->nama_ic }} sebagai HADIR mengambil peti undi?');">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-kecil btn-hijau">Tanda Hadir</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr class="baris-tiada">
|
||||
<td colspan="9" class="teks-tengah">Tiada KTM dijumpai untuk tapisan ini.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="jadual-kaki">
|
||||
<span class="teks-kecil">{{ $assignments->count() }} rekod dipaparkan</span>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
46
resources/views/layouts/app.blade.php
Normal file
46
resources/views/layouts/app.blade.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ms">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@yield('tajuk', config('app.name'))</title>
|
||||
@php $cssVer = @filemtime(public_path('css/app.css')) ?: null; @endphp
|
||||
<link rel="stylesheet" href="{{ asset('css/app.css') }}{{ $cssVer ? '?v=' . $cssVer : '' }}">
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<a class="jenama" href="{{ auth()->check() ? route('dashboard') : url('/') }}">
|
||||
Kehadiran KTM — Peti Undi SPR 2026
|
||||
</a>
|
||||
@auth
|
||||
@php
|
||||
$labelPeranan = match (auth()->user()->role) {
|
||||
'superadmin' => 'Superadmin',
|
||||
'admin' => 'Admin DUN',
|
||||
default => auth()->user()->role,
|
||||
};
|
||||
@endphp
|
||||
<div class="pengguna">
|
||||
<a href="{{ route('dashboard') }}">Dashboard</a>
|
||||
@if (auth()->user()->isSuperadmin())
|
||||
<a href="{{ route('pengguna.index') }}">Pengguna</a>
|
||||
@endif
|
||||
<a href="{{ route('profil.edit') }}">Profil</a>
|
||||
<span>{{ auth()->user()->name }} ({{ $labelPeranan }})</span>
|
||||
<form method="POST" action="{{ route('logout') }}" style="margin:0">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-kecil btn-merah">Log Keluar</button>
|
||||
</form>
|
||||
</div>
|
||||
@endauth
|
||||
</header>
|
||||
|
||||
<main class="container @yield('saiz-container')">
|
||||
@if (session('status'))
|
||||
<div class="makluman makluman-jaya">{{ session('status') }}</div>
|
||||
@endif
|
||||
|
||||
@yield('kandungan')
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
75
resources/views/pengguna/create.blade.php
Normal file
75
resources/views/pengguna/create.blade.php
Normal file
@@ -0,0 +1,75 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('tajuk', 'Daftar Pengguna Baharu')
|
||||
@section('saiz-container', 'container-kecil')
|
||||
|
||||
@section('kandungan')
|
||||
<div class="kad">
|
||||
<h1>Daftar Pengguna Baharu</h1>
|
||||
<p class="teks-kecil">Admin DUN perlu ditetapkan kepada satu DUN. Superadmin boleh urus pengguna dan lihat semua DUN.</p>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="makluman makluman-ralat">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('pengguna.store') }}">
|
||||
@csrf
|
||||
<label for="name">Nama</label>
|
||||
<input type="text" id="name" name="name" value="{{ old('name') }}" required autofocus>
|
||||
|
||||
<label for="email">E-mel</label>
|
||||
<input type="email" id="email" name="email" value="{{ old('email') }}" required>
|
||||
|
||||
<label for="role">Peranan</label>
|
||||
<select id="role" name="role" required>
|
||||
<option value="">— Pilih Peranan —</option>
|
||||
@foreach ($peranan as $kod)
|
||||
<option value="{{ $kod }}" @selected(old('role') === $kod)>
|
||||
{{ match ($kod) {
|
||||
'superadmin' => 'Superadmin',
|
||||
'admin' => 'Admin DUN',
|
||||
default => $kod,
|
||||
} }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<div id="baris-dun">
|
||||
<label for="dun_id">DUN</label>
|
||||
<select id="dun_id" name="dun_id">
|
||||
<option value="">— Pilih DUN —</option>
|
||||
@foreach ($senaraiDun as $dun)
|
||||
<option value="{{ $dun->id }}" @selected(old('dun_id') == $dun->id)>
|
||||
{{ $dun->code }} — {{ $dun->nama }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label for="password">Kata Laluan</label>
|
||||
<input type="password" id="password" name="password" required minlength="8">
|
||||
|
||||
<label for="password_confirmation">Sahkan Kata Laluan</label>
|
||||
<input type="password" id="password_confirmation" name="password_confirmation" required minlength="8">
|
||||
|
||||
<button type="submit" class="btn btn-penuh">Daftar Pengguna</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var role = document.getElementById('role');
|
||||
var barisDun = document.getElementById('baris-dun');
|
||||
var dunSelect = document.getElementById('dun_id');
|
||||
|
||||
function kemaskini() {
|
||||
var perluDun = role.value === 'admin';
|
||||
barisDun.style.display = perluDun ? '' : 'none';
|
||||
dunSelect.required = perluDun;
|
||||
}
|
||||
|
||||
role.addEventListener('change', kemaskini);
|
||||
kemaskini();
|
||||
})();
|
||||
</script>
|
||||
@endsection
|
||||
47
resources/views/pengguna/index.blade.php
Normal file
47
resources/views/pengguna/index.blade.php
Normal file
@@ -0,0 +1,47 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('tajuk', 'Senarai Pengguna')
|
||||
|
||||
@section('kandungan')
|
||||
<div class="kad">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:0.5rem;">
|
||||
<h1 class="mb-0">Pengguna</h1>
|
||||
<a href="{{ route('pengguna.create') }}" class="btn">+ Daftar Pengguna</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($pengguna->isEmpty())
|
||||
<div class="kad teks-tengah">
|
||||
<p>Tiada pengguna lagi.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="kad" style="overflow-x:auto; padding:0;">
|
||||
<table class="jadual">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nama</th>
|
||||
<th>E-mel</th>
|
||||
<th>Peranan</th>
|
||||
<th>DUN</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($pengguna as $p)
|
||||
<tr>
|
||||
<td>{{ $p->name }}</td>
|
||||
<td>{{ $p->email }}</td>
|
||||
<td>
|
||||
{{ match ($p->role) {
|
||||
'superadmin' => 'Superadmin',
|
||||
'admin' => 'Admin DUN',
|
||||
default => $p->role,
|
||||
} }}
|
||||
</td>
|
||||
<td>{{ $p->dun ? $p->dun->code . ' — ' . $p->dun->nama : '-' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
51
resources/views/profil/edit.blade.php
Normal file
51
resources/views/profil/edit.blade.php
Normal file
@@ -0,0 +1,51 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('tajuk', 'Profil Saya')
|
||||
@section('saiz-container', 'container-kecil')
|
||||
|
||||
@section('kandungan')
|
||||
<div class="kad">
|
||||
<h1>Profil Saya</h1>
|
||||
<table class="slip-butiran">
|
||||
<tr><th>Nama</th><td>{{ $pengguna->name }}</td></tr>
|
||||
<tr><th>E-mel</th><td>{{ $pengguna->email }}</td></tr>
|
||||
<tr>
|
||||
<th>Peranan</th>
|
||||
<td>
|
||||
{{ match ($pengguna->role) {
|
||||
'superadmin' => 'Superadmin',
|
||||
'admin' => 'Admin DUN',
|
||||
default => $pengguna->role,
|
||||
} }}
|
||||
@if ($pengguna->dun)
|
||||
— {{ $pengguna->dun->code }} {{ $pengguna->dun->nama }}
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="kad">
|
||||
<h2 class="mb-0">Tukar Kata Laluan</h2>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="makluman makluman-ralat">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('profil.kata-laluan') }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<label for="kata_laluan_semasa">Kata Laluan Semasa</label>
|
||||
<input type="password" id="kata_laluan_semasa" name="kata_laluan_semasa" required>
|
||||
|
||||
<label for="kata_laluan_baharu">Kata Laluan Baharu</label>
|
||||
<input type="password" id="kata_laluan_baharu" name="kata_laluan_baharu" required minlength="8">
|
||||
|
||||
<label for="kata_laluan_baharu_confirmation">Sahkan Kata Laluan Baharu</label>
|
||||
<input type="password" id="kata_laluan_baharu_confirmation" name="kata_laluan_baharu_confirmation" required minlength="8">
|
||||
|
||||
<button type="submit" class="btn btn-penuh">Kemaskini Kata Laluan</button>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user