Update Dashboard Client Portal

This commit is contained in:
ivan-sim
2026-01-21 17:13:45 +07:00
parent b8a72b8119
commit f10589eb85
13 changed files with 1269 additions and 1 deletions

View File

@@ -0,0 +1,182 @@
import {
Box,
Grid,
TextField,
Typography,
Autocomplete,
MenuItem,
} from "@mui/material";
type InvoiceStatus = {
id: string;
name: string;
};
type YearOption = {
id: number;
label: string;
};
type ServiceOption = {
id: string;
label: string;
};
type BillingFilterProps = {
year: number;
status: string | null;
payorId: string;
bn: string;
service: string; // "OP" | "IP" | ""
billing: string;
search: string;
onYearChange: (year: number) => void;
onStatusChange: (status: string | null) => void;
onPayorIdChange: (value: string) => void;
onBnChange: (value: string) => void;
onServiceChange: (value: string) => void;
onBillingChange: (value: string) => void;
onSearchChange: (value: string) => void;
title: string;
};
const BillingFilterCard: React.FC<BillingFilterProps> = ({
year,
status,
payorId,
bn,
service,
billing,
search,
onYearChange,
onStatusChange,
onPayorIdChange,
onBnChange,
onServiceChange,
onBillingChange,
onSearchChange,
title,
}) => {
const statusInvoice: InvoiceStatus[] = [
{ id: "submitted", name: "Pengajuan" },
{ id: "accepted", name: "Belum Dibayar" },
{ id: "partial_paid", name: "Bayar Sebagian" },
{ id: "paid", name: "Sudah Dibayar" },
];
const serviceOptions: ServiceOption[] = [
{ id: "OP", label: "OP (Rawat Jalan)" },
{ id: "IP", label: "IP (Rawat Inap)" },
];
const currentYear = new Date().getFullYear();
const yearOptions: YearOption[] = Array.from({ length: 4 }, (_, i) => {
const y = currentYear - i;
return { id: y, label: y.toString() };
});
return (
<Box>
<Typography fontWeight={600} mb={2}>
{title}
</Typography>
<Grid container spacing={2}>
{/* Tahun */}
<Grid item xs={12} sm={2}>
<Autocomplete
options={yearOptions}
size="small"
getOptionLabel={(o) => o.label}
value={yearOptions.find((y) => y.id === year) || null}
onChange={(_, v) => v && onYearChange(v.id)}
renderInput={(params) => (
<TextField {...params} label="Tahun" fullWidth />
)}
/>
</Grid>
{/* Payor ID */}
<Grid item xs={12} sm={2}>
<TextField
label="Payor ID"
fullWidth
size="small"
value={payorId}
onChange={(e) => onPayorIdChange(e.target.value)}
/>
</Grid>
{/* BN */}
<Grid item xs={12} sm={2}>
<TextField
label="BN"
fullWidth
size="small"
value={bn}
onChange={(e) => onBnChange(e.target.value)}
/>
</Grid>
{/* Service (OP / IP) */}
<Grid item xs={12} sm={2}>
<Autocomplete
options={serviceOptions}
size="small"
getOptionLabel={(o) => o.label}
value={
serviceOptions.find((s) => s.id === service) || null
}
onChange={(_, v) => onServiceChange(v ? v.id : "")}
renderInput={(params) => (
<TextField {...params} label="Service" fullWidth />
)}
/>
</Grid>
{/* Billing / No Invoice */}
<Grid item xs={12} sm={2}>
<TextField
label="Billing (No. Invoice)"
fullWidth
size="small"
value={billing}
onChange={(e) => onBillingChange(e.target.value)}
/>
</Grid>
{/* Status */}
<Grid item xs={12} sm={2}>
<Autocomplete
options={statusInvoice}
size="small"
getOptionLabel={(o) => o.name}
value={
statusInvoice.find((s) => s.id === status) || null
}
onChange={(_, v) => onStatusChange(v ? v.id : null)}
renderInput={(params) => (
<TextField {...params} label="Status Invoice" fullWidth />
)}
/>
</Grid>
{/* Search Provider */}
<Grid item xs={12}>
<TextField
placeholder="Search provider"
fullWidth
size="small"
value={search}
onChange={(e) => onSearchChange(e.target.value)}
/>
</Grid>
</Grid>
</Box>
);
};
export default BillingFilterCard;

View File

@@ -0,0 +1,85 @@
import {
Box,
Typography,
Divider,
} from "@mui/material";
type Item = {
provider: string;
total: number;
};
interface Props {
data: Item[];
}
const rupiah = (v: number) =>
"Rp" + v.toLocaleString("id-ID");
const BillingProviderList: React.FC<Props> = ({ data }) => {
return (
<Box>
{/* HEADER (FIXED) */}
<Box
sx={{
display: "flex",
justifyContent: "space-between",
bgcolor: "#F9FAFB",
px: 2,
py: 1.2,
borderRadius: 1,
mb: 1,
}}
>
<Typography fontSize={12} color="text.secondary">
Nama Provider
</Typography>
<Typography fontSize={12} color="text.secondary">
Total Billing
</Typography>
</Box>
{/* SCROLLABLE LIST */}
<Box
sx={{
maxHeight: 360, // 🔥 tinggi scroll (atur sesuai kebutuhan)
overflowY: "auto",
borderRadius: 1,
}}
>
{data.map((item, idx) => (
<Box key={idx}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
px: 2,
py: 1.4,
}}
>
<Typography fontSize={14}>
{item.provider}
</Typography>
<Typography fontSize={14} fontWeight={500}>
{rupiah(item.total)}
</Typography>
</Box>
<Divider />
</Box>
))}
{data.length === 0 && (
<Typography
textAlign="center"
color="text.secondary"
py={3}
>
Tidak ada data
</Typography>
)}
</Box>
</Box>
);
};
export default BillingProviderList;

View File

@@ -0,0 +1,44 @@
/* ---------------------------------- @mui ---------------------------------- */
import { Container, Grid } from '@mui/material';
/* ------------------------------- components ------------------------------- */
import Page from '../../components/Page';
/* ---------------------------------- hooks --------------------------------- */
import useSettings from '../../hooks/useSettings';
import HeaderBreadcrumbs from '../../components/HeaderBreadcrumbs';
import DashboardBilling from './DashboardBilling';
import DashboardBillingProvider from './DashboardBillingProvider';
import DashboardBillingDiagnosis from './DashboardBillingDiagnosis';
export default function Drugs() {
const { themeStretch } = useSettings();
return (
<Page title="Dashboard">
<Container maxWidth={themeStretch ? false : 'xl'}>
<HeaderBreadcrumbs
heading={'Dashboard'}
links={[
{ name: 'Dashboard', href: '/dashboard' }
]}
/>
<Grid container spacing={2}>
<Grid item xs={12}>
{/* Billing per bulan */}
<DashboardBilling />
</Grid>
<Grid item xs={12}>
{/* Billing per provider */}
<DashboardBillingProvider />
</Grid>
<Grid item xs={12}>
{/* Billing per diagnosis */}
<DashboardBillingDiagnosis />
</Grid>
</Grid>
</Container>
</Page>
);
}

View File

@@ -0,0 +1,125 @@
import React, { useEffect, useState, useContext } from "react";
import {
Box,
Card,
CardContent,
Stack,
Typography,
} from "@mui/material";
import axios from '../../utils/axios';
import { UserCurrentCorporateContext } from '../../contexts/UserCurrentCorporate';
import { MonthlyBilling } from "./billingData";
import MonthlyBillingCollapse from "./MonthlyBillingCollapse";
import BillingFilterCard from "./BillingFilterCard";
const DashboardBilling: React.FC = () => {
const [billingData, setBillingData] = useState<MonthlyBilling[]>([]);
const [year, setYear] = useState<number>(new Date().getFullYear());
const [status, setStatus] = useState<string | null>(null);
const [payorId, setPayorId] = useState("");
const [bn, setBn] = useState("");
const [service, setService] = useState("");
const [billing, setBilling] = useState("");
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
const { corporateValue } = useContext(UserCurrentCorporateContext);
const fetchBilling = async (params: any) => {
setLoading(true);
try {
const response = await axios.get(`${corporateValue}/billing/summary`, {
params,
});
setBillingData(response.data);
} catch (error) {
console.error("Failed fetch billing summary", error);
setBillingData([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchBilling({
year,
status,
payorId,
bn,
service,
billing,
search,
});
}, [year, status, payorId, bn, service, billing, search]);
return (
<Box sx={{ width: "100%", p: 2 }}>
<Card
sx={{
borderRadius: 2,
border: "1px solid #E5E7EB",
boxShadow: "none",
}}
>
<CardContent>
<Stack spacing={2}>
{/* FILTER */}
<BillingFilterCard
year={year}
status={status}
payorId={payorId}
bn={bn}
service={service}
billing={billing}
search={search}
onYearChange={setYear}
onStatusChange={setStatus}
onPayorIdChange={setPayorId}
onBnChange={setBn}
onServiceChange={setService}
onBillingChange={setBilling}
onSearchChange={setSearch}
title="Billing Per Bulan"
/>
{/* LIST */}
{loading && (
<Typography
align="center"
color="text.secondary"
sx={{ py: 4 }}
>
Loading...
</Typography>
)}
{!loading && billingData.length === 0 && (
<Typography
align="center"
color="text.secondary"
sx={{ py: 4 }}
>
Tidak ada data
</Typography>
)}
{!loading &&
billingData.map((item, index) => (
<MonthlyBillingCollapse
key={`${item.month}-${index}`}
data={item}
/>
))}
</Stack>
</CardContent>
</Card>
</Box>
);
};
export default DashboardBilling;

View File

@@ -0,0 +1,138 @@
import React, { useEffect, useState, useContext } from "react";
import {
Box,
Card,
CardContent,
Stack,
Typography,
} from "@mui/material";
import axios from '../../utils/axios';
import { UserCurrentCorporateContext } from '../../contexts/UserCurrentCorporate';
import { MonthlyBilling } from "./billingData";
import MonthlyBillingCollapse from "./MonthlyBillingCollapse";
import BillingFilterCard from "./BillingFilterCard";
import BillingProviderList from "./BillingProviderList";
import TopDiagnosisList from "./TopDiagnosisList";
export interface ProviderBillingItem {
provider: string;
total: number;
}
export interface ProviderBillingResponse {
total: number;
items: ProviderBillingItem[];
}
export interface TopDiagnosisItem {
code: string;
diagnosis: string;
total_case: number;
total_billing: number;
}
const rupiah = (value: number) =>
"Rp" + value.toLocaleString("id-ID");
const DashboardBillingDiagnosis: React.FC = () => {
const [topDiagnosis, setTopDiagnosis] =
useState<TopDiagnosisItem[]>([]);
const [year, setYear] = useState<number>(new Date().getFullYear());
const [status, setStatus] = useState<string | null>(null);
const [payorId, setPayorId] = useState("");
const [bn, setBn] = useState("");
const [service, setService] = useState("");
const [billing, setBilling] = useState("");
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
const { corporateValue } = useContext(UserCurrentCorporateContext);
const fetchTopDiagnosis = async (params: any) => {
setLoading(true);
try {
const response = await axios.get(
`${corporateValue}/billing/top-diagnosis`,
{ params }
);
setTopDiagnosis(response.data);
} catch (error) {
console.error("Failed fetch top diagnosis", error);
setTopDiagnosis([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTopDiagnosis({
year,
status,
payorId,
bn,
service,
billing,
search,
});
}, [year, status, payorId, bn, service, billing, search]);
return (
<Box sx={{ width: "100%", p: 2 }}>
<Card
sx={{
borderRadius: 2,
border: "1px solid #E5E7EB",
boxShadow: "none",
}}
>
<CardContent>
<Stack spacing={2}>
<BillingFilterCard
title="Top 10 Diagnosis"
year={year}
status={status}
payorId={payorId}
bn={bn}
service={service}
billing={billing}
search={search}
onYearChange={setYear}
onStatusChange={setStatus}
onPayorIdChange={setPayorId}
onBnChange={setBn}
onServiceChange={setService}
onBillingChange={setBilling}
onSearchChange={setSearch}
/>
{loading && (
<Typography align="center" color="text.secondary" py={4}>
Loading...
</Typography>
)}
{!loading && topDiagnosis.length === 0 && (
<Typography align="center" color="text.secondary" py={4}>
Tidak ada data
</Typography>
)}
{!loading && topDiagnosis.length > 0 && (
<>
<Typography fontWeight={600}>
Top 10 Diagnosis
</Typography>
<TopDiagnosisList data={topDiagnosis} />
</>
)}
</Stack>
</CardContent>
</Card>
</Box>
);
};
export default DashboardBillingDiagnosis;

View File

@@ -0,0 +1,118 @@
import React, { useEffect, useState, useContext } from "react";
import {
Box,
Card,
CardContent,
Stack,
Typography,
} from "@mui/material";
import axios from '../../utils/axios';
import { UserCurrentCorporateContext } from '../../contexts/UserCurrentCorporate';
import { MonthlyBilling } from "./billingData";
import MonthlyBillingCollapse from "./MonthlyBillingCollapse";
import BillingFilterCard from "./BillingFilterCard";
import BillingProviderList from "./BillingProviderList";
export interface ProviderBillingItem {
provider: string;
total: number;
}
export interface ProviderBillingResponse {
total: number;
items: ProviderBillingItem[];
}
const rupiah = (value: number) =>
"Rp" + value.toLocaleString("id-ID");
const DashboardBillingProvider: React.FC = () => {
const [billingData, setBillingData] =
useState<ProviderBillingResponse | null>(null);
const [year, setYear] = useState<number>(new Date().getFullYear());
const [status, setStatus] = useState<string | null>(null);
const [payorId, setPayorId] = useState("");
const [bn, setBn] = useState("");
const [service, setService] = useState("");
const [billing, setBilling] = useState("");
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
const { corporateValue } = useContext(UserCurrentCorporateContext);
const fetchBilling = async (params: any) => {
setLoading(true);
try {
const response = await axios.get(
`${corporateValue}/billing/provider-summary`,
{ params }
);
setBillingData(response.data);
} catch {
setBillingData(null);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchBilling({
year,
status,
payorId,
bn,
service,
billing,
search,
});
}, [year, status, payorId, bn, service, billing, search]);
return (
<Box sx={{ width: "100%", p: 2 }}>
<Card sx={{ borderRadius: 2, border: "1px solid #E5E7EB", boxShadow: "none" }}>
<CardContent>
<Stack spacing={2}>
<BillingFilterCard
title="Billing Per Provider"
year={year}
status={status}
payorId={payorId}
bn={bn}
service={service}
billing={billing}
search={search}
onYearChange={setYear}
onStatusChange={setStatus}
onPayorIdChange={setPayorId}
onBnChange={setBn}
onServiceChange={setService}
onBillingChange={setBilling}
onSearchChange={setSearch}
/>
{loading && (
<Typography align="center" color="text.secondary" py={4}>
Loading...
</Typography>
)}
{!loading && billingData && (
<>
<Typography fontWeight={600} color="primary">
TOTAL BILLING OVERALL
</Typography>
<Typography fontWeight={700}>
{rupiah(billingData.total)}
</Typography>
<BillingProviderList data={billingData.items} />
</>
)}
</Stack>
</CardContent>
</Card>
</Box>
);
};
export default DashboardBillingProvider;

View File

@@ -0,0 +1,131 @@
import React, { useState } from "react";
import {
Box,
Typography,
Collapse,
IconButton,
Divider,
} from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import CalendarMonthIcon from "@mui/icons-material/CalendarMonth";
import PaymentsIcon from "@mui/icons-material/Payments";
import { MonthlyBilling } from "./billingData";
interface Props {
data: MonthlyBilling;
}
const rupiah = (value: number) =>
"Rp" + value.toLocaleString("id-ID");
const MonthlyBillingCollapse: React.FC<Props> = ({ data }) => {
const [open, setOpen] = useState(false);
return (
<Box
sx={{
border: "1px solid #E5E7EB",
borderRadius: 2,
mb: 1.5,
bgcolor: "#fff",
}}
>
{/* HEADER */}
<Box
onClick={() => setOpen(!open)}
sx={{
px: 2,
py: 1.5,
display: "flex",
justifyContent: "space-between",
cursor: "pointer",
}}
>
<Box display="flex" alignItems="center" gap={1.5}>
{/* ICON */}
<CalendarMonthIcon
sx={{ fontSize: 28 }}
color="action"
/>
{/* TEXT */}
<Box>
<Typography fontWeight={600}>
{data.month}
</Typography>
<Typography fontSize={13} color="primary">
{rupiah(data.total)}
</Typography>
</Box>
</Box>
<IconButton
size="small"
sx={{
transform: open ? "rotate(180deg)" : "rotate(0deg)",
transition: "0.2s",
}}
>
<ExpandMoreIcon />
</IconButton>
</Box>
{/* BODY */}
<Collapse in={open} timeout="auto" unmountOnExit>
<Divider />
<Box sx={{ p: 2 }}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
bgcolor: "#F9FAFB",
px: 1.5,
py: 1,
borderRadius: 1,
mb: 1,
}}
>
<Typography fontSize={12} color="text.secondary">
Nama Provider
</Typography>
<Typography fontSize={12} color="text.secondary">
Total Billing
</Typography>
</Box>
{data.items.map((item, idx) => (
<Box
key={idx}
sx={{
display: "flex",
justifyContent: "space-between",
py: 1,
borderBottom: "1px solid #F1F5F9",
}}
>
<Typography fontSize={14}>{item.provider}</Typography>
<Typography fontSize={14}>
{rupiah(item.total)}
</Typography>
</Box>
))}
{data.items.length === 0 && (
<Typography
fontSize={13}
color="text.secondary"
textAlign="center"
py={2}
>
Tidak ada data
</Typography>
)}
</Box>
</Collapse>
</Box>
);
};
export default MonthlyBillingCollapse;

View File

@@ -0,0 +1,87 @@
import {
Box,
Typography,
Divider,
} from "@mui/material";
// import { TopDiagnosisItem } from "./types";
const rupiah = (v: number) =>
"Rp" + v.toLocaleString("id-ID");
export interface TopDiagnosisItem {
code: string;
diagnosis: string;
total_case: number;
total_billing: number;
}
interface Props {
data: TopDiagnosisItem[];
}
const TopDiagnosisList: React.FC<Props> = ({ data }) => {
return (
<Box>
{/* HEADER */}
<Box
sx={{
display: "grid",
gridTemplateColumns: "50px 90px 1fr 140px 140px",
bgcolor: "#F9FAFB",
px: 2,
py: 1.2,
borderRadius: 1,
mb: 1,
}}
>
<Typography fontSize={12} color="text.secondary">No</Typography>
<Typography fontSize={12} color="text.secondary">Kode</Typography>
<Typography fontSize={12} color="text.secondary">Diagnosa</Typography>
<Typography fontSize={12} color="text.secondary" textAlign="right">
Jumlah Kasus
</Typography>
<Typography fontSize={12} color="text.secondary" textAlign="right">
Total Billing
</Typography>
</Box>
{/* SCROLLABLE LIST */}
<Box sx={{ maxHeight: 380, overflowY: "auto" }}>
{data.map((item, idx) => (
<Box key={idx}>
<Box
sx={{
display: "grid",
gridTemplateColumns: "50px 90px 1fr 140px 140px",
px: 2,
py: 1.4,
}}
>
<Typography fontSize={13}>{idx + 1}</Typography>
<Typography fontSize={13}>{item.code}</Typography>
<Typography fontSize={13}>{item.diagnosis}</Typography>
<Typography fontSize={13} textAlign="right">
{item.total_case}
</Typography>
<Typography fontSize={13} textAlign="right" fontWeight={500}>
{rupiah(item.total_billing)}
</Typography>
</Box>
<Divider />
</Box>
))}
{data.length === 0 && (
<Typography
textAlign="center"
color="text.secondary"
py={3}
>
Tidak ada data
</Typography>
)}
</Box>
</Box>
);
};
export default TopDiagnosisList;

View File

@@ -0,0 +1,35 @@
// billingData.ts
export interface BillingItem {
provider: string;
total: number;
}
export interface MonthlyBilling {
month: string;
total: number;
items: BillingItem[];
}
export const billingData: MonthlyBilling[] = [
{
month: "Januari",
total: 8884000,
items: [
{ provider: "Klinik Karya Morowali Utama", total: 753000 },
{ provider: "Klinik Karya Morowali Utama", total: 398000 },
{ provider: "Klinik Karya Morowali Utama", total: 753000 },
{ provider: "Klinik Karya Morowali Utama", total: 1425000 },
{ provider: "RS Hermina Kendari", total: 1425000 },
],
},
{
month: "Februari",
total: 8884000,
items: [],
},
{
month: "Maret",
total: 8884000,
items: [],
},
];

View File

@@ -437,7 +437,7 @@ export default function Router() {
const Login = Loadable(lazy(() => import('../pages/auth/Login')));
// Dashboard
const Dashboard = Loadable(lazy(() => import('../pages/Dashboard/Index')));
const Dashboard = Loadable(lazy(() => import('../pages/Dashboard/Dashboard')));
const NotFound = Loadable(lazy(() => import('../pages/Page404')));
// Employee Data