diff --git a/Modules/Client/Routes/api.php b/Modules/Client/Routes/api.php index 92a0dea0..6e24cceb 100644 --- a/Modules/Client/Routes/api.php +++ b/Modules/Client/Routes/api.php @@ -20,6 +20,8 @@ use Modules\Internal\Http\Controllers\Api\AuditTrailController; use Modules\Internal\Http\Controllers\Api\CorporateController; use Modules\Internal\Http\Controllers\Api\NavigationController; +use Modules\Internal\Http\Controllers\Api\UserManagementController; + /* |-------------------------------------------------------------------------- | API Routes @@ -99,5 +101,19 @@ Route::prefix('client')->group(function () { // Navigation Route::get('navigations', [NavigationController::class, 'index']); + + // User Management Role + Route::get('user/role', [UserManagementController::class, 'index']); + Route::post('user/role', [UserManagementController::class, 'store']); + Route::get('user/role/{id}', [UserManagementController::class, 'edit']); + Route::put('user/role/{id}', [UserManagementController::class, 'update']); + Route::get('permission_list', [UserManagementController::class, 'permission_list']); + + // User Role Access + Route::get('user/access', [UserManagementController::class, 'list_access']); + Route::post('user/access', [UserManagementController::class, 'store_access']); + Route::get('user/access/{id}', [UserManagementController::class, 'edit_access']); + Route::put('user/access/{id}', [UserManagementController::class, 'update_access']); + Route::get('role-list', [UserManagementController::class, 'list_role']); }); }); diff --git a/database/seeders/NavigationSeeder.php b/database/seeders/NavigationSeeder.php index b5a38158..d071c473 100644 --- a/database/seeders/NavigationSeeder.php +++ b/database/seeders/NavigationSeeder.php @@ -261,30 +261,46 @@ class NavigationSeeder extends Seeder ], 'permission' => 'case-management-client-portal' ], + [ + 'title' => 'User Management', + 'children' => [ + [ + 'title' => 'User Role', + 'path' => '/user-role', + 'permission' => 'user-role-list-client-portal' + ], + [ + 'title' => 'User Access', + 'path' => '/user-access', + 'permission' => 'user-access-list-client-portal' + ] + ], + 'permission' => 'user-management-client-portal' + ] ]; foreach ($menuItems as $menuItemData) { $menuItem = Navigations::updateOrCreate([ 'title' => $menuItemData['title'], - 'permission' => $menuItemData['permission'] + 'permission' => $menuItemData['permission'] ?? null ], [ 'title' => $menuItemData['title'], 'path' => $menuItemData['path'] ?? null, - 'permission' => $menuItemData['permission'] + 'permission' => $menuItemData['permission'] ?? null ]); if (isset($menuItemData['children'])) { foreach ($menuItemData['children'] as $childData) { $menuItemChildren = Navigations::updateOrCreate([ 'title' => $childData['title'], - 'permission' => $childData['permission'] + 'permission' => $childData['permission'] ?? null ], [ 'title' => $childData['title'], 'path' => $childData['path'] ?? null, 'parent_id' => $menuItem->id, - 'permission' => $childData['permission'] + 'permission' => $childData['permission'] ?? null ]); } } diff --git a/database/seeders/PermissionTableSeeder.php b/database/seeders/PermissionTableSeeder.php index fa57a98e..acda25ee 100644 --- a/database/seeders/PermissionTableSeeder.php +++ b/database/seeders/PermissionTableSeeder.php @@ -86,6 +86,9 @@ class PermissionTableSeeder extends Seeder 'formularium-list-client-portal', 'case-management-client-portal', 'service-monitoring-limit-client-portal', + 'user-management-client-portal', + 'user-role-list-client-portal', + 'user-access-list-client-portal' ] ] ]; diff --git a/frontend/client-portal/src/components/nav-section/vertical/NavItem.tsx b/frontend/client-portal/src/components/nav-section/vertical/NavItem.tsx index 44826f7b..178cee99 100644 --- a/frontend/client-portal/src/components/nav-section/vertical/NavItem.tsx +++ b/frontend/client-portal/src/components/nav-section/vertical/NavItem.tsx @@ -21,7 +21,7 @@ export function NavItemRoot({ const renderContent = ( <> - + {/* */} {!isCollapse && ( <> diff --git a/frontend/client-portal/src/pages/EmployeeData/Index.tsx b/frontend/client-portal/src/pages/EmployeeData/Index.tsx index e69a3176..5592621b 100644 --- a/frontend/client-portal/src/pages/EmployeeData/Index.tsx +++ b/frontend/client-portal/src/pages/EmployeeData/Index.tsx @@ -16,7 +16,7 @@ export default function Drugs() { diff --git a/frontend/client-portal/src/pages/Master/FormulariumV2/Index.tsx b/frontend/client-portal/src/pages/Master/FormulariumV2/Index.tsx index 2a25563b..7f81542c 100644 --- a/frontend/client-portal/src/pages/Master/FormulariumV2/Index.tsx +++ b/frontend/client-portal/src/pages/Master/FormulariumV2/Index.tsx @@ -14,7 +14,7 @@ export default function MasterFormularium() { heading={pageTitle} links={[ { - name: "Master", + name: "Case Management", href: "/master/formularium-template-v2" }, { diff --git a/frontend/client-portal/src/pages/UserManagement/UserAccess/CreateUpdate.tsx b/frontend/client-portal/src/pages/UserManagement/UserAccess/CreateUpdate.tsx new file mode 100644 index 00000000..44498f35 --- /dev/null +++ b/frontend/client-portal/src/pages/UserManagement/UserAccess/CreateUpdate.tsx @@ -0,0 +1,63 @@ + +import { useNavigate, useParams } from "react-router-dom"; +import HeaderBreadcrumbs from "../../../components/HeaderBreadcrumbs"; +import Page from "../../../components/Page"; +import {useContext, useEffect, useMemo, useState } from 'react'; +import axios from '../../../utils/axios'; +import UserAccessForm from './Form'; +import { Role, UserAccess } from '../../../@types/user'; + + + +export default function UserAccessCreate() { + const { id } = useParams(); + const [ currentUserAccess, setCurrentUserAccess ] = useState(); + const [ roles, setRole ] = useState(); + + + const navigate = useNavigate(); + + const isEdit = !!id; + + useEffect(() => { + if (isEdit) { + axios.get('/user/access/'+id) + .then((res) => { + setCurrentUserAccess(res.data); + }) + .catch((err) => { + if (err.response.status === 404) { + navigate('/404'); + } + }) + } + axios.get('/role-list') + .then((res)=> { + setRole(res.data) + }) + .catch((err) => { + if (err.response.status === 404) { + navigate('/404'); + } + }) + + }, [id]); + + + return ( + + + + + + ); +} diff --git a/frontend/client-portal/src/pages/UserManagement/UserAccess/Form.tsx b/frontend/client-portal/src/pages/UserManagement/UserAccess/Form.tsx new file mode 100644 index 00000000..015f3cc4 --- /dev/null +++ b/frontend/client-portal/src/pages/UserManagement/UserAccess/Form.tsx @@ -0,0 +1,147 @@ +import * as Yup from 'yup'; +import { LoadingButton } from "@mui/lab"; +import { Box, Card, Grid, Stack, Typography } from "@mui/material"; +import { Role, UserAccess } from "../../../@types/user"; +import { FormProvider, RHFSelect, RHFSwitch, RHFTextField } from "../../../components/hook-form"; +import { useEffect, useMemo } from 'react'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useSnackbar } from 'notistack'; +import { useNavigate, useParams } from 'react-router-dom'; +import axios from '../../../utils/axios'; +import palette from '@/theme/palette'; + +type Props = { + isEdit: boolean; + currentUserAccess?: UserAccess; + roles: Role +}; + +export default function AccsessForm({ isEdit, currentUserAccess, roles }: Props) { + + const { enqueueSnackbar } = useSnackbar(); + const navigate = useNavigate(); + const { id } = useParams(); + + const NewCorporatePlanSchema = Yup.object().shape({ + name: Yup.string().required('Name is required'), + }); + + console.log(currentUserAccess, 'test') + const defaultValues = useMemo( + () => ({ + name: currentUserAccess?.person?.name || '', + username: currentUserAccess?.username || '', + email: currentUserAccess?.email || '', + roles: currentUserAccess?.role?.id || [], + password: '', + }), + [currentUserAccess] + ); + + useEffect(() => { + if (isEdit && currentUserAccess) { + reset(defaultValues); + } + if (!isEdit) { + reset(defaultValues); + } + }, [isEdit, currentUserAccess]); + + const methods = useForm({ + resolver: yupResolver(NewCorporatePlanSchema), + defaultValues, + }); + + const { + reset, + watch, + control, + setValue, + getValues, + setError, + handleSubmit, + formState: { isSubmitting }, + } = methods; + + + const onSubmit = async (data: any) => { + + console.log(data); + if (!isEdit) { + await axios + .post('/user/access', data) + .then((res) => { + enqueueSnackbar('User created successfully', { variant: 'success' }); + }) + .then((res) => { + navigate('/user-access', { replace: true }); + }) + .catch(({ response }) => { + if (response.status === 422) { + for (const [key, value] of Object.entries(response.data.errors)) { + setError(key, { message: value[0] }); + enqueueSnackbar(value[0] ?? 'Failed Processing Request', { variant: 'error' }); + } + } + else { + enqueueSnackbar('Create Failed : '+ response.data.message, { variant: 'error' }); + } + }); + } else { + await axios + .put('/user/access/' + currentUserAccess?.id, data) + .then((res) => { + enqueueSnackbar('User updated successfully', { variant: 'success' }); + }) + .then((res) => { + navigate('/user-access' , { replace: true }); + }) + .catch(({ response }) => { + enqueueSnackbar('Update Failed : '+ response.data.message, { variant: 'error' }); + }); + } + }; + + const optionsRoles = roles?.data?.map(item => ({ + value: item.id, + label: item.name + })) ?? []; + + if (optionsRoles.length > 0) { + optionsRoles.unshift({ value: '', label: '' }); + } + + return ( + + + + + + + User Access + + + + + + {optionsRoles.map((option, index) => ( + + ))} + + + + + { isEdit? 'Update' : 'Create' } + + + + + + + + + ); +} diff --git a/frontend/client-portal/src/pages/UserManagement/UserAccess/History.tsx b/frontend/client-portal/src/pages/UserManagement/UserAccess/History.tsx new file mode 100644 index 00000000..8d733ca7 --- /dev/null +++ b/frontend/client-portal/src/pages/UserManagement/UserAccess/History.tsx @@ -0,0 +1,218 @@ +// @mui +import { + Box, + Button, + Card, + Collapse, + Container, + FormControl, + Grid, + IconButton, + InputLabel, + MenuItem, + OutlinedInput, + Paper, + Select, + SelectChangeEvent, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, + Badge, + Stack, +} from '@mui/material'; +import * as React from 'react'; +import { useParams } from 'react-router-dom'; +import { styled } from '@mui/material/styles'; +import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp'; +import MuiAccordion, { AccordionProps } from '@mui/material/Accordion'; +import { useContext, useEffect, useState } from 'react'; +import MuiAccordionSummary, { + AccordionSummaryProps, +} from '@mui/material/AccordionSummary'; +import useSettings from '../../../hooks/useSettings'; +import axios from '../../../utils/axios'; +import { ConfiguredCorporateContext } from '@/contexts/ConfiguredCorporateContext'; +import MuiAccordionDetails from '@mui/material/AccordionDetails'; +import HeaderBreadcrumbs from '../../../components/HeaderBreadcrumbs'; +import { Corporate } from '@/@types/corporates'; +import { fDate, fDateTime } from '@/utils/formatTime'; + +const Accordion = styled((props: AccordionProps) => ( + +))(({ theme }) => ({ + border: `1px solid ${theme.palette.divider}`, + '&:not(:last-child)': { + borderBottom: 0, + }, + '&:before': { + display: 'none', + }, +})); + +const AccordionSummary = styled((props: AccordionSummaryProps) => ( + } + {...props} + /> +))(({ theme }) => ({ + backgroundColor: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, .05)' + : 'rgba(0, 0, 0, .03)', + flexDirection: 'row-reverse', + '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { + transform: 'rotate(90deg)', + }, + '& .MuiAccordionSummary-content': { + marginLeft: theme.spacing(1), + }, +})); + +const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ + padding: theme.spacing(2), + borderTop: '1px solid rgba(0, 0, 0, .125)', +})); + +export default function CustomizedAccordions() { + const [expanded, setExpanded] = React.useState('panel1'); + + const handleChange = + (panel: string) => (event: React.SyntheticEvent, newExpanded: boolean) => { + setExpanded(newExpanded ? panel : false); + }; + const pageTitle = 'Diagnosis Template History'; + + const { themeStretch } = useSettings(); + + const { id } = useParams(); + + const [corporate, setCorporate] = useState(); + const [ currentCorporate, setCurrentCorporate ] = useState(); + + const configuredCorporateContext = useContext(ConfiguredCorporateContext); + + useEffect(() => { + setCorporate(configuredCorporateContext.currentCorporate); + const model = 'App\\Models\\IcdTemplate'; + const url = `/audittrail/${id}?model=${model}`; + axios.get(url) + .then((res) => { + setCurrentCorporate(res.data); + }) + .catch((error) => { + console.error('Terjadi kesalahan:', error); + }); + + }, [configuredCorporateContext]); + + return ( +
+ + {currentCorporate?.data.map((item, index) => ( + + + {`Data has ${item.action} by ${item.user_id} on ${fDateTime(item.updated_at)}`} + + + + + Field + Old Value + New Values + + + + {Object.entries(item.old_values).map(([key, value]) => { + let renderedValue; + if (key === 'deleted_by' || + key === 'deleted_at' || + key === 'created_by' || + key === 'created_at' || + key === 'updated_by' || + key === 'description' + ) { + return null; // Melewati iterasi saat key adalah 'deleted_by' + } + switch (key) { + case 'welcome_message': + renderedValue = item.new_values[key].replace(/<[^>]*>/g, ''); + value = value.replace(/<[^>]*>/g, ''); + break; + case 'help_text': + renderedValue = item.new_values[key].replace(/<[^>]*>/g, ''); + value = value.replace(/<[^>]*>/g, ''); + break; + case 'active': + renderedValue = item.new_values[key] == 1 ? 'Active' : 'Inactive'; + value = value == 1 ? 'Active' : 'Inactive'; + break; + case 'created_at': + renderedValue = fDateTime(item.new_values[key]); + value = fDateTime(value); + break; + case 'updated_at': + renderedValue = fDateTime(item.new_values[key]); + value = fDateTime(value); + break; + case 'updated_at': + renderedValue = fDateTime(item.new_values[key]); + value = fDateTime(value); + break; + case 'delete_at': + renderedValue = fDateTime(item.new_values[key]); + value = fDateTime(value); + break; + default: + renderedValue = item.new_values[key]; + break; + } + + const field = key.charAt(0).toUpperCase() + key.slice(1); + if (value == renderedValue) { + return null + } else { + return ( + + {`${field}`} + {`${value}`} + {renderedValue} + + ); + } + })} + + + + + ))} +
+ ); +} diff --git a/frontend/client-portal/src/pages/UserManagement/UserAccess/Index.tsx b/frontend/client-portal/src/pages/UserManagement/UserAccess/Index.tsx new file mode 100644 index 00000000..a5f6e278 --- /dev/null +++ b/frontend/client-portal/src/pages/UserManagement/UserAccess/Index.tsx @@ -0,0 +1,37 @@ +import { Container,Card, Grid } from "@mui/material"; +import { useParams } from "react-router-dom"; +import HeaderBreadcrumbs from "../../../components/HeaderBreadcrumbs"; +import Page from "../../../components/Page"; +import useSettings from "../../../hooks/useSettings"; +import List from "./List"; + + + +export default function Divisions() { + const { themeStretch } = useSettings(); + + const { corporate_id } = useParams(); + + const pageTitle = 'User Access'; + return ( + + + + + + + + + + + ); +} diff --git a/frontend/client-portal/src/pages/UserManagement/UserAccess/List.tsx b/frontend/client-portal/src/pages/UserManagement/UserAccess/List.tsx new file mode 100644 index 00000000..3391c365 --- /dev/null +++ b/frontend/client-portal/src/pages/UserManagement/UserAccess/List.tsx @@ -0,0 +1,443 @@ +// @mui +import { Box, Button, Card, Collapse, IconButton, InputLabel, MenuItem, OutlinedInput, Paper, Select, SelectChangeEvent, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography, Badge, Tab, Tabs, CardHeader, Stack, Menu, ButtonGroup, Pagination, Grid, Autocomplete, DialogActions } from '@mui/material'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; +import AddIcon from '@mui/icons-material/Add'; +import UploadIcon from '@mui/icons-material/Upload'; +import CancelIcon from '@mui/icons-material/Cancel'; +import HistoryIcon from '@mui/icons-material/History'; +// hooks +import { Link, NavLink as RouterLink, useNavigate } from 'react-router-dom'; +import React, { ChangeEvent, Component, useEffect, useRef, useState } from 'react'; +import useSettings from '../../../hooks/useSettings'; +import { useParams, useSearchParams } from 'react-router-dom'; +// components +import axios from '../../../utils/axios'; +import { LaravelPaginatedData } from '../../../@types/paginated-data'; +import { UserAccess } from '../../../@types/user'; +import BasePagination from '../../../components/BasePagination'; +import { enqueueSnackbar } from 'notistack'; +import TableMoreMenu from '@/components/table/TableMoreMenu'; +import { Delete, EditOutlined, FindInPageOutlined } from '@mui/icons-material'; +import MuiDialog from '@/components/MuiDialog'; + +export default function List() { + const navigate = useNavigate(); + const { themeStretch } = useSettings(); + const { corporate_id } = useParams(); + const [searchParams, setSearchParams] = useSearchParams(); + const [importResult, setImportResult] = useState(null); + + function SearchInput(props: any) { + // SEARCH + const searchInput = useRef(null); + const [searchText, setSearchText] = useState(""); + + const handleSearchChange = (event: any) => { + const newSearchText = event.target.value ?? '' + setSearchText(newSearchText); + } + + const handleSearchSubmit = (event: any) => { + event.preventDefault(); + props.onSearch(searchText); // Trigger to Parent + } + + useEffect(() => { // Trigger First Search + setSearchText(searchParams.get('search') ?? ''); + }, [searchParams]) + + return ( +
+ + + ); + } + + function ImportForm(props: any) { + // IMPORT + // Create Button Menu + const [anchorEl, setAnchorEl] = React.useState(null); + const createMenu = Boolean(anchorEl); + const importForm = useRef(null) + const [currentImportFileName, setCurrentImportFileName] = useState(null) + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleImportButton = () => { + if (importForm?.current) { + handleClose(); + importForm.current ? importForm.current.click() : console.log('No File selected'); + } else { + alert('No file selected') + } + } + + const handleICDList = async (appliedFilter = null) => { + axios.get('master/diagnosis/list').then((response) => { + const link = document.createElement('a'); + link.href = response.data.data.file_url; + link.setAttribute('download', response.data.data.file_name); + document.body.appendChild(link); + link.click(); + handleClose(); + }); + } + + const handleCancelImportButton = () => { + importForm.current.value = ""; + importForm.current.dispatchEvent(new Event("change", { bubbles: true })); + } + + const handleImportChange = (event: any) => { + if (event.target.files[0]) { + setCurrentImportFileName(event.target.files[0].name) + } else { + setCurrentImportFileName(null); + } + } + + const handleUpload = () => { + if (importForm.current?.files.length) { + const formData = new FormData(); + formData.append("file", importForm.current?.files[0]) + axios.post(`master/diagnosis/import`, formData ) + .then(response => { + handleCancelImportButton(); + loadDataTableData(); + setImportResult(response.data) + // alert('Succesfully read '+ response.data.total_successed_row + ' with ' + response.data.total_failed_row + ' failed rows'); + }) + .catch(response => { + enqueueSnackbar('Looks like something went wrong. Please check your data and try again. ' + response.message, { variant: 'error' }) + }) + } else { + enqueueSnackbar('No File Selected', { variant: 'warning' }) + } + } + + const handleGetTemplate = (type :string) => { + axios.get('corporates/import-document-example/' + type) + .then((response) => { + const link = document.createElement('a'); + link.href = response.data.data.file_url; + link.setAttribute('download', response.data.data.file_name); + document.body.appendChild(link); + link.click(); + handleClose(); + }) + } + + + + return ( +
+ + {( !currentImportFileName && + + {/*

kjasndkjandskjasndkjansdkjansd

*/} + + + + + + +
+ )} +
+ ); + } + + // Called on every row to map the data to the columns + function createData( userAccess: UserAccess ): UserAccess { + return { + ...userAccess, + } + } + + // Generate the every row of the table + function Row(props: { row: ReturnType }) { + const { row } = props; + const [open, setOpen] = React.useState(false); + + const handleActivate = (model: any, status: string) => { + axios + .put(`/master/diagnosis-template/${row.id}/activation`, { + // service_code: service.service_code, + active: status == 'active', + }) + .then((res) => { + setDataTableData({ + ...dataTableData, + data: dataTableData.data.map((model) => { + let updatedModel = model; + if (row.id == model.id) { + updatedModel.active = res.data.icd.active; + } + return updatedModel; + }), + }); + }) + .catch((error) => { + // console.log('asdasd', error.response.data.message) + enqueueSnackbar( + error.response.data.message ?? error.message ?? 'Failed Processing Request', + { variant: 'error' } + ); + }); + }; + + return ( + + *': { borderBottom: '1' } }}> + + {row.person?.name ?? '-'} + {row.email ?? '-'} + {row.role?.name ?? '-'} + + + + {/* navigate(`/master/diagnosis/${row.id}`)}> + + Detail + */} + navigate(`/user/access/${row.id}/edit`)} > + + Edit + + {/* setOpenDialogDelete(true)}> + + Delete + */} + + } /> + + + + + ); + } + // Delete + const reasons = [ + { value: 'agreement', label: 'Agreement changed' }, + { value: 'endorsement', label: 'Endorsement' }, + { value: 'renewal', label: 'Renewal' }, + { value: 'wrong_setting', label: 'Wrong Setting' }, + // Add more options as needed + ]; + + const [isReasonSelected, setIsReasonSelected] = useState(false); + const [formData, setFormData] = useState({ + reason: null + }); + + const marginBottom2 = { + marginBottom: 2, + } + + const style1 = { + color: '#919EAB', + width: '30%' + } + + const handleCloseDialog = () => { + setOpenDialogDelete(false); + resetForm(); + } + + const resetForm = () => { + setFormData({ + reason: null + }); + }; + const handleChange = (field, value) => { + setFormData((prevData) => ({ + ...prevData, + [field]: value, + })); + if (field === 'reason') { + setIsReasonSelected(!!value); + } + } + + const handleSubmit = () => { + if (isReasonSelected && formData.reason !== '') { + alert('zsd.'); + } else { + setIsReasonSelected(false); + } + + } + + // Dialog + const getContent = () => ( + + Are you sure to delete this User? + + + + Reason* + option.label} + fullWidth + value={reasons.find((r) => r.value === formData.reason) || null} // Use find to match the default value + onChange={(e, newValue) => handleChange('reason', newValue?.value)} + renderInput={(params) => ( + + )} + /> + + + + + + + + + + ); + + // Dummy Default Data + const [dataTableIsLoading, setDataTableLoading] = useState(true); + const [dataTableLastRequest, setDataTableLastRequest] = useState(0); + const [dataTableResponseState, setDataTableResponseState] = useState('idle'); + const [dataTableData, setDataTableData] = useState({ + current_page: 1, + data: [], + path: "", + first_page_url: "", + last_page: 1, + last_page_url: "", + next_page_url: "", + prev_page_url: "", + per_page: 10, + from: 0, + to: 0, + total: 0 + }); + const [dataTablePage, setDataTablePage] = useState(5); + + const loadDataTableData = async (appliedFilter : any | null = null) => { + setDataTableLoading(true); + const filter = appliedFilter ? appliedFilter : Object.fromEntries([...searchParams.entries()]); + const response = await axios.get('/user/access', { params: filter }); + console.log(response.data); + setDataTableLoading(false); + + setDataTableData(response.data); + } + + const headStyle = { + fontWeight: 'bold', + }; + + const applyFilter = async (searchFilter: string) => { + await loadDataTableData({ "search" : searchFilter }); + setSearchParams({ "search" : searchFilter }); + } + + const handlePageChange = (event : ChangeEvent, value: number) => { + const filter = Object.fromEntries([...searchParams.entries(), ["page", value]]); + loadDataTableData(filter); + setSearchParams(filter); + } + + const [openDialogDelete, setOpenDialogDelete] = React.useState(false); + + useEffect(() => { + loadDataTableData(); + }, []) + + return ( + + + + {/* The Main Table */} + + + + + + + + + + + + + Name + Email + Role Access + + + + {dataTableIsLoading ? + ( + + + Loading + + + ) : ( + dataTableData.data.length == 0 ? + ( + + + No Data + + + ) : ( + + {dataTableData.data.map(row => ( + + ))} + + ) + )} +
+
+ + +
+ ); +} diff --git a/frontend/client-portal/src/pages/UserManagement/UserRole/CreateUpdate.tsx b/frontend/client-portal/src/pages/UserManagement/UserRole/CreateUpdate.tsx new file mode 100644 index 00000000..c6366d9d --- /dev/null +++ b/frontend/client-portal/src/pages/UserManagement/UserRole/CreateUpdate.tsx @@ -0,0 +1,82 @@ + +import { useNavigate, useParams } from "react-router-dom"; +import HeaderBreadcrumbs from "../../../components/HeaderBreadcrumbs"; +import Page from "../../../components/Page"; +import useSettings from "../../../hooks/useSettings"; +import {useContext, useEffect, useMemo, useState } from 'react'; +import axios from '../../../utils/axios'; +import { useSnackbar } from 'notistack'; +import UserRoleForm from './Form'; +import { Role } from '../../../@types/user'; +import { Corporate } from "@/@types/corporates"; +import { ConfiguredCorporateContext } from "@/contexts/ConfiguredCorporateContext"; + + + +export default function PlanCreate() { + const { themeStretch } = useSettings(); + const { corporate_id, id } = useParams(); + const [corporate, setCorporate] = useState(); + const configuredCorporateContext = useContext(ConfiguredCorporateContext); + + useEffect(() => { + setCorporate(configuredCorporateContext.currentCorporate); + }, [configuredCorporateContext]) + + const [ currentUserRole, setCurrentUserRole ] = useState(); + + + const navigate = useNavigate(); + + const isEdit = !!id; + + const [permissions, setPermissions] = useState([]); + + useEffect(() => { + if (isEdit) { + axios.get('/user/role/'+id) + .then((res) => { + setCurrentUserRole(res.data); + axios.get('/permission_list?guard_name='+res.data.guard_name) + .then((res) => { + setPermissions(res.data); + }) + .catch((err) => { + if (err.response && err.response.status === 404) { + navigate('/404'); + } else { + console.error('Error fetching permissions:', err); + } + }); + }) + .catch((err) => { + if (err.response.status === 404) { + navigate('/404'); + } + }) + } + + + }, [corporate_id, id]); + + return ( + + + + + + ); +} diff --git a/frontend/client-portal/src/pages/UserManagement/UserRole/Form.tsx b/frontend/client-portal/src/pages/UserManagement/UserRole/Form.tsx new file mode 100644 index 00000000..46195612 --- /dev/null +++ b/frontend/client-portal/src/pages/UserManagement/UserRole/Form.tsx @@ -0,0 +1,193 @@ +import * as Yup from 'yup'; +import { LoadingButton } from "@mui/lab"; +import {Box, Card, FormControlLabel, Grid, Stack, Typography } from "@mui/material"; +import Autocomplete from '@mui/material/Autocomplete'; +import TextField from '@mui/material/TextField'; +import { Role } from '../../../@types/user'; +import { Permisions } from '../../../@types/user'; +import { FormProvider, RHFSelect, RHFSwitch, RHFTextField } from "../../../components/hook-form"; +import { useEffect, useMemo, useState } from 'react'; +import { useForm, Controller } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useSnackbar } from 'notistack'; +import { useNavigate, useParams } from 'react-router-dom'; +import axios from '../../../utils/axios'; +import palette from '@/theme/palette'; +import { Checkbox } from '@mui/material'; +import Label from '@/components/Label'; + +type Props = { + isEdit: boolean; + currentUserRole?: Role; + permissions?: Permisions; +}; + +export default function UserRoleForm({ isEdit, currentUserRole, permissions }: Props) { + + const { enqueueSnackbar } = useSnackbar(); + const navigate = useNavigate(); + const { corporate_id } = useParams(); + const [guardName, setGuardName] = useState(currentUserRole?.guard_name || ''); + const [filteredPermissions, setFilteredPermissions] = useState(permissions); + + const NewUserRoleSchema = Yup.object().shape({ + name: Yup.string().required('Name is required'), + }); + + const defaultValues = useMemo( + () => ({ + name: currentUserRole?.name || '', + guard_name: currentUserRole?.guard_name || '', + permission_check: currentUserRole?.permissions?.map(permission => permission.id) || [] + + }), + [currentUserRole, permissions] + ); + + useEffect(() => { + if (isEdit && currentUserRole) { + reset(defaultValues); + } + if (!isEdit) { + reset(defaultValues); + } + }, [isEdit, currentUserRole]); + + const methods = useForm({ + resolver: yupResolver(NewUserRoleSchema), + defaultValues, + }); + + const { + reset, + watch, + control, + setValue, + getValues, + setError, + handleSubmit, + formState: { isSubmitting }, + } = methods; + + + const onSubmit = async (data: any) => { + console.log(data, 'test1') + if (!isEdit) { + await axios + .post('/user/role', data) + .then((res) => { + enqueueSnackbar('User Role created successfully', { variant: 'success' }); + }) + .then((res) => { + navigate('/user-role', { replace: true }); + }) + .catch(({ response }) => { + if (response.status === 422) { + for (const [key, value] of Object.entries(response.data.errors)) { + setError(key, { message: value[0] }); + enqueueSnackbar(value[0] ?? 'Failed Processing Request', { variant: 'error' }); + } + } + else { + enqueueSnackbar('Create Failed : '+ response.data.message, { variant: 'error' }); + } + }); + } else { + await axios + .put('/user/role/' + currentUserRole?.id, data) + .then((res) => { + enqueueSnackbar('User Role updated successfully', { variant: 'success' }); + }) + .then((res) => { + navigate('/user-role' , { replace: true }); + }) + .catch(({ response }) => { + enqueueSnackbar('Update Failed : '+ response.data.message, { variant: 'error' }); + }); + } + }; + + const guard_name_options = [ + { value: '', label: '' }, + // { value: 'web', label: 'Primecenter' }, + { value: 'client-portal', label: 'Client Portal' }, + // { value: 'hospital-portal', label: 'Hospital Portal' } + ]; + + // Buat fungsi handleCheckboxClick di luar komponen utama (UserRoleForm) + const handleCheckboxClick = (permissionId, checked) => { + const currentPermissions = getValues('permission_check') || []; + if (checked) { + setValue('permission_check', [...currentPermissions, permissionId]); + } else { + setValue('permission_check', currentPermissions.filter(id => id !== permissionId)); + } + }; + + useEffect(() => { + // Fetch permissions based on guard_name + if (guardName) { + axios.get(`/permission_list?guard_name=${guardName}`) + .then((res) => { + setFilteredPermissions(res.data); + }) + .catch((err) => { + console.error('Error fetching permissions:', err); + }); + } else { + setFilteredPermissions(permissions); + } + }, [guardName,permissions]); + + const handleGuardNameChange = (event) => { + console.log("ivan") + setGuardName(event.target.value); + setValue('guard_name', event.target.value); + }; + + return ( + + + + + + + User Role + + + {guard_name_options.map((option, index) => ( + + ))} + + Permission + + {filteredPermissions?.map((permission, index) => ( + + handleCheckboxClick(permission.id, e.target.checked)} + /> + } + label={permission.name} + /> + + ))} + + + { isEdit? 'Update' : 'Create' } + + + + + + + + + ); +} diff --git a/frontend/client-portal/src/pages/UserManagement/UserRole/History.tsx b/frontend/client-portal/src/pages/UserManagement/UserRole/History.tsx new file mode 100644 index 00000000..8d733ca7 --- /dev/null +++ b/frontend/client-portal/src/pages/UserManagement/UserRole/History.tsx @@ -0,0 +1,218 @@ +// @mui +import { + Box, + Button, + Card, + Collapse, + Container, + FormControl, + Grid, + IconButton, + InputLabel, + MenuItem, + OutlinedInput, + Paper, + Select, + SelectChangeEvent, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, + Badge, + Stack, +} from '@mui/material'; +import * as React from 'react'; +import { useParams } from 'react-router-dom'; +import { styled } from '@mui/material/styles'; +import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp'; +import MuiAccordion, { AccordionProps } from '@mui/material/Accordion'; +import { useContext, useEffect, useState } from 'react'; +import MuiAccordionSummary, { + AccordionSummaryProps, +} from '@mui/material/AccordionSummary'; +import useSettings from '../../../hooks/useSettings'; +import axios from '../../../utils/axios'; +import { ConfiguredCorporateContext } from '@/contexts/ConfiguredCorporateContext'; +import MuiAccordionDetails from '@mui/material/AccordionDetails'; +import HeaderBreadcrumbs from '../../../components/HeaderBreadcrumbs'; +import { Corporate } from '@/@types/corporates'; +import { fDate, fDateTime } from '@/utils/formatTime'; + +const Accordion = styled((props: AccordionProps) => ( + +))(({ theme }) => ({ + border: `1px solid ${theme.palette.divider}`, + '&:not(:last-child)': { + borderBottom: 0, + }, + '&:before': { + display: 'none', + }, +})); + +const AccordionSummary = styled((props: AccordionSummaryProps) => ( + } + {...props} + /> +))(({ theme }) => ({ + backgroundColor: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, .05)' + : 'rgba(0, 0, 0, .03)', + flexDirection: 'row-reverse', + '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { + transform: 'rotate(90deg)', + }, + '& .MuiAccordionSummary-content': { + marginLeft: theme.spacing(1), + }, +})); + +const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ + padding: theme.spacing(2), + borderTop: '1px solid rgba(0, 0, 0, .125)', +})); + +export default function CustomizedAccordions() { + const [expanded, setExpanded] = React.useState('panel1'); + + const handleChange = + (panel: string) => (event: React.SyntheticEvent, newExpanded: boolean) => { + setExpanded(newExpanded ? panel : false); + }; + const pageTitle = 'Diagnosis Template History'; + + const { themeStretch } = useSettings(); + + const { id } = useParams(); + + const [corporate, setCorporate] = useState(); + const [ currentCorporate, setCurrentCorporate ] = useState(); + + const configuredCorporateContext = useContext(ConfiguredCorporateContext); + + useEffect(() => { + setCorporate(configuredCorporateContext.currentCorporate); + const model = 'App\\Models\\IcdTemplate'; + const url = `/audittrail/${id}?model=${model}`; + axios.get(url) + .then((res) => { + setCurrentCorporate(res.data); + }) + .catch((error) => { + console.error('Terjadi kesalahan:', error); + }); + + }, [configuredCorporateContext]); + + return ( +
+ + {currentCorporate?.data.map((item, index) => ( + + + {`Data has ${item.action} by ${item.user_id} on ${fDateTime(item.updated_at)}`} + + + + + Field + Old Value + New Values + + + + {Object.entries(item.old_values).map(([key, value]) => { + let renderedValue; + if (key === 'deleted_by' || + key === 'deleted_at' || + key === 'created_by' || + key === 'created_at' || + key === 'updated_by' || + key === 'description' + ) { + return null; // Melewati iterasi saat key adalah 'deleted_by' + } + switch (key) { + case 'welcome_message': + renderedValue = item.new_values[key].replace(/<[^>]*>/g, ''); + value = value.replace(/<[^>]*>/g, ''); + break; + case 'help_text': + renderedValue = item.new_values[key].replace(/<[^>]*>/g, ''); + value = value.replace(/<[^>]*>/g, ''); + break; + case 'active': + renderedValue = item.new_values[key] == 1 ? 'Active' : 'Inactive'; + value = value == 1 ? 'Active' : 'Inactive'; + break; + case 'created_at': + renderedValue = fDateTime(item.new_values[key]); + value = fDateTime(value); + break; + case 'updated_at': + renderedValue = fDateTime(item.new_values[key]); + value = fDateTime(value); + break; + case 'updated_at': + renderedValue = fDateTime(item.new_values[key]); + value = fDateTime(value); + break; + case 'delete_at': + renderedValue = fDateTime(item.new_values[key]); + value = fDateTime(value); + break; + default: + renderedValue = item.new_values[key]; + break; + } + + const field = key.charAt(0).toUpperCase() + key.slice(1); + if (value == renderedValue) { + return null + } else { + return ( + + {`${field}`} + {`${value}`} + {renderedValue} + + ); + } + })} + + + + + ))} +
+ ); +} diff --git a/frontend/client-portal/src/pages/UserManagement/UserRole/Index.tsx b/frontend/client-portal/src/pages/UserManagement/UserRole/Index.tsx new file mode 100644 index 00000000..c1971074 --- /dev/null +++ b/frontend/client-portal/src/pages/UserManagement/UserRole/Index.tsx @@ -0,0 +1,37 @@ +import { Container,Card, Grid } from "@mui/material"; +import { useParams } from "react-router-dom"; +import HeaderBreadcrumbs from "../../../components/HeaderBreadcrumbs"; +import Page from "../../../components/Page"; +import useSettings from "../../../hooks/useSettings"; +import List from "./List"; + + + +export default function Divisions() { + const { themeStretch } = useSettings(); + + const { corporate_id } = useParams(); + + const pageTitle = 'User Role'; + return ( + + + + + + + + + + + ); +} diff --git a/frontend/client-portal/src/pages/UserManagement/UserRole/List.tsx b/frontend/client-portal/src/pages/UserManagement/UserRole/List.tsx new file mode 100644 index 00000000..ffba0805 --- /dev/null +++ b/frontend/client-portal/src/pages/UserManagement/UserRole/List.tsx @@ -0,0 +1,442 @@ +// @mui +import { Box, Button, Card, MenuItem, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography, Stack, Menu, Grid, DialogActions } from '@mui/material'; +import { Autocomplete } from "@mui/material"; +import AddIcon from '@mui/icons-material/Add'; +// hooks +import { Link, NavLink as RouterLink, useNavigate } from 'react-router-dom'; +import React, { ChangeEvent, Component, useEffect, useRef, useState } from 'react'; +import useSettings from '../../../hooks/useSettings'; +import { useParams, useSearchParams } from 'react-router-dom'; +// components +import axios from '../../../utils/axios'; +import { LaravelPaginatedData } from '../../../@types/paginated-data'; +import { Role } from '../../../@types/user'; +import BasePagination from '../../../components/BasePagination'; +import { enqueueSnackbar } from 'notistack'; +import TableMoreMenu from '@/components/table/TableMoreMenu'; +import { Delete, EditOutlined, FindInPageOutlined } from '@mui/icons-material'; +import MuiDialog from '@/components/MuiDialog'; + +export default function List() { + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + + function SearchInput(props: any) { + // SEARCH + const searchInput = useRef(null); + const [searchText, setSearchText] = useState(""); + + const handleSearchChange = (event: any) => { + const newSearchText = event.target.value ?? '' + setSearchText(newSearchText); + } + + const handleSearchSubmit = (event: any) => { + event.preventDefault(); + props.onSearch(searchText); // Trigger to Parent + } + + useEffect(() => { // Trigger First Search + setSearchText(searchParams.get('search') ?? ''); + }, [searchParams]) + + return ( +
+ + + ); + } + + function ImportForm(props: any) { + // IMPORT + // Create Button Menu + const [anchorEl, setAnchorEl] = React.useState(null); + const createMenu = Boolean(anchorEl); + const importForm = useRef(null) + const [currentImportFileName, setCurrentImportFileName] = useState(null) + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleImportButton = () => { + if (importForm?.current) { + handleClose(); + importForm.current ? importForm.current.click() : console.log('No File selected'); + } else { + alert('No file selected') + } + } + + const handleICDList = async (appliedFilter = null) => { + axios.get('master/diagnosis/list').then((response) => { + const link = document.createElement('a'); + link.href = response.data.data.file_url; + link.setAttribute('download', response.data.data.file_name); + document.body.appendChild(link); + link.click(); + handleClose(); + }); + } + + const handleCancelImportButton = () => { + importForm.current.value = ""; + importForm.current.dispatchEvent(new Event("change", { bubbles: true })); + } + + const handleImportChange = (event: any) => { + if (event.target.files[0]) { + setCurrentImportFileName(event.target.files[0].name) + } else { + setCurrentImportFileName(null); + } + } + + const handleUpload = () => { + if (importForm.current?.files.length) { + const formData = new FormData(); + formData.append("file", importForm.current?.files[0]) + axios.post(`master/diagnosis/import`, formData ) + .then(response => { + handleCancelImportButton(); + loadDataTableData(); + setImportResult(response.data) + // alert('Succesfully read '+ response.data.total_successed_row + ' with ' + response.data.total_failed_row + ' failed rows'); + }) + .catch(response => { + enqueueSnackbar('Looks like something went wrong. Please check your data and try again. ' + response.message, { variant: 'error' }) + }) + } else { + enqueueSnackbar('No File Selected', { variant: 'warning' }) + } + } + + const handleGetTemplate = (type :string) => { + axios.get('corporates/import-document-example/' + type) + .then((response) => { + const link = document.createElement('a'); + link.href = response.data.data.file_url; + link.setAttribute('download', response.data.data.file_name); + document.body.appendChild(link); + link.click(); + handleClose(); + }) + } + + + + return ( +
+ + {( !currentImportFileName && + + {/*

kjasndkjandskjasndkjansdkjansd

*/} + + + + + + +
+ )} +
+ ); + } + + // Called on every row to map the data to the columns + function createData( userManamgent: Role ): Role { + return { + ...userManamgent, + } + } + + const [id, setId] = useState(null) + + // Generate the every row of the table + function Row(props: { row: ReturnType }) { + const { row } = props; + const handleActivate = (model: any, status: string) => { + axios + .put(`/master/diagnosis-template/${row.id}/activation`, { + // service_code: service.service_code, + active: status == 'active', + }) + .then((res) => { + setDataTableData({ + ...dataTableData, + data: dataTableData.data.map((model) => { + let updatedModel = model; + if (row.id == model.id) { + updatedModel.active = res.data.icd.active; + } + return updatedModel; + }), + }); + }) + .catch((error) => { + // console.log('asdasd', error.response.data.message) + enqueueSnackbar( + error.response.data.message ?? error.message ?? 'Failed Processing Request', + { variant: 'error' } + ); + }); + }; + + return ( + + *': { borderBottom: '1' } }}> + + {row.id} + {row.name ?? '-'} + {row.guard_name ?? '-'} + + + + {/* navigate(`/user/role/${row.id}`)}> + + Detail + */} + navigate(`/user/role/${row.id}/edit`)} > + + Edit + + {/* { setOpenDialogDelete(true); setId(row.id); }}> + + Delete + */} + {/* navigate(`/user/role/${row.id}/history`)}> + + History + */} + + } /> + + + + + ); + } + + // Delete + const reasons = [ + { value: 'agreement', label: 'Agreement changed' }, + { value: 'endorsement', label: 'Endorsement' }, + { value: 'renewal', label: 'Renewal' }, + { value: 'wrong_setting', label: 'Wrong Setting' }, + // Add more options as needed + ]; + + const [isReasonSelected, setIsReasonSelected] = useState(false); + const [formData, setFormData] = useState({ + reason: null + }); + + const marginBottom2 = { + marginBottom: 2, + } + + const style1 = { + color: '#919EAB', + width: '30%' + } + + const handleCloseDialog = () => { + setOpenDialogDelete(false); + resetForm(); + } + + const resetForm = () => { + setFormData({ + reason: null + }); + }; + + const handleChange = (field, value) => { + setFormData((prevData) => ({ + ...prevData, + [field]: value, + })); + if (field === 'reason') { + setIsReasonSelected(!!value); + } + } + + const handleSubmit = () => { + if (isReasonSelected && formData.reason !== '') { + console.log(formData, 'test') + } else { + setIsReasonSelected(false); + } + } + + // Dialog + const getContent = () => ( + + Are you sure to delete this User Role? + + + + Reason* + option.label} + fullWidth + value={reasons.find((r) => r.value === formData.reason) || null} // Use find to match the default value + onChange={(e, newValue) => handleChange('reason', newValue?.value)} + renderInput={(params) => ( + + )} + /> + + + + + + + + + + ); + + // Dummy Default Data + const [dataTableIsLoading, setDataTableLoading] = useState(true); + const [dataTableLastRequest, setDataTableLastRequest] = useState(0); + const [dataTableResponseState, setDataTableResponseState] = useState('idle'); + const [dataTableData, setDataTableData] = useState({ + current_page: 1, + data: [], + path: "", + first_page_url: "", + last_page: 1, + last_page_url: "", + next_page_url: "", + prev_page_url: "", + per_page: 10, + from: 0, + to: 0, + total: 0 + }); + const [dataTablePage, setDataTablePage] = useState(5); + + const loadDataTableData = async (appliedFilter : any | null = null) => { + setDataTableLoading(true); + const filter = appliedFilter ? appliedFilter : Object.fromEntries([...searchParams.entries()]); + const response = await axios.get('/user/role', { params: filter }); + console.log(response.data); + setDataTableLoading(false); + + setDataTableData(response.data); + } + + const headStyle = { + fontWeight: 'bold', + }; + + const applyFilter = async (searchFilter: string) => { + await loadDataTableData({ "search" : searchFilter }); + setSearchParams({ "search" : searchFilter }); + } + + const handlePageChange = (event : ChangeEvent, value: number) => { + const filter = Object.fromEntries([...searchParams.entries(), ["page", value]]); + loadDataTableData(filter); + setSearchParams(filter); + } + + const [openDialogDelete, setOpenDialogDelete] = React.useState(false); + + useEffect(() => { + loadDataTableData(); + }, []) + + return ( + + + + {/* The Main Table */} + + + + + + + + + + + + + ID + Name + Guard Name + + + + {dataTableIsLoading ? + ( + + + Loading + + + ) : ( + dataTableData.data.length == 0 ? + ( + + + No Data + + + ) : ( + + {dataTableData.data.map(row => ( + + ))} + + ) + )} +
+
+ + + + +
+ ); +} diff --git a/frontend/client-portal/src/routes/index.tsx b/frontend/client-portal/src/routes/index.tsx index 4bf282dd..d3d4bc87 100644 --- a/frontend/client-portal/src/routes/index.tsx +++ b/frontend/client-portal/src/routes/index.tsx @@ -314,6 +314,102 @@ export default function Router() { }, ], }, + { + path: 'user-role', + element: ( + + + + + + ), + children: [ + { + element: , + index: true, + }, + ], + }, + { + path: 'user-role/create', + element: ( + + + + + + ), + children: [ + { + element: , + index: true, + }, + ], + }, + { + path: 'user/role/:id/edit', + element: ( + + + + + + ), + children: [ + { + element: , + index: true, + }, + ], + }, + { + path: 'user-access', + element: ( + + + + + + ), + children: [ + { + element: , + index: true, + }, + ], + }, + { + path: 'user-access/create', + element: ( + + + + + + ), + children: [ + { + element: , + index: true, + }, + ], + }, + { + path: 'user/access/:id/edit', + element: ( + + + + + + ), + children: [ + { + element: , + index: true, + }, + ], + }, { path: '*', element: }, ]); } @@ -361,3 +457,9 @@ const MasterFormulariumTemplateV2 = Loadable(lazy(() => import('../pages/Master/ const MasterFormulariumTemplateCreateV2 = Loadable(lazy(() => import('../pages/Master/FormulariumV2/CreateUpdate'))); const MasterFormulariumTemplateHistoriesV2 = Loadable(lazy(() => import('../pages/Master/FormulariumV2/History'))); const MasterFormulariumTemplateDetailV2 = Loadable(lazy(() => import('../pages/Master/FormulariumV2/Detail/Index'))); + +// User Management +const UserRole = Loadable(lazy(() => import('../pages/UserManagement/UserRole/Index'))); +const UserRoleCreate = Loadable(lazy(() => import('../pages/UserManagement/UserRole/CreateUpdate'))); +const UserAccess = Loadable(lazy(() => import('../pages/UserManagement/UserAccess/Index'))); +const UserAccessCreate = Loadable(lazy(() => import('../pages/UserManagement/UserAccess/CreateUpdate')));