diff --git a/src/pages/master/plantSection/component/ListPlantSection.jsx b/src/pages/master/plantSection/component/ListPlantSection.jsx index a6a812c..e8c14f8 100644 --- a/src/pages/master/plantSection/component/ListPlantSection.jsx +++ b/src/pages/master/plantSection/component/ListPlantSection.jsx @@ -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 { PlusOutlined, @@ -21,6 +21,11 @@ const ListPlantSection = ({ const [formDataFilter, setFormDataFilter] = useState({ criteria: '' }); const [trigerFilter, setTrigerFilter] = useState(false); + // Sync refreshList from parent to trigger table refresh + useEffect(() => { + setTrigerFilter((prev) => !prev); + }, [refreshList]); + const columns = [ { title: 'No', diff --git a/src/pages/user/component/ChangePasswordModal.jsx b/src/pages/user/component/ChangePasswordModal.jsx index 5e83330..826359b 100644 --- a/src/pages/user/component/ChangePasswordModal.jsx +++ b/src/pages/user/component/ChangePasswordModal.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import { Modal, Input, Typography, Button, ConfigProvider } from 'antd'; +import { CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons'; import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif'; import { changePassword } from '../../../api/user'; @@ -13,6 +14,15 @@ const ChangePasswordModal = (props) => { }); const [errors, setErrors] = useState({}); + // Password requirements state + const [passwordRequirements, setPasswordRequirements] = useState({ + minLength: false, + hasUppercase: false, + hasLowercase: false, + hasNumber: false, + hasSpecialChar: false, + }); + const validatePassword = (password) => { if (!password) return 'Password wajib diisi'; @@ -70,6 +80,24 @@ const ChangePasswordModal = (props) => { confirmPassword: '', }); 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 () => { @@ -122,6 +150,12 @@ const ChangePasswordModal = (props) => { ...formData, [name]: value, }); + + // Check password requirements on password change + if (name === 'newPassword') { + checkPasswordRequirements(value); + } + // Clear error for this field if (errors[name]) { setErrors({ @@ -211,6 +245,63 @@ const ChangePasswordModal = (props) => { {errors.newPassword && ( {errors.newPassword} )} + + {/* Password Requirements Indicator */} +
+ Password harus memenuhi: +
+
+ {passwordRequirements.minLength ? ( + + ) : ( + + )} + + Minimal 8 karakter + +
+
+ {passwordRequirements.hasUppercase ? ( + + ) : ( + + )} + + Minimal 1 huruf besar (A-Z) + +
+
+ {passwordRequirements.hasLowercase ? ( + + ) : ( + + )} + + Minimal 1 huruf kecil (a-z) + +
+
+ {passwordRequirements.hasNumber ? ( + + ) : ( + + )} + + Minimal 1 angka (0-9) + +
+
+ {passwordRequirements.hasSpecialChar ? ( + + ) : ( + + )} + + Minimal 1 karakter spesial (!@#$%^&*(),.?":{}|<>) + +
+
+
@@ -229,27 +320,6 @@ const ChangePasswordModal = (props) => { )}
- -
- - Persyaratan password: - - -
); diff --git a/src/pages/user/component/DetailUser.jsx b/src/pages/user/component/DetailUser.jsx index d251b5d..9403a99 100644 --- a/src/pages/user/component/DetailUser.jsx +++ b/src/pages/user/component/DetailUser.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; 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 { createUser, updateUser } from '../../../api/user'; import { getAllRole } from '../../../api/role'; @@ -25,13 +26,41 @@ const DetailUser = (props) => { }; const [FormData, setFormData] = useState(defaultData); + const [originalEmail, setOriginalEmail] = useState(''); // Track original email const [errors, setErrors] = useState({}); + // Password requirements state + const [passwordRequirements, setPasswordRequirements] = useState({ + minLength: false, + hasUppercase: false, + hasLowercase: false, + hasNumber: false, + hasSpecialChar: false, + }); + const handleCancel = () => { props.setSelectedData(null); props.setActionMode('list'); setFormData(defaultData); 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) => { @@ -145,10 +174,20 @@ const DetailUser = (props) => { // Backend expects field names with 'user_' prefix const payload = { user_fullname: FormData.user_fullname, - user_email: FormData.user_email, 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) if (FormData.role_id) { payload.role_id = FormData.role_id; @@ -163,6 +202,7 @@ const DetailUser = (props) => { // For update mode: // - Don't send 'user_name' (username is immutable) // - Don't send 'is_active' (backend validation schema doesn't allow it) + // - Only send email if it has changed try { console.log('Payload being sent:', payload); @@ -214,6 +254,12 @@ const DetailUser = (props) => { ...FormData, [name]: value, }); + + // Check password requirements on password change + if (name === 'password') { + checkPasswordRequirements(value); + } + // Clear error for this field if (errors[name]) { setErrors({ @@ -288,8 +334,11 @@ const DetailUser = (props) => { password: '', confirmPassword: '', }); + // Store original email for comparison + setOriginalEmail(props.selectedData.user_email || ''); } else { setFormData(defaultData); + setOriginalEmail(''); } setErrors({}); @@ -451,6 +500,63 @@ const DetailUser = (props) => { {errors.password} )} + + {/* Password Requirements Indicator */} +
+ Password harus memenuhi: +
+
+ {passwordRequirements.minLength ? ( + + ) : ( + + )} + + Minimal 8 karakter + +
+
+ {passwordRequirements.hasUppercase ? ( + + ) : ( + + )} + + Minimal 1 huruf besar (A-Z) + +
+
+ {passwordRequirements.hasLowercase ? ( + + ) : ( + + )} + + Minimal 1 huruf kecil (a-z) + +
+
+ {passwordRequirements.hasNumber ? ( + + ) : ( + + )} + + Minimal 1 angka (0-9) + +
+
+ {passwordRequirements.hasSpecialChar ? ( + + ) : ( + + )} + + Minimal 1 karakter spesial (!@#$%^&*(),.?":{}|<>) + +
+
+
diff --git a/src/pages/user/component/ListUser.jsx b/src/pages/user/component/ListUser.jsx index a8cab68..e160d08 100644 --- a/src/pages/user/component/ListUser.jsx +++ b/src/pages/user/component/ListUser.jsx @@ -50,7 +50,13 @@ const getRoleColor = (role_name, role_level) => { return 'default'; }; -const columns = (showPreviewModal, showEditModal, showDeleteDialog, showApproveDialog, showChangePasswordModal) => [ +const columns = ( + showPreviewModal, + showEditModal, + showDeleteDialog, + showApproveDialog, + showChangePasswordModal +) => [ { title: 'ID', dataIndex: 'user_id', @@ -257,7 +263,7 @@ const ListUser = memo(function ListUser(props) { icon: 'question', title: 'Konfirmasi', 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), }); }; @@ -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); if (response.statusCode == 200) { NotifAlert({ icon: 'success', title: 'Berhasil', - message: 'User "' + response.data.user_fullname + '" berhasil dihapus.', + message: 'User "' + user_fullname + '" berhasil dihapus.', }); doFilter(); } else {