feat: add password requirements validation and indicators in ChangePasswordModal and DetailUser components
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Button, Col, Row, Space, Input, ConfigProvider, Card } from 'antd';
|
import { Button, Col, Row, Space, Input, ConfigProvider, Card } from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
@@ -21,6 +21,11 @@ const ListPlantSection = ({
|
|||||||
const [formDataFilter, setFormDataFilter] = useState({ criteria: '' });
|
const [formDataFilter, setFormDataFilter] = useState({ criteria: '' });
|
||||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||||
|
|
||||||
|
// Sync refreshList from parent to trigger table refresh
|
||||||
|
useEffect(() => {
|
||||||
|
setTrigerFilter((prev) => !prev);
|
||||||
|
}, [refreshList]);
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: 'No',
|
title: 'No',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Modal, Input, Typography, Button, ConfigProvider } from 'antd';
|
import { Modal, Input, Typography, Button, ConfigProvider } from 'antd';
|
||||||
|
import { CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons';
|
||||||
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
|
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
|
||||||
import { changePassword } from '../../../api/user';
|
import { changePassword } from '../../../api/user';
|
||||||
|
|
||||||
@@ -13,6 +14,15 @@ const ChangePasswordModal = (props) => {
|
|||||||
});
|
});
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
|
// Password requirements state
|
||||||
|
const [passwordRequirements, setPasswordRequirements] = useState({
|
||||||
|
minLength: false,
|
||||||
|
hasUppercase: false,
|
||||||
|
hasLowercase: false,
|
||||||
|
hasNumber: false,
|
||||||
|
hasSpecialChar: false,
|
||||||
|
});
|
||||||
|
|
||||||
const validatePassword = (password) => {
|
const validatePassword = (password) => {
|
||||||
if (!password) return 'Password wajib diisi';
|
if (!password) return 'Password wajib diisi';
|
||||||
|
|
||||||
@@ -70,6 +80,24 @@ const ChangePasswordModal = (props) => {
|
|||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
});
|
});
|
||||||
setErrors({});
|
setErrors({});
|
||||||
|
setPasswordRequirements({
|
||||||
|
minLength: false,
|
||||||
|
hasUppercase: false,
|
||||||
|
hasLowercase: false,
|
||||||
|
hasNumber: false,
|
||||||
|
hasSpecialChar: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check password requirements
|
||||||
|
const checkPasswordRequirements = (password) => {
|
||||||
|
setPasswordRequirements({
|
||||||
|
minLength: password.length >= 8,
|
||||||
|
hasUppercase: /[A-Z]/.test(password),
|
||||||
|
hasLowercase: /[a-z]/.test(password),
|
||||||
|
hasNumber: /\d/.test(password),
|
||||||
|
hasSpecialChar: /[!@#$%^&*(),.?":{}|<>]/.test(password),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
@@ -122,6 +150,12 @@ const ChangePasswordModal = (props) => {
|
|||||||
...formData,
|
...formData,
|
||||||
[name]: value,
|
[name]: value,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check password requirements on password change
|
||||||
|
if (name === 'newPassword') {
|
||||||
|
checkPasswordRequirements(value);
|
||||||
|
}
|
||||||
|
|
||||||
// Clear error for this field
|
// Clear error for this field
|
||||||
if (errors[name]) {
|
if (errors[name]) {
|
||||||
setErrors({
|
setErrors({
|
||||||
@@ -211,6 +245,63 @@ const ChangePasswordModal = (props) => {
|
|||||||
{errors.newPassword && (
|
{errors.newPassword && (
|
||||||
<Text style={{ color: 'red', fontSize: '12px' }}>{errors.newPassword}</Text>
|
<Text style={{ color: 'red', fontSize: '12px' }}>{errors.newPassword}</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Password Requirements Indicator */}
|
||||||
|
<div style={{ marginTop: '8px', padding: '8px', backgroundColor: '#f5f5f5', borderRadius: '4px' }}>
|
||||||
|
<Text style={{ fontSize: '12px', fontWeight: '500', color: '#666' }}>Password harus memenuhi:</Text>
|
||||||
|
<div style={{ marginTop: '4px' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '2px' }}>
|
||||||
|
{passwordRequirements.minLength ? (
|
||||||
|
<CheckCircleFilled style={{ color: '#52c41a', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
) : (
|
||||||
|
<CloseCircleFilled style={{ color: '#ff4d4f', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
)}
|
||||||
|
<Text style={{ fontSize: '12px', color: passwordRequirements.minLength ? '#52c41a' : '#ff4d4f' }}>
|
||||||
|
Minimal 8 karakter
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '2px' }}>
|
||||||
|
{passwordRequirements.hasUppercase ? (
|
||||||
|
<CheckCircleFilled style={{ color: '#52c41a', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
) : (
|
||||||
|
<CloseCircleFilled style={{ color: '#ff4d4f', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
)}
|
||||||
|
<Text style={{ fontSize: '12px', color: passwordRequirements.hasUppercase ? '#52c41a' : '#ff4d4f' }}>
|
||||||
|
Minimal 1 huruf besar (A-Z)
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '2px' }}>
|
||||||
|
{passwordRequirements.hasLowercase ? (
|
||||||
|
<CheckCircleFilled style={{ color: '#52c41a', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
) : (
|
||||||
|
<CloseCircleFilled style={{ color: '#ff4d4f', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
)}
|
||||||
|
<Text style={{ fontSize: '12px', color: passwordRequirements.hasLowercase ? '#52c41a' : '#ff4d4f' }}>
|
||||||
|
Minimal 1 huruf kecil (a-z)
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '2px' }}>
|
||||||
|
{passwordRequirements.hasNumber ? (
|
||||||
|
<CheckCircleFilled style={{ color: '#52c41a', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
) : (
|
||||||
|
<CloseCircleFilled style={{ color: '#ff4d4f', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
)}
|
||||||
|
<Text style={{ fontSize: '12px', color: passwordRequirements.hasNumber ? '#52c41a' : '#ff4d4f' }}>
|
||||||
|
Minimal 1 angka (0-9)
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '2px' }}>
|
||||||
|
{passwordRequirements.hasSpecialChar ? (
|
||||||
|
<CheckCircleFilled style={{ color: '#52c41a', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
) : (
|
||||||
|
<CloseCircleFilled style={{ color: '#ff4d4f', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
)}
|
||||||
|
<Text style={{ fontSize: '12px', color: passwordRequirements.hasSpecialChar ? '#52c41a' : '#ff4d4f' }}>
|
||||||
|
Minimal 1 karakter spesial (!@#$%^&*(),.?":{}|<>)
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
@@ -229,27 +320,6 @@ const ChangePasswordModal = (props) => {
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider, Select } from 'antd';
|
import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider, Select } from 'antd';
|
||||||
|
import { CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons';
|
||||||
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
|
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
|
||||||
import { createUser, updateUser } from '../../../api/user';
|
import { createUser, updateUser } from '../../../api/user';
|
||||||
import { getAllRole } from '../../../api/role';
|
import { getAllRole } from '../../../api/role';
|
||||||
@@ -25,13 +26,41 @@ const DetailUser = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [FormData, setFormData] = useState(defaultData);
|
const [FormData, setFormData] = useState(defaultData);
|
||||||
|
const [originalEmail, setOriginalEmail] = useState(''); // Track original email
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
|
// Password requirements state
|
||||||
|
const [passwordRequirements, setPasswordRequirements] = useState({
|
||||||
|
minLength: false,
|
||||||
|
hasUppercase: false,
|
||||||
|
hasLowercase: false,
|
||||||
|
hasNumber: false,
|
||||||
|
hasSpecialChar: false,
|
||||||
|
});
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
props.setSelectedData(null);
|
props.setSelectedData(null);
|
||||||
props.setActionMode('list');
|
props.setActionMode('list');
|
||||||
setFormData(defaultData);
|
setFormData(defaultData);
|
||||||
setErrors({});
|
setErrors({});
|
||||||
|
setPasswordRequirements({
|
||||||
|
minLength: false,
|
||||||
|
hasUppercase: false,
|
||||||
|
hasLowercase: false,
|
||||||
|
hasNumber: false,
|
||||||
|
hasSpecialChar: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check password requirements
|
||||||
|
const checkPasswordRequirements = (password) => {
|
||||||
|
setPasswordRequirements({
|
||||||
|
minLength: password.length >= 8,
|
||||||
|
hasUppercase: /[A-Z]/.test(password),
|
||||||
|
hasLowercase: /[a-z]/.test(password),
|
||||||
|
hasNumber: /\d/.test(password),
|
||||||
|
hasSpecialChar: /[!@#$%^&*(),.?":{}|<>]/.test(password),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const validatePhone = (phone) => {
|
const validatePhone = (phone) => {
|
||||||
@@ -145,10 +174,20 @@ const DetailUser = (props) => {
|
|||||||
// Backend expects field names with 'user_' prefix
|
// Backend expects field names with 'user_' prefix
|
||||||
const payload = {
|
const payload = {
|
||||||
user_fullname: FormData.user_fullname,
|
user_fullname: FormData.user_fullname,
|
||||||
user_email: FormData.user_email,
|
|
||||||
user_phone: phone,
|
user_phone: phone,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// For update mode: only send email if it has changed
|
||||||
|
if (FormData.user_id) {
|
||||||
|
// Only include email if it has changed from original
|
||||||
|
if (FormData.user_email !== originalEmail) {
|
||||||
|
payload.user_email = FormData.user_email;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For create mode: always send email
|
||||||
|
payload.user_email = FormData.user_email;
|
||||||
|
}
|
||||||
|
|
||||||
// Only add role_id if it exists (backend requires number >= 1, no null)
|
// Only add role_id if it exists (backend requires number >= 1, no null)
|
||||||
if (FormData.role_id) {
|
if (FormData.role_id) {
|
||||||
payload.role_id = FormData.role_id;
|
payload.role_id = FormData.role_id;
|
||||||
@@ -163,6 +202,7 @@ const DetailUser = (props) => {
|
|||||||
// For update mode:
|
// For update mode:
|
||||||
// - Don't send 'user_name' (username is immutable)
|
// - Don't send 'user_name' (username is immutable)
|
||||||
// - Don't send 'is_active' (backend validation schema doesn't allow it)
|
// - Don't send 'is_active' (backend validation schema doesn't allow it)
|
||||||
|
// - Only send email if it has changed
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Payload being sent:', payload);
|
console.log('Payload being sent:', payload);
|
||||||
@@ -214,6 +254,12 @@ const DetailUser = (props) => {
|
|||||||
...FormData,
|
...FormData,
|
||||||
[name]: value,
|
[name]: value,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check password requirements on password change
|
||||||
|
if (name === 'password') {
|
||||||
|
checkPasswordRequirements(value);
|
||||||
|
}
|
||||||
|
|
||||||
// Clear error for this field
|
// Clear error for this field
|
||||||
if (errors[name]) {
|
if (errors[name]) {
|
||||||
setErrors({
|
setErrors({
|
||||||
@@ -288,8 +334,11 @@ const DetailUser = (props) => {
|
|||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
});
|
});
|
||||||
|
// Store original email for comparison
|
||||||
|
setOriginalEmail(props.selectedData.user_email || '');
|
||||||
} else {
|
} else {
|
||||||
setFormData(defaultData);
|
setFormData(defaultData);
|
||||||
|
setOriginalEmail('');
|
||||||
}
|
}
|
||||||
setErrors({});
|
setErrors({});
|
||||||
|
|
||||||
@@ -451,6 +500,63 @@ const DetailUser = (props) => {
|
|||||||
{errors.password}
|
{errors.password}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Password Requirements Indicator */}
|
||||||
|
<div style={{ marginTop: '8px', padding: '8px', backgroundColor: '#f5f5f5', borderRadius: '4px' }}>
|
||||||
|
<Text style={{ fontSize: '12px', fontWeight: '500', color: '#666' }}>Password harus memenuhi:</Text>
|
||||||
|
<div style={{ marginTop: '4px' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '2px' }}>
|
||||||
|
{passwordRequirements.minLength ? (
|
||||||
|
<CheckCircleFilled style={{ color: '#52c41a', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
) : (
|
||||||
|
<CloseCircleFilled style={{ color: '#ff4d4f', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
)}
|
||||||
|
<Text style={{ fontSize: '12px', color: passwordRequirements.minLength ? '#52c41a' : '#ff4d4f' }}>
|
||||||
|
Minimal 8 karakter
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '2px' }}>
|
||||||
|
{passwordRequirements.hasUppercase ? (
|
||||||
|
<CheckCircleFilled style={{ color: '#52c41a', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
) : (
|
||||||
|
<CloseCircleFilled style={{ color: '#ff4d4f', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
)}
|
||||||
|
<Text style={{ fontSize: '12px', color: passwordRequirements.hasUppercase ? '#52c41a' : '#ff4d4f' }}>
|
||||||
|
Minimal 1 huruf besar (A-Z)
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '2px' }}>
|
||||||
|
{passwordRequirements.hasLowercase ? (
|
||||||
|
<CheckCircleFilled style={{ color: '#52c41a', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
) : (
|
||||||
|
<CloseCircleFilled style={{ color: '#ff4d4f', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
)}
|
||||||
|
<Text style={{ fontSize: '12px', color: passwordRequirements.hasLowercase ? '#52c41a' : '#ff4d4f' }}>
|
||||||
|
Minimal 1 huruf kecil (a-z)
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '2px' }}>
|
||||||
|
{passwordRequirements.hasNumber ? (
|
||||||
|
<CheckCircleFilled style={{ color: '#52c41a', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
) : (
|
||||||
|
<CloseCircleFilled style={{ color: '#ff4d4f', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
)}
|
||||||
|
<Text style={{ fontSize: '12px', color: passwordRequirements.hasNumber ? '#52c41a' : '#ff4d4f' }}>
|
||||||
|
Minimal 1 angka (0-9)
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '2px' }}>
|
||||||
|
{passwordRequirements.hasSpecialChar ? (
|
||||||
|
<CheckCircleFilled style={{ color: '#52c41a', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
) : (
|
||||||
|
<CloseCircleFilled style={{ color: '#ff4d4f', fontSize: '14px', marginRight: '6px' }} />
|
||||||
|
)}
|
||||||
|
<Text style={{ fontSize: '12px', color: passwordRequirements.hasSpecialChar ? '#52c41a' : '#ff4d4f' }}>
|
||||||
|
Minimal 1 karakter spesial (!@#$%^&*(),.?":{}|<>)
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
|||||||
@@ -50,7 +50,13 @@ const getRoleColor = (role_name, role_level) => {
|
|||||||
return 'default';
|
return 'default';
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = (showPreviewModal, showEditModal, showDeleteDialog, showApproveDialog, showChangePasswordModal) => [
|
const columns = (
|
||||||
|
showPreviewModal,
|
||||||
|
showEditModal,
|
||||||
|
showDeleteDialog,
|
||||||
|
showApproveDialog,
|
||||||
|
showChangePasswordModal
|
||||||
|
) => [
|
||||||
{
|
{
|
||||||
title: 'ID',
|
title: 'ID',
|
||||||
dataIndex: 'user_id',
|
dataIndex: 'user_id',
|
||||||
@@ -257,7 +263,7 @@ const ListUser = memo(function ListUser(props) {
|
|||||||
icon: 'question',
|
icon: 'question',
|
||||||
title: 'Konfirmasi',
|
title: 'Konfirmasi',
|
||||||
message: 'Apakah anda yakin hapus user "' + param.user_fullname + '" ?',
|
message: 'Apakah anda yakin hapus user "' + param.user_fullname + '" ?',
|
||||||
onConfirm: () => handleDelete(param.user_id),
|
onConfirm: () => handleDelete(param.user_id, param.user_fullname),
|
||||||
onCancel: () => props.setSelectedData(null),
|
onCancel: () => props.setSelectedData(null),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -285,14 +291,14 @@ const ListUser = memo(function ListUser(props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (user_id) => {
|
const handleDelete = async (user_id, user_fullname) => {
|
||||||
const response = await deleteUser(user_id);
|
const response = await deleteUser(user_id);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Berhasil',
|
title: 'Berhasil',
|
||||||
message: 'User "' + response.data.user_fullname + '" berhasil dihapus.',
|
message: 'User "' + user_fullname + '" berhasil dihapus.',
|
||||||
});
|
});
|
||||||
doFilter();
|
doFilter();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user