From c88709a32bf8eb1678082ba7d0436f2b163ae2a2 Mon Sep 17 00:00:00 2001 From: ismailmasseran Date: Tue, 14 Jul 2026 12:03:22 +0800 Subject: [PATCH] Feature/phone register (#11) Co-authored-by: ISMAIL MASSERAN Co-authored-by: ISMAIL MASSERAN Reviewed-on: https://git.koppkb.com/KoPKB/My-KOPKB/pulls/11 --- src/constants/employers.ts | 24 + src/main/side-menu.ts | 2 + .../components/EmailVerificationBanner.vue | 46 ++ src/modules/auth/index.ts | 20 +- src/modules/auth/pages/AccountPending.vue | 19 +- src/modules/auth/pages/Login.vue | 20 +- src/modules/auth/pages/Register.vue | 395 +++++++++-- src/modules/auth/pages/VerifyEmail.vue | 164 ----- src/modules/auth/routes.ts | 6 - src/modules/auth/services/auth.service.ts | 72 +- src/modules/auth/types/auth.types.ts | 50 +- .../composables/useExternalSystemDetail.ts | 32 +- .../composables/useExternalSystemLaunch.ts | 49 ++ .../composables/useExternalSystemList.ts | 33 +- .../data/dummy-external-systems.ts | 38 -- .../pages/ExternalSystemDetail.vue | 7 +- .../pages/ExternalSystemList.vue | 17 +- .../services/external-system.service.ts | 41 ++ .../types/external-system.types.ts | 22 + .../feedback/components/HelpdeskFab.vue | 54 ++ .../feedback/composables/useFeedbackList.ts | 104 +++ src/modules/feedback/index.ts | 22 + src/modules/feedback/menu.ts | 10 + src/modules/feedback/pages/FeedbackDetail.vue | 486 ++++++++++++++ src/modules/feedback/pages/FeedbackList.vue | 289 ++++++++ src/modules/feedback/pages/FeedbackSubmit.vue | 347 ++++++++++ src/modules/feedback/routes.ts | 33 + .../feedback/services/feedback.service.ts | 170 +++++ src/modules/feedback/types/feedback.types.ts | 180 +++++ .../pages/MembershipApplication.vue | 2 + .../components/PhoneVerificationSection.vue | 184 ++++++ src/modules/profile/pages/EmploymentTab.vue | 6 +- src/modules/profile/pages/ProfileTab.vue | 11 +- src/modules/user/composables/useUserList.ts | 9 + src/modules/user/pages/UserCreate.vue | 27 +- src/modules/user/pages/UserEdit.vue | 618 +++++++++++++++++- src/modules/user/pages/UserList.vue | 43 ++ .../user/services/userEmployment.service.ts | 47 ++ src/modules/user/types/user.types.ts | 2 + src/router/index.ts | 9 +- src/stores/auth.ts | 2 + src/themes/Enigma/SideMenu/SideMenu.vue | 14 + src/themes/Layout.vue | 13 +- 43 files changed, 3368 insertions(+), 371 deletions(-) create mode 100644 src/constants/employers.ts create mode 100644 src/modules/auth/components/EmailVerificationBanner.vue delete mode 100644 src/modules/auth/pages/VerifyEmail.vue create mode 100644 src/modules/external-system/composables/useExternalSystemLaunch.ts delete mode 100644 src/modules/external-system/data/dummy-external-systems.ts create mode 100644 src/modules/external-system/services/external-system.service.ts create mode 100644 src/modules/feedback/components/HelpdeskFab.vue create mode 100644 src/modules/feedback/composables/useFeedbackList.ts create mode 100644 src/modules/feedback/index.ts create mode 100644 src/modules/feedback/menu.ts create mode 100644 src/modules/feedback/pages/FeedbackDetail.vue create mode 100644 src/modules/feedback/pages/FeedbackList.vue create mode 100644 src/modules/feedback/pages/FeedbackSubmit.vue create mode 100644 src/modules/feedback/routes.ts create mode 100644 src/modules/feedback/services/feedback.service.ts create mode 100644 src/modules/feedback/types/feedback.types.ts create mode 100644 src/modules/profile/components/PhoneVerificationSection.vue create mode 100644 src/modules/user/services/userEmployment.service.ts diff --git a/src/constants/employers.ts b/src/constants/employers.ts new file mode 100644 index 0000000..490b223 --- /dev/null +++ b/src/constants/employers.ts @@ -0,0 +1,24 @@ +export const EMPLOYERS = [ + { + name: 'Infra Quest Sdn. Bhd. (IQSB)', + address: 'Lot 1045, Jalan Dato’ Lundang, 15200 Kota Bharu, Kelantan', + }, + { + name: 'Permodalan Kelantan Berhad (PKB)', + address: + 'Permodalan Kelantan Berhad, Tingkat 4, Wisma Permodalan Kelantan Berhad, Jalan Maju, 15000 Kota Bharu Kelantan', + }, + { + name: 'Koperasi Permodalan Kelantan Berhad (KOPKB)', + address: + 'Lot Pt 448, Tingkat 1,Jalan Kuala Krai, Batu 3, Wakaf Che Yeh, 15150 Kota Bharu, Kelantan.', + }, + { + name: "An-Nisa'", + address: 'Jln Sultan Ibrahim, Bandar Kota Bharu, 15050 Kota Bharu, Kelantan.', + }, + { + name: 'Kel Infra Sdn. Bhd.', + address: 'Tingkat 2 Menara Perbadanan, Jalan Tengku Petra Semerak, 15000 Kota Bharu, Kelantan.', + }, +] as const diff --git a/src/main/side-menu.ts b/src/main/side-menu.ts index 9f4998c..8958ae5 100644 --- a/src/main/side-menu.ts +++ b/src/main/side-menu.ts @@ -6,6 +6,7 @@ import { activityMenu } from '@/modules/activity' import { dashboardMenu } from '@/modules/dashboard/menu' import { externalSystemMenu } from '@/modules/external-system/menu' import { activityLogMenu } from '@/modules/activity-log/menu' +import { feedbackMenu } from '@/modules/feedback' export type { Menu } @@ -17,6 +18,7 @@ const mainMenu: (string | Menu)[] = [ 'Teknologi Maklumat', ...roleMenu, ...activityLogMenu, + ...feedbackMenu, 'Pentadbiran', ...membershipApplicationMenu, ...userMenu, diff --git a/src/modules/auth/components/EmailVerificationBanner.vue b/src/modules/auth/components/EmailVerificationBanner.vue new file mode 100644 index 0000000..8e10740 --- /dev/null +++ b/src/modules/auth/components/EmailVerificationBanner.vue @@ -0,0 +1,46 @@ + + + diff --git a/src/modules/auth/index.ts b/src/modules/auth/index.ts index 7174ddf..acfc5a6 100644 --- a/src/modules/auth/index.ts +++ b/src/modules/auth/index.ts @@ -4,28 +4,36 @@ export { login, logout, register, - verifyEmail, - resendVerificationEmail, + sendPhoneVerificationOtp, + verifyPhoneVerificationOtp, + sendAuthenticatedPhoneVerificationOtp, + verifyAuthenticatedPhoneVerificationOtp, + sendVerificationEmail, requestForgotPassword, resetPassword, fetchCurrentUser, getAuthErrorMessage, getRegisterErrorMessage, - getVerifyEmailErrorMessage, + getPhoneVerificationErrorMessage, getForgotPasswordErrorMessage, getResetPasswordErrorMessage, resolvePostLoginRoute, resolvePostAuthRoute, isAccountPending, - isLoginVerificationRequired, + isEmailVerified, + isPhoneVerified, } from './services/auth.service' export type { LoginCredentials, LoginResponse, RegisterCredentials, RegisterResponse, - VerifyEmailPayload, - VerifyEmailResponse, + SendPhoneVerificationOtpPayload, + SendPhoneVerificationOtpResponse, + VerifyPhoneVerificationOtpPayload, + VerifyPhoneVerificationOtpResponse, + VerifyAuthenticatedPhoneVerificationOtpResponse, + ResendVerificationResponse, ForgotPasswordPayload, ForgotPasswordResponse, ResetPasswordPayload, diff --git a/src/modules/auth/pages/AccountPending.vue b/src/modules/auth/pages/AccountPending.vue index 9d0cdaf..0919a65 100644 --- a/src/modules/auth/pages/AccountPending.vue +++ b/src/modules/auth/pages/AccountPending.vue @@ -1,6 +1,6 @@ - +
Tiada sistem dijumpai

Cuba istilah carian yang berbeza.

diff --git a/src/modules/external-system/services/external-system.service.ts b/src/modules/external-system/services/external-system.service.ts new file mode 100644 index 0000000..0e1b07a --- /dev/null +++ b/src/modules/external-system/services/external-system.service.ts @@ -0,0 +1,41 @@ +import { api } from '@/core/services/api' +import type { + ExternalSystem, + ExternalSystemListResponse, + ExternalSystemResponse, + ExternalSystemSsoLaunchResponse, +} from '../types/external-system.types' + +export async function listExternalSystems(): Promise { + const { data } = await api.get('/v1/external-systems') + + if (!data.success) { + throw new Error(data.message ?? 'Gagal memuatkan senarai sistem luaran.') + } + + return data.data +} + +export async function getExternalSystem(id: string): Promise { + const { data } = await api.get(`/v1/external-systems/${id}`) + + if (!data.success) { + throw new Error(data.message ?? 'Sistem luaran tidak dijumpai.') + } + + return data.data +} + +export async function launchExternalSystemSso( + systemCode: string, +): Promise { + const { data } = await api.post( + `/v1/external-systems/${systemCode}/sso/launch`, + ) + + if (!data.success) { + throw new Error(data.message ?? 'Gagal membuka sistem.') + } + + return data +} diff --git a/src/modules/external-system/types/external-system.types.ts b/src/modules/external-system/types/external-system.types.ts index 2db3a87..2e590f3 100644 --- a/src/modules/external-system/types/external-system.types.ts +++ b/src/modules/external-system/types/external-system.types.ts @@ -13,8 +13,30 @@ export type ExternalSystem = { starts_at: string | null ends_at: string | null opens_in_new_tab: boolean + sso_enabled: boolean contact_email: string | null notes: string | null created_at: string updated_at: string } + +export type ExternalSystemSsoLaunchResponse = { + success: boolean + message?: string + data: { + launch_url: string + expires_at: string + } +} + +export type ExternalSystemListResponse = { + success: boolean + message?: string + data: ExternalSystem[] +} + +export type ExternalSystemResponse = { + success: boolean + message?: string + data: ExternalSystem +} diff --git a/src/modules/feedback/components/HelpdeskFab.vue b/src/modules/feedback/components/HelpdeskFab.vue new file mode 100644 index 0000000..788028b --- /dev/null +++ b/src/modules/feedback/components/HelpdeskFab.vue @@ -0,0 +1,54 @@ + + + diff --git a/src/modules/feedback/composables/useFeedbackList.ts b/src/modules/feedback/composables/useFeedbackList.ts new file mode 100644 index 0000000..62d9262 --- /dev/null +++ b/src/modules/feedback/composables/useFeedbackList.ts @@ -0,0 +1,104 @@ +import { onMounted, ref, watch } from 'vue' +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 { listFeedback } from '../services/feedback.service' +import type { + FeedbackListItem, + FeedbackPriority, + FeedbackStatus, + FeedbackType, +} from '../types/feedback.types' + +export function useFeedbackList() { + const items = ref([]) + const loading = ref(false) + const error = ref(null) + const search = ref('') + const typeFilter = ref('') + const statusFilter = ref('') + const priorityFilter = ref('') + const sortBy = ref([{ key: 'created_at', order: 'desc' }]) + const page = ref(1) + const itemsPerPage = ref(10) + + const { pagination, applyPagination } = useApiPagination({ per_page: 10 }) + + async function fetchItems(requestPage = page.value) { + loading.value = true + error.value = null + + try { + const activeSort = sortBy.value[0] + const data = await listFeedback({ + page: requestPage, + per_page: itemsPerPage.value, + sort_by: activeSort?.key ?? 'created_at', + sort_order: activeSort?.order ?? 'desc', + search: search.value.trim() || undefined, + type: typeFilter.value || undefined, + status: statusFilter.value || undefined, + priority: priorityFilter.value || undefined, + }) + + items.value = data.data + applyPagination(data.pagination) + page.value = data.pagination.current_page + } catch (err) { + error.value = getApiErrorMessage(err, 'Gagal memuatkan senarai maklum balas.') + items.value = [] + } finally { + loading.value = false + } + } + + function handleSortUpdate(value: SortConfig[]) { + sortBy.value = value + fetchItems(1) + } + + const debouncedSearch = debounce(() => { + fetchItems(1) + }, 400) + + watch(search, () => { + debouncedSearch() + }) + + watch([typeFilter, statusFilter, priorityFilter], () => { + fetchItems(1) + }) + + watch(page, (nextPage, previousPage) => { + if (nextPage !== previousPage) { + fetchItems(nextPage) + } + }) + + watch(itemsPerPage, (nextValue, previousValue) => { + if (nextValue !== previousValue) { + fetchItems(1) + } + }) + + onMounted(() => { + fetchItems(1) + }) + + return { + items, + loading, + error, + search, + typeFilter, + statusFilter, + priorityFilter, + sortBy, + page, + itemsPerPage, + pagination, + handleSortUpdate, + fetchItems, + } +} diff --git a/src/modules/feedback/index.ts b/src/modules/feedback/index.ts new file mode 100644 index 0000000..bfb8ce4 --- /dev/null +++ b/src/modules/feedback/index.ts @@ -0,0 +1,22 @@ +export { feedbackPublicRoutes, feedbackLayoutRoutes } from './routes' +export { feedbackMenu } from './menu' +export { default as HelpdeskFab } from './components/HelpdeskFab.vue' +export { + submitFeedback, + listFeedback, + getFeedback, + updateFeedback, + deleteFeedback, + getMyFeedback, + getFeedbackStatistics, +} from './services/feedback.service' +export type { + Feedback, + FeedbackFormState, + FeedbackListItem, + FeedbackStatus, + FeedbackType, + FeedbackPriority, + SubmitFeedbackPayload, + UpdateFeedbackPayload, +} from './types/feedback.types' diff --git a/src/modules/feedback/menu.ts b/src/modules/feedback/menu.ts new file mode 100644 index 0000000..c5f288d --- /dev/null +++ b/src/modules/feedback/menu.ts @@ -0,0 +1,10 @@ +import type { Menu } from '@/core/types/menu' + +export const feedbackMenu: Menu[] = [ + { + icon: 'MessageCircle', + route_name: 'list-feedback', + title: 'Maklum Balas', + permission: 'lihat maklum balas', + }, +] diff --git a/src/modules/feedback/pages/FeedbackDetail.vue b/src/modules/feedback/pages/FeedbackDetail.vue new file mode 100644 index 0000000..970914e --- /dev/null +++ b/src/modules/feedback/pages/FeedbackDetail.vue @@ -0,0 +1,486 @@ + + +