first
This commit is contained in:
34
app/Models/AttendanceRecord.php
Normal file
34
app/Models/AttendanceRecord.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AttendanceRecord extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'member_id',
|
||||
'attended_at',
|
||||
'recorded_by',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'attended_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Member::class);
|
||||
}
|
||||
|
||||
public function recordedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'recorded_by');
|
||||
}
|
||||
}
|
||||
52
app/Models/AuditLog.php
Normal file
52
app/Models/AuditLog.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
|
||||
class AuditLog extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'action',
|
||||
'description',
|
||||
'subject_type',
|
||||
'subject_id',
|
||||
'ip_address',
|
||||
'meta',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'meta' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function subject(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/** Helper ringkas untuk rekod audit trail tindakan penting */
|
||||
public static function record(string $action, ?string $description = null, ?Model $subject = null, array $meta = []): self
|
||||
{
|
||||
return static::create([
|
||||
'user_id' => auth()->id(),
|
||||
'action' => $action,
|
||||
'description' => $description,
|
||||
'subject_type' => $subject ? $subject::class : null,
|
||||
'subject_id' => $subject?->getKey(),
|
||||
'ip_address' => Request::ip(),
|
||||
'meta' => $meta ?: null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
83
app/Models/DrawResult.php
Normal file
83
app/Models/DrawResult.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class DrawResult extends Model
|
||||
{
|
||||
public const STATUS_PENDING = 'pending_confirmation';
|
||||
public const STATUS_CONFIRMED = 'confirmed';
|
||||
public const STATUS_CANCELLED = 'cancelled_absent';
|
||||
public const STATUS_REDRAWN = 'redrawn';
|
||||
|
||||
public const STATUS_LABELS = [
|
||||
self::STATUS_PENDING => 'Menunggu Pengesahan',
|
||||
self::STATUS_CONFIRMED => 'Disahkan',
|
||||
self::STATUS_CANCELLED => 'Dibatalkan (Tiada Di Dewan)',
|
||||
self::STATUS_REDRAWN => 'Telah Dicabut Semula',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'draw_session_id',
|
||||
'prize_id',
|
||||
'member_id',
|
||||
'status',
|
||||
'attempt_number',
|
||||
'spun_at',
|
||||
'confirmed_at',
|
||||
'cancelled_at',
|
||||
'confirmed_by',
|
||||
'cancelled_by',
|
||||
'sebab_batal',
|
||||
'confirmed_member_id',
|
||||
'confirmed_prize_id',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'spun_at' => 'datetime',
|
||||
'confirmed_at' => 'datetime',
|
||||
'cancelled_at' => 'datetime',
|
||||
'attempt_number' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function drawSession(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DrawSession::class);
|
||||
}
|
||||
|
||||
public function prize(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Prize::class);
|
||||
}
|
||||
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Member::class);
|
||||
}
|
||||
|
||||
public function confirmedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'confirmed_by');
|
||||
}
|
||||
|
||||
public function cancelledBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'cancelled_by');
|
||||
}
|
||||
|
||||
public function statusLabel(): string
|
||||
{
|
||||
return self::STATUS_LABELS[$this->status] ?? $this->status;
|
||||
}
|
||||
|
||||
public function scopeConfirmed(Builder $query): Builder
|
||||
{
|
||||
return $query->where('status', self::STATUS_CONFIRMED);
|
||||
}
|
||||
}
|
||||
48
app/Models/DrawSession.php
Normal file
48
app/Models/DrawSession.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class DrawSession extends Model
|
||||
{
|
||||
protected $attributes = [
|
||||
'is_active' => true,
|
||||
'return_cancelled_to_pool' => true,
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'nama',
|
||||
'is_active',
|
||||
'return_cancelled_to_pool',
|
||||
'started_by',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => 'boolean',
|
||||
'return_cancelled_to_pool' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function results(): HasMany
|
||||
{
|
||||
return $this->hasMany(DrawResult::class);
|
||||
}
|
||||
|
||||
public function startedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'started_by');
|
||||
}
|
||||
|
||||
public static function active(): self
|
||||
{
|
||||
return static::firstOrCreate(
|
||||
['is_active' => true],
|
||||
['nama' => 'Mesyuarat Agung Tahunan KOIPB']
|
||||
);
|
||||
}
|
||||
}
|
||||
32
app/Models/ImportLog.php
Normal file
32
app/Models/ImportLog.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ImportLog extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'type',
|
||||
'filename',
|
||||
'total_rows',
|
||||
'success_count',
|
||||
'duplicate_count',
|
||||
'failed_count',
|
||||
'errors',
|
||||
'imported_by',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'errors' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function importedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'imported_by');
|
||||
}
|
||||
}
|
||||
110
app/Models/Member.php
Normal file
110
app/Models/Member.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
class Member extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'no_anggota',
|
||||
'no_pekerja',
|
||||
'no_kp',
|
||||
'nama',
|
||||
'jabatan',
|
||||
'bahagian',
|
||||
'telefon',
|
||||
'status_aktif',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status_aktif' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function attendance(): HasOne
|
||||
{
|
||||
return $this->hasOne(AttendanceRecord::class);
|
||||
}
|
||||
|
||||
public function drawResults(): HasMany
|
||||
{
|
||||
return $this->hasMany(DrawResult::class);
|
||||
}
|
||||
|
||||
public function hasAttended(): bool
|
||||
{
|
||||
return $this->attendance()->exists();
|
||||
}
|
||||
|
||||
/** Adakah anggota ini sudah menang & disahkan? */
|
||||
public function hasConfirmedWin(): bool
|
||||
{
|
||||
return $this->drawResults()->where('status', DrawResult::STATUS_CONFIRMED)->exists();
|
||||
}
|
||||
|
||||
/** No KP bertopeng untuk paparan biasa: 850101-01-**** */
|
||||
public function maskedKp(): ?string
|
||||
{
|
||||
if (! $this->no_kp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$kp = $this->no_kp;
|
||||
// Format standard 12 digit (boleh ada sempang)
|
||||
$digits = preg_replace('/\D/', '', $kp);
|
||||
if (strlen($digits) === 12) {
|
||||
return substr($digits, 0, 6) . '-' . substr($digits, 6, 2) . '-****';
|
||||
}
|
||||
|
||||
// Fallback: tunjukkan 4 aksara pertama, topeng selebihnya
|
||||
$visible = substr($kp, 0, max(strlen($kp) - 4, 0));
|
||||
return $visible . str_repeat('*', min(4, strlen($kp)));
|
||||
}
|
||||
|
||||
public function scopeAktif(Builder $query): Builder
|
||||
{
|
||||
return $query->where('status_aktif', true);
|
||||
}
|
||||
|
||||
public function scopeHadir(Builder $query): Builder
|
||||
{
|
||||
return $query->whereHas('attendance');
|
||||
}
|
||||
|
||||
public function scopeBelumHadir(Builder $query): Builder
|
||||
{
|
||||
return $query->whereDoesntHave('attendance');
|
||||
}
|
||||
|
||||
public function scopeSearch(Builder $query, ?string $term): Builder
|
||||
{
|
||||
$term = trim((string) $term);
|
||||
if ($term === '') {
|
||||
return $query;
|
||||
}
|
||||
|
||||
$like = '%' . $term . '%';
|
||||
$digits = preg_replace('/\D/', '', $term);
|
||||
|
||||
return $query->where(function (Builder $q) use ($like, $term, $digits) {
|
||||
$q->where('nama', 'like', $like)
|
||||
->orWhere('no_pekerja', 'like', $like)
|
||||
->orWhere('no_anggota', 'like', $like)
|
||||
->orWhere('no_kp', 'like', $like);
|
||||
|
||||
if ($digits !== '') {
|
||||
$q->orWhere('no_kp', 'like', '%' . $digits . '%')
|
||||
->orWhere('no_pekerja', 'like', '%' . $digits . '%');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
91
app/Models/Prize.php
Normal file
91
app/Models/Prize.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Prize extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const STATUS_BELUM = 'belum_dicabut';
|
||||
public const STATUS_SEDANG = 'sedang_dicabut';
|
||||
public const STATUS_DISAHKAN = 'disahkan';
|
||||
public const STATUS_REDRAW = 'redraw_required';
|
||||
|
||||
public const STATUS_LABELS = [
|
||||
self::STATUS_BELUM => 'Belum Dicabut',
|
||||
self::STATUS_SEDANG => 'Sedang Dicabut',
|
||||
self::STATUS_DISAHKAN => 'Disahkan',
|
||||
self::STATUS_REDRAW => 'Perlu Cabut Semula',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'status' => self::STATUS_BELUM,
|
||||
'item_index' => 1,
|
||||
'draw_order' => 0,
|
||||
'redraw_count' => 0,
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'kod_hadiah',
|
||||
'nama_hadiah',
|
||||
'kategori',
|
||||
'nilai_anggaran',
|
||||
'draw_order',
|
||||
'item_index',
|
||||
'batch_kod',
|
||||
'status',
|
||||
'redraw_count',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'nilai_anggaran' => 'decimal:2',
|
||||
'draw_order' => 'integer',
|
||||
'item_index' => 'integer',
|
||||
'redraw_count' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function drawResults(): HasMany
|
||||
{
|
||||
return $this->hasMany(DrawResult::class);
|
||||
}
|
||||
|
||||
public function confirmedResult(): HasMany
|
||||
{
|
||||
return $this->hasMany(DrawResult::class)->where('status', DrawResult::STATUS_CONFIRMED);
|
||||
}
|
||||
|
||||
public function statusLabel(): string
|
||||
{
|
||||
return self::STATUS_LABELS[$this->status] ?? $this->status;
|
||||
}
|
||||
|
||||
/** Nama paparan termasuk nombor item bila kuantiti > 1 cth "Hamper #3" */
|
||||
public function displayName(): string
|
||||
{
|
||||
return $this->nama_hadiah;
|
||||
}
|
||||
|
||||
/** Hadiah yang masih boleh dicabut (belum ada pemenang disahkan) */
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return in_array($this->status, [self::STATUS_BELUM, self::STATUS_REDRAW], true);
|
||||
}
|
||||
|
||||
public function scopeAvailable(Builder $query): Builder
|
||||
{
|
||||
return $query->whereIn('status', [self::STATUS_BELUM, self::STATUS_REDRAW]);
|
||||
}
|
||||
|
||||
public function scopeOrdered(Builder $query): Builder
|
||||
{
|
||||
return $query->orderBy('draw_order')->orderBy('item_index')->orderBy('id');
|
||||
}
|
||||
}
|
||||
38
app/Models/User.php
Normal file
38
app/Models/User.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable, HasRoles;
|
||||
|
||||
public const ROLE_ADMIN = 'admin';
|
||||
public const ROLE_KAUNTER = 'petugas_kaunter';
|
||||
public const ROLE_CABUTAN = 'petugas_cabutan';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user