From d706bf062302a3541b9ee4a05b1ffef38d932252 Mon Sep 17 00:00:00 2001 From: R Date: Tue, 14 Feb 2023 13:07:17 +0700 Subject: [PATCH] [WIP] Add Approve Button --- .../Api/ClaimRequestController.php | 97 +++ Modules/Internal/Routes/api.php | 4 + .../layouts/dashboard/navbar/NavConfig.tsx | 7 + .../src/pages/ClaimRequests/CreateUpdate.tsx | 64 ++ .../src/pages/ClaimRequests/Form.tsx | 596 ++++++++++++++++++ .../src/pages/ClaimRequests/Index.tsx | 30 + .../src/pages/ClaimRequests/List.tsx | 272 ++++++++ frontend/dashboard/src/routes/index.tsx | 6 + 8 files changed, 1076 insertions(+) create mode 100644 Modules/Internal/Http/Controllers/Api/ClaimRequestController.php create mode 100755 frontend/dashboard/src/pages/ClaimRequests/CreateUpdate.tsx create mode 100644 frontend/dashboard/src/pages/ClaimRequests/Form.tsx create mode 100755 frontend/dashboard/src/pages/ClaimRequests/Index.tsx create mode 100755 frontend/dashboard/src/pages/ClaimRequests/List.tsx diff --git a/Modules/Internal/Http/Controllers/Api/ClaimRequestController.php b/Modules/Internal/Http/Controllers/Api/ClaimRequestController.php new file mode 100644 index 00000000..0a0933bc --- /dev/null +++ b/Modules/Internal/Http/Controllers/Api/ClaimRequestController.php @@ -0,0 +1,97 @@ +with(['member']) + ->paginate(); + + return $claimRequests; + } + + /** + * 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) + { + // + } + + public function approve($id) + { + $claimRequest = ClaimRequest::findOrFail($id); + + $claimRequest->status = 'approved'; + $claimRequest->save(); + + // Generate LOG + + return $claimRequest; + } +} diff --git a/Modules/Internal/Routes/api.php b/Modules/Internal/Routes/api.php index 3c1a9447..a16798c2 100755 --- a/Modules/Internal/Routes/api.php +++ b/Modules/Internal/Routes/api.php @@ -5,6 +5,7 @@ use Modules\Internal\Http\Controllers\Api\AuthController; use Illuminate\Http\Request; use Modules\Internal\Http\Controllers\Api\BenefitController; use Modules\Internal\Http\Controllers\Api\ClaimController; +use Modules\Internal\Http\Controllers\Api\ClaimRequestController; use Modules\Internal\Http\Controllers\Api\CorporateBenefitController; use Modules\Internal\Http\Controllers\Api\CorporateController; use Modules\Internal\Http\Controllers\Api\CorporateFormulariumController; @@ -120,6 +121,9 @@ Route::prefix('internal')->group(function () { Route::get('search-specialities', [SpecialityController::class, 'searchSpeciality']); Route::resource('organizations', OrganizationController::class); Route::resource('doctors', DoctorController::class); + + Route::get('claim-requests', [ClaimRequestController::class, 'index'])->name('claim-requests.index'); + Route::post('claim-requests/{id}/approve', [ClaimRequestController::class, 'approve'])->name('claim-requests.approve'); }); // Route::resource('organizations', OrganizationController::class); diff --git a/frontend/dashboard/src/layouts/dashboard/navbar/NavConfig.tsx b/frontend/dashboard/src/layouts/dashboard/navbar/NavConfig.tsx index aa12a265..8450de8c 100755 --- a/frontend/dashboard/src/layouts/dashboard/navbar/NavConfig.tsx +++ b/frontend/dashboard/src/layouts/dashboard/navbar/NavConfig.tsx @@ -58,6 +58,13 @@ const navConfig = [ { title: 'Hospitals', path: '/hospitals' }, ], }, + { + title: 'LOG REQUEST', + path: '/claim-requests', + // children: [ + // { title: 'Request', path: '/case-request' }, + // ], + }, { title: 'CASE MANAGEMENT', path: '/claims', diff --git a/frontend/dashboard/src/pages/ClaimRequests/CreateUpdate.tsx b/frontend/dashboard/src/pages/ClaimRequests/CreateUpdate.tsx new file mode 100755 index 00000000..58bb43d6 --- /dev/null +++ b/frontend/dashboard/src/pages/ClaimRequests/CreateUpdate.tsx @@ -0,0 +1,64 @@ +import * as Yup from 'yup'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { Autocomplete, Button, Card, Collapse, Container, Divider, Grid, Stack, Table, TableBody, TableCell, TableRow, TextField, Typography } from '@mui/material'; +import { Controller, useForm } from 'react-hook-form'; +import { useParams, useNavigate } from 'react-router-dom'; +import HeaderBreadcrumbs from '../../components/HeaderBreadcrumbs'; +import { FormProvider, RHFCheckbox, RHFSelect, RHFTextField } from '../../components/hook-form'; +import Page from '../../components/Page'; +import useSettings from '../../hooks/useSettings'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import MemberSelectDialog from '../../components/dialogs/MemberSelectDialog'; +import { styled } from '@mui/system'; +import axios from '../../utils/axios'; +import { enqueueSnackbar } from 'notistack'; +import { LoadingButton } from '@mui/lab'; +import { fCurrency } from '../../utils/formatNumber'; +import Iconify from '../../components/Iconify'; +import Form from './Form'; + +export default function ClaimsCreateUpdate() { + + const { themeStretch } = useSettings(); + const { id } = useParams(); + + const isEdit = id ? true : false; + + const [currentClaim, setCurrentClaim] = useState(); + + useEffect(() => { + if (isEdit) { + axios.get('/claims/' + id).then((res) => { + // console.log('Yeet', res.data); + setCurrentClaim(res.data); + }); + } + }, [id]); + + + return ( + + + + + + +
+ + + ); +} diff --git a/frontend/dashboard/src/pages/ClaimRequests/Form.tsx b/frontend/dashboard/src/pages/ClaimRequests/Form.tsx new file mode 100644 index 00000000..a4898695 --- /dev/null +++ b/frontend/dashboard/src/pages/ClaimRequests/Form.tsx @@ -0,0 +1,596 @@ +import * as Yup from 'yup'; +import { useSnackbar } from 'notistack'; +import { useNavigate } from 'react-router-dom'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { Controller, useForm } from 'react-hook-form'; +import React, { useEffect, useMemo, useState } from 'react'; +import axios from '../../utils/axios'; +import { FormProvider, RHFTextField } from '../../components/hook-form'; +import { + Autocomplete, + Button, + Grid, + Stack, + Table, + TableBody, + TableCell, + TableRow, + TextField, + Typography, + useTheme, + List, + ListItem, + IconButton, + ListItemAvatar, + Avatar, + ListItemText, +} from '@mui/material'; +import Iconify from '../../components/Iconify'; +import { LoadingButton } from '@mui/lab'; +import { fCurrency } from '../../utils/formatNumber'; +import MemberSelectDialog from '../../components/dialogs/MemberSelectDialog'; +import { Add, DeleteOutline } from '@mui/icons-material'; + +type Props = { + isEdit: boolean; + currentClaim?: any; +}; + +export default function ClaimForm({ isEdit, currentClaim }: Props) { + const navigate = useNavigate(); + + const { enqueueSnackbar } = useSnackbar(); + + const NewCorporateSchema = Yup.object().shape({ + name: Yup.string().required('Name is required'), + code: Yup.string().required('Corporate Code is required'), + active: Yup.boolean().required('Corporate Status is required'), + // file: Yup.boolean().required('Corporate Status is required'), + }); + + const defaultValues = useMemo( + () => ({ + member: currentClaim?.member || {}, + member_id: currentClaim?.member_id || null, + diagnosis_id: currentClaim?.diagnosis_id || null, + total_claim: currentClaim?.total_claim || 0, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [currentClaim] + ); + + const methods = useForm({ + resolver: yupResolver(NewCorporateSchema), + defaultValues, + }); + const { + reset, + watch, + control, + setValue, + getValues, + setError, + handleSubmit, + formState: { isSubmitting }, + } = methods; + + const values = watch(); + + const [isCheckingLimit, setIsCheckingLimit] = useState(false); + const [isEligible, setIsEligible] = useState(false); + const [memberBenefits, setMemberBenefits] = useState([]); + const [diagnosisOption, setDiagnosisOption] = useState([]); + const [isMemberDialogOpen, setIsMemberDialogOpen] = useState(false); + const [member, setMember] = useState({}) + + + useEffect(() => { + console.log('defaultValues', defaultValues); + if (isEdit && currentClaim) { + reset(defaultValues); + setMember(defaultValues.member) + } + if (!isEdit) { + reset(defaultValues); + setMember(defaultValues.member) + } + }, [isEdit, currentClaim]); + + const fileSelected = (event, type) => { + const files = event.target.files; + const currentFiles = getValues(`uploaded_files.${type}`) ?? []; + + setValue(`uploaded_files.${type}`, [...currentFiles, ...files]); + + console.log('currentFiles', getValues('uploaded_files')); + }; + + const memberSelected = (member) => { + setMember(member) + }; + + const checkLimit = async () => { + console.log('CHECKING LIMIT'); + }; + + const onSubmit = async (data: any) => { + try { + if (!isEdit) { + const response = await axios.post('/claims', data); + } else { + const response = await axios.put('/claims/' + currentClaim?.id ?? '', data); + } + reset(); + enqueueSnackbar( + !isEdit ? 'Organizations Created Successfully!' : 'Organizations Udpated Successfully!', + { variant: 'success' } + ); + navigate('/claims'); + } catch (error: any) { + if (error && error.response.status === 422) { + for (const [key, value] of Object.entries(error.response.data.errors)) { + setError(key, { message: value[0] }); + enqueueSnackbar(value[0] ?? 'Failed Processing Request', { variant: 'error' }); + } + } else { + enqueueSnackbar(error.message ?? 'Failed Processing Request', { variant: 'error' }); + } + } + + const ascent = document?.querySelector('ascent'); + if (ascent != null) { + ascent.innerHTML = ''; + } + }; + + function generate(files, element: React.ReactElement) { + return files.map((value) => + React.cloneElement(element, { + key: value, + }) + ); + } + + const headStyle = {}; + return ( + + + Member + + + + { + if (!isEdit) setIsMemberDialogOpen(true); + if (isEdit) enqueueSnackbar('Cannot Change Member', { variant: 'error' }); + }} + /> + + {/* + + */} + + + {member?.id && ( + + + + + + + + Name + + {member?.full_name} + + + + DOB + + + {member?.birth_date} ({member?.age + ' years'}) + + + + + Marital Status + + {member?.marital_status} + + + + Record Type + + {member?.record_type} + + + + Principal ID + + + {member?.principal_id} ( + {member?.relation_with_principal}) + + + +
+
+ + + + + + Plan + + {member?.current_plan?.code} + + + + Active + + + {member?.current_plan?.start} -{' '} + {member?.current_plan?.end} (Active) + + + + + Corporate Limit + + + {fCurrency(0)} / {fCurrency(member?.current_plan?.limit_rules)} + + + + + Plan Usage + + + {fCurrency(0)} / {fCurrency(member?.current_plan?.limit_rules)} + + + +
+
+
+
+ )} + + ( + + option ? `#${option.id} (${option.code}) ${option.description}` : '' + } + value={value || ''} + onChange={(event: any, newValue: any) => { + setValue('benefit_id', newValue?.id); + onChange(newValue); + }} + renderInput={(params) => ( + { + // if (event.key === 'Enter') + // searchDiagnosis(event.target.value) + // }} + /> + )} + /> + )} + /> + + ( + (option ? `(${option.code}) ${option.name}` : '')} + value={value || ''} + onChange={(event: any, newValue: any) => { + setValue('diagnosis_id', newValue?.id); + // setValue('diagnosis', newValue) + onChange(newValue); + }} + renderInput={(params) => ( + { + if (event.key === 'Enter') searchDiagnosis(event.target.value); + }} + /> + )} + /> + )} + /> + + {isCheckingLimit && ( + + {/* Checking */} + + + + Please Wait, Checking Eligibilty + + + + )} + {false && isCheckingLimit == false && isEligible == null && ( + + {/* No Data Selected */} + + + + Please Select Diagnosis ! + + + + )} + {!isCheckingLimit && isEligible !== null && isEligible && ( + + {/* Eligible */} + + + + Diagnosis is Eligible + + + + 125.000.000 / 125.000.000 + + + )} + {!isCheckingLimit && isEligible !== null && !isEligible && ( + + {/* Not Eligible */} + {/* + + + Not Eligible + + + + 125.000.000 / 125.000.000 + */} + + )} + + + + {isEdit && ( + + Documents + + + {(getValues('uploaded_files.invoice') && getValues('uploaded_files.invoice').length + ? getValues('uploaded_files.invoice') + : [] + ).map((file, index) => ( + + + + } + > + + + {/* */} + I + + + + + ))} + + + + {(getValues('uploaded_files.prescription') && getValues('uploaded_files.prescription').length + ? getValues('uploaded_files.prescription') + : [] + ).map((file, index) => ( + + + + } + > + + + {/* */} + P + + + + + ))} + + + + + {(getValues('uploaded_files.diagnosis') && getValues('uploaded_files.diagnosis').length + ? getValues('uploaded_files.diagnosis') + : [] + ).map((file, index) => ( + + + + } + > + + + {/* */} + DR + + + + + ))} + + + + )} + + {isEligible === true ? ( + + Create Claim + + ) : ( + + Check Limit + + )} +
+ + +
+ ); +} diff --git a/frontend/dashboard/src/pages/ClaimRequests/Index.tsx b/frontend/dashboard/src/pages/ClaimRequests/Index.tsx new file mode 100755 index 00000000..be18d2b3 --- /dev/null +++ b/frontend/dashboard/src/pages/ClaimRequests/Index.tsx @@ -0,0 +1,30 @@ +import { Card, Stack } from "@mui/material"; +import HeaderBreadcrumbs from "../../components/HeaderBreadcrumbs"; +import Page from "../../components/Page"; +import List from "./List"; + + + +export default function Claims() { + + const pageTitle = 'Claim Request'; + return ( + + + + + {/* */} + + {/* */} + + ); +} diff --git a/frontend/dashboard/src/pages/ClaimRequests/List.tsx b/frontend/dashboard/src/pages/ClaimRequests/List.tsx new file mode 100755 index 00000000..981e5223 --- /dev/null +++ b/frontend/dashboard/src/pages/ClaimRequests/List.tsx @@ -0,0 +1,272 @@ +// @mui +import { + Box, + Button, + Card, + Collapse, + IconButton, + MenuItem, + Table, + TableBody, + TableCell, + TableRow, + TextField, + Typography, + Stack, + Menu, + ButtonGroup, + Link, + Chip, +} 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'; +// hooks +import React, { ChangeEvent, useEffect, useRef, useState } from 'react'; +import { Navigate, useNavigate, useSearchParams } from 'react-router-dom'; +// components +import axios from '../../utils/axios'; +import { LaravelPaginatedData, LaravelPaginatedDataDefault } from '../../@types/paginated-data'; +import DataTable from '../../components/LaravelTable'; +import { fCurrency } from '../../utils/formatNumber'; +import EditRoundedIcon from '@mui/icons-material/EditRounded'; +import { LoadingButton } from '@mui/lab'; +import { enqueueSnackbar } from 'notistack'; +// import LoadingButton from '@/theme/overrides/LoadingButton'; + +export default function List() { + const [searchParams, setSearchParams] = useSearchParams(); + const [importResult, setImportResult] = useState(null); + const navigate = useNavigate(); + + 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({ search: searchText }); // Trigger to Parent + }; + + useEffect(() => { + // Trigger First Search + setSearchText(searchParams.get('search') ?? ''); + }, []); + + 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 handleClose = () => { + setAnchorEl(null); + }; + + return ( +
+ + + {/* */} + +
+ ); + } + + // Dummy Default Data + const [dataTableIsLoading, setDataTableLoading] = useState(true); + const [dataTableData, setDataTableData] = useState( + LaravelPaginatedDataDefault + ); + + const loadDataTableData = async (appliedFilter: any | null = null) => { + setDataTableLoading(true); + const filter = appliedFilter ? appliedFilter : Object.fromEntries([...searchParams.entries()]); + const response = await axios.get('/claim-requests', { params: filter }); + // console.log(response.data); + setDataTableLoading(false); + + setDataTableData(response.data); + }; + + const applyFilter = async (searchFilter: { search: string }) => { + await loadDataTableData(searchFilter); + setSearchParams(searchFilter); + }; + + const handlePageChange = (event: ChangeEvent, value: number): void => { + const filter = Object.fromEntries([...searchParams.entries(), ['page', value]]); + loadDataTableData(filter); + setSearchParams(filter); + }; + + const handleApprove = (claimRequest) => { + axios.post(`claim-requests/${claimRequest.id}/approve`) + .then((response) => { + enqueueSnackbar('Success Approve', {variant: 'success'}) + loadDataTableData() + }) + .catch(({response}) => { + enqueueSnackbar(response.data.message ?? 'Something went wrong!', {variant : "error"}) + }) + } + + useEffect(() => { + loadDataTableData(); + }, []); + + const headStyle = { + fontWeight: 'bold', + }; + + // Called on every row to map the data to the columns + function createData(data: any): any { + return { + ...data, + }; + } + + { + /* ------------------ TABLE ROW ------------------ */ + } + function Row(props: { row: ReturnType }) { + const { row } = props; + const [open, setOpen] = React.useState(false); + const [loadingApprove, setLoadingApprove] = React.useState(false); + + return ( + + *': { borderBottom: 'unset' } }}> + + setOpen(!open)}> + {open ? : } + + + {row.code} + {row.member?.full_name} + {row.submission_date} + + { row.status == 'requested' && ( {handleApprove(row)}}>Approve )} + + {/* COLLAPSIBLE ROW */} + + + + + + Description : {row.description} + + + + + + + ); + } + { + /* ------------------ END TABLE ROW ------------------ */ + } + + function TableContent() { + return ( + + {/* ------------------ TABLE HEADER ------------------ */} + + + + + Code + + + Member Name + + + Submission Date + + + Status + + + Action + + + + {/* ------------------ END TABLE HEADER ------------------ */} + + {/* ------------------ TABLE ROW ------------------ */} + {dataTableIsLoading ? ( + + + + Loading + + + + ) : dataTableData.data.length === 0 ? ( + + + + No Data + + + + ) : ( + + {dataTableData.data.map((row) => ( + + ))} + + )} + {/* ------------------ END TABLE ROW ------------------ */} +
+ ); + } + + return ( + + + + } + /> + + ); +} diff --git a/frontend/dashboard/src/routes/index.tsx b/frontend/dashboard/src/routes/index.tsx index bd77a22d..6c949788 100755 --- a/frontend/dashboard/src/routes/index.tsx +++ b/frontend/dashboard/src/routes/index.tsx @@ -221,6 +221,10 @@ export default function Router() { path: 'claims', element: , }, + { + path: 'claim-requests', + element: , + }, { path: 'claims/create', element: , @@ -339,3 +343,5 @@ const Profile = Loadable(lazy(() => import('../pages/Profile/Index'))); const Claims = Loadable(lazy(() => import('../pages/Claims/Index'))); const ClaimsCreate = Loadable(lazy(() => import('../pages/Claims/CreateUpdate'))); + +const ClaimRequests = Loadable(lazy(() => import('../pages/ClaimRequests/Index'))); \ No newline at end of file