Merge branch 'origin/production' of https://dev.sismedika.online/tubagus/aso into origin/production

This commit is contained in:
Server D3 Linksehat
2026-02-04 14:40:21 +07:00
13 changed files with 1269 additions and 1 deletions

View File

@@ -0,0 +1,286 @@
<?php
namespace Modules\Client\Http\Controllers\Api;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\DB;
class BillingSummaryController extends Controller
{
public function index(Request $request, $corporate_id)
{
$year = $request->year ?? now()->year;
$status = $request->status;
$bn = $request->bn;
$payorId = $request->payorId;
$service = $request->service;
$billing = $request->billing;
$search = $request->search;
$rows = DB::table('invoice_payments')
->join('invoice_payment_details', 'invoice_payment_details.invoice_payment_id', '=', 'invoice_payments.id')
->join('claim_requests', 'claim_requests.id', '=', 'invoice_payment_details.claim_request_id')
->join('request_logs', 'request_logs.id', '=', 'claim_requests.request_log_id')
->join('corporate_employees', 'corporate_employees.member_id', '=', 'request_logs.member_id')
->join('organizations', 'organizations.id', '=', 'request_logs.organization_id')
->leftJoin('members', 'members.id', '=', 'request_logs.member_id')
->whereYear('invoice_payments.created_at', $year)
->where('corporate_employees.corporate_id', '=', $corporate_id)
// FILTERS
->when($status, fn ($q) =>
$q->where('invoice_payments.status', $status)
)
->when($bn, fn ($q) =>
$q->where('members.member_id', $bn)
)
->when($payorId, fn ($q) =>
$q->where('members.payor_id', $payorId)
)
->when($service, fn ($q) =>
$q->where('claim_requests.service_code', $service)
)
->when($billing, fn ($q) =>
$q->where('invoice_payments.invoice_number', $billing)
)
// 🔍 SEARCH PROVIDER (LIKE)
->when($search, fn ($q) =>
$q->where('organizations.name', 'like', '%' . $search . '%')
)
->selectRaw("
MONTH(invoice_payments.created_at) as month_number,
organizations.name as provider,
SUM(request_logs.nominal) as total
")
->groupBy('month_number', 'organizations.name')
->orderBy('month_number')
->get();
return response()->json(
$this->formatMonthly($rows)
);
}
protected function formatMonthly($rows): array
{
$months = [
1 => 'Januari', 2 => 'Februari', 3 => 'Maret',
4 => 'April', 5 => 'Mei', 6 => 'Juni',
7 => 'Juli', 8 => 'Agustus', 9 => 'September',
10 => 'Oktober', 11 => 'November', 12 => 'Desember',
];
return collect($rows)
->groupBy('month_number')
->map(function ($items, $monthNumber) use ($months) {
return [
'month' => $months[$monthNumber],
'total' => $items->sum('total'),
'items' => $items->map(fn ($item) => [
'provider' => $item->provider,
'total' => (float) $item->total,
])->values(),
];
})
->values()
->toArray();
}
public function providerSummary(Request $request, $corporate_id)
{
$year = $request->year ?? now()->year;
$status = $request->status;
$bn = $request->bn;
$payorId = $request->payorId;
$service = $request->service;
$billing = $request->billing;
$search = $request->search;
$query = DB::table('invoice_payments')
->join('invoice_payment_details', 'invoice_payment_details.invoice_payment_id', '=', 'invoice_payments.id')
->join('claim_requests', 'claim_requests.id', '=', 'invoice_payment_details.claim_request_id')
->join('request_logs', 'request_logs.id', '=', 'claim_requests.request_log_id')
->join('corporate_employees', 'corporate_employees.member_id', '=', 'request_logs.member_id')
->join('organizations', 'organizations.id', '=', 'request_logs.organization_id')
->leftJoin('members', 'members.id', '=', 'request_logs.member_id')
->whereYear('invoice_payments.created_at', $year)
->where('corporate_employees.corporate_id', '=', $corporate_id)
// FILTER
->when($status, fn ($q) =>
$q->where('invoice_payments.status', $status)
)
->when($bn, fn ($q) =>
$q->where('members.member_id', $bn)
)
->when($payorId, fn ($q) =>
$q->where('members.payor_id', $payorId)
)
->when($service, fn ($q) =>
$q->where('claim_requests.service_code', $service)
)
->when($billing, fn ($q) =>
$q->where('invoice_payments.invoice_number', $billing)
)
->when($search, fn ($q) =>
$q->where('organizations.name', 'like', '%' . $search . '%')
);
$items = $query
->selectRaw("
organizations.name as provider,
SUM(request_logs.nominal) as total
")
->groupBy('organizations.name')
->orderByDesc('total')
->get()
->map(fn ($row) => [
'provider' => $row->provider,
'total' => (float) $row->total,
]);
return response()->json([
'total' => $items->sum('total'),
'items' => $items,
]);
}
public function topDiagnosis(Request $request, $corporate_id)
{
$year = $request->year ?? now()->year;
$status = $request->status;
$bn = $request->bn;
$payorId = $request->payorId;
$service = $request->service;
$billing = $request->billing;
$search = $request->search;
$rows = DB::table('invoice_payments')
->join('invoice_payment_details', 'invoice_payment_details.invoice_payment_id', '=', 'invoice_payments.id')
->join('claim_requests', 'claim_requests.id', '=', 'invoice_payment_details.claim_request_id')
->join('request_logs', 'request_logs.id', '=', 'claim_requests.request_log_id')
->join('corporate_employees', 'corporate_employees.member_id', '=', 'request_logs.member_id')
// 🔥 ICD JOIN
->leftJoin(
'icd',
DB::raw("icd.code"),
'=',
DB::raw("SUBSTRING_INDEX(request_logs.diagnosis, ',', 1)")
)
->leftJoin('members', 'members.id', '=', 'request_logs.member_id')
->where('corporate_employees.corporate_id', '=', $corporate_id)
->whereYear('invoice_payments.created_at', $year)
// FILTER
->when($status, fn ($q) =>
$q->where('invoice_payments.status', $status)
)
->when($bn, fn ($q) =>
$q->where('members.member_id', $bn)
)
->when($payorId, fn ($q) =>
$q->where('members.payor_id', $payorId)
)
->when($service, fn ($q) =>
$q->where('claim_requests.service_code', $service)
)
->when($billing, fn ($q) =>
$q->where('invoice_payments.invoice_number', $billing)
)
->selectRaw("
SUBSTRING_INDEX(request_logs.diagnosis, ',', 1) as code_diagnosis,
icd.name as diagnosis,
COUNT(request_logs.id) as total_case,
SUM(request_logs.nominal) as total_billing
")
->groupBy('code_diagnosis', 'icd.name')
->orderByDesc('total_case')
->limit(10)
->get();
return response()->json(
$rows->map(fn ($row) => [
'code' => $row->code_diagnosis,
'diagnosis' => $row->diagnosis ?? '-',
'total_case' => (int) $row->total_case,
'total_billing' => (float) $row->total_billing,
])
);
}
/**
* Display a listing of the resource.
* @return Renderable
*/
// public function index()
// {
// return view('client::index');
// }
/**
* Show the form for creating a new resource.
* @return Renderable
*/
public function create()
{
return view('client::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('client::show');
}
/**
* Show the form for editing the specified resource.
* @param int $id
* @return Renderable
*/
public function edit($id)
{
return view('client::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)
{
//
}
}

View File

@@ -1,6 +1,7 @@
<?php
use Modules\Client\Http\Controllers\Api\AuthController;
use Modules\Client\Http\Controllers\Api\BillingSummaryController;
use Modules\Client\Http\Controllers\Api\CorporateDivisionController;
use Modules\Client\Http\Controllers\Api\CorporateManageController;
use Modules\Client\Http\Controllers\Api\CorporateMemberController;
@@ -78,6 +79,9 @@ Route::prefix('client')->group(function () {
Route::get('corporate', [CorporateCurrentController::class, 'index']);
Route::put('corporate-update', [CorporateCurrentController::class, 'update']);
Route::get('get-deposits', [CorporateMemberController::class, 'getDeposit']);
Route::get('billing/summary', [BillingSummaryController::class, 'index']);
Route::get('/billing/provider-summary', [BillingSummaryController::class, 'providerSummary']);
Route::get('/billing/top-diagnosis', [BillingSummaryController::class, 'topDiagnosis']);
Route::get('get-limits/{member_id}', [CorporateMemberController::class, 'getLimits']);

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('navigations', function (Blueprint $table) {
$table->integer('urutan')
->after('updated_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('navigations', function (Blueprint $table) {
$table->dropColumn('urutan');
});
}
};

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