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

1
database/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.sqlite*

View File

@@ -0,0 +1,28 @@
<?php
namespace Database\Factories;
use App\Models\Consultant;
use Illuminate\Database\Eloquent\Factories\Factory;
class ConsultantFactory extends Factory
{
protected $model = Consultant::class;
public function definition(): array
{
return [
'nama_perunding' => fake()->company().' Sdn Bhd',
'no_pendaftaran_syarikat' => fake()->numerify('20########').' ('.fake()->numerify('#######-A').')',
'alamat' => fake()->address(),
'emel' => fake()->unique()->safeEmail(),
'telefon' => fake()->numerify('07-#######'),
'aktif' => true,
];
}
public function tidakAktif(): static
{
return $this->state(fn () => ['aktif' => false]);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Database\Factories;
use App\Models\DataCentre;
use Illuminate\Database\Eloquent\Factories\Factory;
class DataCentreFactory extends Factory
{
protected $model = DataCentre::class;
public function definition(): array
{
return [
'nama_pusat_data' => 'Pusat Data '.fake()->city(),
'tajuk_permohonan' => 'Permohonan Lesen '.fake()->words(2, true),
'lokasi_pusat_data' => fake()->city(),
'alamat' => fake()->address(),
'keluasan_tapak' => fake()->randomFloat(2, 1, 20),
'it_load' => fake()->randomFloat(2, 5, 100),
'status' => 'aktif',
'current_consultant_id' => null,
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Database\Factories;
use App\Models\ReportingCycle;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\Factory;
class ReportingCycleFactory extends Factory
{
protected $model = ReportingCycle::class;
public function definition(): array
{
$renewalYear = fake()->numberBetween(2026, 2035);
$reportingYear = $renewalYear - 2;
return [
'renewal_year' => $renewalYear,
'reporting_year' => $reportingYear,
'period_start' => Carbon::create($reportingYear, 1, 1),
'period_end' => Carbon::create($reportingYear, 12, 31),
'cut_off_date' => Carbon::create($renewalYear - 1, 12, 1),
'licence_period_year' => 1,
'status' => 'aktif',
];
}
public function renewalYear(int $year): static
{
return $this->state(fn () => [
'renewal_year' => $year,
'reporting_year' => $year - 2,
'period_start' => Carbon::create($year - 2, 1, 1),
'period_end' => Carbon::create($year - 2, 12, 31),
'cut_off_date' => Carbon::create($year - 1, 12, 1),
]);
}
public function overdue(): static
{
return $this->state(fn () => ['cut_off_date' => now()->subDays(5)]);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}

View File

@@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->bigInteger('expiration')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->bigInteger('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};

View File

@@ -0,0 +1,59 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedSmallInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('connection');
$table->string('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
$table->index(['connection', 'queue', 'failed_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};

View File

@@ -0,0 +1,137 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
throw_if(empty($tableNames), 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
/**
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
*/
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
$table->id(); // permission id
$table->string('name');
$table->string('guard_name');
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
/**
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
*/
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
$table->id(); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name');
$table->string('guard_name');
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->cascadeOnDelete();
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}
});
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->cascadeOnDelete();
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
});
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->cascadeOnDelete();
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->cascadeOnDelete();
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
throw_if(empty($tableNames), 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
Schema::dropIfExists($tableNames['role_has_permissions']);
Schema::dropIfExists($tableNames['model_has_roles']);
Schema::dropIfExists($tableNames['model_has_permissions']);
Schema::dropIfExists($tableNames['roles']);
Schema::dropIfExists($tableNames['permissions']);
}
};

View File

@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('activity_log', function (Blueprint $table) {
$table->id();
$table->string('log_name')->nullable()->index();
$table->text('description');
$table->nullableMorphs('subject', 'subject');
$table->string('event')->nullable();
$table->nullableMorphs('causer', 'causer');
$table->json('attribute_changes')->nullable();
$table->json('properties')->nullable();
$table->timestamps();
});
}
};

View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('phone')->nullable()->after('email');
$table->string('jawatan')->nullable()->after('phone'); // jawatan rasmi
$table->boolean('is_active')->default(true)->after('jawatan');
$table->softDeletes();
$table->index('is_active');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['phone', 'jawatan', 'is_active', 'deleted_at']);
});
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('consultants', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('nama_perunding');
$table->string('no_pendaftaran_syarikat')->nullable();
$table->text('alamat')->nullable();
$table->string('emel')->nullable();
$table->string('telefon')->nullable();
$table->boolean('aktif')->default(true);
$table->timestamps();
$table->softDeletes();
$table->index('aktif');
$table->index('nama_perunding');
});
}
public function down(): void
{
Schema::dropIfExists('consultants');
}
};

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('data_centres', function (Blueprint $table) {
$table->id();
$table->string('nama_pusat_data');
$table->string('tajuk_permohonan')->nullable();
$table->string('lokasi_pusat_data')->nullable();
$table->text('alamat')->nullable();
$table->decimal('keluasan_tapak', 15, 2)->nullable()->comment('Keluasan tapak (ekar/m2)');
$table->decimal('it_load', 15, 2)->nullable()->comment('Kapasiti penjanaan elektrik komponen IT / IT Load (MW)');
$table->string('status')->default('aktif'); // aktif / tidak_aktif
$table->foreignId('current_consultant_id')->nullable()->constrained('consultants')->nullOnDelete();
$table->timestamps();
$table->softDeletes();
$table->index('status');
$table->index('current_consultant_id');
$table->index('nama_pusat_data');
});
}
public function down(): void
{
Schema::dropIfExists('data_centres');
}
};

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('data_centre_consultant_assignments', function (Blueprint $table) {
$table->id();
$table->foreignId('data_centre_id')->constrained('data_centres')->cascadeOnDelete();
$table->foreignId('consultant_id')->constrained('consultants')->cascadeOnDelete();
$table->date('start_date');
$table->date('end_date')->nullable();
$table->text('reason')->nullable(); // sebab perubahan
$table->foreignId('assigned_by')->nullable()->constrained('users')->nullOnDelete();
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->index(['data_centre_id', 'is_active'], 'dcca_dc_active_idx');
$table->index('consultant_id', 'dcca_consultant_idx');
});
}
public function down(): void
{
Schema::dropIfExists('data_centre_consultant_assignments');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('checklist_templates', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('version')->default('1.0');
$table->text('description')->nullable();
$table->boolean('is_active')->default(false);
$table->timestamps();
$table->softDeletes();
$table->index('is_active');
});
}
public function down(): void
{
Schema::dropIfExists('checklist_templates');
}
};

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('checklist_sections', function (Blueprint $table) {
$table->id();
$table->foreignId('checklist_template_id')->constrained('checklist_templates')->cascadeOnDelete();
$table->string('code')->comment('Kod seksyen cth: A, B, C, D, E, LAIN');
$table->string('title');
$table->text('description')->nullable();
$table->unsignedInteger('order')->default(0);
$table->boolean('is_formula_enabled')->default(false)->comment('Seksyen menggunakan formula atau tidak');
$table->timestamps();
$table->index(['checklist_template_id', 'order']);
});
}
public function down(): void
{
Schema::dropIfExists('checklist_sections');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('checklist_scopes', function (Blueprint $table) {
$table->id();
$table->foreignId('checklist_section_id')->constrained('checklist_sections')->cascadeOnDelete();
$table->string('code')->comment('cth: SKOP1, SKOP2, SKOP3');
$table->string('title');
$table->text('description')->nullable();
$table->unsignedInteger('order')->default(0);
$table->timestamps();
$table->index(['checklist_section_id', 'order']);
});
}
public function down(): void
{
Schema::dropIfExists('checklist_scopes');
}
};

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('checklist_items', function (Blueprint $table) {
$table->id();
$table->foreignId('checklist_section_id')->constrained('checklist_sections')->cascadeOnDelete();
$table->foreignId('checklist_scope_id')->nullable()->constrained('checklist_scopes')->nullOnDelete();
$table->string('code')->comment('Kod rujukan cth: A.1.i, 2(i)');
$table->string('formula_token')->nullable()->comment('Token unik untuk rujukan formula cth: SKOP1, EE, RE');
$table->string('label');
$table->text('description')->nullable();
$table->string('input_type')->default('number'); // number,text,textarea,select,checkbox,file_reference,calculated
$table->json('options')->nullable()->comment('Pilihan untuk input jenis select/checkbox');
$table->string('unit')->nullable()->comment('cth: tCO2e, %, tahun');
$table->boolean('is_required')->default(false);
$table->boolean('is_calculated')->default(false)->comment('Item formula/dikira automatik');
$table->text('formula')->nullable()->comment('Ungkapan formula jika item dikira');
$table->unsignedInteger('order')->default(0);
$table->timestamps();
$table->index(['checklist_section_id', 'order']);
$table->index('formula_token');
});
}
public function down(): void
{
Schema::dropIfExists('checklist_items');
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('checklist_formulas', function (Blueprint $table) {
$table->id();
$table->foreignId('checklist_template_id')->constrained('checklist_templates')->cascadeOnDelete();
$table->foreignId('checklist_section_id')->nullable()->constrained('checklist_sections')->cascadeOnDelete();
$table->string('result_token')->comment('Token hasil yang dikira cth: A, B, C, D, E');
$table->string('label')->nullable();
$table->text('expression')->comment('Ungkapan RHS cth: SKOP1 + SKOP2 + SKOP3 atau A - B');
$table->string('unit')->nullable();
$table->boolean('guard_div_zero')->default(false)->comment('Lindung pembahagian dengan sifar');
$table->unsignedInteger('order')->default(0);
$table->timestamps();
$table->index(['checklist_template_id', 'order']);
$table->index('result_token');
});
}
public function down(): void
{
Schema::dropIfExists('checklist_formulas');
}
};

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('reporting_cycles', function (Blueprint $table) {
$table->id();
$table->unsignedSmallInteger('renewal_year')->unique()->comment('Tahun pembaharuan lesen');
$table->unsignedSmallInteger('reporting_year')->comment('Tahun pelaporan = renewal_year - 2');
$table->date('period_start')->comment('1 Januari tahun pelaporan');
$table->date('period_end')->comment('31 Disember tahun pelaporan');
$table->date('cut_off_date')->comment('Tarikh tutup serahan, boleh dikonfigur JPP');
$table->unsignedSmallInteger('licence_period_year')->default(1);
$table->foreignId('checklist_template_id')->nullable()->constrained('checklist_templates')->nullOnDelete();
$table->string('status')->default('aktif'); // aktif / tutup
$table->text('catatan')->nullable();
$table->timestamps();
$table->index('status');
$table->index('reporting_year');
});
}
public function down(): void
{
Schema::dropIfExists('reporting_cycles');
}
};

View File

@@ -0,0 +1,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('submissions', function (Blueprint $table) {
$table->id();
$table->foreignId('data_centre_id')->constrained('data_centres')->cascadeOnDelete();
$table->foreignId('reporting_cycle_id')->constrained('reporting_cycles')->cascadeOnDelete();
$table->foreignId('consultant_id')->constrained('consultants')->cascadeOnDelete();
$table->foreignId('checklist_template_id')->nullable()->constrained('checklist_templates')->nullOnDelete();
// draf, baru, dalam_tindakan, pembetulan_perunding, selesai
$table->string('status')->default('draf');
$table->boolean('is_locked')->default(false);
$table->foreignId('locked_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('locked_at')->nullable();
$table->text('lock_reason')->nullable();
$table->foreignId('assigned_officer_id')->nullable()->constrained('users')->nullOnDelete();
$table->boolean('hardcopy_received')->default(false);
$table->date('hardcopy_received_date')->nullable();
$table->date('hardcopy_sent_date')->nullable()->comment('Tarikh hardcopy dihantar ke Bahagian Kawalan Perancangan');
$table->timestamp('submitted_at')->nullable();
$table->foreignId('submitted_by')->nullable()->constrained('users')->nullOnDelete();
$table->text('review_notes')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['data_centre_id', 'reporting_cycle_id'], 'submissions_dc_cycle_unique');
$table->index('status');
$table->index('consultant_id');
$table->index('reporting_cycle_id');
$table->index('assigned_officer_id');
});
}
public function down(): void
{
Schema::dropIfExists('submissions');
}
};

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('submission_answers', function (Blueprint $table) {
$table->id();
$table->foreignId('submission_id')->constrained('submissions')->cascadeOnDelete();
$table->foreignId('checklist_item_id')->constrained('checklist_items')->cascadeOnDelete();
$table->text('value')->nullable();
$table->string('unit')->nullable();
$table->string('page_reference')->nullable()->comment('No muka surat / rujukan dalam laporan PDF');
$table->text('consultant_note')->nullable();
$table->text('officer_note')->nullable();
// belum_semak, sah, tidak_sah
$table->string('validation_status')->default('belum_semak');
$table->timestamps();
$table->unique(['submission_id', 'checklist_item_id'], 'answers_submission_item_unique');
$table->index('checklist_item_id');
});
}
public function down(): void
{
Schema::dropIfExists('submission_answers');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('submission_results', function (Blueprint $table) {
$table->id();
$table->foreignId('submission_id')->constrained('submissions')->cascadeOnDelete();
$table->string('result_token')->comment('A, B, C, D, E');
$table->string('label')->nullable();
$table->decimal('value', 20, 4)->nullable();
$table->string('unit')->nullable();
$table->timestamps();
$table->unique(['submission_id', 'result_token'], 'results_submission_token_unique');
});
}
public function down(): void
{
Schema::dropIfExists('submission_results');
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('submission_documents', function (Blueprint $table) {
$table->id();
$table->foreignId('submission_id')->constrained('submissions')->cascadeOnDelete();
$table->string('original_filename');
$table->string('stored_path');
$table->string('file_hash', 64)->nullable();
$table->string('mime_type')->nullable();
$table->unsignedBigInteger('size')->nullable();
$table->unsignedInteger('version')->default(1);
$table->boolean('is_active')->default(true);
$table->foreignId('uploaded_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->index(['submission_id', 'is_active']);
});
}
public function down(): void
{
Schema::dropIfExists('submission_documents');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('submission_status_histories', function (Blueprint $table) {
$table->id();
$table->foreignId('submission_id')->constrained('submissions')->cascadeOnDelete();
$table->string('from_status')->nullable();
$table->string('to_status');
$table->foreignId('changed_by')->nullable()->constrained('users')->nullOnDelete();
$table->text('notes')->nullable();
$table->timestamps();
$table->index('submission_id');
});
}
public function down(): void
{
Schema::dropIfExists('submission_status_histories');
}
};

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('submission_correction_requests', function (Blueprint $table) {
$table->id();
$table->foreignId('submission_id')->constrained('submissions')->cascadeOnDelete();
$table->foreignId('checklist_item_id')->nullable()->constrained('checklist_items')->nullOnDelete();
$table->string('page_reference')->nullable()->comment('No muka surat yang perlu dibetulkan');
$table->string('location')->nullable()->comment('Lokasi / seksyen / item');
$table->text('description')->comment('Penerangan apa yang perlu dibetulkan');
// terbuka, dijawab, selesai
$table->string('status')->default('terbuka');
$table->text('consultant_response')->nullable();
$table->timestamp('responded_at')->nullable();
$table->foreignId('responded_by')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('resolved_at')->nullable();
$table->foreignId('resolved_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->index(['submission_id', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('submission_correction_requests');
}
};

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('submission_comments', function (Blueprint $table) {
$table->id();
$table->foreignId('submission_id')->constrained('submissions')->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
$table->text('body');
$table->boolean('is_internal')->default(false)->comment('Mesej dalaman JPP sahaja / luaran kepada perunding');
$table->string('attachment_path')->nullable();
$table->string('attachment_name')->nullable();
$table->foreignId('deleted_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->softDeletes();
$table->index(['submission_id', 'created_at']);
});
}
public function down(): void
{
Schema::dropIfExists('submission_comments');
}
};

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('app_settings', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->text('value')->nullable();
$table->string('type')->default('string'); // string, integer, boolean, date
$table->string('group')->default('umum');
$table->string('label')->nullable();
$table->text('description')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('app_settings');
}
};

View File

@@ -0,0 +1,51 @@
<?php
namespace Database\Seeders;
use App\Models\AppSetting;
use Illuminate\Database\Seeder;
class AppSettingsSeeder extends Seeder
{
public function run(): void
{
$settings = [
[
'key' => 'default_cut_off_day',
'value' => '1',
'type' => 'integer',
'group' => 'kitaran',
'label' => 'Hari tarikh tutup lalai',
'description' => 'Hari bulan tarikh tutup serahan lalai (cth: 1).',
],
[
'key' => 'default_cut_off_month',
'value' => '12',
'type' => 'integer',
'group' => 'kitaran',
'label' => 'Bulan tarikh tutup lalai',
'description' => 'Bulan tarikh tutup serahan lalai (cth: 12 = Disember). Pembaharuan lesen pada/sebelum 1 Disember.',
],
[
'key' => 'reminder_email_enabled',
'value' => '0',
'type' => 'boolean',
'group' => 'peringatan',
'label' => 'Hantar peringatan emel',
'description' => 'Aktifkan penghantaran peringatan emel (perlu konfigurasi SMTP).',
],
[
'key' => 'max_upload_size_kb',
'value' => '20480',
'type' => 'integer',
'group' => 'dokumen',
'label' => 'Saiz maksimum muat naik (KB)',
'description' => 'Had saiz fail PDF yang boleh dimuat naik dalam KB.',
],
];
foreach ($settings as $s) {
AppSetting::firstOrCreate(['key' => $s['key']], $s);
}
}
}

View File

@@ -0,0 +1,207 @@
<?php
namespace Database\Seeders;
use App\Models\ChecklistFormula;
use App\Models\ChecklistItem;
use App\Models\ChecklistScope;
use App\Models\ChecklistSection;
use App\Models\ChecklistTemplate;
use Illuminate\Database\Seeder;
/**
* Menyemai templat senarai semak awal berdasarkan dokumen MBIP.
*
* PENTING: Struktur ini hanya benih awal. Reka bentuk pangkalan data adalah
* dinamik JPP boleh menambah/membuang seksyen, skop, item & formula kemudian
* melalui "Pembina Templat Senarai Semak" tanpa perubahan kod.
*/
class ChecklistTemplateSeeder extends Seeder
{
public function run(): void
{
$template = ChecklistTemplate::firstOrCreate(
['name' => 'Templat Senarai Semak Kemampanan Pusat Data MBIP'],
['version' => '1.0', 'is_active' => true, 'description' => 'Templat asas pelaporan jejak karbon pusat data MBIP.']
);
// Elak pendua jika seeder dijalankan semula.
if ($template->sections()->exists()) {
return;
}
$tCO2e = 'tCO2e';
// ============================================================
// SEKSYEN A — Anggaran Jumlah Pelepasan Karbon
// ============================================================
$secA = $this->section($template, 'A', 'Anggaran Jumlah Pelepasan Karbon Oleh Pusat Data', 1, true,
'Jumlah Anggaran GHG Inventori Pusat Data sebagai asas / Baseline Jejak Karbon Tahun.');
$skop1 = $this->scope($secA, 'SKOP1', 'Skop 1 — Pelepasan Langsung', 1,
'Pembakaran bahan api fosil ketika operasi (arang batu, LPG, dll); penggunaan bahan api kenderaan (petrol); pelepasan fugitive daripada penyaman udara/HVAC/chiller, SF6 dalam GIS dan sistem pemadaman kebakaran.');
$this->item($secA, 'A.1.i', 'SKOP1', 'Jumlah pelepasan Skop 1', 'number', $tCO2e, 1, true, $skop1->id,
'Jumlah keseluruhan pelepasan langsung (Skop 1) dalam tCO2e.');
$skop2 = $this->scope($secA, 'SKOP2', 'Skop 2 — Tenaga Dibeli', 2,
'Pelepasan daripada jumlah penggunaan elektrik dalam bangunan & kemudahan pusat data (mengambil kira operasi dan kapasiti penjanaan elektrik komponen IT); penggunaan kenderaan elektrik ketika operasi.');
$this->item($secA, 'A.1.ii', 'SKOP2', 'Jumlah pelepasan Skop 2', 'number', $tCO2e, 2, true, $skop2->id,
'Jumlah keseluruhan pelepasan tenaga dibeli (Skop 2) dalam tCO2e.');
$skop3 = $this->scope($secA, 'SKOP3', 'Skop 3 — Pelepasan Tidak Langsung Lain', 3,
'Sisa pepejal dilupuskan di luar kawasan; pembakaran bahan api kenderaan pekerja pergi/balik kerja; jumlah karbon daripada bahan binaan / embodied carbon.');
$this->item($secA, 'A.1.iii', 'SKOP3', 'Jumlah pelepasan Skop 3', 'number', $tCO2e, 3, true, $skop3->id,
'Jumlah keseluruhan pelepasan tidak langsung lain (Skop 3) dalam tCO2e.');
ChecklistFormula::create([
'checklist_template_id' => $template->id,
'checklist_section_id' => $secA->id,
'result_token' => 'A',
'label' => 'A — Jumlah Pelepasan Karbon (Skop 1 + Skop 2 + Skop 3)',
'expression' => 'SKOP1 + SKOP2 + SKOP3',
'unit' => $tCO2e,
'order' => 1,
]);
// ============================================================
// SEKSYEN B — Mekanisme Pengurangan Karbon
// ============================================================
$secB = $this->section($template, 'B', 'Mekanisme Pengurangan Karbon', 2, true);
$this->item($secB, '2(i)', 'EE', 'Jumlah karbon penggunaan kecekapan tenaga / EE', 'number', $tCO2e, 1, true, null,
'Contoh: LED, inverter penghawa dingin, audit tenaga, dll.');
$this->item($secB, '2(ii)', 'RE', 'Jumlah karbon penggunaan tenaga boleh diperbaharui / RE', 'number', $tCO2e, 2, true, null,
'Contoh: solar, hidro, biomas, dll.');
ChecklistFormula::create([
'checklist_template_id' => $template->id,
'checklist_section_id' => $secB->id,
'result_token' => 'B',
'label' => 'B — Jumlah Pengurangan Karbon (2(i) + 2(ii))',
'expression' => 'EE + RE',
'unit' => $tCO2e,
'order' => 2,
]);
// ============================================================
// SEKSYEN C — Anggaran Jumlah Baki Karbon
// ============================================================
$secC = $this->section($template, 'C', 'Anggaran Jumlah Baki Karbon', 3, true,
'Jumlah anggaran baki GHG / residual emissions.');
// Item dikira automatik (tiada input perunding).
$this->item($secC, '3', 'C_RES', 'Jumlah anggaran baki GHG / residual emissions', 'calculated', $tCO2e, 1, false, null,
'Dikira automatik: C = A - B.', true, 'A - B');
ChecklistFormula::create([
'checklist_template_id' => $template->id,
'checklist_section_id' => $secC->id,
'result_token' => 'C',
'label' => 'C — Baki Karbon (A - B)',
'expression' => 'A - B',
'unit' => $tCO2e,
'order' => 3,
]);
// ============================================================
// SEKSYEN D — Anggaran Jumlah Bersih Karbon
// ============================================================
$secD = $this->section($template, 'D', 'Anggaran Jumlah Bersih Karbon', 4, true);
$this->item($secD, '4(i)', 'D4I', 'Jumlah karbon melalui inisiatif (kredit karbon, CSR, mRECs)', 'number', $tCO2e, 1, true, null,
'Nyatakan tahun inisiatif dalam catatan perunding.');
$this->item($secD, '4(ii)', 'D4II', 'Jumlah anggaran pengurangan GHG melalui mekanisme dalam Seksyen B', 'number', $tCO2e, 2, true, null);
ChecklistFormula::create([
'checklist_template_id' => $template->id,
'checklist_section_id' => $secD->id,
'result_token' => 'D',
'label' => 'D — Jumlah Bersih Karbon (A - 4(i) - 4(ii))',
'expression' => 'A - D4I - D4II',
'unit' => $tCO2e,
'order' => 4,
]);
// ============================================================
// SEKSYEN E — Peratus Pengurangan GHG
// ============================================================
$secE = $this->section($template, 'E', 'Peratus Pengurangan GHG Pusat Data', 5, true,
'E = D / A x 100. Termasuk perlindungan pembahagian dengan sifar.');
$this->item($secE, '5', 'E_PCT', 'Peratus pengurangan GHG', 'calculated', '%', 1, false, null,
'Dikira automatik: E = D / A x 100.', true, 'D / A * 100');
ChecklistFormula::create([
'checklist_template_id' => $template->id,
'checklist_section_id' => $secE->id,
'result_token' => 'E',
'label' => 'E — Peratus Pengurangan GHG (D / A x 100)',
'expression' => 'D / A * 100',
'unit' => '%',
'guard_div_zero' => true,
'order' => 5,
]);
// ============================================================
// SEKSYEN LAIN-LAIN — Tiada formula
// ============================================================
$secLain = $this->section($template, 'LAIN', 'Lain-lain Perkara', 6, false);
$this->item($secLain, 'L.1', null, 'Penyertaan sijil bangunan hijau & rating diterima', 'select', null, 1, false, null,
'Pilih sijil bangunan hijau jika ada.', false, null,
['GBI', 'GreenRE', 'LEED', 'Greenmark', 'Lain-lain', 'Tiada']);
$this->item($secLain, 'L.2', null, 'Sasaran pengurangan GHG pusat data secara keseluruhan (jika ada)', 'textarea', null, 2, false, null);
$this->item($secLain, 'L.3', null, 'Jumlah pelepasan karbon daripada Business as Usual / BAU untuk 2030', 'number', $tCO2e, 3, false, null);
$this->item($secLain, 'L.4', null, 'Jumlah pengurangan karbon melalui Counter Measure / CM untuk 2030', 'number', $tCO2e, 4, false, null);
}
protected function section(ChecklistTemplate $t, string $code, string $title, int $order, bool $formula, ?string $desc = null): ChecklistSection
{
return ChecklistSection::create([
'checklist_template_id' => $t->id,
'code' => $code,
'title' => $title,
'description' => $desc,
'order' => $order,
'is_formula_enabled' => $formula,
]);
}
protected function scope(ChecklistSection $s, string $code, string $title, int $order, ?string $desc = null): ChecklistScope
{
return ChecklistScope::create([
'checklist_section_id' => $s->id,
'code' => $code,
'title' => $title,
'description' => $desc,
'order' => $order,
]);
}
protected function item(
ChecklistSection $s,
string $code,
?string $token,
string $label,
string $inputType,
?string $unit,
int $order,
bool $required,
?int $scopeId = null,
?string $desc = null,
bool $calculated = false,
?string $formula = null,
?array $options = null,
): ChecklistItem {
return ChecklistItem::create([
'checklist_section_id' => $s->id,
'checklist_scope_id' => $scopeId,
'code' => $code,
'formula_token' => $token,
'label' => $label,
'description' => $desc,
'input_type' => $inputType,
'options' => $options,
'unit' => $unit,
'is_required' => $required,
'is_calculated' => $calculated,
'formula' => $formula,
'order' => $order,
]);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
RolesAndPermissionsSeeder::class,
UserSeeder::class,
AppSettingsSeeder::class,
ChecklistTemplateSeeder::class,
DemoDataSeeder::class,
]);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Database\Seeders;
use App\Models\ChecklistTemplate;
use App\Models\Consultant;
use App\Models\DataCentre;
use App\Models\DataCentreConsultantAssignment;
use App\Models\ReportingCycle;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Database\Seeder;
class DemoDataSeeder extends Seeder
{
public function run(): void
{
$consultant = Consultant::where('emel', 'perunding@example.com')->first();
$officer = User::where('email', 'pegawai@mbip.gov.my')->first();
$template = ChecklistTemplate::activeTemplate();
if (! $consultant) {
return;
}
// Pusat data contoh.
$dataCentres = [
['nama_pusat_data' => 'Pusat Data Iskandar 1', 'tajuk_permohonan' => 'Permohonan Lesen Pusat Data Hyperscale Iskandar', 'lokasi_pusat_data' => 'Nusajaya', 'it_load' => 50.00, 'keluasan_tapak' => 12.50],
['nama_pusat_data' => 'Pusat Data Tebrau Tech Park', 'tajuk_permohonan' => 'Permohonan Lesen Pusat Data Tebrau', 'lokasi_pusat_data' => 'Tebrau', 'it_load' => 30.00, 'keluasan_tapak' => 8.20],
['nama_pusat_data' => 'Pusat Data Pasir Gudang', 'tajuk_permohonan' => 'Permohonan Lesen Pusat Data Industri', 'lokasi_pusat_data' => 'Pasir Gudang', 'it_load' => 20.00, 'keluasan_tapak' => 5.00],
];
foreach ($dataCentres as $data) {
$dc = DataCentre::firstOrCreate(
['nama_pusat_data' => $data['nama_pusat_data']],
array_merge($data, [
'alamat' => $data['lokasi_pusat_data'].', Johor',
'status' => 'aktif',
])
);
// Tugaskan perunding aktif jika belum ada.
if (! $dc->current_consultant_id) {
$dc->update(['current_consultant_id' => $consultant->id]);
DataCentreConsultantAssignment::firstOrCreate(
['data_centre_id' => $dc->id, 'consultant_id' => $consultant->id, 'is_active' => true],
[
'start_date' => Carbon::create(2026, 1, 1),
'reason' => 'Penugasan awal perunding.',
'assigned_by' => $officer?->id,
]
);
}
}
// Kitaran pelaporan contoh: pembaharuan 2028 => pelaporan 2026.
foreach ([2027, 2028] as $renewalYear) {
$reportingYear = ReportingCycle::reportingYearFor($renewalYear);
ReportingCycle::firstOrCreate(
['renewal_year' => $renewalYear],
[
'reporting_year' => $reportingYear,
'period_start' => Carbon::create($reportingYear, 1, 1),
'period_end' => Carbon::create($reportingYear, 12, 31),
'cut_off_date' => Carbon::create($renewalYear - 1, 12, 1),
'licence_period_year' => 1,
'checklist_template_id' => $template?->id,
'status' => $renewalYear === 2028 ? 'aktif' : 'tutup',
]
);
}
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\App;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar;
class RolesAndPermissionsSeeder extends Seeder
{
/** Senarai kebenaran sistem => label Bahasa Melayu. */
public const PERMISSIONS = [
'pengguna.urus' => 'Urus pengguna sistem',
'perunding.urus' => 'Urus perunding',
'pusat_data.urus' => 'Urus pusat data',
'tugasan.urus' => 'Urus penugasan perunding',
'kitaran.urus' => 'Urus kitaran pelaporan/pembaharuan',
'templat.urus' => 'Urus templat senarai semak',
'serahan.lihat_semua' => 'Lihat semua serahan',
'serahan.semak' => 'Semak serahan (status, ulasan, pembetulan, penugasan pegawai)',
'serahan.kunci' => 'Kunci/buka kunci serahan',
'serahan.hardcopy' => 'Tanda penerimaan dokumen salinan keras',
'serahan.urus_sendiri' => 'Urus serahan sendiri (perunding)',
'laporan.lihat' => 'Lihat laporan & statistik',
'peringatan.lihat' => 'Lihat senarai peringatan/belum serah',
'komen.cipta' => 'Hantar komen/perbualan',
'komen.padam' => 'Padam (soft delete) komen — pentadbir sahaja',
'tetapan.urus' => 'Urus tetapan aplikasi',
];
public const ROLES = [
'Super Admin' => '*',
'JPP Admin' => [
'pengguna.urus', 'perunding.urus', 'pusat_data.urus', 'tugasan.urus',
'kitaran.urus', 'templat.urus', 'serahan.lihat_semua', 'serahan.semak',
'serahan.kunci', 'serahan.hardcopy', 'laporan.lihat', 'peringatan.lihat',
'komen.cipta', 'komen.padam', 'tetapan.urus',
],
'Pegawai JPP' => [
'perunding.urus', 'pusat_data.urus', 'tugasan.urus', 'kitaran.urus',
'serahan.lihat_semua', 'serahan.semak', 'serahan.kunci', 'serahan.hardcopy',
'laporan.lihat', 'peringatan.lihat', 'komen.cipta',
],
'Penolong Pegawai' => [
'pusat_data.urus', 'serahan.lihat_semua', 'serahan.semak', 'serahan.hardcopy',
'laporan.lihat', 'peringatan.lihat', 'komen.cipta',
],
'Kerani' => [
'serahan.lihat_semua', 'serahan.hardcopy', 'peringatan.lihat', 'komen.cipta',
],
'Perunding' => [
'serahan.urus_sendiri', 'komen.cipta',
],
];
public function run(): void
{
app(PermissionRegistrar::class)->forgetCachedPermissions();
$guard = 'web';
foreach (self::PERMISSIONS as $name => $label) {
Permission::firstOrCreate(['name' => $name, 'guard_name' => $guard]);
}
foreach (self::ROLES as $roleName => $perms) {
$role = Role::firstOrCreate(['name' => $roleName, 'guard_name' => $guard]);
if ($perms === '*') {
$role->syncPermissions(Permission::where('guard_name', $guard)->get());
} else {
$role->syncPermissions($perms);
}
}
app(PermissionRegistrar::class)->forgetCachedPermissions();
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Database\Seeders;
use App\Models\Consultant;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class UserSeeder extends Seeder
{
public function run(): void
{
$users = [
['name' => 'Super Admin', 'email' => 'superadmin@mbip.gov.my', 'role' => 'Super Admin', 'jawatan' => 'Pentadbir Sistem'],
['name' => 'JPP Admin', 'email' => 'jppadmin@mbip.gov.my', 'role' => 'JPP Admin', 'jawatan' => 'Pentadbir JPP'],
['name' => 'Pegawai JPP', 'email' => 'pegawai@mbip.gov.my', 'role' => 'Pegawai JPP', 'jawatan' => 'Pegawai Perancang Bandar'],
['name' => 'Penolong Pegawai JPP', 'email' => 'penolong@mbip.gov.my', 'role' => 'Penolong Pegawai', 'jawatan' => 'Penolong Pegawai Perancang Bandar'],
['name' => 'Kerani JPP', 'email' => 'kerani@mbip.gov.my', 'role' => 'Kerani', 'jawatan' => 'Pembantu Tadbir'],
];
foreach ($users as $data) {
$user = User::firstOrCreate(
['email' => $data['email']],
[
'name' => $data['name'],
'password' => Hash::make('password'),
'jawatan' => $data['jawatan'],
'is_active' => true,
'email_verified_at' => now(),
]
);
$user->syncRoles([$data['role']]);
}
// Akaun perunding contoh + profil perunding berkait.
$perundingUser = User::firstOrCreate(
['email' => 'perunding@example.com'],
[
'name' => 'Perunding Contoh Sdn Bhd',
'password' => Hash::make('password'),
'jawatan' => 'Perunding Alam Sekitar',
'is_active' => true,
'email_verified_at' => now(),
]
);
$perundingUser->syncRoles(['Perunding']);
Consultant::firstOrCreate(
['emel' => 'perunding@example.com'],
[
'user_id' => $perundingUser->id,
'nama_perunding' => 'Perunding Contoh Sdn Bhd',
'no_pendaftaran_syarikat' => '202301000123 (1234567-A)',
'alamat' => 'No. 1, Jalan Hijau, 81200 Johor Bahru, Johor',
'telefon' => '07-1234567',
'aktif' => true,
]
);
}
}