diff --git a/src/pages/account/home/user-profile/AccountUserProfileContent.tsx b/src/pages/account/home/user-profile/AccountUserProfileContent.tsx index b971b5e..45cec39 100644 --- a/src/pages/account/home/user-profile/AccountUserProfileContent.tsx +++ b/src/pages/account/home/user-profile/AccountUserProfileContent.tsx @@ -1,17 +1,32 @@ import { BasicSettings, Password } from './blocks'; +import GenerateQr from './blocks/GenerateQr'; import { PinCode } from './blocks/PinCode'; import { AccountUserProfileContextProvider } from './hooks'; const AccountUserProfileContent = () => { return ( -
- - {/* */} - - - {/* Uncomment the line below to enable the Delete Account feature */} - {/* */} - +
+
+
+

Account Settings

+

Manage your security preferences and account settings

+
+ + +
+ {/* Left Column - Security Settings */} +
+ + +
+ + {/* Right Column - MFA Setup */} +
+ +
+
+
+
); }; diff --git a/src/pages/account/home/user-profile/blocks/GenerateQr.tsx b/src/pages/account/home/user-profile/blocks/GenerateQr.tsx new file mode 100644 index 0000000..f8fba97 --- /dev/null +++ b/src/pages/account/home/user-profile/blocks/GenerateQr.tsx @@ -0,0 +1,219 @@ +import { getAuth } from '@/auth'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { apiConfig } from '@/config/api.config'; +import { useCallApi } from '@/hooks'; +import { RefreshCw, Shield, Smartphone, QrCode } from 'lucide-react'; +import { useState } from 'react'; +import { toast } from 'sonner'; +import clsx from 'clsx'; + +const API_URL = apiConfig.service_dashboard; + +const GenerateQr = () => { + const { GetData, PostData } = useCallApi(); + const parsedUser = getAuth()?.user; + const [qrCodeUrl, setQrCodeUrl] = useState(null); + const [inputToken, setInputToken] = useState(''); + const [loading, setLoading] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + + const getQrCode = async () => { + if (!parsedUser?.id) { + toast.error('User ID not found'); + return; + } + + setLoading(true); + setError(null); + + try { + const response = await GetData(`${API_URL}/user/generate_mfa/${parsedUser.id}`, {}); + if (response?.status) { + setQrCodeUrl(response.data.token_base64); + toast.success('QR Code generated successfully'); + } else { + setError('QR Code not found'); + toast.error(response?.message); + } + } catch (error) { + toast.error('Something went wrong, please try again'); + } finally { + setLoading(false); + } + }; + + const verifyToken = async () => { + if (!parsedUser?.id) { + toast.error('User ID not found'); + return; + } + + setIsSubmitting(true); + + try { + const response = await PostData(`${API_URL}/user/verify_mfa`, { + id: parsedUser.id, + token: inputToken + }); + + if (response?.status) { + toast.success('Token verified successfully'); + setInputToken(''); + } else { + toast.error(response?.message); + } + } catch (error) { + toast.error('Something went wrong, please try again'); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+
+
+ +
+

Multi-Factor Authentication

+
+
+ +
+ {!qrCodeUrl ? ( + // Initial Setup State +
+
+ +
+ +
+

Secure Your Account

+

+ Add an extra layer of security to your account with multi-factor authentication + using your mobile device. +

+
+ +
+
+
+ 1 +
+
+

Download an authenticator app

+

+ Google Authenticator{' '} + + click here + +

+
+
+
+ + +
+ ) : ( + // QR Code Generated State +
+
+
+ +
+ +
+

Scan QR Code

+

+ Open your authenticator app and scan this QR code +

+
+
+ +
+
+ QR Code MFA +
+
+ +
+
+ + setInputToken(e.target.value.replace(/\D/g, '').slice(0, 6))} + maxLength={6} + /> +

+ Enter the 6-digit code from your authenticator app +

+
+ + +
+
+ )} + + {error && ( +
+

{error}

+
+ )} +
+
+ ); +}; + +export default GenerateQr; diff --git a/src/pages/account/home/user-profile/blocks/Password.tsx b/src/pages/account/home/user-profile/blocks/Password.tsx index 0e49ec4..218f2bb 100644 --- a/src/pages/account/home/user-profile/blocks/Password.tsx +++ b/src/pages/account/home/user-profile/blocks/Password.tsx @@ -6,6 +6,7 @@ import { KeenIcon } from '@/components'; import clsx from 'clsx'; type PasswordType = 'password' | 'retype_password' | 'current_password'; + const Password = () => { const { setPassword } = useContext(AccountUserProfileContext); const { getUser } = useAuthContext(); @@ -93,103 +94,139 @@ const Password = () => { }, []); return ( -
-
-

Password

+
+
+

Change Password

+

+ Update your password to keep your account secure +

-
-
- -
+ +
+ {/* Current Password */} +
+ +
setCurrentPassword(e.target.value)} disabled={isSubmitting} /> -
-
- -
- setNewPassword(e.target.value)} - disabled={isSubmitting} - type={showPassword.password ? 'text' : 'password'} - /> - -
-
-
-
- -
- setConfirmPassword(e.target.value)} - disabled={isSubmitting} - /> - -
-
-
- - {passwordErrors.length > 0 && ( -
- {passwordErrors.map((error, index) => ( -

{error}

- ))} -
- )} -
-
-
+ {/* New Password */} +
+ +
+ setNewPassword(e.target.value)} + disabled={isSubmitting} + /> + +
+
+ + {/* Confirm Password */} +
+ +
+ setConfirmPassword(e.target.value)} + disabled={isSubmitting} + /> + +
+ + {/* Password Requirements */} + {passwordErrors.length > 0 && ( +
+ {passwordErrors.map((error, index) => ( +

+ + {error} +

+ ))} +
+ )} +
+ + {/* Submit Button */} +
diff --git a/src/pages/account/home/user-profile/blocks/PinCode.tsx b/src/pages/account/home/user-profile/blocks/PinCode.tsx index a1def53..a456b6c 100644 --- a/src/pages/account/home/user-profile/blocks/PinCode.tsx +++ b/src/pages/account/home/user-profile/blocks/PinCode.tsx @@ -2,6 +2,7 @@ import { useState, useContext, useCallback, MouseEvent } from 'react'; import { AccountUserProfileContext } from '../hooks'; import { toast } from 'sonner'; import { KeenIcon } from '@/components'; +import clsx from 'clsx'; type PasswordType = 'current' | 'new' | 'retype'; @@ -16,12 +17,12 @@ const PinCode = () => { const [showPassword, setShowPassword] = useState({ current: false, new: false, - retype: false, + retype: false }); // Fungsi hanya angka, maksimal 6 digit const handleNumericInput = (value: string) => { - return value.replace(/\D/g, '').slice(0, 100); + return value.replace(/\D/g, '').slice(0, 6); }; const handleResetPassword = useCallback(async () => { @@ -37,7 +38,7 @@ const PinCode = () => { await setPincode({ currentpincode: currentPincode, newpincode: newPincode, - retypepincode: retypePincode, + retypepincode: retypePincode }); setCurrentPincode(''); @@ -57,13 +58,10 @@ const PinCode = () => { const isButtonDisabled = isSubmitting || !currentPincode || !newPincode || !retypePincode; - const togglePassword = useCallback( - (event: MouseEvent, key: PasswordType) => { - event.preventDefault(); - setShowPassword((prev) => ({ ...prev, [key]: !prev[key] })); - }, - [] - ); + const togglePassword = useCallback((event: MouseEvent, key: PasswordType) => { + event.preventDefault(); + setShowPassword((prev) => ({ ...prev, [key]: !prev[key] })); + }, []); const handleRetypeChange = (value: string) => { const numericValue = handleNumericInput(value); @@ -76,105 +74,146 @@ const PinCode = () => { }; return ( -
-
-

Pin Code

+
+
+

Change Pin Code

+

+ Update your 6-digit pin code for additional security +

-
+ +
{/* Current Pin Code */} -
- -
+
+ +
setCurrentPincode(handleNumericInput(e.target.value))} disabled={isSubmitting} - type={showPassword.current ? 'text' : 'password'} + maxLength={6} />
{/* New Pin Code */} -
- -
+
+ +
{ const value = handleNumericInput(e.target.value); setNewPincode(value); if (retypePincode) { - setErrorRetype(value !== retypePincode ? 'New Pin Code and Retype Pin Code do not match.' : ''); + setErrorRetype( + value !== retypePincode ? 'New Pin Code and Retype Pin Code do not match.' : '' + ); } }} disabled={isSubmitting} - type={showPassword.new ? 'text' : 'password'} + maxLength={6} />
+

Enter 6 digits only

{/* Retype Pin Code */} -
- -
+
+ +
handleRetypeChange(e.target.value)} disabled={isSubmitting} - type={showPassword.retype ? 'text' : 'password'} + maxLength={6} />
+ + {/* Error message */} + {errorRetype && ( +

+ + {errorRetype} +

+ )}
- {/* Error message under Retype */} - {errorRetype && ( -
{errorRetype}
- )} - - {/* Submit button */} -
+ {/* Submit Button */} +
diff --git a/src/pages/menu/manage-menu/hooks/ManageMenusContext.tsx b/src/pages/menu/manage-menu/hooks/ManageMenusContext.tsx index b9bf885..58200ad 100644 --- a/src/pages/menu/manage-menu/hooks/ManageMenusContext.tsx +++ b/src/pages/menu/manage-menu/hooks/ManageMenusContext.tsx @@ -188,44 +188,50 @@ const ManageMenusContextProvider = ({ children }: { children: React.ReactNode }) [handleEditDialog, handleDeleteDialog] ); + // Fungsi helper untuk mengumpulkan semua parent nodes + const collectParents = (data: any[]) => { + const parentsList: any[] = []; + + const traverse = (items: any[]) => { + items.forEach((item) => { + // Jika item memiliki children, maka dia adalah parent + if (item.children && item.children.length > 0) { + if (!parentsList.find((p) => p.id === item.id)) { + parentsList.push({ id: item.id, name: item.name }); + } + // Rekursi untuk children + traverse(item.children); + } + }); + }; + + traverse(data); + return parentsList; + }; + const flattenChildren = (parent: any, parentIdx: number, depth = 0, parentName = '') => { let result: any[] = []; - if (parent.id_parent === null) { - if (!parents.find((el: any) => el.id === parent.id)) - setParents((el: any) => [...el, { id: parent.id, name: parent.name }]); - - result.push({ - id: parent.id, - module: parent.module, - parentName: parentName || parent.name, - name: parent.name, - link: parent.link, - id_parent: parent.id_parent, - status: parent.status, - order_number: parent.order_number - }); - } + // Selalu tambahkan item saat ini ke result (baik parent maupun child) + result.push({ + id: parent.id, + module: parent.module, + parentName: parentName, + name: parent.name, + link: parent.link, + id_parent: parent.id_parent, + status: parent.status, + order_number: parent.order_number + }); + // Jika tidak ada children, return result if (!parent.children || parent.children.length === 0) { return result; } + // Proses children secara rekursif const childrenFlattened = parent.children.flatMap((child: any, childIdx: number) => { - if (child.children && child.children.length > 0) { - return flattenChildren(child, parentIdx * 100 + childIdx, depth + 1, child.name); - } - - return { - id: child.id, - module: parent.module, - parentName: parentName || parent.name, - name: child.name, - link: child.link, - id_parent: child.id_parent, - status: child.status, - order_number: parent.order_number - }; + return flattenChildren(child, parentIdx * 100 + childIdx, depth + 1, parent.name); }); return [...result, ...childrenFlattened]; @@ -237,7 +243,7 @@ const ManageMenusContextProvider = ({ children }: { children: React.ReactNode }) filter = filter.length === 0 ? {} : { name: { like: `%${filter[0].value?.toLowerCase()}%` } }; const query: any = { - limit:100, + limit: 100, page: 1, with_deleted: false, order_field: sorting[0].id, @@ -254,15 +260,22 @@ const ManageMenusContextProvider = ({ children }: { children: React.ReactNode }) if (query.filter && query.filter.length > 0) { return { data: response?.data.list, totalCount: response?.data.total_count }; } else { + // Reset parents array + setParents([]); + + // Kumpulkan semua parents terlebih dahulu + const allParents = collectParents(response?.data.list || []); + setParents(allParents); + + // Transform data dengan flatten const transformedData = response?.data.list.flatMap((row: any, parentIdx: number) => flattenChildren(row, parentIdx) ); const total_count = transformedData.length; - // **Pagination di frontend saja (tanpa hit API ulang)** + // Pagination di frontend const paginatedData = transformedData.slice(page * limit, (page + 1) * limit); - const totalPages = Math.ceil(total_count / limit); return { data: paginatedData, totalCount: total_count }; }