From 872957a88b796a13dc995d067e7cd9c7b6fab2cf Mon Sep 17 00:00:00 2001 From: ismailmasseran Date: Wed, 8 Jul 2026 12:15:41 +0800 Subject: [PATCH] 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 Reviewed-on: https://git.koppkb.com/KoPKB/My-KOPKB/pulls/9 --- Modules/Auth/Entities/User.php | 2 + Modules/Auth/Transformers/UserResource.php | 1 + ...embershipApplicationPassedNotification.php | 3 ++ .../Resources/views/emails/passed.blade.php | 9 +++++ .../Services/MembershipApplicationService.php | 34 ++++++++++++----- ...8_100000_add_leave_date_to_users_table.php | 22 +++++++++++ .../User/Http/Controllers/UserController.php | 37 ++++++++++++++++++- Modules/User/Http/Requests/UserRequest.php | 4 ++ .../Contracts/UserRepositoryInterface.php | 3 +- Modules/User/Repositories/UserRepository.php | 28 +++++++++++++- Modules/User/Services/UserService.php | 6 ++- .../User/Transformers/UserListResource.php | 1 + 12 files changed, 134 insertions(+), 16 deletions(-) create mode 100644 Modules/User/Database/Migrations/2026_07_08_100000_add_leave_date_to_users_table.php diff --git a/Modules/Auth/Entities/User.php b/Modules/Auth/Entities/User.php index a20c45f..f1b61c9 100644 --- a/Modules/Auth/Entities/User.php +++ b/Modules/Auth/Entities/User.php @@ -54,6 +54,7 @@ class User extends Authenticatable 'member_type', 'public_profile_token', 'join_date', + 'leave_date', 'birth_date', 'birth_place', 'onboarding_completed_at', @@ -136,6 +137,7 @@ class User extends Authenticatable 'member_type' => 'string', 'public_profile_token_expires_at' => 'datetime', 'join_date' => 'date', + 'leave_date' => 'date', 'birth_date' => 'date', 'birth_place' => 'string', 'onboarding_completed_at' => 'datetime', diff --git a/Modules/Auth/Transformers/UserResource.php b/Modules/Auth/Transformers/UserResource.php index b3cc612..1876e62 100644 --- a/Modules/Auth/Transformers/UserResource.php +++ b/Modules/Auth/Transformers/UserResource.php @@ -38,6 +38,7 @@ class UserResource extends JsonResource 'member_type' => $this->member_type, 'public_profile_token' => $this->public_profile_token, 'join_date' => $this->join_date, + 'leave_date' => $this->leave_date, 'birth_date' => $this->birth_date, 'birth_place' => $this->birth_place, 'onboarding_completed_at' => $this->onboarding_completed_at, diff --git a/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php b/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php index c07ec92..1e6331b 100644 --- a/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php +++ b/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php @@ -19,6 +19,7 @@ class MembershipApplicationPassedNotification extends Notification implements Sh public function __construct( public MembershipApplication $application, public Document $resultLetter, + public ?string $plainPassword = null, ) {} /** @@ -40,6 +41,8 @@ class MembershipApplicationPassedNotification extends Notification implements Sh data: [ 'application' => $this->application, 'applicant' => $applicant, + 'plainPassword' => $this->plainPassword, + 'loginUrl' => rtrim(config('user.frontend_url'), '/'), ], )->attach( Storage::disk(Document::STORAGE_DISK)->path($this->resultLetter->path), diff --git a/Modules/MembershipApplication/Resources/views/emails/passed.blade.php b/Modules/MembershipApplication/Resources/views/emails/passed.blade.php index 6a8f90b..03b7f95 100644 --- a/Modules/MembershipApplication/Resources/views/emails/passed.blade.php +++ b/Modules/MembershipApplication/Resources/views/emails/passed.blade.php @@ -9,6 +9,15 @@ Permohonan keahlian anda (**{{ $application->application_number }}**) **dilulusk 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. @include('emails.partials.footer') diff --git a/Modules/MembershipApplication/Services/MembershipApplicationService.php b/Modules/MembershipApplication/Services/MembershipApplicationService.php index 5c20f4b..29264a8 100644 --- a/Modules/MembershipApplication/Services/MembershipApplicationService.php +++ b/Modules/MembershipApplication/Services/MembershipApplicationService.php @@ -274,15 +274,18 @@ class MembershipApplicationService } return DB::transaction(function () use ($application, $boardMeetingReference, $boardMeetingDate) { + $plainPassword = null; + if ($application->board_result === BoardResult::Pass->value && ! $application->user_id) { - $user = $this->createMemberFromApplication($application); - $application->update(['user_id' => $user->id]); + $member = $this->createMemberFromApplication($application); + $application->update(['user_id' => $member['user']->id]); + $plainPassword = $member['plainPassword']; $application->refresh(); } $resultLetter = $this->storeResultLetter($application, $boardMeetingReference, $boardMeetingDate); - $this->sendResultNotification($application, $resultLetter); + $this->sendResultNotification($application, $resultLetter, $plainPassword); $application->update([ 'status' => ApplicationStatus::Completed, @@ -438,7 +441,7 @@ class MembershipApplicationService 'application' => $application, 'applicant' => $applicant, 'boardMeetingReference' => $boardMeetingReference, - 'boardMeetingDate' => Carbon::parse($boardMeetingDate)->translatedFormat('d M Y'), + 'boardMeetingDate' => Carbon::parse($boardMeetingDate)->translatedFormat('d F Y'), 'isPassed' => $isPassed, 'letterSubject' => $isPassed ? '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']); $applicant = $application->applicant; @@ -458,10 +464,12 @@ class MembershipApplicationService ]); } + $plainPassword = Str::password(16); + $user = User::create([ 'name' => $applicant->name, 'email' => $applicant->email, - 'password' => Hash::make(Str::password(16)), + 'password' => Hash::make($plainPassword), 'ic_number' => $applicant->ic_number, 'phone_number' => $applicant->phone_number, 'position' => $applicant->current_position, @@ -507,7 +515,10 @@ class MembershipApplicationService app(EmailVerificationOtpService::class)->send($user); - return $user; + return [ + 'user' => $user, + 'plainPassword' => $plainPassword, + ]; } public function lookupMemberByIcNumber(string $icNumber): ?User @@ -532,12 +543,15 @@ class MembershipApplicationService ->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'); $notification = $application->board_result === BoardResult::Pass->value - ? new MembershipApplicationPassedNotification($application, $resultLetter) + ? new MembershipApplicationPassedNotification($application, $resultLetter, $plainPassword) : new MembershipApplicationFailedNotification($application, $resultLetter); Notification::route('mail', $application->applicant->email)->notify($notification); diff --git a/Modules/User/Database/Migrations/2026_07_08_100000_add_leave_date_to_users_table.php b/Modules/User/Database/Migrations/2026_07_08_100000_add_leave_date_to_users_table.php new file mode 100644 index 0000000..501e1ed --- /dev/null +++ b/Modules/User/Database/Migrations/2026_07_08_100000_add_leave_date_to_users_table.php @@ -0,0 +1,22 @@ +date('leave_date')->nullable()->after('join_date'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('leave_date'); + }); + } +}; diff --git a/Modules/User/Http/Controllers/UserController.php b/Modules/User/Http/Controllers/UserController.php index 3e3ffb3..979a2ef 100644 --- a/Modules/User/Http/Controllers/UserController.php +++ b/Modules/User/Http/Controllers/UserController.php @@ -44,7 +44,7 @@ class UserController extends BaseCrudController $validated['uuid'] = Str::uuid(); $validated['password'] = Hash::make('suteraselamanya'); - return $validated; + return $this->applyLeaveDateForStatus($validated); } /** @@ -58,6 +58,25 @@ class UserController extends BaseCrudController 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; } @@ -171,13 +190,27 @@ class UserController extends BaseCrudController $this->authorize('viewAny', $this->modelClass); 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); + $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( $perPage, $request->get('search', ''), $request->get('status', ''), $request->get('sort_by', 'id'), - $request->get('sort_order', 'asc') + $request->get('sort_order', 'asc'), + $dateFilters ); return response()->json([ diff --git a/Modules/User/Http/Requests/UserRequest.php b/Modules/User/Http/Requests/UserRequest.php index 1fb5207..49a30b7 100644 --- a/Modules/User/Http/Requests/UserRequest.php +++ b/Modules/User/Http/Requests/UserRequest.php @@ -44,6 +44,7 @@ class UserRequest extends FormRequest 'member_number' => 'required|integer', 'member_type' => 'required|string|max:255', 'join_date' => 'nullable|date', + 'leave_date' => 'nullable|date', 'birth_date' => 'nullable|date', 'birth_place' => 'nullable|string|max:255', ]; @@ -74,6 +75,7 @@ class UserRequest extends FormRequest 'member_number' => 'required|integer', 'member_type' => 'required|string|max:255', 'join_date' => 'nullable|date', + 'leave_date' => 'nullable|date', 'birth_date' => 'nullable|date', 'birth_place' => 'nullable|string|max:255', ]; @@ -100,6 +102,7 @@ class UserRequest extends FormRequest 'member_number' => 'required|integer', 'member_type' => 'required|string|max:255', 'join_date' => 'required|date', + 'leave_date' => 'nullable|date', 'birth_date' => 'required|date', 'birth_place' => 'required|string|max:255', ]; @@ -136,6 +139,7 @@ class UserRequest extends FormRequest 'member_type.max' => 'Jenis anggota tidak boleh melebihi 255 aksara.', 'join_date.required' => 'Tarikh join diperlukan.', 'join_date.date' => 'Tarikh join tidak sah.', + 'leave_date.date' => 'Tarikh berhenti tidak sah.', 'birth_date.required' => 'Tarikh lahir diperlukan.', 'birth_date.date' => 'Tarikh lahir tidak sah.', 'birth_place.required' => 'Tempat lahir diperlukan.', diff --git a/Modules/User/Repositories/Contracts/UserRepositoryInterface.php b/Modules/User/Repositories/Contracts/UserRepositoryInterface.php index d6a0916..c5527ee 100644 --- a/Modules/User/Repositories/Contracts/UserRepositoryInterface.php +++ b/Modules/User/Repositories/Contracts/UserRepositoryInterface.php @@ -20,7 +20,8 @@ interface UserRepositoryInterface string $search = '', string $status = '', string $sortBy = 'name', - string $sortOrder = 'asc' + string $sortOrder = 'asc', + array $dateFilters = [] ); /** diff --git a/Modules/User/Repositories/UserRepository.php b/Modules/User/Repositories/UserRepository.php index 33ce614..b53d18f 100644 --- a/Modules/User/Repositories/UserRepository.php +++ b/Modules/User/Repositories/UserRepository.php @@ -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 */ @@ -46,7 +65,8 @@ class UserRepository implements UserRepositoryInterface string $search = '', string $status = '', string $sortBy = 'name', - string $sortOrder = 'asc' + string $sortOrder = 'asc', + array $dateFilters = [] ) { $allowedSortColumns = [ 'id', @@ -55,6 +75,8 @@ class UserRepository implements UserRepositoryInterface 'position', 'status', 'member_number', + 'join_date', + 'leave_date', 'created_at', 'deleted_at', ]; @@ -73,6 +95,8 @@ class UserRepository implements UserRepositoryInterface $query->where('status', $status); } + $this->applyDateRangeFilters($query, $dateFilters); + return $query->paginate($perPage); } @@ -93,6 +117,8 @@ class UserRepository implements UserRepositoryInterface 'position', 'status', 'member_number', + 'join_date', + 'leave_date', 'created_at', 'deleted_at', ]; diff --git a/Modules/User/Services/UserService.php b/Modules/User/Services/UserService.php index 0033e64..67116df 100644 --- a/Modules/User/Services/UserService.php +++ b/Modules/User/Services/UserService.php @@ -21,14 +21,16 @@ class UserService string $search, string $status, string $sortBy, - string $sortOrder + string $sortOrder, + array $dateFilters = [] ): LengthAwarePaginator { return $this->repository->getAllWithRelationsPaginated( $perPage, $search, $status, $sortBy, - $sortOrder + $sortOrder, + $dateFilters ); } diff --git a/Modules/User/Transformers/UserListResource.php b/Modules/User/Transformers/UserListResource.php index 136aef2..2a9f4dd 100644 --- a/Modules/User/Transformers/UserListResource.php +++ b/Modules/User/Transformers/UserListResource.php @@ -25,6 +25,7 @@ class UserListResource extends JsonResource 'member_number' => $this->member_number, 'member_type' => $this->member_type, 'join_date' => $this->join_date, + 'leave_date' => $this->leave_date, 'birth_date' => $this->birth_date, 'birth_place' => $this->birth_place, 'created_at' => $this->created_at,