This commit is contained in:
Saufi
2026-06-03 08:51:22 +08:00
commit a14d43fe34
347 changed files with 38197 additions and 0 deletions

1
database/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,44 @@
<?php
namespace Database\Factories;
use App\Models\Application;
use App\Models\Position;
use App\Models\PusatMengundi;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* @extends Factory<Application>
*/
class ApplicationFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$pusat = PusatMengundi::factory()->create();
$position = Position::factory()->create([
'is_public_applyable' => true,
]);
return [
'election_id' => $pusat->election_id,
'pusat_mengundi_id' => $pusat->id,
'public_uuid' => (string) Str::uuid(),
'source' => 'public',
'status' => 'submitted',
'name' => fake()->name(),
'ic_number' => fake()->unique()->numerify('############'),
'phone_number' => fake()->numerify('01########'),
'email' => fake()->safeEmail(),
'address' => fake()->address(),
'requested_position_id' => $position->id,
'bank_name' => 'Maybank',
'bank_account_number' => fake()->numerify('############'),
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Database\Factories;
use App\Models\BahagianPilihanraya;
use App\Models\Election;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<BahagianPilihanraya>
*/
class BahagianPilihanrayaFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'election_id' => Election::factory(),
'code' => strtoupper(fake()->unique()->bothify('B##')),
'name' => 'Bahagian '.fake()->city(),
];
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Database\Factories;
use App\Models\BahagianPilihanraya;
use App\Models\DaerahMengundi;
use App\Models\Election;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<DaerahMengundi>
*/
class DaerahMengundiFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$bahagian = BahagianPilihanraya::factory()->create();
return [
'election_id' => $bahagian->election_id,
'bahagian_pilihanraya_id' => $bahagian->id,
'code' => strtoupper(fake()->unique()->bothify('D##')),
'name' => 'Daerah Mengundi '.fake()->streetName(),
];
}
public function forElection(Election $election): static
{
return $this->state(fn () => [
'election_id' => $election->id,
'bahagian_pilihanraya_id' => BahagianPilihanraya::factory()->for($election)->create()->id,
]);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Database\Factories;
use App\Models\Dun;
use App\Models\Election;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Dun>
*/
class DunFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'election_id' => Election::factory(),
'code' => strtoupper(fake()->unique()->bothify('N##')),
'name' => fake()->city(),
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Database\Factories;
use App\Models\Election;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* @extends Factory<Election>
*/
class ElectionFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$pollingDate = now()->addWeeks(4)->toDateString();
return [
'public_uuid' => (string) Str::uuid(),
'name' => 'Pilihanraya Negeri '.fake()->year(),
'code' => strtoupper(fake()->unique()->bothify('PRN-####')),
'status' => 'active',
'starts_at' => now()->toDateString(),
'polling_date' => $pollingDate,
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Database\Factories;
use App\Models\Position;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Position>
*/
class PositionFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$code = strtoupper(fake()->unique()->lexify('???'));
return [
'code' => $code,
'name' => 'Jawatan '.$code,
'scope' => fake()->randomElement(['pusat', 'saluran', 'external']),
'is_public_applyable' => false,
'is_assignable' => true,
'allows_dual_role' => false,
'sort_order' => fake()->numberBetween(1, 99),
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Database\Factories;
use App\Models\Dun;
use App\Models\PusatMengundi;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* @extends Factory<PusatMengundi>
*/
class PusatMengundiFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
$dun = Dun::factory()->create();
return [
'election_id' => $dun->election_id,
'dun_id' => $dun->id,
'public_uuid' => (string) Str::uuid(),
'code' => strtoupper(fake()->unique()->bothify('PM###')),
'name' => 'SK '.fake()->city(),
'address' => fake()->address(),
'registration_url' => null,
'qr_code_path' => null,
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Database\Factories;
use App\Models\PusatMengundi;
use App\Models\SaluranMengundi;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<SaluranMengundi>
*/
class SaluranMengundiFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$pusat = PusatMengundi::factory()->create();
return [
'election_id' => $pusat->election_id,
'pusat_mengundi_id' => $pusat->id,
'number' => (string) fake()->numberBetween(1, 10),
'name' => 'Saluran '.fake()->numberBetween(1, 10),
'voter_count' => fake()->numberBetween(250, 750),
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Database\Factories;
use App\Models\Position;
use App\Models\PusatMengundi;
use App\Models\StaffAssignment;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<StaffAssignment>
*/
class StaffAssignmentFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$pusat = PusatMengundi::factory()->create();
return [
'election_id' => $pusat->election_id,
'position_id' => Position::factory(),
'pusat_mengundi_id' => $pusat->id,
'status' => 'active',
'assigned_at' => now(),
'source' => 'factory',
];
}
}

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,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,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,376 @@
<?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('user_profiles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('ic_number', 20)->nullable()->index();
$table->string('phone_number', 30)->nullable();
$table->text('address')->nullable();
$table->string('bank_name')->nullable();
$table->string('bank_account_number')->nullable();
$table->timestamps();
});
Schema::create('elections', function (Blueprint $table) {
$table->id();
$table->uuid('public_uuid')->unique();
$table->string('name');
$table->string('code')->unique();
$table->string('status')->default('draft')->index();
$table->date('starts_at')->nullable();
$table->date('polling_date')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('election_settings', function (Blueprint $table) {
$table->id();
$table->foreignId('election_id')->unique()->constrained()->cascadeOnDelete();
$table->date('registration_start_date');
$table->date('registration_end_date');
$table->date('polling_date');
$table->boolean('is_registration_open')->default(true);
$table->boolean('is_registration_open_override')->nullable();
$table->boolean('is_attendance_active')->default(false);
$table->timestamps();
});
Schema::create('bahagian_pilihanrayas', function (Blueprint $table) {
$table->id();
$table->foreignId('election_id')->constrained()->cascadeOnDelete();
$table->string('code');
$table->string('name');
$table->timestamps();
$table->softDeletes();
$table->unique(['election_id', 'code']);
});
Schema::create('daerah_mengundis', function (Blueprint $table) {
$table->id();
$table->foreignId('election_id')->constrained()->cascadeOnDelete();
$table->foreignId('bahagian_pilihanraya_id')->constrained('bahagian_pilihanrayas')->cascadeOnDelete();
$table->string('code');
$table->string('name');
$table->timestamps();
$table->softDeletes();
$table->unique(['election_id', 'code']);
});
Schema::create('pusat_mengundis', function (Blueprint $table) {
$table->id();
$table->foreignId('election_id')->constrained()->cascadeOnDelete();
$table->foreignId('daerah_mengundi_id')->constrained()->cascadeOnDelete();
$table->uuid('public_uuid')->unique();
$table->string('code');
$table->string('name');
$table->text('address')->nullable();
$table->string('registration_url', 2048)->nullable();
$table->string('qr_code_path')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['election_id', 'code']);
});
Schema::create('saluran_mengundis', function (Blueprint $table) {
$table->id();
$table->foreignId('election_id')->constrained()->cascadeOnDelete();
$table->foreignId('pusat_mengundi_id')->constrained()->cascadeOnDelete();
$table->string('number');
$table->string('name')->nullable();
$table->unsignedInteger('voter_count')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['pusat_mengundi_id', 'number']);
});
Schema::create('positions', function (Blueprint $table) {
$table->id();
$table->string('code')->unique();
$table->string('name');
$table->string('scope')->default('pusat')->index();
$table->boolean('is_public_applyable')->default(false);
$table->boolean('is_assignable')->default(true);
$table->boolean('allows_dual_role')->default(false);
$table->unsignedSmallInteger('sort_order')->default(0);
$table->timestamps();
$table->softDeletes();
});
Schema::create('position_quotas', function (Blueprint $table) {
$table->id();
$table->foreignId('election_id')->constrained()->cascadeOnDelete();
$table->foreignId('pusat_mengundi_id')->constrained()->cascadeOnDelete();
$table->foreignId('saluran_mengundi_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('position_id')->constrained()->restrictOnDelete();
$table->unsignedInteger('quota')->default(0);
$table->timestamps();
$table->softDeletes();
$table->index(['election_id', 'pusat_mengundi_id', 'position_id'], 'pq_election_pusat_position_idx');
$table->index(['election_id', 'saluran_mengundi_id', 'position_id'], 'pq_election_saluran_position_idx');
});
Schema::create('applications', function (Blueprint $table) {
$table->id();
$table->foreignId('election_id')->constrained()->cascadeOnDelete();
$table->foreignId('pusat_mengundi_id')->constrained()->cascadeOnDelete();
$table->foreignId('selected_ktm_assignment_id')->nullable();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->uuid('public_uuid')->unique();
$table->string('source')->default('public')->index();
$table->string('status')->default('submitted')->index();
$table->string('name');
$table->string('ic_number', 20);
$table->string('phone_number', 30);
$table->string('email')->nullable();
$table->text('address')->nullable();
$table->foreignId('requested_position_id')->constrained('positions')->restrictOnDelete();
$table->foreignId('approved_position_id')->nullable()->constrained('positions')->nullOnDelete();
$table->string('bank_name')->nullable();
$table->string('bank_account_number')->nullable();
$table->foreignId('created_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('approved_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('approved_at')->nullable();
$table->foreignId('rejected_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('rejected_at')->nullable();
$table->text('rejection_reason')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['election_id', 'ic_number']);
$table->index(['election_id', 'pusat_mengundi_id', 'status']);
});
Schema::create('application_documents', function (Blueprint $table) {
$table->id();
$table->foreignId('application_id')->constrained()->cascadeOnDelete();
$table->string('document_type')->index();
$table->string('disk')->default('local');
$table->string('path');
$table->string('original_name');
$table->string('mime_type', 120);
$table->unsignedBigInteger('size');
$table->foreignId('uploaded_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->softDeletes();
});
Schema::create('application_status_histories', function (Blueprint $table) {
$table->id();
$table->foreignId('application_id')->constrained()->cascadeOnDelete();
$table->string('from_status')->nullable();
$table->string('to_status');
$table->foreignId('changed_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->text('note')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
});
Schema::create('staff_assignments', function (Blueprint $table) {
$table->id();
$table->foreignId('election_id')->constrained()->cascadeOnDelete();
$table->foreignId('application_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('position_id')->constrained()->restrictOnDelete();
$table->foreignId('pusat_mengundi_id')->constrained()->cascadeOnDelete();
$table->foreignId('saluran_mengundi_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('reports_to_assignment_id')->nullable()->constrained('staff_assignments')->nullOnDelete();
$table->string('status')->default('active')->index();
$table->foreignId('assigned_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('assigned_at')->nullable();
$table->string('source')->default('system');
$table->timestamps();
$table->softDeletes();
$table->index(['election_id', 'pusat_mengundi_id', 'position_id'], 'sa_election_pusat_position_idx');
$table->index(['election_id', 'saluran_mengundi_id', 'position_id'], 'sa_election_saluran_position_idx');
});
Schema::table('applications', function (Blueprint $table) {
$table->foreign('selected_ktm_assignment_id')->references('id')->on('staff_assignments')->nullOnDelete();
});
Schema::create('staff_assignment_histories', function (Blueprint $table) {
$table->id();
$table->foreignId('staff_assignment_id')->constrained()->cascadeOnDelete();
$table->foreignId('from_position_id')->nullable()->constrained('positions')->nullOnDelete();
$table->foreignId('to_position_id')->nullable()->constrained('positions')->nullOnDelete();
$table->foreignId('from_pusat_mengundi_id')->nullable()->constrained('pusat_mengundis')->nullOnDelete();
$table->foreignId('to_pusat_mengundi_id')->nullable()->constrained('pusat_mengundis')->nullOnDelete();
$table->foreignId('from_saluran_mengundi_id')->nullable()->constrained('saluran_mengundis')->nullOnDelete();
$table->foreignId('to_saluran_mengundi_id')->nullable()->constrained('saluran_mengundis')->nullOnDelete();
$table->foreignId('from_reports_to_assignment_id')->nullable()->constrained('staff_assignments')->nullOnDelete();
$table->foreignId('to_reports_to_assignment_id')->nullable()->constrained('staff_assignments')->nullOnDelete();
$table->foreignId('changed_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->text('note')->nullable();
$table->timestamps();
});
Schema::create('police_escorts', function (Blueprint $table) {
$table->id();
$table->foreignId('election_id')->constrained()->cascadeOnDelete();
$table->foreignId('pusat_mengundi_id')->constrained()->cascadeOnDelete();
$table->foreignId('saluran_mengundi_id')->nullable()->constrained()->nullOnDelete();
$table->string('name');
$table->string('ic_number', 20)->nullable();
$table->string('phone_number', 30)->nullable();
$table->string('rank')->nullable();
$table->string('station')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('kkm_representatives', function (Blueprint $table) {
$table->id();
$table->foreignId('election_id')->constrained()->cascadeOnDelete();
$table->foreignId('pusat_mengundi_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('ic_number', 20)->nullable();
$table->string('phone_number', 30)->nullable();
$table->string('agency')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('jkm_representatives', function (Blueprint $table) {
$table->id();
$table->foreignId('election_id')->constrained()->cascadeOnDelete();
$table->foreignId('pusat_mengundi_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('ic_number', 20)->nullable();
$table->string('phone_number', 30)->nullable();
$table->string('agency')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('wheelchair_allocations', function (Blueprint $table) {
$table->id();
$table->foreignId('election_id')->constrained()->cascadeOnDelete();
$table->foreignId('pusat_mengundi_id')->constrained()->cascadeOnDelete();
$table->unsignedInteger('allocated_quantity')->default(0);
$table->text('notes')->nullable();
$table->timestamps();
$table->unique(['election_id', 'pusat_mengundi_id']);
});
Schema::create('wheelchair_transactions', function (Blueprint $table) {
$table->id();
$table->foreignId('wheelchair_allocation_id')->constrained()->cascadeOnDelete();
$table->string('transaction_type')->index();
$table->unsignedInteger('quantity')->default(0);
$table->timestamp('taken_at')->nullable();
$table->string('taken_by_name')->nullable();
$table->timestamp('returned_at')->nullable();
$table->string('return_condition')->nullable();
$table->foreignId('recorded_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->text('notes')->nullable();
$table->timestamps();
});
Schema::create('attendances', function (Blueprint $table) {
$table->id();
$table->foreignId('election_id')->constrained()->cascadeOnDelete();
$table->foreignId('staff_assignment_id')->constrained()->cascadeOnDelete();
$table->foreignId('pusat_mengundi_id')->constrained()->cascadeOnDelete();
$table->foreignId('saluran_mengundi_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('position_id')->constrained()->restrictOnDelete();
$table->string('status')->default('not_recorded')->index();
$table->timestamp('check_in_time')->nullable();
$table->foreignId('recorded_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->text('note')->nullable();
$table->timestamps();
$table->unique(['election_id', 'staff_assignment_id'], 'att_election_staff_unique');
$table->index(['election_id', 'pusat_mengundi_id', 'status'], 'att_election_pusat_status_idx');
});
Schema::create('bank_verifications', function (Blueprint $table) {
$table->id();
$table->foreignId('application_id')->unique()->constrained()->cascadeOnDelete();
$table->string('status')->default('pending')->index();
$table->foreignId('verified_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('verified_at')->nullable();
$table->text('finance_note')->nullable();
$table->timestamps();
});
Schema::create('export_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('election_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('generated_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('generated_at')->nullable();
$table->string('report_type')->index();
$table->json('filter_parameters')->nullable();
$table->string('file_name');
$table->string('disk')->nullable();
$table->string('path')->nullable();
$table->timestamps();
$table->index(['generated_by_user_id', 'generated_at'], 'exports_user_generated_idx');
});
Schema::create('system_notes', function (Blueprint $table) {
$table->id();
$table->foreignId('election_id')->nullable()->constrained()->nullOnDelete();
$table->morphs('noteable');
$table->string('note_type')->index();
$table->text('note');
$table->foreignId('created_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::disableForeignKeyConstraints();
Schema::dropIfExists('system_notes');
Schema::dropIfExists('export_logs');
Schema::dropIfExists('bank_verifications');
Schema::dropIfExists('attendances');
Schema::dropIfExists('wheelchair_transactions');
Schema::dropIfExists('wheelchair_allocations');
Schema::dropIfExists('jkm_representatives');
Schema::dropIfExists('kkm_representatives');
Schema::dropIfExists('police_escorts');
Schema::dropIfExists('staff_assignment_histories');
Schema::dropIfExists('staff_assignments');
Schema::dropIfExists('application_status_histories');
Schema::dropIfExists('application_documents');
Schema::dropIfExists('applications');
Schema::dropIfExists('position_quotas');
Schema::dropIfExists('positions');
Schema::dropIfExists('saluran_mengundis');
Schema::dropIfExists('pusat_mengundis');
Schema::dropIfExists('daerah_mengundis');
Schema::dropIfExists('bahagian_pilihanrayas');
Schema::dropIfExists('election_settings');
Schema::dropIfExists('elections');
Schema::dropIfExists('user_profiles');
Schema::enableForeignKeyConstraints();
}
};

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
DB::table('positions')
->where('code', 'CALON_SIMPANAN')
->update([
'code' => 'CALON_TAMBAHAN',
'name' => 'Calon Tambahan',
]);
}
public function down(): void
{
DB::table('positions')
->where('code', 'CALON_TAMBAHAN')
->update([
'code' => 'CALON_SIMPANAN',
'name' => 'Calon Simpanan',
]);
}
};

View File

@@ -0,0 +1,43 @@
<?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
{
// applications: (election_id, status) for dashboard count and admin list queries
// applications: (selected_ktm_assignment_id, requested_position_id) for
// KtmVacancyService reserved-application count query
Schema::table('applications', function (Blueprint $table): void {
$table->index(['election_id', 'status'], 'app_election_status_idx');
$table->index(
['selected_ktm_assignment_id', 'requested_position_id'],
'app_ktm_position_idx'
);
});
// staff_assignments: (reports_to_assignment_id, position_id, status) for
// KtmVacancyService active-assignment count under a KTM
Schema::table('staff_assignments', function (Blueprint $table): void {
$table->index(
['reports_to_assignment_id', 'position_id', 'status'],
'sa_reports_to_pos_status_idx'
);
});
}
public function down(): void
{
Schema::table('applications', function (Blueprint $table): void {
$table->dropIndex('app_election_status_idx');
$table->dropIndex('app_ktm_position_idx');
});
Schema::table('staff_assignments', function (Blueprint $table): void {
$table->dropIndex('sa_reports_to_pos_status_idx');
});
}
};

View File

@@ -0,0 +1,24 @@
<?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('export_logs', function (Blueprint $table): void {
$table->timestamp('purged_at')->nullable()->after('path');
$table->foreignId('purged_by_user_id')->nullable()->constrained('users')->nullOnDelete()->after('purged_at');
});
}
public function down(): void
{
Schema::table('export_logs', function (Blueprint $table): void {
$table->dropConstrainedForeignId('purged_by_user_id');
$table->dropColumn('purged_at');
});
}
};

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('duns', function (Blueprint $table): void {
$table->id();
$table->foreignId('election_id')->constrained()->cascadeOnDelete();
$table->string('code', 20);
$table->string('name');
$table->timestamps();
$table->softDeletes();
$table->unique(['election_id', 'code']);
});
}
public function down(): void
{
Schema::dropIfExists('duns');
}
};

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::table('pusat_mengundis', function (Blueprint $table): void {
// Allow existing records that reference daerah_mengundi_id to remain valid.
$table->unsignedBigInteger('daerah_mengundi_id')->nullable()->change();
$table->foreignId('dun_id')->nullable()->after('election_id')
->constrained('duns')->nullOnDelete();
$table->string('ppm_name')->nullable()->after('address');
$table->string('ppm_phone', 30)->nullable()->after('ppm_name');
});
}
public function down(): void
{
Schema::table('pusat_mengundis', function (Blueprint $table): void {
$table->dropConstrainedForeignId('dun_id');
$table->dropColumn(['ppm_name', 'ppm_phone']);
$table->unsignedBigInteger('daerah_mengundi_id')->nullable(false)->change();
});
}
};

View File

@@ -0,0 +1,25 @@
<?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::table('wheelchair_transactions', function (Blueprint $table) {
$table->string('returned_by_name')->nullable()->after('return_condition');
});
}
public function down(): void
{
Schema::table('wheelchair_transactions', function (Blueprint $table) {
$table->dropColumn('returned_by_name');
});
}
};

View File

@@ -0,0 +1,38 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Seed the application's database.
*/
public function run(): void
{
$this->call(RolePermissionSeeder::class);
$this->call(PositionSeeder::class);
$admin = User::query()->firstOrCreate([
'email' => config('election.seed_admin.email'),
], [
'name' => config('election.seed_admin.name'),
'password' => Hash::make(config('election.seed_admin.password')),
'email_verified_at' => now(),
]);
if ($admin->email_verified_at === null) {
$admin->forceFill(['email_verified_at' => now()])->save();
}
$admin->assignRole('Admin');
$this->call(SampleElectionSeeder::class);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Database\Seeders;
use App\Models\Position;
use Illuminate\Database\Seeder;
class PositionSeeder extends Seeder
{
/**
* Seed default election staffing positions.
*/
public function run(): void
{
$positions = [
['code' => 'PPM', 'name' => 'Penyelia Pusat Mengundi', 'scope' => 'pusat', 'is_public_applyable' => false, 'is_assignable' => true, 'allows_dual_role' => true, 'sort_order' => 10],
['code' => 'KTM', 'name' => 'Ketua Tempat Mengundi', 'scope' => 'saluran', 'is_public_applyable' => true, 'is_assignable' => true, 'allows_dual_role' => true, 'sort_order' => 20],
['code' => 'KPDP', 'name' => 'Kerani Penyemak Daftar Pemilih', 'scope' => 'saluran', 'is_public_applyable' => true, 'is_assignable' => true, 'allows_dual_role' => false, 'sort_order' => 30],
['code' => 'KP', 'name' => 'Kerani Pengundian', 'scope' => 'saluran', 'is_public_applyable' => true, 'is_assignable' => true, 'allows_dual_role' => false, 'sort_order' => 40],
['code' => 'PAPM', 'name' => 'Penolong Penyelia Pusat Mengundi', 'scope' => 'pusat', 'is_public_applyable' => true, 'is_assignable' => true, 'allows_dual_role' => false, 'sort_order' => 50],
['code' => 'POLIS', 'name' => 'Polis Pengiring', 'scope' => 'external', 'is_public_applyable' => false, 'is_assignable' => false, 'allows_dual_role' => false, 'sort_order' => 60],
['code' => 'KKM', 'name' => 'Wakil KKM', 'scope' => 'external', 'is_public_applyable' => false, 'is_assignable' => false, 'allows_dual_role' => false, 'sort_order' => 70],
['code' => 'JKM', 'name' => 'Wakil JKM', 'scope' => 'external', 'is_public_applyable' => false, 'is_assignable' => false, 'allows_dual_role' => false, 'sort_order' => 80],
['code' => 'CALON_SIMPANAN', 'name' => 'Calon Simpanan', 'scope' => 'saluran', 'is_public_applyable' => false, 'is_assignable' => true, 'allows_dual_role' => false, 'sort_order' => 90],
];
foreach ($positions as $position) {
Position::query()->updateOrCreate(
['code' => $position['code']],
$position,
);
}
Position::query()
->where('code', 'CALON_SIMPANAN')
->update([
'code' => 'CALON_TAMBAHAN',
'name' => 'Calon Tambahan',
]);
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar;
class RolePermissionSeeder extends Seeder
{
/**
* Seed the application's roles and permissions.
*/
public function run(): void
{
app(PermissionRegistrar::class)->forgetCachedPermissions();
$permissions = [
'admin.dashboard.view',
'users.manage',
'roles.manage',
'election.manage',
'hierarchy.manage',
'positions.manage',
'quotas.manage',
'qr.manage',
'applications.view_all',
'applications.create_manual',
'applications.update',
'applications.approve',
'applications.reject',
'assignments.manage_all',
'representatives.manage',
'wheelchairs.manage',
'attendance.view_all',
'attendance.record_all',
'finance.dashboard.view',
'finance.view',
'finance.verify',
'finance.export',
'applications.view_finance_fields',
'exports.generate_all',
'audit.view',
'system_notes.create',
'ppm.dashboard.view',
'applications.view_own_pusat',
'applications.review_own_pusat',
'applications.approve_own_pusat',
'applications.reject_own_pusat',
'applications.change_role_own_pusat',
'assignments.manage_own_pusat',
'representatives.view_own_pusat',
'attendance.record_own_pusat',
'attendance.view_own_pusat',
'exports.generate_own_pusat',
'ktm.dashboard.view',
'ktm.team.view',
'ktm.kp.create',
'ktm.kp.approve',
'ktm.kp.delete_created',
'applications.view_own_team',
'public_application.create',
'public_application.view_own_status',
];
$permissionModels = [];
foreach ($permissions as $permission) {
$permissionModels[$permission] = Permission::findOrCreate($permission, 'web');
}
$rolePermissions = [
'Admin' => $permissions,
'Admin Kewangan' => [
'finance.dashboard.view',
'finance.view',
'finance.verify',
'finance.export',
'applications.view_finance_fields',
],
'PPM' => [
'ppm.dashboard.view',
'applications.view_own_pusat',
'applications.review_own_pusat',
'applications.approve_own_pusat',
'applications.reject_own_pusat',
'applications.change_role_own_pusat',
'assignments.manage_own_pusat',
'representatives.view_own_pusat',
'attendance.record_own_pusat',
'attendance.view_own_pusat',
'exports.generate_own_pusat',
],
'KTM' => [
'ktm.dashboard.view',
'ktm.team.view',
'ktm.kp.create',
'ktm.kp.approve',
'ktm.kp.delete_created',
'applications.view_own_team',
],
'Pemohon' => [
'public_application.create',
'public_application.view_own_status',
],
];
foreach ($rolePermissions as $roleName => $permissionNames) {
$role = Role::findOrCreate($roleName, 'web');
$role->syncPermissions(array_map(
fn (string $permission) => $permissionModels[$permission],
$permissionNames,
));
}
app(PermissionRegistrar::class)->forgetCachedPermissions();
}
}

View File

@@ -0,0 +1,248 @@
<?php
namespace Database\Seeders;
use App\Models\Dun;
use App\Models\Election;
use App\Models\Position;
use App\Models\PositionQuota;
use App\Models\PusatMengundi;
use App\Models\SaluranMengundi;
use App\Models\StaffAssignment;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class SampleElectionSeeder extends Seeder
{
/**
* Seed sample election hierarchy for development and tests.
*/
public function run(): void
{
$this->cleanupSampleData();
$election = Election::query()->firstOrNew(['code' => 'PRN2026']);
if (! $election->exists) {
$election->public_uuid = (string) Str::uuid();
}
$election->forceFill([
'name' => 'Pilihanraya Negeri 2026',
'status' => 'active',
'starts_at' => now()->toDateString(),
'polling_date' => now()->addWeeks(4)->toDateString(),
])->save();
$election->settings()->updateOrCreate(
['election_id' => $election->id],
[
'registration_start_date' => now()->toDateString(),
'registration_end_date' => now()->addWeeks(3)->toDateString(),
'polling_date' => $election->polling_date,
'is_registration_open' => true,
'is_registration_open_override' => null,
'is_attendance_active' => false,
],
);
$dun = Dun::query()->updateOrCreate(
['election_id' => $election->id, 'code' => 'N49'],
['name' => 'Kota Iskandar'],
);
$pusat = new PusatMengundi;
$pusat->public_uuid = (string) Str::uuid();
$pusat->forceFill([
'election_id' => $election->id,
'code' => 'PM001',
'dun_id' => $dun->id,
'name' => 'SK Seri Contoh',
'address' => 'Jalan Contoh 1, 43000 Kajang, Selangor',
])->save();
$pusat->forceFill([
'registration_url' => url('/pohon/'.$pusat->public_uuid),
])->save();
$this->seedSaluran($election, $pusat);
$this->seedQuotas($election, $pusat);
$this->seedOperationalUsersAndAssignments($election, $pusat);
$pusat->wheelchairAllocation()->updateOrCreate(
['election_id' => $election->id],
[
'allocated_quantity' => 2,
'notes' => 'Sample allocation for Phase 2.',
],
);
}
private function cleanupSampleData(): void
{
$election = Election::query()->where('code', 'PRN2026')->first();
if (! $election) {
return;
}
PositionQuota::query()->where('election_id', $election->id)->forceDelete();
SaluranMengundi::query()->where('election_id', $election->id)->forceDelete();
PusatMengundi::query()->where('election_id', $election->id)->forceDelete();
}
private function seedSaluran(Election $election, PusatMengundi $pusat): void
{
foreach (range(1, 3) as $number) {
$pusat->saluranMengundis()->updateOrCreate(
['number' => (string) $number],
[
'election_id' => $election->id,
'name' => null,
'voter_count' => 450 + ($number * 25),
],
);
}
}
private function seedQuotas(Election $election, PusatMengundi $pusat): void
{
$positions = Position::query()->whereIn('code', [
'PPM',
'PAPM',
'KTM',
'KPDP',
'KP',
'POLIS',
'CALON_TAMBAHAN',
])->get()->keyBy('code');
PositionQuota::query()->updateOrCreate(
[
'election_id' => $election->id,
'pusat_mengundi_id' => $pusat->id,
'saluran_mengundi_id' => null,
'position_id' => $positions['PPM']->id,
],
['quota' => 1],
);
PositionQuota::query()->updateOrCreate(
[
'election_id' => $election->id,
'pusat_mengundi_id' => $pusat->id,
'saluran_mengundi_id' => null,
'position_id' => $positions['PAPM']->id,
],
['quota' => 3],
);
/** @var Collection<int, SaluranMengundi> $salurans */
$salurans = $pusat->saluranMengundis()->get();
foreach ($salurans as $saluran) {
foreach ([
'KTM' => 1,
'KPDP' => 1,
'KP' => 4,
'POLIS' => 1,
'CALON_TAMBAHAN' => 1,
] as $code => $quota) {
PositionQuota::query()->updateOrCreate(
[
'election_id' => $election->id,
'pusat_mengundi_id' => $pusat->id,
'saluran_mengundi_id' => $saluran->id,
'position_id' => $positions[$code]->id,
],
['quota' => $quota],
);
}
}
}
private function seedOperationalUsersAndAssignments(Election $election, PusatMengundi $pusat): void
{
$ppm = $this->seedUser('PPM Contoh', 'ppm@prn.local', ['PPM', 'KTM']);
$ktm = $this->seedUser('KTM Contoh', 'ktm@prn.local', ['KTM']);
$this->seedUser('Admin Kewangan Contoh', 'kewangan@prn.local', ['Admin Kewangan']);
$positions = Position::query()->whereIn('code', ['PPM', 'KTM'])->get()->keyBy('code');
/** @var SaluranMengundi $saluranPertama */
$saluranPertama = $pusat->saluranMengundis()->where('number', '1')->firstOrFail();
/** @var SaluranMengundi $saluranKedua */
$saluranKedua = $pusat->saluranMengundis()->where('number', '2')->firstOrFail();
StaffAssignment::query()->updateOrCreate(
[
'election_id' => $election->id,
'user_id' => $ppm->id,
'position_id' => $positions['PPM']->id,
'pusat_mengundi_id' => $pusat->id,
'saluran_mengundi_id' => null,
],
[
'status' => 'active',
'assigned_at' => now(),
'source' => 'sample_seed',
],
);
StaffAssignment::query()->updateOrCreate(
[
'election_id' => $election->id,
'user_id' => $ppm->id,
'position_id' => $positions['KTM']->id,
'pusat_mengundi_id' => $pusat->id,
'saluran_mengundi_id' => $saluranPertama->id,
],
[
'status' => 'active',
'assigned_at' => now(),
'source' => 'sample_seed_dual_role',
],
);
StaffAssignment::query()->updateOrCreate(
[
'election_id' => $election->id,
'user_id' => $ktm->id,
'position_id' => $positions['KTM']->id,
'pusat_mengundi_id' => $pusat->id,
'saluran_mengundi_id' => $saluranKedua->id,
],
[
'status' => 'active',
'assigned_at' => now(),
'source' => 'sample_seed',
],
);
}
/**
* @param array<int, string> $roles
*/
private function seedUser(string $name, string $email, array $roles): User
{
$user = User::query()->firstOrCreate(
['email' => $email],
[
'name' => $name,
'password' => Hash::make('password'),
'email_verified_at' => now(),
],
);
if ($user->email_verified_at === null) {
$user->forceFill(['email_verified_at' => now()])->save();
}
$user->syncRoles($roles);
return $user;
}
}