first commit

This commit is contained in:
Saufi
2026-06-14 08:52:33 +08:00
commit 359229685d
87 changed files with 12477 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AuthController extends Controller
{
public function showLogin()
{
if (Auth::check()) {
return redirect()->route('sesi.index');
}
return view('auth.login');
}
public function login(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (Auth::attempt($credentials, $request->boolean('remember'))) {
$request->session()->regenerate();
return redirect()->intended(route('sesi.index'));
}
return back()->withErrors([
'email' => 'Emel atau kata laluan tidak sah.',
])->onlyInput('email');
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login');
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace App\Http\Controllers;
use App\Models\KehadiranTaklimat;
use App\Models\SesiTaklimat;
use App\Models\Sppm\PetugasPermohonan;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class CheckinController extends Controller
{
/**
* Papar borang daftar kehadiran selepas petugas scan QR DUN.
*/
public function show(string $token)
{
$sesi = SesiTaklimat::where('qr_token', $token)->firstOrFail();
$sesi->load('dun');
if (! $sesi->is_active) {
return view('checkin.closed', compact('sesi'));
}
return view('checkin.form', compact('sesi'));
}
/**
* Sahkan no KP dengan SPPM dan rekod kehadiran.
*/
public function submit(Request $request, string $token)
{
$sesi = SesiTaklimat::where('qr_token', $token)->firstOrFail();
$sesi->load('dun');
if (! $sesi->is_active) {
return view('checkin.closed', compact('sesi'));
}
$request->validate(
['no_kp' => ['required', 'string', 'max:20']],
['no_kp.required' => 'Sila masukkan nombor kad pengenalan anda.'],
);
$noKp = preg_replace('/[^0-9A-Za-z]/', '', $request->input('no_kp'));
// Semak dalam SPPM: no_kp padan + status 'assigned' + DUN yang betul
$petugas = PetugasPermohonan::assignedUntukDun($sesi->dun_id)
->whereRaw("REPLACE(REPLACE(no_kp, '-', ''), ' ', '') = ?", [$noKp])
->with(['assignments' => fn ($q) => $q->orderBy('urutan')->with(['pusatMengundi', 'saluran', 'jawatan'])])
->first();
if (! $petugas) {
return back()->withErrors([
'no_kp' => 'Maaf, rekod anda tidak ditemui sebagai petugas bertugas (assigned) untuk '
. ($sesi->dun->nama ?? 'DUN ini')
. '. Sila semak nombor kad pengenalan atau rujuk kaunter pendaftaran.',
])->onlyInput('no_kp');
}
// Jika sudah daftar dalam sesi ini, papar semula slip sedia ada
$kehadiran = KehadiranTaklimat::where('sesi_taklimat_id', $sesi->id)
->where('permohonan_id', $petugas->id)
->first();
$sudahDaftar = (bool) $kehadiran;
if (! $kehadiran) {
$kehadiran = $this->rekodKehadiran($sesi, $petugas, $noKp);
}
return view('checkin.slip', [
'sesi' => $sesi,
'petugas' => $petugas,
'kehadiran' => $kehadiran,
'penempatan' => $petugas->butiranPenempatan(),
'sudahDaftar' => $sudahDaftar,
]);
}
/**
* Jana nombor siri seterusnya dan simpan kehadiran secara atomik.
*/
private function rekodKehadiran(SesiTaklimat $sesi, PetugasPermohonan $petugas, string $noKp): KehadiranTaklimat
{
$attempts = 0;
while (true) {
try {
return DB::transaction(function () use ($sesi, $petugas, $noKp) {
$nomborSeterusnya = (int) KehadiranTaklimat::where('sesi_taklimat_id', $sesi->id)
->lockForUpdate()
->max('nombor_siri') + 1;
return KehadiranTaklimat::create([
'sesi_taklimat_id' => $sesi->id,
'permohonan_id' => $petugas->id,
'no_kp' => $noKp,
'nombor_siri' => $nomborSeterusnya,
'hadir_at' => now(),
]);
});
} catch (UniqueConstraintViolationException $e) {
// Hantar berganda: rekod petugas ini mungkin sudah wujud
$sediaAda = KehadiranTaklimat::where('sesi_taklimat_id', $sesi->id)
->where('permohonan_id', $petugas->id)
->first();
if ($sediaAda) {
return $sediaAda;
}
// Perlumbaan nombor siri — cuba semula sehingga 3 kali
if (++$attempts >= 3) {
throw $e;
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers;
use App\Models\KehadiranTaklimat;
use App\Models\SesiTaklimat;
use Illuminate\Http\Request;
class KaunterController extends Controller
{
/**
* Skrin kaunter: masukkan nombor siri slip petugas.
*/
public function index(Request $request, SesiTaklimat $sesi)
{
$this->pastikanAkses($request, $sesi);
$sesi->load('dun');
$kehadiran = null;
$penempatan = null;
$ralat = null;
if ($request->filled('nombor_siri')) {
$nombor = (int) ltrim($request->input('nombor_siri'), '0');
$kehadiran = KehadiranTaklimat::where('sesi_taklimat_id', $sesi->id)
->where('nombor_siri', $nombor)
->with('permohonan.assignments.pusatMengundi', 'permohonan.assignments.saluran', 'permohonan.assignments.jawatan')
->first();
if ($kehadiran) {
$penempatan = $kehadiran->permohonan?->butiranPenempatan();
} else {
$ralat = 'Nombor siri tidak ditemui dalam sesi ini.';
}
}
return view('kaunter.index', compact('sesi', 'kehadiran', 'penempatan', 'ralat'));
}
/**
* Rekod petugas telah ambil borang SPR.
*/
public function serahBorang(Request $request, SesiTaklimat $sesi, KehadiranTaklimat $kehadiran)
{
$this->pastikanAkses($request, $sesi);
if ((int) $kehadiran->sesi_taklimat_id !== (int) $sesi->id) {
abort(404);
}
if (! $kehadiran->borang_diambil) {
$kehadiran->update([
'borang_diambil' => true,
'borang_diambil_at' => now(),
'borang_diambil_oleh_user_id' => $request->user()->id,
]);
}
return redirect()
->route('kaunter.index', $sesi)
->with('status', 'Borang direkodkan sebagai telah diserahkan kepada petugas (siri ' . $kehadiran->nombor_siri_papar . ').');
}
private function pastikanAkses(Request $request, SesiTaklimat $sesi): void
{
$user = $request->user();
if ($user->isPengurus() && (int) $sesi->dun_id !== (int) $user->dun_id) {
abort(403, 'Sesi ini bukan untuk DUN anda.');
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers;
use App\Models\SesiTaklimat;
use App\Models\Sppm\PetugasPermohonan;
use Illuminate\Http\Request;
class PengurusController extends Controller
{
/**
* Senarai petugas hadir dan tidak/belum hadir bagi satu sesi taklimat.
*/
public function senarai(Request $request, SesiTaklimat $sesi)
{
$user = $request->user();
if ($user->isPengurus() && (int) $sesi->dun_id !== (int) $user->dun_id) {
abort(403, 'Sesi ini bukan untuk DUN anda.');
}
$sesi->load('dun');
// Semua petugas 'assigned' untuk DUN sesi ini
$semuaPetugas = PetugasPermohonan::assignedUntukDun($sesi->dun_id)
->with(['assignments' => fn ($q) => $q->orderBy('urutan')->with(['pusatMengundi', 'saluran', 'jawatan'])])
->orderBy('nama_ic')
->get();
$kehadiran = $sesi->kehadiran()->get()->keyBy('permohonan_id');
[$hadir, $tidakHadir] = $semuaPetugas->partition(
fn (PetugasPermohonan $p) => $kehadiran->has($p->id)
);
return view('pengurus.senarai', [
'sesi' => $sesi,
'hadir' => $hadir->values(),
'tidakHadir' => $tidakHadir->values(),
'kehadiran' => $kehadiran,
]);
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace App\Http\Controllers;
use App\Models\SesiTaklimat;
use App\Models\Sppm\Dun;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class SesiTaklimatController extends Controller
{
/**
* Senarai sesi taklimat. Pengurus hanya nampak DUN sendiri.
*/
public function index(Request $request)
{
$user = $request->user();
$sesi = SesiTaklimat::withCount('kehadiran')
->when($user->isPengurus(), fn ($q) => $q->where('dun_id', $user->dun_id))
->orderByDesc('tarikh')
->get();
$sesi->load('dun');
return view('sesi.index', compact('sesi'));
}
public function create(Request $request)
{
$user = $request->user();
$senaraiDun = Dun::where('is_active', true)
->when($user->isPengurus(), fn ($q) => $q->where('id', $user->dun_id))
->orderBy('code')
->get();
return view('sesi.create', compact('senaraiDun'));
}
public function store(Request $request)
{
$user = $request->user();
$data = $request->validate([
'dun_id' => [
'required',
'integer',
// Hanya 1 sesi dalam 1 hari untuk setiap dun
Rule::unique('sesi_taklimat', 'dun_id')->where('tarikh', $request->input('tarikh')),
],
'tarikh' => ['required', 'date'],
'nama' => ['required', 'string', 'max:255'],
'lokasi' => ['required', 'string', 'max:255'],
'masa' => ['nullable', 'string', 'max:100'],
], [
'dun_id.unique' => 'Sesi taklimat untuk DUN ini sudah wujud pada tarikh tersebut. Hanya 1 sesi dibenarkan sehari bagi setiap DUN.',
]);
if ($user->isPengurus() && (int) $data['dun_id'] !== (int) $user->dun_id) {
abort(403, 'Anda hanya boleh mencipta sesi untuk DUN anda sendiri.');
}
if (! Dun::where('id', $data['dun_id'])->where('is_active', true)->exists()) {
return back()->withErrors(['dun_id' => 'DUN tidak sah.'])->withInput();
}
$data['created_by_user_id'] = $user->id;
$sesi = SesiTaklimat::create($data);
return redirect()->route('sesi.show', $sesi)->with('status', 'Sesi taklimat berjaya dicipta.');
}
public function show(Request $request, SesiTaklimat $sesi)
{
$this->pastikanAkses($request, $sesi);
$sesi->load('dun');
$sesi->loadCount([
'kehadiran',
'kehadiran as borang_diambil_count' => fn ($q) => $q->where('borang_diambil', true),
]);
$checkinUrl = route('checkin.show', $sesi->qr_token);
return view('sesi.show', compact('sesi', 'checkinUrl'));
}
/**
* Papar QR untuk dicetak / dipamerkan di lokasi taklimat.
*/
public function qr(Request $request, SesiTaklimat $sesi)
{
$this->pastikanAkses($request, $sesi);
$sesi->load('dun');
$checkinUrl = route('checkin.show', $sesi->qr_token);
return view('sesi.qr', compact('sesi', 'checkinUrl'));
}
public function toggle(Request $request, SesiTaklimat $sesi)
{
$this->pastikanAkses($request, $sesi);
$sesi->update(['is_active' => ! $sesi->is_active]);
return back()->with('status', $sesi->is_active ? 'Sesi diaktifkan.' : 'Sesi ditutup.');
}
private function pastikanAkses(Request $request, SesiTaklimat $sesi): void
{
$user = $request->user();
if ($user->isPengurus() && (int) $sesi->dun_id !== (int) $user->dun_id) {
abort(403, 'Sesi ini bukan untuk DUN anda.');
}
}
}