From ebd3caecc6ddaa6a53decd789e44c11f52454c40 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 --- be/Modules/Auth/Entities/User.php | 2 + be/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 +++++++- be/Modules/User/Http/Requests/UserRequest.php | 4 + .../Contracts/UserRepositoryInterface.php | 3 +- .../User/Repositories/UserRepository.php | 28 +++++- be/Modules/User/Services/UserService.php | 6 +- .../User/Transformers/UserListResource.php | 1 + fe/src/components/ui/usage/DataTable.vue | 14 ++- .../dashboard/pages/DashboardOverview.vue | 2 +- .../pages/MembershipApplication.vue | 34 +++++-- .../pages/MembershipApplicationDetail.vue | 18 +++- .../membership-application-form.utils.ts | 4 + .../modules/user/composables/useUserList.ts | 37 +++++++- fe/src/modules/user/menu.ts | 2 +- fe/src/modules/user/pages/UserEdit.vue | 28 +++++- fe/src/modules/user/pages/UserList.vue | 92 +++++++++++++++++-- fe/src/modules/user/pages/UserProfileTab.vue | 4 + fe/src/modules/user/types/user.types.ts | 8 ++ 23 files changed, 352 insertions(+), 41 deletions(-) create mode 100644 be/Modules/User/Database/Migrations/2026_07_08_100000_add_leave_date_to_users_table.php diff --git a/be/Modules/Auth/Entities/User.php b/be/Modules/Auth/Entities/User.php index a20c45f..f1b61c9 100644 --- a/be/Modules/Auth/Entities/User.php +++ b/be/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/be/Modules/Auth/Transformers/UserResource.php b/be/Modules/Auth/Transformers/UserResource.php index b3cc612..1876e62 100644 --- a/be/Modules/Auth/Transformers/UserResource.php +++ b/be/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/be/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php b/be/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php index c07ec92..1e6331b 100644 --- a/be/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php +++ b/be/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/be/Modules/MembershipApplication/Resources/views/emails/passed.blade.php b/be/Modules/MembershipApplication/Resources/views/emails/passed.blade.php index 6a8f90b..03b7f95 100644 --- a/be/Modules/MembershipApplication/Resources/views/emails/passed.blade.php +++ b/be/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/be/Modules/MembershipApplication/Services/MembershipApplicationService.php b/be/Modules/MembershipApplication/Services/MembershipApplicationService.php index 5c20f4b..29264a8 100644 --- a/be/Modules/MembershipApplication/Services/MembershipApplicationService.php +++ b/be/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/be/Modules/User/Database/Migrations/2026_07_08_100000_add_leave_date_to_users_table.php b/be/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/be/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/be/Modules/User/Http/Controllers/UserController.php b/be/Modules/User/Http/Controllers/UserController.php index 3e3ffb3..979a2ef 100644 --- a/be/Modules/User/Http/Controllers/UserController.php +++ b/be/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/be/Modules/User/Http/Requests/UserRequest.php b/be/Modules/User/Http/Requests/UserRequest.php index 1fb5207..49a30b7 100644 --- a/be/Modules/User/Http/Requests/UserRequest.php +++ b/be/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/be/Modules/User/Repositories/Contracts/UserRepositoryInterface.php b/be/Modules/User/Repositories/Contracts/UserRepositoryInterface.php index d6a0916..c5527ee 100644 --- a/be/Modules/User/Repositories/Contracts/UserRepositoryInterface.php +++ b/be/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/be/Modules/User/Repositories/UserRepository.php b/be/Modules/User/Repositories/UserRepository.php index 33ce614..b53d18f 100644 --- a/be/Modules/User/Repositories/UserRepository.php +++ b/be/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/be/Modules/User/Services/UserService.php b/be/Modules/User/Services/UserService.php index 0033e64..67116df 100644 --- a/be/Modules/User/Services/UserService.php +++ b/be/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/be/Modules/User/Transformers/UserListResource.php b/be/Modules/User/Transformers/UserListResource.php index 136aef2..2a9f4dd 100644 --- a/be/Modules/User/Transformers/UserListResource.php +++ b/be/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, diff --git a/fe/src/components/ui/usage/DataTable.vue b/fe/src/components/ui/usage/DataTable.vue index 5f63286..a6d2df0 100644 --- a/fe/src/components/ui/usage/DataTable.vue +++ b/fe/src/components/ui/usage/DataTable.vue @@ -100,6 +100,8 @@ const props = withDefaults(defineProps<{ currentSort?: SortConfig[]; exportable?: boolean; exportFileName?: string; + /** Title shown at the top of exported PDF documents. */ + exportPdfTitle?: string; /** When true, table body scrolls inside a max-height box instead of growing the page. */ scrollable?: boolean; /** Max height of the scrollable table area (CSS value). */ @@ -622,12 +624,22 @@ const exportToPDF = () => { else if (colCount > 8) fontSize = 7; const cellPadding = colCount > 8 ? 4 : 8; + let tableStartY = 20; + if (props.exportPdfTitle?.trim()) { + doc.setFontSize(14); + doc.text(props.exportPdfTitle.trim(), doc.internal.pageSize.getWidth() / 2, 28, { + align: 'center', + }); + tableStartY = 48; + } + const tableOptions: Parameters[1] = { head: [headers], body: data, + startY: tableStartY, styles: { fontSize, cellPadding }, headStyles: { fillColor: [66, 139, 202] }, - margin: { top: 20 }, + margin: { top: tableStartY }, }; // For wide tables: let table use natural column widths and split across pages diff --git a/fe/src/modules/dashboard/pages/DashboardOverview.vue b/fe/src/modules/dashboard/pages/DashboardOverview.vue index f03d278..8742090 100644 --- a/fe/src/modules/dashboard/pages/DashboardOverview.vue +++ b/fe/src/modules/dashboard/pages/DashboardOverview.vue @@ -59,7 +59,7 @@ const availableModules = computed(() => [ visible: hasPermission('lihat aktiviti'), }, { - title: 'Senarai Pengguna', + title: 'Senarai Daftar Anggota', description: 'Pantau dan urus akaun pengguna sistem.', route: 'list-users', icon: 'Users' as Icon, diff --git a/fe/src/modules/membership-application/pages/MembershipApplication.vue b/fe/src/modules/membership-application/pages/MembershipApplication.vue index a43724a..d818540 100644 --- a/fe/src/modules/membership-application/pages/MembershipApplication.vue +++ b/fe/src/modules/membership-application/pages/MembershipApplication.vue @@ -22,6 +22,7 @@ import { AlertRoot, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Lucide } from '@/components/ui/lucide' import { getApiErrorMessage, getApiValidationErrors } from '@/core/utils/getApiErrorMessage' import { submitMembershipApplication, lookupMemberByIcNumber } from '../services/membership-application.service' +import { sanitizeIcNumberInput } from '../utils/membership-application-form.utils' import type { MembershipApplicationFormState, MembershipApplicationHeirForm, @@ -37,7 +38,7 @@ const MIN_FEE_MONTHLY_CONTRIBUTION = 30 const steps = [ { id: 1, label: 'Maklumat Peribadi' }, - { id: 2, label: 'Hubungan & Alamat' }, + { id: 2, label: 'No. Telefon & Alamat' }, { id: 3, label: 'Maklumat Pekerjaan' }, { id: 4, label: 'Maklumat Penama' }, { id: 5, label: 'Dokumen & Hantar' }, @@ -221,9 +222,22 @@ function clearReference(role: 'proposer' | 'supporter') { } function handleReferenceIcInput(role: 'proposer' | 'supporter') { + form.references[role].ic_number = sanitizeIcNumberInput(form.references[role].ic_number) clearReference(role) } +function handleApplicantIcInput() { + form.applicant.ic_number = sanitizeIcNumberInput(form.applicant.ic_number) + delete fieldErrors['applicant.ic_number'] +} + +function handleHeirIcInput(index: number) { + const heir = form.heirs[index] + if (!heir) return + heir.ic_number = sanitizeIcNumberInput(heir.ic_number) + delete fieldErrors[`heirs.${index}.ic_number`] +} + async function lookupReference(role: 'proposer' | 'supporter') { const reference = form.references[role] const icNumber = reference.ic_number.trim() @@ -618,7 +632,8 @@ function stepLabelClass(stepId: number) { No. Kad Pengenalan - + {{ fieldErrors['applicant.ic_number'] }} @@ -800,7 +815,8 @@ function stepLabelClass(stepId: number) { No. Kad Pengenalan - + {{ fieldErrors[`heirs.${index}.ic_number`] }} @@ -857,8 +873,9 @@ function stepLabelClass(stepId: number) { No. KP Pencadang + inputmode="numeric" maxlength="12" placeholder="Contoh: 900101011234" + :disabled="referenceLookupLoading.proposer" @input="handleReferenceIcInput('proposer')" + @blur="lookupReference('proposer')" />

{{ form.references.proposer.name }}

@@ -870,8 +887,9 @@ function stepLabelClass(stepId: number) { No. KP Penyokong + inputmode="numeric" maxlength="12" placeholder="Contoh: 850505055678" + :disabled="referenceLookupLoading.supporter" @input="handleReferenceIcInput('supporter')" + @blur="lookupReference('supporter')" />

{{ form.references.supporter.name }}

@@ -907,7 +925,7 @@ function stepLabelClass(stepId: number) { {{ fieldErrors['documents.employer_letter'] - }} + }}
diff --git a/fe/src/modules/membership-application/pages/MembershipApplicationDetail.vue b/fe/src/modules/membership-application/pages/MembershipApplicationDetail.vue index 9dd5694..af07050 100644 --- a/fe/src/modules/membership-application/pages/MembershipApplicationDetail.vue +++ b/fe/src/modules/membership-application/pages/MembershipApplicationDetail.vue @@ -65,6 +65,8 @@ const DOCUMENT_TYPE_LABELS: Record = { [RESULT_LETTER_DOCUMENT_TYPE]: 'Surat Keputusan', } +const BOARD_MEETING_REFERENCE_PREFIX = 'Mesyuarat Lembaga Bil.' + const router = useRouter() const route = useRoute() const { hasPermission } = usePermissions() @@ -430,8 +432,10 @@ async function confirmPendingAction() { }) boardRemarks.value = '' } else { + const boardMeetingReferenceValue = `${BOARD_MEETING_REFERENCE_PREFIX} ${boardMeetingReference.value.trim()}` + response = await completeMembershipApplication(applicationId.value, { - board_meeting_reference: boardMeetingReference.value.trim(), + board_meeting_reference: boardMeetingReferenceValue, board_meeting_date: boardMeetingDate.value, }) } @@ -1013,9 +1017,15 @@ onUnmounted(() => {
{{ confirmDialogDescription }}
Rujukan Mesyuarat Lembaga - +
+ + {{ BOARD_MEETING_REFERENCE_PREFIX }} + + +
{{ boardMeetingReferenceError }}
diff --git a/fe/src/modules/membership-application/utils/membership-application-form.utils.ts b/fe/src/modules/membership-application/utils/membership-application-form.utils.ts index 483b8cd..a5fece2 100644 --- a/fe/src/modules/membership-application/utils/membership-application-form.utils.ts +++ b/fe/src/modules/membership-application/utils/membership-application-form.utils.ts @@ -61,6 +61,10 @@ export type ApplicantDocumentUploadType = (typeof APPLICANT_DOCUMENT_UPLOAD_TYPE export type AdminDocumentUploadType = (typeof ADMIN_DOCUMENT_UPLOAD_TYPES)[number] export type DocumentUploadType = ApplicantDocumentUploadType | AdminDocumentUploadType +export function sanitizeIcNumberInput(value: string): string { + return value.replace(/\D/g, '') +} + export function createSelectCollection(options: SelectOption[]) { return select.collection({ items: options, diff --git a/fe/src/modules/user/composables/useUserList.ts b/fe/src/modules/user/composables/useUserList.ts index f19daf2..2174de9 100644 --- a/fe/src/modules/user/composables/useUserList.ts +++ b/fe/src/modules/user/composables/useUserList.ts @@ -1,4 +1,4 @@ -import { onMounted, ref, watch } from 'vue' +import { computed, onMounted, ref, watch } from 'vue' import debounce from 'lodash/debounce' import type { SortConfig } from '@/components/ui/usage/DataTable.vue' import { useApiPagination } from '@/composables/useApiPagination' @@ -12,12 +12,29 @@ export function useUserList() { const error = ref(null) const search = ref('') const statusFilter = ref('') + const joinDateFrom = ref('') + const joinDateTo = ref('') + const leaveDateFrom = ref('') + const leaveDateTo = ref('') const sortBy = ref([{ key: 'name', order: 'asc' }]) const page = ref(1) const itemsPerPage = ref(10) const { pagination, applyPagination } = useApiPagination({ per_page: 10 }) + const hasJoinDateFilters = computed(() => Boolean(joinDateFrom.value || joinDateTo.value)) + const hasLeaveDateFilters = computed(() => Boolean(leaveDateFrom.value || leaveDateTo.value)) + + function clearJoinDateFilters() { + joinDateFrom.value = '' + joinDateTo.value = '' + } + + function clearLeaveDateFilters() { + leaveDateFrom.value = '' + leaveDateTo.value = '' + } + async function fetchUsers(requestPage = page.value) { loading.value = true error.value = null @@ -31,11 +48,17 @@ export function useUserList() { sort_order: activeSort?.order ?? 'asc', search: search.value.trim() || undefined, status: statusFilter.value.trim() || undefined, + join_date_from: joinDateFrom.value || undefined, + join_date_to: joinDateTo.value || undefined, + leave_date_from: leaveDateFrom.value || undefined, + leave_date_to: leaveDateTo.value || undefined, }) users.value = data.data applyPagination(data.pagination) page.value = data.pagination.current_page + } catch (err) { + error.value = getApiErrorMessage(err, 'Gagal memuatkan senarai pengguna.') } finally { loading.value = false } @@ -58,6 +81,10 @@ export function useUserList() { fetchUsers(1) }) + watch([joinDateFrom, joinDateTo, leaveDateFrom, leaveDateTo], () => { + fetchUsers(1) + }) + watch(page, (nextPage, previousPage) => { if (nextPage !== previousPage) { fetchUsers(nextPage) @@ -80,6 +107,14 @@ export function useUserList() { error, search, statusFilter, + joinDateFrom, + joinDateTo, + leaveDateFrom, + leaveDateTo, + hasJoinDateFilters, + hasLeaveDateFilters, + clearJoinDateFilters, + clearLeaveDateFilters, sortBy, page, itemsPerPage, diff --git a/fe/src/modules/user/menu.ts b/fe/src/modules/user/menu.ts index 9fba37a..a91501e 100644 --- a/fe/src/modules/user/menu.ts +++ b/fe/src/modules/user/menu.ts @@ -4,7 +4,7 @@ export const userMenu: Menu[] = [ { icon: 'Users', route_name: 'list-users', - title: 'Senarai Pengguna', + title: 'Senarai Daftar Anggota', permission: 'lihat pengguna', }, ] diff --git a/fe/src/modules/user/pages/UserEdit.vue b/fe/src/modules/user/pages/UserEdit.vue index 0808321..a6e5e6d 100644 --- a/fe/src/modules/user/pages/UserEdit.vue +++ b/fe/src/modules/user/pages/UserEdit.vue @@ -1,6 +1,7 @@