From cfd0b81f8086321dee29775e5f0be47c657ec817 Mon Sep 17 00:00:00 2001 From: ISMAIL MASSERAN Date: Thu, 9 Jul 2026 12:48:26 +0800 Subject: [PATCH] DONE: merge heir tab into profile tab, santize userIcNum and name, rename to malay, add stat card on user list --- src/composables/useRoleSwitcher.ts | 4 +- src/modules/auth/pages/Login.vue | 4 +- src/modules/auth/pages/Register.vue | 18 +- .../pages/MembershipApplication.vue | 25 +- .../membership-application-form.utils.ts | 14 +- .../profile/pages/ProfileOverview2.vue | 11 - src/modules/profile/pages/ProfileTab.vue | 18 +- src/modules/user/composables/useUserList.ts | 36 ++- src/modules/user/pages/UserCreate.vue | 15 +- src/modules/user/pages/UserEdit.vue | 15 +- src/modules/user/pages/UserHeirTab.vue | 38 --- src/modules/user/pages/UserList.vue | 241 ++++++++++++++---- src/modules/user/pages/UserProfileTab.vue | 27 ++ src/modules/user/pages/UserProfileView.vue | 117 ++++++--- src/modules/user/services/user.service.ts | 23 ++ src/modules/user/types/user.types.ts | 2 + src/utils/form-input.utils.ts | 7 + 17 files changed, 448 insertions(+), 167 deletions(-) delete mode 100644 src/modules/user/pages/UserHeirTab.vue create mode 100644 src/utils/form-input.utils.ts diff --git a/src/composables/useRoleSwitcher.ts b/src/composables/useRoleSwitcher.ts index 65b6fde..8fe06ed 100644 --- a/src/composables/useRoleSwitcher.ts +++ b/src/composables/useRoleSwitcher.ts @@ -86,9 +86,9 @@ export function useRoleSwitcher() { toast: true, position: 'top-end', icon: 'success', - title: res.message || 'Peranan telah ditukar.', + title: res.message, showConfirmButton: false, - timer: 3000, + timer: 500, }) await router.push(resolvePostLoginRoute(res.redirect_path)) diff --git a/src/modules/auth/pages/Login.vue b/src/modules/auth/pages/Login.vue index 89b6cad..317940c 100644 --- a/src/modules/auth/pages/Login.vue +++ b/src/modules/auth/pages/Login.vue @@ -117,7 +117,7 @@ const appVersion = import.meta.env.VITE_APP_VERSION + placeholder="Kata Laluan" autocomplete="current-password" required />
@@ -127,7 +127,7 @@ const appVersion = import.meta.env.VITE_APP_VERSION
diff --git a/src/modules/auth/pages/Register.vue b/src/modules/auth/pages/Register.vue index 4f40dac..ae5f4ee 100644 --- a/src/modules/auth/pages/Register.vue +++ b/src/modules/auth/pages/Register.vue @@ -8,6 +8,7 @@ import { Input } from '@/components/ui/input' import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert' import { PasswordInput } from '@/components/ui/password-input' import { getRegisterErrorMessage, register } from '@/modules/auth' +import { sanitizeIcNumberInput, sanitizeNameInput } from '@/utils/form-input.utils' import illustrationUrl from '@/assets/images/logo.svg' const router = useRouter() @@ -21,6 +22,14 @@ const termsAccepted = ref(false) const loading = ref(false) const errorMessage = ref('') +const handleNameInput = () => { + name.value = sanitizeNameInput(name.value) +} + +const handleIcNumberInput = () => { + icNumber.value = sanitizeIcNumberInput(icNumber.value) +} + const handleRegister = async () => { if (!termsAccepted.value) { errorMessage.value = 'Sila bersetuju dengan Dasar Privasi dan Terma dan Syarat.' @@ -90,15 +99,16 @@ const handleRegister = async () => {
+ placeholder="Nama Penuh" autocomplete="name" required @input="handleNameInput" /> + inputmode="numeric" maxlength="15" placeholder="Contoh: 900101011234" required + @input="handleIcNumberInput" /> + placeholder="Kata Laluan" autocomplete="new-password" minlength="8" required /> + placeholder="Sahkan Kata Laluan" autocomplete="new-password" minlength="8" required />
Nama Penuh - + {{ fieldErrors['applicant.name'] }} @@ -808,7 +820,8 @@ function stepLabelClass(stepId: number) {
Nama - + {{ fieldErrors[`heirs.${index}.name`] }} 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 a5fece2..f1ec091 100644 --- a/src/modules/membership-application/utils/membership-application-form.utils.ts +++ b/src/modules/membership-application/utils/membership-application-form.utils.ts @@ -61,9 +61,7 @@ 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 { sanitizeIcNumberInput } from '@/utils/form-input.utils' export function createSelectCollection(options: SelectOption[]) { return select.collection({ @@ -172,11 +170,11 @@ export function detailToFormState(detail: MembershipApplicationDetail): Membersh }, heirs: detail.heirs.length ? detail.heirs.map((heir) => ({ - name: toFormString(heir.name), - ic_number: toFormString(heir.ic_number), - relationship: toFormString(heir.relationship), - phone_number: toFormString(heir.phone_number), - })) + name: toFormString(heir.name), + ic_number: toFormString(heir.ic_number), + relationship: toFormString(heir.relationship), + phone_number: toFormString(heir.phone_number), + })) : [createEmptyHeir()], references: { proposer: { diff --git a/src/modules/profile/pages/ProfileOverview2.vue b/src/modules/profile/pages/ProfileOverview2.vue index bcf3d73..1105295 100644 --- a/src/modules/profile/pages/ProfileOverview2.vue +++ b/src/modules/profile/pages/ProfileOverview2.vue @@ -15,7 +15,6 @@ import MemberDigitalCard from '../components/MemberDigitalCard.vue' import ProfileTab from './ProfileTab.vue' import EmploymentTab from './EmploymentTab.vue' import BankDetailTab from './BankDetailTab.vue' -import HeirTab from './HeirTab.vue' import ChangePasswordTab from './ChangePasswordTab.vue' const authStore = useAuthStore() @@ -263,12 +262,6 @@ onMounted(async () => { - - - - @@ -294,10 +287,6 @@ onMounted(async () => { - - - -
diff --git a/src/modules/profile/pages/ProfileTab.vue b/src/modules/profile/pages/ProfileTab.vue index 89a0841..f4fef6a 100644 --- a/src/modules/profile/pages/ProfileTab.vue +++ b/src/modules/profile/pages/ProfileTab.vue @@ -20,6 +20,7 @@ import { SelectItemText, } from '@/components/ui/select' import { getApiErrorMessage, getApiValidationErrors } from '@/core/utils/getApiErrorMessage' +import { sanitizeIcNumberInput, sanitizeNameInput } from '@/utils/form-input.utils' import { createAddress, deleteAddress, @@ -29,6 +30,7 @@ import { import { updateProfile } from '@/modules/profile/services/profile.service' import type { Address, AddressPayload } from '@/modules/profile/types/address.types' import { useAuthStore } from '@/stores/auth' +import HeirTab from './HeirTab.vue' defineProps<{ embedded?: boolean @@ -159,6 +161,14 @@ const stateValue = ref([]) const addressTypeInitial = ref([]) const stateInitial = ref([]) +function handleNameInput() { + form.name = sanitizeNameInput(form.name) +} + +function handleIcNumberInput() { + form.ic_number = sanitizeIcNumberInput(form.ic_number) +} + function clearProfileFieldError(field: ProfileFieldKey) { delete profileErrors[field] } @@ -569,7 +579,8 @@ onMounted(async () => {
Nama - + E-mel @@ -577,7 +588,8 @@ onMounted(async () => { No. Kad Pengenalan - + No. Telefon @@ -817,5 +829,7 @@ onMounted(async () => {
+ +
diff --git a/src/modules/user/composables/useUserList.ts b/src/modules/user/composables/useUserList.ts index 2174de9..c7082d9 100644 --- a/src/modules/user/composables/useUserList.ts +++ b/src/modules/user/composables/useUserList.ts @@ -3,13 +3,15 @@ import debounce from 'lodash/debounce' import type { SortConfig } from '@/components/ui/usage/DataTable.vue' import { useApiPagination } from '@/composables/useApiPagination' import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage' -import { listUsers } from '../services/user.service' +import { getUserStats, listUsers } from '../services/user.service' import type { UserListItem } from '../types/user.types' export function useUserList() { const users = ref([]) const loading = ref(false) + const statsLoading = ref(false) const error = ref(null) + const statsError = ref(null) const search = ref('') const statusFilter = ref('') const joinDateFrom = ref('') @@ -19,6 +21,7 @@ export function useUserList() { const sortBy = ref([{ key: 'name', order: 'asc' }]) const page = ref(1) const itemsPerPage = ref(10) + const stats = ref<{ total: number; joined_this_month: number }>({ total: 0, joined_this_month: 0 }) const { pagination, applyPagination } = useApiPagination({ per_page: 10 }) @@ -64,6 +67,29 @@ export function useUserList() { } } + async function fetchStats() { + statsLoading.value = true + statsError.value = null + + try { + const res = await getUserStats({ + 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, + }) + + stats.value = res.data + } catch (err) { + statsError.value = getApiErrorMessage(err, 'Gagal memuatkan statistik pengguna.') + stats.value = { total: 0, joined_this_month: 0 } + } finally { + statsLoading.value = false + } + } + function handleSortUpdate(value: SortConfig[]) { sortBy.value = value fetchUsers(1) @@ -71,6 +97,7 @@ export function useUserList() { const debouncedSearch = debounce(() => { fetchUsers(1) + fetchStats() }, 400) watch(search, () => { @@ -79,10 +106,12 @@ export function useUserList() { watch(statusFilter, () => { fetchUsers(1) + fetchStats() }) watch([joinDateFrom, joinDateTo, leaveDateFrom, leaveDateTo], () => { fetchUsers(1) + fetchStats() }) watch(page, (nextPage, previousPage) => { @@ -99,11 +128,15 @@ export function useUserList() { onMounted(() => { fetchUsers(1) + fetchStats() }) return { users, loading, + stats, + statsLoading, + statsError, error, search, statusFilter, @@ -121,5 +154,6 @@ export function useUserList() { pagination, handleSortUpdate, fetchUsers, + fetchStats, } } diff --git a/src/modules/user/pages/UserCreate.vue b/src/modules/user/pages/UserCreate.vue index f488b72..f8f4c80 100644 --- a/src/modules/user/pages/UserCreate.vue +++ b/src/modules/user/pages/UserCreate.vue @@ -19,6 +19,7 @@ import { SelectItemText, } from '@/components/ui/select' import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage' +import { sanitizeIcNumberInput, sanitizeNameInput } from '@/utils/form-input.utils' import { createUser } from '../services/user.service' type SelectOption = { label: string; value: string } @@ -105,6 +106,14 @@ const form = reactive({ birth_place: '', }) +function handleNameInput() { + form.name = sanitizeNameInput(form.name) +} + +function handleIcNumberInput() { + form.ic_number = sanitizeIcNumberInput(form.ic_number) +} + function requireSelectValue(label: string | undefined, fieldName: string): string { if (!label) { throw new Error(`${fieldName} diperlukan.`) @@ -201,6 +210,7 @@ const formDisabled = computed(() => saving.value) placeholder="Nama penuh" :disabled="formDisabled" required + @input="handleNameInput" /> @@ -226,9 +236,12 @@ const formDisabled = computed(() => saving.value) v-model="form.ic_number" class="w-full" type="text" - placeholder="Nombor kad pengenalan" + inputmode="numeric" + maxlength="15" + placeholder="Contoh: 900101011234" :disabled="formDisabled" required + @input="handleIcNumberInput" /> diff --git a/src/modules/user/pages/UserEdit.vue b/src/modules/user/pages/UserEdit.vue index a6e5e6d..fc3d9bc 100644 --- a/src/modules/user/pages/UserEdit.vue +++ b/src/modules/user/pages/UserEdit.vue @@ -20,6 +20,7 @@ import { SelectItemText, } from '@/components/ui/select' import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage' +import { sanitizeIcNumberInput, sanitizeNameInput } from '@/utils/form-input.utils' import { getUser, updateUser } from '../services/user.service' type SelectOption = { label: string; value: string } @@ -121,6 +122,14 @@ const form = reactive({ birth_place: '', }) +function handleNameInput() { + form.name = sanitizeNameInput(form.name) +} + +function handleIcNumberInput() { + form.ic_number = sanitizeIcNumberInput(form.ic_number) +} + function toDateInputValue(value: string | null | undefined): string { if (!value) return '' return value.slice(0, 10) @@ -239,7 +248,7 @@ onMounted(() => { Nama + :disabled="formDisabled" required @input="handleNameInput" /> @@ -251,8 +260,8 @@ onMounted(() => {
Nombor Kad Pengenalan - + diff --git a/src/modules/user/pages/UserHeirTab.vue b/src/modules/user/pages/UserHeirTab.vue deleted file mode 100644 index d764516..0000000 --- a/src/modules/user/pages/UserHeirTab.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/src/modules/user/pages/UserList.vue b/src/modules/user/pages/UserList.vue index a7f6c9d..a00a2f8 100644 --- a/src/modules/user/pages/UserList.vue +++ b/src/modules/user/pages/UserList.vue @@ -1,12 +1,19 @@