Compare commits

...

3 Commits

Author SHA1 Message Date
7a8a46ee64 fix: remove duplicate /roles in API endpoints
- Fix getAllRole endpoint from roles/roles to roles
- Fix getRoleById endpoint from roles/roles/:id to roles/:id
- Fix createRole endpoint from roles/roles to roles
- Fix updateRole endpoint from roles/roles/:id to roles/:id
- Fix deleteRole endpoint from roles/roles/:id to roles/:id
- Add try-catch error handling in getAllRole
- Support both server-side and client-side pagination

This fixes SQL error: Conversion failed when converting the nvarchar value 'roles' to data type int
Backend was interpreting duplicate /roles as an ID parameter
2025-10-10 15:45:39 +07:00
c3b5ec2121 feat: refactor role display with dynamic colors and capitalize
- Add helper function capitalizeFirstLetter for consistent formatting
- Add helper function getRoleColor to determine tag color by role_level or role_name
- Refactor role_name column render to use dynamic helpers
- Replace 30+ lines of hardcoded conditions with 8 lines of clean code
- Support any role name from database without code changes
- Display role names with first letter capitalized
- Color mapping: Level 1=purple, Level 2=blue, Level 3=cyan, Level 4=green
- Add KeyOutlined icon import for change password feature
2025-10-10 15:44:46 +07:00
59c90c3519 feat: integrate dynamic role selection in user form
- Add getAllRole API import to fetch roles dynamically
- Add roleList and loadingRoles state management
- Implement fetchRoles function to get all roles from API
- Update Select component to display roles dynamically from database
- Display role format: Role Name (Level X)
- Add loading indicator while fetching roles
- Fetch roles automatically when modal opens
2025-10-10 15:44:21 +07:00
3 changed files with 102 additions and 41 deletions

View File

@@ -1,49 +1,76 @@
import { SendRequest } from '../components/Global/ApiRequest'; import { SendRequest } from '../components/Global/ApiRequest';
const getAllRole = async (queryParams) => { const getAllRole = async (queryParams) => {
const response = await SendRequest({ try {
method: 'get', const response = await SendRequest({
prefix: `roles/roles?${queryParams.toString()}`, method: 'get',
}); prefix: `roles?${queryParams.toString()}`,
});
console.log('Role API Response:', response); console.log('Role API Response:', response);
// Parse query params to get page and limit // Check if backend returns paginated data
const params = Object.fromEntries(queryParams); if (response.paging) {
const currentPage = parseInt(params.page) || 1; // Backend already provides pagination info
const currentLimit = parseInt(params.limit) || 10; return {
status: response.statusCode || 200,
data: {
data: response.data || [],
paging: response.paging,
total: response.paging.total || 0
}
};
}
// Backend returns all data, so we need to do client-side pagination // Fallback: If backend returns all data without pagination
const allData = response.data || []; const params = Object.fromEntries(queryParams);
const totalData = allData.length; const currentPage = parseInt(params.page) || 1;
const currentLimit = parseInt(params.limit) || 10;
// Calculate start and end index for current page const allData = response.data || [];
const startIndex = (currentPage - 1) * currentLimit; const totalData = allData.length;
const endIndex = startIndex + currentLimit;
// Slice data for current page // Client-side pagination
const paginatedData = allData.slice(startIndex, endIndex); const startIndex = (currentPage - 1) * currentLimit;
const endIndex = startIndex + currentLimit;
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: { data: paginatedData,
data: paginatedData, paging: {
paging: { page: currentPage,
page: currentPage, limit: currentLimit,
limit: currentLimit, total: totalData,
page_total: Math.ceil(totalData / currentLimit),
},
total: totalData, total: totalData,
page_total: Math.ceil(totalData / currentLimit),
}, },
total: totalData, };
}, } catch (error) {
}; console.error('getAllRole error:', error);
return {
status: 500,
data: {
data: [],
paging: {
page: 1,
limit: 10,
total: 0,
page_total: 0
},
total: 0
},
error: error.message
};
}
}; };
const getRoleById = async (id) => { const getRoleById = async (id) => {
const response = await SendRequest({ const response = await SendRequest({
method: 'get', method: 'get',
prefix: `roles/roles/${id}`, prefix: `roles/${id}`,
}); });
return response.data; return response.data;
}; };
@@ -51,7 +78,7 @@ const getRoleById = async (id) => {
const createRole = async (queryParams) => { const createRole = async (queryParams) => {
const response = await SendRequest({ const response = await SendRequest({
method: 'post', method: 'post',
prefix: `roles/roles`, prefix: `roles`,
params: queryParams, params: queryParams,
}); });
@@ -90,7 +117,7 @@ const createRole = async (queryParams) => {
const updateRole = async (role_id, queryParams) => { const updateRole = async (role_id, queryParams) => {
const response = await SendRequest({ const response = await SendRequest({
method: 'put', method: 'put',
prefix: `roles/roles/${role_id}`, prefix: `roles/${role_id}`,
params: queryParams, params: queryParams,
}); });
@@ -129,7 +156,7 @@ const updateRole = async (role_id, queryParams) => {
const deleteRole = async (queryParams) => { const deleteRole = async (queryParams) => {
const response = await SendRequest({ const response = await SendRequest({
method: 'delete', method: 'delete',
prefix: `roles/roles/${queryParams}`, prefix: `roles/${queryParams}`,
}); });
console.log('Delete API Response:', response); console.log('Delete API Response:', response);

View File

@@ -41,11 +41,32 @@ const DetailUser = (props) => {
const validatePassword = (password) => { const validatePassword = (password) => {
if (!password) return 'Password wajib diisi'; if (!password) return 'Password wajib diisi';
if (password.length < 8) return 'Password minimal 8 karakter';
if (!/[A-Z]/.test(password)) return 'Password harus ada huruf besar'; // Must be at least 8 characters long
if (!/[a-z]/.test(password)) return 'Password harus ada huruf kecil'; if (password.length < 8) {
if (!/\d/.test(password)) return 'Password harus ada angka'; return 'Password must be at least 8 characters long';
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) return 'Password harus ada karakter spesial'; }
// 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; return null;
}; };

View File

@@ -8,6 +8,7 @@ import {
SearchOutlined, SearchOutlined,
CheckOutlined, CheckOutlined,
CloseOutlined, CloseOutlined,
KeyOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif'; import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@@ -49,7 +50,7 @@ const getRoleColor = (role_name, role_level) => {
return 'default'; return 'default';
}; };
const columns = (showPreviewModal, showEditModal, showDeleteDialog, showApproveDialog) => [ const columns = (showPreviewModal, showEditModal, showDeleteDialog, showApproveDialog, showChangePasswordModal) => [
{ {
title: 'ID', title: 'ID',
dataIndex: 'user_id', dataIndex: 'user_id',
@@ -135,7 +136,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog, showApproveD
title: 'Aksi', title: 'Aksi',
key: 'aksi', key: 'aksi',
align: 'center', align: 'center',
width: '15%', width: '18%',
render: (_, record) => ( render: (_, record) => (
<Space> <Space>
<Button <Button
@@ -158,6 +159,12 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog, showApproveD
icon={<EditOutlined style={{ color: '#faad14' }} />} icon={<EditOutlined style={{ color: '#faad14' }} />}
onClick={() => showEditModal(record)} onClick={() => showEditModal(record)}
/> />
<Button
type="text"
style={{ borderColor: '#722ed1' }}
icon={<KeyOutlined style={{ color: '#722ed1' }} />}
onClick={() => showChangePasswordModal(record)}
/>
<Button <Button
type="text" type="text"
danger danger
@@ -255,6 +262,11 @@ const ListUser = memo(function ListUser(props) {
}); });
}; };
const showChangePasswordModal = (param) => {
props.setSelectedUserForPassword(param);
props.setShowChangePasswordModal(true);
};
const handleApprove = async (user_id) => { const handleApprove = async (user_id) => {
const response = await approveUser(user_id); const response = await approveUser(user_id);
if (response.statusCode == 200) { if (response.statusCode == 200) {
@@ -366,7 +378,8 @@ const ListUser = memo(function ListUser(props) {
showPreviewModal, showPreviewModal,
showEditModal, showEditModal,
showDeleteDialog, showDeleteDialog,
showApproveDialog showApproveDialog,
showChangePasswordModal
)} )}
triger={trigerFilter} triger={trigerFilter}
/> />