Update Dashboard Client Portal
This commit is contained in:
286
Modules/Client/Http/Controllers/Api/BillingSummaryController.php
Normal file
286
Modules/Client/Http/Controllers/Api/BillingSummaryController.php
Normal 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)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
182
frontend/client-portal/src/pages/Dashboard/BillingFilterCard.tsx
Normal file
182
frontend/client-portal/src/pages/Dashboard/BillingFilterCard.tsx
Normal 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;
|
||||
@@ -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;
|
||||
44
frontend/client-portal/src/pages/Dashboard/Dashboard.tsx
Normal file
44
frontend/client-portal/src/pages/Dashboard/Dashboard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
125
frontend/client-portal/src/pages/Dashboard/DashboardBilling.tsx
Normal file
125
frontend/client-portal/src/pages/Dashboard/DashboardBilling.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
35
frontend/client-portal/src/pages/Dashboard/billingData.ts
Normal file
35
frontend/client-portal/src/pages/Dashboard/billingData.ts
Normal 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: [],
|
||||
},
|
||||
];
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user