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.');
}
}

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->enum('role', ['admin', 'pengurus', 'kaunter'])->default('admin')->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->enum('role', ['admin', 'pengurus'])->default('admin')->change();
});
}
};

1656
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -46,6 +46,11 @@ a { color: var(--biru); }
font-size: 0.9rem;
}
.topbar .pengguna a {
color: #fff;
text-decoration: underline;
}
.container {
max-width: 960px;
margin: 1.25rem auto;
@@ -199,6 +204,7 @@ table.jadual tbody tr:nth-child(even) { background: #f8f9fb; }
.lencana-merah { background: #fdecea; color: var(--merah); }
.lencana-kelabu { background: #ececec; color: #555; }
.lencana-biru { background: #e8f0fe; color: var(--biru); }
.lencana-kuning { background: #fbf0d9; color: var(--kuning); }
.baris-tindakan {
display: flex;
@@ -287,6 +293,7 @@ table.jadual tbody tr:nth-child(even) { background: #f8f9fb; }
}
.tajuk-jadual.hijau { border-color: var(--hijau); color: var(--hijau); }
.tajuk-jadual.merah { border-color: var(--merah); color: var(--merah); }
.tajuk-jadual.kuning { border-color: var(--kuning); color: var(--kuning); }
.jadual-kepala .cari-input {
width: auto;

View File

@@ -22,5 +22,7 @@
<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

View 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

View 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

View File

@@ -0,0 +1,30 @@
@extends('layouts.app')
@section('tajuk', 'Pernah Hadir')
@section('saiz-container', 'container-kecil')
@section('kandungan')
<div class="kad teks-tengah">
<div class="makluman makluman-info">
Anda telah pun hadir taklimat sebelum ini. Tiada keperluan untuk daftar semula.
</div>
<p class="teks-kecil mb-0">Status</p>
<div class="slip-nombor">Pernah Hadir</div>
</div>
<div class="kad">
<table class="slip-butiran">
<tr><th>Nama</th><td><strong>{{ $petugas->nama_ic }}</strong></td></tr>
<tr><th>No. KP</th><td>{{ $petugas->no_kp }}</td></tr>
</table>
<h2 class="tajuk-jadual" style="margin-top:1.5rem;">Sesi Taklimat Sebelum Ini</h2>
<table class="slip-butiran">
<tr><th>DUN</th><td>{{ $sesiSebelumIni->dun->code ?? '' }} {{ $sesiSebelumIni->dun->nama ?? '-' }}</td></tr>
<tr><th>Sesi</th><td>{{ $sesiSebelumIni->nama }} ({{ $sesiSebelumIni->tarikh->format('d/m/Y') }})</td></tr>
<tr><th>Lokasi</th><td>{{ $sesiSebelumIni->lokasi }}</td></tr>
<tr><th>Nombor Siri</th><td>{{ $kehadiranSebelumIni->nombor_siri_papar }}</td></tr>
<tr><th>Masa Daftar</th><td>{{ $kehadiranSebelumIni->hadir_at->format('d/m/Y h:i A') }}</td></tr>
</table>
</div>
@endsection

View File

@@ -13,8 +13,20 @@
Kehadiran Taklimat SPR 2026
</a>
@auth
@php
$labelPeranan = match (auth()->user()->role) {
'admin' => 'Admin',
'pengurus' => 'Pengurus DUN',
'kaunter' => 'Kaunter',
default => auth()->user()->role,
};
@endphp
<div class="pengguna">
<span>{{ auth()->user()->name }} ({{ auth()->user()->role === 'admin' ? 'Admin' : 'Pengurus DUN' }})</span>
@if (auth()->user()->isAdmin() || auth()->user()->isPengurus())
<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>

View File

@@ -0,0 +1,82 @@
@extends('layouts.app')
@section('tajuk', 'Daftar Pengguna Baharu')
@section('saiz-container', 'container-kecil')
@section('kandungan')
<div class="kad">
<h1>Daftar Pengguna Baharu</h1>
@unless (auth()->user()->isAdmin())
<p class="teks-kecil">Pengguna yang didaftarkan akan ditetapkan untuk DUN anda sendiri.</p>
@endunless
@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) {
'admin' => 'Admin',
'pengurus' => 'Pengurus DUN',
'kaunter' => 'Kaunter',
default => $kod,
} }}
</option>
@endforeach
</select>
@if (auth()->user()->isAdmin())
<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>
@endif
<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>
@if (auth()->user()->isAdmin())
<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 !== '' && role.value !== 'admin';
barisDun.style.display = perluDun ? '' : 'none';
dunSelect.required = perluDun;
}
role.addEventListener('change', kemaskini);
kemaskini();
})();
</script>
@endif
@endsection

View File

@@ -0,0 +1,48 @@
@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) {
'admin' => 'Admin',
'pengurus' => 'Pengurus DUN',
'kaunter' => 'Kaunter',
default => $p->role,
} }}
</td>
<td>{{ $p->dun ? $p->dun->code . ' — ' . $p->dun->nama : '-' }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
@endsection

View File

@@ -13,13 +13,17 @@
<div class="stat-grid">
<div class="stat">
<div class="nilai">{{ $hadir->count() + $tidakHadir->count() }}</div>
<div class="nilai">{{ $hadir->count() + $hadirSesiLain->count() + $tidakHadir->count() }}</div>
<div class="label">Jumlah Petugas</div>
</div>
<div class="stat">
<div class="nilai" style="color:var(--hijau)">{{ $hadir->count() }}</div>
<div class="label">Hadir</div>
</div>
<div class="stat">
<div class="nilai" style="color:var(--kuning)">{{ $hadirSesiLain->count() }}</div>
<div class="label">Pernah Hadir (Sesi Lain)</div>
</div>
<div class="stat">
<div class="nilai" style="color:var(--merah)">{{ $tidakHadir->count() }}</div>
<div class="label">Tidak / Belum Hadir</div>
@@ -81,6 +85,55 @@
</div>
</div>
{{-- ============= PERNAH HADIR (SESI LAIN) ============= --}}
<div class="kad jadual-kad" data-alat data-per-page="10" style="padding:0;">
<div class="jadual-kepala">
<h2 class="tajuk-jadual kuning">PERNAH HADIR - SESI LAIN ({{ $hadirSesiLain->count() }})</h2>
<input type="text" class="cari-input tiada-cetak" data-cari
placeholder="Cari nama, telefon, pusat mengundi…" aria-label="Cari senarai pernah hadir sesi lain">
</div>
<div class="jadual-bekas">
<table class="jadual jadual-responsif">
<thead>
<tr>
<th>Nama</th>
<th>No. Telefon</th>
<th>Emel</th>
<th>Pusat Mengundi</th>
<th>Saluran</th>
<th>Jawatan</th>
<th>Sesi Dihadiri</th>
<th>Tarikh Hadir</th>
</tr>
</thead>
<tbody>
@forelse ($hadirSesiLain as $p)
@php
$penempatan = $p->butiranPenempatan();
$rekod = $kehadiranSesiLain->get($p->id);
@endphp
<tr class="baris-data">
<td data-label="Nama">{{ $p->nama_ic }}</td>
<td data-label="No. Telefon">{{ $p->no_telefon ?? '-' }}</td>
<td data-label="Emel">{{ $p->emel ?? '-' }}</td>
<td data-label="Pusat Mengundi">{{ $penempatan['pusat_mengundi'] }}</td>
<td data-label="Saluran">{{ $penempatan['saluran'] }}</td>
<td data-label="Jawatan">{{ $penempatan['jawatan'] }}</td>
<td data-label="Sesi Dihadiri">{{ $rekod?->sesi?->nama }}</td>
<td data-label="Tarikh Hadir">{{ $rekod?->hadir_at?->format('d/m/Y') }}</td>
</tr>
@empty
<tr class="baris-tiada"><td colspan="8" class="teks-tengah">Tiada petugas dalam kumpulan ini.</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="jadual-kaki tiada-cetak">
<span class="teks-kecil" data-info></span>
<div class="paging" data-pager></div>
</div>
</div>
{{-- ================ TIDAK / BELUM HADIR ================ --}}
<div class="kad jadual-kad" data-alat data-per-page="10" style="padding:0;">
<div class="jadual-kepala">

View File

@@ -0,0 +1,52 @@
@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) {
'admin' => 'Admin',
'pengurus' => 'Pengurus DUN',
'kaunter' => 'Kaunter',
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

View File

@@ -6,10 +6,12 @@
<div class="kad">
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:0.5rem;">
<h1 class="mb-0">Sesi Taklimat</h1>
@unless (auth()->user()->isKaunter())
<div class="baris-tindakan">
<a href="{{ route('sesi.eksport') }}" class="btn btn-luar"> Export Kehadiran</a>
<a href="{{ route('sesi.create') }}" class="btn">+ Cipta Sesi</a>
</div>
@endunless
</div>
</div>

View File

@@ -32,12 +32,14 @@
<a href="{{ route('sesi.qr', $sesi) }}" class="btn">Papar / Cetak QR Code</a>
<a href="{{ route('kaunter.index', $sesi) }}" class="btn btn-hijau">Kaunter Borang</a>
<a href="{{ route('pengurus.senarai', $sesi) }}" class="btn btn-luar">Senarai Kehadiran</a>
@unless (auth()->user()->isKaunter())
<form method="POST" action="{{ route('sesi.toggle', $sesi) }}" style="margin:0">
@csrf
<button type="submit" class="btn {{ $sesi->is_active ? 'btn-merah' : 'btn-hijau' }}">
{{ $sesi->is_active ? 'Tutup Pendaftaran' : 'Buka Pendaftaran' }}
</button>
</form>
@endunless
</div>
</div>

View File

@@ -4,7 +4,10 @@ use App\Http\Controllers\AuthController;
use App\Http\Controllers\CheckinController;
use App\Http\Controllers\EksportKehadiranController;
use App\Http\Controllers\KaunterController;
use App\Http\Controllers\PasswordResetController;
use App\Http\Controllers\PengurusController;
use App\Http\Controllers\PenggunaController;
use App\Http\Controllers\ProfilController;
use App\Http\Controllers\SesiTaklimatController;
use Illuminate\Support\Facades\Route;
@@ -19,20 +22,39 @@ Route::get('/login', [AuthController::class, 'showLogin'])->name('login');
Route::post('/login', [AuthController::class, 'login'])->name('login.attempt');
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
// Lupa kata laluan
Route::get('/lupa-katalaluan', [PasswordResetController::class, 'showLinkRequestForm'])->name('password.request');
Route::post('/lupa-katalaluan', [PasswordResetController::class, 'sendResetLink'])->name('password.email');
Route::get('/set-kata-laluan/{token}', [PasswordResetController::class, 'showResetForm'])->name('password.reset');
Route::post('/set-kata-laluan', [PasswordResetController::class, 'reset'])->name('password.update');
Route::middleware(['auth'])->group(function () {
// Pengurusan sesi taklimat (admin & pengurus dun)
Route::get('/sesi', [SesiTaklimatController::class, 'index'])->name('sesi.index');
// Profil sendiri: tukar kata laluan (semua peranan)
Route::get('/profil', [ProfilController::class, 'edit'])->name('profil.edit');
Route::put('/profil/kata-laluan', [ProfilController::class, 'updatePassword'])->name('profil.kata-laluan');
Route::middleware(['role:admin,pengurus'])->group(function () {
// Pengurusan sesi taklimat (cipta/tutup/eksport) — bukan kaunter
Route::get('/sesi/eksport-kehadiran', EksportKehadiranController::class)->name('sesi.eksport');
Route::get('/sesi/cipta', [SesiTaklimatController::class, 'create'])->name('sesi.create');
Route::post('/sesi', [SesiTaklimatController::class, 'store'])->name('sesi.store');
Route::post('/sesi/{sesi}/toggle', [SesiTaklimatController::class, 'toggle'])->name('sesi.toggle');
// Pengurusan pengguna: admin daftar sesiapa, pengurus daftar kaunter/pengurus DUN sendiri
Route::get('/pengguna', [PenggunaController::class, 'index'])->name('pengguna.index');
Route::get('/pengguna/cipta', [PenggunaController::class, 'create'])->name('pengguna.create');
Route::post('/pengguna', [PenggunaController::class, 'store'])->name('pengguna.store');
});
Route::middleware(['role:admin,pengurus,kaunter'])->group(function () {
// Lihat sesi & kehadiran (admin, pengurus dun, kaunter — dibatasi DUN sendiri kecuali admin)
Route::get('/sesi', [SesiTaklimatController::class, 'index'])->name('sesi.index');
Route::get('/sesi/{sesi}', [SesiTaklimatController::class, 'show'])->name('sesi.show');
Route::get('/sesi/{sesi}/qr', [SesiTaklimatController::class, 'qr'])->name('sesi.qr');
Route::post('/sesi/{sesi}/toggle', [SesiTaklimatController::class, 'toggle'])->name('sesi.toggle');
Route::get('/sesi/{sesi}/senarai', [PengurusController::class, 'senarai'])->name('pengurus.senarai');
// Kaunter: rekod nombor siri & serah borang
Route::get('/sesi/{sesi}/kaunter', [KaunterController::class, 'index'])->name('kaunter.index');
Route::post('/sesi/{sesi}/kaunter/{kehadiran}/serah', [KaunterController::class, 'serahBorang'])->name('kaunter.serah');
// Pengurus dun: senarai hadir / tidak hadir
Route::get('/sesi/{sesi}/senarai', [PengurusController::class, 'senarai'])->name('pengurus.senarai');
});
});