From 322687c1d179a95f969fcac7de8b1a5731397709 Mon Sep 17 00:00:00 2001 From: Saufi Date: Sat, 11 Jul 2026 03:55:20 +0800 Subject: [PATCH] Flag saluran bermasalah + pautan Call/WhatsApp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Halaman baharu "Saluran Bermasalah": senarai semua saluran dalam DUN (dengan KTM & telefonnya), tanda/buang tanda bermasalah berserta catatan; admin DUN terhad ke DUN sendiri, superadmin semua DUN - Dashboard: baris saluran bermasalah diserlahkan kuning berserta lencana BERMASALAH dan catatan - No. telefon KTM kini ada butang "📞 Call" (tel:) dan "WhatsApp" (wa.me, dinormalkan ke format 60xxxxxxxxx) di dashboard dan halaman saluran bermasalah Co-Authored-By: Claude Fable 5 --- app/Http/Controllers/DashboardController.php | 7 + .../SaluranBermasalahController.php | 123 +++++++++++++++++ app/Models/SaluranBermasalah.php | 36 +++++ app/Models/Sppm/PetugasPermohonan.php | 30 +++++ app/Models/Sppm/Saluran.php | 12 ++ ...000003_create_saluran_bermasalah_table.php | 31 +++++ public/css/app.css | 10 ++ resources/views/dashboard.blade.php | 23 +++- resources/views/layouts/app.blade.php | 1 + resources/views/saluran/index.blade.php | 124 ++++++++++++++++++ routes/web.php | 6 + 11 files changed, 400 insertions(+), 3 deletions(-) create mode 100644 app/Http/Controllers/SaluranBermasalahController.php create mode 100644 app/Models/SaluranBermasalah.php create mode 100644 database/migrations/2026_07_11_000003_create_saluran_bermasalah_table.php create mode 100644 resources/views/saluran/index.blade.php diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 77d471a..3118736 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Models\KehadiranKtm; +use App\Models\SaluranBermasalah; use App\Models\Sppm\Dun; use App\Models\Sppm\PetugasAssignment; use App\Models\Sppm\PusatMengundi; @@ -46,6 +47,11 @@ class DashboardController extends Controller ->get() ->keyBy('assignment_id'); + // Saluran yang ditanda bermasalah (untuk highlight kuning) + $bermasalah = SaluranBermasalah::whereIn('saluran_id', $assignments->pluck('saluran_id')->filter()) + ->get() + ->keyBy('saluran_id'); + // Tapis ikut status selepas kehadiran diketahui if (in_array($request->input('status'), ['hadir', 'belum'], true)) { $mahuHadir = $request->input('status') === 'hadir'; @@ -64,6 +70,7 @@ class DashboardController extends Controller return view('dashboard', [ 'assignments' => $assignments, 'kehadiran' => $kehadiran, + 'bermasalah' => $bermasalah, 'senaraiPusat' => $senaraiPusat, 'senaraiDun' => $senaraiDun, 'dunId' => $dunId, diff --git a/app/Http/Controllers/SaluranBermasalahController.php b/app/Http/Controllers/SaluranBermasalahController.php new file mode 100644 index 0000000..6288202 --- /dev/null +++ b/app/Http/Controllers/SaluranBermasalahController.php @@ -0,0 +1,123 @@ +user(); + $dunId = $user->isSuperadmin() + ? ($request->filled('dun_id') ? $request->integer('dun_id') : null) + : (int) $user->dun_id; + + $senaraiDun = $user->isSuperadmin() + ? Dun::where('is_active', true)->orderBy('code')->get() + : collect(); + + $senaraiSaluran = Saluran::whereHas('pusatMengundi', function ($q) use ($dunId) { + if ($dunId) { + $q->where('dun_id', $dunId); + } + }) + ->when($request->filled('pusat_id'), fn ($q) => $q->where('pusat_mengundi_id', $request->integer('pusat_id'))) + ->with('pusatMengundi') + ->get() + ->sortBy([ + fn ($a, $b) => strcmp($a->pusatMengundi?->nama ?? '', $b->pusatMengundi?->nama ?? ''), + fn ($a, $b) => ($a->nombor_saluran ?? 0) <=> ($b->nombor_saluran ?? 0), + ]) + ->values(); + + // KTM bagi setiap saluran (untuk paparan nama & telefon) + $ktm = PetugasAssignment::ktm() + ->whereIn('saluran_id', $senaraiSaluran->pluck('id')) + ->with('permohonan') + ->get() + ->keyBy('saluran_id'); + + $bermasalah = SaluranBermasalah::whereIn('saluran_id', $senaraiSaluran->pluck('id')) + ->with('dilaporOleh') + ->get() + ->keyBy('saluran_id'); + + $senaraiPusat = PusatMengundi::when($dunId, fn ($q) => $q->where('dun_id', $dunId)) + ->orderBy('nama') + ->get(['id', 'nama']); + + return view('saluran.index', [ + 'senaraiSaluran' => $senaraiSaluran, + 'ktm' => $ktm, + 'bermasalah' => $bermasalah, + 'senaraiPusat' => $senaraiPusat, + 'senaraiDun' => $senaraiDun, + 'dunId' => $dunId, + ]); + } + + /** + * Tanda saluran sebagai bermasalah (dengan catatan pilihan). + */ + public function store(Request $request) + { + $data = $request->validate([ + 'saluran_id' => ['required', 'integer'], + 'catatan' => ['nullable', 'string', 'max:500'], + ]); + + $saluran = Saluran::with('pusatMengundi')->findOrFail($data['saluran_id']); + $this->pastikanDunDibenarkan($request, $saluran); + + SaluranBermasalah::updateOrCreate( + ['saluran_id' => $saluran->id], + [ + 'pusat_mengundi_id' => $saluran->pusat_mengundi_id, + 'dun_id' => $saluran->pusatMengundi->dun_id, + 'catatan' => $data['catatan'] ?? null, + 'dilapor_oleh_user_id' => $request->user()->id, + ], + ); + + return back()->with('status', 'Saluran '.$saluran->nombor_saluran.' ('.$saluran->pusatMengundi->nama.') ditanda BERMASALAH.'); + } + + /** + * Buang tanda bermasalah pada saluran. + */ + public function destroy(Request $request, int $saluranId) + { + $saluran = Saluran::with('pusatMengundi')->findOrFail($saluranId); + $this->pastikanDunDibenarkan($request, $saluran); + + SaluranBermasalah::where('saluran_id', $saluran->id)->delete(); + + return back()->with('status', 'Tanda bermasalah pada Saluran '.$saluran->nombor_saluran.' ('.$saluran->pusatMengundi->nama.') telah dibuang.'); + } + + /** + * Admin DUN hanya boleh urus saluran dalam DUN sendiri. + */ + private function pastikanDunDibenarkan(Request $request, Saluran $saluran): void + { + $user = $request->user(); + + if ($user->isSuperadmin()) { + return; + } + + if ((int) $saluran->pusatMengundi?->dun_id !== (int) $user->dun_id) { + abort(403, 'Saluran ini bukan dalam DUN anda.'); + } + } +} diff --git a/app/Models/SaluranBermasalah.php b/app/Models/SaluranBermasalah.php new file mode 100644 index 0000000..6e1bd3d --- /dev/null +++ b/app/Models/SaluranBermasalah.php @@ -0,0 +1,36 @@ +belongsTo(Saluran::class, 'saluran_id'); + } + + public function pusatMengundi(): BelongsTo + { + return $this->belongsTo(PusatMengundi::class, 'pusat_mengundi_id'); + } + + public function dilaporOleh(): BelongsTo + { + return $this->belongsTo(User::class, 'dilapor_oleh_user_id'); + } +} diff --git a/app/Models/Sppm/PetugasPermohonan.php b/app/Models/Sppm/PetugasPermohonan.php index 3dfd246..72427ee 100644 --- a/app/Models/Sppm/PetugasPermohonan.php +++ b/app/Models/Sppm/PetugasPermohonan.php @@ -30,6 +30,36 @@ class PetugasPermohonan extends Model return $this->belongsTo(Dun::class, 'dun_id'); } + /** + * No. telefon dalam digit sahaja (untuk pautan tel:). + */ + public function telefonDigits(): ?string + { + $digit = preg_replace('/\D/', '', (string) $this->no_telefon); + + return $digit !== '' ? $digit : null; + } + + /** + * Pautan WhatsApp (wa.me) — normalkan ke format antarabangsa Malaysia. + */ + public function whatsappUrl(): ?string + { + $digit = $this->telefonDigits(); + + if ($digit === null) { + return null; + } + + if (str_starts_with($digit, '0')) { + $digit = '6'.$digit; + } elseif (! str_starts_with($digit, '60')) { + $digit = '60'.$digit; + } + + return 'https://wa.me/'.$digit; + } + public function assignments(): HasMany { return $this->hasMany(PetugasAssignment::class, 'permohonan_id'); diff --git a/app/Models/Sppm/Saluran.php b/app/Models/Sppm/Saluran.php index 3d0b3ca..4616827 100644 --- a/app/Models/Sppm/Saluran.php +++ b/app/Models/Sppm/Saluran.php @@ -3,10 +3,22 @@ namespace App\Models\Sppm; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; class Saluran extends Model { protected $connection = 'sppm'; protected $table = 'saluran'; protected $guarded = []; + + public function pusatMengundi(): BelongsTo + { + return $this->belongsTo(PusatMengundi::class, 'pusat_mengundi_id'); + } + + public function assignments(): HasMany + { + return $this->hasMany(PetugasAssignment::class, 'saluran_id'); + } } diff --git a/database/migrations/2026_07_11_000003_create_saluran_bermasalah_table.php b/database/migrations/2026_07_11_000003_create_saluran_bermasalah_table.php new file mode 100644 index 0000000..abd8685 --- /dev/null +++ b/database/migrations/2026_07_11_000003_create_saluran_bermasalah_table.php @@ -0,0 +1,31 @@ +id(); + // Rujukan ke pangkalan data sppm (tiada FK sebenar merentas DB) + $table->unsignedBigInteger('saluran_id')->unique(); + $table->unsignedBigInteger('pusat_mengundi_id')->index(); + $table->unsignedBigInteger('dun_id')->index(); + $table->string('catatan', 500)->nullable(); + $table->foreignId('dilapor_oleh_user_id')->constrained('users'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('saluran_bermasalah'); + } +}; diff --git a/public/css/app.css b/public/css/app.css index fa90a64..45c71c1 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -192,6 +192,12 @@ table.jadual thead th { table.jadual tbody tr:nth-child(even) { background: #f8f9fb; } +/* Saluran ditanda bermasalah: serlahkan kuning */ +table.jadual tbody tr.baris-bermasalah, +table.jadual tbody tr.baris-bermasalah:nth-child(even) { + background: #fff3cd; +} + .lencana { display: inline-block; padding: 0.15rem 0.6rem; @@ -358,6 +364,10 @@ table.jadual tbody tr:nth-child(even) { background: #f8f9fb; } padding: 0.3rem 0.6rem; background: #fff !important; } + .jadual-responsif tbody tr.baris-bermasalah { + background: #fff3cd !important; + border-color: var(--kuning); + } .jadual-responsif tbody td { display: flex; justify-content: space-between; diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 2f91a2a..26a5522 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -96,14 +96,31 @@ @php $rekod = $kehadiran->get($a->id); $p = $a->permohonan; + $flag = $a->saluran_id ? $bermasalah->get($a->saluran_id) : null; @endphp - + {{ $i + 1 }} {{ $a->pusatMengundi?->nama ?? '-' }} - {{ $a->saluran ? 'Saluran ' . $a->saluran->nombor_saluran : '-' }} + + {{ $a->saluran ? 'Saluran ' . $a->saluran->nombor_saluran : '-' }} + @if ($flag) +
catatan) title="{{ $flag->catatan }}" @endif>BERMASALAH
+ @if ($flag->catatan) +
{{ $flag->catatan }}
+ @endif + @endif + {{ $p?->nama_ic ?? '-' }} {{ $p?->no_kp ?? '-' }} - {{ $p?->no_telefon ?? '-' }} + + {{ $p?->no_telefon ?? '-' }} + @if ($p?->telefonDigits()) + + @endif + {{ $a->jawatan?->nama ?? '-' }} @if ($rekod) diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 80c227d..b786274 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -22,6 +22,7 @@ @endphp
Dashboard + Saluran Bermasalah @if (auth()->user()->isSuperadmin()) Pengguna @endif diff --git a/resources/views/saluran/index.blade.php b/resources/views/saluran/index.blade.php new file mode 100644 index 0000000..ac138d0 --- /dev/null +++ b/resources/views/saluran/index.blade.php @@ -0,0 +1,124 @@ +@extends('layouts.app') + +@section('tajuk', 'Saluran Bermasalah') + +@section('kandungan') +
+
+

Saluran Bermasalah

+ {{ $bermasalah->count() }} saluran bermasalah +
+

Tanda saluran yang bermasalah supaya diserlahkan kuning dalam dashboard kehadiran.

+
+ + @if ($errors->any()) +
{{ $errors->first() }}
+ @endif + +
+
+
+ @if (auth()->user()->isSuperadmin()) +
+ + +
+ @endif + +
+ + +
+
+
+
+ +
+
+ + + + + + + + + + + + + + @forelse ($senaraiSaluran as $i => $s) + @php + $flag = $bermasalah->get($s->id); + $p = $ktm->get($s->id)?->permohonan; + @endphp + + + + + + + + + + @empty + + + + @endforelse + +
BilPusat MengundiSaluranKTMStatusCatatanTindakan
{{ $i + 1 }}{{ $s->pusatMengundi?->nama ?? '-' }}Saluran {{ $s->nombor_saluran }} + {{ $p?->nama_ic ?? '-' }} + @if ($p?->telefonDigits()) + + @endif + + @if ($flag) + BERMASALAH +
{{ $flag->created_at->format('d/m/Y h:i A') }}
oleh {{ $flag->dilaporOleh?->name }}
+ @else + OK + @endif +
{{ $flag?->catatan ?? '-' }} + @if ($flag) +
+ @csrf + @method('DELETE') + +
+ @else +
+ @csrf + + + +
+ @endif +
Tiada saluran dijumpai.
+
+ +
+ {{ $senaraiSaluran->count() }} saluran dipaparkan +
+
+@endsection diff --git a/routes/web.php b/routes/web.php index cd14bf9..1f17406 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,6 +7,7 @@ use App\Http\Controllers\KehadiranController; use App\Http\Controllers\PasswordResetController; use App\Http\Controllers\PenggunaController; use App\Http\Controllers\ProfilController; +use App\Http\Controllers\SaluranBermasalahController; use Illuminate\Support\Facades\Route; Route::redirect('/', '/login'); @@ -37,6 +38,11 @@ Route::middleware(['auth'])->group(function () { // Muat turun senarai kehadiran (Excel) Route::get('/eksport-kehadiran', EksportKehadiranController::class)->name('kehadiran.eksport'); + + // Flag saluran bermasalah + Route::get('/saluran-bermasalah', [SaluranBermasalahController::class, 'index'])->name('saluran.index'); + Route::post('/saluran-bermasalah', [SaluranBermasalahController::class, 'store'])->name('saluran.store'); + Route::delete('/saluran-bermasalah/{saluran}', [SaluranBermasalahController::class, 'destroy'])->name('saluran.destroy'); }); Route::middleware(['role:superadmin'])->group(function () {