DONE: use full name for date in letter, include password in email after membership is done, block ic_number to exclude - (#9)

Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local>
Reviewed-on: KoPKB/My-KOPKB#9
This commit is contained in:
2026-07-08 12:15:41 +08:00
parent 8589982573
commit 872957a88b
12 changed files with 134 additions and 16 deletions
+2
View File
@@ -54,6 +54,7 @@ class User extends Authenticatable
'member_type', 'member_type',
'public_profile_token', 'public_profile_token',
'join_date', 'join_date',
'leave_date',
'birth_date', 'birth_date',
'birth_place', 'birth_place',
'onboarding_completed_at', 'onboarding_completed_at',
@@ -136,6 +137,7 @@ class User extends Authenticatable
'member_type' => 'string', 'member_type' => 'string',
'public_profile_token_expires_at' => 'datetime', 'public_profile_token_expires_at' => 'datetime',
'join_date' => 'date', 'join_date' => 'date',
'leave_date' => 'date',
'birth_date' => 'date', 'birth_date' => 'date',
'birth_place' => 'string', 'birth_place' => 'string',
'onboarding_completed_at' => 'datetime', 'onboarding_completed_at' => 'datetime',
@@ -38,6 +38,7 @@ class UserResource extends JsonResource
'member_type' => $this->member_type, 'member_type' => $this->member_type,
'public_profile_token' => $this->public_profile_token, 'public_profile_token' => $this->public_profile_token,
'join_date' => $this->join_date, 'join_date' => $this->join_date,
'leave_date' => $this->leave_date,
'birth_date' => $this->birth_date, 'birth_date' => $this->birth_date,
'birth_place' => $this->birth_place, 'birth_place' => $this->birth_place,
'onboarding_completed_at' => $this->onboarding_completed_at, 'onboarding_completed_at' => $this->onboarding_completed_at,
@@ -19,6 +19,7 @@ class MembershipApplicationPassedNotification extends Notification implements Sh
public function __construct( public function __construct(
public MembershipApplication $application, public MembershipApplication $application,
public Document $resultLetter, public Document $resultLetter,
public ?string $plainPassword = null,
) {} ) {}
/** /**
@@ -40,6 +41,8 @@ class MembershipApplicationPassedNotification extends Notification implements Sh
data: [ data: [
'application' => $this->application, 'application' => $this->application,
'applicant' => $applicant, 'applicant' => $applicant,
'plainPassword' => $this->plainPassword,
'loginUrl' => rtrim(config('user.frontend_url'), '/'),
], ],
)->attach( )->attach(
Storage::disk(Document::STORAGE_DISK)->path($this->resultLetter->path), Storage::disk(Document::STORAGE_DISK)->path($this->resultLetter->path),
@@ -9,6 +9,15 @@ Permohonan keahlian anda (**{{ $application->application_number }}**) **dilulusk
Surat keputusan rasmi dilampirkan dalam e-mel ini. Surat keputusan rasmi dilampirkan dalam e-mel ini.
@if ($plainPassword)
Akaun portal anda telah didaftarkan. Maklumat log masuk adalah seperti berikut:
- **E-mel:** {{ $applicant->email }}
- **Kata laluan:** {{ $plainPassword }}
Sila log masuk di [{{ $loginUrl }}]({{ $loginUrl }}) dan tukar kata laluan anda selepas log masuk kali pertama.
@endif
Terima kasih atas minat anda. Untuk sebarang pertanyaan, sila hubungi pejabat koperasi. Terima kasih atas minat anda. Untuk sebarang pertanyaan, sila hubungi pejabat koperasi.
@include('emails.partials.footer') @include('emails.partials.footer')
@@ -274,15 +274,18 @@ class MembershipApplicationService
} }
return DB::transaction(function () use ($application, $boardMeetingReference, $boardMeetingDate) { return DB::transaction(function () use ($application, $boardMeetingReference, $boardMeetingDate) {
$plainPassword = null;
if ($application->board_result === BoardResult::Pass->value && ! $application->user_id) { if ($application->board_result === BoardResult::Pass->value && ! $application->user_id) {
$user = $this->createMemberFromApplication($application); $member = $this->createMemberFromApplication($application);
$application->update(['user_id' => $user->id]); $application->update(['user_id' => $member['user']->id]);
$plainPassword = $member['plainPassword'];
$application->refresh(); $application->refresh();
} }
$resultLetter = $this->storeResultLetter($application, $boardMeetingReference, $boardMeetingDate); $resultLetter = $this->storeResultLetter($application, $boardMeetingReference, $boardMeetingDate);
$this->sendResultNotification($application, $resultLetter); $this->sendResultNotification($application, $resultLetter, $plainPassword);
$application->update([ $application->update([
'status' => ApplicationStatus::Completed, 'status' => ApplicationStatus::Completed,
@@ -438,7 +441,7 @@ class MembershipApplicationService
'application' => $application, 'application' => $application,
'applicant' => $applicant, 'applicant' => $applicant,
'boardMeetingReference' => $boardMeetingReference, 'boardMeetingReference' => $boardMeetingReference,
'boardMeetingDate' => Carbon::parse($boardMeetingDate)->translatedFormat('d M Y'), 'boardMeetingDate' => Carbon::parse($boardMeetingDate)->translatedFormat('d F Y'),
'isPassed' => $isPassed, 'isPassed' => $isPassed,
'letterSubject' => $isPassed 'letterSubject' => $isPassed
? 'KELULUSAN PERMOHONAN MENJADI ANGGOTA KOPERASI PERMODALAN KELANTAN BERHAD (KoPKB)' ? 'KELULUSAN PERMOHONAN MENJADI ANGGOTA KOPERASI PERMODALAN KELANTAN BERHAD (KoPKB)'
@@ -447,7 +450,10 @@ class MembershipApplicationService
]; ];
} }
protected function createMemberFromApplication(MembershipApplication $application): User /**
* @return array{user: User, plainPassword: string}
*/
protected function createMemberFromApplication(MembershipApplication $application): array
{ {
$application->loadMissing(['applicant', 'heirs']); $application->loadMissing(['applicant', 'heirs']);
$applicant = $application->applicant; $applicant = $application->applicant;
@@ -458,10 +464,12 @@ class MembershipApplicationService
]); ]);
} }
$plainPassword = Str::password(16);
$user = User::create([ $user = User::create([
'name' => $applicant->name, 'name' => $applicant->name,
'email' => $applicant->email, 'email' => $applicant->email,
'password' => Hash::make(Str::password(16)), 'password' => Hash::make($plainPassword),
'ic_number' => $applicant->ic_number, 'ic_number' => $applicant->ic_number,
'phone_number' => $applicant->phone_number, 'phone_number' => $applicant->phone_number,
'position' => $applicant->current_position, 'position' => $applicant->current_position,
@@ -507,7 +515,10 @@ class MembershipApplicationService
app(EmailVerificationOtpService::class)->send($user); app(EmailVerificationOtpService::class)->send($user);
return $user; return [
'user' => $user,
'plainPassword' => $plainPassword,
];
} }
public function lookupMemberByIcNumber(string $icNumber): ?User public function lookupMemberByIcNumber(string $icNumber): ?User
@@ -532,12 +543,15 @@ class MembershipApplicationService
->notify(new MembershipApplicationSubmittedNotification($application)); ->notify(new MembershipApplicationSubmittedNotification($application));
} }
protected function sendResultNotification(MembershipApplication $application, Document $resultLetter): void protected function sendResultNotification(
{ MembershipApplication $application,
Document $resultLetter,
?string $plainPassword = null,
): void {
$application->loadMissing('applicant'); $application->loadMissing('applicant');
$notification = $application->board_result === BoardResult::Pass->value $notification = $application->board_result === BoardResult::Pass->value
? new MembershipApplicationPassedNotification($application, $resultLetter) ? new MembershipApplicationPassedNotification($application, $resultLetter, $plainPassword)
: new MembershipApplicationFailedNotification($application, $resultLetter); : new MembershipApplicationFailedNotification($application, $resultLetter);
Notification::route('mail', $application->applicant->email)->notify($notification); Notification::route('mail', $application->applicant->email)->notify($notification);
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->date('leave_date')->nullable()->after('join_date');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('leave_date');
});
}
};
@@ -44,7 +44,7 @@ class UserController extends BaseCrudController
$validated['uuid'] = Str::uuid(); $validated['uuid'] = Str::uuid();
$validated['password'] = Hash::make('suteraselamanya'); $validated['password'] = Hash::make('suteraselamanya');
return $validated; return $this->applyLeaveDateForStatus($validated);
} }
/** /**
@@ -58,6 +58,25 @@ class UserController extends BaseCrudController
unset($validated['email']); unset($validated['email']);
} }
return $this->applyLeaveDateForStatus($validated, $user);
}
/**
* Set or clear leave_date based on membership status transitions.
*/
protected function applyLeaveDateForStatus(array $validated, ?User $user = null): array
{
$newStatus = $validated['status'] ?? $user?->status;
$currentStatus = $user?->status;
if ($newStatus === 'inactive') {
if ($currentStatus !== 'inactive') {
$validated['leave_date'] = $validated['leave_date'] ?? now()->toDateString();
}
} elseif (in_array($newStatus, ['active', 'pending'], true)) {
$validated['leave_date'] = null;
}
return $validated; return $validated;
} }
@@ -171,13 +190,27 @@ class UserController extends BaseCrudController
$this->authorize('viewAny', $this->modelClass); $this->authorize('viewAny', $this->modelClass);
try { try {
$request->validate([
'join_date_from' => 'nullable|date',
'join_date_to' => 'nullable|date',
'leave_date_from' => 'nullable|date',
'leave_date_to' => 'nullable|date',
]);
$perPage = min((int) $request->get('per_page', 10), 500); $perPage = min((int) $request->get('per_page', 10), 500);
$dateFilters = array_filter([
'join_date_from' => $request->get('join_date_from'),
'join_date_to' => $request->get('join_date_to'),
'leave_date_from' => $request->get('leave_date_from'),
'leave_date_to' => $request->get('leave_date_to'),
]);
$items = $this->userService->getPaginatedList( $items = $this->userService->getPaginatedList(
$perPage, $perPage,
$request->get('search', ''), $request->get('search', ''),
$request->get('status', ''), $request->get('status', ''),
$request->get('sort_by', 'id'), $request->get('sort_by', 'id'),
$request->get('sort_order', 'asc') $request->get('sort_order', 'asc'),
$dateFilters
); );
return response()->json([ return response()->json([
@@ -44,6 +44,7 @@ class UserRequest extends FormRequest
'member_number' => 'required|integer', 'member_number' => 'required|integer',
'member_type' => 'required|string|max:255', 'member_type' => 'required|string|max:255',
'join_date' => 'nullable|date', 'join_date' => 'nullable|date',
'leave_date' => 'nullable|date',
'birth_date' => 'nullable|date', 'birth_date' => 'nullable|date',
'birth_place' => 'nullable|string|max:255', 'birth_place' => 'nullable|string|max:255',
]; ];
@@ -74,6 +75,7 @@ class UserRequest extends FormRequest
'member_number' => 'required|integer', 'member_number' => 'required|integer',
'member_type' => 'required|string|max:255', 'member_type' => 'required|string|max:255',
'join_date' => 'nullable|date', 'join_date' => 'nullable|date',
'leave_date' => 'nullable|date',
'birth_date' => 'nullable|date', 'birth_date' => 'nullable|date',
'birth_place' => 'nullable|string|max:255', 'birth_place' => 'nullable|string|max:255',
]; ];
@@ -100,6 +102,7 @@ class UserRequest extends FormRequest
'member_number' => 'required|integer', 'member_number' => 'required|integer',
'member_type' => 'required|string|max:255', 'member_type' => 'required|string|max:255',
'join_date' => 'required|date', 'join_date' => 'required|date',
'leave_date' => 'nullable|date',
'birth_date' => 'required|date', 'birth_date' => 'required|date',
'birth_place' => 'required|string|max:255', 'birth_place' => 'required|string|max:255',
]; ];
@@ -136,6 +139,7 @@ class UserRequest extends FormRequest
'member_type.max' => 'Jenis anggota tidak boleh melebihi 255 aksara.', 'member_type.max' => 'Jenis anggota tidak boleh melebihi 255 aksara.',
'join_date.required' => 'Tarikh join diperlukan.', 'join_date.required' => 'Tarikh join diperlukan.',
'join_date.date' => 'Tarikh join tidak sah.', 'join_date.date' => 'Tarikh join tidak sah.',
'leave_date.date' => 'Tarikh berhenti tidak sah.',
'birth_date.required' => 'Tarikh lahir diperlukan.', 'birth_date.required' => 'Tarikh lahir diperlukan.',
'birth_date.date' => 'Tarikh lahir tidak sah.', 'birth_date.date' => 'Tarikh lahir tidak sah.',
'birth_place.required' => 'Tempat lahir diperlukan.', 'birth_place.required' => 'Tempat lahir diperlukan.',
@@ -20,7 +20,8 @@ interface UserRepositoryInterface
string $search = '', string $search = '',
string $status = '', string $status = '',
string $sortBy = 'name', string $sortBy = 'name',
string $sortOrder = 'asc' string $sortOrder = 'asc',
array $dateFilters = []
); );
/** /**
+27 -1
View File
@@ -26,6 +26,25 @@ class UserRepository implements UserRepositoryInterface
}); });
} }
private function applyDateRangeFilters($query, array $dateFilters): void
{
if (! empty($dateFilters['join_date_from'])) {
$query->whereDate('join_date', '>=', $dateFilters['join_date_from']);
}
if (! empty($dateFilters['join_date_to'])) {
$query->whereDate('join_date', '<=', $dateFilters['join_date_to']);
}
if (! empty($dateFilters['leave_date_from'])) {
$query->whereDate('leave_date', '>=', $dateFilters['leave_date_from']);
}
if (! empty($dateFilters['leave_date_to'])) {
$query->whereDate('leave_date', '<=', $dateFilters['leave_date_to']);
}
}
/** /**
* Get all Users with pagination and search * Get all Users with pagination and search
*/ */
@@ -46,7 +65,8 @@ class UserRepository implements UserRepositoryInterface
string $search = '', string $search = '',
string $status = '', string $status = '',
string $sortBy = 'name', string $sortBy = 'name',
string $sortOrder = 'asc' string $sortOrder = 'asc',
array $dateFilters = []
) { ) {
$allowedSortColumns = [ $allowedSortColumns = [
'id', 'id',
@@ -55,6 +75,8 @@ class UserRepository implements UserRepositoryInterface
'position', 'position',
'status', 'status',
'member_number', 'member_number',
'join_date',
'leave_date',
'created_at', 'created_at',
'deleted_at', 'deleted_at',
]; ];
@@ -73,6 +95,8 @@ class UserRepository implements UserRepositoryInterface
$query->where('status', $status); $query->where('status', $status);
} }
$this->applyDateRangeFilters($query, $dateFilters);
return $query->paginate($perPage); return $query->paginate($perPage);
} }
@@ -93,6 +117,8 @@ class UserRepository implements UserRepositoryInterface
'position', 'position',
'status', 'status',
'member_number', 'member_number',
'join_date',
'leave_date',
'created_at', 'created_at',
'deleted_at', 'deleted_at',
]; ];
+4 -2
View File
@@ -21,14 +21,16 @@ class UserService
string $search, string $search,
string $status, string $status,
string $sortBy, string $sortBy,
string $sortOrder string $sortOrder,
array $dateFilters = []
): LengthAwarePaginator { ): LengthAwarePaginator {
return $this->repository->getAllWithRelationsPaginated( return $this->repository->getAllWithRelationsPaginated(
$perPage, $perPage,
$search, $search,
$status, $status,
$sortBy, $sortBy,
$sortOrder $sortOrder,
$dateFilters
); );
} }
@@ -25,6 +25,7 @@ class UserListResource extends JsonResource
'member_number' => $this->member_number, 'member_number' => $this->member_number,
'member_type' => $this->member_type, 'member_type' => $this->member_type,
'join_date' => $this->join_date, 'join_date' => $this->join_date,
'leave_date' => $this->leave_date,
'birth_date' => $this->birth_date, 'birth_date' => $this->birth_date,
'birth_place' => $this->birth_place, 'birth_place' => $this->birth_place,
'created_at' => $this->created_at, 'created_at' => $this->created_at,