first
This commit is contained in:
36
resources/views/admin/audit/index.blade.php
Normal file
36
resources/views/admin/audit/index.blade.php
Normal file
@@ -0,0 +1,36 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Audit Trail')
|
||||
|
||||
@section('content')
|
||||
<form class="mb-3" method="GET">
|
||||
<div class="input-group" style="max-width:360px;">
|
||||
<select name="action" class="form-select">
|
||||
<option value="">Semua Tindakan</option>
|
||||
@foreach($actions as $a)<option value="{{ $a }}" @selected($action===$a)>{{ $a }}</option>@endforeach
|
||||
</select>
|
||||
<button class="btn btn-teal"><i class="bi bi-funnel"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card"><div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-koipb table-hover align-middle">
|
||||
<thead><tr><th>Masa</th><th>Pengguna</th><th>Tindakan</th><th>Keterangan</th><th>IP</th></tr></thead>
|
||||
<tbody>
|
||||
@forelse($logs as $log)
|
||||
<tr>
|
||||
<td><small>{{ $log->created_at->format('d/m/Y H:i:s') }}</small></td>
|
||||
<td>{{ $log->user?->name ?? 'Sistem' }}</td>
|
||||
<td><span class="badge bg-dark2">{{ $log->action }}</span></td>
|
||||
<td>{{ $log->description }}</td>
|
||||
<td><small class="text-muted">{{ $log->ip_address ?? '-' }}</small></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="5" class="text-center text-muted py-4">Tiada rekod audit.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ $logs->links() }}
|
||||
</div></div>
|
||||
@endsection
|
||||
40
resources/views/admin/members/edit.blade.php
Normal file
40
resources/views/admin/members/edit.blade.php
Normal file
@@ -0,0 +1,40 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Kemaskini Anggota')
|
||||
|
||||
@section('content')
|
||||
<div class="row justify-content-center"><div class="col-lg-8">
|
||||
<div class="card"><div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.members.update', $member) }}">
|
||||
@csrf @method('PUT')
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><label class="form-label">Nama *</label>
|
||||
<input name="nama" class="form-control @error('nama') is-invalid @enderror" value="{{ old('nama', $member->nama) }}" required>
|
||||
@error('nama')<div class="invalid-feedback">{{ $message }}</div>@enderror</div>
|
||||
<div class="col-md-6"><label class="form-label">No Anggota</label>
|
||||
<input name="no_anggota" class="form-control" value="{{ old('no_anggota', $member->no_anggota) }}"></div>
|
||||
<div class="col-md-6"><label class="form-label">No Pekerja</label>
|
||||
<input name="no_pekerja" class="form-control @error('no_pekerja') is-invalid @enderror" value="{{ old('no_pekerja', $member->no_pekerja) }}">
|
||||
@error('no_pekerja')<div class="invalid-feedback">{{ $message }}</div>@enderror</div>
|
||||
<div class="col-md-6"><label class="form-label">No KP</label>
|
||||
<input name="no_kp" class="form-control @error('no_kp') is-invalid @enderror" value="{{ old('no_kp', $member->no_kp) }}">
|
||||
@error('no_kp')<div class="invalid-feedback">{{ $message }}</div>@enderror</div>
|
||||
<div class="col-md-6"><label class="form-label">Jabatan</label>
|
||||
<input name="jabatan" class="form-control" value="{{ old('jabatan', $member->jabatan) }}"></div>
|
||||
<div class="col-md-6"><label class="form-label">Bahagian</label>
|
||||
<input name="bahagian" class="form-control" value="{{ old('bahagian', $member->bahagian) }}"></div>
|
||||
<div class="col-md-6"><label class="form-label">Telefon</label>
|
||||
<input name="telefon" class="form-control" value="{{ old('telefon', $member->telefon) }}"></div>
|
||||
<div class="col-md-6"><label class="form-label">Status</label>
|
||||
<select name="status_aktif" class="form-select">
|
||||
<option value="1" @selected($member->status_aktif)>Aktif</option>
|
||||
<option value="0" @selected(!$member->status_aktif)>Tidak Aktif</option>
|
||||
</select></div>
|
||||
</div>
|
||||
<div class="mt-4 d-flex gap-2">
|
||||
<button class="btn btn-teal"><i class="bi bi-save"></i> Simpan</button>
|
||||
<a href="{{ route('admin.members.index') }}" class="btn btn-light">Kembali</a>
|
||||
</div>
|
||||
</form>
|
||||
</div></div>
|
||||
</div></div>
|
||||
@endsection
|
||||
55
resources/views/admin/members/import.blade.php
Normal file
55
resources/views/admin/members/import.blade.php
Normal file
@@ -0,0 +1,55 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Import Anggota')
|
||||
|
||||
@section('content')
|
||||
@php $result = session('import_result') ? \App\Models\ImportLog::find(session('import_result')) : null; @endphp
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-5">
|
||||
<div class="card"><div class="card-body">
|
||||
<h5 class="fw-bold"><i class="bi bi-upload text-teal"></i> Muat Naik Fail</h5>
|
||||
<p class="text-muted small">Format CSV atau XLSX. Lajur: <code>no_anggota, no_pekerja, no_kp, nama, jabatan, bahagian, telefon, status_aktif</code>. Hanya <b>nama</b> wajib.</p>
|
||||
<form method="POST" action="{{ route('admin.members.import.store') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<input type="file" name="file" class="form-control mb-2 @error('file') is-invalid @enderror" accept=".csv,.xlsx,.xls" required>
|
||||
@error('file')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||
<button class="btn btn-teal w-100"><i class="bi bi-cloud-upload"></i> Import Sekarang</button>
|
||||
</form>
|
||||
<a href="{{ asset('samples/contoh-anggota.csv') }}" class="btn btn-sm btn-link mt-2"><i class="bi bi-download"></i> Muat turun templat contoh</a>
|
||||
</div></div>
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
@if($result)
|
||||
<div class="card mb-3 border-start border-4 border-success"><div class="card-body">
|
||||
<h6 class="fw-bold">Ringkasan Import: {{ $result->filename }}</h6>
|
||||
<div class="row text-center g-2 my-2">
|
||||
<div class="col"><div class="stat-card bg-teal py-2"><div class="value fs-4">{{ $result->total_rows }}</div><div class="label">Jumlah Baris</div></div></div>
|
||||
<div class="col"><div class="stat-card bg-green py-2"><div class="value fs-4">{{ $result->success_count }}</div><div class="label">Berjaya</div></div></div>
|
||||
<div class="col"><div class="stat-card bg-gold py-2"><div class="value fs-4">{{ $result->duplicate_count }}</div><div class="label">Duplicate</div></div></div>
|
||||
<div class="col"><div class="stat-card bg-red py-2"><div class="value fs-4">{{ $result->failed_count }}</div><div class="label">Gagal</div></div></div>
|
||||
</div>
|
||||
@if($result->errors)
|
||||
<details><summary class="text-danger">Lihat {{ count($result->errors) }} isu</summary>
|
||||
<ul class="small mt-2 mb-0">@foreach($result->errors as $e)<li>{{ $e }}</li>@endforeach</ul>
|
||||
</details>
|
||||
@endif
|
||||
</div></div>
|
||||
@endif
|
||||
|
||||
<div class="card"><div class="card-body">
|
||||
<h6 class="fw-bold"><i class="bi bi-clock-history"></i> Log Import Terkini</h6>
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th>Fail</th><th>Berjaya</th><th>Dup</th><th>Gagal</th><th>Oleh</th><th>Masa</th></tr></thead>
|
||||
<tbody>
|
||||
@forelse($logs as $log)
|
||||
<tr><td>{{ $log->filename }}</td><td class="text-success">{{ $log->success_count }}</td>
|
||||
<td class="text-warning">{{ $log->duplicate_count }}</td><td class="text-danger">{{ $log->failed_count }}</td>
|
||||
<td>{{ $log->importedBy?->name ?? '-' }}</td><td><small>{{ $log->created_at->format('d/m H:i') }}</small></td></tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="text-center text-muted">Belum ada import.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
49
resources/views/admin/members/index.blade.php
Normal file
49
resources/views/admin/members/index.blade.php
Normal file
@@ -0,0 +1,49 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Urus Anggota')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
||||
<form class="d-flex gap-2" method="GET" style="max-width:420px;">
|
||||
<input type="text" name="q" value="{{ $q }}" class="form-control" placeholder="Cari nama / no pekerja / KP...">
|
||||
<button class="btn btn-teal"><i class="bi bi-search"></i></button>
|
||||
</form>
|
||||
<a href="{{ route('admin.members.import') }}" class="btn btn-outline-teal"><i class="bi bi-upload"></i> Import Anggota</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-koipb table-hover align-middle">
|
||||
<thead><tr>
|
||||
<th>No Anggota</th><th>Nama</th><th>No Pekerja</th><th>No KP</th><th>Jabatan</th><th>Status</th><th>Hadir</th><th></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@forelse($members as $m)
|
||||
<tr>
|
||||
<td>{{ $m->no_anggota ?? '-' }}</td>
|
||||
<td class="fw-semibold">{{ $m->nama }}
|
||||
@if($m->confirmed_wins) <span class="badge bg-gold"><i class="bi bi-trophy"></i> Menang</span> @endif
|
||||
</td>
|
||||
<td>{{ $m->no_pekerja ?? '-' }}</td>
|
||||
<td><small class="text-muted">{{ $m->maskedKp() ?? '-' }}</small></td>
|
||||
<td>{{ $m->jabatan ?? '-' }}</td>
|
||||
<td>@if($m->status_aktif)<span class="pill pill-disahkan">Aktif</span>@else<span class="pill pill-redraw">Tidak Aktif</span>@endif</td>
|
||||
<td>@if($m->attendance)<span class="badge badge-hadir">Hadir</span>@else<span class="badge badge-belum">Belum</span>@endif</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('admin.members.edit', $m) }}" class="btn btn-sm btn-outline-secondary"><i class="bi bi-pencil"></i></a>
|
||||
<form action="{{ route('admin.members.destroy', $m) }}" method="POST" class="d-inline" onsubmit="return confirm('Padam anggota {{ $m->nama }}?')">
|
||||
@csrf @method('DELETE')
|
||||
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="8" class="text-center text-muted py-4">Tiada anggota. Sila import senarai anggota.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ $members->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
29
resources/views/admin/prizes/edit.blade.php
Normal file
29
resources/views/admin/prizes/edit.blade.php
Normal file
@@ -0,0 +1,29 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Kemaskini Hadiah')
|
||||
|
||||
@section('content')
|
||||
<div class="row justify-content-center"><div class="col-lg-7">
|
||||
<div class="card"><div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.prizes.update', $prize) }}">
|
||||
@csrf @method('PUT')
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8"><label class="form-label">Nama Hadiah *</label>
|
||||
<input name="nama_hadiah" class="form-control @error('nama_hadiah') is-invalid @enderror" value="{{ old('nama_hadiah', $prize->nama_hadiah) }}" required>
|
||||
@error('nama_hadiah')<div class="invalid-feedback">{{ $message }}</div>@enderror</div>
|
||||
<div class="col-md-4"><label class="form-label">Susunan Cabutan *</label>
|
||||
<input type="number" name="draw_order" class="form-control" value="{{ old('draw_order', $prize->draw_order) }}" required></div>
|
||||
<div class="col-md-4"><label class="form-label">Kod</label>
|
||||
<input name="kod_hadiah" class="form-control" value="{{ old('kod_hadiah', $prize->kod_hadiah) }}"></div>
|
||||
<div class="col-md-4"><label class="form-label">Kategori</label>
|
||||
<input name="kategori" class="form-control" value="{{ old('kategori', $prize->kategori) }}"></div>
|
||||
<div class="col-md-4"><label class="form-label">Nilai Anggaran (RM)</label>
|
||||
<input type="number" step="0.01" name="nilai_anggaran" class="form-control" value="{{ old('nilai_anggaran', $prize->nilai_anggaran) }}"></div>
|
||||
</div>
|
||||
<div class="mt-4 d-flex gap-2">
|
||||
<button class="btn btn-teal"><i class="bi bi-save"></i> Simpan</button>
|
||||
<a href="{{ route('admin.prizes.index') }}" class="btn btn-light">Kembali</a>
|
||||
</div>
|
||||
</form>
|
||||
</div></div>
|
||||
</div></div>
|
||||
@endsection
|
||||
51
resources/views/admin/prizes/import.blade.php
Normal file
51
resources/views/admin/prizes/import.blade.php
Normal file
@@ -0,0 +1,51 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Import Hadiah')
|
||||
|
||||
@section('content')
|
||||
@php $result = session('import_result') ? \App\Models\ImportLog::find(session('import_result')) : null; @endphp
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-5">
|
||||
<div class="card"><div class="card-body">
|
||||
<h5 class="fw-bold"><i class="bi bi-upload text-teal"></i> Muat Naik Fail</h5>
|
||||
<p class="text-muted small">Format CSV atau XLSX. Lajur: <code>kod_hadiah, nama_hadiah, kategori, nilai_anggaran, susunan_cabutan, kuantiti</code>.
|
||||
Jika <b>kuantiti > 1</b>, sistem akan jana beberapa item berasingan (cth Hamper #1 … #5).</p>
|
||||
<form method="POST" action="{{ route('admin.prizes.import.store') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<input type="file" name="file" class="form-control mb-2 @error('file') is-invalid @enderror" accept=".csv,.xlsx,.xls" required>
|
||||
@error('file')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||
<button class="btn btn-teal w-100"><i class="bi bi-cloud-upload"></i> Import Sekarang</button>
|
||||
</form>
|
||||
<a href="{{ asset('samples/contoh-hadiah.csv') }}" class="btn btn-sm btn-link mt-2"><i class="bi bi-download"></i> Muat turun templat contoh</a>
|
||||
</div></div>
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
@if($result)
|
||||
<div class="card mb-3 border-start border-4 border-success"><div class="card-body">
|
||||
<h6 class="fw-bold">Ringkasan Import: {{ $result->filename }}</h6>
|
||||
<div class="row text-center g-2 my-2">
|
||||
<div class="col"><div class="stat-card bg-teal py-2"><div class="value fs-4">{{ $result->total_rows }}</div><div class="label">Baris</div></div></div>
|
||||
<div class="col"><div class="stat-card bg-green py-2"><div class="value fs-4">{{ $result->success_count }}</div><div class="label">Item Dijana</div></div></div>
|
||||
<div class="col"><div class="stat-card bg-red py-2"><div class="value fs-4">{{ $result->failed_count }}</div><div class="label">Gagal</div></div></div>
|
||||
</div>
|
||||
@if($result->errors)
|
||||
<details><summary class="text-danger">Lihat {{ count($result->errors) }} isu</summary>
|
||||
<ul class="small mt-2 mb-0">@foreach($result->errors as $e)<li>{{ $e }}</li>@endforeach</ul></details>
|
||||
@endif
|
||||
</div></div>
|
||||
@endif
|
||||
<div class="card"><div class="card-body">
|
||||
<h6 class="fw-bold"><i class="bi bi-clock-history"></i> Log Import Terkini</h6>
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th>Fail</th><th>Item</th><th>Gagal</th><th>Oleh</th><th>Masa</th></tr></thead>
|
||||
<tbody>
|
||||
@forelse($logs as $log)
|
||||
<tr><td>{{ $log->filename }}</td><td class="text-success">{{ $log->success_count }}</td>
|
||||
<td class="text-danger">{{ $log->failed_count }}</td><td>{{ $log->importedBy?->name ?? '-' }}</td>
|
||||
<td><small>{{ $log->created_at->format('d/m H:i') }}</small></td></tr>
|
||||
@empty<tr><td colspan="5" class="text-center text-muted">Belum ada import.</td></tr>@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
42
resources/views/admin/prizes/index.blade.php
Normal file
42
resources/views/admin/prizes/index.blade.php
Normal file
@@ -0,0 +1,42 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Urus Hadiah')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="text-muted">Susunan cabutan mengikut <code>draw_order</code>.</div>
|
||||
<a href="{{ route('admin.prizes.import') }}" class="btn btn-outline-teal"><i class="bi bi-upload"></i> Import Hadiah</a>
|
||||
</div>
|
||||
|
||||
<div class="card"><div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-koipb table-hover align-middle">
|
||||
<thead><tr><th>Susunan</th><th>Kod</th><th>Hadiah</th><th>Kategori</th><th>Nilai (RM)</th><th>Status</th><th>Pemenang</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
@forelse($prizes as $p)
|
||||
@php $pill = ['belum_dicabut'=>'pill-belum','sedang_dicabut'=>'pill-sedang','disahkan'=>'pill-disahkan','redraw_required'=>'pill-redraw'][$p->status]; @endphp
|
||||
<tr>
|
||||
<td><span class="badge bg-dark2">#{{ $p->draw_order }}</span></td>
|
||||
<td>{{ $p->kod_hadiah ?? '-' }}</td>
|
||||
<td class="fw-semibold">{{ $p->nama_hadiah }}</td>
|
||||
<td>{{ $p->kategori ?? '-' }}</td>
|
||||
<td>{{ $p->nilai_anggaran ? number_format($p->nilai_anggaran, 2) : '-' }}</td>
|
||||
<td><span class="pill {{ $pill }}">{{ $p->statusLabel() }}</span>
|
||||
@if($p->redraw_count) <small class="text-muted">({{ $p->redraw_count }}x redraw)</small>@endif</td>
|
||||
<td>{{ $p->drawResults->firstWhere('status','confirmed')?->member?->nama ?? '-' }}</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('admin.prizes.edit', $p) }}" class="btn btn-sm btn-outline-secondary"><i class="bi bi-pencil"></i></a>
|
||||
<form action="{{ route('admin.prizes.destroy', $p) }}" method="POST" class="d-inline" onsubmit="return confirm('Padam hadiah ini?')">
|
||||
@csrf @method('DELETE')
|
||||
<button class="btn btn-sm btn-outline-danger" @disabled($p->status==='disahkan')><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="8" class="text-center text-muted py-4">Tiada hadiah. Sila import senarai hadiah.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ $prizes->links() }}
|
||||
</div></div>
|
||||
@endsection
|
||||
40
resources/views/admin/settings/index.blade.php
Normal file
40
resources/views/admin/settings/index.blade.php
Normal file
@@ -0,0 +1,40 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Tetapan & Reset')
|
||||
|
||||
@section('content')
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<div class="card"><div class="card-body">
|
||||
<h5 class="fw-bold"><i class="bi bi-gear text-teal"></i> Tetapan Sesi Cabutan</h5>
|
||||
<form method="POST" action="{{ route('admin.settings.session') }}">
|
||||
@csrf @method('PUT')
|
||||
<div class="mb-3"><label class="form-label">Nama Sesi / Event</label>
|
||||
<input name="nama" class="form-control" value="{{ old('nama', $session->nama) }}" required></div>
|
||||
<div class="mb-3"><label class="form-label">Peserta dibatalkan (tiada di dewan)</label>
|
||||
<select name="return_cancelled_to_pool" class="form-select">
|
||||
<option value="1" @selected($session->return_cancelled_to_pool)>Masukkan semula ke pool cabutan (default)</option>
|
||||
<option value="0" @selected(!$session->return_cancelled_to_pool)>Keluarkan terus dari pool</option>
|
||||
</select></div>
|
||||
<button class="btn btn-teal"><i class="bi bi-save"></i> Simpan Tetapan</button>
|
||||
</form>
|
||||
</div></div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-danger"><div class="card-body">
|
||||
<h5 class="fw-bold text-danger"><i class="bi bi-exclamation-triangle"></i> Reset Data Event</h5>
|
||||
<p class="text-muted small">Untuk mula event baharu. Tindakan ini tidak boleh diundur.</p>
|
||||
<form method="POST" action="{{ route('admin.settings.reset') }}" id="resetForm">
|
||||
@csrf
|
||||
<div class="mb-3"><label class="form-label">Skop Reset</label>
|
||||
<select name="scope" class="form-select">
|
||||
<option value="draw">Reset cabutan sahaja (kekalkan kehadiran)</option>
|
||||
<option value="all">Reset semua (kehadiran + cabutan)</option>
|
||||
</select></div>
|
||||
<div class="mb-3"><label class="form-label">Taip <code>RESET</code> untuk sahkan</label>
|
||||
<input name="confirm" class="form-control" placeholder="RESET" autocomplete="off" required></div>
|
||||
<button class="btn btn-danger" onclick="return confirm('Anda pasti mahu reset data event? Tindakan ini kekal.')"><i class="bi bi-trash"></i> Reset Sekarang</button>
|
||||
</form>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
69
resources/views/attendance/index.blade.php
Normal file
69
resources/views/attendance/index.blade.php
Normal file
@@ -0,0 +1,69 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Senarai Kehadiran')
|
||||
|
||||
@section('content')
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6 col-md-3"><div class="stat-card bg-green"><div class="label">Sudah Hadir</div><div class="value">{{ $counts['hadir'] }}</div></div></div>
|
||||
<div class="col-6 col-md-3"><div class="stat-card bg-dark2"><div class="label">Belum Hadir</div><div class="value">{{ $counts['belum'] }}</div></div></div>
|
||||
<div class="col-6 col-md-3"><div class="stat-card bg-teal"><div class="label">Jumlah Anggota</div><div class="value">{{ $counts['semua'] }}</div></div></div>
|
||||
<div class="col-6 col-md-3 d-flex align-items-end">
|
||||
<a href="{{ route('reports.export', 'attendance') }}" class="btn btn-outline-teal w-100"><i class="bi bi-file-earmark-excel"></i> Export Excel</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form class="row g-2 mb-3" method="GET">
|
||||
<div class="col-md-4">
|
||||
<input type="text" name="q" value="{{ $q }}" class="form-control" placeholder="Cari nama / no pekerja / KP...">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="status" class="form-select">
|
||||
<option value="hadir" @selected($status==='hadir')>Sudah Hadir</option>
|
||||
<option value="belum" @selected($status==='belum')>Belum Hadir</option>
|
||||
<option value="semua" @selected($status==='semua')>Semua</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="jabatan" class="form-select">
|
||||
<option value="">Semua Jabatan</option>
|
||||
@foreach($jabatanList as $j)
|
||||
<option value="{{ $j }}" @selected($jabatan===$j)>{{ $j }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2"><button class="btn btn-teal w-100"><i class="bi bi-funnel"></i> Tapis</button></div>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-koipb table-hover align-middle">
|
||||
<thead><tr>
|
||||
<th>#</th><th>Nama</th><th>No Pekerja</th><th>No KP</th><th>Jabatan / Bahagian</th><th>Status</th><th>Masa Hadir</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@forelse($members as $m)
|
||||
<tr>
|
||||
<td>{{ $loop->iteration + ($members->currentPage()-1)*$members->perPage() }}</td>
|
||||
<td class="fw-semibold">{{ $m->nama }}</td>
|
||||
<td>{{ $m->no_pekerja ?? '-' }}</td>
|
||||
<td><small class="text-muted">{{ $m->maskedKp() ?? '-' }}</small></td>
|
||||
<td>{{ $m->jabatan ?? '-' }} <span class="text-muted">/ {{ $m->bahagian ?? '-' }}</span></td>
|
||||
<td>
|
||||
@if($m->attendance)
|
||||
<span class="badge badge-hadir">Hadir</span>
|
||||
@else
|
||||
<span class="badge badge-belum">Belum</span>
|
||||
@endif
|
||||
</td>
|
||||
<td><small>{{ $m->attendance?->attended_at?->format('d/m/Y H:i:s') ?? '-' }}</small></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="7" class="text-center text-muted py-4">Tiada rekod.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ $members->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
49
resources/views/auth/login.blade.php
Normal file
49
resources/views/auth/login.blade.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ms">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Log Masuk · Sistem MAT KOIPB</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="{{ asset('css/koipb.css') }}?v=1" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-wrap">
|
||||
<div class="card login-card shadow-lg">
|
||||
<div class="head">
|
||||
<img src="{{ asset('images/logo-koipb.jpg') }}" alt="KOIPB" onerror="this.style.display='none'">
|
||||
<h4 class="fw-bold mt-2 mb-0" style="color:var(--koipb-dark)">Koperasi Iskandar Puteri Berhad</h4>
|
||||
<p class="text-muted small">Sistem Kehadiran & Cabutan Bertuah MAT</p>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger py-2"><i class="bi bi-exclamation-circle"></i> {{ $errors->first() }}</div>
|
||||
@endif
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">E-mel</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-envelope"></i></span>
|
||||
<input type="email" name="email" class="form-control" value="{{ old('email') }}" required autofocus placeholder="admin@koipb.test">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Kata Laluan</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-lock"></i></span>
|
||||
<input type="password" name="password" class="form-control" required placeholder="••••••••">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" name="remember" class="form-check-input" id="remember">
|
||||
<label class="form-check-label" for="remember">Ingat saya</label>
|
||||
</div>
|
||||
<button class="btn btn-teal w-100 py-2 fw-bold"><i class="bi bi-box-arrow-in-right"></i> Log Masuk</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
105
resources/views/counter/index.blade.php
Normal file
105
resources/views/counter/index.blade.php
Normal file
@@ -0,0 +1,105 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Kaunter Kehadiran')
|
||||
|
||||
@section('content')
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-9">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body counter-search">
|
||||
<label class="fw-bold mb-2 fs-5"><i class="bi bi-search text-teal"></i> Cari Anggota</label>
|
||||
<input type="text" id="searchBox" class="form-control" autocomplete="off"
|
||||
placeholder="Taip nama / no. KP / no. pekerja..." autofocus>
|
||||
<div class="form-text">Carian automatik. Boleh guna nama, nombor kad pengenalan atau nombor pekerja MBIP.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="searchStatus" class="text-center text-muted py-2"></div>
|
||||
<div id="resultArea"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
const searchUrl = "{{ route('counter.search') }}";
|
||||
const storeUrl = "{{ route('counter.store') }}";
|
||||
let timer = null, lastTerm = '';
|
||||
|
||||
const $box = $('#searchBox'), $status = $('#searchStatus'), $area = $('#resultArea');
|
||||
|
||||
$box.on('input', function(){
|
||||
const term = this.value.trim();
|
||||
clearTimeout(timer);
|
||||
if (term.length < 2) { $area.empty(); $status.text(''); return; }
|
||||
timer = setTimeout(() => doSearch(term), 220);
|
||||
});
|
||||
|
||||
function doSearch(term){
|
||||
lastTerm = term;
|
||||
$status.html('<span class="spinner-border spinner-border-sm text-teal"></span> Mencari...');
|
||||
$.getJSON(searchUrl, { q: term })
|
||||
.done(res => { if (term !== lastTerm) return; render(res); })
|
||||
.fail(() => $status.html('<span class="text-danger">Ralat carian.</span>'));
|
||||
}
|
||||
|
||||
function render(res){
|
||||
$area.empty();
|
||||
if (!res.data || res.data.length === 0){
|
||||
$status.html('');
|
||||
$area.html(`<div class="alert alert-danger d-flex align-items-center gap-2 fs-6">
|
||||
<i class="bi bi-x-octagon-fill fs-3"></i>
|
||||
<div><b>Rekod tidak dijumpai.</b><br>Individu ini bukan anggota koperasi yang berdaftar.</div></div>`);
|
||||
return;
|
||||
}
|
||||
$status.html(`<small>${res.data.length} rekod dijumpai</small>`);
|
||||
res.data.forEach(m => $area.append(cardHtml(m)));
|
||||
}
|
||||
|
||||
function cardHtml(m){
|
||||
const hadirBadge = m.hadir
|
||||
? `<span class="badge badge-hadir"><i class="bi bi-check-circle"></i> Sudah Hadir</span>`
|
||||
: `<span class="badge badge-belum">Belum Hadir</span>`;
|
||||
const timeInfo = m.hadir ? `<div class="small text-success mt-1"><i class="bi bi-clock"></i> Masa hadir: ${m.attended_at ?? '-'}</div>` : '';
|
||||
const aktif = m.status_aktif ? '' : `<span class="badge bg-warning text-dark ms-1">Tidak Aktif</span>`;
|
||||
const btn = m.hadir
|
||||
? `<button class="btn btn-success" disabled><i class="bi bi-check2-all"></i> Telah Hadir</button>`
|
||||
: `<button class="btn btn-teal btn-lg px-4" onclick="recordAttend(${m.id}, this)"><i class="bi bi-person-check"></i> Rekod Hadir</button>`;
|
||||
|
||||
return `<div class="card result-card mb-2 ${m.hadir ? 'hadir':''}" id="m-${m.id}">
|
||||
<div class="card-body d-flex flex-wrap justify-content-between align-items-center gap-3">
|
||||
<div>
|
||||
<div class="fs-5 fw-bold">${escapeHtml(m.nama)} ${aktif}</div>
|
||||
<div class="text-muted small">
|
||||
<i class="bi bi-person-badge"></i> ${m.no_pekerja ?? '-'} ·
|
||||
<i class="bi bi-credit-card-2-front"></i> ${m.masked_kp ?? '-'} ·
|
||||
<i class="bi bi-building"></i> ${escapeHtml(m.jabatan ?? '-')} / ${escapeHtml(m.bahagian ?? '-')}
|
||||
</div>
|
||||
<div class="mt-1">${hadirBadge}</div>
|
||||
${timeInfo}
|
||||
</div>
|
||||
<div class="text-end" id="act-${m.id}">${btn}</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
function recordAttend(id, btn){
|
||||
$(btn).prop('disabled', true).html('<span class="spinner-border spinner-border-sm"></span> Menyimpan...');
|
||||
$.post(storeUrl, { member_id: id })
|
||||
.done(res => {
|
||||
const m = res.member;
|
||||
$(`#m-${id}`).addClass('hadir');
|
||||
$(`#act-${id}`).html('<button class="btn btn-success" disabled><i class="bi bi-check2-all"></i> Telah Hadir</button>');
|
||||
if (res.status === 'ok'){
|
||||
Swal.fire({ icon:'success', title:'Kehadiran Direkod!', text: res.message, timer: 1600, showConfirmButton:false });
|
||||
} else {
|
||||
Swal.fire({ icon:'info', title:'Sudah Hadir', text: res.message });
|
||||
}
|
||||
})
|
||||
.fail(xhr => {
|
||||
$(btn).prop('disabled', false).html('<i class="bi bi-person-check"></i> Rekod Hadir');
|
||||
Swal.fire({ icon:'error', title:'Ralat', text: (xhr.responseJSON?.message) || 'Gagal merekod kehadiran.' });
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(s){ return (s ?? '').toString().replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
70
resources/views/dashboard.blade.php
Normal file
70
resources/views/dashboard.blade.php
Normal file
@@ -0,0 +1,70 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Dashboard')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$cards = [
|
||||
['Jumlah Anggota', $stats['total_members'], 'bg-teal', 'bi-people-fill'],
|
||||
['Jumlah Hadir', $stats['hadir'], 'bg-green', 'bi-person-check-fill'],
|
||||
['Belum Hadir', $stats['belum_hadir'], 'bg-dark2', 'bi-person-dash-fill'],
|
||||
['Jumlah Hadiah', $stats['total_prizes'], 'bg-cyan', 'bi-gift-fill'],
|
||||
['Hadiah Belum Dicabut', $stats['prizes_belum'], 'bg-gold', 'bi-hourglass-split'],
|
||||
['Hadiah Sudah Dicabut', $stats['prizes_disahkan'], 'bg-teal', 'bi-check2-circle'],
|
||||
['Pemenang Disahkan', $stats['pemenang_disahkan'], 'bg-green', 'bi-trophy-fill'],
|
||||
['Cabutan Dibatalkan', $stats['cabutan_dibatalkan'], 'bg-red', 'bi-x-octagon-fill'],
|
||||
];
|
||||
@endphp
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
@foreach($cards as [$label, $value, $bg, $icon])
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="stat-card {{ $bg }}">
|
||||
<i class="bi {{ $icon }} ico"></i>
|
||||
<div class="label">{{ $label }}</div>
|
||||
<div class="value">{{ number_format($value) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-7">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-white fw-bold"><i class="bi bi-graph-up-arrow text-teal"></i> Kemajuan Event</div>
|
||||
<div class="card-body">
|
||||
@php
|
||||
$hadirPct = $stats['total_members'] ? round($stats['hadir'] / $stats['total_members'] * 100) : 0;
|
||||
$cabutPct = $stats['total_prizes'] ? round($stats['prizes_disahkan'] / $stats['total_prizes'] * 100) : 0;
|
||||
@endphp
|
||||
<label class="small fw-semibold">Kehadiran ({{ $stats['hadir'] }}/{{ $stats['total_members'] }})</label>
|
||||
<div class="progress mb-3" style="height:22px;">
|
||||
<div class="progress-bar bg-success fw-bold" style="width: {{ $hadirPct }}%">{{ $hadirPct }}%</div>
|
||||
</div>
|
||||
<label class="small fw-semibold">Hadiah Disahkan ({{ $stats['prizes_disahkan'] }}/{{ $stats['total_prizes'] }})</label>
|
||||
<div class="progress" style="height:22px;">
|
||||
<div class="progress-bar fw-bold" style="width: {{ $cabutPct }}%; background:var(--koipb-teal)">{{ $cabutPct }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-white fw-bold"><i class="bi bi-trophy text-teal"></i> Pemenang Terkini</div>
|
||||
<div class="card-body p-0">
|
||||
@forelse($recentWinners as $w)
|
||||
<div class="d-flex align-items-center gap-3 px-3 py-2 border-bottom">
|
||||
<span class="badge bg-gold rounded-pill">#{{ $w->prize->draw_order }}</span>
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-semibold">{{ $w->member->nama }}</div>
|
||||
<div class="small text-muted">{{ $w->prize->nama_hadiah }}</div>
|
||||
</div>
|
||||
<i class="bi bi-trophy-fill" style="color:var(--koipb-gold)"></i>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-center text-muted py-4"><i class="bi bi-hourglass"></i> Belum ada pemenang disahkan.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
334
resources/views/draw/index.blade.php
Normal file
334
resources/views/draw/index.blade.php
Normal file
@@ -0,0 +1,334 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ms">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Cabutan Bertuah · KOIPB MAT</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="{{ asset('css/koipb.css') }}?v=1" rel="stylesheet">
|
||||
</head>
|
||||
<body class="projector">
|
||||
<div class="draw-topbar">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<img src="{{ asset('images/logo-koipb.jpg') }}" onerror="this.style.display='none'">
|
||||
<div>
|
||||
<div class="fw-bold fs-5">Cabutan Bertuah Mesyuarat Agung Tahunan</div>
|
||||
<div class="small" style="color:var(--koipb-teal-light)">{{ $session->nama }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="badge bg-light text-dark fs-6"><i class="bi bi-people-fill text-teal"></i> Peserta Layak: <b id="eligibleCount">{{ $eligibleCount }}</b></span>
|
||||
<a href="{{ route('dashboard') }}" class="btn btn-outline-light btn-sm"><i class="bi bi-grid"></i> Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="draw-grid">
|
||||
<!-- KIRI: WHEEL -->
|
||||
<div class="wheel-stage">
|
||||
<div class="mb-3 w-100 text-center" style="max-width:560px;">
|
||||
<div class="text-uppercase small text-white-50" style="letter-spacing:2px;">Nombor Cabutan</div>
|
||||
<div class="d-flex align-items-center justify-content-center gap-3">
|
||||
<button id="prevPrize" class="btn btn-outline-light btn-sm rounded-circle" title="Hadiah sebelum" style="width:38px;height:38px;"><i class="bi bi-chevron-up"></i></button>
|
||||
<div id="currentNumber" style="font-size:3.4rem;font-weight:900;line-height:1;color:var(--koipb-gold);">—</div>
|
||||
<div class="text-white-50 fs-5">/ <span id="totalNumber">{{ $startNumber }}</span></div>
|
||||
<button id="nextPrize" class="btn btn-outline-light btn-sm rounded-circle" title="Hadiah seterusnya" style="width:38px;height:38px;"><i class="bi bi-chevron-down"></i></button>
|
||||
</div>
|
||||
<div id="prizeNameTop" class="prize-title mt-1">—</div>
|
||||
<div id="doneNote" class="badge bg-success fs-6 mt-2" style="display:none"><i class="bi bi-check2-all"></i> Semua cabutan selesai</div>
|
||||
</div>
|
||||
|
||||
<div class="wheel-pointer"><i class="bi bi-caret-down-fill"></i></div>
|
||||
<canvas id="wheelCanvas" width="500" height="500"></canvas>
|
||||
|
||||
<button id="spinBtn" class="btn-spin mt-4" disabled><i class="bi bi-arrow-repeat"></i> SPIN</button>
|
||||
<div class="small mt-2 text-white-50" id="poolNote"></div>
|
||||
</div>
|
||||
|
||||
<!-- KANAN: PANEL KEPUTUSAN -->
|
||||
<div class="panel-result" id="resultPanel">
|
||||
<div id="bigMsg" class="text-center mb-3" style="display:none"></div>
|
||||
|
||||
<div class="text-uppercase small text-white-50">Nombor Cabutan</div>
|
||||
<div class="d-flex align-items-baseline gap-3">
|
||||
<div class="display-5 fw-bold" id="prizeOrder">—</div>
|
||||
<div class="prize-title" id="prizeName">Belum dipilih</div>
|
||||
</div>
|
||||
<hr class="border-light opacity-25">
|
||||
|
||||
<div id="winnerBox" class="flex-grow-1 d-flex flex-column justify-content-center text-center" style="opacity:.4">
|
||||
<div class="text-white-50">Calon Pemenang</div>
|
||||
<div class="winner-name my-2" id="winnerName">—</div>
|
||||
<div id="winnerMeta" class="fs-5 text-white-50"></div>
|
||||
</div>
|
||||
|
||||
<div class="text-center my-3">
|
||||
<span class="pill" id="statusPill" style="display:none"></span>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center" id="actionBtns">
|
||||
<button id="confirmBtn" class="btn btn-success btn-lg px-4" style="display:none"><i class="bi bi-check-circle-fill"></i> Confirm Pemenang</button>
|
||||
<button id="cancelBtn" class="btn btn-danger btn-lg px-4" style="display:none"><i class="bi bi-x-circle-fill"></i> Batal / Tiada Di Dewan</button>
|
||||
<button id="redrawBtn" class="btn btn-warning btn-lg px-4" style="display:none"><i class="bi bi-arrow-repeat"></i> Cabut Semula</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/dist/confetti.browser.min.js"></script>
|
||||
<script>
|
||||
const urls = {
|
||||
pool: "{{ route('draw.pool') }}",
|
||||
spin: "{{ route('draw.spin') }}",
|
||||
confirm: "{{ route('draw.confirm') }}",
|
||||
cancel: "{{ route('draw.cancel') }}",
|
||||
};
|
||||
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content } });
|
||||
|
||||
// Senarai hadiah berserta Nombor Cabutan (seq), serta nombor mula (had kehadiran)
|
||||
let PRIZES = {!! json_encode($prizes) !!};
|
||||
const START_NUMBER = {{ $startNumber }};
|
||||
|
||||
const COLORS = ['#0a8f9c','#2b3036','#14b8c8','#e0a91b','#1e9e5a','#086e78','#1bb6c4','#3a4149'];
|
||||
const canvas = document.getElementById('wheelCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let pool = [], rotation = 0, spinning = false, currentResultId = null, currentPrizeId = null, currentSeq = null;
|
||||
|
||||
function loadPool(cb){
|
||||
$.getJSON(urls.pool).done(res => { pool = res.pool || []; $('#poolNote').text(pool.length + ' peserta dalam pool'); drawWheel(); cb && cb(); });
|
||||
}
|
||||
|
||||
function drawWheel(){
|
||||
const n = Math.max(pool.length, 1);
|
||||
const cx = 250, cy = 250, r = 240;
|
||||
const seg = (2*Math.PI)/n;
|
||||
ctx.clearRect(0,0,500,500);
|
||||
ctx.save(); ctx.translate(cx,cy); ctx.rotate(rotation);
|
||||
const showLabels = pool.length > 0 && pool.length <= 40;
|
||||
for (let i=0;i<n;i++){
|
||||
ctx.beginPath(); ctx.moveTo(0,0);
|
||||
ctx.arc(0,0,r, i*seg, (i+1)*seg);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = COLORS[i % COLORS.length];
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(255,255,255,.15)'; ctx.stroke();
|
||||
if (showLabels && pool[i]){
|
||||
ctx.save();
|
||||
ctx.rotate(i*seg + seg/2);
|
||||
ctx.textAlign='right'; ctx.fillStyle='#fff'; ctx.font='bold 13px Segoe UI';
|
||||
const lbl = pool[i].label || pool[i].nama || '';
|
||||
ctx.fillText(lbl.substring(0,16), r-14, 5);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
// hub
|
||||
ctx.beginPath(); ctx.arc(cx,cy,38,0,2*Math.PI); ctx.fillStyle='#fff'; ctx.fill();
|
||||
ctx.fillStyle='#0a8f9c'; ctx.font='bold 30px Segoe UI'; ctx.textAlign='center';
|
||||
ctx.fillText('★', cx, cy+11);
|
||||
if (!showLabels && pool.length>0){
|
||||
ctx.fillStyle='#fff'; ctx.font='bold 18px Segoe UI';
|
||||
ctx.fillText(pool.length + ' peserta', cx, cy+70);
|
||||
}
|
||||
}
|
||||
|
||||
function animateTo(winnerIndex, done){
|
||||
const n = pool.length; if (n===0){ done(); return; }
|
||||
const seg = (2*Math.PI)/n;
|
||||
// sudut supaya tengah segmen winner berada di atas (pointer pada -90°)
|
||||
const target = (-Math.PI/2) - (winnerIndex*seg + seg/2);
|
||||
const turns = 6 + Math.floor(Math.random()*3);
|
||||
const finalRot = target + turns*2*Math.PI;
|
||||
const startRot = rotation % (2*Math.PI);
|
||||
const duration = 5200, t0 = performance.now();
|
||||
function frame(now){
|
||||
let p = Math.min((now-t0)/duration, 1);
|
||||
const ease = 1 - Math.pow(1-p, 3); // ease-out cubic
|
||||
rotation = startRot + (finalRot - startRot) * ease;
|
||||
drawWheel();
|
||||
if (p < 1) requestAnimationFrame(frame); else { spinning=false; done(); }
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
// ---- Nombor Cabutan: cabutan menurun dari nombor terakhir (had = kehadiran) ----
|
||||
function prizeBySeq(seq){ return PRIZES.find(p => p.seq === seq) || null; }
|
||||
|
||||
// Hadiah pertama yang boleh dicabut (seq tertinggi <= START_NUMBER yang belum disahkan)
|
||||
function firstDrawable(){
|
||||
for (let s = START_NUMBER; s >= 1; s--){
|
||||
const p = prizeBySeq(s);
|
||||
if (p && p.status !== 'disahkan') return s;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// Hadiah seterusnya (menurun) yang masih boleh dicabut, bermula bawah daripada 'from'
|
||||
function nextDrawableDown(from){
|
||||
for (let s = from; s >= 1; s--){
|
||||
const p = prizeBySeq(s);
|
||||
if (p && p.status !== 'disahkan') return s;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function prevDrawableUp(from){
|
||||
for (let s = from; s <= START_NUMBER; s++){
|
||||
const p = prizeBySeq(s);
|
||||
if (p && p.status !== 'disahkan') return s;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function gotoSeq(seq){
|
||||
resetPanel();
|
||||
currentSeq = seq;
|
||||
const p = seq ? prizeBySeq(seq) : null;
|
||||
if (!p){
|
||||
currentPrizeId = null;
|
||||
$('#currentNumber').text('—');
|
||||
$('#spinBtn').prop('disabled', true);
|
||||
$('#prevPrize,#nextPrize').prop('disabled', true);
|
||||
if (START_NUMBER === 0){
|
||||
$('#prizeNameTop').text('Tiada kehadiran direkod');
|
||||
$('#prizeOrder').text('—'); $('#prizeName').text('Tiada peserta hadir');
|
||||
$('#doneNote').hide();
|
||||
} else {
|
||||
$('#prizeNameTop').text('Selesai');
|
||||
$('#prizeOrder').text('—'); $('#prizeName').text('Selesai');
|
||||
$('#doneNote').show();
|
||||
}
|
||||
return;
|
||||
}
|
||||
$('#doneNote').hide();
|
||||
currentPrizeId = p.id;
|
||||
$('#currentNumber').text(seq);
|
||||
$('#prizeNameTop').text(p.nama_hadiah);
|
||||
$('#prizeOrder').text('No. ' + seq);
|
||||
$('#prizeName').text(p.nama_hadiah);
|
||||
$('#spinBtn').prop('disabled', false);
|
||||
// butang naik/turun
|
||||
$('#prevPrize').prop('disabled', prevDrawableUp(seq + 1) === 0);
|
||||
$('#nextPrize').prop('disabled', nextDrawableDown(seq - 1) === 0);
|
||||
}
|
||||
|
||||
$('#nextPrize').on('click', () => { if (spinning) return; const s = nextDrawableDown(currentSeq - 1); if (s) gotoSeq(s); });
|
||||
$('#prevPrize').on('click', () => { if (spinning) return; const s = prevDrawableUp(currentSeq + 1); if (s) gotoSeq(s); });
|
||||
|
||||
function resetPanel(){
|
||||
$('#resultPanel').removeClass('win cancel');
|
||||
$('#bigMsg').hide().empty();
|
||||
$('#statusPill').hide();
|
||||
$('#confirmBtn,#cancelBtn,#redrawBtn').hide();
|
||||
$('#winnerBox').css('opacity', .4);
|
||||
$('#winnerName').text('—'); $('#winnerMeta').text('');
|
||||
currentResultId = null;
|
||||
}
|
||||
|
||||
function lockUi(lock){
|
||||
$('#spinBtn,#prevPrize,#nextPrize,#confirmBtn,#cancelBtn,#redrawBtn').prop('disabled', lock);
|
||||
}
|
||||
|
||||
// ---- SPIN ----
|
||||
function doSpin(){
|
||||
if (spinning || !currentPrizeId) return;
|
||||
spinning = true; lockUi(true); resetPanel();
|
||||
$('#spinBtn').html('<span class="spinner-border spinner-border-sm"></span> Memusing...');
|
||||
$.post(urls.spin, { prize_id: currentPrizeId })
|
||||
.done(res => {
|
||||
pool = res.pool;
|
||||
animateTo(res.winner_index, () => revealWinner(res));
|
||||
})
|
||||
.fail(xhr => {
|
||||
spinning=false; lockUi(false); $('#spinBtn').html('<i class="bi bi-arrow-repeat"></i> SPIN');
|
||||
Swal.fire({icon:'warning', title:'Tidak boleh spin', text: xhr.responseJSON?.message || 'Ralat.'});
|
||||
});
|
||||
}
|
||||
|
||||
function revealWinner(res){
|
||||
currentResultId = res.result_id;
|
||||
const w = res.winner;
|
||||
$('#winnerBox').css('opacity', 1);
|
||||
$('#winnerName').text(w.nama);
|
||||
$('#winnerMeta').html(`${w.no_pekerja ?? ''} · ${w.jabatan ?? '-'}<br><small>${w.masked_kp ?? ''}</small>`);
|
||||
$('#statusPill').attr('class','pill pill-sedang').text('Menunggu Pengesahan').show();
|
||||
$('#spinBtn').html('<i class="bi bi-arrow-repeat"></i> SPIN').prop('disabled', true);
|
||||
$('#prevPrize,#nextPrize').prop('disabled', true);
|
||||
$('#confirmBtn,#cancelBtn').show().prop('disabled', false);
|
||||
$('#redrawBtn').hide();
|
||||
}
|
||||
|
||||
// ---- CONFIRM ----
|
||||
$('#confirmBtn').on('click', function(){
|
||||
lockUi(true);
|
||||
$.post(urls.confirm, { result_id: currentResultId })
|
||||
.done(res => {
|
||||
$('#resultPanel').addClass('win').removeClass('cancel');
|
||||
$('#statusPill').attr('class','pill pill-disahkan').text('DISAHKAN ✓').show();
|
||||
$('#bigMsg').html('<div class="big-msg" style="color:var(--koipb-gold)">🎉 TAHNIAH! 🎉</div>').show();
|
||||
$('#confirmBtn,#cancelBtn,#redrawBtn').hide();
|
||||
fireConfetti();
|
||||
markPrizeDone(currentPrizeId);
|
||||
// Auto-turun ke nombor cabutan seterusnya selepas 2.4 saat
|
||||
const nextSeq = nextDrawableDown(currentSeq - 1);
|
||||
setTimeout(() => loadPool(() => gotoSeq(nextSeq)), 2400);
|
||||
})
|
||||
.fail(xhr => { lockUi(false); Swal.fire({icon:'error', title:'Gagal sahkan', text: xhr.responseJSON?.message || 'Ralat.'}); });
|
||||
});
|
||||
|
||||
// ---- CANCEL ----
|
||||
$('#cancelBtn').on('click', function(){
|
||||
Swal.fire({
|
||||
icon:'warning', title:'Batalkan cabutan?', text:'Pemenang tiada di dewan. Hadiah akan dicabut semula.',
|
||||
showCancelButton:true, confirmButtonText:'Ya, Batalkan', cancelButtonText:'Tidak', confirmButtonColor:'#c1272d',
|
||||
input:'text', inputPlaceholder:'Sebab (pilihan)'
|
||||
}).then(r => {
|
||||
if (!r.isConfirmed) return;
|
||||
lockUi(true);
|
||||
$.post(urls.cancel, { result_id: currentResultId, sebab: r.value || '' })
|
||||
.done(res => {
|
||||
$('#resultPanel').addClass('cancel').removeClass('win');
|
||||
$('#statusPill').attr('class','pill pill-redraw').text('DIBATALKAN').show();
|
||||
$('#bigMsg').html('<div class="big-msg" style="color:#ff8a8a;font-size:2rem">Cabutan Dibatalkan<br><small style="font-size:1.1rem">Pemenang Tiada Di Dewan</small></div>').show();
|
||||
$('#confirmBtn,#cancelBtn').hide();
|
||||
$('#redrawBtn').show().prop('disabled', false);
|
||||
loadPool();
|
||||
})
|
||||
.fail(xhr => { lockUi(false); Swal.fire({icon:'error', title:'Gagal', text: xhr.responseJSON?.message || 'Ralat.'}); });
|
||||
});
|
||||
});
|
||||
|
||||
// ---- REDRAW (cabut semula hadiah sama) ----
|
||||
$('#redrawBtn').on('click', () => doSpin());
|
||||
$('#spinBtn').on('click', () => doSpin());
|
||||
|
||||
function markPrizeDone(prizeId){
|
||||
const p = PRIZES.find(x => x.id === prizeId);
|
||||
if (p) p.status = 'disahkan';
|
||||
}
|
||||
|
||||
function fireConfetti(){
|
||||
const end = Date.now() + 1500;
|
||||
(function frame(){
|
||||
confetti({ particleCount: 6, angle: 60, spread: 70, origin: { x: 0 }, colors:['#0a8f9c','#e0a91b','#1e9e5a','#fff'] });
|
||||
confetti({ particleCount: 6, angle: 120, spread: 70, origin: { x: 1 }, colors:['#0a8f9c','#e0a91b','#1e9e5a','#fff'] });
|
||||
if (Date.now() < end) requestAnimationFrame(frame);
|
||||
})();
|
||||
confetti({ particleCount: 160, spread: 100, origin: { y: 0.6 } });
|
||||
}
|
||||
|
||||
// ---- Mula: pilih nombor cabutan tertinggi yang boleh dicabut ----
|
||||
@if($pending)
|
||||
@php $pendingWinner = ['nama' => $pending->member->nama, 'no_pekerja' => $pending->member->no_pekerja, 'jabatan' => $pending->member->jabatan, 'masked_kp' => $pending->member->maskedKp()]; @endphp
|
||||
loadPool(() => {
|
||||
const pendingPrize = PRIZES.find(p => p.id === {{ $pending->prize_id }});
|
||||
gotoSeq(pendingPrize ? pendingPrize.seq : firstDrawable());
|
||||
revealWinner({ result_id: {{ $pending->id }}, winner: {!! json_encode($pendingWinner) !!} });
|
||||
});
|
||||
@else
|
||||
loadPool(() => gotoSeq(firstDrawable()));
|
||||
@endif
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
122
resources/views/layouts/app.blade.php
Normal file
122
resources/views/layouts/app.blade.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ms">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>@yield('title', 'Sistem MAT') · KOIPB</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="{{ asset('css/koipb.css') }}?v=1" rel="stylesheet">
|
||||
@stack('head')
|
||||
</head>
|
||||
<body>
|
||||
@php
|
||||
$u = auth()->user();
|
||||
$isAdmin = $u?->hasRole(\App\Models\User::ROLE_ADMIN);
|
||||
$canKaunter = $u?->hasAnyRole([\App\Models\User::ROLE_ADMIN, \App\Models\User::ROLE_KAUNTER]);
|
||||
$canCabutan = $u?->hasAnyRole([\App\Models\User::ROLE_ADMIN, \App\Models\User::ROLE_CABUTAN]);
|
||||
@endphp
|
||||
<div class="koipb-shell">
|
||||
<aside class="koipb-sidebar" id="koipbSidebar">
|
||||
<div class="brand">
|
||||
<img src="{{ asset('images/logo-koipb.jpg') }}" alt="KOIPB" onerror="this.style.visibility='hidden'">
|
||||
<div>
|
||||
<div class="t1">KOIPB</div>
|
||||
<div class="t2">Sistem MAT</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="koipb-nav">
|
||||
<a href="{{ route('dashboard') }}" class="{{ request()->routeIs('dashboard') ? 'active' : '' }}">
|
||||
<span class="ico"><i class="bi bi-speedometer2"></i></span> Dashboard
|
||||
</a>
|
||||
|
||||
@if($canKaunter)
|
||||
<div class="nav-section">Kehadiran</div>
|
||||
<a href="{{ route('counter.index') }}" class="{{ request()->routeIs('counter.*') ? 'active' : '' }}">
|
||||
<span class="ico"><i class="bi bi-person-check"></i></span> Kaunter Kehadiran
|
||||
</a>
|
||||
<a href="{{ route('attendance.index') }}" class="{{ request()->routeIs('attendance.*') ? 'active' : '' }}">
|
||||
<span class="ico"><i class="bi bi-list-check"></i></span> Senarai Kehadiran
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if($canCabutan)
|
||||
<div class="nav-section">Cabutan Bertuah</div>
|
||||
<a href="{{ route('draw.index') }}" class="{{ request()->routeIs('draw.*') ? 'active' : '' }}">
|
||||
<span class="ico"><i class="bi bi-trophy"></i></span> Skrin Cabutan
|
||||
</a>
|
||||
<a href="{{ route('reports.index') }}" class="{{ request()->routeIs('reports.*') ? 'active' : '' }}">
|
||||
<span class="ico"><i class="bi bi-file-earmark-bar-graph"></i></span> Laporan
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if($isAdmin)
|
||||
<div class="nav-section">Pentadbiran</div>
|
||||
<a href="{{ route('admin.members.index') }}" class="{{ request()->routeIs('admin.members.*') ? 'active' : '' }}">
|
||||
<span class="ico"><i class="bi bi-people"></i></span> Anggota
|
||||
</a>
|
||||
<a href="{{ route('admin.prizes.index') }}" class="{{ request()->routeIs('admin.prizes.*') ? 'active' : '' }}">
|
||||
<span class="ico"><i class="bi bi-gift"></i></span> Hadiah
|
||||
</a>
|
||||
<a href="{{ route('admin.audit.index') }}" class="{{ request()->routeIs('admin.audit.*') ? 'active' : '' }}">
|
||||
<span class="ico"><i class="bi bi-shield-check"></i></span> Audit Trail
|
||||
</a>
|
||||
<a href="{{ route('admin.settings.index') }}" class="{{ request()->routeIs('admin.settings.*') ? 'active' : '' }}">
|
||||
<span class="ico"><i class="bi bi-gear"></i></span> Tetapan & Reset
|
||||
</a>
|
||||
@endif
|
||||
</nav>
|
||||
<div class="sb-foot">
|
||||
<div class="text-white-50 small">{{ $u?->name }}</div>
|
||||
<div class="small mb-2" style="color:var(--koipb-teal-light)">
|
||||
@foreach($u?->getRoleNames() ?? [] as $r){{ \Illuminate\Support\Str::headline($r) }}@endforeach
|
||||
</div>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button class="btn btn-sm btn-outline-light w-100"><i class="bi bi-box-arrow-right"></i> Log Keluar</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="koipb-backdrop d-none" id="koipbBackdrop"></div>
|
||||
|
||||
<div class="koipb-main">
|
||||
<header class="koipb-topbar">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button class="btn btn-light koipb-burger" id="koipbBurger"><i class="bi bi-list"></i></button>
|
||||
<h1>@yield('title', 'Dashboard')</h1>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge bg-light text-dark border d-none d-md-inline">
|
||||
<i class="bi bi-calendar-event text-teal"></i> {{ now()->format('d/m/Y') }}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
<main class="koipb-content">
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show"><i class="bi bi-check-circle"></i> {{ session('success') }}<button class="btn-close" data-bs-dismiss="alert"></button></div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show"><i class="bi bi-exclamation-triangle"></i> {{ session('error') }}<button class="btn-close" data-bs-dismiss="alert"></button></div>
|
||||
@endif
|
||||
@yield('content')
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/dist/confetti.browser.min.js"></script>
|
||||
<script>
|
||||
window.csrfToken = document.querySelector('meta[name="csrf-token"]').content;
|
||||
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': window.csrfToken } });
|
||||
(function(){
|
||||
const sb = document.getElementById('koipbSidebar'), bd = document.getElementById('koipbBackdrop');
|
||||
document.getElementById('koipbBurger')?.addEventListener('click', () => { sb.classList.add('show'); bd.classList.remove('d-none'); });
|
||||
bd?.addEventListener('click', () => { sb.classList.remove('show'); bd.classList.add('d-none'); });
|
||||
})();
|
||||
</script>
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
70
resources/views/reports/index.blade.php
Normal file
70
resources/views/reports/index.blade.php
Normal file
@@ -0,0 +1,70 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Laporan')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$exportBtns = fn($type) => '<div class="btn-group btn-group-sm">'
|
||||
.'<a href="'.route('reports.export',$type).'?format=xlsx" class="btn btn-outline-teal"><i class="bi bi-file-earmark-excel"></i> Excel</a>'
|
||||
.'<a href="'.route('reports.export',$type).'?format=pdf" class="btn btn-outline-secondary"><i class="bi bi-file-earmark-pdf"></i> PDF</a></div>';
|
||||
@endphp
|
||||
|
||||
<ul class="nav nav-tabs mb-3" role="tablist">
|
||||
<li class="nav-item"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-winners">Pemenang ({{ $winners->count() }})</button></li>
|
||||
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-undrawn">Belum Dicabut ({{ $undrawn->count() }})</button></li>
|
||||
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-cancelled">Dibatalkan ({{ $cancelled->count() }})</button></li>
|
||||
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-attendance">Kehadiran</button></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="tab-winners">
|
||||
<div class="card"><div class="card-body">
|
||||
<div class="d-flex justify-content-between mb-2"><h6 class="fw-bold mb-0">Senarai Pemenang Disahkan</h6>{!! $exportBtns('winners') !!}</div>
|
||||
<div class="table-responsive"><table class="table table-koipb table-hover align-middle">
|
||||
<thead><tr><th>Susunan</th><th>Hadiah</th><th>Pemenang</th><th>No Pekerja</th><th>Jabatan</th><th>Masa Disahkan</th></tr></thead>
|
||||
<tbody>
|
||||
@forelse($winners as $w)
|
||||
<tr><td><span class="badge bg-gold">#{{ $w->prize->draw_order }}</span></td>
|
||||
<td>{{ $w->prize->nama_hadiah }}</td><td class="fw-semibold">{{ $w->member->nama }}</td>
|
||||
<td>{{ $w->member->no_pekerja }}</td><td>{{ $w->member->jabatan }}</td>
|
||||
<td><small>{{ $w->confirmed_at?->format('d/m/Y H:i') }}</small></td></tr>
|
||||
@empty<tr><td colspan="6" class="text-center text-muted py-3">Belum ada pemenang.</td></tr>@endforelse
|
||||
</tbody></table></div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="tab-undrawn">
|
||||
<div class="card"><div class="card-body">
|
||||
<div class="d-flex justify-content-between mb-2"><h6 class="fw-bold mb-0">Hadiah Belum Dicabut</h6>{!! $exportBtns('undrawn') !!}</div>
|
||||
<div class="table-responsive"><table class="table table-koipb table-hover align-middle">
|
||||
<thead><tr><th>Susunan</th><th>Kod</th><th>Hadiah</th><th>Kategori</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
@forelse($undrawn as $p)
|
||||
<tr><td><span class="badge bg-dark2">#{{ $p->draw_order }}</span></td><td>{{ $p->kod_hadiah }}</td>
|
||||
<td>{{ $p->nama_hadiah }}</td><td>{{ $p->kategori }}</td><td><span class="pill pill-belum">{{ $p->statusLabel() }}</span></td></tr>
|
||||
@empty<tr><td colspan="5" class="text-center text-muted py-3">Semua hadiah telah dicabut.</td></tr>@endforelse
|
||||
</tbody></table></div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="tab-cancelled">
|
||||
<div class="card"><div class="card-body">
|
||||
<div class="d-flex justify-content-between mb-2"><h6 class="fw-bold mb-0">Cabutan Dibatalkan</h6>{!! $exportBtns('cancelled') !!}</div>
|
||||
<div class="table-responsive"><table class="table table-koipb table-hover align-middle">
|
||||
<thead><tr><th>Hadiah</th><th>Nama</th><th>No Pekerja</th><th>Sebab</th><th>Cubaan</th><th>Masa</th></tr></thead>
|
||||
<tbody>
|
||||
@forelse($cancelled as $c)
|
||||
<tr><td>{{ $c->prize->nama_hadiah }}</td><td>{{ $c->member->nama }}</td><td>{{ $c->member->no_pekerja }}</td>
|
||||
<td>{{ $c->sebab_batal }}</td><td>#{{ $c->attempt_number }}</td><td><small>{{ $c->cancelled_at?->format('d/m/Y H:i') }}</small></td></tr>
|
||||
@empty<tr><td colspan="6" class="text-center text-muted py-3">Tiada cabutan dibatalkan.</td></tr>@endforelse
|
||||
</tbody></table></div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="tab-attendance">
|
||||
<div class="card"><div class="card-body">
|
||||
<div class="d-flex justify-content-between mb-2"><h6 class="fw-bold mb-0">Senarai Kehadiran</h6>{!! $exportBtns('attendance') !!}</div>
|
||||
<p class="text-muted">Lihat senarai penuh & penapis di <a href="{{ route('attendance.index') }}">Senarai Kehadiran</a>. Butang di atas untuk eksport.</p>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
35
resources/views/reports/pdf.blade.php
Normal file
35
resources/views/reports/pdf.blade.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
* { font-family: DejaVu Sans, sans-serif; }
|
||||
body { font-size: 11px; color: #222; }
|
||||
.head { border-bottom: 3px solid #0a8f9c; padding-bottom: 8px; margin-bottom: 12px; }
|
||||
.head h2 { margin: 0; color: #0a8f9c; }
|
||||
.head .sub { color: #555; font-size: 10px; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th { background: #2b3036; color: #fff; padding: 6px; text-align: left; font-size: 10px; }
|
||||
td { padding: 5px 6px; border-bottom: 1px solid #ddd; }
|
||||
tr:nth-child(even) td { background: #f4f7f8; }
|
||||
.foot { margin-top: 10px; font-size: 9px; color: #888; text-align: right; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="head">
|
||||
<h2>{{ $title }}</h2>
|
||||
<div class="sub">Koperasi Iskandar Puteri Berhad (KOIPB) · Mesyuarat Agung Tahunan · Dijana: {{ now()->format('d/m/Y H:i') }}</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr>@foreach($headings as $h)<th>{{ $h }}</th>@endforeach</tr></thead>
|
||||
<tbody>
|
||||
@forelse($rows as $row)
|
||||
<tr>@foreach((array) $row as $cell)<td>{{ $cell }}</td>@endforeach</tr>
|
||||
@empty
|
||||
<tr><td colspan="{{ count($headings) }}" style="text-align:center;padding:14px;">Tiada rekod.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="foot">Jumlah rekod: {{ count($rows) }}</div>
|
||||
</body>
|
||||
</html>
|
||||
223
resources/views/welcome.blade.php
Normal file
223
resources/views/welcome.blade.php
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user