first
This commit is contained in:
1
database/.gitignore
vendored
Normal file
1
database/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
44
database/factories/ApplicationFactory.php
Normal file
44
database/factories/ApplicationFactory.php
Normal 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('############'),
|
||||
];
|
||||
}
|
||||
}
|
||||
27
database/factories/BahagianPilihanrayaFactory.php
Normal file
27
database/factories/BahagianPilihanrayaFactory.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
39
database/factories/DaerahMengundiFactory.php
Normal file
39
database/factories/DaerahMengundiFactory.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
25
database/factories/DunFactory.php
Normal file
25
database/factories/DunFactory.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
32
database/factories/ElectionFactory.php
Normal file
32
database/factories/ElectionFactory.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
32
database/factories/PositionFactory.php
Normal file
32
database/factories/PositionFactory.php
Normal 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),
|
||||
];
|
||||
}
|
||||
}
|
||||
33
database/factories/PusatMengundiFactory.php
Normal file
33
database/factories/PusatMengundiFactory.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
31
database/factories/SaluranMengundiFactory.php
Normal file
31
database/factories/SaluranMengundiFactory.php
Normal 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),
|
||||
];
|
||||
}
|
||||
}
|
||||
33
database/factories/StaffAssignmentFactory.php
Normal file
33
database/factories/StaffAssignmentFactory.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
45
database/factories/UserFactory.php
Normal file
45
database/factories/UserFactory.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal 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');
|
||||
}
|
||||
};
|
||||
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal 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');
|
||||
}
|
||||
};
|
||||
59
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
59
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal 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');
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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']);
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
]);
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
27
database/migrations/2026_06_02_000002_create_duns_table.php
Normal file
27
database/migrations/2026_06_02_000002_create_duns_table.php
Normal 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');
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
38
database/seeders/DatabaseSeeder.php
Normal file
38
database/seeders/DatabaseSeeder.php
Normal 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);
|
||||
}
|
||||
}
|
||||
41
database/seeders/PositionSeeder.php
Normal file
41
database/seeders/PositionSeeder.php
Normal 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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
119
database/seeders/RolePermissionSeeder.php
Normal file
119
database/seeders/RolePermissionSeeder.php
Normal 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();
|
||||
}
|
||||
}
|
||||
248
database/seeders/SampleElectionSeeder.php
Normal file
248
database/seeders/SampleElectionSeeder.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user