From fce657412e70f014dc4826fe9e3cd81a73ad831e Mon Sep 17 00:00:00 2001 From: Tb Fajri Date: Sun, 9 Jun 2024 12:49:40 +0700 Subject: [PATCH] User access dan user role dinamis --- .../Controllers/Api/NavigationController.php | 125 +++++ .../Api/UserManagementController.php | 157 ++++++- Modules/Internal/Routes/api.php | 19 + app/Http/Kernel.php | 6 + app/Models/Navigations.php | 19 + app/Models/User.php | 36 +- ...4_06_08_113357_create_navigation_table.php | 36 ++ database/seeders/NavigationSeeder.php | 247 ++++++++++ database/seeders/PermissionTableSeeder.php | 81 ++++ frontend/dashboard/src/@types/user.ts | 26 + .../layouts/dashboard/navbar/NavConfig.tsx | 9 +- .../dashboard/navbar/NavbarVertical.tsx | 67 ++- .../UserAccess/CreateUpdate.tsx | 63 +++ .../pages/UserManagement/UserAccess/Form.tsx | 147 ++++++ .../UserManagement/UserAccess/History.tsx | 218 +++++++++ .../pages/UserManagement/UserAccess/Index.tsx | 33 ++ .../pages/UserManagement/UserAccess/List.tsx | 443 ++++++++++++++++++ .../UserManagement/UserRole/CreateUpdate.tsx | 81 ++++ .../pages/UserManagement/UserRole/Form.tsx | 170 +++++++ .../pages/UserManagement/UserRole/History.tsx | 218 +++++++++ .../pages/UserManagement/UserRole/Index.tsx | 33 ++ .../pages/UserManagement/UserRole/List.tsx | 442 +++++++++++++++++ frontend/dashboard/src/routes/index.tsx | 37 +- 23 files changed, 2686 insertions(+), 27 deletions(-) create mode 100644 Modules/Internal/Http/Controllers/Api/NavigationController.php create mode 100644 app/Models/Navigations.php create mode 100644 database/migrations/2024_06_08_113357_create_navigation_table.php create mode 100644 database/seeders/NavigationSeeder.php create mode 100644 database/seeders/PermissionTableSeeder.php create mode 100644 frontend/dashboard/src/pages/UserManagement/UserAccess/CreateUpdate.tsx create mode 100644 frontend/dashboard/src/pages/UserManagement/UserAccess/Form.tsx create mode 100644 frontend/dashboard/src/pages/UserManagement/UserAccess/History.tsx create mode 100644 frontend/dashboard/src/pages/UserManagement/UserAccess/Index.tsx create mode 100644 frontend/dashboard/src/pages/UserManagement/UserAccess/List.tsx create mode 100644 frontend/dashboard/src/pages/UserManagement/UserRole/CreateUpdate.tsx create mode 100644 frontend/dashboard/src/pages/UserManagement/UserRole/Form.tsx create mode 100644 frontend/dashboard/src/pages/UserManagement/UserRole/History.tsx create mode 100644 frontend/dashboard/src/pages/UserManagement/UserRole/Index.tsx create mode 100644 frontend/dashboard/src/pages/UserManagement/UserRole/List.tsx diff --git a/Modules/Internal/Http/Controllers/Api/NavigationController.php b/Modules/Internal/Http/Controllers/Api/NavigationController.php new file mode 100644 index 00000000..74b8e79f --- /dev/null +++ b/Modules/Internal/Http/Controllers/Api/NavigationController.php @@ -0,0 +1,125 @@ +toArray(); + $navigationMaster = []; + + if ($navigations) { + // Buat array untuk menyimpan menu utama + foreach ($navigations as $navigation) { + if ($navigation['parent_id'] == 0) { + // Tambahkan menu utama ke $navigationMaster + $navigation['children'] = []; // Siapkan array untuk children + $navigationMaster[$navigation['id']] = $navigation; + } + } + + // Tambahkan submenu ke menu utama yang sesuai + foreach ($navigations as $navigation) { + if ($navigation['parent_id'] != 0 && isset($navigationMaster[$navigation['parent_id']])) { + $navigationMaster[$navigation['parent_id']]['children'][] = $navigation; + } + } + } + + // Ubah array menjadi list tanpa indeks id + $navigationMaster = array_values($navigationMaster); + + // Transformasi data untuk sesuai dengan format yang diinginkan + $formattedNavigation = [ + 'items' => array_map(function ($navItem) { + return [ + 'title' => $navItem['title'], + 'path' => $navItem['path'], + 'children' => array_map(function ($child) { + return [ + 'title' => $child['title'], + 'path' => $child['path'], + 'icon' => $child['icon'], // Asumsikan Anda memiliki field 'icon' di tabel navigasi + 'permission' => $child['permission'], + ]; + }, $navItem['children']), + 'permission' => $navItem['permission'], + ]; + }, $navigationMaster) + ]; + + return response()->json($formattedNavigation); + } + + + /** + * Show the form for creating a new resource. + * @return Renderable + */ + public function create() + { + return view('internal::create'); + } + + /** + * Store a newly created resource in storage. + * @param Request $request + * @return Renderable + */ + public function store(Request $request) + { + // + } + + /** + * Show the specified resource. + * @param int $id + * @return Renderable + */ + public function show($id) + { + return view('internal::show'); + } + + /** + * Show the form for editing the specified resource. + * @param int $id + * @return Renderable + */ + public function edit($id) + { + return view('internal::edit'); + } + + /** + * Update the specified resource in storage. + * @param Request $request + * @param int $id + * @return Renderable + */ + public function update(Request $request, $id) + { + // + } + + /** + * Remove the specified resource from storage. + * @param int $id + * @return Renderable + */ + public function destroy($id) + { + // + } +} diff --git a/Modules/Internal/Http/Controllers/Api/UserManagementController.php b/Modules/Internal/Http/Controllers/Api/UserManagementController.php index 1486b251..2c21943c 100644 --- a/Modules/Internal/Http/Controllers/Api/UserManagementController.php +++ b/Modules/Internal/Http/Controllers/Api/UserManagementController.php @@ -6,12 +6,159 @@ use App\Helpers\Helper; use Illuminate\Contracts\Support\Renderable; use Illuminate\Http\Request; use Illuminate\Routing\Controller; +use Spatie\Permission\Models\Role; +use Illuminate\Support\Facades\Hash; +use Spatie\Permission\Models\Permission; use App\Models\User; +use App\Models\Person; +use Crypt; -class UserManagemet extends Controller +class UserManagementController extends Controller { - public function index(Request $request){ - $user = User::all(); - return Helper::responseJson(data: $user); + public function index(Request $request) + { + $query = Role::query(); + if ($request->has('search')) { + $search = $request->get('search'); + $query->where('name', 'like', "%{$search}%"); + } + $userRole = $query->paginate(10); + return Helper::paginateResources($userRole); } -} \ No newline at end of file + + public function permission_list(Request $request) + { + $permissions = Permission::all(); + return response()->json($permissions); + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'guard_name' => 'required|string|max:255', // Pastikan setiap permission ada di tabel permissions + ]); + + $newRole = Role::create([ + 'name' => $validated['name'], + 'guard_name' => $validated['guard_name'], + ]); + + if (isset($request->permission_check)) { + $newRole->syncPermissions($request->permission_check); + } + + return response()->json($newRole, 201); + } + + public function edit($id) + { + $role = Role::with('permissions')->findOrFail($id); + return response()->json($role); + } + + public function update(Request $request, $id) + { + $role = Role::with('permissions')->findOrFail($id); + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'guard_name' => 'required|string|max:255', + 'permission_check' => 'nullable|array', + 'permission_check.*' => 'exists:permissions,id', // Pastikan setiap permission ada di tabel permissions + ]); + + $role->update([ + 'name' => $validated['name'], + 'guard_name' => $validated['guard_name'], + ]); + + if (isset($validated['permission_check'])) { + + $permissions = Permission::whereIn('id', $validated['permission_check']) + ->where('guard_name', $validated['guard_name']) + ->get(); + if ($permissions->count() !== count($validated['permission_check'])) { + return response()->json(['error' => 'One or more permissions are invalid for the specified guard.'], 422); + } + $role->syncPermissions($permissions); + } + + return response()->json($role); + } + + public function list_role(Request $request) + { + $query = Role::all(); + $data = [ + 'data' => $query + ]; + return response()->json($data); + } + + public function store_access(Request $request){ + $user = User::create([ + 'email' => $request->email, + 'username' => $request->username, + 'role_id' => $request->roles, + 'password' => Hash::make($request->password), + ]); + + $person = Person::updateOrCreate( + [ + 'id' => $user->person_id + ], + [ + 'name' => $request->name ?? null + ] + ); + $user->person_id = $person->id; + $user->save(); + return response()->json($user); + } + + // List Access + public function list_access(Request $request){ + $userAccess = User::query(); + if ($request->has('search')) { + $search = $request->get('search'); + $userAccess->where('name', 'like', "%{$search}%"); + } + $userAccess = $userAccess->paginate(10); + return Helper::paginateResources($userAccess); + } + + public function edit_access($id){ + $userAccess = User::findOrFail($id); + return response()->json($userAccess); + } + + public function update_access(Request $request, $id){ + $userAccess = User::findOrFail($id); + + if (!$userAccess) { + return response()->json(['error' => 'User Not found.'], 404); + } + + $userAccess->email = $request->email; + $userAccess->username = $request->username; + $userAccess->role_id = $request->roles; + + if ($request->password){ + $userAccess->password = Hash::make($request->password); + } + + $person = Person::updateOrCreate( + [ + 'id' => $userAccess->person_id + ], + [ + 'name' => $request->name ?? null + ] + ); + + $userAccess->person_id = $person->id; + $userAccess->save(); + + return response()->json($userAccess); + } +} diff --git a/Modules/Internal/Routes/api.php b/Modules/Internal/Routes/api.php index a348d893..1c5d310f 100644 --- a/Modules/Internal/Routes/api.php +++ b/Modules/Internal/Routes/api.php @@ -39,10 +39,12 @@ use Modules\Internal\Http\Controllers\Api\ServiceController; use Modules\Internal\Http\Controllers\Api\PrescriptionController; use Modules\Internal\Http\Controllers\Api\SpecialityController; use Modules\Internal\Http\Controllers\Api\VillageController; +use Modules\Internal\Http\Controllers\Api\NavigationController; use Modules\Internal\Http\Controllers\Api\AuditTrailController; use Modules\Internal\Http\Controllers\Api\DailyMonitoringController; use Modules\Internal\Http\Controllers\Api\LaboratoriumResultController; use Modules\Internal\Http\Controllers\Api\CorporateManageController; +use Modules\Internal\Http\Controllers\Api\UserManagementController; use Modules\Internal\Http\Controllers\ClaimEncounterController; // Report @@ -354,6 +356,23 @@ Route::prefix('internal')->group(function () { }); }); + // 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']); + + // Navigation + Route::get('navigations', [NavigationController::class, 'index']); + }); Route::get('province', [ProvinceController::class, 'index']); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 2885a5dd..d3d14d34 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -67,5 +67,11 @@ class Kernel extends HttpKernel 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'linksehat.old.auth' => \App\Http\Middleware\LinksehatOldAuthMiddleware::class, + + // Role + 'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class, + 'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class, + 'role_or_permission' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class, + ]; } diff --git a/app/Models/Navigations.php b/app/Models/Navigations.php new file mode 100644 index 00000000..51b31dae --- /dev/null +++ b/app/Models/Navigations.php @@ -0,0 +1,19 @@ +role_id; + + if (!$roleId) { + return []; + } + + // Ambil permissions dari role_has_permissions dan permissions tabel + $permissions = DB::table('role_has_permissions') + ->join('permissions', 'role_has_permissions.permission_id', '=', 'permissions.id') + ->where('role_has_permissions.role_id', $roleId) + ->select('permissions.id', 'permissions.name', 'permissions.guard_name', 'permissions.path') + ->get(); + + return $permissions; + } + public function managedCorporates() { return $this->belongsToMany(Corporate::class, 'corporate_manager', 'user_id', 'corporate_id'); @@ -97,7 +122,12 @@ class User extends Authenticatable return $this->belongsTo(Person::class, 'person_id'); } - public function ownedPersons() + public function role() + { + return $this->belongsTo(Role::class, 'role_id'); + } + + public function ownedPersons() { return $this->hasMany(Person::class, 'owner_user_id'); } diff --git a/database/migrations/2024_06_08_113357_create_navigation_table.php b/database/migrations/2024_06_08_113357_create_navigation_table.php new file mode 100644 index 00000000..cfc26ab0 --- /dev/null +++ b/database/migrations/2024_06_08_113357_create_navigation_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('title'); + $table->string('path'); + $table->string('icon'); + $table->string('permission'); + $table->foreignId('parent_id'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('navigations'); + } +}; diff --git a/database/seeders/NavigationSeeder.php b/database/seeders/NavigationSeeder.php new file mode 100644 index 00000000..1f85614f --- /dev/null +++ b/database/seeders/NavigationSeeder.php @@ -0,0 +1,247 @@ + 'Dashboard', + 'path' => '/dashboard', + 'permission' => 'dashboard' + ], + // DOCTORS & HOSPITALS + [ + 'title' => 'DOCTORS & HOSPITALS', + 'children' => [ + [ + 'title' => 'Doctors', + 'path' => '/master/doctors', + 'permission' => 'doctor-list' + ], + [ + 'title' => 'Hospitals', + 'path' => '/master/hospitals', + 'permission' => 'hospital-list' + ], + ], + 'permission' => null + ], + // PHARMACY & DELIVERY MANAGEMENT + [ + 'title' => 'PHARMACY & DELIVERY MANAGEMENT', + 'children' => [ + [ + 'title' => 'Drug', + 'path' => '/master/drugs', + 'permission' => 'drug-list' + ], + [ + 'title' => 'Inventory', + 'path' => '/inventory', + 'permission' => null + ], + [ + 'title' => 'Delivery Services', + 'path' => '/delivery', + 'permission' => null + ], + ], + 'permission' => null + ], + // STATION BENEFIT & MEMBERSHIP + [ + 'title' => 'STATION BENEFIT & MEMBERSHIP', + 'openWhen' => ['/corporates', '/formularium', '/diagnosis', '/hospitals'], + 'children' => [ + [ + 'title' => 'Corporate', + 'path' => '/corporates', + 'permission' => 'corporate-list', + ], + // ['title' => 'Corporate Create', 'path' => '/corporates/create'], + [ + 'title' => 'Formularium', + 'path' => '/master/formularium-template-v2', + 'permission' => 'formularium-list', + ], + [ + 'title' => 'Master ICD-10 Diagnosis', + 'path' => '/master/diagnosis', + 'permission' => 'diagnosis-list', + ], + [ + 'title' => 'Hospitals', + 'path' => '/hospitals', + 'permission' => null, + ], + ], + 'permission' => null + ], + // CLAIM REQUEST + [ + 'title' => 'CLAIM REQUEST', + 'path' => '/claim-requests', + 'permission' => 'claim-request-list' + ], + // CLAIM MANAGEMENT + [ + 'title' => 'CLAIM MANAGEMENT', + 'path' => '/claims', + 'permission' => 'claim-management-list' + ], + // CASE MANAGEMENT + [ + 'title' => 'CASE MANAGEMENT', + 'children' => [ + [ + 'title' => 'Daily Monitoring', + 'path' => '/case_management/daily_monitoring', + 'permission' => 'daily-monitoring-list' + ], + // ['title' => 'Laboratorium Result', 'path' => '/case_management/laboratorium_result'], + [ + 'title' => 'Inpatient Monitoring', + 'path' => '/case_management/inpatient_monitoring', + 'permission' => 'final-log-list' + ], + ], + 'permission' => null + ], + // CUSTOMER SERVICES + [ + 'title' => 'CUSTOMER SERVICES', + 'children' => [ + [ + 'title' => 'Request', + 'path' => '/custormer-service/request', + 'permission' => 'request-log-list' + ], + // ['title' => 'Membership', 'path' => '/cs-membership'], + [ + 'title' => 'Final LOG', + 'path' => '/custormer-service/final-log', + 'permission' => 'final-log-list' + ], + ], + 'permission' => null + ], + // REPORT + [ + 'title' => 'REPORT', + 'children' => [ + [ + 'title' => 'Files Provider', + 'path' => 'report/files-provider', + 'permission' => 'report-files-provider-list' + ], + [ + 'title' => 'Letter of Guarantee', + 'path' => '/report/logs', + 'permission' => 'report-log-list' + ], + [ + 'title' => 'Appointment', + 'path' => '/report/appointments', + 'permission' => 'report-appointment-list' + ], + [ + 'title' => 'Live Chat', + 'path' => '/report/live-chat', + 'permission' => 'report-livechat-list' + ], + [ + 'title' => 'Linksehat Payment', + 'path' => '/report/linksehat-payments', + 'permission' => 'report-livechat-payment' + ], + // ['title' => 'Prescription', 'path' => '/report/prescription'], + [ + 'title' => 'Doctor Rating', + 'path' => '/report/doctorrating', + 'permission' => 'report-doctor-rating' + ], + ], + 'permission' => null + ], + // MASTER + [ + 'title' => 'MASTER', + 'children' => [ + [ + 'title' => 'Diagnosis', + 'path' => '/master/diagnosis', + 'permission' => 'diagnosis-list' + ], + ], + 'permission' => null + ], + // USER MANAGEMENT + [ + 'title' => 'USER MANAGEMENT', + 'children' => [ + [ + 'title' => 'User Role', + 'path' => '/user-role', + 'permission' => 'user-role-list' + ], + [ + 'title' => 'User Access', + 'path' => '/user-access', + 'permission' => 'user-role-access' + ], + ], + 'permission' => null + ], + // LINKING TOOLS + [ + 'title' => 'LINKING TOOLS', + 'path' => '/linking', + 'permission' => 'linkking-list' + ], + // E-PRESCRIPTION + [ + 'title' => 'E-PRESCRIPTION', + 'path' => '/e-prescription/live-chat', + 'permission' => 'prescription-list' + ], + ]; + + foreach ($menuItems as $menuItemData) { + $menuItem = Navigations::updateOrCreate([ + 'title' => $menuItemData['title'] + ], + [ + 'title' => $menuItemData['title'], + 'path' => $menuItemData['path'] ?? null, + 'permission' => $menuItemData['permission'] + ]); + + if (isset($menuItemData['children'])) { + foreach ($menuItemData['children'] as $childData) { + $menuItemChildren = Navigations::updateOrCreate([ + 'title' => $childData['title'] + ], + [ + 'title' => $childData['title'], + 'path' => $childData['path'] ?? null, + 'parent_id' => $menuItem->id, + 'permission' => $childData['permission'] + ]); + } + } + } + } +} diff --git a/database/seeders/PermissionTableSeeder.php b/database/seeders/PermissionTableSeeder.php new file mode 100644 index 00000000..a96d4894 --- /dev/null +++ b/database/seeders/PermissionTableSeeder.php @@ -0,0 +1,81 @@ + $permission], + [ + 'name' => $permission, + 'guard_name' => 'web' + ]); + } + } +} diff --git a/frontend/dashboard/src/@types/user.ts b/frontend/dashboard/src/@types/user.ts index df02c82e..1f1faa8b 100644 --- a/frontend/dashboard/src/@types/user.ts +++ b/frontend/dashboard/src/@types/user.ts @@ -126,3 +126,29 @@ export type UserPost = { message: string; }[]; }; + +export type Role = { + id: number; + name: string; + guard_name: string; + permissions: number[] +}; + +export type Permisions = { + name: string; + guard_name: string; + id: number +}[] + +export type UserAccess = { + id: number; + username: string; + email: string; + person: Person; + role: Role; +} + +export type Person = { + name: string; +} + diff --git a/frontend/dashboard/src/layouts/dashboard/navbar/NavConfig.tsx b/frontend/dashboard/src/layouts/dashboard/navbar/NavConfig.tsx index df8ffd5d..fbb27ad7 100644 --- a/frontend/dashboard/src/layouts/dashboard/navbar/NavConfig.tsx +++ b/frontend/dashboard/src/layouts/dashboard/navbar/NavConfig.tsx @@ -1,5 +1,8 @@ +import React, { useEffect, useState } from 'react'; + // components import SvgIconStyle from '../../../components/SvgIconStyle'; +import axios from '@/utils/axios'; // ---------------------------------------------------------------------- @@ -19,8 +22,6 @@ export const accessGroup = { admin: ["/report/logs"] } - - const navConfig = [ // GENERAL // ---------------------------------------------------------------------- @@ -117,8 +118,8 @@ const navConfig = [ { title: 'USER MANAGEMENT', children: [ - { title: 'User Access', path: '/master/diagnosis' }, - { title: 'User Role', path: '/master/diagnosis' }, + { title: 'User Role', path: '/user-role' }, + { title: 'User Access', path: '/user-access' }, ], }, { diff --git a/frontend/dashboard/src/layouts/dashboard/navbar/NavbarVertical.tsx b/frontend/dashboard/src/layouts/dashboard/navbar/NavbarVertical.tsx index 6f7bd107..c9124f18 100644 --- a/frontend/dashboard/src/layouts/dashboard/navbar/NavbarVertical.tsx +++ b/frontend/dashboard/src/layouts/dashboard/navbar/NavbarVertical.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useLocation } from 'react-router-dom'; // @mui import { styled, useTheme } from '@mui/material/styles'; @@ -15,11 +15,12 @@ import Logo from '../../../components/Logo'; import Scrollbar from '../../../components/Scrollbar'; import { NavSectionVertical } from '../../../components/nav-section'; // -import navConfig from './NavConfig'; +// import navConfig from './NavConfig'; import NavbarDocs from './NavbarDocs'; import NavbarAccount from './NavbarAccount'; import CollapseButton from './CollapseButton'; import useAuth from '@/hooks/useAuth'; +import axios from '@/utils/axios'; // ---------------------------------------------------------------------- @@ -48,7 +49,49 @@ export default function NavbarVertical({ isOpenSidebar, onCloseSidebar }: Props) const isDesktop = useResponsive('up', 'lg'); const { isCollapse, collapseClick, collapseHover, onToggleCollapse, onHoverEnter, onHoverLeave } = - useCollapseDrawer(); + useCollapseDrawer(); + + const [navConfig, setNavConfig] = useState([]); + useEffect(() => { + const fetchNavConfig = async () => { + try { + const response = await axios.get('/navigations'); + const data = response.data.items; + // Ambil data izin pengguna dari user state atau API + const userPermissions = user.permissions.map(permission => permission.name); + + // Fungsi untuk memeriksa apakah pengguna memiliki izin untuk item tertentu + const hasPermission = (permission) => { + return userPermissions.includes(permission); + }; + // Filter data berdasarkan izin pengguna + const filteredNavConfig = data.map(section => { + if (section.children && section.children.length > 0) { + // Cek apakah ada satu atau lebih children yang memiliki izin + const filteredChildren = section.children.filter(child => hasPermission(child.permission)); + + if (filteredChildren.length > 0) { + return { + ...section, + children: filteredChildren + }; + } else { + return null; // Skip sections where no children have permissions + } + } + // Jika tidak ada children, cek izin untuk section itu sendiri + return hasPermission(section.permission) ? section : null; + }).filter(section => section !== null); + + setNavConfig([{ items: filteredNavConfig }]); + } catch (error) { + console.error('Failed to fetch nav config:', error); + } + }; + + fetchNavConfig(); + }, [user]); + useEffect(() => { if (isOpenSidebar) { @@ -57,15 +100,15 @@ export default function NavbarVertical({ isOpenSidebar, onCloseSidebar }: Props) // eslint-disable-next-line react-hooks/exhaustive-deps }, [pathname]); - const filteredItems = navConfig.filter(section => { - return section.items.some(item => { - return item.title === 'E-PRESCRIPTION'; - }); - }); + // const filteredItems = navConfig.filter(section => { + // return section.items.some(item => { + // return item.title === 'E-PRESCRIPTION'; + // }); + // }); - const filteredData = filteredItems.map(section => ({ - items: section.items.filter(item => item.title === "E-PRESCRIPTION") - })); + // const filteredData = filteredItems.map(section => ({ + // items: section.items.filter(item => item.title === "E-PRESCRIPTION") + // })); const renderContent = ( - + diff --git a/frontend/dashboard/src/pages/UserManagement/UserAccess/CreateUpdate.tsx b/frontend/dashboard/src/pages/UserManagement/UserAccess/CreateUpdate.tsx new file mode 100644 index 00000000..44498f35 --- /dev/null +++ b/frontend/dashboard/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/dashboard/src/pages/UserManagement/UserAccess/Form.tsx b/frontend/dashboard/src/pages/UserManagement/UserAccess/Form.tsx new file mode 100644 index 00000000..015f3cc4 --- /dev/null +++ b/frontend/dashboard/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/dashboard/src/pages/UserManagement/UserAccess/History.tsx b/frontend/dashboard/src/pages/UserManagement/UserAccess/History.tsx new file mode 100644 index 00000000..8d733ca7 --- /dev/null +++ b/frontend/dashboard/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/dashboard/src/pages/UserManagement/UserAccess/Index.tsx b/frontend/dashboard/src/pages/UserManagement/UserAccess/Index.tsx new file mode 100644 index 00000000..c69dedde --- /dev/null +++ b/frontend/dashboard/src/pages/UserManagement/UserAccess/Index.tsx @@ -0,0 +1,33 @@ +import { 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/dashboard/src/pages/UserManagement/UserAccess/List.tsx b/frontend/dashboard/src/pages/UserManagement/UserAccess/List.tsx new file mode 100644 index 00000000..5b532480 --- /dev/null +++ b/frontend/dashboard/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/dashboard/src/pages/UserManagement/UserRole/CreateUpdate.tsx b/frontend/dashboard/src/pages/UserManagement/UserRole/CreateUpdate.tsx new file mode 100644 index 00000000..5bf8d905 --- /dev/null +++ b/frontend/dashboard/src/pages/UserManagement/UserRole/CreateUpdate.tsx @@ -0,0 +1,81 @@ + +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); + }) + .catch((err) => { + if (err.response.status === 404) { + navigate('/404'); + } + }) + } + axios.get('/permission_list') + .then((res) => { + setPermissions(res.data); + }) + .catch((err) => { + if (err.response && err.response.status === 404) { + navigate('/404'); + } else { + console.error('Error fetching permissions:', err); + } + }); + + }, [corporate_id, id]); + + return ( + + + + + + ); +} diff --git a/frontend/dashboard/src/pages/UserManagement/UserRole/Form.tsx b/frontend/dashboard/src/pages/UserManagement/UserRole/Form.tsx new file mode 100644 index 00000000..80fc2f7f --- /dev/null +++ b/frontend/dashboard/src/pages/UserManagement/UserRole/Form.tsx @@ -0,0 +1,170 @@ +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 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)); + } + }; + + return ( + + + + + + + User Role + + + {guard_name_options.map((option, index) => ( + + ))} + + Permission + + {permissions?.map((permission, index) => ( + + handleCheckboxClick(permission.id, e.target.checked)} + /> + } + label={permission.name} + /> + + ))} + + + { isEdit? 'Update' : 'Create' } + + + + + + + + + ); +} diff --git a/frontend/dashboard/src/pages/UserManagement/UserRole/History.tsx b/frontend/dashboard/src/pages/UserManagement/UserRole/History.tsx new file mode 100644 index 00000000..8d733ca7 --- /dev/null +++ b/frontend/dashboard/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/dashboard/src/pages/UserManagement/UserRole/Index.tsx b/frontend/dashboard/src/pages/UserManagement/UserRole/Index.tsx new file mode 100644 index 00000000..83ce9b22 --- /dev/null +++ b/frontend/dashboard/src/pages/UserManagement/UserRole/Index.tsx @@ -0,0 +1,33 @@ +import { 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/dashboard/src/pages/UserManagement/UserRole/List.tsx b/frontend/dashboard/src/pages/UserManagement/UserRole/List.tsx new file mode 100644 index 00000000..a3bbf99e --- /dev/null +++ b/frontend/dashboard/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/dashboard/src/routes/index.tsx b/frontend/dashboard/src/routes/index.tsx index 2e1a40f0..f3681f67 100644 --- a/frontend/dashboard/src/routes/index.tsx +++ b/frontend/dashboard/src/routes/index.tsx @@ -523,6 +523,34 @@ export default function Router() { path: 'e-prescription/live-chat/:id/show', element: , }, + { + path: 'user-role', + element: , + }, + { + path: 'user-role/create', + element: , + }, + { + path: 'user/role/:id/edit', + element: , + }, + { + path: 'user-access', + element: , + }, + { + path: 'user-access/create', + element: , + }, + { + path: 'user/access/:id/edit', + element: , + }, + // { + // path: 'e-prescription/live-chat/:id/show', + // element: , + // }, ], }, // { @@ -653,9 +681,6 @@ const RequestLogDetail = Loadable(lazy(() => import('../pages/CustomerService/R const FinalLog = Loadable(lazy(() => import('../pages/CustomerService/FinalLog/Index'))) const FinalLogDetail = Loadable(lazy(() => import('../pages/CustomerService/FinalLog/Detail'))) - - - const MasterDiagnosisTemplate = Loadable(lazy(() => import('../pages/Master/Diagnosis/Master/Index'))); const MasterDiagnosisTemplateCreate = Loadable(lazy(() => import('../pages/Master/Diagnosis/Master/CreateUpdate'))); const MasterDiagnosisTemplateHistories = Loadable( @@ -733,3 +758,9 @@ const ClaimRequestsDetail = Loadable(lazy(() => import('../pages/ClaimRequests/D const Membership = Loadable(lazy(() => import('../pages/Service/Membership/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')));