DONE: layout letter with letterhead and footer, notification for newly...
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'MembershipApplication',
|
||||
];
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('membership_applications', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->string('application_number');
|
||||
$table->foreignUuid('user_id')->constrained('users')->onDelete('cascade')->nullable();
|
||||
$table->string('status');
|
||||
$table->string('board_result');
|
||||
$table->string('submitted_at');
|
||||
$table->string('completed_at');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('membership_applications');
|
||||
}
|
||||
};
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('membership_application_applicants', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignUuid('membership_application_id')->constrained('membership_applications')->onDelete('cascade');
|
||||
$table->string('name');
|
||||
$table->string('email');
|
||||
$table->string('ic_number');
|
||||
$table->string('birth_date');
|
||||
$table->string('birth_place');
|
||||
$table->string('marriage_status')->comment('Belum Berkahwin, Berkahwin, Bercerai, Balu, Duda');
|
||||
|
||||
// contact information
|
||||
$table->text('address');
|
||||
$table->string('phone_number');
|
||||
$table->string('office_number')->nullable();
|
||||
$table->string('postcode');
|
||||
|
||||
// Employment
|
||||
$table->string('employer_name');
|
||||
$table->text('employer_address');
|
||||
$table->string('current_position');
|
||||
$table->date('start_work_date');
|
||||
|
||||
// contribution
|
||||
$table->decimal('stock_monthly_contribution', 10, 2);
|
||||
$table->decimal('fee_monthly_contribution', 10, 2);
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
DB::statement('
|
||||
ALTER TABLE membership_application_applicants
|
||||
ADD COLUMN gender user_gender_enum NULL
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('membership_application_applicants');
|
||||
}
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('membership_application_heirs', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignUuid('membership_application_id')->constrained('membership_applications')->onDelete('cascade');
|
||||
$table->string('name');
|
||||
$table->string('ic_number');
|
||||
$table->string('relationship')->comment('Isteri, Anak, Orang Tua, Saudara, Lain-lain');
|
||||
$table->string('phone_number');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('membership_application_heirs');
|
||||
}
|
||||
};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?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('membership_application_references', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignUuid('membership_application_id')
|
||||
->constrained('membership_applications')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->string('reference_type')->comment('PROPOSER, SUPPORTER');
|
||||
|
||||
// Applicant may select at submit; admin may fill in later
|
||||
$table->foreignUuid('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
|
||||
// Set only when admin assigns or updates this reference
|
||||
$table->foreignUuid('assigned_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('assigned_at')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['membership_application_id', 'reference_type']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('membership_application_references');
|
||||
}
|
||||
};
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?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('membership_application_reviews', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignUuid('membership_application_id')
|
||||
->constrained('membership_applications')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->string('stage')->comment('MANAGEMENT, BOARD');
|
||||
$table->string('decision')->comment('MANAGEMENT: APPROVED, REJECTED | BOARD: PASS, FAIL');
|
||||
$table->text('remarks')->nullable();
|
||||
|
||||
$table->foreignUuid('reviewer_id')->constrained('users')->restrictOnDelete();
|
||||
$table->timestamp('reviewed_at');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['membership_application_id', 'stage']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('membership_application_reviews');
|
||||
}
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class MembershipApplicationDatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// $this->call([]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Emails;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Modules\MembershipApplication\Entities\MembershipApplication;
|
||||
|
||||
class MembershipApplicationFailedNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
protected MembershipApplication $application
|
||||
) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$this->application->loadMissing('applicant');
|
||||
|
||||
return (new MailMessage)
|
||||
->subject('Keputusan Permohonan Keahlian')
|
||||
->markdown('membershipapplication::emails.failed', [
|
||||
'name' => $this->application->applicant->name,
|
||||
'applicationNumber' => $this->application->application_number,
|
||||
'logoPath' => public_path('images/logo-kopkb.svg'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Emails;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Modules\MembershipApplication\Entities\MembershipApplication;
|
||||
|
||||
class MembershipApplicationPassedNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
protected MembershipApplication $application
|
||||
) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$this->application->loadMissing('applicant');
|
||||
|
||||
return (new MailMessage)
|
||||
->subject('Permohonan Keahlian Diluluskan')
|
||||
->markdown('membershipapplication::emails.passed', [
|
||||
'name' => $this->application->applicant->name,
|
||||
'applicationNumber' => $this->application->application_number,
|
||||
'logoPath' => public_path('images/logo-kopkb.svg'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Emails;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Modules\MembershipApplication\Entities\MembershipApplication;
|
||||
|
||||
class MembershipApplicationSubmittedNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
protected MembershipApplication $application
|
||||
) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$this->application->loadMissing(['applicant', 'heirs', 'documents', 'references.member']);
|
||||
|
||||
return (new MailMessage)
|
||||
->subject('Pengesahan Permohonan Keahlian')
|
||||
->markdown('membershipapplication::emails.submitted', [
|
||||
'application' => $this->application,
|
||||
'applicant' => $this->application->applicant,
|
||||
'heirs' => $this->application->heirs,
|
||||
'documents' => $this->application->documents,
|
||||
'documentLabels' => [
|
||||
'ic_copy' => 'Salinan Kad Pengenalan',
|
||||
'photo' => 'Gambar Passport',
|
||||
'salary_slip' => 'Slip Gaji',
|
||||
'employer_letter' => 'Surat Pengesahan Majikan',
|
||||
],
|
||||
'logoPath' => public_path('images/logo-kopkb.svg'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Entities;
|
||||
|
||||
use App\Traits\HasDocuments;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Modules\Auth\Entities\User;
|
||||
use Modules\MembershipApplication\Enums\ApplicationStatus;
|
||||
|
||||
class MembershipApplication extends Model
|
||||
{
|
||||
use HasDocuments, HasUuids;
|
||||
|
||||
protected $table = 'membership_applications';
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'application_number',
|
||||
'user_id',
|
||||
'status',
|
||||
'board_result',
|
||||
'submitted_at',
|
||||
'completed_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => ApplicationStatus::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function applicant(): HasOne
|
||||
{
|
||||
return $this->hasOne(MembershipApplicationApplicant::class);
|
||||
}
|
||||
|
||||
public function heirs(): HasMany
|
||||
{
|
||||
return $this->hasMany(MembershipApplicationHeir::class);
|
||||
}
|
||||
|
||||
public function references(): HasMany
|
||||
{
|
||||
return $this->hasMany(MembershipApplicationReference::class);
|
||||
}
|
||||
|
||||
public function reviews(): HasMany
|
||||
{
|
||||
return $this->hasMany(MembershipApplicationReview::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Entities;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class MembershipApplicationApplicant extends Model
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
protected $table = 'membership_application_applicants';
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'membership_application_id',
|
||||
'name',
|
||||
'email',
|
||||
'ic_number',
|
||||
'birth_date',
|
||||
'birth_place',
|
||||
'gender',
|
||||
'marriage_status',
|
||||
'address',
|
||||
'phone_number',
|
||||
'office_number',
|
||||
'postcode',
|
||||
'employer_name',
|
||||
'employer_address',
|
||||
'current_position',
|
||||
'start_work_date',
|
||||
'stock_monthly_contribution',
|
||||
'fee_monthly_contribution',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'start_work_date' => 'date',
|
||||
'stock_monthly_contribution' => 'decimal:2',
|
||||
'fee_monthly_contribution' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
|
||||
public function membershipApplication(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(MembershipApplication::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Entities;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class MembershipApplicationHeir extends Model
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
protected $table = 'membership_application_heirs';
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'membership_application_id',
|
||||
'name',
|
||||
'ic_number',
|
||||
'relationship',
|
||||
'phone_number',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'string',
|
||||
'ic_number' => 'string',
|
||||
'relationship' => 'string',
|
||||
'phone_number' => 'string',
|
||||
];
|
||||
}
|
||||
|
||||
public function membershipApplication(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(MembershipApplication::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Entities;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Modules\Auth\Entities\User;
|
||||
use Modules\MembershipApplication\Enums\ReferenceType;
|
||||
|
||||
class MembershipApplicationReference extends Model
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
protected $table = 'membership_application_references';
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'membership_application_id',
|
||||
'reference_type',
|
||||
'user_id',
|
||||
'assigned_by',
|
||||
'assigned_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'reference_type' => ReferenceType::class,
|
||||
'assigned_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function membershipApplication(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(MembershipApplication::class);
|
||||
}
|
||||
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function assignedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'assigned_by');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Entities;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Modules\Auth\Entities\User;
|
||||
use Modules\MembershipApplication\Enums\BoardDecision;
|
||||
use Modules\MembershipApplication\Enums\ManagementDecision;
|
||||
use Modules\MembershipApplication\Enums\ReviewStage;
|
||||
|
||||
class MembershipApplicationReview extends Model
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
protected $table = 'membership_application_reviews';
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'membership_application_id',
|
||||
'stage',
|
||||
'decision',
|
||||
'remarks',
|
||||
'reviewer_id',
|
||||
'reviewed_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'stage' => ReviewStage::class,
|
||||
'reviewed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function membershipApplication(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(MembershipApplication::class);
|
||||
}
|
||||
|
||||
public function reviewer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'reviewer_id');
|
||||
}
|
||||
|
||||
public function managementDecision(): ?ManagementDecision
|
||||
{
|
||||
if ($this->stage !== ReviewStage::Management) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ManagementDecision::tryFrom($this->decision);
|
||||
}
|
||||
|
||||
public function boardDecision(): ?BoardDecision
|
||||
{
|
||||
if ($this->stage !== ReviewStage::Board) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return BoardDecision::tryFrom($this->decision);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Enums;
|
||||
|
||||
enum ApplicationStatus: string
|
||||
{
|
||||
case Submitted = 'SUBMITTED';
|
||||
case PendingBoard = 'PENDING_BOARD';
|
||||
case ManagementRejected = 'MANAGEMENT_REJECTED';
|
||||
case PendingNotification = 'PENDING_NOTIFICATION';
|
||||
case Completed = 'COMPLETED';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Enums;
|
||||
|
||||
enum BoardDecision: string
|
||||
{
|
||||
case Pass = 'PASS';
|
||||
case Fail = 'FAIL';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Enums;
|
||||
|
||||
enum BoardResult: string
|
||||
{
|
||||
case Pass = 'PASS';
|
||||
case Fail = 'FAIL';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Enums;
|
||||
|
||||
enum ManagementDecision: string
|
||||
{
|
||||
case Approved = 'APPROVED';
|
||||
case Rejected = 'REJECTED';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Enums;
|
||||
|
||||
enum ReferenceType: string
|
||||
{
|
||||
case Proposer = 'PROPOSER';
|
||||
case Supporter = 'SUPPORTER';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Enums;
|
||||
|
||||
enum ReviewStage: string
|
||||
{
|
||||
case Management = 'MANAGEMENT';
|
||||
case Board = 'BOARD';
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\DocumentService;
|
||||
use App\Services\LetterService;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Modules\MembershipApplication\Entities\MembershipApplication;
|
||||
use Modules\MembershipApplication\Enums\ApplicationStatus;
|
||||
use Modules\MembershipApplication\Enums\BoardDecision;
|
||||
use Modules\MembershipApplication\Enums\BoardResult;
|
||||
use Modules\MembershipApplication\Enums\ManagementDecision;
|
||||
use Modules\MembershipApplication\Http\Requests\AssignReferencesRequest;
|
||||
use Modules\MembershipApplication\Http\Requests\BatchCompleteRequest;
|
||||
use Modules\MembershipApplication\Http\Requests\BoardReviewRequest;
|
||||
use Modules\MembershipApplication\Http\Requests\ManagementReviewRequest;
|
||||
use Modules\MembershipApplication\Http\Requests\UpdateMembershipApplicationRequest;
|
||||
use Modules\MembershipApplication\Http\Requests\UploadMembershipApplicationDocumentRequest;
|
||||
use Modules\MembershipApplication\Services\MembershipApplicationService;
|
||||
use Modules\MembershipApplication\Transformers\MembershipApplicationListResource;
|
||||
use Modules\MembershipApplication\Transformers\MembershipApplicationResource;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class MembershipApplicationController extends Controller
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public function __construct(
|
||||
protected MembershipApplicationService $membershipApplicationService,
|
||||
protected DocumentService $documentService,
|
||||
protected LetterService $letterService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorize('viewAny', MembershipApplication::class);
|
||||
|
||||
$perPage = min((int) $request->get('per_page', 10), 100);
|
||||
$search = (string) ($request->get('search', '') ?? '');
|
||||
$status = $request->get('status');
|
||||
$sortBy = (string) ($request->get('sort_by', 'submitted_at') ?? 'submitted_at');
|
||||
$sortOrder = (string) ($request->get('sort_order', 'desc') ?? 'desc');
|
||||
|
||||
$applications = $this->membershipApplicationService->getPaginatedList(
|
||||
$perPage,
|
||||
$search,
|
||||
$status,
|
||||
$sortBy,
|
||||
$sortOrder
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => MembershipApplicationListResource::collection($applications->items()),
|
||||
'pagination' => [
|
||||
'current_page' => $applications->currentPage(),
|
||||
'per_page' => $applications->perPage(),
|
||||
'total' => $applications->total(),
|
||||
'last_page' => $applications->lastPage(),
|
||||
'from' => $applications->firstItem(),
|
||||
'to' => $applications->lastItem(),
|
||||
'has_more_pages' => $applications->hasMorePages(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(MembershipApplication $membershipApplication): JsonResponse
|
||||
{
|
||||
$this->authorize('view', $membershipApplication);
|
||||
|
||||
$application = $this->membershipApplicationService->getByIdWithRelations($membershipApplication->id);
|
||||
|
||||
if (! $application) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Permohonan tidak dijumpai.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => new MembershipApplicationResource($application),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateMembershipApplicationRequest $request, MembershipApplication $membershipApplication): JsonResponse
|
||||
{
|
||||
$this->authorize('update', $membershipApplication);
|
||||
|
||||
$application = $this->membershipApplicationService->update(
|
||||
$membershipApplication,
|
||||
$request->validated(),
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Permohonan keahlian berjaya dikemaskini.',
|
||||
'data' => new MembershipApplicationResource($application),
|
||||
]);
|
||||
}
|
||||
|
||||
public function uploadDocument(UploadMembershipApplicationDocumentRequest $request, MembershipApplication $membershipApplication): JsonResponse
|
||||
{
|
||||
$this->authorize('update', $membershipApplication);
|
||||
|
||||
$application = $this->membershipApplicationService->uploadDocument(
|
||||
$membershipApplication,
|
||||
$request->validated('type'),
|
||||
$request->file('file'),
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Dokumen berjaya dimuat naik.',
|
||||
'data' => new MembershipApplicationResource($application),
|
||||
]);
|
||||
}
|
||||
|
||||
public function managementReview(ManagementReviewRequest $request, MembershipApplication $membershipApplication): JsonResponse {
|
||||
$this->authorize('managementReview', $membershipApplication);
|
||||
|
||||
$application = $this->membershipApplicationService->managementReview(
|
||||
$membershipApplication,
|
||||
ManagementDecision::from($request->validated('decision')),
|
||||
$request->validated('remarks'),
|
||||
$request->user()
|
||||
);
|
||||
|
||||
$message = $application->status === ApplicationStatus::PendingBoard
|
||||
? 'Permohonan diluluskan dan dihantar ke lembaga.'
|
||||
: 'Permohonan ditolak.';
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => new MembershipApplicationResource($application),
|
||||
]);
|
||||
}
|
||||
|
||||
public function boardReview(BoardReviewRequest $request, MembershipApplication $membershipApplication): JsonResponse {
|
||||
$this->authorize('boardReview', $membershipApplication);
|
||||
|
||||
$application = $this->membershipApplicationService->boardReview(
|
||||
$membershipApplication,
|
||||
BoardDecision::from($request->validated('decision')),
|
||||
$request->validated('remarks'),
|
||||
$request->user()
|
||||
);
|
||||
|
||||
$message = $application->board_result === BoardResult::Pass->value
|
||||
? 'Permohonan lulus semakan lembaga.'
|
||||
: 'Permohonan gagal semakan lembaga.';
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => new MembershipApplicationResource($application),
|
||||
]);
|
||||
}
|
||||
|
||||
public function assignReferences(AssignReferencesRequest $request, MembershipApplication $membershipApplication): JsonResponse {
|
||||
$this->authorize('assignReferences', $membershipApplication);
|
||||
|
||||
$application = $this->membershipApplicationService->assignReferences(
|
||||
$membershipApplication,
|
||||
$request->validated(),
|
||||
$request->user()
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Pencadang dan penyokong berjaya dikemaskini.',
|
||||
'data' => new MembershipApplicationResource($application),
|
||||
]);
|
||||
}
|
||||
|
||||
public function complete(Request $request, MembershipApplication $membershipApplication): JsonResponse
|
||||
{
|
||||
$this->authorize('complete', $membershipApplication);
|
||||
|
||||
$application = $this->membershipApplicationService->complete(
|
||||
$membershipApplication,
|
||||
$request->user()
|
||||
);
|
||||
|
||||
$message = $application->board_result === BoardResult::Pass->value
|
||||
? 'Permohonan selesai. Akaun ahli telah dicipta dan e-mel dihantar.'
|
||||
: 'Permohonan selesai. E-mel keputusan dihantar.';
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => new MembershipApplicationResource($application),
|
||||
]);
|
||||
}
|
||||
|
||||
public function batchComplete(BatchCompleteRequest $request): JsonResponse
|
||||
{
|
||||
$this->authorize('completeBatch', MembershipApplication::class);
|
||||
|
||||
$result = $this->membershipApplicationService->completeBatch(
|
||||
$request->validated('application_ids'),
|
||||
$request->user()
|
||||
);
|
||||
|
||||
$succeededCount = count($result['succeeded']);
|
||||
$failedCount = count($result['failed']);
|
||||
|
||||
if ($succeededCount === 0) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Tiada permohonan berjaya diselesaikan.',
|
||||
'data' => [
|
||||
'succeeded' => [],
|
||||
'failed' => $result['failed'],
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$message = $failedCount > 0
|
||||
? "{$succeededCount} permohonan berjaya, {$failedCount} gagal."
|
||||
: "{$succeededCount} permohonan berjaya diselesaikan.";
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => [
|
||||
'succeeded' => MembershipApplicationResource::collection($result['succeeded']),
|
||||
'failed' => $result['failed'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function downloadDocument(MembershipApplication $membershipApplication, string $documentId): StreamedResponse {
|
||||
$this->authorize('view', $membershipApplication);
|
||||
|
||||
$document = $membershipApplication->documents()->findOrFail($documentId);
|
||||
|
||||
return $this->documentService->downloadDocument($document->id);
|
||||
}
|
||||
|
||||
public function approvedLetter(Request $request, MembershipApplication $membershipApplication): Response
|
||||
{
|
||||
$this->authorize('view', $membershipApplication);
|
||||
|
||||
$application = $this->membershipApplicationService->getByIdWithRelations($membershipApplication->id);
|
||||
|
||||
if (! $application) {
|
||||
abort(404, 'Permohonan tidak dijumpai.');
|
||||
}
|
||||
|
||||
$view = 'membershipapplication::pdf.approved-letter';
|
||||
$data = ['application' => $application];
|
||||
$format = $request->query('format', 'html');
|
||||
$filename = 'surat-kelulusan-'.$application->application_number.'.pdf';
|
||||
|
||||
if ($format === 'pdf') {
|
||||
return $this->letterService->pdfResponse($view, $data, $filename);
|
||||
}
|
||||
|
||||
return $this->letterService->htmlResponse($view, $data);
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Modules\MembershipApplication\Http\Requests\LookupMemberRequest;
|
||||
use Modules\MembershipApplication\Http\Requests\StoreMembershipApplicationRequest;
|
||||
use Modules\MembershipApplication\Services\MembershipApplicationService;
|
||||
use Modules\MembershipApplication\Transformers\MembershipApplicationResource;
|
||||
|
||||
class PublicMembershipApplicationController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected MembershipApplicationService $membershipApplicationService
|
||||
) {}
|
||||
|
||||
public function store(StoreMembershipApplicationRequest $request): JsonResponse
|
||||
{
|
||||
$documents = collect($request->file('documents', []))
|
||||
->filter()
|
||||
->all();
|
||||
|
||||
$application = $this->membershipApplicationService->submit(
|
||||
$request->safe()->except(['documents']),
|
||||
$documents
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Permohonan keahlian berjaya dihantar.',
|
||||
'data' => new MembershipApplicationResource($application),
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function lookupMember(LookupMemberRequest $request): JsonResponse
|
||||
{
|
||||
$member = $this->membershipApplicationService->lookupMemberByIcNumber(
|
||||
$request->validated('ic_number')
|
||||
);
|
||||
|
||||
if (! $member) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Ahli dengan nombor kad pengenalan ini tidak dijumpai.',
|
||||
'data' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Ahli dijumpai.',
|
||||
'data' => [
|
||||
'id' => $member->id,
|
||||
'name' => $member->name,
|
||||
'ic_number' => $member->ic_number,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AssignReferencesRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'proposer_id' => 'nullable|uuid|exists:users,id',
|
||||
'supporter_id' => 'nullable|uuid|exists:users,id',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'proposer_id.exists' => 'Pencadang tidak dijumpai.',
|
||||
'supporter_id.exists' => 'Penyokong tidak dijumpai.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class BatchCompleteRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'application_ids' => 'required|array|min:1|max:100',
|
||||
'application_ids.*' => 'required|uuid|distinct|exists:membership_applications,id',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'application_ids.required' => 'Senarai permohonan diperlukan.',
|
||||
'application_ids.min' => 'Sekurang-kurangnya satu permohonan diperlukan.',
|
||||
'application_ids.max' => 'Maksimum 100 permohonan setiap batch.',
|
||||
'application_ids.*.exists' => 'Salah satu permohonan tidak dijumpai.',
|
||||
'application_ids.*.distinct' => 'Permohonan duplikat tidak dibenarkan.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\MembershipApplication\Enums\BoardDecision;
|
||||
|
||||
class BoardReviewRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'decision' => ['required', Rule::enum(BoardDecision::class)],
|
||||
'remarks' => 'nullable|string|max:1000',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'decision.required' => 'Keputusan diperlukan.',
|
||||
'decision.enum' => 'Keputusan tidak sah. Pilih PASS atau FAIL.',
|
||||
'remarks.max' => 'Catatan tidak boleh melebihi 1000 aksara.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LookupMemberRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ic_number' => 'required|string|max:20',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'ic_number.required' => 'Nombor kad pengenalan diperlukan.',
|
||||
'ic_number.max' => 'Nombor kad pengenalan tidak sah.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\MembershipApplication\Enums\ManagementDecision;
|
||||
|
||||
class ManagementReviewRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'decision' => ['required', Rule::enum(ManagementDecision::class)],
|
||||
'remarks' => 'nullable|string|max:1000',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'decision.required' => 'Keputusan diperlukan.',
|
||||
'decision.enum' => 'Keputusan tidak sah. Pilih APPROVED atau REJECTED.',
|
||||
'remarks.max' => 'Catatan tidak boleh melebihi 1000 aksara.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreMembershipApplicationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'applicant.name' => 'required|string|max:255',
|
||||
'applicant.email' => 'required|email|max:255',
|
||||
'applicant.ic_number' => 'required|string|max:20',
|
||||
'applicant.birth_date' => 'required|date',
|
||||
'applicant.birth_place' => 'required|string|max:255',
|
||||
'applicant.gender' => ['required', Rule::in(['Lelaki', 'Perempuan'])],
|
||||
'applicant.marriage_status' => 'required|string|max:255',
|
||||
'applicant.address' => 'required|string',
|
||||
'applicant.phone_number' => 'required|string|max:20',
|
||||
'applicant.office_number' => 'nullable|string|max:20',
|
||||
'applicant.postcode' => 'required|string|max:10',
|
||||
'applicant.employer_name' => 'required|string|max:255',
|
||||
'applicant.employer_address' => 'required|string',
|
||||
'applicant.current_position' => 'required|string|max:255',
|
||||
'applicant.start_work_date' => 'required|date',
|
||||
'applicant.stock_monthly_contribution' => 'required|numeric|min:0',
|
||||
'applicant.fee_monthly_contribution' => 'required|numeric|min:0',
|
||||
|
||||
'heirs' => 'required|array|min:1',
|
||||
'heirs.*.name' => 'required|string|max:255',
|
||||
'heirs.*.ic_number' => 'required|string|max:20',
|
||||
'heirs.*.relationship' => 'required|string|max:255',
|
||||
'heirs.*.phone_number' => 'required|string|max:20',
|
||||
|
||||
'references.proposer_ic_number' => 'nullable|string|max:20',
|
||||
'references.supporter_ic_number' => 'nullable|string|max:20',
|
||||
|
||||
'documents.ic_copy' => 'required|file|mimes:pdf,jpg,jpeg,png|max:10240',
|
||||
'documents.photo' => 'nullable|file|mimes:jpg,jpeg,png|max:10240',
|
||||
'documents.salary_slip' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:10240',
|
||||
'documents.employer_letter' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:10240',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'applicant.name.required' => 'Nama penuh diperlukan.',
|
||||
'applicant.email.required' => 'Emel diperlukan.',
|
||||
'applicant.email.email' => 'Emel tidak sah.',
|
||||
'applicant.ic_number.required' => 'Nombor kad pengenalan diperlukan.',
|
||||
'applicant.birth_date.required' => 'Tarikh lahir diperlukan.',
|
||||
'applicant.birth_place.required' => 'Tempat lahir diperlukan.',
|
||||
'applicant.gender.required' => 'Jantina diperlukan.',
|
||||
'applicant.gender.in' => 'Jantina tidak sah.',
|
||||
'applicant.marriage_status.required' => 'Status perkahwinan diperlukan.',
|
||||
'applicant.address.required' => 'Alamat diperlukan.',
|
||||
'applicant.phone_number.required' => 'Nombor telefon diperlukan.',
|
||||
'applicant.postcode.required' => 'Poskod diperlukan.',
|
||||
'applicant.employer_name.required' => 'Nama majikan diperlukan.',
|
||||
'applicant.employer_address.required' => 'Alamat majikan diperlukan.',
|
||||
'applicant.current_position.required' => 'Jawatan semasa diperlukan.',
|
||||
'applicant.start_work_date.required' => 'Tarikh mula berkhidmat diperlukan.',
|
||||
'applicant.stock_monthly_contribution.required' => 'Caruman saham bulanan diperlukan.',
|
||||
'applicant.fee_monthly_contribution.required' => 'Caruman yuran bulanan diperlukan.',
|
||||
|
||||
'heirs.required' => 'Maklumat waris diperlukan.',
|
||||
'heirs.min' => 'Sekurang-kurangnya satu waris diperlukan.',
|
||||
'heirs.*.name.required' => 'Nama waris diperlukan.',
|
||||
'heirs.*.ic_number.required' => 'Nombor kad pengenalan waris diperlukan.',
|
||||
'heirs.*.relationship.required' => 'Hubungan waris diperlukan.',
|
||||
'heirs.*.phone_number.required' => 'Nombor telefon waris diperlukan.',
|
||||
|
||||
'references.proposer_ic_number.max' => 'Nombor kad pengenalan pencadang tidak sah.',
|
||||
'references.supporter_ic_number.max' => 'Nombor kad pengenalan penyokong tidak sah.',
|
||||
|
||||
'documents.ic_copy.required' => 'Salinan kad pengenalan diperlukan.',
|
||||
'documents.ic_copy.mimes' => 'Salinan kad pengenalan mestilah PDF, JPG, JPEG atau PNG.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateMembershipApplicationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function validationData(): array
|
||||
{
|
||||
return $this->except(array_keys($this->route()?->parameters() ?? []));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'applicant' => 'sometimes|array',
|
||||
'applicant.name' => 'sometimes|required|string|max:255',
|
||||
'applicant.email' => 'sometimes|required|email|max:255',
|
||||
'applicant.ic_number' => 'sometimes|required|string|max:20',
|
||||
'applicant.birth_date' => 'sometimes|required|date',
|
||||
'applicant.birth_place' => 'sometimes|required|string|max:255',
|
||||
'applicant.gender' => ['sometimes', 'required', Rule::in(['Lelaki', 'Perempuan'])],
|
||||
'applicant.marriage_status' => 'sometimes|required|string|max:255',
|
||||
'applicant.address' => 'sometimes|required|string',
|
||||
'applicant.phone_number' => 'sometimes|required|string|max:20',
|
||||
'applicant.office_number' => 'nullable|string|max:20',
|
||||
'applicant.postcode' => 'sometimes|required|string|max:10',
|
||||
'applicant.employer_name' => 'sometimes|required|string|max:255',
|
||||
'applicant.employer_address' => 'sometimes|required|string',
|
||||
'applicant.current_position' => 'sometimes|required|string|max:255',
|
||||
'applicant.start_work_date' => 'sometimes|required|date',
|
||||
'applicant.stock_monthly_contribution' => 'sometimes|required|numeric|min:0',
|
||||
'applicant.fee_monthly_contribution' => 'sometimes|required|numeric|min:0',
|
||||
|
||||
'heirs' => 'sometimes|array|min:1',
|
||||
'heirs.*.name' => 'required|string|max:255',
|
||||
'heirs.*.ic_number' => 'required|string|max:20',
|
||||
'heirs.*.relationship' => 'required|string|max:255',
|
||||
'heirs.*.phone_number' => 'required|string|max:20',
|
||||
|
||||
'references' => 'sometimes|array',
|
||||
'references.proposer_ic_number' => 'nullable|string|max:20',
|
||||
'references.supporter_ic_number' => 'nullable|string|max:20',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'applicant.name.required' => 'Nama penuh diperlukan.',
|
||||
'applicant.email.required' => 'Emel diperlukan.',
|
||||
'applicant.email.email' => 'Emel tidak sah.',
|
||||
'applicant.ic_number.required' => 'Nombor kad pengenalan diperlukan.',
|
||||
'applicant.birth_date.required' => 'Tarikh lahir diperlukan.',
|
||||
'applicant.birth_place.required' => 'Tempat lahir diperlukan.',
|
||||
'applicant.gender.required' => 'Jantina diperlukan.',
|
||||
'applicant.gender.in' => 'Jantina tidak sah.',
|
||||
'applicant.marriage_status.required' => 'Status perkahwinan diperlukan.',
|
||||
'applicant.address.required' => 'Alamat diperlukan.',
|
||||
'applicant.phone_number.required' => 'Nombor telefon diperlukan.',
|
||||
'applicant.postcode.required' => 'Poskod diperlukan.',
|
||||
'applicant.employer_name.required' => 'Nama majikan diperlukan.',
|
||||
'applicant.employer_address.required' => 'Alamat majikan diperlukan.',
|
||||
'applicant.current_position.required' => 'Jawatan semasa diperlukan.',
|
||||
'applicant.start_work_date.required' => 'Tarikh mula berkhidmat diperlukan.',
|
||||
'applicant.stock_monthly_contribution.required' => 'Caruman saham bulanan diperlukan.',
|
||||
'applicant.fee_monthly_contribution.required' => 'Caruman yuran bulanan diperlukan.',
|
||||
|
||||
'heirs.min' => 'Sekurang-kurangnya satu waris diperlukan.',
|
||||
'heirs.*.name.required' => 'Nama waris diperlukan.',
|
||||
'heirs.*.ic_number.required' => 'Nombor kad pengenalan waris diperlukan.',
|
||||
'heirs.*.relationship.required' => 'Hubungan waris diperlukan.',
|
||||
'heirs.*.phone_number.required' => 'Nombor telefon waris diperlukan.',
|
||||
|
||||
'references.proposer_ic_number.max' => 'Nombor kad pengenalan pencadang tidak sah.',
|
||||
'references.supporter_ic_number.max' => 'Nombor kad pengenalan penyokong tidak sah.',
|
||||
];
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UploadMembershipApplicationDocumentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'type' => ['required', Rule::in(['ic_copy', 'photo', 'salary_slip', 'employer_letter'])],
|
||||
'file' => [
|
||||
'required',
|
||||
'file',
|
||||
'max:10240',
|
||||
Rule::when(
|
||||
$this->input('type') === 'photo',
|
||||
'mimes:jpg,jpeg,png',
|
||||
'mimes:pdf,jpg,jpeg,png',
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'type.required' => 'Jenis dokumen diperlukan.',
|
||||
'type.in' => 'Jenis dokumen tidak sah.',
|
||||
'file.required' => 'Fail diperlukan.',
|
||||
'file.mimes' => 'Format fail tidak disokong.',
|
||||
'file.max' => 'Saiz fail melebihi had maksimum 10MB.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Policies;
|
||||
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Modules\MembershipApplication\Entities\MembershipApplication;
|
||||
|
||||
class MembershipApplicationPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny($user): bool
|
||||
{
|
||||
return $user->hasPermissionTo('lihat permohonan keahlian');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view($user, ?MembershipApplication $membershipApplication = null): bool
|
||||
{
|
||||
return $user->hasPermissionTo('lihat permohonan keahlian');
|
||||
}
|
||||
|
||||
/**
|
||||
* Management officer: approve or reject an application.
|
||||
*/
|
||||
public function managementReview($user, ?MembershipApplication $membershipApplication = null): bool
|
||||
{
|
||||
return $user->hasPermissionTo('semak permohonan keahlian pentadbiran');
|
||||
}
|
||||
|
||||
/**
|
||||
* Board member: pass or fail an application.
|
||||
*/
|
||||
public function boardReview($user, ?MembershipApplication $membershipApplication = null): bool
|
||||
{
|
||||
return $user->hasPermissionTo('semak permohonan keahlian lembaga');
|
||||
}
|
||||
|
||||
/**
|
||||
* General manager: send result notification and create account if passed.
|
||||
*/
|
||||
public function complete($user, ?MembershipApplication $membershipApplication = null): bool
|
||||
{
|
||||
return $user->hasPermissionTo('selesaikan permohonan keahlian');
|
||||
}
|
||||
|
||||
/**
|
||||
* General manager: batch complete multiple applications.
|
||||
*/
|
||||
public function completeBatch($user): bool
|
||||
{
|
||||
return $user->hasPermissionTo('selesaikan permohonan keahlian');
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin: assign or update proposer and supporter.
|
||||
*/
|
||||
public function assignReferences($user, ?MembershipApplication $membershipApplication = null): bool
|
||||
{
|
||||
return $user->hasPermissionTo('tetapkan pencadang penyokong');
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin: edit application details before completion.
|
||||
*/
|
||||
public function update($user, ?MembershipApplication $membershipApplication = null): bool
|
||||
{
|
||||
return $user->hasPermissionTo('kemaskini permohonan keahlian');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The event handler mappings for the application.
|
||||
*
|
||||
* @var array<string, array<int, string>>
|
||||
*/
|
||||
protected $listen = [];
|
||||
|
||||
/**
|
||||
* Indicates if events should be discovered.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected static $shouldDiscoverEvents = true;
|
||||
|
||||
/**
|
||||
* Configure the proper event listeners for email verification.
|
||||
*/
|
||||
protected function configureEmailVerification(): void {}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Modules\MembershipApplication\Entities\MembershipApplication;
|
||||
use Modules\MembershipApplication\Policies\MembershipApplicationPolicy;
|
||||
use Nwidart\Modules\Traits\PathNamespace;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
class MembershipApplicationServiceProvider extends ServiceProvider
|
||||
{
|
||||
use PathNamespace;
|
||||
|
||||
protected string $name = 'MembershipApplication';
|
||||
|
||||
protected string $nameLower = 'membershipapplication';
|
||||
|
||||
/**
|
||||
* Boot the application events.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerRateLimiters();
|
||||
$this->registerCommands();
|
||||
$this->registerCommandSchedules();
|
||||
$this->registerTranslations();
|
||||
$this->registerConfig();
|
||||
$this->registerViews();
|
||||
$this->loadMigrationsFrom(module_path($this->name, 'Database/Migrations'));
|
||||
$this->registerPolicies();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register policies.
|
||||
*/
|
||||
protected function registerPolicies(): void
|
||||
{
|
||||
Gate::policy(MembershipApplication::class, MembershipApplicationPolicy::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(EventServiceProvider::class);
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
|
||||
// Register repository binding
|
||||
$this->app->bind(
|
||||
\Modules\MembershipApplication\Repositories\Contracts\MembershipApplicationRepositoryInterface::class,
|
||||
\Modules\MembershipApplication\Repositories\MembershipApplicationRepository::class
|
||||
);
|
||||
}
|
||||
|
||||
protected function registerRateLimiters(): void
|
||||
{
|
||||
RateLimiter::for('membership-application-submit', function ($request) {
|
||||
return Limit::perHour(10)->by($request->ip());
|
||||
});
|
||||
|
||||
RateLimiter::for('membership-application-lookup', function ($request) {
|
||||
return Limit::perMinute(30)->by($request->ip());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register commands in the format of Command::class
|
||||
*/
|
||||
protected function registerCommands(): void
|
||||
{
|
||||
// $this->commands([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register command Schedules.
|
||||
*/
|
||||
protected function registerCommandSchedules(): void
|
||||
{
|
||||
// $this->app->booted(function () {
|
||||
// $schedule = $this->app->make(Schedule::class);
|
||||
// $schedule->command('inspire')->hourly();
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Register translations.
|
||||
*/
|
||||
public function registerTranslations(): void
|
||||
{
|
||||
$langPath = resource_path('lang/modules/'.$this->nameLower);
|
||||
|
||||
if (is_dir($langPath)) {
|
||||
$this->loadTranslationsFrom($langPath, $this->nameLower);
|
||||
$this->loadJsonTranslationsFrom($langPath);
|
||||
} else {
|
||||
$this->loadTranslationsFrom(module_path($this->name, 'Lang'), $this->nameLower);
|
||||
$this->loadJsonTranslationsFrom(module_path($this->name, 'Lang'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register config.
|
||||
*/
|
||||
protected function registerConfig(): void
|
||||
{
|
||||
$configPath = module_path($this->name, config('modules.paths.generator.config.path'));
|
||||
|
||||
if (is_dir($configPath)) {
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath));
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile() && $file->getExtension() === 'php') {
|
||||
$config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname());
|
||||
$config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config);
|
||||
$segments = explode('.', $this->nameLower.'.'.$config_key);
|
||||
|
||||
// Remove duplicated adjacent segments
|
||||
$normalized = [];
|
||||
foreach ($segments as $segment) {
|
||||
if (end($normalized) !== $segment) {
|
||||
$normalized[] = $segment;
|
||||
}
|
||||
}
|
||||
|
||||
$key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized);
|
||||
|
||||
$this->publishes([$file->getPathname() => config_path($config)], 'config');
|
||||
$this->merge_config_from($file->getPathname(), $key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge config from the given path recursively.
|
||||
*/
|
||||
protected function merge_config_from(string $path, string $key): void
|
||||
{
|
||||
$existing = config($key, []);
|
||||
$module_config = require $path;
|
||||
|
||||
config([$key => array_replace_recursive($existing, $module_config)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register views.
|
||||
*/
|
||||
public function registerViews(): void
|
||||
{
|
||||
$viewPath = resource_path('views/modules/'.$this->nameLower);
|
||||
$sourcePath = module_path($this->name, 'Resources/Views');
|
||||
|
||||
$this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']);
|
||||
|
||||
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower);
|
||||
|
||||
Blade::componentNamespace(config('modules.namespace').'\\' . $this->name . '\\View\\Components', $this->nameLower);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*/
|
||||
public function provides(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
private function getPublishableViewPaths(): array
|
||||
{
|
||||
$paths = [];
|
||||
foreach (config('view.paths') as $path) {
|
||||
if (is_dir($path.'/modules/'.$this->nameLower)) {
|
||||
$paths[] = $path.'/modules/'.$this->nameLower;
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected string $name = 'MembershipApplication';
|
||||
|
||||
/**
|
||||
* Called before routes are registered.
|
||||
*
|
||||
* Register any model bindings or pattern based filters.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the routes for the application.
|
||||
*/
|
||||
public function map(): void
|
||||
{
|
||||
$this->mapApiRoutes();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "web" routes for the application.
|
||||
*
|
||||
* These routes all receive session state, CSRF protection, etc.
|
||||
*/
|
||||
protected function mapWebRoutes(): void
|
||||
{
|
||||
Route::middleware('web')->group(module_path($this->name, '/Routes/web.php'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "api" routes for the application.
|
||||
*
|
||||
* These routes are typically stateless.
|
||||
*/
|
||||
protected function mapApiRoutes(): void
|
||||
{
|
||||
Route::middleware('api')->name('api.')->group(module_path($this->name, '/Routes/api.php'));
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Repositories\Contracts;
|
||||
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Modules\MembershipApplication\Entities\MembershipApplication;
|
||||
|
||||
interface MembershipApplicationRepositoryInterface
|
||||
{
|
||||
public function create(array $data): MembershipApplication;
|
||||
|
||||
public function findById(string $id): ?MembershipApplication;
|
||||
|
||||
public function findByIdWithRelations(string $id): ?MembershipApplication;
|
||||
|
||||
public function getAllPaginated(
|
||||
int $perPage = 10,
|
||||
string $search = '',
|
||||
?string $status = null,
|
||||
string $sortBy = 'submitted_at',
|
||||
string $sortOrder = 'desc'
|
||||
): LengthAwarePaginator;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Repositories;
|
||||
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Modules\MembershipApplication\Entities\MembershipApplication;
|
||||
use Modules\MembershipApplication\Repositories\Contracts\MembershipApplicationRepositoryInterface;
|
||||
|
||||
class MembershipApplicationRepository implements MembershipApplicationRepositoryInterface
|
||||
{
|
||||
public function create(array $data): MembershipApplication
|
||||
{
|
||||
return MembershipApplication::create($data);
|
||||
}
|
||||
|
||||
public function findById(string $id): ?MembershipApplication
|
||||
{
|
||||
return MembershipApplication::find($id);
|
||||
}
|
||||
|
||||
public function findByIdWithRelations(string $id): ?MembershipApplication
|
||||
{
|
||||
return MembershipApplication::with([
|
||||
'applicant',
|
||||
'heirs',
|
||||
'references.member:id,name,ic_number',
|
||||
'documents',
|
||||
'reviews.reviewer:id,name',
|
||||
])->find($id);
|
||||
}
|
||||
|
||||
public function getAllPaginated(
|
||||
int $perPage = 10,
|
||||
string $search = '',
|
||||
?string $status = null,
|
||||
string $sortBy = 'submitted_at',
|
||||
string $sortOrder = 'desc'
|
||||
): LengthAwarePaginator {
|
||||
$allowedSortColumns = [
|
||||
'application_number',
|
||||
'status',
|
||||
'submitted_at',
|
||||
'created_at',
|
||||
];
|
||||
$sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'submitted_at';
|
||||
$sortOrder = strtolower($sortOrder) === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
$query = MembershipApplication::query()
|
||||
->with(['applicant:id,membership_application_id,name,email,ic_number'])
|
||||
->orderBy($sortBy, $sortOrder);
|
||||
|
||||
if ($status !== null && $status !== '') {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
if ($search !== '') {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('application_number', 'ILIKE', "%{$search}%")
|
||||
->orWhereHas('applicant', function ($applicantQuery) use ($search) {
|
||||
$applicantQuery->where('name', 'ILIKE', "%{$search}%")
|
||||
->orWhere('email', 'ILIKE', "%{$search}%")
|
||||
->orWhere('ic_number', 'ILIKE', "%{$search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
@component('mail::message')
|
||||
@if (! empty($logoPath) && file_exists($logoPath))
|
||||
<div style="text-align: center; margin-bottom: 24px;">
|
||||
<img src="{{ $message->embed($logoPath) }}" alt="{{ config('app.name') }}" style="max-height: 80px; width: auto;">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
# Keputusan Permohonan Keahlian
|
||||
|
||||
Assalamualaikum **{{ $name }}**,
|
||||
|
||||
Permohonan keahlian anda (**{{ $applicationNumber }}**) **tidak diluluskan** pada peringkat lembaga.
|
||||
|
||||
Terima kasih atas minat anda. Untuk sebarang pertanyaan, sila hubungi pejabat koperasi.
|
||||
|
||||
Terima kasih,<br>
|
||||
{{ config('app.name') }}
|
||||
@endcomponent
|
||||
@@ -0,0 +1,18 @@
|
||||
@component('mail::message')
|
||||
@if (! empty($logoPath) && file_exists($logoPath))
|
||||
<div style="text-align: center; margin-bottom: 24px;">
|
||||
<img src="{{ $message->embed($logoPath) }}" alt="{{ config('app.name') }}" style="max-height: 80px; width: auto;">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
# Keputusan Permohonan Keahlian
|
||||
|
||||
Assalamualaikum **{{ $name }}**,
|
||||
|
||||
Permohonan keahlian anda (**{{ $applicationNumber }}**) **diluluskan** pada peringkat lembaga.
|
||||
|
||||
Terima kasih atas minat anda. Untuk sebarang pertanyaan, sila hubungi pejabat koperasi.
|
||||
|
||||
Terima kasih,<br>
|
||||
{{ config('app.name') }}
|
||||
@endcomponent
|
||||
@@ -0,0 +1,79 @@
|
||||
@component('mail::message')
|
||||
@if (! empty($logoPath) && file_exists($logoPath))
|
||||
<div style="text-align: center; margin-bottom: 24px;">
|
||||
<img src="{{ $message->embed($logoPath) }}" alt="{{ config('app.name') }}" style="max-height: 80px; width: auto;">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
# Pengesahan Permohonan Keahlian
|
||||
|
||||
Assalamualaikum **{{ $applicant->name }}**,
|
||||
|
||||
Terima kasih. Permohonan keahlian anda telah berjaya diterima.
|
||||
|
||||
**No. Permohonan:** {{ $application->application_number }}
|
||||
|
||||
Berikut adalah ringkasan maklumat yang anda hantar:
|
||||
|
||||
## Maklumat Peribadi
|
||||
|
||||
- **Nama:** {{ $applicant->name }}
|
||||
- **Emel:** {{ $applicant->email }}
|
||||
- **No. Kad Pengenalan:** {{ $applicant->ic_number }}
|
||||
- **Tarikh Lahir:** {{ $applicant->birth_date }}
|
||||
- **Tempat Lahir:** {{ $applicant->birth_place }}
|
||||
- **Jantina:** {{ $applicant->gender }}
|
||||
- **Status Perkahwinan:** {{ $applicant->marriage_status }}
|
||||
|
||||
## Hubungan & Alamat
|
||||
|
||||
- **Alamat:** {{ $applicant->address }}
|
||||
- **Poskod:** {{ $applicant->postcode }}
|
||||
- **No. Telefon:** {{ $applicant->phone_number }}
|
||||
- **No. Pejabat:** {{ $applicant->office_number ?: '-' }}
|
||||
|
||||
## Maklumat Pekerjaan & Caruman
|
||||
|
||||
- **Nama Majikan:** {{ $applicant->employer_name }}
|
||||
- **Alamat Majikan:** {{ $applicant->employer_address }}
|
||||
- **Jawatan Semasa:** {{ $applicant->current_position }}
|
||||
- **Tarikh Mula Berkhidmat:** {{ $applicant->start_work_date?->format('d/m/Y') ?? $applicant->start_work_date }}
|
||||
- **Caruman Saham (RM):** {{ number_format((float) $applicant->stock_monthly_contribution, 2) }}
|
||||
- **Caruman Yuran (RM):** {{ number_format((float) $applicant->fee_monthly_contribution, 2) }}
|
||||
|
||||
## Maklumat Waris
|
||||
|
||||
@forelse ($heirs as $index => $heir)
|
||||
**Waris {{ $index + 1 }}**
|
||||
- **Nama:** {{ $heir->name }}
|
||||
- **No. Kad Pengenalan:** {{ $heir->ic_number }}
|
||||
- **Hubungan:** {{ $heir->relationship }}
|
||||
- **No. Telefon:** {{ $heir->phone_number }}
|
||||
|
||||
@empty
|
||||
Tiada maklumat waris.
|
||||
@endforelse
|
||||
|
||||
## Pencadang & Penyokong
|
||||
|
||||
@php
|
||||
$proposer = $application->references->first(fn ($ref) => ($ref->reference_type?->value ?? $ref->reference_type) === 'PROPOSER');
|
||||
$supporter = $application->references->first(fn ($ref) => ($ref->reference_type?->value ?? $ref->reference_type) === 'SUPPORTER');
|
||||
@endphp
|
||||
|
||||
- **Pencadang:** {{ $proposer?->member?->name ? $proposer->member->name . ' (' . $proposer->member->ic_number . ')' : '-' }}
|
||||
- **Penyokong:** {{ $supporter?->member?->name ? $supporter->member->name . ' (' . $supporter->member->ic_number . ')' : '-' }}
|
||||
|
||||
## Dokumen Dimuat Naik
|
||||
|
||||
@forelse ($documents as $document)
|
||||
- **{{ $documentLabels[$document->type] ?? $document->type }}:** {{ $document->name }}
|
||||
@empty
|
||||
Tiada dokumen.
|
||||
@endforelse
|
||||
|
||||
Anda akan menerima emel apabila keputusan permohonan tersedia.
|
||||
|
||||
Terima kasih,<br>
|
||||
{{ config('app.name') }}
|
||||
@endcomponent
|
||||
@@ -0,0 +1,12 @@
|
||||
{{--
|
||||
Approval letter — extends the shared base layout.
|
||||
Fill in sections below when the final design is ready.
|
||||
--}}
|
||||
@extends('pdf.layouts.letter')
|
||||
|
||||
{{-- @section('letter-reference', $application->application_number) --}}
|
||||
{{-- @section('letter-date', ...) --}}
|
||||
{{-- @section('letter-recipient') ... @endsection --}}
|
||||
{{-- @section('letter-subject') PERKARA: ... @endsection --}}
|
||||
{{-- @section('letter-body') ... @endsection --}}
|
||||
{{-- @section('letter-signature') ... @endsection --}}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\MembershipApplication\Http\Controllers\MembershipApplicationController;
|
||||
use Modules\MembershipApplication\Http\Controllers\PublicMembershipApplicationController;
|
||||
|
||||
// public routes
|
||||
Route::prefix('v1/public')->group(function () {
|
||||
Route::middleware('throttle:membership-application-lookup')->get('membership-applications/member-lookup',[PublicMembershipApplicationController::class, 'lookupMember'])->name('membership-application.public.member-lookup');
|
||||
|
||||
Route::middleware('throttle:membership-application-submit')->post('membership-applications',[PublicMembershipApplicationController::class, 'store'])->name('membership-application.public.store');
|
||||
});
|
||||
|
||||
// admin routes
|
||||
Route::middleware(['auth:sanctum', 'single.session'])->prefix('v1')->group(function () {
|
||||
// Membership application management routes
|
||||
Route::get('membership-applications', [MembershipApplicationController::class, 'index'])->name('membership-application.index');
|
||||
|
||||
// Membership application detail routes
|
||||
Route::get('membership-applications/{membershipApplication}', [MembershipApplicationController::class, 'show'])->name('membership-application.show');
|
||||
|
||||
// Membership application update routes - admin
|
||||
Route::patch('membership-applications/{membershipApplication}', [MembershipApplicationController::class, 'update'])->name('membership-application.update');
|
||||
|
||||
// Membership application management review routes (level 1)
|
||||
Route::post('membership-applications/{membershipApplication}/management-review', [MembershipApplicationController::class, 'managementReview'])->name('membership-application.management-review');
|
||||
|
||||
// Membership application board review routes (level 2)
|
||||
Route::post('membership-applications/{membershipApplication}/board-review', [MembershipApplicationController::class, 'boardReview'])->name('membership-application.board-review');
|
||||
|
||||
// Membership application assign references routes
|
||||
Route::put('membership-applications/{membershipApplication}/references', [MembershipApplicationController::class, 'assignReferences'])->name('membership-application.assign-references');
|
||||
|
||||
// Membership application batch complete routes (level 3)
|
||||
Route::post('membership-applications/batch-complete', [MembershipApplicationController::class, 'batchComplete'])->name('membership-application.batch-complete');
|
||||
// Membership application complete routes (level 3)
|
||||
Route::post('membership-applications/{membershipApplication}/complete', [MembershipApplicationController::class, 'complete'])->name('membership-application.complete');
|
||||
|
||||
// Membership application upload document routes
|
||||
Route::post('membership-applications/{membershipApplication}/documents', [MembershipApplicationController::class, 'uploadDocument'])->name('membership-application.upload-document');
|
||||
// download document routes
|
||||
Route::get('membership-applications/{membershipApplication}/documents/{documentId}/download', [MembershipApplicationController::class, 'downloadDocument'])->name('membership-application.download-document');
|
||||
|
||||
// approved letter preview (html or pdf)
|
||||
Route::get('membership-applications/{membershipApplication}/approved-letter', [MembershipApplicationController::class, 'approvedLetter'])->name('membership-application.approved-letter');
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\MembershipApplication\Http\Controllers\MembershipApplicationController;
|
||||
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::resource('membershipapplications', MembershipApplicationController::class)->names('membershipapplication');
|
||||
});
|
||||
@@ -0,0 +1,478 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Services;
|
||||
|
||||
use App\Services\DocumentService;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Modules\Auth\Entities\User;
|
||||
use Modules\Auth\Services\EmailVerificationOtpService;
|
||||
use Modules\MembershipApplication\Entities\MembershipApplication;
|
||||
use Modules\MembershipApplication\Enums\ApplicationStatus;
|
||||
use Modules\MembershipApplication\Enums\BoardDecision;
|
||||
use Modules\MembershipApplication\Enums\BoardResult;
|
||||
use Modules\MembershipApplication\Enums\ManagementDecision;
|
||||
use Modules\MembershipApplication\Enums\ReferenceType;
|
||||
use Modules\MembershipApplication\Enums\ReviewStage;
|
||||
use Modules\MembershipApplication\Emails\MembershipApplicationFailedNotification;
|
||||
use Modules\MembershipApplication\Emails\MembershipApplicationPassedNotification;
|
||||
use Modules\MembershipApplication\Emails\MembershipApplicationSubmittedNotification;
|
||||
use Modules\MembershipApplication\Repositories\Contracts\MembershipApplicationRepositoryInterface;
|
||||
use Modules\Role\Entities\Role;
|
||||
|
||||
class MembershipApplicationService
|
||||
{
|
||||
public function __construct(
|
||||
protected MembershipApplicationRepositoryInterface $repository,
|
||||
protected DocumentService $documentService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* applicant: array<string, mixed>,
|
||||
* heirs: list<array<string, mixed>>,
|
||||
* references?: array{proposer_ic_number?: string|null, supporter_ic_number?: string|null}
|
||||
* } $data
|
||||
* @param array<string, UploadedFile> $documents
|
||||
*/
|
||||
public function submit(array $data, array $documents): MembershipApplication
|
||||
{
|
||||
$application = DB::transaction(function () use ($data, $documents) {
|
||||
// membership_applications table
|
||||
$application = $this->repository->create([
|
||||
'application_number' => $this->generateApplicationNumber(),
|
||||
'status' => ApplicationStatus::Submitted,
|
||||
'board_result' => '',
|
||||
'submitted_at' => now()->toDateTimeString(),
|
||||
'completed_at' => '',
|
||||
]);
|
||||
|
||||
// membership_application_applicants table
|
||||
$application->applicant()->create($data['applicant']);
|
||||
|
||||
// membership_application_heirs table
|
||||
foreach ($data['heirs'] as $heir) {
|
||||
$application->heirs()->create($heir);
|
||||
}
|
||||
|
||||
// membership_application_references table
|
||||
$this->syncReferences(
|
||||
$application,
|
||||
$data['references'] ?? [],
|
||||
(string) ($data['applicant']['ic_number'] ?? ''),
|
||||
);
|
||||
|
||||
// documents table
|
||||
foreach ($documents as $type => $file) {
|
||||
$this->documentService->validateFile($file);
|
||||
$this->documentService->uploadDocument($application, $file, $type);
|
||||
}
|
||||
|
||||
return $this->repository->findByIdWithRelations($application->id);
|
||||
});
|
||||
|
||||
$this->sendSubmissionConfirmation($application);
|
||||
|
||||
return $application;
|
||||
}
|
||||
|
||||
public function getPaginatedList(int $perPage, string $search, ?string $status, string $sortBy, string $sortOrder): LengthAwarePaginator
|
||||
{
|
||||
return $this->repository->getAllPaginated($perPage, $search, $status, $sortBy, $sortOrder);
|
||||
}
|
||||
|
||||
public function getByIdWithRelations(string $id): ?MembershipApplication
|
||||
{
|
||||
return $this->repository->findByIdWithRelations($id);
|
||||
}
|
||||
|
||||
public function update(MembershipApplication $application, array $data): MembershipApplication
|
||||
{
|
||||
if ($application->status === ApplicationStatus::Completed) {
|
||||
throw ValidationException::withMessages([
|
||||
'status' => ['Permohonan yang telah selesai tidak boleh dikemaskini.'],
|
||||
]);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($application, $data) {
|
||||
$application->loadMissing('applicant');
|
||||
|
||||
if (isset($data['applicant'])) {
|
||||
$application->applicant->update($data['applicant']);
|
||||
}
|
||||
|
||||
if (isset($data['heirs'])) {
|
||||
$application->heirs()->delete();
|
||||
foreach ($data['heirs'] as $heir) {
|
||||
$application->heirs()->create($heir);
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('references', $data)) {
|
||||
$this->syncReferences(
|
||||
$application,
|
||||
$data['references'] ?? [],
|
||||
(string) ($data['applicant']['ic_number'] ?? $application->applicant->ic_number),
|
||||
);
|
||||
}
|
||||
|
||||
return $this->repository->findByIdWithRelations($application->id);
|
||||
});
|
||||
}
|
||||
|
||||
public function uploadDocument(MembershipApplication $application, string $type, UploadedFile $file): MembershipApplication
|
||||
{
|
||||
if ($application->status === ApplicationStatus::Completed) {
|
||||
throw ValidationException::withMessages([
|
||||
'status' => ['Permohonan yang telah selesai tidak boleh dikemaskini.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->documentService->validateFile($file);
|
||||
|
||||
$application->documentsOfType($type)->get()->each(
|
||||
fn ($document) => $this->documentService->deleteDocument($document->id)
|
||||
);
|
||||
|
||||
$this->documentService->uploadDocument($application, $file, $type);
|
||||
|
||||
return $this->repository->findByIdWithRelations($application->id);
|
||||
}
|
||||
|
||||
public function managementReview(MembershipApplication $application, ManagementDecision $decision, ?string $remarks, User $reviewer): MembershipApplication {
|
||||
if ($application->status !== ApplicationStatus::Submitted) {
|
||||
throw ValidationException::withMessages([
|
||||
'status' => ['Permohonan ini tidak boleh disemak pada peringkat pentadbiran.'],
|
||||
]);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($application, $decision, $remarks, $reviewer) {
|
||||
$application->reviews()->create([
|
||||
'stage' => ReviewStage::Management,
|
||||
'decision' => $decision->value,
|
||||
'remarks' => $remarks,
|
||||
'reviewer_id' => $reviewer->id,
|
||||
'reviewed_at' => now(),
|
||||
]);
|
||||
|
||||
$application->update([
|
||||
'status' => $decision === ManagementDecision::Approved
|
||||
? ApplicationStatus::PendingBoard
|
||||
: ApplicationStatus::ManagementRejected,
|
||||
]);
|
||||
|
||||
return $this->repository->findByIdWithRelations($application->id);
|
||||
});
|
||||
}
|
||||
|
||||
public function boardReview(MembershipApplication $application, BoardDecision $decision, ?string $remarks, User $reviewer): MembershipApplication
|
||||
{
|
||||
if ($application->status !== ApplicationStatus::PendingBoard) {
|
||||
throw ValidationException::withMessages([
|
||||
'status' => ['Permohonan ini tidak boleh disemak pada peringkat lembaga.'],
|
||||
]);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($application, $decision, $remarks, $reviewer) {
|
||||
$application->reviews()->create([
|
||||
'stage' => ReviewStage::Board,
|
||||
'decision' => $decision->value,
|
||||
'remarks' => $remarks,
|
||||
'reviewer_id' => $reviewer->id,
|
||||
'reviewed_at' => now(),
|
||||
]);
|
||||
|
||||
$application->update([
|
||||
'board_result' => $decision === BoardDecision::Pass
|
||||
? BoardResult::Pass->value
|
||||
: BoardResult::Fail->value,
|
||||
'status' => ApplicationStatus::PendingNotification,
|
||||
]);
|
||||
|
||||
return $this->repository->findByIdWithRelations($application->id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{proposer_id?: string|null, supporter_id?: string|null} $references
|
||||
*/
|
||||
public function assignReferences(MembershipApplication $application, array $references, User $assignedBy): MembershipApplication
|
||||
{
|
||||
return DB::transaction(function () use ($application, $references, $assignedBy) {
|
||||
$referenceMap = [
|
||||
'proposer_id' => ReferenceType::Proposer,
|
||||
'supporter_id' => ReferenceType::Supporter,
|
||||
];
|
||||
|
||||
foreach ($referenceMap as $key => $type) {
|
||||
if (! array_key_exists($key, $references)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$application->references()->updateOrCreate(
|
||||
['reference_type' => $type->value],
|
||||
[
|
||||
'user_id' => $references[$key],
|
||||
'assigned_by' => $references[$key] ? $assignedBy->id : null,
|
||||
'assigned_at' => $references[$key] ? now() : null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return $this->repository->findByIdWithRelations($application->id);
|
||||
});
|
||||
}
|
||||
|
||||
public function complete(MembershipApplication $application, User $processedBy): MembershipApplication
|
||||
{
|
||||
if ($application->status !== ApplicationStatus::PendingNotification) {
|
||||
throw ValidationException::withMessages([
|
||||
'status' => ['Permohonan ini tidak boleh diselesaikan pada masa ini.'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (! in_array($application->board_result, [BoardResult::Pass->value, BoardResult::Fail->value], true)) {
|
||||
throw ValidationException::withMessages([
|
||||
'board_result' => ['Keputusan lembaga belum ditetapkan.'],
|
||||
]);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($application) {
|
||||
if ($application->board_result === BoardResult::Pass->value && ! $application->user_id) {
|
||||
$user = $this->createMemberFromApplication($application);
|
||||
$application->update(['user_id' => $user->id]);
|
||||
}
|
||||
|
||||
$this->sendResultNotification($application);
|
||||
|
||||
$application->update([
|
||||
'status' => ApplicationStatus::Completed,
|
||||
'completed_at' => now()->toDateTimeString(),
|
||||
]);
|
||||
|
||||
return $this->repository->findByIdWithRelations($application->id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $applicationIds
|
||||
* @return array{
|
||||
* succeeded: list<MembershipApplication>,
|
||||
* failed: list<array{id: string, application_number: string|null, message: string}>
|
||||
* }
|
||||
*/
|
||||
public function completeBatch(array $applicationIds, User $processedBy): array
|
||||
{
|
||||
$succeeded = [];
|
||||
$failed = [];
|
||||
|
||||
foreach ($applicationIds as $id) {
|
||||
$application = $this->repository->findById($id);
|
||||
|
||||
if (! $application) {
|
||||
$failed[] = [
|
||||
'id' => $id,
|
||||
'application_number' => null,
|
||||
'message' => 'Permohonan tidak dijumpai.',
|
||||
];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$succeeded[] = $this->complete($application, $processedBy);
|
||||
} catch (ValidationException $e) {
|
||||
$failed[] = [
|
||||
'id' => $id,
|
||||
'application_number' => $application->application_number,
|
||||
'message' => collect($e->errors())->flatten()->first() ?? 'Permohonan tidak boleh diselesaikan.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'succeeded' => $succeeded,
|
||||
'failed' => $failed,
|
||||
];
|
||||
}
|
||||
|
||||
protected function createMemberFromApplication(MembershipApplication $application): User
|
||||
{
|
||||
$application->loadMissing(['applicant', 'heirs']);
|
||||
$applicant = $application->applicant;
|
||||
|
||||
if (User::where('email', $applicant->email)->exists()) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => ['E-mel pemohon sudah didaftarkan dalam sistem.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$user = User::create([
|
||||
'name' => $applicant->name,
|
||||
'email' => $applicant->email,
|
||||
'password' => Hash::make(Str::password(16)),
|
||||
'ic_number' => $applicant->ic_number,
|
||||
'phone_number' => $applicant->phone_number,
|
||||
'position' => $applicant->current_position,
|
||||
'status' => 'active',
|
||||
'gender' => $applicant->gender,
|
||||
'marriage_status' => $applicant->marriage_status,
|
||||
'member_number' => $this->generateMemberNumber(),
|
||||
'member_type' => 'Anggota',
|
||||
'join_date' => now()->toDateString(),
|
||||
'birth_date' => $applicant->birth_date,
|
||||
'birth_place' => $applicant->birth_place,
|
||||
]);
|
||||
|
||||
$role = Role::where('name', 'Anggota')->first();
|
||||
if ($role) {
|
||||
$user->assignRole($role);
|
||||
}
|
||||
|
||||
$user->addresses()->create([
|
||||
'address_type' => 'home',
|
||||
'address_line_1' => $applicant->address,
|
||||
'postcode' => $applicant->postcode,
|
||||
]);
|
||||
|
||||
$user->employments()->create([
|
||||
'company_name' => $applicant->employer_name,
|
||||
'job_title' => $applicant->current_position,
|
||||
'employment_type' => 'Permanent',
|
||||
'salary' => 0,
|
||||
'start_date' => $applicant->start_work_date,
|
||||
'is_current' => true,
|
||||
]);
|
||||
|
||||
foreach ($application->heirs as $index => $heir) {
|
||||
$user->heirs()->create([
|
||||
'name' => $heir->name,
|
||||
'ic_number' => $heir->ic_number,
|
||||
'relationship' => $heir->relationship,
|
||||
'phone_number' => $heir->phone_number,
|
||||
'is_primary' => $index === 0,
|
||||
]);
|
||||
}
|
||||
|
||||
app(EmailVerificationOtpService::class)->send($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function lookupMemberByIcNumber(string $icNumber): ?User
|
||||
{
|
||||
$icNumber = trim($icNumber);
|
||||
|
||||
if ($icNumber === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return User::query()
|
||||
->where('ic_number', $icNumber)
|
||||
->where('status', 'active')
|
||||
->first();
|
||||
}
|
||||
|
||||
protected function sendSubmissionConfirmation(MembershipApplication $application): void
|
||||
{
|
||||
$application->loadMissing(['applicant', 'heirs', 'documents', 'references.member']);
|
||||
|
||||
Notification::route('mail', $application->applicant->email)
|
||||
->notify(new MembershipApplicationSubmittedNotification($application));
|
||||
}
|
||||
|
||||
protected function sendResultNotification(MembershipApplication $application): void
|
||||
{
|
||||
$application->loadMissing('applicant');
|
||||
|
||||
$notification = $application->board_result === BoardResult::Pass->value
|
||||
? new MembershipApplicationPassedNotification($application)
|
||||
: new MembershipApplicationFailedNotification($application);
|
||||
|
||||
Notification::route('mail', $application->applicant->email)->notify($notification);
|
||||
}
|
||||
|
||||
protected function generateMemberNumber(): int
|
||||
{
|
||||
return ((int) User::max('member_number')) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{proposer_ic_number?: string|null, supporter_ic_number?: string|null} $references
|
||||
*/
|
||||
protected function syncReferences(MembershipApplication $application, array $references, string $applicantIcNumber): void
|
||||
{
|
||||
$referenceMap = [
|
||||
'references.proposer_ic_number' => ['field' => 'proposer_ic_number', 'type' => ReferenceType::Proposer],
|
||||
'references.supporter_ic_number' => ['field' => 'supporter_ic_number', 'type' => ReferenceType::Supporter],
|
||||
];
|
||||
|
||||
$resolvedUserIds = [];
|
||||
$applicantIcNumber = trim($applicantIcNumber);
|
||||
|
||||
foreach ($referenceMap as $errorKey => $config) {
|
||||
if (! array_key_exists($config['field'], $references)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$icNumber = trim((string) ($references[$config['field']] ?? ''));
|
||||
|
||||
if ($icNumber === '') {
|
||||
$application->references()->where('reference_type', $config['type']->value)->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($applicantIcNumber !== '' && strcasecmp($icNumber, $applicantIcNumber) === 0) {
|
||||
throw ValidationException::withMessages([
|
||||
$errorKey => ['Pencadang/penyokong tidak boleh sama dengan pemohon.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$member = $this->lookupMemberByIcNumber($icNumber);
|
||||
|
||||
if (! $member) {
|
||||
throw ValidationException::withMessages([
|
||||
$errorKey => ['Ahli dengan nombor kad pengenalan ini tidak dijumpai.'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (in_array($member->id, $resolvedUserIds, true)) {
|
||||
throw ValidationException::withMessages([
|
||||
$errorKey => ['Pencadang dan penyokong mestilah ahli yang berbeza.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$resolvedUserIds[] = $member->id;
|
||||
|
||||
$application->references()->updateOrCreate(
|
||||
['reference_type' => $config['type']->value],
|
||||
['user_id' => $member->id]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function generateApplicationNumber(): string
|
||||
{
|
||||
$year = now()->year;
|
||||
$prefix = "APP-{$year}-";
|
||||
|
||||
$latestNumber = MembershipApplication::query()
|
||||
->where('application_number', 'like', "{$prefix}%")
|
||||
->orderByDesc('application_number')
|
||||
->value('application_number');
|
||||
|
||||
$sequence = 1;
|
||||
|
||||
if ($latestNumber && preg_match('/-(\d+)$/', $latestNumber, $matches)) {
|
||||
$sequence = ((int) $matches[1]) + 1;
|
||||
}
|
||||
|
||||
return sprintf('%s%05d', $prefix, $sequence);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Transformers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class MembershipApplicationApplicantResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'email' => $this->email,
|
||||
'ic_number' => $this->ic_number,
|
||||
'birth_date' => $this->birth_date,
|
||||
'birth_place' => $this->birth_place,
|
||||
'gender' => $this->gender,
|
||||
'marriage_status' => $this->marriage_status,
|
||||
'address' => $this->address,
|
||||
'phone_number' => $this->phone_number,
|
||||
'office_number' => $this->office_number,
|
||||
'postcode' => $this->postcode,
|
||||
'employer_name' => $this->employer_name,
|
||||
'employer_address' => $this->employer_address,
|
||||
'current_position' => $this->current_position,
|
||||
'start_work_date' => $this->start_work_date?->toDateString(),
|
||||
'stock_monthly_contribution' => $this->stock_monthly_contribution,
|
||||
'fee_monthly_contribution' => $this->fee_monthly_contribution,
|
||||
];
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Transformers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class MembershipApplicationDocumentResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'type' => $this->type,
|
||||
'mime_type' => $this->mime_type,
|
||||
'file_size' => $this->file_size,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Transformers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class MembershipApplicationHeirResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'ic_number' => $this->ic_number,
|
||||
'relationship' => $this->relationship,
|
||||
'phone_number' => $this->phone_number,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Transformers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class MembershipApplicationListResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'application_number' => $this->application_number,
|
||||
'status' => $this->status?->value ?? $this->status,
|
||||
'board_result' => $this->board_result ?: null,
|
||||
'submitted_at' => $this->submitted_at,
|
||||
'applicant' => $this->whenLoaded('applicant', fn () => [
|
||||
'name' => $this->applicant->name,
|
||||
'email' => $this->applicant->email,
|
||||
'ic_number' => $this->applicant->ic_number,
|
||||
]),
|
||||
'created_at' => $this->created_at?->toISOString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Transformers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class MembershipApplicationReferenceResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'reference_type' => $this->reference_type?->value ?? $this->reference_type,
|
||||
'user_id' => $this->user_id,
|
||||
'member' => $this->whenLoaded('member', fn () => [
|
||||
'id' => $this->member->id,
|
||||
'name' => $this->member->name,
|
||||
'ic_number' => $this->member->ic_number,
|
||||
]),
|
||||
'assigned_by' => $this->assigned_by,
|
||||
'assigned_at' => $this->assigned_at?->toISOString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Transformers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class MembershipApplicationResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'application_number' => $this->application_number,
|
||||
'status' => $this->status?->value ?? $this->status,
|
||||
'board_result' => $this->board_result ?: null,
|
||||
'submitted_at' => $this->submitted_at,
|
||||
'completed_at' => $this->completed_at ?: null,
|
||||
'applicant' => new MembershipApplicationApplicantResource($this->whenLoaded('applicant')),
|
||||
'heirs' => MembershipApplicationHeirResource::collection($this->whenLoaded('heirs')),
|
||||
'references' => MembershipApplicationReferenceResource::collection($this->whenLoaded('references')),
|
||||
'documents' => MembershipApplicationDocumentResource::collection($this->whenLoaded('documents')),
|
||||
'reviews' => MembershipApplicationReviewResource::collection($this->whenLoaded('reviews')),
|
||||
'created_at' => $this->created_at?->toISOString(),
|
||||
'updated_at' => $this->updated_at?->toISOString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Transformers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class MembershipApplicationReviewResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'stage' => $this->stage?->value ?? $this->stage,
|
||||
'decision' => $this->decision,
|
||||
'remarks' => $this->remarks,
|
||||
'reviewer' => $this->whenLoaded('reviewer', fn () => [
|
||||
'id' => $this->reviewer->id,
|
||||
'name' => $this->reviewer->name,
|
||||
]),
|
||||
'reviewed_at' => $this->reviewed_at?->toISOString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "nwidart/membershipapplication",
|
||||
"description": "",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Widart",
|
||||
"email": "n.widart@gmail.com"
|
||||
}
|
||||
],
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [],
|
||||
"aliases": {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\MembershipApplication\\": "App",
|
||||
"Modules\\MembershipApplication\\Database\\Factories\\": "database/factories/",
|
||||
"Modules\\MembershipApplication\\Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Modules\\MembershipApplication\\Tests\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "MembershipApplication",
|
||||
"alias": "membershipapplication",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"priority": 0,
|
||||
"providers": [
|
||||
"Modules\\MembershipApplication\\Providers\\MembershipApplicationServiceProvider"
|
||||
],
|
||||
"files": []
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^1.1.2",
|
||||
"laravel-vite-plugin": "^0.7.5",
|
||||
"sass": "^1.69.5",
|
||||
"postcss": "^8.3.7",
|
||||
"vite": "^4.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user