Compare commits

...

2 Commits

Author SHA1 Message Date
Saufi
57c20353c7 Carian langsung (AJAX) pada dashboard
Taip nama/no. KP terus mencari tanpa klik butang (debounce 300ms).
Tukar pusat mengundi/status juga menapis serta-merta. Jadual dan
statistik digantikan melalui fetch; URL dikemas kini (replaceState)
supaya refresh dan eksport kekal konsisten. Butang Cari kekal sebagai
fallback tanpa JavaScript; pilihan DUN (superadmin) kekal muat semula
penuh kerana senarai pusat mengundi berubah.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 03:21:24 +08:00
Saufi
cd5cfb49a9 Superadmin boleh padam pengguna
- Butang Padam dalam senarai pengguna (dengan pengesahan)
- Tidak boleh padam akaun sendiri — dipapar '(akaun anda)'
- Tidak boleh padam pengguna yang telah mencatat rekod kehadiran
  (disokong FK dicatat_oleh_user_id di peringkat DB)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 03:18:07 +08:00
4 changed files with 101 additions and 3 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\KehadiranKtm;
use App\Models\Sppm\Dun; use App\Models\Sppm\Dun;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -65,4 +66,21 @@ class PenggunaController extends Controller
return redirect()->route('pengguna.index')->with('status', 'Pengguna baharu berjaya didaftarkan.'); return redirect()->route('pengguna.index')->with('status', 'Pengguna baharu berjaya didaftarkan.');
} }
public function destroy(Request $request, User $pengguna)
{
if ($pengguna->id === $request->user()->id) {
return back()->withErrors(['pengguna' => 'Anda tidak boleh memadam akaun anda sendiri.']);
}
if (KehadiranKtm::where('dicatat_oleh_user_id', $pengguna->id)->exists()) {
return back()->withErrors([
'pengguna' => "Pengguna {$pengguna->name} tidak boleh dipadam kerana telah mencatat rekod kehadiran.",
]);
}
$pengguna->delete();
return redirect()->route('pengguna.index')->with('status', "Pengguna {$pengguna->name} telah dipadam.");
}
} }

View File

@@ -3,7 +3,7 @@
@section('tajuk', 'Dashboard Kehadiran KTM') @section('tajuk', 'Dashboard Kehadiran KTM')
@section('kandungan') @section('kandungan')
<div class="stat-grid"> <div class="stat-grid" id="stat-grid">
<div class="stat"> <div class="stat">
<div class="nilai">{{ $jumlahSemua }}</div> <div class="nilai">{{ $jumlahSemua }}</div>
<div class="label">Jumlah KTM</div> <div class="label">Jumlah KTM</div>
@@ -19,7 +19,7 @@
</div> </div>
<div class="kad"> <div class="kad">
<form method="GET" action="{{ route('dashboard') }}"> <form method="GET" action="{{ route('dashboard') }}" id="borang-tapis">
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(180px, 1fr)); gap:0 0.75rem;"> <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(180px, 1fr)); gap:0 0.75rem;">
@if (auth()->user()->isSuperadmin()) @if (auth()->user()->isSuperadmin())
<div> <div>
@@ -71,7 +71,7 @@
</form> </form>
</div> </div>
<div class="kad" style="padding:0;"> <div class="kad" style="padding:0;" id="senarai-ktm">
<div class="jadual-kepala"> <div class="jadual-kepala">
<h2 class="tajuk-jadual mb-0">Senarai KTM (Ketua Tempat Mengundi)</h2> <h2 class="tajuk-jadual mb-0">Senarai KTM (Ketua Tempat Mengundi)</h2>
</div> </div>
@@ -145,4 +145,66 @@
<span class="teks-kecil">{{ $assignments->count() }} rekod dipaparkan</span> <span class="teks-kecil">{{ $assignments->count() }} rekod dipaparkan</span>
</div> </div>
</div> </div>
<script>
(function () {
var borang = document.getElementById('borang-tapis');
var inputCari = document.getElementById('q');
var pilihPusat = document.getElementById('pusat_id');
var pilihStatus = document.getElementById('status');
var timer = null;
var fetchSemasa = null;
// Cari terus semasa menaip (AJAX) — butang Cari kekal sebagai fallback tanpa JS.
function cariSekarang() {
var params = new URLSearchParams(new FormData(borang));
var url = borang.action + (params.toString() ? '?' + params.toString() : '');
if (fetchSemasa) {
fetchSemasa.abort();
}
fetchSemasa = new AbortController();
document.getElementById('senarai-ktm').style.opacity = '0.5';
fetch(url, {
headers: { 'X-Requested-With': 'XMLHttpRequest' },
signal: fetchSemasa.signal,
})
.then(function (r) { return r.text(); })
.then(function (html) {
var doc = new DOMParser().parseFromString(html, 'text/html');
['stat-grid', 'senarai-ktm'].forEach(function (id) {
var baru = doc.getElementById(id);
if (baru) {
document.getElementById(id).replaceWith(baru);
}
});
// Kekalkan URL supaya refresh/eksport/set-semula konsisten
window.history.replaceState(null, '', url);
})
.catch(function (e) {
if (e.name !== 'AbortError') {
borang.submit(); // fallback: muat semula penuh
}
});
}
function cariLewat() {
clearTimeout(timer);
timer = setTimeout(cariSekarang, 300);
}
inputCari.addEventListener('input', cariLewat);
pilihPusat.addEventListener('change', cariSekarang);
pilihStatus.addEventListener('change', cariSekarang);
// Elak penghantaran borang penuh bila tekan Enter dalam kotak carian
borang.addEventListener('submit', function (e) {
e.preventDefault();
clearTimeout(timer);
cariSekarang();
});
})();
</script>
@endsection @endsection

View File

@@ -10,6 +10,10 @@
</div> </div>
</div> </div>
@if ($errors->any())
<div class="makluman makluman-ralat">{{ $errors->first() }}</div>
@endif
@if ($pengguna->isEmpty()) @if ($pengguna->isEmpty())
<div class="kad teks-tengah"> <div class="kad teks-tengah">
<p>Tiada pengguna lagi.</p> <p>Tiada pengguna lagi.</p>
@@ -23,6 +27,7 @@
<th>E-mel</th> <th>E-mel</th>
<th>Peranan</th> <th>Peranan</th>
<th>DUN</th> <th>DUN</th>
<th>Tindakan</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -38,6 +43,18 @@
} }} } }}
</td> </td>
<td>{{ $p->dun ? $p->dun->code . ' — ' . $p->dun->nama : '-' }}</td> <td>{{ $p->dun ? $p->dun->code . ' — ' . $p->dun->nama : '-' }}</td>
<td>
@if ($p->id === auth()->id())
<span class="teks-kecil">(akaun anda)</span>
@else
<form method="POST" action="{{ route('pengguna.destroy', $p) }}" style="margin:0"
onsubmit="return confirm('Padam pengguna {{ $p->name }}? Tindakan ini tidak boleh dikembalikan.');">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-kecil btn-merah">Padam</button>
</form>
@endif
</td>
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

@@ -44,5 +44,6 @@ Route::middleware(['auth'])->group(function () {
Route::get('/pengguna', [PenggunaController::class, 'index'])->name('pengguna.index'); Route::get('/pengguna', [PenggunaController::class, 'index'])->name('pengguna.index');
Route::get('/pengguna/cipta', [PenggunaController::class, 'create'])->name('pengguna.create'); Route::get('/pengguna/cipta', [PenggunaController::class, 'create'])->name('pengguna.create');
Route::post('/pengguna', [PenggunaController::class, 'store'])->name('pengguna.store'); Route::post('/pengguna', [PenggunaController::class, 'store'])->name('pengguna.store');
Route::delete('/pengguna/{pengguna}', [PenggunaController::class, 'destroy'])->name('pengguna.destroy');
}); });
}); });