first commit

This commit is contained in:
Saufi
2026-06-24 20:32:14 +08:00
commit 10fb30ad69
201 changed files with 21356 additions and 0 deletions

44
app/Models/AppSetting.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
class AppSetting extends Model
{
protected $fillable = [
'key',
'value',
'type',
'group',
'label',
'description',
];
public static function get(string $key, $default = null)
{
$setting = Cache::remember("setting.$key", 3600, fn () => static::where('key', $key)->first());
if (! $setting) {
return $default;
}
return match ($setting->type) {
'integer' => (int) $setting->value,
'boolean' => filter_var($setting->value, FILTER_VALIDATE_BOOLEAN),
default => $setting->value,
};
}
public static function set(string $key, $value): void
{
static::where('key', $key)->update(['value' => $value]);
Cache::forget("setting.$key");
}
protected static function booted(): void
{
static::saved(fn ($m) => Cache::forget("setting.{$m->key}"));
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ChecklistFormula extends Model
{
use HasFactory;
protected $fillable = [
'checklist_template_id',
'checklist_section_id',
'result_token',
'label',
'expression',
'unit',
'guard_div_zero',
'order',
];
protected function casts(): array
{
return [
'guard_div_zero' => 'boolean',
'order' => 'integer',
];
}
public function template(): BelongsTo
{
return $this->belongsTo(ChecklistTemplate::class, 'checklist_template_id');
}
public function section(): BelongsTo
{
return $this->belongsTo(ChecklistSection::class, 'checklist_section_id');
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class ChecklistItem extends Model
{
use HasFactory;
public const INPUT_TYPES = [
'number' => 'Nombor',
'text' => 'Teks',
'textarea' => 'Teks Panjang',
'select' => 'Senarai Pilihan',
'checkbox' => 'Kotak Semak',
'file_reference' => 'Rujukan Fail',
'calculated' => 'Dikira Automatik',
];
protected $fillable = [
'checklist_section_id',
'checklist_scope_id',
'code',
'formula_token',
'label',
'description',
'input_type',
'options',
'unit',
'is_required',
'is_calculated',
'formula',
'order',
];
protected function casts(): array
{
return [
'options' => 'array',
'is_required' => 'boolean',
'is_calculated' => 'boolean',
'order' => 'integer',
];
}
public function section(): BelongsTo
{
return $this->belongsTo(ChecklistSection::class, 'checklist_section_id');
}
public function scope(): BelongsTo
{
return $this->belongsTo(ChecklistScope::class, 'checklist_scope_id');
}
public function answers(): HasMany
{
return $this->hasMany(SubmissionAnswer::class);
}
public function isNumeric(): bool
{
return in_array($this->input_type, ['number', 'calculated'], true);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class ChecklistScope extends Model
{
use HasFactory;
protected $fillable = [
'checklist_section_id',
'code',
'title',
'description',
'order',
];
protected function casts(): array
{
return [
'order' => 'integer',
];
}
public function section(): BelongsTo
{
return $this->belongsTo(ChecklistSection::class, 'checklist_section_id');
}
public function items(): HasMany
{
return $this->hasMany(ChecklistItem::class)->orderBy('order');
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class ChecklistSection extends Model
{
use HasFactory;
protected $fillable = [
'checklist_template_id',
'code',
'title',
'description',
'order',
'is_formula_enabled',
];
protected function casts(): array
{
return [
'is_formula_enabled' => 'boolean',
'order' => 'integer',
];
}
public function template(): BelongsTo
{
return $this->belongsTo(ChecklistTemplate::class, 'checklist_template_id');
}
public function scopes(): HasMany
{
return $this->hasMany(ChecklistScope::class)->orderBy('order');
}
public function items(): HasMany
{
return $this->hasMany(ChecklistItem::class)->orderBy('order');
}
public function formula(): HasMany
{
return $this->hasMany(ChecklistFormula::class);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class ChecklistTemplate extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'name',
'version',
'description',
'is_active',
];
protected function casts(): array
{
return [
'is_active' => 'boolean',
];
}
public function sections(): HasMany
{
return $this->hasMany(ChecklistSection::class)->orderBy('order');
}
public function formulas(): HasMany
{
return $this->hasMany(ChecklistFormula::class)->orderBy('order');
}
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public static function activeTemplate(): ?self
{
return static::active()->latest('id')->first();
}
}

68
app/Models/Consultant.php Normal file
View File

@@ -0,0 +1,68 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Activitylog\Support\LogOptions;
use Spatie\Activitylog\Models\Concerns\LogsActivity;
class Consultant extends Model
{
use HasFactory, SoftDeletes, LogsActivity;
protected $fillable = [
'user_id',
'nama_perunding',
'no_pendaftaran_syarikat',
'alamat',
'emel',
'telefon',
'aktif',
];
protected function casts(): array
{
return [
'aktif' => 'boolean',
];
}
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['nama_perunding', 'no_pendaftaran_syarikat', 'emel', 'telefon', 'aktif', 'user_id'])
->logOnlyDirty()
->dontLogEmptyChanges()
->useLogName('perunding');
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/** Pusat data yang sedang ditugaskan kepada perunding ini. */
public function dataCentres(): HasMany
{
return $this->hasMany(DataCentre::class, 'current_consultant_id');
}
public function assignments(): HasMany
{
return $this->hasMany(DataCentreConsultantAssignment::class);
}
public function submissions(): HasMany
{
return $this->hasMany(Submission::class);
}
public function scopeAktif($query)
{
return $query->where('aktif', true);
}
}

69
app/Models/DataCentre.php Normal file
View File

@@ -0,0 +1,69 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Activitylog\Support\LogOptions;
use Spatie\Activitylog\Models\Concerns\LogsActivity;
class DataCentre extends Model
{
use HasFactory, SoftDeletes, LogsActivity;
protected $fillable = [
'nama_pusat_data',
'tajuk_permohonan',
'lokasi_pusat_data',
'alamat',
'keluasan_tapak',
'it_load',
'status',
'current_consultant_id',
];
protected function casts(): array
{
return [
'keluasan_tapak' => 'decimal:2',
'it_load' => 'decimal:2',
];
}
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['nama_pusat_data', 'tajuk_permohonan', 'status', 'current_consultant_id', 'it_load', 'keluasan_tapak'])
->logOnlyDirty()
->dontLogEmptyChanges()
->useLogName('pusat_data');
}
public function currentConsultant(): BelongsTo
{
return $this->belongsTo(Consultant::class, 'current_consultant_id');
}
public function assignments(): HasMany
{
return $this->hasMany(DataCentreConsultantAssignment::class)->latest('start_date');
}
public function activeAssignment()
{
return $this->hasOne(DataCentreConsultantAssignment::class)->where('is_active', true);
}
public function submissions(): HasMany
{
return $this->hasMany(Submission::class);
}
public function scopeAktif($query)
{
return $query->where('status', 'aktif');
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class DataCentreConsultantAssignment extends Model
{
use HasFactory;
protected $fillable = [
'data_centre_id',
'consultant_id',
'start_date',
'end_date',
'reason',
'assigned_by',
'is_active',
];
protected function casts(): array
{
return [
'start_date' => 'date',
'end_date' => 'date',
'is_active' => 'boolean',
];
}
public function dataCentre(): BelongsTo
{
return $this->belongsTo(DataCentre::class);
}
public function consultant(): BelongsTo
{
return $this->belongsTo(Consultant::class);
}
public function assignedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'assigned_by');
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Spatie\Activitylog\Support\LogOptions;
use Spatie\Activitylog\Models\Concerns\LogsActivity;
class ReportingCycle extends Model
{
use HasFactory, LogsActivity;
protected $fillable = [
'renewal_year',
'reporting_year',
'period_start',
'period_end',
'cut_off_date',
'licence_period_year',
'checklist_template_id',
'status',
'catatan',
];
protected function casts(): array
{
return [
'renewal_year' => 'integer',
'reporting_year' => 'integer',
'period_start' => 'date',
'period_end' => 'date',
'cut_off_date' => 'date',
'licence_period_year' => 'integer',
];
}
/**
* Peraturan perniagaan: tahun pelaporan = tahun pembaharuan - 2.
* Cth: pembaharuan 2028 => pelaporan 2026.
*/
public static function reportingYearFor(int $renewalYear): int
{
return $renewalYear - 2;
}
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['renewal_year', 'reporting_year', 'cut_off_date', 'status'])
->logOnlyDirty()
->dontLogEmptyChanges()
->useLogName('kitaran_pelaporan');
}
public function checklistTemplate(): BelongsTo
{
return $this->belongsTo(ChecklistTemplate::class);
}
public function submissions(): HasMany
{
return $this->hasMany(Submission::class);
}
public function scopeAktif($query)
{
return $query->where('status', 'aktif');
}
public function isOverdue(): bool
{
return $this->cut_off_date && now()->startOfDay()->gt($this->cut_off_date);
}
}

152
app/Models/Submission.php Normal file
View File

@@ -0,0 +1,152 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Activitylog\Support\LogOptions;
use Spatie\Activitylog\Models\Concerns\LogsActivity;
class Submission extends Model
{
use HasFactory, SoftDeletes, LogsActivity;
public const STATUSES = [
'draf' => 'Draf',
'baru' => 'Baru',
'dalam_tindakan' => 'Dalam Tindakan',
'pembetulan_perunding' => 'Pembetulan Perunding',
'selesai' => 'Selesai',
];
protected $fillable = [
'data_centre_id',
'reporting_cycle_id',
'consultant_id',
'checklist_template_id',
'status',
'is_locked',
'locked_by',
'locked_at',
'lock_reason',
'assigned_officer_id',
'hardcopy_received',
'hardcopy_received_date',
'hardcopy_sent_date',
'submitted_at',
'submitted_by',
'review_notes',
];
protected function casts(): array
{
return [
'is_locked' => 'boolean',
'locked_at' => 'datetime',
'hardcopy_received' => 'boolean',
'hardcopy_received_date' => 'date',
'hardcopy_sent_date' => 'date',
'submitted_at' => 'datetime',
];
}
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['status', 'is_locked', 'assigned_officer_id', 'hardcopy_received', 'hardcopy_sent_date'])
->logOnlyDirty()
->dontLogEmptyChanges()
->useLogName('serahan');
}
public function dataCentre(): BelongsTo
{
return $this->belongsTo(DataCentre::class);
}
public function reportingCycle(): BelongsTo
{
return $this->belongsTo(ReportingCycle::class);
}
public function consultant(): BelongsTo
{
return $this->belongsTo(Consultant::class);
}
public function checklistTemplate(): BelongsTo
{
return $this->belongsTo(ChecklistTemplate::class);
}
public function assignedOfficer(): BelongsTo
{
return $this->belongsTo(User::class, 'assigned_officer_id');
}
public function answers(): HasMany
{
return $this->hasMany(SubmissionAnswer::class);
}
public function results(): HasMany
{
return $this->hasMany(SubmissionResult::class);
}
public function documents(): HasMany
{
return $this->hasMany(SubmissionDocument::class)->orderByDesc('version');
}
public function activeDocument(): HasOne
{
return $this->hasOne(SubmissionDocument::class)->where('is_active', true);
}
public function statusHistories(): HasMany
{
return $this->hasMany(SubmissionStatusHistory::class)->latest();
}
public function correctionRequests(): HasMany
{
return $this->hasMany(SubmissionCorrectionRequest::class)->latest();
}
public function comments(): HasMany
{
return $this->hasMany(SubmissionComment::class)->oldest();
}
/** Perunding boleh edit selagi belum dikunci dan belum selesai. */
public function isEditableByConsultant(): bool
{
return ! $this->is_locked && $this->status !== 'selesai';
}
public function isSubmitted(): bool
{
return ! in_array($this->status, ['draf'], true);
}
public function statusLabel(): string
{
return self::STATUSES[$this->status] ?? $this->status;
}
public function statusBadgeClass(): string
{
return [
'draf' => 'secondary',
'baru' => 'info',
'dalam_tindakan' => 'primary',
'pembetulan_perunding' => 'warning',
'selesai' => 'success',
][$this->status] ?? 'secondary';
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SubmissionAnswer extends Model
{
use HasFactory;
protected $fillable = [
'submission_id',
'checklist_item_id',
'value',
'unit',
'page_reference',
'consultant_note',
'officer_note',
'validation_status',
];
public function submission(): BelongsTo
{
return $this->belongsTo(Submission::class);
}
public function item(): BelongsTo
{
return $this->belongsTo(ChecklistItem::class, 'checklist_item_id');
}
public function numericValue(): ?float
{
return is_numeric($this->value) ? (float) $this->value : null;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class SubmissionComment extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'submission_id',
'user_id',
'body',
'is_internal',
'attachment_path',
'attachment_name',
'deleted_by',
];
protected function casts(): array
{
return [
'is_internal' => 'boolean',
];
}
public function submission(): BelongsTo
{
return $this->belongsTo(Submission::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SubmissionCorrectionRequest extends Model
{
use HasFactory;
public const STATUSES = [
'terbuka' => 'Terbuka',
'dijawab' => 'Dijawab',
'selesai' => 'Selesai',
];
protected $fillable = [
'submission_id',
'checklist_item_id',
'page_reference',
'location',
'description',
'status',
'consultant_response',
'responded_at',
'responded_by',
'created_by',
'resolved_at',
'resolved_by',
];
protected function casts(): array
{
return [
'responded_at' => 'datetime',
'resolved_at' => 'datetime',
];
}
public function submission(): BelongsTo
{
return $this->belongsTo(Submission::class);
}
public function item(): BelongsTo
{
return $this->belongsTo(ChecklistItem::class, 'checklist_item_id');
}
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function respondedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'responded_by');
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SubmissionDocument extends Model
{
use HasFactory;
protected $fillable = [
'submission_id',
'original_filename',
'stored_path',
'file_hash',
'mime_type',
'size',
'version',
'is_active',
'uploaded_by',
];
protected function casts(): array
{
return [
'is_active' => 'boolean',
'size' => 'integer',
'version' => 'integer',
];
}
public function submission(): BelongsTo
{
return $this->belongsTo(Submission::class);
}
public function uploadedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'uploaded_by');
}
public function humanSize(): string
{
$bytes = (int) $this->size;
if ($bytes >= 1048576) {
return round($bytes / 1048576, 2).' MB';
}
if ($bytes >= 1024) {
return round($bytes / 1024, 2).' KB';
}
return $bytes.' B';
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SubmissionResult extends Model
{
use HasFactory;
protected $fillable = [
'submission_id',
'result_token',
'label',
'value',
'unit',
];
protected function casts(): array
{
return [
'value' => 'decimal:4',
];
}
public function submission(): BelongsTo
{
return $this->belongsTo(Submission::class);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SubmissionStatusHistory extends Model
{
use HasFactory;
protected $fillable = [
'submission_id',
'from_status',
'to_status',
'changed_by',
'notes',
];
public function submission(): BelongsTo
{
return $this->belongsTo(Submission::class);
}
public function changedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'changed_by');
}
}

72
app/Models/User.php Normal file
View File

@@ -0,0 +1,72 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\Activitylog\Support\LogOptions;
use Spatie\Activitylog\Models\Concerns\LogsActivity;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasFactory, Notifiable, HasRoles, SoftDeletes, LogsActivity;
protected $fillable = [
'name',
'email',
'password',
'phone',
'jawatan',
'is_active',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_active' => 'boolean',
];
}
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['name', 'email', 'phone', 'jawatan', 'is_active'])
->logOnlyDirty()
->dontLogEmptyChanges()
->useLogName('pengguna');
}
public function consultant(): HasOne
{
return $this->hasOne(Consultant::class);
}
public function assignedSubmissions(): HasMany
{
return $this->hasMany(Submission::class, 'assigned_officer_id');
}
/** Adakah pengguna ini seorang perunding. */
public function isPerunding(): bool
{
return $this->hasRole('Perunding');
}
/** Adakah pengguna ini kakitangan JPP (bukan perunding). */
public function isJpp(): bool
{
return $this->hasAnyRole(['Super Admin', 'JPP Admin', 'Pegawai JPP', 'Penolong Pegawai', 'Kerani']);
}
}