This commit is contained in:
Saufi
2026-06-24 18:30:00 +08:00
commit c0c3d7c09c
122 changed files with 16321 additions and 0 deletions

View 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
View 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
View 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);
}
}

View 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
View 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
View 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
View 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
View 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',
];
}
}