Compare commits
3 Commits
7a8a46ee64
...
d7a09840b9
| Author | SHA1 | Date | |
|---|---|---|---|
| d7a09840b9 | |||
| e00ecbf116 | |||
| 2817f3c31c |
@@ -1,6 +1,7 @@
|
|||||||
import { SendRequest } from '../components/Global/ApiRequest';
|
import { SendRequest } from '../components/Global/ApiRequest';
|
||||||
|
|
||||||
const getAllDevice = async (queryParams) => {
|
const getAllDevice = async (queryParams) => {
|
||||||
|
try {
|
||||||
const response = await SendRequest({
|
const response = await SendRequest({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
prefix: `device?${queryParams.toString()}`,
|
prefix: `device?${queryParams.toString()}`,
|
||||||
@@ -8,23 +9,50 @@ const getAllDevice = async (queryParams) => {
|
|||||||
console.log('getAllDevice response:', response);
|
console.log('getAllDevice response:', response);
|
||||||
console.log('Query params:', queryParams.toString());
|
console.log('Query params:', queryParams.toString());
|
||||||
|
|
||||||
// Parse query params to get page and limit
|
// Backend response structure:
|
||||||
|
// {
|
||||||
|
// statusCode: 200,
|
||||||
|
// data: [...devices],
|
||||||
|
// paging: {
|
||||||
|
// current_page: 1,
|
||||||
|
// current_limit: 10,
|
||||||
|
// total_limit: 50,
|
||||||
|
// total_page: 5
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Check if backend returns paginated data
|
||||||
|
if (response.paging) {
|
||||||
|
const totalData = response.data?.[0]?.total_data || response.rows || response.data?.length || 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.statusCode || 200,
|
||||||
|
data: {
|
||||||
|
data: response.data || [],
|
||||||
|
paging: {
|
||||||
|
page: response.paging.current_page || 1,
|
||||||
|
limit: response.paging.current_limit || 10,
|
||||||
|
total: totalData,
|
||||||
|
page_total: response.paging.total_page || Math.ceil(totalData / (response.paging.current_limit || 10))
|
||||||
|
},
|
||||||
|
total: totalData
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: If backend returns all data without pagination (old behavior)
|
||||||
const params = Object.fromEntries(queryParams);
|
const params = Object.fromEntries(queryParams);
|
||||||
const currentPage = parseInt(params.page) || 1;
|
const currentPage = parseInt(params.page) || 1;
|
||||||
const currentLimit = parseInt(params.limit) || 10;
|
const currentLimit = parseInt(params.limit) || 10;
|
||||||
|
|
||||||
// Backend returns all data, so we need to do client-side pagination
|
|
||||||
const allData = response.data || [];
|
const allData = response.data || [];
|
||||||
const totalData = allData.length;
|
const totalData = allData.length;
|
||||||
|
|
||||||
// Calculate start and end index for current page
|
// Client-side pagination
|
||||||
const startIndex = (currentPage - 1) * currentLimit;
|
const startIndex = (currentPage - 1) * currentLimit;
|
||||||
const endIndex = startIndex + currentLimit;
|
const endIndex = startIndex + currentLimit;
|
||||||
|
|
||||||
// Slice data for current page
|
|
||||||
const paginatedData = allData.slice(startIndex, endIndex);
|
const paginatedData = allData.slice(startIndex, endIndex);
|
||||||
|
|
||||||
// Transform response to match TableList expected structure
|
|
||||||
return {
|
return {
|
||||||
status: response.statusCode || 200,
|
status: response.statusCode || 200,
|
||||||
data: {
|
data: {
|
||||||
@@ -38,6 +66,23 @@ const getAllDevice = async (queryParams) => {
|
|||||||
total: totalData
|
total: totalData
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getAllDevice error:', error);
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
data: {
|
||||||
|
data: [],
|
||||||
|
paging: {
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
total: 0,
|
||||||
|
page_total: 0
|
||||||
|
},
|
||||||
|
total: 0
|
||||||
|
},
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDeviceById = async (id) => {
|
const getDeviceById = async (id) => {
|
||||||
|
|||||||
@@ -1,31 +1,66 @@
|
|||||||
import { SendRequest } from '../components/Global/ApiRequest';
|
import { SendRequest } from '../components/Global/ApiRequest';
|
||||||
|
|
||||||
const getAllUser = async (queryParams) => {
|
const getAllUser = async (queryParams) => {
|
||||||
|
try {
|
||||||
|
console.log('getAllUser queryParams:', queryParams.toString());
|
||||||
|
|
||||||
const response = await SendRequest({
|
const response = await SendRequest({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
prefix: `user?${queryParams.toString()}`,
|
prefix: `user?${queryParams.toString()}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Parse query params to get page and limit
|
console.log('getAllUser response:', response);
|
||||||
|
|
||||||
|
// Backend now handles pagination, just return the response
|
||||||
|
// Expected backend response structure:
|
||||||
|
// {
|
||||||
|
// statusCode: 200,
|
||||||
|
// data: [...users],
|
||||||
|
// paging: { page, limit, total, page_total }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Check if backend returns paginated data
|
||||||
|
if (response.paging) {
|
||||||
|
// Filter out super admin users (is_sa = true)
|
||||||
|
const allData = response.data || [];
|
||||||
|
const filteredData = allData.filter(user => user.is_sa !== true && user.is_sa !== 1);
|
||||||
|
|
||||||
|
// Recalculate pagination info after filtering
|
||||||
|
const totalAfterFilter = filteredData.length;
|
||||||
|
const currentPage = response.paging.page || 1;
|
||||||
|
const currentLimit = response.paging.limit || 10;
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.statusCode || 200,
|
||||||
|
data: {
|
||||||
|
data: filteredData,
|
||||||
|
paging: {
|
||||||
|
page: currentPage,
|
||||||
|
limit: currentLimit,
|
||||||
|
total: totalAfterFilter,
|
||||||
|
page_total: Math.ceil(totalAfterFilter / currentLimit)
|
||||||
|
},
|
||||||
|
total: totalAfterFilter
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: If backend returns all data without pagination (old behavior)
|
||||||
const params = Object.fromEntries(queryParams);
|
const params = Object.fromEntries(queryParams);
|
||||||
const currentPage = parseInt(params.page) || 1;
|
const currentPage = parseInt(params.page) || 1;
|
||||||
const currentLimit = parseInt(params.limit) || 10;
|
const currentLimit = parseInt(params.limit) || 10;
|
||||||
|
|
||||||
// Backend returns all data, so we need to do client-side pagination
|
|
||||||
const allData = response.data || [];
|
const allData = response.data || [];
|
||||||
|
|
||||||
// Filter out users with is_sa = true
|
// Filter out users with is_sa = true or 1 (client-side filtering)
|
||||||
const filteredData = allData.filter(user => user.is_sa !== true);
|
const filteredData = allData.filter(user => user.is_sa !== true && user.is_sa !== 1);
|
||||||
const totalData = filteredData.length;
|
const totalData = filteredData.length;
|
||||||
|
|
||||||
// Calculate start and end index for current page
|
// Client-side pagination
|
||||||
const startIndex = (currentPage - 1) * currentLimit;
|
const startIndex = (currentPage - 1) * currentLimit;
|
||||||
const endIndex = startIndex + currentLimit;
|
const endIndex = startIndex + currentLimit;
|
||||||
|
|
||||||
// Slice data for current page
|
|
||||||
const paginatedData = filteredData.slice(startIndex, endIndex);
|
const paginatedData = filteredData.slice(startIndex, endIndex);
|
||||||
|
|
||||||
// Transform response to match TableList expected structure
|
|
||||||
return {
|
return {
|
||||||
status: response.statusCode || 200,
|
status: response.statusCode || 200,
|
||||||
data: {
|
data: {
|
||||||
@@ -39,6 +74,24 @@ const getAllUser = async (queryParams) => {
|
|||||||
total: totalData
|
total: totalData
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getAllUser error:', error);
|
||||||
|
// Return empty data on error to prevent app crash
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
data: {
|
||||||
|
data: [],
|
||||||
|
paging: {
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
total: 0,
|
||||||
|
page_total: 0
|
||||||
|
},
|
||||||
|
total: 0
|
||||||
|
},
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserById = async (id) => {
|
const getUserById = async (id) => {
|
||||||
@@ -103,4 +156,23 @@ const approveUser = async (user_id) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export { getAllUser, getUserById, createUser, updateUser, deleteUser, approveUser };
|
const changePassword = async (user_id, new_password) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'put',
|
||||||
|
prefix: `user/change-password/${user_id}`,
|
||||||
|
params: {
|
||||||
|
new_password: new_password
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Change Password Response:', response);
|
||||||
|
|
||||||
|
// Return full response with statusCode
|
||||||
|
return {
|
||||||
|
statusCode: response.statusCode || 200,
|
||||||
|
data: response.data,
|
||||||
|
message: response.message || 'Password berhasil diubah'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getAllUser, getUserById, createUser, updateUser, deleteUser, approveUser, changePassword };
|
||||||
@@ -2,6 +2,7 @@ import React, { memo, useState, useEffect } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import ListUser from './component/ListUser';
|
import ListUser from './component/ListUser';
|
||||||
import DetailUser from './component/DetailUser';
|
import DetailUser from './component/DetailUser';
|
||||||
|
import ChangePasswordModal from './component/ChangePasswordModal';
|
||||||
import { useBreadcrumb } from '../../layout/LayoutBreadcrumb';
|
import { useBreadcrumb } from '../../layout/LayoutBreadcrumb';
|
||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ const IndexUser = memo(function IndexUser() {
|
|||||||
const [selectedData, setSelectedData] = useState(null);
|
const [selectedData, setSelectedData] = useState(null);
|
||||||
const [readOnly, setReadOnly] = useState(false);
|
const [readOnly, setReadOnly] = useState(false);
|
||||||
const [showModal, setShowmodal] = useState(false);
|
const [showModal, setShowmodal] = useState(false);
|
||||||
|
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
|
||||||
|
const [selectedUserForPassword, setSelectedUserForPassword] = useState(null);
|
||||||
|
|
||||||
const setMode = (param) => {
|
const setMode = (param) => {
|
||||||
setShowmodal(true);
|
setShowmodal(true);
|
||||||
@@ -63,6 +66,8 @@ const IndexUser = memo(function IndexUser() {
|
|||||||
selectedData={selectedData}
|
selectedData={selectedData}
|
||||||
setSelectedData={setSelectedData}
|
setSelectedData={setSelectedData}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
setShowChangePasswordModal={setShowChangePasswordModal}
|
||||||
|
setSelectedUserForPassword={setSelectedUserForPassword}
|
||||||
/>
|
/>
|
||||||
<DetailUser
|
<DetailUser
|
||||||
setActionMode={setMode}
|
setActionMode={setMode}
|
||||||
@@ -72,6 +77,12 @@ const IndexUser = memo(function IndexUser() {
|
|||||||
showModal={showModal}
|
showModal={showModal}
|
||||||
actionMode={actionMode}
|
actionMode={actionMode}
|
||||||
/>
|
/>
|
||||||
|
<ChangePasswordModal
|
||||||
|
showModal={showChangePasswordModal}
|
||||||
|
setShowModal={setShowChangePasswordModal}
|
||||||
|
selectedUser={selectedUserForPassword}
|
||||||
|
setSelectedUser={setSelectedUserForPassword}
|
||||||
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
258
src/pages/user/component/ChangePasswordModal.jsx
Normal file
258
src/pages/user/component/ChangePasswordModal.jsx
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Modal, Input, Typography, Button, ConfigProvider } from 'antd';
|
||||||
|
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
|
||||||
|
import { changePassword } from '../../../api/user';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const ChangePasswordModal = (props) => {
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
});
|
||||||
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
|
const validatePassword = (password) => {
|
||||||
|
if (!password) return 'Password wajib diisi';
|
||||||
|
|
||||||
|
// Must be at least 8 characters long
|
||||||
|
if (password.length < 8) {
|
||||||
|
return 'Password must be at least 8 characters long';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must contain at least one uppercase letter
|
||||||
|
if (!/[A-Z]/.test(password)) {
|
||||||
|
return 'Password must contain at least one uppercase letter';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must contain at least one lowercase letter
|
||||||
|
if (!/[a-z]/.test(password)) {
|
||||||
|
return 'Password must contain at least one lowercase letter';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must contain at least one number
|
||||||
|
if (!/\d/.test(password)) {
|
||||||
|
return 'Password must contain at least one number';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must contain at least one special character
|
||||||
|
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
||||||
|
return 'Password must contain at least one special character';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
const newErrors = {};
|
||||||
|
|
||||||
|
const passwordError = validatePassword(formData.newPassword);
|
||||||
|
if (passwordError) {
|
||||||
|
newErrors.newPassword = passwordError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.confirmPassword) {
|
||||||
|
newErrors.confirmPassword = 'Konfirmasi password wajib diisi';
|
||||||
|
} else if (formData.newPassword !== formData.confirmPassword) {
|
||||||
|
newErrors.confirmPassword = 'Password tidak cocok';
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
props.setShowModal(false);
|
||||||
|
props.setSelectedUser(null);
|
||||||
|
setFormData({
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
});
|
||||||
|
setErrors({});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!validateForm()) {
|
||||||
|
NotifOk({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Peringatan',
|
||||||
|
message: 'Mohon periksa kembali form Anda',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfirmLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await changePassword(props.selectedUser.user_id, formData.newPassword);
|
||||||
|
|
||||||
|
console.log('Change Password Response:', response);
|
||||||
|
|
||||||
|
if (response && response.statusCode === 200) {
|
||||||
|
NotifOk({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
message: `Password untuk user "${props.selectedUser.user_fullname}" berhasil diubah.`,
|
||||||
|
});
|
||||||
|
|
||||||
|
handleCancel();
|
||||||
|
} else {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: response?.message || 'Terjadi kesalahan saat mengubah password.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Change Password Error:', error);
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
message: error.message || 'Terjadi kesalahan pada server. Coba lagi nanti.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfirmLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[name]: value,
|
||||||
|
});
|
||||||
|
// Clear error for this field
|
||||||
|
if (errors[name]) {
|
||||||
|
setErrors({
|
||||||
|
...errors,
|
||||||
|
[name]: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!props.showModal) {
|
||||||
|
setFormData({
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
});
|
||||||
|
setErrors({});
|
||||||
|
}
|
||||||
|
}, [props.showModal]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={`Ubah Password - ${props.selectedUser?.user_fullname || ''}`}
|
||||||
|
open={props.showModal}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
footer={[
|
||||||
|
<React.Fragment key="modal-footer">
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: { colorBgContainer: '#E9F6EF' },
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: 'white',
|
||||||
|
defaultColor: '#23A55A',
|
||||||
|
defaultBorderColor: '#23A55A',
|
||||||
|
defaultHoverColor: '#23A55A',
|
||||||
|
defaultHoverBorderColor: '#23A55A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button onClick={handleCancel}>Batal</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: {
|
||||||
|
colorBgContainer: '#209652',
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: '#23a55a',
|
||||||
|
defaultColor: '#FFFFFF',
|
||||||
|
defaultBorderColor: '#23a55a',
|
||||||
|
defaultHoverColor: '#FFFFFF',
|
||||||
|
defaultHoverBorderColor: '#23a55a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button loading={confirmLoading} onClick={handleSave}>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
</React.Fragment>,
|
||||||
|
]}
|
||||||
|
width={500}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<Text strong>Username</Text>
|
||||||
|
<Input
|
||||||
|
value={props.selectedUser?.user_name || ''}
|
||||||
|
disabled
|
||||||
|
style={{ backgroundColor: '#f5f5f5' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<Text strong>Password Baru</Text>
|
||||||
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
|
<Input.Password
|
||||||
|
name="newPassword"
|
||||||
|
value={formData.newPassword}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Masukkan password baru"
|
||||||
|
status={errors.newPassword ? 'error' : ''}
|
||||||
|
/>
|
||||||
|
{errors.newPassword && (
|
||||||
|
<Text style={{ color: 'red', fontSize: '12px' }}>{errors.newPassword}</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<Text strong>Konfirmasi Password</Text>
|
||||||
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
|
<Input.Password
|
||||||
|
name="confirmPassword"
|
||||||
|
value={formData.confirmPassword}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Konfirmasi password baru"
|
||||||
|
status={errors.confirmPassword ? 'error' : ''}
|
||||||
|
/>
|
||||||
|
{errors.confirmPassword && (
|
||||||
|
<Text style={{ color: 'red', fontSize: '12px' }}>
|
||||||
|
{errors.confirmPassword}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: '#f0f8ff',
|
||||||
|
borderRadius: 4,
|
||||||
|
border: '1px solid #d6e4ff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: '12px', color: '#595959' }}>
|
||||||
|
<strong>Persyaratan password:</strong>
|
||||||
|
<ul style={{ marginTop: 8, marginBottom: 0, paddingLeft: 20 }}>
|
||||||
|
<li>Minimal 8 karakter</li>
|
||||||
|
<li>Minimal 1 huruf besar (A-Z)</li>
|
||||||
|
<li>Minimal 1 huruf kecil (a-z)</li>
|
||||||
|
<li>Minimal 1 angka (0-9)</li>
|
||||||
|
<li>Minimal 1 karakter spesial (!@#$%^&*)</li>
|
||||||
|
</ul>
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChangePasswordModal;
|
||||||
Reference in New Issue
Block a user