From f7d8759a767cab99e4df2f0b49130a587ab5f2f5 Mon Sep 17 00:00:00 2001 From: R Date: Wed, 23 Nov 2022 11:39:17 +0700 Subject: [PATCH 1/7] Fix Member Plan --- .../Http/Controllers/Api/MemberController.php | 5 +- .../Services/MemberEnrollmentService.php | 94 ++++-- app/Models/Member.php | 12 +- .../dashboard/src/@types/paginated-data.ts | 15 + .../dashboard/src/pages/Claims/Create.tsx | 241 ++++++++++++++ frontend/dashboard/src/pages/Claims/Index.tsx | 29 ++ .../src/pages/Claims/LaravelTable.tsx | 26 ++ frontend/dashboard/src/pages/Claims/List.tsx | 303 ++++++++++++++++++ .../src/pages/Corporates/Member/List.tsx | 2 +- frontend/dashboard/src/routes/index.tsx | 8 + 10 files changed, 707 insertions(+), 28 deletions(-) create mode 100755 frontend/dashboard/src/pages/Claims/Create.tsx create mode 100755 frontend/dashboard/src/pages/Claims/Index.tsx create mode 100644 frontend/dashboard/src/pages/Claims/LaravelTable.tsx create mode 100755 frontend/dashboard/src/pages/Claims/List.tsx diff --git a/Modules/Internal/Http/Controllers/Api/MemberController.php b/Modules/Internal/Http/Controllers/Api/MemberController.php index fe178a21..c31d7c0b 100755 --- a/Modules/Internal/Http/Controllers/Api/MemberController.php +++ b/Modules/Internal/Http/Controllers/Api/MemberController.php @@ -26,7 +26,7 @@ class MemberController extends Controller */ public function index(Request $request, $corporate_id) { - $benefits = Member::query() + $members = Member::query() ->filter($request->all()) // ->where('corporate_id', $corporate_id) ->whereHas('employeds', function ($employeds) use ($corporate_id) { @@ -40,10 +40,11 @@ class MemberController extends Controller }); } ]) + ->with('currentPlan') ->paginate() ->appends($request->all()); - return $benefits; + return $members; } /** diff --git a/Modules/Internal/Services/MemberEnrollmentService.php b/Modules/Internal/Services/MemberEnrollmentService.php index 011ea50f..fc45e427 100755 --- a/Modules/Internal/Services/MemberEnrollmentService.php +++ b/Modules/Internal/Services/MemberEnrollmentService.php @@ -423,6 +423,15 @@ class MemberEnrollmentService ]), 0, null, $row); } + // Validate If Plan Exist + // TODO validate corporate plan + $plan = Plan::query() + ->where('code', $row['plan_id']) + ->first(); + if (!$plan) { + throw new ImportRowException(__('enrollment.PLAN_NOT_FOUND'), 0, null, $row); + } + $this->validateRow($row); try { @@ -450,6 +459,13 @@ class MemberEnrollmentService 'nik' => $row['nik'], 'status' => $row['employment_status'] ]); + + $member->memberPlans()->create([ + 'plan_id' => $plan->code, + 'status' => 'active', + 'start' => Carbon::parse(strtotime($row['member_effective_date'])), + 'end' => Carbon::parse(strtotime($row['member_expiry_date'])), + ]); } DB::commit(); } catch (\Exception $e) { @@ -458,35 +474,65 @@ class MemberEnrollmentService } break; case "2": // Member Information Update (Without Replacement Card) - $memberPolicy = MemberPolicy::query() - ->where('policy_id', $row['policy_number']) - ->where('member_id', $row['member_id']) - ->with('member') - ->first(); + $member = Member::query() + ->where('member_id', $row['member_id']) + ->first(); + + // Validate If Exist Member + if (!$member) { + throw new ImportRowException(__('enrollment.MEMBER_NOT_FOUND', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } - if (!$memberPolicy) { - throw new ImportRowException(__('enrollment.MEMBER_NOT_EXISTS', [ - 'member_id' => $row['member_id'], - 'policy_id' => $row['policy_number'] - ]), 0, null, $row); + try { + $memberPolicy = MemberPolicy::query() + ->where('policy_id', $row['policy_number']) + ->where('member_id', $row['member_id']) + ->with('member') + ->first(); + + if (!$memberPolicy) { + throw new ImportRowException(__('enrollment.MEMBER_NOT_EXISTS', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + if ($memberPolicy->status != 'active') { + throw new ImportRowException(__('enrollment.MEMBER_INACTIVE', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + $memberPolicy->member->fill($member_data); + if (!$memberPolicy->member->isDirty()) { + throw new ImportRowException(__('enrollment.MEMBER_NO_CHANGE'), 0, null, $row); + } + + $memberPolicy->member->save(); + DB::commit(); + } catch (\Exception $e) { + DB::rollback(); + throw new ImportRowException($e->getMessage(), $e->getCode(), $e, $row); } - - if ($memberPolicy->status != 'active') { - throw new ImportRowException(__('enrollment.MEMBER_INACTIVE', [ - 'member_id' => $row['member_id'], - 'policy_id' => $row['policy_number'] - ]), 0, null, $row); - } - - $memberPolicy->member->fill($member_data); - if (!$memberPolicy->member->isDirty()) { - throw new ImportRowException(__('enrollment.MEMBER_NO_CHANGE'), 0, null, $row); - } - - $memberPolicy->member->save(); break; case "3": // Member Deletion + $member = Member::query() + ->where('member_id', $row['member_id']) + ->first(); + + // Validate If Exist Member + if (!$member) { + throw new ImportRowException(__('enrollment.MEMBER_NOT_FOUND', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + $memberPolicy = MemberPolicy::query() ->where('policy_id', $row['policy_number']) ->where('member_id', $row['member_id']) diff --git a/app/Models/Member.php b/app/Models/Member.php index ea1657d7..3ddf2b4c 100755 --- a/app/Models/Member.php +++ b/app/Models/Member.php @@ -56,6 +56,11 @@ class Member extends Model "end_no_claim", ]; + public function claims() + { + return $this->hasMany(Claim::class, 'member_id', 'id'); + } + public function employeds() { return $this->hasMany(CorporateEmployee::class, 'member_id'); @@ -68,7 +73,12 @@ class Member extends Model public function plans() { - return $this->hasManyThrough(MemberPlan::class, Plan::class, 'member_id', 'plan_id', 'id', 'plan_id'); + return $this->hasManyThrough(Plan::class, MemberPlan::class, 'member_id', 'code', 'id', 'plan_id'); + } + + public function currentPlan() + { + return $this->hasOneThrough(Plan::class, MemberPlan::class, 'member_id', 'code', 'id', 'plan_id')->latest(); } public function policies() diff --git a/frontend/dashboard/src/@types/paginated-data.ts b/frontend/dashboard/src/@types/paginated-data.ts index 738f8785..eca6d930 100755 --- a/frontend/dashboard/src/@types/paginated-data.ts +++ b/frontend/dashboard/src/@types/paginated-data.ts @@ -12,3 +12,18 @@ export type LaravelPaginatedData = { to?: number; total: number; } + +export const LaravelPaginatedDataDefault = { + 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 +} \ No newline at end of file diff --git a/frontend/dashboard/src/pages/Claims/Create.tsx b/frontend/dashboard/src/pages/Claims/Create.tsx new file mode 100755 index 00000000..5e9fccdf --- /dev/null +++ b/frontend/dashboard/src/pages/Claims/Create.tsx @@ -0,0 +1,241 @@ +import * as Yup from 'yup'; +import { yupResolver } from "@hookform/resolvers/yup"; +import { Card, Collapse, Divider, Grid, Stack, Typography } from "@mui/material"; +import { useForm } from "react-hook-form"; +import { useParams } 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 CorporateTabNavigations from "../CorporateTabNavigations"; +import DivisionsList from "./List"; +import { useMemo, useState } from 'react'; + + + +export default function Divisions() { + const { themeStretch } = useSettings(); + + const { corporate_id } = useParams(); + + const NewDivisionSchema = 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'), + }); + + const defaultValues = useMemo( + () => ({ + code: '', + }), + [] + ); + + const methods = useForm({ + resolver: yupResolver(NewDivisionSchema), + defaultValues, + }); + + const { + reset, + watch, + control, + setValue, + getValues, + setError, + handleSubmit, + formState: { isSubmitting }, + } = methods; + + const onSubmit = async (data: any) => { + console.log(data); + }; + + const [open, setOpen] = useState(false); + + const benefits = [ + { + 'category' : 'General Practitioner', + 'childs' : [ + { + 'name' : 'External Doctor Online', + 'code' : 'gp-external-doctor-online' + }, + { + 'name' : 'External Doctor Offline', + 'code' : 'gp-external-doctor-offline' + }, + { + 'name' : 'Internal Doctor Online', + 'code' : 'gp-internal-doctor-online' + }, + { + 'name' : 'Internal Doctor Offline', + 'code' : 'gp-internal-doctor-offline' + }, + ] + }, + { + 'category' : 'Specialist', + 'childs' : [ + { + 'name' : 'External Doctor Online', + 'code' : 'sp-external-doctor-online' + }, + { + 'name' : 'External Doctor Offline', + 'code' : 'sp-external-doctor-offline' + }, + { + 'name' : 'Internal Doctor Online', + 'code' : 'sp-internal-doctor-online' + }, + { + 'name' : 'Internal Doctor Offline', + 'code' : 'sp-internal-doctor-offline' + }, + ] + }, + { + 'category' : 'Medicines', + 'childs' : [ + { + 'name' : 'Vitamins', + 'code' : 'medicines-vitamins' + }, + { + 'name' : 'Delivery Fee', + 'code' : 'medicines-delivery-fee' + }, + ] + }, + ]; + + const products = [ + { + 'name' : 'Inpatient', + 'code' : 'IP', + }, + { + 'name' : 'Outpatient', + 'code' : 'OP', + }, + { + 'name' : 'Dental', + 'code' : 'DT', + }, + { + 'name' : 'Dental', + 'code' : 'DTL', + }, + { + 'name' : 'Matternity', + 'code' : 'MT', + }, + { + 'name' : 'Special Benefit', + 'code' : 'SB', + }, + ]; + + return ( + + + + + + + + + + + + Benefit Detail + + + + + + Benefit Configuration + + + }> + + + {benefits.map(row => ( + + {row.category} + + {row.childs.map(benefit => ( + + + + ))} + + + ))} + Admin Fee + + {benefits.map(row => ( + + + + ))} + + + + + + + {benefits.map(row => ( + + {row.category} + + {row.childs.map(benefit => ( + + + + ))} + + + ))} + Admin Fee + + {benefits.map(row => ( + + + + ))} + + + + + + + + + + + ); +} diff --git a/frontend/dashboard/src/pages/Claims/Index.tsx b/frontend/dashboard/src/pages/Claims/Index.tsx new file mode 100755 index 00000000..0c5ba319 --- /dev/null +++ b/frontend/dashboard/src/pages/Claims/Index.tsx @@ -0,0 +1,29 @@ +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'; + return ( + + + + + + + + + ); +} diff --git a/frontend/dashboard/src/pages/Claims/LaravelTable.tsx b/frontend/dashboard/src/pages/Claims/LaravelTable.tsx new file mode 100644 index 00000000..0ee480e1 --- /dev/null +++ b/frontend/dashboard/src/pages/Claims/LaravelTable.tsx @@ -0,0 +1,26 @@ +import { Card, Paper, TableContainer } from "@mui/material"; +import { LaravelPaginatedData } from "../../@types/paginated-data"; +import BasePagination from "../../components/BasePagination"; + +type LaravelTableProps = { + isLoading: boolean; + lastRequest: number; + data: LaravelPaginatedData; + handlePageChange: void; + TableContent: any; +}; + +export default function LaravelTable(props: LaravelTableProps) { + return ( + + + { props.TableContent } + + + { !props.isLoading ? + () : + (
) + } +
+ ) +} \ No newline at end of file diff --git a/frontend/dashboard/src/pages/Claims/List.tsx b/frontend/dashboard/src/pages/Claims/List.tsx new file mode 100755 index 00000000..79772d12 --- /dev/null +++ b/frontend/dashboard/src/pages/Claims/List.tsx @@ -0,0 +1,303 @@ +// @mui +import { Box, Button, Card, Collapse, IconButton, MenuItem, Table, TableBody, TableCell, TableRow, TextField, Typography, Stack, Menu, ButtonGroup } 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 { useSearchParams } from 'react-router-dom'; +// components +import axios from '../../utils/axios'; +import { LaravelPaginatedData, LaravelPaginatedDataDefault } from '../../@types/paginated-data'; +import DataTable from './LaravelTable'; + +export default function List() { + 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({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 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 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(`corporates/${corporate_id}/import-plan-benefit`, 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' }) + } + } + + return ( +
+ + {( !currentImportFileName && + + {/*

kjasndkjandskjasndkjansdkjansd

*/} + + + Import + Download Template + +
+ )} + + {( currentImportFileName && + + + + + + + + )} + {( importResult && + + Last Import Result Report : {importResult.result_file?.name ?? "-"} + + )} +
+ ); + } + + // 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('/master/drugs', { 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); + } + + useEffect(() => { + loadDataTableData(); + }, []) + + const headStyle = { + fontWeight: 'bold', + }; + + // Called on every row to map the data to the columns + function createData( data: Icd ): Icd { + return { + ...data, + } + } + + {/* ------------------ TABLE ROW ------------------ */} + function Row(props: { row: ReturnType }) { + const { row } = props; + const [open, setOpen] = React.useState(false); + + return ( + + *': { borderBottom: 'unset' } }}> + + setOpen(!open)} + > + {open ? : } + + + {row.type} + {row.code} + {row.name} + {row.version} + + + + + {/* COLLAPSIBLE ROW */} + + + + + + Description : {row.description} + + + + + + + ); + } + {/* ------------------ END TABLE ROW ------------------ */} + + function TableContent() { + return ( + + {/* ------------------ TABLE HEADER ------------------ */} + + + + Type + Code + Name + Version + 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/pages/Corporates/Member/List.tsx b/frontend/dashboard/src/pages/Corporates/Member/List.tsx index 371349fa..b60c8ec4 100755 --- a/frontend/dashboard/src/pages/Corporates/Member/List.tsx +++ b/frontend/dashboard/src/pages/Corporates/Member/List.tsx @@ -268,7 +268,7 @@ export default function CorporatePlanList() { {row.name} {row.nric} {row.email} - {row.plan_id} + {row.current_plan?.code} {row.current_policy?.start} {row.current_policy?.end} {( row.active ? () diff --git a/frontend/dashboard/src/routes/index.tsx b/frontend/dashboard/src/routes/index.tsx index 0f977c1e..65e8f05b 100755 --- a/frontend/dashboard/src/routes/index.tsx +++ b/frontend/dashboard/src/routes/index.tsx @@ -205,6 +205,12 @@ export default function Router() { path: 'master/formularium/create', element: , }, + + + { + path: 'claims', + element: , + }, ] }, // { @@ -287,3 +293,5 @@ const CorporateServicesCreate = Loadable(lazy(() => import('../pages/Corporates/ const CorporateHospitals = Loadable(lazy(() => import('../pages/Corporates/Hospital/Index'))); const CorporateClaimHistories = Loadable(lazy(() => import('../pages/Corporates/ClaimHistory/Index'))); + +const Claims = Loadable(lazy(() => import('../pages/Claims/Index'))); \ No newline at end of file From d5b43d9896e1fec7c3e46e2cdcca27a270df26fe Mon Sep 17 00:00:00 2001 From: R Date: Wed, 23 Nov 2022 13:16:24 +0700 Subject: [PATCH 2/7] [WIP] --- app/Models/Claim.php | 26 ++++++ app/Models/Plan.php | 5 + .../2022_11_22_135948_create_claims_table.php | 43 +++++++++ database/seeders/DummyClaimSeeder.php | 36 +++++++ frontend/client-portal/pnpm-lock.yaml | 93 +++++++++++++++++++ 5 files changed, 203 insertions(+) create mode 100644 app/Models/Claim.php create mode 100644 database/migrations/2022_11_22_135948_create_claims_table.php create mode 100644 database/seeders/DummyClaimSeeder.php diff --git a/app/Models/Claim.php b/app/Models/Claim.php new file mode 100644 index 00000000..35e0dd4a --- /dev/null +++ b/app/Models/Claim.php @@ -0,0 +1,26 @@ +belongsTo(Member::class, 'member_id'); + } +} diff --git a/app/Models/Plan.php b/app/Models/Plan.php index 28943183..b486291b 100755 --- a/app/Models/Plan.php +++ b/app/Models/Plan.php @@ -165,4 +165,9 @@ class Plan extends Model // { // return $this->belongsTo(Corporate::class); // } + + public function benefits() + { + return $this->hasMany(Benefit::class, 'plan_code', 'id'); + } } diff --git a/database/migrations/2022_11_22_135948_create_claims_table.php b/database/migrations/2022_11_22_135948_create_claims_table.php new file mode 100644 index 00000000..d595e75d --- /dev/null +++ b/database/migrations/2022_11_22_135948_create_claims_table.php @@ -0,0 +1,43 @@ +id(); + $table->string('code')->index(); + $table->foreignId('member_id')->index(); + $table->foreignId('diagnosis_id')->index(); + $table->string('total_claim'); + $table->string('currency'); + $table->foreignId('plan_id')->index(); + $table->foreignId('benefit_id')->index(); + + $table->timestamps(); + $table->softDeletes(); + $table->unsignedBigInteger('created_by')->nullable()->index(); + $table->unsignedBigInteger('updated_by')->nullable()->index(); + $table->unsignedBigInteger('deleted_by')->nullable()->index(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('claims'); + } +}; diff --git a/database/seeders/DummyClaimSeeder.php b/database/seeders/DummyClaimSeeder.php new file mode 100644 index 00000000..3ea7a395 --- /dev/null +++ b/database/seeders/DummyClaimSeeder.php @@ -0,0 +1,36 @@ +get(); + + foreach ($members as $member) { + for ($x = 0; $x < 10; $x++) { + $member->claims()->create([ + 'code' => Str::random('16'), + 'member_id' => $member->id, + 'diagnosis_id' => Icd::inRandomOrder()->first()->id, + 'total_claim' => 5000000, + 'currency' => 'IDR', + 'plan_id' => $member->currentPlan->id, + 'benefit_id' => $member->currentPlan->benefits()->inRandomOrder()->first()->id, + ]); + } + } + } +} diff --git a/frontend/client-portal/pnpm-lock.yaml b/frontend/client-portal/pnpm-lock.yaml index c8339f97..f4c3bca4 100755 --- a/frontend/client-portal/pnpm-lock.yaml +++ b/frontend/client-portal/pnpm-lock.yaml @@ -26,6 +26,7 @@ specifiers: '@typescript-eslint/eslint-plugin': ^5.35.1 '@typescript-eslint/parser': ^5.35.1 '@vitejs/plugin-react': ^1.3.2 + apexcharts: ^3.36.3 axios: ^0.27.2 change-case: ^4.1.2 csstype: ^3.1.0 @@ -52,12 +53,14 @@ specifiers: numeral: ^2.0.6 prettier: ^2.7.1 react: ^17.0.2 + react-apexcharts: ^1.4.0 react-dom: ^17.0.2 react-dropzone: ^14.2.2 react-helmet-async: ^1.3.0 react-hook-form: ^7.34.2 react-intersection-observer: ^8.34.0 react-lazy-load-image-component: ^1.5.5 + react-number-format: ^5.1.1 react-quill: 2.0.0-beta.4 react-router: ^6.3.0 react-router-dom: ^6.3.0 @@ -85,6 +88,7 @@ dependencies: '@mui/x-data-grid': 5.16.0_r4jqxufjb3aftjrjm24vhpn4hm '@mui/x-date-pickers': 5.0.0-beta.2_y3fv7pzpxqpbmxcbzsros3kjnu '@vitejs/plugin-react': 1.3.2 + apexcharts: 3.36.3 axios: 0.27.2 change-case: 4.1.2 csstype: 3.1.0 @@ -98,12 +102,14 @@ dependencies: nprogress: 0.2.0 numeral: 2.0.6 react: 17.0.2 + react-apexcharts: 1.4.0_rurziszfowucu2gmkqclv6xeru react-dom: 17.0.2_react@17.0.2 react-dropzone: 14.2.2_react@17.0.2 react-helmet-async: 1.3.0_sfoxds7t5ydpegc3knd667wn6m react-hook-form: 7.34.2_react@17.0.2 react-intersection-observer: 8.34.0_react@17.0.2 react-lazy-load-image-component: 1.5.5_sfoxds7t5ydpegc3knd667wn6m + react-number-format: 5.1.2_sfoxds7t5ydpegc3knd667wn6m react-quill: 2.0.0-beta.4_sfoxds7t5ydpegc3knd667wn6m react-router: 6.3.0_react@17.0.2 react-router-dom: 6.3.0_sfoxds7t5ydpegc3knd667wn6m @@ -2683,6 +2689,17 @@ packages: color-convert: 2.0.1 dev: true + /apexcharts/3.36.3: + resolution: {integrity: sha512-8/FXEs0ohXMff07Gv28XjhPwEJphIUdq2/wii/pcvi54Tw6z1mjrV8ydN8rlWi/ve8BAPBefJkLmRWv7UOBsLw==} + dependencies: + svg.draggable.js: 2.2.2 + svg.easing.js: 2.0.0 + svg.filter.js: 2.0.2 + svg.pathmorphing.js: 0.1.3 + svg.resize.js: 1.4.3 + svg.select.js: 3.0.1 + dev: false + /argparse/2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true @@ -4934,6 +4951,17 @@ packages: safe-buffer: 5.2.1 dev: true + /react-apexcharts/1.4.0_rurziszfowucu2gmkqclv6xeru: + resolution: {integrity: sha512-DrcMV4aAMrUG+n6412yzyATWEyCDWlpPBBhVbpzBC4PDeuYU6iF84SmExbck+jx5MUm4U5PM3/T307Mc3kzc9Q==} + peerDependencies: + apexcharts: ^3.18.0 + react: '>=0.13' + dependencies: + apexcharts: 3.36.3 + prop-types: 15.8.1 + react: 17.0.2 + dev: false + /react-dom/17.0.2_react@17.0.2: resolution: {integrity: sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==} peerDependencies: @@ -5016,6 +5044,17 @@ packages: react-dom: 17.0.2_react@17.0.2 dev: false + /react-number-format/5.1.2_sfoxds7t5ydpegc3knd667wn6m: + resolution: {integrity: sha512-NXm/MvZVjmPqrFbjAut/prCyBZ+pA+O+C12rDkWZrwS3JXz1J42RC1kpclkdnkx2KDjRCNFLb21FlwGcNBZddw==} + peerDependencies: + react: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^18.0.0 + react-dom: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^18.0.0 + dependencies: + prop-types: 15.8.1 + react: 17.0.2 + react-dom: 17.0.2_react@17.0.2 + dev: false + /react-quill/2.0.0-beta.4_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-KyAHvAlPjP4xLElKZJefMth91Z6FbbXRvq9OSu6xN3KBaoasLP9p+3dcxg4Ywr4tBlpMGXcPszYSAgd5CpJ45Q==} peerDependencies: @@ -5461,6 +5500,60 @@ packages: resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} dev: false + /svg.draggable.js/2.2.2: + resolution: {integrity: sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==} + engines: {node: '>= 0.8.0'} + dependencies: + svg.js: 2.7.1 + dev: false + + /svg.easing.js/2.0.0: + resolution: {integrity: sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==} + engines: {node: '>= 0.8.0'} + dependencies: + svg.js: 2.7.1 + dev: false + + /svg.filter.js/2.0.2: + resolution: {integrity: sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==} + engines: {node: '>= 0.8.0'} + dependencies: + svg.js: 2.7.1 + dev: false + + /svg.js/2.7.1: + resolution: {integrity: sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==} + dev: false + + /svg.pathmorphing.js/0.1.3: + resolution: {integrity: sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==} + engines: {node: '>= 0.8.0'} + dependencies: + svg.js: 2.7.1 + dev: false + + /svg.resize.js/1.4.3: + resolution: {integrity: sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==} + engines: {node: '>= 0.8.0'} + dependencies: + svg.js: 2.7.1 + svg.select.js: 2.1.2 + dev: false + + /svg.select.js/2.1.2: + resolution: {integrity: sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==} + engines: {node: '>= 0.8.0'} + dependencies: + svg.js: 2.7.1 + dev: false + + /svg.select.js/3.0.1: + resolution: {integrity: sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==} + engines: {node: '>= 0.8.0'} + dependencies: + svg.js: 2.7.1 + dev: false + /temp-dir/2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} engines: {node: '>=8'} From b3eb9b5f9d7a2bde8cf9d052ec5d320490d5ab3f Mon Sep 17 00:00:00 2001 From: R Date: Fri, 25 Nov 2022 05:14:40 +0700 Subject: [PATCH 3/7] [WIP] Claims --- .../Http/Controllers/Api/ClaimController.php | 93 +++++ .../Api/CorporateMemberController.php | 206 +++++++++++ .../Controllers/Api/DiagnosisController.php | 8 + .../Http/Controllers/Api/MemberController.php | 130 +------ Modules/Internal/Routes/api.php | 14 +- app/Models/Claim.php | 15 + app/Models/Corporate.php | 5 + app/Models/LimitJournal.php | 24 ++ app/Models/Member.php | 46 +++ app/Services/ClaimService.php | 27 ++ ..._23_140658_create_limit_journals_table.php | 41 +++ .../Claims => components}/LaravelTable.tsx | 10 +- .../dashboard/src/components/MuiDialog.tsx | 56 +++ .../components/dialogs/MemberSelectDialog.tsx | 275 ++++++++++++++ .../layouts/dashboard/navbar/NavConfig.tsx | 7 +- .../dashboard/src/pages/Claims/Create.tsx | 343 +++++++++--------- frontend/dashboard/src/pages/Claims/List.tsx | 81 ++--- frontend/dashboard/src/routes/index.tsx | 7 +- 18 files changed, 1012 insertions(+), 376 deletions(-) create mode 100644 Modules/Internal/Http/Controllers/Api/ClaimController.php create mode 100755 Modules/Internal/Http/Controllers/Api/CorporateMemberController.php mode change 100755 => 100644 Modules/Internal/Http/Controllers/Api/MemberController.php create mode 100644 app/Models/LimitJournal.php create mode 100644 app/Services/ClaimService.php create mode 100644 database/migrations/2022_11_23_140658_create_limit_journals_table.php rename frontend/dashboard/src/{pages/Claims => components}/LaravelTable.tsx (73%) create mode 100644 frontend/dashboard/src/components/MuiDialog.tsx create mode 100644 frontend/dashboard/src/components/dialogs/MemberSelectDialog.tsx diff --git a/Modules/Internal/Http/Controllers/Api/ClaimController.php b/Modules/Internal/Http/Controllers/Api/ClaimController.php new file mode 100644 index 00000000..4909f83d --- /dev/null +++ b/Modules/Internal/Http/Controllers/Api/ClaimController.php @@ -0,0 +1,93 @@ +paginate(10); + + return response()->json($claims); + } + + /** + * 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 checkLimit(Request $request) + { + return true; + } +} diff --git a/Modules/Internal/Http/Controllers/Api/CorporateMemberController.php b/Modules/Internal/Http/Controllers/Api/CorporateMemberController.php new file mode 100755 index 00000000..e3573b98 --- /dev/null +++ b/Modules/Internal/Http/Controllers/Api/CorporateMemberController.php @@ -0,0 +1,206 @@ +memberEnrollmentService = $memberEnrollmentService; + } + /** + * Display a listing of the resource. + * @return Renderable + */ + public function index(Request $request, $corporate_id) + { + $members = Member::query() + ->filter($request->all()) + // ->where('corporate_id', $corporate_id) + ->whereHas('employeds', function ($employeds) use ($corporate_id) { + $employeds->where('corporate_id', $corporate_id); + }) + ->with([ + 'employeds', + 'currentPolicy' => function ($policy) use ($corporate_id) { + $policy->whereHas('corporatePolicy', function($corporatePolicy) use ($corporate_id) { + $corporatePolicy->where('corporate_id', $corporate_id); + }); + } + ]) + ->with('currentPlan') + ->paginate() + ->appends($request->all()); + + return $members; + } + + /** + * 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 import(Request $request, $corporate_id) + { + $request->validate([ + 'file' => 'required|file|mimes:xls,xlsx,csv,txt', + ]); + $corporate = Corporate::findOrFail($corporate_id)->load('currentPolicy'); + + $file_name = now()->getPreciseTimestamp(3).'-'.$request->file('file')->getClientOriginalName(); + $file = $request->file('file')->storeAs('temp', $file_name); + + $reader = ReaderEntityFactory::createReaderFromFile(Storage::path('temp/'.$file_name)); + $reader->open(Storage::path('temp/'.$file_name)); + + $writer = WriterEntityFactory::createXLSXWriter(); + $writer->openToFile(Storage::disk('public')->path('temp/result-'.$file_name)); + + $headers_map_to_table_fields = $this->memberEnrollmentService->doc_headers_to_field_map; + + // Write Header to File with certain Format from MemberEnrollmentService::$result_doc_headers + $result_headers = $this->memberEnrollmentService->result_doc_headers; + $singleRow = WriterEntityFactory::createRow($this->memberEnrollmentService->makeResultRow($result_headers)); + $writer->addRow($singleRow); + + $imported_member_data = 0; + $failed_member_data = []; + foreach ($reader->getSheetIterator() as $sheet) { + $doc_headers_indexes = []; + foreach ($sheet->getRowIterator() as $index => $row) { + if ($index == 1) { // First Row Must be Header + foreach ($row->getCells() as $index => $cell) { + // Clear up the string and remove all spaces + $title = $cell->getValue(); + $title = preg_replace( "/\r|\n/", " ", $title ); + $title = preg_replace('/\xc2\xa0/', " ", $title ); + $title = rtrim($title); + $title = ltrim($title); + $doc_headers_indexes[$index] = $title; + } + } else { // Next Row Should be Data + // Collecting Values from table rows and map it to correct fields + $new_member_data = []; + foreach ($row->getCells() as $header_index => $cell) { + if (isset($headers_map_to_table_fields[$doc_headers_indexes[$header_index]])) { + $new_member_data[$headers_map_to_table_fields[$doc_headers_indexes[$header_index]]] = $cell->getValue(); + } + } + + try { + // dd($new_member_data); + $rowResponse = $this->memberEnrollmentService->handleImportRow($corporate, $new_member_data); + + // Write Success Result to File + $singleRow = WriterEntityFactory::createRow($this->memberEnrollmentService->makeResultRowWithResultFormat($rowResponse)); + $writer->addRow($singleRow); + $imported_member_data++; + } catch (ImportRowException $e) { + // Write Data Validation Error to File + $new_member_data = array_merge($new_member_data, [ + 'ingestion_code' => $e->getCode(), + 'ingestion_status' => $e->getMessage(), + ]); + $singleRow = WriterEntityFactory::createRow($this->memberEnrollmentService->makeResultRowWithResultFormat($new_member_data)); + $writer->addRow($singleRow); + $failed_member_data[] = ['row_number' => $index, 'error' => $e->getMessage()]; + } catch (\Exception $e) { + // Write Server Error to File + $new_member_data = array_merge($new_member_data, [ + 'ingestion_code' => $e->getCode(), + 'ingestion_status' => $e->getMessage(), + ]); + $singleRow = WriterEntityFactory::createRow($this->memberEnrollmentService->makeResultRowWithResultFormat($new_member_data)); + $writer->addRow($singleRow); + $failed_member_data[] = ['row_number' => $index, 'error' => $e->getMessage()]; + } + } + + } + + break; //only read first sheet + } + $reader->close(); + $writer->close(); + Storage::delete('temp/'.$file_name); + // throw(404); + + return [ + 'total_success_row' => $imported_member_data, + 'total_failed_row' => count($failed_member_data), + 'failed_row' => $failed_member_data, + 'result_file' => [ + 'url' => Storage::disk('public')->url('temp/result-'.$file_name), + 'name' => 'result-'.$file_name, + ] + ]; + } +} diff --git a/Modules/Internal/Http/Controllers/Api/DiagnosisController.php b/Modules/Internal/Http/Controllers/Api/DiagnosisController.php index 1d49b710..f42a6b49 100755 --- a/Modules/Internal/Http/Controllers/Api/DiagnosisController.php +++ b/Modules/Internal/Http/Controllers/Api/DiagnosisController.php @@ -79,4 +79,12 @@ class DiagnosisController extends Controller { // } + + public function search(Request $request) + { + return Icd::when($request->search ?? null, function($icd, $search) { + $icd->where('name', 'LIKE', '%'.$search.'%') + ->orWhere('code', 'LIKE', '%'.$search.'%'); + })->limit(10)->get(); + } } diff --git a/Modules/Internal/Http/Controllers/Api/MemberController.php b/Modules/Internal/Http/Controllers/Api/MemberController.php old mode 100755 new mode 100644 index c31d7c0b..f8defb31 --- a/Modules/Internal/Http/Controllers/Api/MemberController.php +++ b/Modules/Internal/Http/Controllers/Api/MemberController.php @@ -2,49 +2,20 @@ namespace Modules\Internal\Http\Controllers\Api; -use App\Exceptions\ImportRowException; -use App\Models\Corporate; use App\Models\Member; -use Box\Spout\Reader\Common\Creator\ReaderEntityFactory; -use Box\Spout\Writer\Common\Creator\WriterEntityFactory; -use Box\Spout\Common\Entity\Row; use Illuminate\Contracts\Support\Renderable; use Illuminate\Http\Request; use Illuminate\Routing\Controller; -use Illuminate\Support\Facades\Storage; -use Modules\Internal\Services\MemberEnrollmentService; class MemberController extends Controller { - public function __construct(MemberEnrollmentService $memberEnrollmentService) - { - $this->memberEnrollmentService = $memberEnrollmentService; - } /** * Display a listing of the resource. * @return Renderable */ - public function index(Request $request, $corporate_id) + public function index() { - $members = Member::query() - ->filter($request->all()) - // ->where('corporate_id', $corporate_id) - ->whereHas('employeds', function ($employeds) use ($corporate_id) { - $employeds->where('corporate_id', $corporate_id); - }) - ->with([ - 'employeds', - 'currentPolicy' => function ($policy) use ($corporate_id) { - $policy->whereHas('corporatePolicy', function($corporatePolicy) use ($corporate_id) { - $corporatePolicy->where('corporate_id', $corporate_id); - }); - } - ]) - ->with('currentPlan') - ->paginate() - ->appends($request->all()); - - return $members; + return Member::paginate(); } /** @@ -106,101 +77,4 @@ class MemberController extends Controller { // } - - - public function import(Request $request, $corporate_id) - { - $request->validate([ - 'file' => 'required|file|mimes:xls,xlsx,csv,txt', - ]); - $corporate = Corporate::findOrFail($corporate_id)->load('currentPolicy'); - - $file_name = now()->getPreciseTimestamp(3).'-'.$request->file('file')->getClientOriginalName(); - $file = $request->file('file')->storeAs('temp', $file_name); - - $reader = ReaderEntityFactory::createReaderFromFile(Storage::path('temp/'.$file_name)); - $reader->open(Storage::path('temp/'.$file_name)); - - $writer = WriterEntityFactory::createXLSXWriter(); - $writer->openToFile(Storage::disk('public')->path('temp/result-'.$file_name)); - - $headers_map_to_table_fields = $this->memberEnrollmentService->doc_headers_to_field_map; - - // Write Header to File with certain Format from MemberEnrollmentService::$result_doc_headers - $result_headers = $this->memberEnrollmentService->result_doc_headers; - $singleRow = WriterEntityFactory::createRow($this->memberEnrollmentService->makeResultRow($result_headers)); - $writer->addRow($singleRow); - - $imported_member_data = 0; - $failed_member_data = []; - foreach ($reader->getSheetIterator() as $sheet) { - $doc_headers_indexes = []; - foreach ($sheet->getRowIterator() as $index => $row) { - if ($index == 1) { // First Row Must be Header - foreach ($row->getCells() as $index => $cell) { - // Clear up the string and remove all spaces - $title = $cell->getValue(); - $title = preg_replace( "/\r|\n/", " ", $title ); - $title = preg_replace('/\xc2\xa0/', " ", $title ); - $title = rtrim($title); - $title = ltrim($title); - $doc_headers_indexes[$index] = $title; - } - } else { // Next Row Should be Data - // Collecting Values from table rows and map it to correct fields - $new_member_data = []; - foreach ($row->getCells() as $header_index => $cell) { - if (isset($headers_map_to_table_fields[$doc_headers_indexes[$header_index]])) { - $new_member_data[$headers_map_to_table_fields[$doc_headers_indexes[$header_index]]] = $cell->getValue(); - } - } - - try { - // dd($new_member_data); - $rowResponse = $this->memberEnrollmentService->handleImportRow($corporate, $new_member_data); - - // Write Success Result to File - $singleRow = WriterEntityFactory::createRow($this->memberEnrollmentService->makeResultRowWithResultFormat($rowResponse)); - $writer->addRow($singleRow); - $imported_member_data++; - } catch (ImportRowException $e) { - // Write Data Validation Error to File - $new_member_data = array_merge($new_member_data, [ - 'ingestion_code' => $e->getCode(), - 'ingestion_status' => $e->getMessage(), - ]); - $singleRow = WriterEntityFactory::createRow($this->memberEnrollmentService->makeResultRowWithResultFormat($new_member_data)); - $writer->addRow($singleRow); - $failed_member_data[] = ['row_number' => $index, 'error' => $e->getMessage()]; - } catch (\Exception $e) { - // Write Server Error to File - $new_member_data = array_merge($new_member_data, [ - 'ingestion_code' => $e->getCode(), - 'ingestion_status' => $e->getMessage(), - ]); - $singleRow = WriterEntityFactory::createRow($this->memberEnrollmentService->makeResultRowWithResultFormat($new_member_data)); - $writer->addRow($singleRow); - $failed_member_data[] = ['row_number' => $index, 'error' => $e->getMessage()]; - } - } - - } - - break; //only read first sheet - } - $reader->close(); - $writer->close(); - Storage::delete('temp/'.$file_name); - // throw(404); - - return [ - 'total_success_row' => $imported_member_data, - 'total_failed_row' => count($failed_member_data), - 'failed_row' => $failed_member_data, - 'result_file' => [ - 'url' => Storage::disk('public')->url('temp/result-'.$file_name), - 'name' => 'result-'.$file_name, - ] - ]; - } } diff --git a/Modules/Internal/Routes/api.php b/Modules/Internal/Routes/api.php index 6d3c1b46..57709300 100755 --- a/Modules/Internal/Routes/api.php +++ b/Modules/Internal/Routes/api.php @@ -1,11 +1,14 @@ group(function () { Route::get('corporates/{corporate_id}/divisions/{id}/edit', [DivisionController::class, 'edit']); Route::put('corporates/{corporate_id}/divisions/{id}', [DivisionController::class, 'update']); - Route::get('corporates/{corporate_id}/members', [MemberController::class, 'index']); - Route::post('corporates/{corporate_id}/members/import', [MemberController::class, 'import']); + Route::get('corporates/{corporate_id}/members', [CorporateMemberController::class, 'index']); + Route::post('corporates/{corporate_id}/members/import', [CorporateMemberController::class, 'import']); Route::get('corporates/{corporate_id}/diagnosis-exclusions', [DiagnosisExclusionController::class, 'index']); Route::post('corporates/{corporate_id}/diagnosis-exclusions/import', [DiagnosisExclusionController::class, 'import']); @@ -86,12 +89,17 @@ Route::prefix('internal')->group(function () { // Route::get('corporates/{corporate_id}/diagnosis-exclusions/import', [DiagnosisExclusionController::class, 'import']); Route::get('master/diagnosis', [DiagnosisController::class, 'index']); + Route::get('master/diagnosis/search', [DiagnosisController::class, 'search']); Route::get('master/drugs', [DrugController::class, 'index']); Route::get('master/formulariums', [FormulariumController::class, 'index']); Route::post('master/formulariums', [FormulariumController::class, 'store']); Route::post('master/formulariums/import', [FormulariumController::class, 'import']); - + Route::get('members', [MemberController::class, 'index']); + + Route::get('claims', [ClaimController::class, 'index']); + Route::post('check-limit', [ClaimController::class, 'checkLimit']); + }); // Route::get('something', [DiagnosisExclusionController::class, 'index']); diff --git a/app/Models/Claim.php b/app/Models/Claim.php index 35e0dd4a..39919309 100644 --- a/app/Models/Claim.php +++ b/app/Models/Claim.php @@ -23,4 +23,19 @@ class Claim extends Model { return $this->belongsTo(Member::class, 'member_id'); } + + public function diagnosis() + { + return $this->belongsTo(Icd::class, 'diagnosis_id'); + } + + public function plan() + { + return $this->belongsTo(Plan::class, 'plan_id'); + } + + public function benefit() + { + return $this->belongsTo(Benefit::class, 'benefit_id'); + } } diff --git a/app/Models/Corporate.php b/app/Models/Corporate.php index 561af4a6..d8147665 100755 --- a/app/Models/Corporate.php +++ b/app/Models/Corporate.php @@ -82,6 +82,11 @@ class Corporate extends Model return $this->morphMany(ImportLog::class, 'importable'); } + public function limitJournals() + { + return $this->morphMany(LimitJournal::class, 'journalable'); + } + public function services() { return $this->hasManyThrough(CorporateService::class, Service::class, 'corporate_id', 'service_code', 'id', 'service_code'); diff --git a/app/Models/LimitJournal.php b/app/Models/LimitJournal.php new file mode 100644 index 00000000..abba9b09 --- /dev/null +++ b/app/Models/LimitJournal.php @@ -0,0 +1,24 @@ +morphTo(); + } +} diff --git a/app/Models/Member.php b/app/Models/Member.php index 3ddf2b4c..759b47bd 100755 --- a/app/Models/Member.php +++ b/app/Models/Member.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Traits\Blameable; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -56,6 +57,8 @@ class Member extends Model "end_no_claim", ]; + protected $appends = ['full_name']; + public function claims() { return $this->hasMany(Claim::class, 'member_id', 'id'); @@ -66,6 +69,35 @@ class Member extends Model return $this->hasMany(CorporateEmployee::class, 'member_id'); } + public function corporates() + { + return $this + ->belongsToMany(Corporate::class, 'corporate_employees', 'corporate_id', 'member_id') + ->withPivot([ + 'branch_code', + 'divison_id', + 'nik', + 'status', + 'start', + 'end' + ]); + } + + public function currentCorporate() + { + return $this->belongsToMany(Corporate::class, 'corporate_employees', 'corporate_id', 'member_id') + // ->withPivot([ + // 'branch_code', + // 'divison_id', + // 'nik', + // 'status', + // 'start', + // 'end' + // ]) + ->where('start', '<', now()) + ->where('end', '>', now()); + } + public function memberPlans() { return $this->hasMany(MemberPlan::class, 'member_id'); @@ -91,6 +123,20 @@ class Member extends Model return $this->hasOne(MemberPolicy::class, 'member_id', 'member_id')->where('status', 'active')->latestOfMany(); } + public function getFullNameAttribute() + { + $arr = []; + if (!empty($this->name_prefix)) { + $arr[] = $this->name_prefix; + } + $arr[] = $this->name; + if (!empty($this->name_suffix)) { + $arr[] = $this->name_suffix; + } + + return implode(' ', $arr); + } + public function scopeFilter($query, array $filters) { $query->when($filters['search'] ?? false, function ($query, $search) { diff --git a/app/Services/ClaimService.php b/app/Services/ClaimService.php new file mode 100644 index 00000000..05f3618d --- /dev/null +++ b/app/Services/ClaimService.php @@ -0,0 +1,27 @@ + Str::random('16'), + 'member_id' => $member->id, + 'diagnosis_id' => $icd, + 'total_claim' => $totalClaim, + 'currency' => 'IDR', + 'plan_id' => $member->currentPlan->id, + 'benefit_id' => $benefit->id, + ]); + + $corporate = $member->asd; + + return $claim; + } +} \ No newline at end of file diff --git a/database/migrations/2022_11_23_140658_create_limit_journals_table.php b/database/migrations/2022_11_23_140658_create_limit_journals_table.php new file mode 100644 index 00000000..ff82510a --- /dev/null +++ b/database/migrations/2022_11_23_140658_create_limit_journals_table.php @@ -0,0 +1,41 @@ +id(); + $table->morphs('journalable'); + $table->string('mutation'); + $table->string('type'); + $table->string('balance'); + $table->text('description'); + + $table->timestamps(); + $table->softDeletes(); + $table->unsignedBigInteger('created_by')->nullable()->index(); + $table->unsignedBigInteger('updated_by')->nullable()->index(); + $table->unsignedBigInteger('deleted_by')->nullable()->index(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('limit_journals'); + } +}; diff --git a/frontend/dashboard/src/pages/Claims/LaravelTable.tsx b/frontend/dashboard/src/components/LaravelTable.tsx similarity index 73% rename from frontend/dashboard/src/pages/Claims/LaravelTable.tsx rename to frontend/dashboard/src/components/LaravelTable.tsx index 0ee480e1..18325952 100644 --- a/frontend/dashboard/src/pages/Claims/LaravelTable.tsx +++ b/frontend/dashboard/src/components/LaravelTable.tsx @@ -1,6 +1,6 @@ import { Card, Paper, TableContainer } from "@mui/material"; -import { LaravelPaginatedData } from "../../@types/paginated-data"; -import BasePagination from "../../components/BasePagination"; +import { LaravelPaginatedData } from "../@types/paginated-data"; +import BasePagination from "./BasePagination"; type LaravelTableProps = { isLoading: boolean; @@ -10,7 +10,7 @@ type LaravelTableProps = { TableContent: any; }; -export default function LaravelTable(props: LaravelTableProps) { +function LaravelTable(props: LaravelTableProps) { return ( @@ -23,4 +23,6 @@ export default function LaravelTable(props: LaravelTableProps) { } ) -} \ No newline at end of file +} + +export default LaravelTable \ No newline at end of file diff --git a/frontend/dashboard/src/components/MuiDialog.tsx b/frontend/dashboard/src/components/MuiDialog.tsx new file mode 100644 index 00000000..54ee1110 --- /dev/null +++ b/frontend/dashboard/src/components/MuiDialog.tsx @@ -0,0 +1,56 @@ +import { Dialog, DialogTitle, DialogContent, Stack, Typography, IconButton } from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import { ReactElement } from 'react'; +import Iconify from './Iconify'; + +// ---------------------------------------------------------------------- + +type MuiDialogProps = { + title?: { + name?: string; + icon?: string; + }; + openDialog: boolean; + setOpenDialog: Function; + content?: ReactElement; + maxWidth?: string; +}; + +// ---------------------------------------------------------------------- + +const MuiDialog = ({ title, openDialog, setOpenDialog, content, maxWidth }: MuiDialogProps) => { + const handleClose = () => { + setOpenDialog(false); + }; + + let maxWidthDialog = 'md'; + + if (maxWidth) { + maxWidthDialog = maxWidth; + } + + return ( + + + + {title?.icon ? ( + + + {title?.name} + + ) : ( + {title?.name ? title?.name : ''} + )} + + + + + + + {content ? content : 'Testing Content Dialog'} + + + ); +}; + +export default MuiDialog; diff --git a/frontend/dashboard/src/components/dialogs/MemberSelectDialog.tsx b/frontend/dashboard/src/components/dialogs/MemberSelectDialog.tsx new file mode 100644 index 00000000..3d336714 --- /dev/null +++ b/frontend/dashboard/src/components/dialogs/MemberSelectDialog.tsx @@ -0,0 +1,275 @@ +// @mui +import { styled } from '@mui/material/styles'; +import { + Typography, + LinearProgress, + linearProgressClasses, + Stack, + FormControlLabel, + TableRow, + TableCell, + Table, + TableBody, + TextField, + Button, +} from '@mui/material'; +import { LoadingButton } from '@mui/lab'; +import Checkbox from '@mui/material/Checkbox'; +// components +import { FormProvider, RHFTextField } from '../../components/hook-form'; +// React +import React, { ReactElement, useEffect, useState } from 'react'; +import { fCurrency } from '../../utils/formatNumber'; +// yup +import * as Yup from 'yup'; +// form +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import MuiDialog from '../MuiDialog'; +import { Member } from '../../@types/member'; +import { LaravelPaginatedDataDefault } from '../../@types/paginated-data'; +import DataTable from '../LaravelTable'; +import axios from '../../utils/axios'; + +// ---------------------------------------------------------------------- + +type DataContent = { + info: string; + date: string; + time: string; +}; + +type MuiDialogProps = { + title?: { + name?: string; + icon?: string; + }; + openDialog: boolean; + setOpenDialog: Function; + content?: ReactElement; + onSelect?: Function; +}; + +type FormValuesProps = { + topup: string; +}; + +// ---------------------------------------------------------------------- + +const testData = { + companyName: 'PT. Aman Mineral', + policyNumber: 12345678, + totalMembers: 50, + totalCases: 100, + totalPersen: 75, + myLimit: '375.000.000', + totalLimit: 500000000, +}; + +const BorderLinearProgress = styled(LinearProgress)(({ theme }) => ({ + height: 10, + borderRadius: 6, + [`&.${linearProgressClasses.colorPrimary}`]: { + backgroundColor: theme.palette.grey[theme.palette.mode === 'light' ? 300 : 800], + }, + [`& .${linearProgressClasses.bar}`]: { + borderRadius: 6, + background: 'linear-gradient(270deg, #19BBBB 38.42%, #FF9565 76.21%, #FE7253 104.02%)', + }, +})); + +// ---------------------------------------------------------------------- + +export default function MemberSelectDialog({ + openDialog, + setOpenDialog, + onSelect +}: MuiDialogProps) { + const [dataTableLoading, setDataTableLoading] = useState(false) + const [dataTableData, setDataTableData] = useState(LaravelPaginatedDataDefault) + const [searchFilter, setSearchFilter] = useState({ + search: '' + }) + + const handleSearchChange = (event: any) => { + setSearchFilter({...searchFilter, ...{search: event.target.value}}) + } + + const loadDataTableData = async (appliedFilter : any | null = null) => { + setDataTableLoading(true); + const filter = appliedFilter ? appliedFilter : {}; + const response = await axios.get('/members', { params: filter }); + // console.log(response.data); + setDataTableLoading(false); + + setDataTableData(response.data); + } + + const applyFilter = async (searchFilter: {search: string}) => { + await loadDataTableData(searchFilter); + } + + const handleFilterSubmit = (event: any) => { + event.preventDefault(); + applyFilter(searchFilter) + } + + useEffect(() => { + console.log('effect openDialog'); + if (openDialog === false) { + } + }, [openDialog]); + + const handlePageChange = () => { + console.log('handle change') + } + + // Called on every row to map the data to the columns + function createData(member: Member): Member { + return { + ...member, + }; + } + + const Row = (props: { row: ReturnType, onSelect }) => ( + + *': { borderBottom: 'unset' } }}> + {/* + setOpen(!open)} + > + {open ? : } + + */} + {/* { columns.map((column, index) => + { row[column.id] ?? '-' } + ) } */} + {/* TODO FIX DISPLAY DATA FROM RELATION */} + + {/* { id: 'member_id', label: 'MemberID', minWidth: 100, align: "left" }, + { id: 'principal_id', label: 'Mapping ID', minWidth: 100, align: "left" }, + { id: 'nik', label: 'NIK', minWidth: 100, align: "left" }, + { id: 'current_policy.policy_number', label: 'Policy Number', minWidth: 100, align: "left" }, + { id: 'effective_date', label: 'Effective Date', minWidth: 100, align: "left" }, + { id: 'name', label: 'Name', minWidth: 100, align: "left" }, + { id: 'nric', label: 'NRIC', minWidth: 100, align: "left" }, + { id: 'email', label: 'E-mail', minWidth: 100, align: "left" }, + { id: 'plan_id', label: 'PlanID', minWidth: 100, align: "left" }, + { id: 'termination_date', label: 'Termination Date', minWidth: 100, align: 'right' }, + { id: 'activation_date', label: 'Activation Date', minWidth: 100, align: "right" }, + */} + {props.row.member_id} + {props.row.principal_id} + {props.row.employeds?.[0]?.nik} + {props.row.current_policy?.policy_id} + {props.row.current_policy?.start} + {props.row.name} + {props.row.nric} + {props.row.email} + {props.row.current_plan?.code} + {props.row.current_policy?.start} + {props.row.current_policy?.end} + + + {/* COLLAPSIBLE ROW */} + {/* + + + + + No Extra Data + + + {false && } + + + */} + + ); + + const headStyle = { + fontWeight: 'bold', + }; + const TableContent = () => ( + + {/* ------------------ TABLE HEADER ------------------ */} + + + + + Type + + + Code + + + Name + + + Version + + + Status + + + Action + + + + {/* ------------------ END TABLE HEADER ------------------ */} + + {/* ------------------ TABLE ROW ------------------ */} + {dataTableLoading ? ( + + + + Loading + + + + ) : dataTableData.data.length === 0 ? ( + + + + No Data + + + + ) : ( + + {dataTableData.data.map((row) => ( + + ))} + + )} + {/* ------------------ END TABLE ROW ------------------ */} +
+ ); + + const getContent = () => ( + +
+ + + } + /> +
+ ); + + return ( + + ); +} diff --git a/frontend/dashboard/src/layouts/dashboard/navbar/NavConfig.tsx b/frontend/dashboard/src/layouts/dashboard/navbar/NavConfig.tsx index a7191de0..1072023d 100755 --- a/frontend/dashboard/src/layouts/dashboard/navbar/NavConfig.tsx +++ b/frontend/dashboard/src/layouts/dashboard/navbar/NavConfig.tsx @@ -62,9 +62,10 @@ const navConfig = [ }, { title: 'CASE MANAGEMENT', - children: [ - { title: 'Request', path: '/case-request' }, - ], + path: '/claims', + // children: [ + // { title: 'Request', path: '/case-request' }, + // ], }, { title: 'CUSTOMER SERVICES', diff --git a/frontend/dashboard/src/pages/Claims/Create.tsx b/frontend/dashboard/src/pages/Claims/Create.tsx index 5e9fccdf..a40c882e 100755 --- a/frontend/dashboard/src/pages/Claims/Create.tsx +++ b/frontend/dashboard/src/pages/Claims/Create.tsx @@ -1,38 +1,40 @@ import * as Yup from 'yup'; -import { yupResolver } from "@hookform/resolvers/yup"; -import { Card, Collapse, Divider, Grid, Stack, Typography } from "@mui/material"; -import { useForm } from "react-hook-form"; -import { useParams } 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 CorporateTabNavigations from "../CorporateTabNavigations"; -import DivisionsList from "./List"; -import { useMemo, useState } from 'react'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { Autocomplete, Button, Card, Collapse, Divider, Grid, Stack, TextField, Typography } from '@mui/material'; +import { Controller, useForm } from 'react-hook-form'; +import { useParams } 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'; +export default function Create() { + const [member, setMember] = useState(); + const selectedMemberDisplay = useRef(null); - -export default function Divisions() { - const { themeStretch } = useSettings(); - - const { corporate_id } = useParams(); - - const NewDivisionSchema = Yup.object().shape({ + const NewClaimSchema = 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'), + diagnosis_id: Yup.string().required('Diagnosis is required'), + total_claim: Yup.string().required('Total Claim is required'), }); const defaultValues = useMemo( () => ({ - code: '', + name: '', + diagnosis_id: null, + total_claim: 0, }), [] ); const methods = useForm({ - resolver: yupResolver(NewDivisionSchema), + resolver: yupResolver(NewClaimSchema), defaultValues, }); @@ -51,119 +53,79 @@ export default function Divisions() { console.log(data); }; - const [open, setOpen] = useState(false); + const [isMemberDialogOpen, setIsMemberDialogOpen] = useState(false); - const benefits = [ - { - 'category' : 'General Practitioner', - 'childs' : [ - { - 'name' : 'External Doctor Online', - 'code' : 'gp-external-doctor-online' - }, - { - 'name' : 'External Doctor Offline', - 'code' : 'gp-external-doctor-offline' - }, - { - 'name' : 'Internal Doctor Online', - 'code' : 'gp-internal-doctor-online' - }, - { - 'name' : 'Internal Doctor Offline', - 'code' : 'gp-internal-doctor-offline' - }, - ] - }, - { - 'category' : 'Specialist', - 'childs' : [ - { - 'name' : 'External Doctor Online', - 'code' : 'sp-external-doctor-online' - }, - { - 'name' : 'External Doctor Offline', - 'code' : 'sp-external-doctor-offline' - }, - { - 'name' : 'Internal Doctor Online', - 'code' : 'sp-internal-doctor-online' - }, - { - 'name' : 'Internal Doctor Offline', - 'code' : 'sp-internal-doctor-offline' - }, - ] - }, - { - 'category' : 'Medicines', - 'childs' : [ - { - 'name' : 'Vitamins', - 'code' : 'medicines-vitamins' - }, - { - 'name' : 'Delivery Fee', - 'code' : 'medicines-delivery-fee' - }, - ] - }, - ]; + const memberSelected = (selectedMember: any) => { + setMember(selectedMember); + }; - const products = [ - { - 'name' : 'Inpatient', - 'code' : 'IP', - }, - { - 'name' : 'Outpatient', - 'code' : 'OP', - }, - { - 'name' : 'Dental', - 'code' : 'DT', - }, - { - 'name' : 'Dental', - 'code' : 'DTL', - }, - { - 'name' : 'Matternity', - 'code' : 'MT', - }, - { - 'name' : 'Special Benefit', - 'code' : 'SB', - }, - ]; + const [diagnosis, setDiagnosis] = useState([]); + + const searchDiagnosis = (search) => { + axios.get('master/diagnosis/search', {params: {search}}) + .then(function(res) { + setDiagnosis(res.data); + }) + } + + useEffect(() => { // Trigger First Search + axios.get('master/diagnosis/search') + .then(function(res) { + setDiagnosis(res.data); + }) + + }, []) + + const [isEligible, setIsEligible] = useState(false) + + useEffect(() => { + setIsEligible(false) + console.log('member change') + }, [member]) + + const [isCheckingLimit, setIsCheckingLimit] = useState(false) + const checkLimit = (event) => { + event.preventDefault(); + console.log(getValues('diagnosis_id')) + if (!member || !getValues('diagnosis_id')) { + enqueueSnackbar('Please Check the Data', { variant: 'error' }) + + return false; + } + + setIsCheckingLimit(true) + axios.post('check-limit', { + 'member_id' : member.id, + 'diagnosis' : getValues('diagnosis_id'), + 'total_claim' : getValues('total_claim') + }) + .then((res) => { + setIsEligible(true) + }) + .catch((err) => { + enqueueSnackbar('Failed Checking Limit : ' + err.message ?? '', { variant: 'error' }) + }) + .then(() => { + setIsCheckingLimit(false) + }) + } return ( - - + - @@ -171,71 +133,96 @@ export default function Divisions() { - Benefit Detail + Member - - - - - Benefit Configuration - - - }> - - - {benefits.map(row => ( - - {row.category} - - {row.childs.map(benefit => ( - - - - ))} - - - ))} - Admin Fee - - {benefits.map(row => ( - - - - ))} - - - - - - - {benefits.map(row => ( - - {row.category} - - {row.childs.map(benefit => ( - - - - ))} - - - ))} - Admin Fee - - {benefits.map(row => ( - - - - ))} - - + + + + + + + + + { member && ( + +
+ Plan : {JSON.stringify(member)} +
+
+ Benefit : +
+
+ Another : +
+
+ )} + + ( + + option ? `(${option.code}) ${option.name}` : '' + } + value={value || ''} + onChange={(event: any, newValue: any) => { + setValue('diagnosis_id', newValue?.id) + onChange(newValue); + }} + renderInput={(params) => ( + { + searchDiagnosis(event.target.value) + }} + /> + )} + /> + )} + /> + + + + { isEligible === true ? ( + + Create Claim + + ) : ( + + Check Limit + + )}
+ +
); } diff --git a/frontend/dashboard/src/pages/Claims/List.tsx b/frontend/dashboard/src/pages/Claims/List.tsx index 79772d12..db8a8930 100755 --- a/frontend/dashboard/src/pages/Claims/List.tsx +++ b/frontend/dashboard/src/pages/Claims/List.tsx @@ -1,5 +1,5 @@ // @mui -import { Box, Button, Card, Collapse, IconButton, MenuItem, Table, TableBody, TableCell, TableRow, TextField, Typography, Stack, Menu, ButtonGroup } from '@mui/material'; +import { Box, Button, Card, Collapse, IconButton, MenuItem, Table, TableBody, TableCell, TableRow, TextField, Typography, Stack, Menu, ButtonGroup, Link } from '@mui/material'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; import AddIcon from '@mui/icons-material/Add'; @@ -11,7 +11,8 @@ import { useSearchParams } from 'react-router-dom'; // components import axios from '../../utils/axios'; import { LaravelPaginatedData, LaravelPaginatedDataDefault } from '../../@types/paginated-data'; -import DataTable from './LaravelTable'; +import DataTable from '../../components/LaravelTable'; +import { fCurrency } from '../../utils/formatNumber'; export default function List() { const [searchParams, setSearchParams] = useSearchParams(); @@ -102,57 +103,17 @@ export default function List() { return (
- - {( !currentImportFileName && + - {/*

kjasndkjandskjasndkjansdkjansd

*/} - - - Import - Download Template - +
- )} - - {( currentImportFileName && - - - - - - - - )} - {( importResult && - - Last Import Result Report : {importResult.result_file?.name ?? "-"} - - )}
); } @@ -164,7 +125,7 @@ export default function List() { const loadDataTableData = async (appliedFilter : any | null = null) => { setDataTableLoading(true); const filter = appliedFilter ? appliedFilter : Object.fromEntries([...searchParams.entries()]); - const response = await axios.get('/master/drugs', { params: filter }); + const response = await axios.get('/claims', { params: filter }); // console.log(response.data); setDataTableLoading(false); @@ -191,7 +152,7 @@ export default function List() { }; // Called on every row to map the data to the columns - function createData( data: Icd ): Icd { + function createData( data: any ): any { return { ...data, } @@ -214,13 +175,14 @@ export default function List() { {open ? : } - {row.type} {row.code} - {row.name} - {row.version} + {row.member.full_name} + {row.plan.code} + {row.benefit.code} + ({row.diagnosis.code}) {row.diagnosis.name} + {fCurrency(row.total_claim)} - - + {/* */} {/* COLLAPSIBLE ROW */} @@ -246,12 +208,13 @@ export default function List() { - Type Code - Name - Version - Status - Action + Member Name + Plan + Benefit + Diagnosis + Total Claim + {/* Action */} {/* ------------------ END TABLE HEADER ------------------ */} diff --git a/frontend/dashboard/src/routes/index.tsx b/frontend/dashboard/src/routes/index.tsx index 65e8f05b..a4c07e68 100755 --- a/frontend/dashboard/src/routes/index.tsx +++ b/frontend/dashboard/src/routes/index.tsx @@ -211,6 +211,10 @@ export default function Router() { path: 'claims', element: , }, + { + path: 'claims/create', + element: + } ] }, // { @@ -294,4 +298,5 @@ const CorporateServicesCreate = Loadable(lazy(() => import('../pages/Corporates/ const CorporateHospitals = Loadable(lazy(() => import('../pages/Corporates/Hospital/Index'))); const CorporateClaimHistories = Loadable(lazy(() => import('../pages/Corporates/ClaimHistory/Index'))); -const Claims = Loadable(lazy(() => import('../pages/Claims/Index'))); \ No newline at end of file +const Claims = Loadable(lazy(() => import('../pages/Claims/Index'))); +const ClaimsCreate = Loadable(lazy(() => import('../pages/Claims/Create'))); \ No newline at end of file From f5372e5d0aa42845dc582b828f6c693d1615bc7e Mon Sep 17 00:00:00 2001 From: R Date: Mon, 5 Dec 2022 04:30:00 +0700 Subject: [PATCH 4/7] [WIP] Store Limit --- .../Http/Controllers/Api/ClaimController.php | 28 +- .../Http/Controllers/Api/MemberController.php | 11 +- Modules/Internal/Routes/api.php | 2 + Modules/Internal/Services/ClaimService.php | 115 ++++++++ .../Services/MemberEnrollmentService.php | 3 +- app/Http/Controllers/Api/MemberController.php | 1 + app/Models/Claim.php | 24 ++ app/Models/Corporate.php | 5 + app/Models/CorporatePolicy.php | 12 + app/Models/Icd.php | 9 + app/Models/LimitJournal.php | 15 +- app/Models/Member.php | 40 ++- ..._23_140658_create_limit_journals_table.php | 4 +- frontend/client-portal/package.json | 1 + frontend/client-portal/pnpm-lock.yaml | 62 +++-- .../components/dialogs/MemberSelectDialog.tsx | 2 +- .../dashboard/src/pages/Claims/Create.tsx | 258 +++++++++++++++--- frontend/dashboard/src/pages/Claims/Index.tsx | 1 + frontend/dashboard/src/pages/Claims/List.tsx | 8 +- 19 files changed, 525 insertions(+), 76 deletions(-) create mode 100644 Modules/Internal/Services/ClaimService.php diff --git a/Modules/Internal/Http/Controllers/Api/ClaimController.php b/Modules/Internal/Http/Controllers/Api/ClaimController.php index 4909f83d..876c2d82 100644 --- a/Modules/Internal/Http/Controllers/Api/ClaimController.php +++ b/Modules/Internal/Http/Controllers/Api/ClaimController.php @@ -2,10 +2,14 @@ namespace Modules\Internal\Http\Controllers\Api; +use App\Models\Benefit; use App\Models\Claim; +use App\Models\Icd; +use App\Models\Member; use Illuminate\Contracts\Support\Renderable; use Illuminate\Http\Request; use Illuminate\Routing\Controller; +use Modules\Internal\Services\ClaimService; class ClaimController extends Controller { @@ -21,6 +25,7 @@ class ClaimController extends Controller 'plan', 'benefit' ]) + ->latest() ->paginate(10); return response()->json($claims); @@ -42,7 +47,28 @@ class ClaimController extends Controller */ public function store(Request $request) { - // + $request->validate([ + 'diagnosis_id' => 'required', + 'member_id' => 'required', + 'total_claim' => 'required', + 'benefit_id' => 'required' + ]); + + // return response()->json($request->toArray()); + + $member = Member::find($request->member_id); + $benefit = Benefit::find($request->benefit_id); + $diagnosis = Icd::find($request->diagnosis_id); + + // Check Eligibility + $validation = ClaimService::checkMemberEligibility($member, $benefit, $diagnosis, $request->total_claim); + + // Store Claim + if ($validation['isEligible']) { + $claim = ClaimService::storeClaim($member, $diagnosis, $request->total_claim, $benefit); + } + + return response()->json($claim); } /** diff --git a/Modules/Internal/Http/Controllers/Api/MemberController.php b/Modules/Internal/Http/Controllers/Api/MemberController.php index f8defb31..96925c7b 100644 --- a/Modules/Internal/Http/Controllers/Api/MemberController.php +++ b/Modules/Internal/Http/Controllers/Api/MemberController.php @@ -15,7 +15,9 @@ class MemberController extends Controller */ public function index() { - return Member::paginate(); + return Member::query() + ->with('currentPlan') + ->paginate(); } /** @@ -77,4 +79,11 @@ class MemberController extends Controller { // } + + public function benefits($member_id) + { + $member = Member::findOrFail($member_id); + + return response()->json($member->currentPlan->benefits()->select(['description', 'code', 'id'])->get()); + } } diff --git a/Modules/Internal/Routes/api.php b/Modules/Internal/Routes/api.php index 57709300..c9eab7ed 100755 --- a/Modules/Internal/Routes/api.php +++ b/Modules/Internal/Routes/api.php @@ -96,8 +96,10 @@ Route::prefix('internal')->group(function () { Route::post('master/formulariums/import', [FormulariumController::class, 'import']); Route::get('members', [MemberController::class, 'index']); + Route::get('members/{member_id}/benefits', [MemberController::class, 'benefits']); Route::get('claims', [ClaimController::class, 'index']); + Route::post('claims', [ClaimController::class, 'store']); Route::post('check-limit', [ClaimController::class, 'checkLimit']); }); diff --git a/Modules/Internal/Services/ClaimService.php b/Modules/Internal/Services/ClaimService.php new file mode 100644 index 00000000..569cf8dd --- /dev/null +++ b/Modules/Internal/Services/ClaimService.php @@ -0,0 +1,115 @@ +currentPlan; + $policy = $member->currentPolicy; + $corporate = $policy->corporate; + + + $isEligible = true; + $validationErrors = []; + + // Eligibility Validation + + if (!in_array($member->marital_status, explode(',', $benefit->msc))) { + $validationErrors[] = [ + 'error' => 'msc', + 'message' => 'Only '.$benefit->msc + ]; + $isEligible = false; + } + + if (!in_array($member->gender_code, explode(',', $benefit->genders))) { + $validationErrors[] = [ + 'error' => 'genders', + 'message' => 'Only '.$benefit->genders + ]; + $isEligible = false; + } + + if (!empty($benefit->min_age) && $member->age < $benefit->min_age) { + $validationErrors[] = [ + 'error' => 'min_age', + 'message' => 'Minimum Age is '.$benefit->min_age + ]; + $isEligible = false; + } + + if (!empty($benefit->max_age) && $member->age > $benefit->max_age) { + $validationErrors[] = [ + 'error' => 'max_age', + 'message' => 'Maximum Age is '.$benefit->min_age + ]; + $isEligible = false; + } + + // TODO complete validations + + // Limit Validation + if ($totalClaim > 0) { + if (bcsub($corporate->limit_balance, $totalClaim) < 0) { + $validationErrors[] = [ + 'error' => 'corporate_limit', + 'message' => 'Corporate Limit cannot cover this' + ]; + $isEligible = false; + } + + if (bcsub($benefit->limit_amount, $totalClaim) < 0) { + $validationErrors[] = [ + 'error' => 'benefit_limit', + 'message' => 'Benefit Limit cannot cover this' + ]; + $isEligible = false; + } + + // TODO complete validations + + } + + return [ + 'isEligible' => $isEligible, + 'errors' => $validationErrors + ]; + } + + public static function storeClaim($member, $diagnosis, $totalClaim, $benefit) + { + try { + DB::beginTransaction(); + + $claim = Claim::create([ + 'member_id' => $member->id, + 'diagnosis_id' => $diagnosis->id, + 'total_claim' => $totalClaim, + 'currency' => 'IDR', + 'plan_id' => $member->currentPlan->id, + 'benefit_id' => $benefit->id, + ]); + + $policy = $member->currentPolicy; + $policy->limitJournals()->create([ + 'previous_balance' => $policy->limit_balance, + 'total_credit' => $totalClaim, + 'type' => 'credit', + 'balance' => bcsub($policy->limit_balance, $totalClaim), + 'description' => 'Log for Claim #'. $claim->code, + ]); + + DB::commit(); + return $claim; + } catch (\Exception $error) { + DB::rollBack(); + + return false; + } + } +} \ No newline at end of file diff --git a/Modules/Internal/Services/MemberEnrollmentService.php b/Modules/Internal/Services/MemberEnrollmentService.php index fc45e427..50a7c283 100755 --- a/Modules/Internal/Services/MemberEnrollmentService.php +++ b/Modules/Internal/Services/MemberEnrollmentService.php @@ -427,6 +427,7 @@ class MemberEnrollmentService // TODO validate corporate plan $plan = Plan::query() ->where('code', $row['plan_id']) + ->where('corporate_id', $corporate->id) ->first(); if (!$plan) { throw new ImportRowException(__('enrollment.PLAN_NOT_FOUND'), 0, null, $row); @@ -461,7 +462,7 @@ class MemberEnrollmentService ]); $member->memberPlans()->create([ - 'plan_id' => $plan->code, + 'plan_id' => $plan->id, 'status' => 'active', 'start' => Carbon::parse(strtotime($row['member_effective_date'])), 'end' => Carbon::parse(strtotime($row['member_expiry_date'])), diff --git a/app/Http/Controllers/Api/MemberController.php b/app/Http/Controllers/Api/MemberController.php index 164aa93b..97bcfd29 100755 --- a/app/Http/Controllers/Api/MemberController.php +++ b/app/Http/Controllers/Api/MemberController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; +use App\Models\Member; use Illuminate\Http\Request; class MemberController extends Controller diff --git a/app/Models/Claim.php b/app/Models/Claim.php index 39919309..5c5d6b63 100644 --- a/app/Models/Claim.php +++ b/app/Models/Claim.php @@ -4,6 +4,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Str; class Claim extends Model { @@ -19,6 +20,28 @@ class Claim extends Model 'benefit_id', ]; + protected $hidden = [ + 'created_at', + 'updated_at', + 'deleted_at', + 'created_by', + 'updated_by', + 'deleted_by', + ]; + + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + try { + $model->code = (string) Str::orderedUuid(); // generate uuid + } catch (\Exception $e) { + abort(500, $e->getMessage()); + } + }); + } + public function member() { return $this->belongsTo(Member::class, 'member_id'); @@ -38,4 +61,5 @@ class Claim extends Model { return $this->belongsTo(Benefit::class, 'benefit_id'); } + } diff --git a/app/Models/Corporate.php b/app/Models/Corporate.php index d8147665..04ad740f 100755 --- a/app/Models/Corporate.php +++ b/app/Models/Corporate.php @@ -106,4 +106,9 @@ class Corporate extends Model { return $this->hasMany(Corporate::class, 'parent_id'); } + + public function getLimitBalanceAttribute() + { + return $this->currentPolicy->limit_balance; + } } diff --git a/app/Models/CorporatePolicy.php b/app/Models/CorporatePolicy.php index 1e8d99d5..7ccde342 100755 --- a/app/Models/CorporatePolicy.php +++ b/app/Models/CorporatePolicy.php @@ -34,6 +34,11 @@ class CorporatePolicy extends Model return $this->belongsTo(Corporate::class); } + public function limitJournals() + { + return $this->morphMany(LimitJournal::class, 'journalable'); + } + public function setCodeAttribute($value) { $this->attributes['code'] = !empty($value) ? $value : Str::upper(Str::random('6')); @@ -53,4 +58,11 @@ class CorporatePolicy extends Model { $this->attributes['end'] = !empty($value) ? Carbon::parse($value)->format('Y-m-d') : null; } + + public function getLimitBalanceAttribute() + { + $journal = $this->limitJournals()->latest()->first(); + + return $journal ? $journal->balance : (!empty($this->total_premi) ? $this->total_premi : "0"); + } } diff --git a/app/Models/Icd.php b/app/Models/Icd.php index 40115736..9dfa57ff 100755 --- a/app/Models/Icd.php +++ b/app/Models/Icd.php @@ -27,6 +27,15 @@ class Icd extends Model 'active' ]; + protected $hidden = [ + 'created_at', + 'updated_at', + 'deleted_at', + 'created_by', + 'updated_by', + 'deleted_by', + ]; + public function getTypeAttribute() { return 'ICD-'.$this->rev; diff --git a/app/Models/LimitJournal.php b/app/Models/LimitJournal.php index abba9b09..707715e9 100644 --- a/app/Models/LimitJournal.php +++ b/app/Models/LimitJournal.php @@ -11,7 +11,9 @@ class LimitJournal extends Model protected $fillable = [ 'journalable', - 'mutation', + 'previous_balance', + 'total_credit', + 'total_debit', 'type', 'balance', 'description', @@ -21,4 +23,15 @@ class LimitJournal extends Model { return $this->morphTo(); } + + public function setTotalCreditAttribute($value) + { + $this->attributes['total_credit'] = empty($value) ? 0 : $value; + } + + public function setTotalDebitAttribute($value) + { + $this->attributes['total_debit'] = empty($value) ? 0 : $value; + } + } diff --git a/app/Models/Member.php b/app/Models/Member.php index 759b47bd..dd499477 100755 --- a/app/Models/Member.php +++ b/app/Models/Member.php @@ -57,7 +57,20 @@ class Member extends Model "end_no_claim", ]; - protected $appends = ['full_name']; + protected $appends = [ + 'full_name', + 'age', + 'gender_code' + ]; + + protected $hidden = [ + 'created_at', + 'updated_at', + 'deleted_at', + 'created_by', + 'updated_by', + 'deleted_by', + ]; public function claims() { @@ -105,12 +118,12 @@ class Member extends Model public function plans() { - return $this->hasManyThrough(Plan::class, MemberPlan::class, 'member_id', 'code', 'id', 'plan_id'); + return $this->hasManyThrough(Plan::class, MemberPlan::class, 'member_id', 'id', 'id', 'plan_id'); } public function currentPlan() { - return $this->hasOneThrough(Plan::class, MemberPlan::class, 'member_id', 'code', 'id', 'plan_id')->latest(); + return $this->hasOneThrough(Plan::class, MemberPlan::class, 'member_id', 'id', 'id', 'plan_id')->latest(); } public function policies() @@ -120,7 +133,21 @@ class Member extends Model public function currentPolicy() { - return $this->hasOne(MemberPolicy::class, 'member_id', 'member_id')->where('status', 'active')->latestOfMany(); + return $this->hasOneThrough(CorporatePolicy::class, MemberPolicy::class, 'member_id', 'code', 'member_id', 'policy_id') + ->where('corporate_policies.start', '<', now()) + ->where('corporate_policies.end', '>', now()) + ->where('member_policies.start', '<', now()) + ->where('member_policies.end', '>', now()); + // return $this->hasOne(MemberPolicy::class, 'member_id', 'member_id')->where('status', 'active')->latestOfMany(); + } + + public function getAgeAttribute() + { + if ($this->birth_date) { + return Carbon::parse($this->birth_date)->diffInYears(now()); + } else { + return null; + } } public function getFullNameAttribute() @@ -137,6 +164,11 @@ class Member extends Model return implode(' ', $arr); } + public function getGenderCodeAttribute() + { + return $this->gender ? ($this->gender == 'female' ? 'F' : 'M') : $this->gender; + } + public function scopeFilter($query, array $filters) { $query->when($filters['search'] ?? false, function ($query, $search) { diff --git a/database/migrations/2022_11_23_140658_create_limit_journals_table.php b/database/migrations/2022_11_23_140658_create_limit_journals_table.php index ff82510a..3682ca24 100644 --- a/database/migrations/2022_11_23_140658_create_limit_journals_table.php +++ b/database/migrations/2022_11_23_140658_create_limit_journals_table.php @@ -16,7 +16,9 @@ return new class extends Migration Schema::create('limit_journals', function (Blueprint $table) { $table->id(); $table->morphs('journalable'); - $table->string('mutation'); + $table->string('previous_balance'); + $table->string('total_debit')->nullable()->comment('Should be in positive value'); + $table->string('total_credit')->nullable()->comment('Should be in negative value'); $table->string('type'); $table->string('balance'); $table->text('description'); diff --git a/frontend/client-portal/package.json b/frontend/client-portal/package.json index 0ee98044..15207f05 100755 --- a/frontend/client-portal/package.json +++ b/frontend/client-portal/package.json @@ -47,6 +47,7 @@ "@mui/lab": "5.0.0-alpha.80", "@mui/material": "^5.10.2", "@mui/system": "^5.10.2", + "@mui/utils": "^5.10.16", "@mui/x-data-grid": "^5.16.0", "@mui/x-date-pickers": "5.0.0-beta.2", "@vitejs/plugin-react": "^1.3.2", diff --git a/frontend/client-portal/pnpm-lock.yaml b/frontend/client-portal/pnpm-lock.yaml index f4c3bca4..7e4cba0c 100755 --- a/frontend/client-portal/pnpm-lock.yaml +++ b/frontend/client-portal/pnpm-lock.yaml @@ -15,6 +15,7 @@ specifiers: '@mui/lab': 5.0.0-alpha.80 '@mui/material': ^5.10.2 '@mui/system': ^5.10.2 + '@mui/utils': ^5.10.16 '@mui/x-data-grid': ^5.16.0 '@mui/x-date-pickers': 5.0.0-beta.2 '@types/lodash': ^4.14.184 @@ -85,6 +86,7 @@ dependencies: '@mui/lab': 5.0.0-alpha.80_vli2fzedtzvbon5ke6iprktoka '@mui/material': 5.10.2_mm2isjkwxtdnw3xlwl5r7b6ile '@mui/system': 5.10.2_ynexvg6j2j66sbh5giu7mkkh7u + '@mui/utils': 5.10.16_react@17.0.2 '@mui/x-data-grid': 5.16.0_r4jqxufjb3aftjrjm24vhpn4hm '@mui/x-date-pickers': 5.0.0-beta.2_y3fv7pzpxqpbmxcbzsros3kjnu '@vitejs/plugin-react': 1.3.2 @@ -1455,14 +1457,20 @@ packages: engines: {node: '>=6.9.0'} dependencies: core-js-pure: 3.25.0 - regenerator-runtime: 0.13.9 + regenerator-runtime: 0.13.11 dev: true /@babel/runtime/7.18.9: resolution: {integrity: sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==} engines: {node: '>=6.9.0'} dependencies: - regenerator-runtime: 0.13.9 + regenerator-runtime: 0.13.11 + + /@babel/runtime/7.20.6: + resolution: {integrity: sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.11 /@babel/template/7.18.10: resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==} @@ -1554,7 +1562,7 @@ packages: '@babel/core': 7.18.13 '@babel/helper-module-imports': 7.18.6 '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.18.13 - '@babel/runtime': 7.18.9 + '@babel/runtime': 7.20.6 '@emotion/hash': 0.9.0 '@emotion/memoize': 0.8.0 '@emotion/serialize': 1.1.0 @@ -1838,10 +1846,10 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.18.9 + '@babel/runtime': 7.20.6 '@emotion/is-prop-valid': 1.2.0 '@mui/types': 7.1.5_@types+react@17.0.48 - '@mui/utils': 5.9.3_react@17.0.2 + '@mui/utils': 5.10.16_react@17.0.2 '@popperjs/core': 2.11.6 '@types/react': 17.0.48 clsx: 1.2.1 @@ -1862,10 +1870,10 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.18.9 + '@babel/runtime': 7.20.6 '@emotion/is-prop-valid': 1.2.0 '@mui/types': 7.1.5_@types+react@17.0.48 - '@mui/utils': 5.9.3_react@17.0.2 + '@mui/utils': 5.10.16_react@17.0.2 '@popperjs/core': 2.11.6 '@types/react': 17.0.48 clsx: 1.2.1 @@ -1924,7 +1932,7 @@ packages: '@mui/base': 5.0.0-alpha.79_sk3eihvpffgp52mstba5zhq3vu '@mui/material': 5.10.2_mm2isjkwxtdnw3xlwl5r7b6ile '@mui/system': 5.10.2_ynexvg6j2j66sbh5giu7mkkh7u - '@mui/utils': 5.9.3_react@17.0.2 + '@mui/utils': 5.10.16_react@17.0.2 '@mui/x-date-pickers': 5.0.0-alpha.0_ktjudfp74mtqzmc2e56ri5vvri '@types/react': 17.0.48 clsx: 1.2.1 @@ -1964,7 +1972,7 @@ packages: '@mui/core-downloads-tracker': 5.10.2 '@mui/system': 5.10.2_ynexvg6j2j66sbh5giu7mkkh7u '@mui/types': 7.1.5_@types+react@17.0.48 - '@mui/utils': 5.9.3_react@17.0.2 + '@mui/utils': 5.10.16_react@17.0.2 '@types/react': 17.0.48 '@types/react-transition-group': 4.4.5 clsx: 1.2.1 @@ -1986,8 +1994,8 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.18.9 - '@mui/utils': 5.9.3_react@17.0.2 + '@babel/runtime': 7.20.6 + '@mui/utils': 5.10.16_react@17.0.2 '@types/react': 17.0.48 prop-types: 15.8.1 react: 17.0.2 @@ -2006,7 +2014,7 @@ packages: '@emotion/styled': optional: true dependencies: - '@babel/runtime': 7.18.9 + '@babel/runtime': 7.20.6 '@emotion/cache': 11.10.3 '@emotion/react': 11.10.0_u65opluo5sjgh3eblfliq6jsnm '@emotion/styled': 11.10.0_mpqqkyoc5j3vv2sg2f5vubqi7u @@ -2037,7 +2045,7 @@ packages: '@mui/private-theming': 5.9.3_skqlhrap4das3cz5b6iqdn2lqi '@mui/styled-engine': 5.10.2_2av45khroaevmcc27aulpaf2sy '@mui/types': 7.1.5_@types+react@17.0.48 - '@mui/utils': 5.9.3_react@17.0.2 + '@mui/utils': 5.10.16_react@17.0.2 '@types/react': 17.0.48 clsx: 1.2.1 csstype: 3.1.0 @@ -2056,13 +2064,13 @@ packages: '@types/react': 17.0.48 dev: false - /@mui/utils/5.9.3_react@17.0.2: - resolution: {integrity: sha512-l0N5bcrenE9hnwZ/jPecpIRqsDFHkPXoFUcmkgysaJwVZzJ3yQkGXB47eqmXX5yyGrSc6HksbbqXEaUya+siew==} + /@mui/utils/5.10.16_react@17.0.2: + resolution: {integrity: sha512-3MB/SGsgiiu9Z55CFmAfiONUoR7AAue/H4F6w3mc2LnhFQCsoVvXhioDPcsiRpUMIQr34jDPzGXdCuqWooPCXQ==} engines: {node: '>=12.0.0'} peerDependencies: react: ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.18.9 + '@babel/runtime': 7.20.6 '@types/prop-types': 15.7.5 '@types/react-is': 17.0.3 prop-types: 15.8.1 @@ -2082,7 +2090,7 @@ packages: '@babel/runtime': 7.18.9 '@mui/material': 5.10.2_mm2isjkwxtdnw3xlwl5r7b6ile '@mui/system': 5.10.2_ynexvg6j2j66sbh5giu7mkkh7u - '@mui/utils': 5.9.3_react@17.0.2 + '@mui/utils': 5.10.16_react@17.0.2 clsx: 1.2.1 prop-types: 15.8.1 react: 17.0.2 @@ -2117,7 +2125,7 @@ packages: '@date-io/moment': 2.15.0 '@mui/material': 5.10.2_mm2isjkwxtdnw3xlwl5r7b6ile '@mui/system': 5.10.2_ynexvg6j2j66sbh5giu7mkkh7u - '@mui/utils': 5.9.3_react@17.0.2 + '@mui/utils': 5.10.16_react@17.0.2 clsx: 1.2.1 date-fns: 2.29.2 prop-types: 15.8.1 @@ -2165,7 +2173,7 @@ packages: '@emotion/styled': 11.10.0_mpqqkyoc5j3vv2sg2f5vubqi7u '@mui/material': 5.10.2_mm2isjkwxtdnw3xlwl5r7b6ile '@mui/system': 5.10.2_ynexvg6j2j66sbh5giu7mkkh7u - '@mui/utils': 5.9.3_react@17.0.2 + '@mui/utils': 5.10.16_react@17.0.2 '@types/react-transition-group': 4.4.5 clsx: 1.2.1 date-fns: 2.29.2 @@ -2708,7 +2716,7 @@ packages: resolution: {integrity: sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==} engines: {node: '>=6.0'} dependencies: - '@babel/runtime': 7.18.9 + '@babel/runtime': 7.20.6 '@babel/runtime-corejs3': 7.18.9 dev: true @@ -2798,7 +2806,7 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} dependencies: - '@babel/runtime': 7.18.9 + '@babel/runtime': 7.20.6 cosmiconfig: 7.0.1 resolve: 1.22.1 @@ -3192,7 +3200,7 @@ packages: /dom-helpers/5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: - '@babel/runtime': 7.18.9 + '@babel/runtime': 7.20.6 csstype: 3.1.0 dev: false @@ -5100,7 +5108,7 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' dependencies: - '@babel/runtime': 7.18.9 + '@babel/runtime': 7.20.6 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -5127,13 +5135,13 @@ packages: resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} dev: true - /regenerator-runtime/0.13.9: - resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} + /regenerator-runtime/0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} /regenerator-transform/0.15.0: resolution: {integrity: sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==} dependencies: - '@babel/runtime': 7.18.9 + '@babel/runtime': 7.20.6 dev: true /regexp.prototype.flags/1.4.3: @@ -5846,7 +5854,7 @@ packages: '@apideck/better-ajv-errors': 0.3.6_ajv@8.11.0 '@babel/core': 7.18.13 '@babel/preset-env': 7.18.10_@babel+core@7.18.13 - '@babel/runtime': 7.18.9 + '@babel/runtime': 7.20.6 '@rollup/plugin-babel': 5.3.1_2uin6pbxavst3oir53roxbd5qi '@rollup/plugin-node-resolve': 11.2.1_rollup@2.78.1 '@rollup/plugin-replace': 2.4.2_rollup@2.78.1 diff --git a/frontend/dashboard/src/components/dialogs/MemberSelectDialog.tsx b/frontend/dashboard/src/components/dialogs/MemberSelectDialog.tsx index 3d336714..3d1662af 100644 --- a/frontend/dashboard/src/components/dialogs/MemberSelectDialog.tsx +++ b/frontend/dashboard/src/components/dialogs/MemberSelectDialog.tsx @@ -251,7 +251,7 @@ export default function MemberSelectDialog({ const getContent = () => (
- + (null); const NewClaimSchema = Yup.object().shape({ - name: Yup.string().required('Name is required'), - diagnosis_id: Yup.string().required('Diagnosis is required'), - total_claim: Yup.string().required('Total Claim is required'), + member_id: Yup.string().required('Please select Member'), + total_claim: Yup.number().typeError('Claim should be a number').min(1, 'Claim cannot 0').required('Total Claim is required'), + diagnosis: Yup.object().required('Choose Diagnosis'), + benefit: Yup.object().required('Please Select Benefit') }); const defaultValues = useMemo( () => ({ - name: '', + member_id: null, + benefit_id: null, diagnosis_id: null, total_claim: 0, + benefit: null }), [] ); @@ -50,13 +57,41 @@ export default function Create() { } = methods; const onSubmit = async (data: any) => { - console.log(data); + axios.post('claims', getValues()) + .then(function(res) { + console.log('SUCCESS', res) + enqueueSnackbar('Success Creating Claim', { variant: 'success' }) + navigate('/claims'); + }) + .catch(function (err) { + console.log('ERROR CUK', err.data) + enqueueSnackbar('Failed Creating Claim', { variant: 'error' }) + }) }; + const [memberBenefits, setMemberBenefits] = useState([]); + const getMemberBenefits = (member) => { + axios.get(`members/${member.id}/benefits`) + .then( (res) => { + setMemberBenefits(res.data); + }) + .catch( (err) => { + enqueueSnackbar('Failed getting member benefits', { variant: 'error' }) + }) + } + const [isMemberDialogOpen, setIsMemberDialogOpen] = useState(false); const memberSelected = (selectedMember: any) => { + // Reset Selected Benefit + setMemberBenefits([]); + setValue('benefit_id', null); + setValue('benefit', null); + setMember(selectedMember); + setValue('member_id', selectedMember.id) + + getMemberBenefits(selectedMember) }; const [diagnosis, setDiagnosis] = useState([]); @@ -76,16 +111,12 @@ export default function Create() { }, []) - const [isEligible, setIsEligible] = useState(false) - - useEffect(() => { - setIsEligible(false) - console.log('member change') - }, [member]) + const [isEligible, setIsEligible] = useState(null) const [isCheckingLimit, setIsCheckingLimit] = useState(false) const checkLimit = (event) => { event.preventDefault(); + console.log(getValues('diagnosis_id')) if (!member || !getValues('diagnosis_id')) { enqueueSnackbar('Please Check the Data', { variant: 'error' }) @@ -109,6 +140,10 @@ export default function Create() { setIsCheckingLimit(false) }) } + + const headStyle = { + fontWeight: 'bold' + }; return ( @@ -136,41 +171,115 @@ export default function Create() { Member - - + {setIsMemberDialogOpen(true)}} /> - + {/* - + */} { member && ( -
- Plan : {JSON.stringify(member)} -
-
- Benefit : -
-
- Another : -
+ + + + + + 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) + // }} + /> + )} + /> + )} + /> + { setValue('diagnosis_id', newValue?.id) + // setValue('diagnosis', newValue) onChange(newValue); }} renderInput={(params) => ( - { - searchDiagnosis(event.target.value) + onKeyPress={(event) => { + if (event.key === 'Enter') + searchDiagnosis(event.target.value) }} /> )} @@ -200,10 +312,86 @@ export default function Create() { )} /> + { 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 + + + )} + + + + + Create Claim + { isEligible === true ? ( - + Create Claim ) : ( diff --git a/frontend/dashboard/src/pages/Claims/Index.tsx b/frontend/dashboard/src/pages/Claims/Index.tsx index 0c5ba319..15c2d7d8 100755 --- a/frontend/dashboard/src/pages/Claims/Index.tsx +++ b/frontend/dashboard/src/pages/Claims/Index.tsx @@ -14,6 +14,7 @@ export default function Claims() { {row.code} - {row.member.full_name} - {row.plan.code} - {row.benefit.code} - ({row.diagnosis.code}) {row.diagnosis.name} + {row.member?.full_name} + {row.plan?.code} + {row.benefit?.code} + ({row.diagnosis?.code}) {row.diagnosis?.name} {fCurrency(row.total_claim)} {/* */} From d3a806ff0bb44cc928fae9dee41042ea27a3e1ca Mon Sep 17 00:00:00 2001 From: R Date: Mon, 5 Dec 2022 04:37:50 +0700 Subject: [PATCH 5/7] [WIP] Fix Failed Creating Limit --- .../Internal/Http/Controllers/Api/ClaimController.php | 5 +++++ frontend/dashboard/src/pages/Claims/Create.tsx | 11 ++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Modules/Internal/Http/Controllers/Api/ClaimController.php b/Modules/Internal/Http/Controllers/Api/ClaimController.php index 876c2d82..ce98c742 100644 --- a/Modules/Internal/Http/Controllers/Api/ClaimController.php +++ b/Modules/Internal/Http/Controllers/Api/ClaimController.php @@ -66,6 +66,11 @@ class ClaimController extends Controller // Store Claim if ($validation['isEligible']) { $claim = ClaimService::storeClaim($member, $diagnosis, $request->total_claim, $benefit); + } else { + return response()->json([ + 'data' => $validation, + 'message' => $validation['errors'][0]['message'] + ], 403); } return response()->json($claim); diff --git a/frontend/dashboard/src/pages/Claims/Create.tsx b/frontend/dashboard/src/pages/Claims/Create.tsx index efb6aa69..948fd347 100755 --- a/frontend/dashboard/src/pages/Claims/Create.tsx +++ b/frontend/dashboard/src/pages/Claims/Create.tsx @@ -64,8 +64,8 @@ export default function ClaimsCreate() { navigate('/claims'); }) .catch(function (err) { - console.log('ERROR CUK', err.data) - enqueueSnackbar('Failed Creating Claim', { variant: 'error' }) + console.log('ERROR CUK', err) + enqueueSnackbar('Failed Creating Claim : '+ err.response.data.message, { variant: 'error' }) }) }; @@ -265,7 +265,7 @@ export default function ClaimsCreate() { }} renderInput={(params) => ( ( - - - Create Claim - { isEligible === true ? ( Create Claim From 3bc88777402da578b1dfff2249e5165c59c313f1 Mon Sep 17 00:00:00 2001 From: R Date: Mon, 5 Dec 2022 13:07:20 +0700 Subject: [PATCH 6/7] Fix Corporate Member --- .../Api/CorporateMemberController.php | 6 +----- app/Models/CorporateManager.php | 13 +++++++++++++ app/Models/CorporatePolicy.php | 15 ++++++++++++++- database/seeders/DummyMemberSeeder.php | 18 ++++++++++++++---- .../dashboard/src/pages/Corporates/Index.tsx | 10 ++++++++++ frontend/dashboard/src/routes/index.tsx | 4 ++++ 6 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 app/Models/CorporateManager.php diff --git a/Modules/Internal/Http/Controllers/Api/CorporateMemberController.php b/Modules/Internal/Http/Controllers/Api/CorporateMemberController.php index e3573b98..02963ee4 100755 --- a/Modules/Internal/Http/Controllers/Api/CorporateMemberController.php +++ b/Modules/Internal/Http/Controllers/Api/CorporateMemberController.php @@ -34,11 +34,7 @@ class CorporateMemberController extends Controller }) ->with([ 'employeds', - 'currentPolicy' => function ($policy) use ($corporate_id) { - $policy->whereHas('corporatePolicy', function($corporatePolicy) use ($corporate_id) { - $corporatePolicy->where('corporate_id', $corporate_id); - }); - } + 'currentPolicy', ]) ->with('currentPlan') ->paginate() diff --git a/app/Models/CorporateManager.php b/app/Models/CorporateManager.php new file mode 100644 index 00000000..bc3aa8a0 --- /dev/null +++ b/app/Models/CorporateManager.php @@ -0,0 +1,13 @@ +belongsTo(Corporate::class); @@ -39,6 +47,11 @@ class CorporatePolicy extends Model return $this->morphMany(LimitJournal::class, 'journalable'); } + public function latestLimitJournal() + { + return $this->morphOne(LimitJournal::class, 'journalable')->latestOfMany(); + } + public function setCodeAttribute($value) { $this->attributes['code'] = !empty($value) ? $value : Str::upper(Str::random('6')); @@ -61,7 +74,7 @@ class CorporatePolicy extends Model public function getLimitBalanceAttribute() { - $journal = $this->limitJournals()->latest()->first(); + $journal = $this->latestLimitJournal; return $journal ? $journal->balance : (!empty($this->total_premi) ? $this->total_premi : "0"); } diff --git a/database/seeders/DummyMemberSeeder.php b/database/seeders/DummyMemberSeeder.php index 361510f4..bb0ce713 100755 --- a/database/seeders/DummyMemberSeeder.php +++ b/database/seeders/DummyMemberSeeder.php @@ -16,9 +16,19 @@ class DummyMemberSeeder extends Seeder */ public function run() { - User::create([ - 'email' => 'admin@linksehat.dev', - 'password' => Hash::make('password') - ]); + $userEmails = [ + 'admin@linksehat.dev', + 'manager+one@gmail.com', + 'manager+two@gmail.com' + ]; + + foreach ($userEmails as $email) { + User::updateOrCreate([ + 'email' => $email + ], [ + 'email' => $email, + 'password' => Hash::make('password') + ]); + } } } diff --git a/frontend/dashboard/src/pages/Corporates/Index.tsx b/frontend/dashboard/src/pages/Corporates/Index.tsx index 5c0ea756..6f99353a 100755 --- a/frontend/dashboard/src/pages/Corporates/Index.tsx +++ b/frontend/dashboard/src/pages/Corporates/Index.tsx @@ -398,6 +398,16 @@ export default function Corporates() { ? fCurrency(row.current_policy?.minimal_deposit_net) : '-'} + + + Corporate Limit + + + :{' '} + {row.current_policy + ? fCurrency(row.current_policy?.limit_balance) + : '-'} + diff --git a/frontend/dashboard/src/routes/index.tsx b/frontend/dashboard/src/routes/index.tsx index a4c07e68..bc126492 100755 --- a/frontend/dashboard/src/routes/index.tsx +++ b/frontend/dashboard/src/routes/index.tsx @@ -214,6 +214,10 @@ export default function Router() { { path: 'claims/create', element: + }, + { + path: 'claims/:id', + element: } ] }, From 885edec290087305cb18419015200f06479a9537 Mon Sep 17 00:00:00 2001 From: R Date: Mon, 5 Dec 2022 16:20:09 +0700 Subject: [PATCH 7/7] [WIP] Update Claim Table --- .../Api/CorporateMemberController.php | 3 ++ app/Models/Claim.php | 16 ++++++++++ app/Models/Member.php | 5 ++-- app/Services/ClaimService.php | 17 +++++++++++ ...07_040543_create_corporate_plans_table.php | 30 +++++++++---------- .../2022_11_22_135948_create_claims_table.php | 15 ++++++++++ 6 files changed, 69 insertions(+), 17 deletions(-) diff --git a/Modules/Internal/Http/Controllers/Api/CorporateMemberController.php b/Modules/Internal/Http/Controllers/Api/CorporateMemberController.php index 02963ee4..c44197ba 100755 --- a/Modules/Internal/Http/Controllers/Api/CorporateMemberController.php +++ b/Modules/Internal/Http/Controllers/Api/CorporateMemberController.php @@ -35,6 +35,9 @@ class CorporateMemberController extends Controller ->with([ 'employeds', 'currentPolicy', + 'claims' => function ($claim) { + return $claim->used(); + } ]) ->with('currentPlan') ->paginate() diff --git a/app/Models/Claim.php b/app/Models/Claim.php index 5c5d6b63..683ae02b 100644 --- a/app/Models/Claim.php +++ b/app/Models/Claim.php @@ -28,6 +28,15 @@ class Claim extends Model 'updated_by', 'deleted_by', ]; + + public static $status = [ + 'draft' => 'Draft', + 'requested' => 'Requested', + 'received' => 'Received', + 'approved' => 'Approved', + 'paid' => 'Paid', + 'declined' => 'Declined' + ]; protected static function boot() { @@ -61,5 +70,12 @@ class Claim extends Model { return $this->belongsTo(Benefit::class, 'benefit_id'); } + + public function scopeUsed($query, $startDate, $endDate) + { + return $query + ->whereIn('status', ['approved', 'paid']) + ->whereBetween('requested_at', $startDate, $endDate); + } } diff --git a/app/Models/Member.php b/app/Models/Member.php index dd499477..78f4a323 100755 --- a/app/Models/Member.php +++ b/app/Models/Member.php @@ -60,7 +60,8 @@ class Member extends Model protected $appends = [ 'full_name', 'age', - 'gender_code' + 'gender_code', + '' ]; protected $hidden = [ @@ -125,7 +126,7 @@ class Member extends Model { return $this->hasOneThrough(Plan::class, MemberPlan::class, 'member_id', 'id', 'id', 'plan_id')->latest(); } - + public function policies() { return $this->hasMany(MemberPolicy::class, 'member_id', 'member_id'); diff --git a/app/Services/ClaimService.php b/app/Services/ClaimService.php index 05f3618d..050644b7 100644 --- a/app/Services/ClaimService.php +++ b/app/Services/ClaimService.php @@ -4,6 +4,8 @@ namespace App\Services; use App\Models\Claim; use App\Models\Icd; +use App\Models\Member; +use Carbon\Carbon; use Str; class ClaimService{ @@ -24,4 +26,19 @@ class ClaimService{ return $claim; } + + public static function getMemberTotalUsage(Member $member, $startDate = null, $endDate = null) + { + $startDate = empty($startDate) ? Carbon::now()->startOfMonth() : $startDate; + $endDate = empty($endDate) ? Carbon::now()->startOfMonth() : $endDate; + + $claims = $member->claims() + ->used($startDate, $endDate) + ->get(); + $total = $claims->sum(function($claim){ + return $claim->total_claim; + }); + + return $total; + } } \ No newline at end of file diff --git a/database/migrations/2022_07_07_040543_create_corporate_plans_table.php b/database/migrations/2022_07_07_040543_create_corporate_plans_table.php index 7042d877..f0218877 100755 --- a/database/migrations/2022_07_07_040543_create_corporate_plans_table.php +++ b/database/migrations/2022_07_07_040543_create_corporate_plans_table.php @@ -13,22 +13,22 @@ return new class extends Migration */ public function up() { - Schema::create('corporate_plans', function (Blueprint $table) { - $table->id(); - $table->foreignId('corporate_id')->nullable()->index(); - $table->string('code')->index(); - $table->string('name')->nullable(); - $table->text('description')->nullable(); - $table->boolean('active')->default(true); - $table->timestamps(); - $table->softDeletes(); + // Schema::create('corporate_plans', function (Blueprint $table) { + // $table->id(); + // $table->foreignId('corporate_id')->nullable()->index(); + // $table->string('code')->index(); + // $table->string('name')->nullable(); + // $table->text('description')->nullable(); + // $table->boolean('active')->default(true); + // $table->timestamps(); + // $table->softDeletes(); - $table->foreignId('created_by')->nullable()->index(); - $table->foreignId('updated_by')->nullable()->index(); - $table->foreignId('deleted_by')->nullable()->index(); + // $table->foreignId('created_by')->nullable()->index(); + // $table->foreignId('updated_by')->nullable()->index(); + // $table->foreignId('deleted_by')->nullable()->index(); - $table->unique(["corporate_id", "code"], 'corporate_plans_unique'); - }); + // $table->unique(["corporate_id", "code"], 'corporate_plans_unique'); + // }); } /** @@ -38,6 +38,6 @@ return new class extends Migration */ public function down() { - Schema::dropIfExists('corporate_plans'); + // Schema::dropIfExists('corporate_plans'); } }; diff --git a/database/migrations/2022_11_22_135948_create_claims_table.php b/database/migrations/2022_11_22_135948_create_claims_table.php index d595e75d..6416783c 100644 --- a/database/migrations/2022_11_22_135948_create_claims_table.php +++ b/database/migrations/2022_11_22_135948_create_claims_table.php @@ -22,6 +22,21 @@ return new class extends Migration $table->string('currency'); $table->foreignId('plan_id')->index(); $table->foreignId('benefit_id')->index(); + + $table->dateTime('requested_at')->nullable(); + $table->unsignedBigInteger('requested_by')->nullable()->index(); + + $table->dateTime('received_at')->nullable(); + $table->unsignedBigInteger('received_by')->nullable()->index(); + + $table->dateTime('approved_at')->nullable(); + $table->unsignedBigInteger('approved_by')->nullable()->index(); + + $table->dateTime('approved_at')->nullable(); + $table->unsignedBigInteger('approved_by')->nullable()->index(); + + $table->dateTime('paid_at')->nullable(); + $table->unsignedBigInteger('paid_by')->nullable()->index(); $table->timestamps(); $table->softDeletes();