diff --git a/Modules/Client/Http/Controllers/Api/ClaimReportController.php b/Modules/Client/Http/Controllers/Api/ClaimReportController.php index 2ced9d97..e8b3967e 100644 --- a/Modules/Client/Http/Controllers/Api/ClaimReportController.php +++ b/Modules/Client/Http/Controllers/Api/ClaimReportController.php @@ -124,6 +124,13 @@ class ClaimReportController extends Controller ->where('claim_request_files.claim_request_id', '=', $claimRequestId) ->get(); $results['request_files'] = $request_files; + $documents = DB::table('files') + ->where('fileable_type', 'App\Models\ClaimRequest') + ->where('fileable_id', $claimRequestId) + ->select('original_name', \DB::raw("CONCAT('" . env('APP_URL') . "/storage/', path) as path"), 'type') + ->orderBy('id', 'desc') + ->get(); + $results['documents'] = $documents; return Helper::responseJson($results); } diff --git a/Modules/Internal/Http/Controllers/Api/ClaimRequestController.php b/Modules/Internal/Http/Controllers/Api/ClaimRequestController.php index 10e4ada3..88711980 100644 --- a/Modules/Internal/Http/Controllers/Api/ClaimRequestController.php +++ b/Modules/Internal/Http/Controllers/Api/ClaimRequestController.php @@ -15,9 +15,10 @@ use Modules\Internal\Transformers\ClaimRequestShowResource; use Illuminate\Support\Facades\Storage; use App\Services\ClaimRequestService; use App\Exceptions\ImportRowException; - use App\Models\File; use App\Models\FilesMcu; +use Illuminate\Support\Facades\DB; +use App\Models\Member; class ClaimRequestController extends Controller { @@ -336,4 +337,111 @@ class ClaimRequestController extends Controller ] ]; } + + public function claimRequestDetail($claimRequestId) + { + $status = DB::table('claim_requests') + ->leftJoin('claims', 'claim_requests.id', '=', 'claims.claim_request_id') + ->leftJoin('members', 'claim_requests.member_id', '=', 'members.id') + ->leftJoin('corporate_employees', 'members.id', '=', 'corporate_employees.member_id') + ->leftJoin('corporate_divisions', 'corporate_employees.division_id', '=', 'corporate_divisions.id') + ->where('claim_requests.id', '=', $claimRequestId) + ->select( + 'claim_requests.submission_date', + DB::raw(' + CASE + WHEN claim_requests.status = "requested" THEN "requested" + WHEN claim_requests.status = "approved" AND claims.status = "approved" THEN "approved" + WHEN claim_requests.status = "approved" AND claims.status = "declined" THEN "declined" + WHEN claim_requests.status = "approved" AND claims.status = "disbrusmented" THEN "disbrusmented" + /*WHEN claim_requests.status = "approved" AND claims.status = "received" THEN "pending"*/ + WHEN claim_requests.status = "approved" AND claims.status = "received" THEN "reviewed" + ELSE "" + END AS status + ') + ) + ->first(); + $results['status'] = $status; + $timeline = DB::table('claim_logs') + ->where('claim_logs.claim_request_id', '=', $claimRequestId) + ->select( + DB::raw(' + CASE + WHEN claim_logs.status = "requested" THEN "Request" + WHEN claim_logs.status = "reviewed" THEN "Review" + WHEN claim_logs.status = "approved" THEN "Approval" + ELSE "-" + END AS txt_status + '), + DB::raw(' + CASE + WHEN claim_logs.status = "requested" THEN "#159C9C" + WHEN claim_logs.status = "reviewed" THEN "#0C53B7" + WHEN claim_logs.status = "approved" THEN "#229A16" + ELSE "-" + END AS txt_status_color + '), + DB::raw(' + CASE + WHEN claim_logs.status = "requested" THEN "#00AB5529" + WHEN claim_logs.status = "reviewed" THEN "#1890FF29" + WHEN claim_logs.status = "approved" THEN "#54D62C29" + ELSE "-" + END AS txt_status_backgroundColor + '), + 'claim_logs.date', + 'claim_logs.description', + 'claim_logs.status' + ) + ->orderBy('claim_logs.id', 'desc') + ->get(); + $results['timeline'] = $timeline; + $request_files = DB::table('claim_request_files') + ->where('claim_request_files.claim_request_id', '=', $claimRequestId) + ->get(); + $results['request_files'] = $request_files; + $documents = DB::table('files') + ->where('fileable_type', 'App\Models\ClaimRequest') + ->where('fileable_id', $claimRequestId) + ->select('original_name', \DB::raw("CONCAT('" . env('APP_URL') . "/storage/', path) as path"), 'type') + ->orderBy('id', 'desc') + ->get(); + $results['documents'] = $documents; + $dialog_submits = DB::table('claim_requests') + ->leftJoin('members', 'claim_requests.member_id','=', 'members.id') + ->where('claim_requests.id', $claimRequestId) + ->select('claim_requests.code', 'members.name', 'claim_requests.submission_date', 'claim_requests.service_code','claim_requests.status') + ->first(); + $results['dialog_submits'] = $dialog_submits; + + return Helper::responseJson($results); + } + + public function invoiceFiles(Request $request, $claim_id) + { + if ($request->hasFile('invoice_files')) { + foreach ($request->invoice_files as $file) { + $pathFile = File::storeFile('claim-invoice', $claim_id, $file); + File::updateOrCreate([ + 'fileable_type'=>'App\Models\ClaimRequest', + 'fileable_id' => $claim_id, + 'type' => 'claim-invoice', + 'name' => File::getFileName('claim-invoice', $claim_id, $file), + 'original_name' => $file->getClientOriginalName(), + 'extension' => $file->getClientOriginalExtension(), + 'path' => $pathFile, + 'created_by' => auth()->user()->id, + 'updated_by' => auth()->user()->id, + ]); + } + } + if($request->date) + { + DB::table('claim_requests') + ->where('id', $claim_id) + ->update(['invoice_date' => $request->date]); + + } + return Helper::responseJson(data: $request->toArray(), message: 'Invoice Success Uploaded'); + } } diff --git a/Modules/Internal/Routes/api.php b/Modules/Internal/Routes/api.php index 71dddf4d..6c664350 100644 --- a/Modules/Internal/Routes/api.php +++ b/Modules/Internal/Routes/api.php @@ -218,6 +218,8 @@ Route::prefix('internal')->group(function () { Route::get('claim-requests/{id}', [ClaimRequestController::class, 'show'])->name('claim-requests.show'); Route::put('claim-requests/{id}', [ClaimRequestController::class, 'update'])->name('claim-requests.update'); Route::post('claim-requests/import', [ClaimRequestController::class, 'importClaim'])->name('claim-requests.importClaim'); + Route::get('claim-requests/detail/{id}', [ClaimRequestController::class, 'claimRequestDetail']); + Route::post('claim-requests/{id}/invoice-files', [ClaimRequestController::class, 'invoiceFiles']); }); Route::get('province', [ProvinceController::class, 'index']); diff --git a/app/Models/File.php b/app/Models/File.php index 6bf0952f..f172de82 100644 --- a/app/Models/File.php +++ b/app/Models/File.php @@ -43,6 +43,7 @@ class File extends Model 'claim-result' => 'claim/', 'claim-diagnosis' => 'claim/', 'claim-kondisi' => 'claim/', + 'claim-invoice' => 'claim/', 'docs' => 'docs/', ]; diff --git a/database/migrations/2023_10_26_085839_add_column_invoice_date_to_claim_requests.php b/database/migrations/2023_10_26_085839_add_column_invoice_date_to_claim_requests.php new file mode 100644 index 00000000..aca5eb06 --- /dev/null +++ b/database/migrations/2023_10_26_085839_add_column_invoice_date_to_claim_requests.php @@ -0,0 +1,32 @@ +dateTime('invoice_date')->nullable()->after('claim_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('claim_requests', function (Blueprint $table) { + $table->dropColumn('invoice_date'); + }); + } +}; diff --git a/frontend/client-portal/src/pages/ClaimReport/DetailTimeline.tsx b/frontend/client-portal/src/pages/ClaimReport/DetailTimeline.tsx index 1e93f302..b058df06 100644 --- a/frontend/client-portal/src/pages/ClaimReport/DetailTimeline.tsx +++ b/frontend/client-portal/src/pages/ClaimReport/DetailTimeline.tsx @@ -13,6 +13,8 @@ import AddIcon from '@mui/icons-material/Add'; import Iconify from '../../components/Iconify'; import { useEffect, useState } from 'react'; import { format } from 'date-fns'; +import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; +import DescriptionIcon from '@mui/icons-material/Description'; const Item1 = styled(Paper)(({ theme }) => ({ ...theme.typography.body2, @@ -37,10 +39,12 @@ const Item2 = styled(Paper)(({ theme }) => ({ export default function NoOppositeContent({data}) { const [timeline, setTimeline] = useState(null); const [requestFile, setRequestFile] = useState(null); + const [document, setDocument] = useState(null); useEffect(() => { if (data && data.data) { setTimeline(data.data.timeline); setRequestFile(data.data.request_files); + setDocument(data.data.documents); } }, [data]); @@ -65,7 +69,7 @@ export default function NoOppositeContent({data}) { - + {dataTimeline.date ? format(new Date(dataTimeline.date), "HH : ii") : ''} {dataTimeline.txt_status} @@ -81,11 +85,13 @@ export default function NoOppositeContent({data}) { @@ -95,6 +101,43 @@ export default function NoOppositeContent({data}) { ) : ''} + {dataTimeline.status === 'requested' ? ( + + + + + Documents + + + {document?.map((dataDocument, index) => ( + + + {dataDocument.type === 'claim-diagnosis' ? + 'Diagnosis' + : dataDocument.type === 'claim-kondisi' ? + 'Condition' + : dataDocument.type === 'claim-result' ? + 'Supporting Result' + : dataDocument.type === 'claim-invoice' ? + 'Invoice' + : ''} + + + + + {dataDocument.original_name ? dataDocument.original_name : '-'} + + + + ))} + + + + ) : ''} diff --git a/frontend/dashboard/src/pages/ClaimRequests/Detail.tsx b/frontend/dashboard/src/pages/ClaimRequests/Detail.tsx new file mode 100644 index 00000000..b4300ac6 --- /dev/null +++ b/frontend/dashboard/src/pages/ClaimRequests/Detail.tsx @@ -0,0 +1,300 @@ +// mui +import { Container, Grid, Stack, Typography, Card, TextField, Divider, ButtonBase, Box, IconButton } from '@mui/material'; +// components +import Page from '../../components/Page'; +// utils +import useSettings from '../../hooks/useSettings'; +// react +import { useNavigate, useParams, useLocation } from 'react-router-dom'; +import { useEffect, useState, useRef } from 'react'; +import axios from '../../utils/axios'; +// pages +import DetailTimeline from '../../pages/ClaimRequests/DetailTimeline'; +import DetailStepper from '../../pages/ClaimRequests/DetailStepper'; +import { format } from 'date-fns'; +import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; +import Button from '@mui/material/Button'; +import AddIcon from '@mui/icons-material/Add'; +import RemoveIcon from '@mui/icons-material/Remove'; +import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import Iconify from '@/components/Iconify'; +import { fPostFormat } from '@/utils/formatTime'; +import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; +import DownloadIcon from '@mui/icons-material/Download'; +import { Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import { fDateTimesecond } from '@/utils/formatTime'; +import { makeFormData } from '@/utils/jsonToFormData'; + +import { enqueueSnackbar } from 'notistack'; + +// ---------------------------------------------------------------------- + +export default function UserProfile() { + const location = useLocation(); + const queryParams = new URLSearchParams(location.search); + const code = queryParams.get('code'); + + const navigate = useNavigate(); + const { themeStretch } = useSettings(); + const [data, setData] = useState(); + const [dataDialog, setDataDialog] = useState(); + + const { id } = useParams(); + + useEffect(() => { + axios + .get('/claim-requests/detail/'+id) + .then((response) => { + setData(response.data); + setDataDialog(response.data.data.dialog_submits); + + }) + .catch((error) => { + console.error(error); + }); + + }, []); + + const [isInvoiceVisible, setInvoiceVisibility] = useState(false); + + const handleInvoice = () => { + setInvoiceVisibility(!isInvoiceVisible); + } + const currentDate = new Date(); + const formattedCurrentDate = format(currentDate, 'dd MMM yyyy'); + const [dateInvoice, setDateInvoice] = useState(currentDate); + + const fileInvoiceInput = useRef(null); + const [fileInvoices, setFileInvoices] = useState([]); + + const handleInvoiceInputChange = (event) => { + if (event.target.files[0]) { + setFileInvoices([...fileInvoices, ...event.target.files]); + } else { + console.log('NO FILE'); + } + }; + const removeInvoiceFiles = (filesState, index) => { + setFileInvoices( + filesState.filter((file, fileIndex) => { + return fileIndex != index; + }) + ); + }; + const date = dateInvoice ? fPostFormat(dateInvoice, 'yyyy-MM-dd') : null; + + const [openDialogSubmit, setOpenDialogSubmit] = useState(false); + const handleCloseDialogSubmit = () => { + setOpenDialogSubmit(false); + } + const handleSubmitData = () => { + if(fileInvoices.length > 0) + { + //submit data + axios + .post('claim-requests/'+id+'/approve') + .then((response) => { + enqueueSnackbar('Success Submit Claim Request', { variant: 'success' }); + setOpenDialogSubmit(false); + }) + .catch(({ response }) => { + enqueueSnackbar(response.data.message ?? 'Something went wrong!', { variant: 'error' }); + }); + //Upload file invoices + const formData = makeFormData({ + date:date, + invoice_files: fileInvoices, + }); + axios + .post('claim-requests/'+id+'/invoice-files', formData) + .then((response) => { + enqueueSnackbar(response.data.message ?? 'Success upload invoice', { variant: 'success' }); + }) + .catch(({ response }) => { + enqueueSnackbar(response.data.message ?? 'Something Went Wrong', { variant: 'error' }); + }); + } + else + { + enqueueSnackbar('Please upload file invoice, before submit', { variant: 'error' }); + } + + setTimeout(() => + { + window.location.reload(); + }, 5000); + + }; + + return ( + + + + navigate(-1)} sx={{cursor:'pointer'}}/> + {code} + {data ? ( + + Submission Date + {(data && data.data) ? format(new Date(data.data.status.submission_date), "d MMM yyyy") : ''} + + ) : ''} + + {data ? ( + + + + + + + Format Claim + + + + + + Request Claim + + + + + + + + { + setDateInvoice(newValue); + }} + inputFormat="dd MMM yyyy" + renderInput={(params) => } + /> + + } + spacing={1} + sx={{ marginY: 2 }} + > + {fileInvoices && + fileInvoices.map((file, index) => ( + + + + {file.name ? file.name : '-'} + + { + removeInvoiceFiles(fileInvoices, index); + }} + sx={{cursor: 'pointer'}} + > + + ))} + + fileInvoiceInput.current?.click()}> + + + + Upload Invoice + + + + + + + + + + + + + {dataDialog && dataDialog.status === 'requested' ? ( + <> + + + + ) : ''} + {/* Dialog Submits */} + + + + + Confirmation + + + + + + + + {dataDialog ? ( + + Are you sure to submit this claim ? + + + Code + {dataDialog.code} + + + Name + {dataDialog.name} + + + Date Submission + {fDateTimesecond(dataDialog.submission_date)} + + + Claim Method + Service Type + + + Service Type + + {dataDialog.service_code === 'IP' ? 'Inpatient' : 'Outpatient'} + + + + + ) : ''} + + + + + + + + + + ) : ''} + + + ); +} \ No newline at end of file diff --git a/frontend/dashboard/src/pages/ClaimRequests/DetailStepper.tsx b/frontend/dashboard/src/pages/ClaimRequests/DetailStepper.tsx new file mode 100644 index 00000000..b788e29f --- /dev/null +++ b/frontend/dashboard/src/pages/ClaimRequests/DetailStepper.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stepper from '@mui/material/Stepper'; +import Step from '@mui/material/Step'; +import StepLabel from '@mui/material/StepLabel'; +import { useEffect, useState } from 'react'; +import ClearIcon from '@mui/icons-material/Clear'; + +const steps = [ + 'Request', + 'Review', + 'Approval', + 'Decline', + ]; + + export default function HorizontalLinearAlternativeLabelStepper({data}) { + const [active, setActive] = useState(0); + const [status, SetStatus] = useState(null); + let updatedSteps = [...steps]; + useEffect(() => { + if (data && data.data) { + if (data.data.status.status === 'requested') { + setActive(1); + updatedSteps = updatedSteps.filter(step => step !== 'Decline'); + } + else if (data.data.status.status === 'reviewed') { + setActive(2); + updatedSteps = updatedSteps.filter(step => step !== 'Decline'); + } + else if (data.data.status.status === 'approved') + { + setActive(3); + updatedSteps = updatedSteps.filter(step => step !== 'Decline'); + } + else if(data.data.status.status === 'declined') + { + setActive(4) + updatedSteps = updatedSteps.filter(step => step !== 'Approval'); + } + } + SetStatus(updatedSteps); + }, [data]); + + + + + return ( + + + {status?.map((label) => ( + + : ''}>{label} + + ))} + + + ); +} diff --git a/frontend/dashboard/src/pages/ClaimRequests/DetailTimeline.tsx b/frontend/dashboard/src/pages/ClaimRequests/DetailTimeline.tsx new file mode 100644 index 00000000..b058df06 --- /dev/null +++ b/frontend/dashboard/src/pages/ClaimRequests/DetailTimeline.tsx @@ -0,0 +1,147 @@ +import * as React from 'react'; +import Timeline from '@mui/lab/Timeline'; +import TimelineItem, { timelineItemClasses } from '@mui/lab/TimelineItem'; +import TimelineSeparator from '@mui/lab/TimelineSeparator'; +import TimelineConnector from '@mui/lab/TimelineConnector'; +import TimelineContent from '@mui/lab/TimelineContent'; +import TimelineDot from '@mui/lab/TimelineDot'; +import {Typography, Card, Stack} from '@mui/material'; +import { styled } from '@mui/material/styles'; +import Paper from '@mui/material/Paper'; +import Button from '@mui/material/Button'; +import AddIcon from '@mui/icons-material/Add'; +import Iconify from '../../components/Iconify'; +import { useEffect, useState } from 'react'; +import { format } from 'date-fns'; +import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; +import DescriptionIcon from '@mui/icons-material/Description'; + +const Item1 = styled(Paper)(({ theme }) => ({ + ...theme.typography.body2, + padding: theme.spacing(1), + textAlign: 'center', + backgroundColor: '#919EAB29', + color: '#637381', + width: 'fit-content', + marginRight: 'auto', +})); + +const Item2 = styled(Paper)(({ theme }) => ({ + backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + ...theme.typography.body2, + padding: theme.spacing(1), + textAlign: 'center', + color: theme.palette.text.secondary, + width: 'fit-content', + marginLeft: 'auto', +})); + +export default function NoOppositeContent({data}) { + const [timeline, setTimeline] = useState(null); + const [requestFile, setRequestFile] = useState(null); + const [document, setDocument] = useState(null); + useEffect(() => { + if (data && data.data) { + setTimeline(data.data.timeline); + setRequestFile(data.data.request_files); + setDocument(data.data.documents); + } + + }, [data]); + return ( + <> + {timeline?.map((dataTimeline, index) => ( + + {dataTimeline.date ? format(new Date(dataTimeline.date), "d MMM yyyy") : ''} + + + + + + + + + + {dataTimeline.date ? format(new Date(dataTimeline.date), "HH : ii") : ''} + {dataTimeline.txt_status} + + + Detail: + {dataTimeline.description} + + {dataTimeline.status === 'reviewed' && requestFile ? ( + <> + {requestFile?.map((dataRequestFile, index) => ( + + {dataRequestFile.description} + + + ))} + + ) : ''} + + + {dataTimeline.status === 'requested' ? ( + + + + + Documents + + + {document?.map((dataDocument, index) => ( + + + {dataDocument.type === 'claim-diagnosis' ? + 'Diagnosis' + : dataDocument.type === 'claim-kondisi' ? + 'Condition' + : dataDocument.type === 'claim-result' ? + 'Supporting Result' + : dataDocument.type === 'claim-invoice' ? + 'Invoice' + : ''} + + + + + {dataDocument.original_name ? dataDocument.original_name : '-'} + + + + ))} + + + + ) : ''} + + + + ))} + + ); +} diff --git a/frontend/dashboard/src/pages/ClaimRequests/List.tsx b/frontend/dashboard/src/pages/ClaimRequests/List.tsx index 576ad6fa..852f3cb9 100644 --- a/frontend/dashboard/src/pages/ClaimRequests/List.tsx +++ b/frontend/dashboard/src/pages/ClaimRequests/List.tsx @@ -357,7 +357,7 @@ export default function List() { Edit - ''}> + navigate ('/claim-requests/detail/'+row.id+'/?code='+row.code)}> Detail diff --git a/frontend/dashboard/src/pages/Corporates/Hospital/List.tsx b/frontend/dashboard/src/pages/Corporates/Hospital/List.tsx index 1ad86b41..4f6b1ffb 100644 --- a/frontend/dashboard/src/pages/Corporates/Hospital/List.tsx +++ b/frontend/dashboard/src/pages/Corporates/Hospital/List.tsx @@ -350,11 +350,11 @@ export default function HospitalList() { Code - {nameField} + {codeField} Hospital Name - {codeField} + {nameField} diff --git a/frontend/dashboard/src/routes/index.tsx b/frontend/dashboard/src/routes/index.tsx index 75a4c793..2dbaec4e 100644 --- a/frontend/dashboard/src/routes/index.tsx +++ b/frontend/dashboard/src/routes/index.tsx @@ -381,6 +381,10 @@ export default function Router() { path: 'claim-requests/edit/:id', element: , }, + { + path: 'claim-requests/detail/:id', + element: , + }, { path: 'claims/create', element: , @@ -559,6 +563,7 @@ const ClaimShow = Loadable(lazy(() => import('../pages/Claims/Show'))); const ClaimRequests = Loadable(lazy(() => import('../pages/ClaimRequests/Index'))); const ClaimRequestsCreate = Loadable(lazy(() => import('../pages/ClaimRequests/CreateUpdate'))); +const ClaimRequestsDetail = Loadable(lazy(() => import('../pages/ClaimRequests/Detail'))); const Membership = Loadable(lazy(() => import('../pages/Service/Membership/index'))); diff --git a/frontend/hospital-portal/src/sections/dashboard/FormRequestClaim.tsx b/frontend/hospital-portal/src/sections/dashboard/FormRequestClaim.tsx index b946a6f5..cdb781b4 100644 --- a/frontend/hospital-portal/src/sections/dashboard/FormRequestClaim.tsx +++ b/frontend/hospital-portal/src/sections/dashboard/FormRequestClaim.tsx @@ -234,7 +234,7 @@ export default function FormRequestClaim({ member, handleSubmitSuccess }) { {/* -------------------------------Upload Dokumen Diagnosa------------------------------- */} - Diagnosis Dokumen + Diagnosis Dokument {/* Hasil Lab, */}