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 @@ + + +