feat: add password requirements validation and indicators in ChangePasswordModal and DetailUser components

This commit is contained in:
2025-10-14 15:42:28 +07:00
parent eb90d89e0e
commit 9f6cb66c37
4 changed files with 214 additions and 27 deletions

View File

@@ -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',

View File

@@ -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 (!@#$%^&*(),.?":&#123;&#125;|&lt;&gt;)
</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>
); );

View File

@@ -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 (!@#$%^&*(),.?":&#123;&#125;|&lt;&gt;)
</Text>
</div>
</div>
</div>
</div> </div>
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>

View File

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