Add Primayan Medicare report frontend and navigation

This commit is contained in:
Iqbal
2026-05-11 09:10:33 +07:00
parent 5dc04d01e2
commit 21a909aacb
6 changed files with 547 additions and 46 deletions

View File

@@ -106,6 +106,7 @@ const navConfig = [
{ title: 'Linksehat Payment', path: '/report/linksehat-payments' },
// { title: 'Prescription', path: '/report/prescription' },
{ title: 'Doctor Rating', path: '/report/doctor-rating' },
{ title: 'Primayan Medicare', path: '/report/primayan-medicare' },
],
},

View File

@@ -0,0 +1,32 @@
import { Container } from '@mui/material';
import HeaderBreadcrumbs from '../../../components/HeaderBreadcrumbs';
import Page from '../../../components/Page';
import useSettings from '../../../hooks/useSettings';
import List from './List';
export default function PrimayanMedicare() {
const { themeStretch } = useSettings();
const pageTitle = 'Primayan Medicare';
return (
<Page title={pageTitle}>
<Container maxWidth={themeStretch ? false : 'xl'}>
<HeaderBreadcrumbs
heading={pageTitle}
links={[
{
name: 'Report',
href: '/report',
},
{
name: 'Primayan Medicare',
href: '/report/primayan-medicare',
},
]}
/>
<List />
</Container>
</Page>
);
}

View File

@@ -0,0 +1,402 @@
import {
Card,
Grid,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
TextField,
Stack,
Chip,
FormControl,
InputLabel,
Select,
MenuItem,
Menu,
} from '@mui/material';
import { useSearchParams } from 'react-router-dom';
import React, { ChangeEvent, useEffect, useRef, useState } from 'react';
import axios from '../../../utils/axios';
import { LaravelPaginatedData } from '../../../@types/paginated-data';
import BasePagination from '../../../components/BasePagination';
import { fNumber } from '@/utils/formatNumber';
import { fDateOnly } from '@/utils/formatTime';
import { DesktopDatePicker, LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { LoadingButton } from '@mui/lab';
import UploadIcon from '@mui/icons-material/Upload';
// ----------------------------------------------------------------------
const statusColors: Record<string, 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning'> = {
draft: 'default',
requested: 'info',
received: 'primary',
approved: 'success',
postpone: 'warning',
paid: 'success',
declined: 'error',
};
export default function List() {
const [searchParams, setSearchParams] = useSearchParams();
const [providers, setProviders] = useState<any[]>([]);
function Filter() {
const searchInput = useRef<HTMLInputElement>(null);
const [searchText, setSearchText] = useState('');
const [exportLoading, setExportLoading] = useState(false);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const exportMenuOpen = Boolean(anchorEl);
const handleExportClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleExportClose = () => {
setAnchorEl(null);
};
const handleExport = (exportType: string) => {
const parameters = Object.fromEntries([...searchParams.entries()]);
setExportLoading(true);
axios
.get('/primayan-medicare/export', {
params: { ...parameters, export_type: exportType },
})
.then((response) => {
const link = document.createElement('a');
link.href = response.data.data.file_url;
link.setAttribute('download', response.data.data.file_name);
document.body.appendChild(link);
link.click();
handleExportClose();
setExportLoading(false);
})
.catch(() => {
setExportLoading(false);
});
};
const handleSearchChange = (event: any) => {
setSearchText(event.target.value ?? '');
};
useEffect(() => {
setSearchText(searchParams.get('search') ?? '');
}, []);
return (
<form style={{ width: '100%' }}>
<Grid container spacing={2} sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
<Grid item md={2}>
<TextField
id="search-input"
ref={searchInput}
variant="outlined"
fullWidth
onChange={handleSearchChange}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
const filter = Object.fromEntries([
...searchParams.entries(),
['search', searchText],
['page', '1'],
]);
setSearchParams(filter);
loadDataTableData(filter);
}
}}
label="Search"
value={searchText}
InputProps={{
placeholder: 'Nama, Member ID',
}}
/>
</Grid>
<Grid item md={2}>
<FormControl fullWidth>
<InputLabel>Status</InputLabel>
<Select
value={searchParams.get('status') ?? 'all'}
label="Status"
onChange={(el) => {
const filter = Object.fromEntries([
...searchParams.entries(),
['status', el.target.value],
['page', '1'],
]);
setSearchParams(filter);
loadDataTableData(filter);
}}
>
<MenuItem value={'all'}>Semua</MenuItem>
<MenuItem value={'draft'}>Draft</MenuItem>
<MenuItem value={'requested'}>Requested</MenuItem>
<MenuItem value={'received'}>Received</MenuItem>
<MenuItem value={'approved'}>Approved</MenuItem>
<MenuItem value={'postpone'}>Postpone</MenuItem>
<MenuItem value={'paid'}>Paid</MenuItem>
<MenuItem value={'declined'}>Declined</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item md={2}>
<FormControl fullWidth>
<InputLabel>Provider</InputLabel>
<Select
value={searchParams.get('provider_id') ?? 'all'}
label="Provider"
onChange={(el) => {
const filter = Object.fromEntries([
...searchParams.entries(),
['provider_id', el.target.value],
['page', '1'],
]);
setSearchParams(filter);
loadDataTableData(filter);
}}
>
<MenuItem value={'all'}>Semua</MenuItem>
{providers.map((provider) => (
<MenuItem key={provider.id} value={provider.id}>
{provider.name}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item md={2}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DesktopDatePicker
value={searchParams.get('start_date') || null}
inputFormat="dd/MM/yyyy"
onChange={(value) => {
try {
if (value && !!Date.parse(value)) {
const date = fDateOnly(value);
let entries = [...searchParams.entries(), ['start_date', date ?? '']];
if (!searchParams.get('end_date')) {
entries = [...entries, ['end_date', date ?? '']];
}
const filter = Object.fromEntries([...entries, ['page', '1']]);
setSearchParams(filter);
loadDataTableData(filter);
}
} catch (e) {}
}}
renderInput={(params) => <TextField {...params} fullWidth label="Start Date" />}
/>
</LocalizationProvider>
</Grid>
<Grid item md={2}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DesktopDatePicker
value={searchParams.get('end_date') || null}
inputFormat="dd/MM/yyyy"
onChange={(value) => {
try {
if (value && !!Date.parse(value)) {
const date = fDateOnly(value);
let entries = [...searchParams.entries(), ['end_date', date ?? '']];
if (!searchParams.get('start_date')) {
entries = [...entries, ['start_date', date ?? '']];
}
const filter = Object.fromEntries([...entries, ['page', '1']]);
setSearchParams(filter);
loadDataTableData(filter);
}
} catch (e) {}
}}
renderInput={(params) => <TextField {...params} fullWidth label="End Date" />}
/>
</LocalizationProvider>
</Grid>
<Grid item md={1}>
<LoadingButton
variant="outlined"
startIcon={<UploadIcon />}
sx={{ p: 1.8 }}
onClick={handleExportClick}
loading={exportLoading}
>
Export
</LoadingButton>
<Menu
id="export-menu"
anchorEl={anchorEl}
open={exportMenuOpen}
onClose={handleExportClose}
MenuListProps={{
'aria-labelledby': 'export-button',
}}
>
<MenuItem onClick={() => handleExport('list')}>Download List Excel</MenuItem>
<MenuItem onClick={() => handleExport('summary')}>Download Summary Excel</MenuItem>
</Menu>
</Grid>
</Grid>
</form>
);
}
function FilterForm() {
return (
<Grid
container
spacing={2}
sx={{ p: 2, justifyContent: 'space-between', alignItems: 'center' }}
>
<Grid item xs={12} md={12} lg={12}>
<Filter />
</Grid>
</Grid>
);
}
function Row(props: { row: any }) {
const { row } = props;
const serviceCodeMap: Record<string, string> = {
'OP': 'Outpatient',
'IP': 'Inpatient',
'DE': 'Dental',
'MA': 'Maternal',
'GL': 'Optical',
'MCU': 'Medical Check Up',
'MEDIVAC': 'Medical Emergency Evacuation',
};
const serviceName = serviceCodeMap[row.service_code] || row.service_code || '-';
return (
<TableRow>
<TableCell align="left">
{(row.claim_code || row.request_code) && row.log_code
? `${row.claim_code ?? row.request_code} / ${row.log_code}`
: row.claim_code ?? row.request_code ?? row.log_code ?? '-'}
</TableCell>
<TableCell align="left">{row.member_name ?? '-'}</TableCell>
<TableCell align="left">{row.member_number ?? '-'}</TableCell>
<TableCell align="left">{serviceName}</TableCell>
<TableCell align="left">{row.provider_name ?? '-'}</TableCell>
<TableCell align="left">{row.request_date ?? '-'}</TableCell>
<TableCell align="right">{fNumber(parseInt(row.amount_incurred ?? 0))}</TableCell>
<TableCell align="right">{fNumber(parseInt(row.amount_approved ?? 0))}</TableCell>
<TableCell align="right">{fNumber(parseInt(row.excess_paid ?? 0))}</TableCell>
<TableCell align="center">
<Chip
label={row.status ?? '-'}
color={statusColors[row.status] ?? 'default'}
size="small"
/>
</TableCell>
</TableRow>
);
}
const headStyle = {
fontWeight: 'bold',
};
const [dataTableIsLoading, setDataTableLoading] = useState(true);
const [dataTableData, setDataTableData] = useState<LaravelPaginatedData>({
current_page: 1,
data: [],
path: '',
first_page_url: '',
last_page: 1,
last_page_url: '',
next_page_url: '',
prev_page_url: '',
per_page: 10,
from: 0,
to: 0,
total: 0,
});
const loadDataTableData = async (appliedFilter: any | null = null) => {
setDataTableLoading(true);
const filter = appliedFilter ? appliedFilter : Object.fromEntries([...searchParams.entries()]);
try {
const response = await axios.get('/primayan-medicare', {
params: filter,
});
setDataTableData(response.data.data.results);
setProviders(response.data.data.providers);
} catch (error) {
console.error('Failed to load Primayan Medicare data:', error);
}
setDataTableLoading(false);
};
const handlePageChange = (event: ChangeEvent, value: number) => {
const filter = Object.fromEntries([...searchParams.entries(), ['page', value]]);
loadDataTableData(filter);
setSearchParams(filter);
};
useEffect(() => {
loadDataTableData();
}, []);
return (
<Stack>
<Card sx={{ marginTop: '30px' }}>
<FilterForm />
<TableContainer component={Paper}>
<Table>
<TableBody>
<TableRow>
<TableCell style={headStyle} align="left">Code Claim / Code LOG</TableCell>
<TableCell style={headStyle} align="left">Nama Member</TableCell>
<TableCell style={headStyle} align="left">Member ID</TableCell>
<TableCell style={headStyle} align="left">Service</TableCell>
<TableCell style={headStyle} align="left">Provider</TableCell>
<TableCell style={headStyle} align="left">Tanggal Request</TableCell>
<TableCell style={headStyle} align="right">Amount Incurred</TableCell>
<TableCell style={headStyle} align="right">Amount Approved</TableCell>
<TableCell style={headStyle} align="right">Excess Paid</TableCell>
<TableCell style={headStyle} align="center">Status</TableCell>
</TableRow>
</TableBody>
{dataTableIsLoading ? (
<TableBody>
<TableRow>
<TableCell colSpan={10} align="center">
Loading
</TableCell>
</TableRow>
</TableBody>
) : dataTableData.data.length === 0 ? (
<TableBody>
<TableRow>
<TableCell colSpan={10} align="center">
No Data
</TableCell>
</TableRow>
</TableBody>
) : (
<TableBody>
{dataTableData.data.map((row, index) => (
<Row key={row.id ?? index} row={row} />
))}
</TableBody>
)}
</Table>
</TableContainer>
<BasePagination paginationData={dataTableData} onPageChange={handlePageChange} />
</Card>
</Stack>
);
}

View File

@@ -471,6 +471,10 @@ export default function Router() {
path: 'report/linksehat-payments',
element: <LinksehatPayment />,
},
{
path: 'report/primayan-medicare',
element: <PrimayanMedicare />,
},
{
path: 'report/phr',
@@ -770,6 +774,8 @@ const EPrescriptionShow = Loadable(lazy(() => import('../pages/EPrescription/Liv
const LinksehatPayment = Loadable(lazy(() => import('../pages/Report/LinksehatPayments/Index')));
const PrimayanMedicare = Loadable(lazy(() => import('../pages/Report/PrimayanMedicare/Index')));
const RiwayatMedisPeserta = Loadable(lazy(() => import('../pages/Report/RiwayatMedisPeserta/Index')));
const RujukanPasien = Loadable(lazy(() => import('../pages/Report/Rujukan/Index')));
const Prescription = Loadable(lazy(() => import('../pages/Report/Prescription/Index')));