taklimat ulangan+profil

This commit is contained in:
Saufi
2026-07-04 14:45:16 +08:00
parent e803f55b80
commit 0c134b9038
24 changed files with 2375 additions and 34 deletions

View File

@@ -59,6 +59,23 @@ class CheckinController extends Controller
])->onlyInput('no_kp');
}
// Pernah hadir di sesi taklimat LAIN? Taklimat ulangan hanya untuk
// petugas yang belum pernah hadir langsung — tak perlu rekod semula.
$rekodSesiLain = KehadiranTaklimat::where('permohonan_id', $petugas->id)
->where('sesi_taklimat_id', '!=', $sesi->id)
->with('sesi.dun')
->orderBy('hadir_at')
->first();
if ($rekodSesiLain) {
return view('checkin.pernah-hadir', [
'sesi' => $sesi,
'petugas' => $petugas,
'kehadiranSebelumIni' => $rekodSesiLain,
'sesiSebelumIni' => $rekodSesiLain->sesi,
]);
}
// Jika sudah daftar dalam sesi ini, papar semula slip sedia ada
$kehadiran = KehadiranTaklimat::where('sesi_taklimat_id', $sesi->id)
->where('permohonan_id', $petugas->id)

View File

@@ -66,7 +66,7 @@ class KaunterController extends Controller
{
$user = $request->user();
if ($user->isPengurus() && (int) $sesi->dun_id !== (int) $user->dun_id) {
if (! $user->isAdmin() && (int) $sesi->dun_id !== (int) $user->dun_id) {
abort(403, 'Sesi ini bukan untuk DUN anda.');
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
class PasswordResetController extends Controller
{
public function showLinkRequestForm()
{
return view('auth.lupa-katalaluan');
}
public function sendResetLink(Request $request)
{
$request->validate(['email' => ['required', 'email']]);
$status = Password::sendResetLink($request->only('email'));
return $status === Password::RESET_LINK_SENT
? back()->with('status', 'Pautan set semula kata laluan telah dihantar ke e-mel anda.')
: back()->withErrors(['email' => $this->mesejRalat($status)]);
}
public function showResetForm(Request $request, string $token)
{
return view('auth.set-kata-laluan', [
'token' => $token,
'email' => $request->query('email', ''),
]);
}
public function reset(Request $request)
{
$data = $request->validate([
'token' => ['required', 'string'],
'email' => ['required', 'email'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
], [
'password.min' => 'Kata laluan perlu sekurang-kurangnya 8 aksara.',
'password.confirmed' => 'Pengesahan kata laluan tidak sepadan.',
]);
$status = Password::reset(
$data,
function ($user, $password) {
$user->forceFill(['password' => Hash::make($password)])->save();
}
);
return $status === Password::PASSWORD_RESET
? redirect()->route('login')->with('status', 'Kata laluan berjaya ditetapkan semula. Sila log masuk.')
: back()->withErrors(['email' => $this->mesejRalat($status)])->onlyInput('email');
}
private function mesejRalat(string $status): string
{
return match ($status) {
Password::INVALID_USER => 'Akaun dengan e-mel ini tidak ditemui.',
Password::RESET_THROTTLED => 'Sila tunggu sebentar sebelum cuba lagi.',
Password::INVALID_TOKEN => 'Pautan set semula tidak sah atau telah tamat tempoh.',
default => 'Gagal memproses permintaan. Sila cuba lagi.',
};
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace App\Http\Controllers;
use App\Models\Sppm\Dun;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
class PenggunaController extends Controller
{
/**
* Peranan yang boleh didaftarkan oleh pengurus DUN (untuk DUN sendiri sahaja).
*/
private const PERANAN_PENGURUS = ['pengurus', 'kaunter'];
private const PERANAN_ADMIN = ['admin', 'pengurus', 'kaunter'];
/**
* Senarai pengguna. Pengurus hanya nampak pengguna DUN sendiri.
*/
public function index(Request $request)
{
$user = $request->user();
$pengguna = User::query()
->when(
$user->isPengurus(),
fn ($q) => $q->where('dun_id', $user->dun_id)->whereIn('role', self::PERANAN_PENGURUS)
)
->with('dun')
->orderBy('role')
->orderBy('name')
->get();
return view('pengguna.index', compact('pengguna'));
}
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();
$peranan = $user->isAdmin() ? self::PERANAN_ADMIN : self::PERANAN_PENGURUS;
return view('pengguna.create', compact('senaraiDun', 'peranan'));
}
public function store(Request $request)
{
$user = $request->user();
$perananDibenarkan = $user->isAdmin() ? self::PERANAN_ADMIN : self::PERANAN_PENGURUS;
$data = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
'role' => ['required', 'string', Rule::in($perananDibenarkan)],
'dun_id' => ['nullable', 'integer'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
], [
'email.unique' => 'E-mel ini sudah didaftarkan.',
'password.min' => 'Kata laluan perlu sekurang-kurangnya 8 aksara.',
'password.confirmed' => 'Pengesahan kata laluan tidak sepadan.',
]);
if ($user->isPengurus()) {
// Pengurus hanya boleh daftar untuk DUN sendiri, tidak kira input.
$data['dun_id'] = $user->dun_id;
} elseif ($data['role'] === 'admin') {
$data['dun_id'] = null;
} elseif (empty($data['dun_id'])) {
return back()->withErrors(['dun_id' => 'Sila pilih DUN untuk peranan ini.'])->withInput();
}
if (! empty($data['dun_id']) && ! Dun::where('id', $data['dun_id'])->where('is_active', true)->exists()) {
return back()->withErrors(['dun_id' => 'DUN tidak sah.'])->withInput();
}
User::create([
'name' => $data['name'],
'email' => $data['email'],
'role' => $data['role'],
'dun_id' => $data['dun_id'] ?? null,
'password' => Hash::make($data['password']),
]);
return redirect()->route('pengguna.index')->with('status', 'Pengguna baharu berjaya didaftarkan.');
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Models\KehadiranTaklimat;
use App\Models\SesiTaklimat;
use App\Models\Sppm\PetugasPermohonan;
use Illuminate\Http\Request;
@@ -9,13 +10,15 @@ use Illuminate\Http\Request;
class PengurusController extends Controller
{
/**
* Senarai petugas hadir dan tidak/belum hadir bagi satu sesi taklimat.
* Senarai petugas bagi satu sesi taklimat, dipecahkan kepada 3 kumpulan:
* hadir (sesi ini), pernah hadir (sesi taklimat lain), dan
* tidak/belum hadir langsung (tidak pernah hadir mana-mana sesi).
*/
public function senarai(Request $request, SesiTaklimat $sesi)
{
$user = $request->user();
if ($user->isPengurus() && (int) $sesi->dun_id !== (int) $user->dun_id) {
if (! $user->isAdmin() && (int) $sesi->dun_id !== (int) $user->dun_id) {
abort(403, 'Sesi ini bukan untuk DUN anda.');
}
@@ -29,15 +32,28 @@ class PengurusController extends Controller
$kehadiran = $sesi->kehadiran()->get()->keyBy('permohonan_id');
[$hadir, $tidakHadir] = $semuaPetugas->partition(
fn (PetugasPermohonan $p) => $kehadiran->has($p->id)
$belumHadirSesiIni = $semuaPetugas->reject(fn (PetugasPermohonan $p) => $kehadiran->has($p->id));
// Antara yang belum hadir sesi ini, semak siapa pernah hadir di sesi LAIN.
$kehadiranSesiLain = KehadiranTaklimat::whereIn('permohonan_id', $belumHadirSesiIni->pluck('id'))
->where('sesi_taklimat_id', '!=', $sesi->id)
->with('sesi.dun')
->orderBy('hadir_at')
->get()
->unique('permohonan_id')
->keyBy('permohonan_id');
[$hadirSesiLain, $tidakHadir] = $belumHadirSesiIni->partition(
fn (PetugasPermohonan $p) => $kehadiranSesiLain->has($p->id)
);
return view('pengurus.senarai', [
'sesi' => $sesi,
'hadir' => $hadir->values(),
'hadir' => $semuaPetugas->filter(fn (PetugasPermohonan $p) => $kehadiran->has($p->id))->values(),
'hadirSesiLain' => $hadirSesiLain->values(),
'tidakHadir' => $tidakHadir->values(),
'kehadiran' => $kehadiran,
'kehadiranSesiLain' => $kehadiranSesiLain,
]);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class ProfilController extends Controller
{
public function edit(Request $request)
{
return view('profil.edit', ['pengguna' => $request->user()]);
}
public function updatePassword(Request $request)
{
$data = $request->validate([
'kata_laluan_semasa' => ['required', 'string'],
'kata_laluan_baharu' => ['required', 'string', 'min:8', 'confirmed'],
], [
'kata_laluan_semasa.required' => 'Sila masukkan kata laluan semasa.',
'kata_laluan_baharu.required' => 'Sila masukkan kata laluan baharu.',
'kata_laluan_baharu.min' => 'Kata laluan baharu perlu sekurang-kurangnya 8 aksara.',
'kata_laluan_baharu.confirmed' => 'Pengesahan kata laluan baharu tidak sepadan.',
]);
$user = $request->user();
if (! Hash::check($data['kata_laluan_semasa'], $user->password)) {
return back()->withErrors(['kata_laluan_semasa' => 'Kata laluan semasa tidak tepat.']);
}
$user->update(['password' => Hash::make($data['kata_laluan_baharu'])]);
return back()->with('status', 'Kata laluan berjaya dikemaskini.');
}
}

View File

@@ -9,14 +9,14 @@ use Illuminate\Http\Request;
class SesiTaklimatController extends Controller
{
/**
* Senarai sesi taklimat. Pengurus hanya nampak DUN sendiri.
* Senarai sesi taklimat. Pengurus & kaunter 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))
->when(! $user->isAdmin(), fn ($q) => $q->where('dun_id', $user->dun_id))
->orderByDesc('tarikh')
->get();
@@ -106,7 +106,7 @@ class SesiTaklimatController extends Controller
{
$user = $request->user();
if ($user->isPengurus() && (int) $sesi->dun_id !== (int) $user->dun_id) {
if (! $user->isAdmin() && (int) $sesi->dun_id !== (int) $user->dun_id) {
abort(403, 'Sesi ini bukan untuk DUN anda.');
}
}

View File

@@ -3,10 +3,13 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Models\Sppm\Dun;
use App\Notifications\ResetKataLaluanNotification;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@@ -27,6 +30,24 @@ class User extends Authenticatable
return $this->role === 'pengurus';
}
public function isKaunter(): bool
{
return $this->role === 'kaunter';
}
public function dun(): BelongsTo
{
return $this->belongsTo(Dun::class, 'dun_id');
}
/**
* Guna e-mel dalam Bahasa Melayu untuk pautan set semula kata laluan.
*/
public function sendPasswordResetNotification(#[\SensitiveParameter] $token): void
{
$this->notify(new ResetKataLaluanNotification($token));
}
/**
* Get the attributes that should be cast.
*

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Notifications;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ResetKataLaluanNotification extends Notification
{
public function __construct(private readonly string $token)
{
}
/**
* @return array<int,string>
*/
public function via(mixed $notifiable): array
{
return ['mail'];
}
public function toMail(mixed $notifiable): MailMessage
{
$url = url(route('password.reset', [
'token' => $this->token,
'email' => $notifiable->getEmailForPasswordReset(),
], false));
$minit = config('auth.passwords.users.expire');
return (new MailMessage)
->subject('Set Semula Kata Laluan - Kehadiran Taklimat SPR')
->line('Kami menerima permintaan untuk set semula kata laluan akaun anda.')
->action('Set Semula Kata Laluan', $url)
->line("Pautan ini akan tamat tempoh dalam {$minit} minit.")
->line('Jika anda tidak membuat permintaan ini, sila abaikan e-mel ini.');
}
}