Merge branch 'origin/production' of https://dev.sismedika.online/tubagus/aso into origin/production
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
|
<?php
|
||||||
|
|
||||||
use Modules\Client\Http\Controllers\Api\AuthController;
|
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\CorporateDivisionController;
|
||||||
use Modules\Client\Http\Controllers\Api\CorporateManageController;
|
use Modules\Client\Http\Controllers\Api\CorporateManageController;
|
||||||
use Modules\Client\Http\Controllers\Api\CorporateMemberController;
|
use Modules\Client\Http\Controllers\Api\CorporateMemberController;
|
||||||
@@ -78,6 +79,9 @@ Route::prefix('client')->group(function () {
|
|||||||
Route::get('corporate', [CorporateCurrentController::class, 'index']);
|
Route::get('corporate', [CorporateCurrentController::class, 'index']);
|
||||||
Route::put('corporate-update', [CorporateCurrentController::class, 'update']);
|
Route::put('corporate-update', [CorporateCurrentController::class, 'update']);
|
||||||
Route::get('get-deposits', [CorporateMemberController::class, 'getDeposit']);
|
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']);
|
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')));
|
const Login = Loadable(lazy(() => import('../pages/auth/Login')));
|
||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
const Dashboard = Loadable(lazy(() => import('../pages/Dashboard/Index')));
|
const Dashboard = Loadable(lazy(() => import('../pages/Dashboard/Dashboard')));
|
||||||
const NotFound = Loadable(lazy(() => import('../pages/Page404')));
|
const NotFound = Loadable(lazy(() => import('../pages/Page404')));
|
||||||
|
|
||||||
// Employee Data
|
// Employee Data
|
||||||
|
|||||||
Reference in New Issue
Block a user