diff --git a/src/components/ui/usage/DataTable.vue b/src/components/ui/usage/DataTable.vue index 5f63286..a6d2df0 100644 --- a/src/components/ui/usage/DataTable.vue +++ b/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/src/modules/dashboard/pages/DashboardOverview.vue b/src/modules/dashboard/pages/DashboardOverview.vue index f03d278..8742090 100644 --- a/src/modules/dashboard/pages/DashboardOverview.vue +++ b/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/src/modules/membership-application/pages/MembershipApplication.vue b/src/modules/membership-application/pages/MembershipApplication.vue index a43724a..d818540 100644 --- a/src/modules/membership-application/pages/MembershipApplication.vue +++ b/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/src/modules/membership-application/pages/MembershipApplicationDetail.vue b/src/modules/membership-application/pages/MembershipApplicationDetail.vue index 9dd5694..af07050 100644 --- a/src/modules/membership-application/pages/MembershipApplicationDetail.vue +++ b/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/src/modules/membership-application/utils/membership-application-form.utils.ts b/src/modules/membership-application/utils/membership-application-form.utils.ts index 483b8cd..a5fece2 100644 --- a/src/modules/membership-application/utils/membership-application-form.utils.ts +++ b/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/src/modules/user/composables/useUserList.ts b/src/modules/user/composables/useUserList.ts index f19daf2..2174de9 100644 --- a/src/modules/user/composables/useUserList.ts +++ b/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/src/modules/user/menu.ts b/src/modules/user/menu.ts index 9fba37a..a91501e 100644 --- a/src/modules/user/menu.ts +++ b/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/src/modules/user/pages/UserEdit.vue b/src/modules/user/pages/UserEdit.vue index 0808321..a6e5e6d 100644 --- a/src/modules/user/pages/UserEdit.vue +++ b/src/modules/user/pages/UserEdit.vue @@ -1,6 +1,7 @@