Merge branch 'feature/report-primayan'

This commit is contained in:
Iqbal
2026-05-11 15:25:02 +07:00
8 changed files with 1067 additions and 46 deletions

View File

@@ -0,0 +1,502 @@
<?php
namespace Modules\Internal\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Models\Corporate;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Fill;
class PrimayanMedicareController extends Controller
{
/**
* List transactions for Primayan Medicare (corporate code: PRAY)
*/
public function index(Request $request)
{
$limit = $request->has('per_page') ? $request->input('per_page') : 10;
$corporate = Corporate::where('code', 'PRAY')->first();
if (!$corporate) {
return response()->json([
'message' => 'Corporate Primayan Medicare (PRAY) not found',
'data' => []
], 404);
}
// Subquery to aggregate amounts from request_log_benefits
$subQueryAmount = DB::table('request_log_benefits')
->select('request_log_id',
DB::raw('SUM(amount_incurred) as total_incurred'),
DB::raw('SUM(amount_approved) as total_approved'),
DB::raw('SUM(excess_paid) as total_excess'))
->whereNull('deleted_at')
->groupBy('request_log_id');
$query = DB::table('claim_requests')
->join('members', 'claim_requests.member_id', '=', 'members.id')
->join('corporate_employees', 'corporate_employees.member_id', '=', 'members.id')
->leftJoin('organizations', 'claim_requests.organization_id', '=', 'organizations.id')
->leftJoin('claims', 'claim_requests.claim_id', '=', 'claims.id')
->leftJoin('request_logs', 'claim_requests.request_log_id', '=', 'request_logs.id')
->leftJoinSub($subQueryAmount, 'log_amounts', function ($join) {
$join->on('request_logs.id', '=', 'log_amounts.request_log_id');
})
->leftJoin('plans', 'claims.plan_id', '=', 'plans.id')
->leftJoin('benefits', 'claims.benefit_id', '=', 'benefits.id')
->where('corporate_employees.corporate_id', $corporate->id)
->whereNull('corporate_employees.deleted_at')
->whereNull('claim_requests.deleted_at')
->when($request->input('search'), function ($query, $search) {
$query->where(function ($query) use ($search) {
$query->orWhere('members.name', 'like', "%" . $search . "%")
->orWhere('members.member_id', 'like', "%" . $search . "%")
->orWhere('claim_requests.code', 'like', "%" . $search . "%")
->orWhere('request_logs.code', 'like', "%" . $search . "%")
->orWhere('claims.code', 'like', "%" . $search . "%")
->orWhere('corporate_employees.nik', 'like', "%" . $search . "%");
});
})
->when($request->input('start_date'), function ($query, $start_date) {
$query->where('claim_requests.created_at', '>=', $start_date);
})
->when($request->input('end_date'), function ($query, $end_date) {
$query->where('claim_requests.created_at', '<=', $end_date . ' 23:59:59');
})
->when($request->input('status'), function ($query, $status) {
if ($status !== 'all') {
$query->where('claim_requests.status', $status);
}
})
->when($request->input('provider_id'), function ($query, $provider_id) {
if ($provider_id !== 'all') {
$query->where('claim_requests.organization_id', $provider_id);
}
})
->select(
'claim_requests.id',
'claim_requests.code as request_code',
'claim_requests.status',
'claim_requests.created_at as request_date',
'claim_requests.service_code',
'request_logs.log_type',
'claims.code as claim_code',
'request_logs.code as log_code',
DB::raw('COALESCE(log_amounts.total_incurred, claims.amount_incurred, 0) as amount_incurred'),
DB::raw('COALESCE(log_amounts.total_approved, claims.amount_approved, 0) as amount_approved'),
DB::raw('COALESCE(claims.amount_not_approved, 0) as amount_not_approved'),
DB::raw('COALESCE(log_amounts.total_excess, claims.excess_paid, 0) as excess_paid'),
'claims.benefit_code',
'claims.benefit_desc',
'members.name as member_name',
'members.member_id as member_number',
'members.birth_date',
'members.gender',
'corporate_employees.nik',
'corporate_employees.branch_code',
'organizations.name as provider_name',
'plans.code as plan_code',
'benefits.code as benefit_code_ref'
)
->orderBy('claim_requests.created_at', 'desc');
$results = $query->paginate($limit)->appends($request->all());
// Get Providers for filter
$providers = DB::table('claim_requests')
->join('members', 'claim_requests.member_id', '=', 'members.id')
->join('corporate_employees', 'corporate_employees.member_id', '=', 'members.id')
->join('organizations', 'claim_requests.organization_id', '=', 'organizations.id')
->where('corporate_employees.corporate_id', $corporate->id)
->whereNull('claim_requests.deleted_at')
->select('organizations.id', 'organizations.name')
->groupBy('organizations.id', 'organizations.name')
->get();
return Helper::responseJson([
'results' => Helper::paginateResources($results),
'providers' => $providers,
]);
}
/**
* Export transactions to Excel
*/
public function export(Request $request)
{
$corporate = Corporate::where('code', 'PRAY')->first();
if (!$corporate) {
return response()->json([
'message' => 'Corporate Primayan Medicare (PRAY) not found',
], 404);
}
$exportType = $request->input('export_type', 'list');
if ($exportType === 'summary') {
return $this->exportSummary($corporate);
}
// Subquery to aggregate amounts from request_log_benefits
$subQueryAmount = DB::table('request_log_benefits')
->select('request_log_id',
DB::raw('SUM(amount_incurred) as total_incurred'),
DB::raw('SUM(amount_approved) as total_approved'),
DB::raw('SUM(excess_paid) as total_excess'))
->whereNull('deleted_at')
->groupBy('request_log_id');
$results = DB::table('claim_requests')
->join('members', 'claim_requests.member_id', '=', 'members.id')
->join('corporate_employees', 'corporate_employees.member_id', '=', 'members.id')
->leftJoin('organizations', 'claim_requests.organization_id', '=', 'organizations.id')
->leftJoin('claims', 'claim_requests.claim_id', '=', 'claims.id')
->leftJoin('request_logs', 'claim_requests.request_log_id', '=', 'request_logs.id')
->leftJoinSub($subQueryAmount, 'log_amounts', function ($join) {
$join->on('request_logs.id', '=', 'log_amounts.request_log_id');
})
->leftJoin('plans', 'claims.plan_id', '=', 'plans.id')
->leftJoin('benefits', 'claims.benefit_id', '=', 'benefits.id')
->where('corporate_employees.corporate_id', $corporate->id)
->whereNull('corporate_employees.deleted_at')
->whereNull('claim_requests.deleted_at')
->when($request->input('search'), function ($query, $search) {
$query->where(function ($query) use ($search) {
$query->orWhere('members.name', 'like', "%" . $search . "%")
->orWhere('members.member_id', 'like', "%" . $search . "%")
->orWhere('claim_requests.code', 'like', "%" . $search . "%")
->orWhere('request_logs.code', 'like', "%" . $search . "%")
->orWhere('claims.code', 'like', "%" . $search . "%")
->orWhere('corporate_employees.nik', 'like', "%" . $search . "%");
});
})
->when($request->input('start_date'), function ($query, $start_date) {
$query->where('claim_requests.created_at', '>=', $start_date);
})
->when($request->input('end_date'), function ($query, $end_date) {
$query->where('claim_requests.created_at', '<=', $end_date . ' 23:59:59');
})
->when($request->input('status'), function ($query, $status) {
if ($status !== 'all') {
$query->where('claim_requests.status', $status);
}
})
->when($request->input('provider_id'), function ($query, $provider_id) {
if ($provider_id !== 'all') {
$query->where('claim_requests.organization_id', $provider_id);
}
})
->select(
'claim_requests.id',
'claim_requests.code as request_code',
'claim_requests.status',
'claim_requests.created_at as request_date',
'claim_requests.service_code',
'request_logs.log_type',
'claims.code as claim_code',
'request_logs.code as log_code',
DB::raw('COALESCE(log_amounts.total_incurred, claims.amount_incurred, 0) as amount_incurred'),
DB::raw('COALESCE(log_amounts.total_approved, claims.amount_approved, 0) as amount_approved'),
DB::raw('COALESCE(claims.amount_not_approved, 0) as amount_not_approved'),
DB::raw('COALESCE(log_amounts.total_excess, claims.excess_paid, 0) as excess_paid'),
'claims.benefit_code',
'claims.benefit_desc',
'members.name as member_name',
'members.member_id as member_number',
'members.birth_date',
'members.gender',
'corporate_employees.nik',
'corporate_employees.branch_code',
'organizations.name as provider_name',
'plans.code as plan_code',
'benefits.code as benefit_code_ref'
)
->orderBy('claim_requests.created_at', 'desc')
->get();
$fileName = 'PrimayanMedicare-List-' . now()->format('Y-m-d_H-i-s') . '.xlsx';
$filePath = storage_path('app/public/temp/' . $fileName);
if (!is_dir(storage_path('app/public/temp'))) {
mkdir(storage_path('app/public/temp'), 0755, true);
}
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$serviceCodeMap = [
'OP' => 'Outpatient',
'IP' => 'Inpatient',
'DE' => 'Dental',
'MA' => 'Maternal',
'GL' => 'Optical',
'MCU' => 'Medical Check Up',
'MEDIVAC' => 'Medical Emergency Evacuation',
];
$headers = [
'No',
'Code Claim / Code LOG',
'Member Name',
'Member Number',
'NIK',
'Birth Date',
'Gender',
'Plan',
'Benefit',
'Service',
'Tipe LOG',
'Provider',
'Request Date',
'Amount Incurred',
'Amount Approved',
'Amount Not Approved',
'Excess Paid',
'Status',
];
$sheet->fromArray($headers, null, 'A1');
$sheet->getStyle('A1:R1')->applyFromArray([
'font' => [
'bold' => true,
],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
]
]);
$rowIndex = 2;
$no = 1;
$logTypeMap = [
'reference' => 'Rujukan',
'prescription' => 'Resep',
'consultation' => 'Konsultasi',
];
foreach ($results as $item) {
$logTypeName = $logTypeMap[$item->log_type] ?? $item->log_type ?? '-';
$rowData = [
$no++,
($item->claim_code && $item->log_code)
? $item->claim_code . ' / ' . $item->log_code
: ($item->claim_code ?? $item->log_code ?? $item->request_code ?? '-'),
$item->member_name ?? '-',
$item->member_number ?? '-',
$item->nik ?? '-',
$item->birth_date ?? '-',
$item->gender ?? '-',
$item->plan_code ?? '-',
$item->benefit_name ?? $item->benefit_desc ?? '-',
$item->service_code ?? '-',
$logTypeName,
$item->provider_name ?? '-',
$item->request_date ?? '-',
(int)($item->amount_incurred ?? 0),
(int)($item->amount_approved ?? 0),
(int)($item->amount_not_approved ?? 0),
(int)($item->excess_paid ?? 0),
$item->status ?? '-',
];
$sheet->fromArray($rowData, null, 'A' . $rowIndex);
$rowIndex++;
}
foreach (range('A', 'R') as $col) {
$sheet->getColumnDimension($col)->setAutoSize(true);
}
$writer = new Xlsx($spreadsheet);
$writer->save($filePath);
return Helper::responseJson([
'file_name' => 'Primayan Medicare List ' . now()->format('Y-m-d H:i:s'),
'file_url' => Storage::disk('public')->url('temp/' . $fileName),
]);
}
private function exportSummary($corporate)
{
// Get all branch codes
$branches = DB::table('corporate_employees')
->where('corporate_id', $corporate->id)
->whereNull('deleted_at')
->select('branch_code')
->groupBy('branch_code')
->get();
$fileName = 'PrimayanMedicare-Summary-' . now()->format('Y-m-d_H-i-s') . '.xlsx';
$filePath = storage_path('app/public/temp/' . $fileName);
if (!is_dir(storage_path('app/public/temp'))) {
mkdir(storage_path('app/public/temp'), 0755, true);
}
$codeRSMap = [
'AI' => 'ABI', 'BB' => 'PHBB', 'BT' => 'PHBT', 'BU' => 'PHBU',
'BW' => 'PHBW', 'CI' => 'PHPC', 'CO' => 'FABS', 'DE' => 'PHDE',
'EV' => 'PHEV', 'HE' => 'PHHE', 'KG' => 'PHKG', 'KI' => 'KMI',
'KR' => 'PHKA', 'KV' => 'KAVA', 'LM' => 'LMS', 'LY' => 'LYNAS',
'MK' => 'PHMA', 'MS' => 'EYEQU', 'PF' => 'FMC', 'PS' => 'PHPK',
'PY' => 'PHBP', 'RA' => 'PHRA', 'SB' => 'PHSB', 'SF' => 'SFI',
'SG' => 'PHSM', 'SM' => 'SIM', 'SO' => 'PHSW', 'SS' => 'SAS',
'TG' => 'PHTA', 'UP' => 'UKRIDA', 'WS' => 'WEST'
];
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// Header Date Range (Row 1)
$dateRange = now()->startOfMonth()->format('j F') . ' - ' . now()->format('j F Y');
$sheet->setCellValue('A1', strtoupper($dateRange));
$sheet->mergeCells('A1:B1');
$sheet->getStyle('A1')->applyFromArray([
'font' => [
'bold' => true,
],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
]
]);
// Table Headers (Row 3)
$headers = ['Code RS', 'Branch Code', 'Total Benefit', 'Deposit 10%', 'Pemakaian Benefit Obat & Rujukan', 'Pemakaian Telekonsultasi', 'Sisa Deposit'];
$sheet->fromArray($headers, null, 'A3');
$sheet->getStyle('A3:G3')->applyFromArray([
'font' => [
'bold' => true,
],
'fill' => [
'fillType' => Fill::FILL_SOLID,
'startColor' => [
'rgb' => 'C6E0B4'
]
],
'borders' => [
'allBorders' => [
'borderStyle' => Border::BORDER_THIN,
]
],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
]
]);
$row = 4;
foreach ($branches as $branch) {
$branchCode = $branch->branch_code ?? '-';
$codeRS = $codeRSMap[$branchCode] ?? '-';
$totalDeposit = DB::table('corporate_employees')
->join('member_plans', 'corporate_employees.member_id', '=', 'member_plans.member_id')
->join('plans', 'member_plans.plan_id', '=', 'plans.id')
->where('corporate_employees.corporate_id', $corporate->id)
->where(function($query) use ($branchCode) {
if ($branchCode === '-') {
$query->whereNull('corporate_employees.branch_code')->orWhere('corporate_employees.branch_code', '');
} else {
$query->where('corporate_employees.branch_code', $branchCode);
}
})
->whereNull('corporate_employees.deleted_at')
->whereNull('member_plans.deleted_at')
->sum(DB::raw('CAST(plans.limit_rules as UNSIGNED)'));
$deposit10 = $totalDeposit * 0.1;
$usageMedicines = DB::table('request_log_medicines')
->join('request_logs', 'request_log_medicines.request_log_id', '=', 'request_logs.id')
->join('corporate_employees', 'request_logs.member_id', '=', 'corporate_employees.member_id')
->where('corporate_employees.corporate_id', $corporate->id)
->where(function($query) use ($branchCode) {
if ($branchCode === '-') {
$query->whereNull('corporate_employees.branch_code')->orWhere('corporate_employees.branch_code', '');
} else {
$query->where('corporate_employees.branch_code', $branchCode);
}
})
->whereIn('request_logs.log_type', ['prescription', 'reference'])
->sum('request_log_medicines.price');
$consultationCount = DB::table('request_logs')
->join('corporate_employees', 'request_logs.member_id', '=', 'corporate_employees.member_id')
->where('corporate_employees.corporate_id', $corporate->id)
->where(function($query) use ($branchCode) {
if ($branchCode === '-') {
$query->whereNull('corporate_employees.branch_code')
->orWhere('corporate_employees.branch_code', '');
} else {
$query->where('corporate_employees.branch_code', $branchCode);
}
})
->where('request_logs.log_type', 'consultation')
->count();
$usageConsultation = $consultationCount * 11100;
$remainingDeposit = $deposit10 - $usageMedicines - $usageConsultation;
$sheet->setCellValue('A' . $row, $codeRS);
$sheet->setCellValue('B' . $row, $branchCode);
$sheet->setCellValue('C' . $row, $totalDeposit);
$sheet->setCellValue('D' . $row, $deposit10);
$sheet->setCellValue('E' . $row, $usageMedicines);
$sheet->setCellValue('F' . $row, $usageConsultation);
$sheet->setCellValue('G' . $row, $remainingDeposit);
// Number format with thousand separator
$sheet->getStyle('C' . $row . ':G' . $row)->getNumberFormat()->setFormatCode('#,##0');
$row++;
}
$lastRow = $row - 1;
foreach (range('A', 'G') as $col) {
$sheet->getColumnDimension($col)->setAutoSize(true);
}
$sheet->getStyle('A3:G' . $lastRow)->applyFromArray([
'borders' => [
'allBorders' => [
'borderStyle' => Border::BORDER_THIN,
'color' => ['rgb' => '000000']
]
]
]);
$sheet->freezePane('A4');
$sheet->setCellValue('E1', '=SUM(E4:E' . $lastRow . ')');
$sheet->setCellValue('F1', '=SUM(F4:F' . $lastRow . ')');
$sheet->getStyle('E1:F1')
->getNumberFormat()
->setFormatCode('#,##0');
$sheet->getStyle('E1:F1')->applyFromArray([
'font' => [
'bold' => true,
'size' => 12,
],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
]
]);
$writer = new Xlsx($spreadsheet);
$writer->save($filePath);
return Helper::responseJson([
'file_name' => $fileName,
'file_url' => Storage::disk('public')->url('temp/' . $fileName),
]);
}
}

View File

@@ -62,6 +62,7 @@ use Modules\Primaya\Http\Controllers\Api\MasterController;
// Report
use Modules\Internal\Http\Controllers\Api\ReportLogController;
use Modules\Internal\Http\Controllers\Api\PrimayanMedicareController;
/*
@@ -112,6 +113,10 @@ Route::prefix('internal')->group(function () {
Route::get('linksehat/rujukan', [RujukanController::class, 'index']);
Route::get('linksehat/rujukan/generate-excel', [RujukanController::class, 'generateExcel']);
// Report Primayan Medicare
Route::get('primayan-medicare', [PrimayanMedicareController::class, 'index']);
Route::get('primayan-medicare/export', [PrimayanMedicareController::class, 'export']);
Route::post('logout', [AuthController::class, 'logout'])->name('logout');
Route::get('/user', function (Request $request) {
return $request->user();

View File

@@ -16,9 +16,10 @@ class NavigationSeeder extends Seeder
public function run()
{
$menuItems = [
// DOCTORS & HOSPITALS
// Dashboard
[
'title' => 'Dashboard',
'urutan' => 1,
'children' => [
[
'title' => 'Dashboard',
@@ -31,6 +32,7 @@ class NavigationSeeder extends Seeder
// DOCTORS & HOSPITALS
[
'title' => 'DOCTORS & HOSPITALS',
'urutan' => 2,
'children' => [
[
'title' => 'Doctors',
@@ -48,6 +50,7 @@ class NavigationSeeder extends Seeder
// PHARMACY & DELIVERY MANAGEMENT
[
'title' => 'PHARMACY & DELIVERY MANAGEMENT',
'urutan' => 3,
'children' => [
[
'title' => 'Drug',
@@ -70,6 +73,7 @@ class NavigationSeeder extends Seeder
// STATION BENEFIT & MEMBERSHIP
[
'title' => 'STATION BENEFIT & MEMBERSHIP',
'urutan' => 4,
'openWhen' => ['/corporates', '/formularium', '/diagnosis', '/hospitals'],
'children' => [
[
@@ -77,7 +81,6 @@ class NavigationSeeder extends Seeder
'path' => '/corporates',
'permission' => 'corporate-list',
],
// ['title' => 'Corporate Create', 'path' => '/corporates/create'],
[
'title' => 'Formularium',
'path' => '/master/formularium-template-v2',
@@ -96,28 +99,33 @@ class NavigationSeeder extends Seeder
],
'permission' => null
],
// CLAIM REQUEST
// MASTER
[
'title' => 'CLAIM REQUEST',
'path' => '/claim-requests',
'title' => 'MASTER',
'urutan' => 5,
'children' => [
[
'title' => 'CLAIM REQUEST',
'path' => '/claim-requests',
'permission' => 'claim-request-list'
'title' => 'Diagnosis',
'path' => '/master/diagnosis',
'permission' => 'diagnosis-list'
],
],
'permission' => null
],
// CLAIM MANAGEMENT
// CUSTOMER SERVICES
[
'title' => 'CLAIM MANAGEMENT',
'path' => '/claims',
'title' => 'CUSTOMER SERVICES',
'urutan' => 6,
'children' => [
[
'title' => 'CLAIM MANAGEMENT',
'path' => '/claims',
'permission' => 'claim-management-list'
'title' => 'Request',
'path' => '/custormer-service/request',
'permission' => 'request-log-list'
],
[
'title' => 'Final LOG',
'path' => '/custormer-service/final-log',
'permission' => 'final-log-list'
],
],
'permission' => null
@@ -125,13 +133,13 @@ class NavigationSeeder extends Seeder
// CASE MANAGEMENT
[
'title' => 'CASE MANAGEMENT',
'urutan' => 7,
'children' => [
[
'title' => 'Daily Monitoring',
'path' => '/case_management/daily_monitoring',
'permission' => 'daily-monitoring-list'
],
// ['title' => 'Laboratorium Result', 'path' => '/case_management/laboratorium_result'],
[
'title' => 'Inpatient Monitoring',
'path' => '/case_management/inpatient_monitoring',
@@ -145,9 +153,56 @@ class NavigationSeeder extends Seeder
],
'permission' => null
],
// Invoice
// CLAIM REQUEST
[
'title' => 'CLAIM REQUEST',
'urutan' => 8,
'path' => '/claim-requests',
'children' => [
[
'title' => 'CLAIM REQUEST',
'path' => '/claim-requests',
'permission' => 'claim-request-list'
],
],
'permission' => null
],
// CLAIM MANAGEMENT
[
'title' => 'CLAIM MANAGEMENT',
'urutan' => 9,
'path' => '/claims',
'children' => [
[
'title' => 'CLAIM MANAGEMENT',
'path' => '/claims',
'permission' => 'claim-management-list'
],
],
'permission' => null
],
// USER MANAGEMENT
[
'title' => 'USER MANAGEMENT',
'urutan' => 10,
'children' => [
[
'title' => 'User Role',
'path' => '/user-role',
'permission' => 'user-role-list'
],
[
'title' => 'User Access',
'path' => '/user-access',
'permission' => 'user-access-list'
],
],
'permission' => null
],
// INVOICE
[
'title' => 'INVOICE',
'urutan' => 11,
'children' => [
[
'title' => 'Invoice',
@@ -157,79 +212,78 @@ class NavigationSeeder extends Seeder
],
'permission' => null
],
// CUSTOMER SERVICES
[
'title' => 'CUSTOMER SERVICES',
'children' => [
[
'title' => 'Request',
'path' => '/custormer-service/request',
'permission' => 'request-log-list'
],
// ['title' => 'Membership', 'path' => '/cs-membership'],
[
'title' => 'Final LOG',
'path' => '/custormer-service/final-log',
'permission' => 'final-log-list'
],
],
'permission' => null
],
// REPORT
[
'title' => 'REPORT',
'urutan' => 12,
'children' => [
[
'title' => 'Files Provider',
'path' => 'report/files-provider',
'permission' => 'report-files-provider-list'
'permission' => 'report-files-provider-list',
'urutan' => 1,
],
[
'title' => 'Letter of Guarantee',
'path' => '/report/logs',
'permission' => 'report-log-list'
'permission' => 'report-log-list',
'urutan' => 2,
],
[
'title' => 'Appointment',
'path' => '/report/appointments',
'permission' => 'report-appointment-list'
'permission' => 'report-appointment-list',
'urutan' => 3,
],
[
'title' => 'Live Chat',
'path' => '/report/live-chat',
'permission' => 'report-livechat-list'
'permission' => 'report-livechat-list',
'urutan' => 4,
],
[
'title' => 'Linksehat Payment',
'path' => '/report/linksehat-payments',
'permission' => 'report-livechat-payment'
'permission' => 'report-livechat-payment',
'urutan' => 5,
],
[
'title' => 'Prescription',
'path' => '/report/prescription',
'permission' => 'report-prescription'
'permission' => 'report-prescription',
'urutan' => 6,
],
[
'title' => 'Doctor Rating',
'path' => '/report/doctor-rating',
'permission' => 'report-doctor-rating'
'permission' => 'report-doctor-rating',
'urutan' => 7,
],
[
'title' => 'Doctor Online',
'path' => '/report/doctor-online',
'permission' => 'report-doctor-online'
'permission' => 'report-doctor-online',
'urutan' => 8,
],
[
'title' => 'Katalog Dokter',
'path' => '/report/katalog-dokter',
'permission' => 'report-katalog-dokter'
]
'permission' => 'report-katalog-dokter',
'urutan' => 9,
],
[
'title' => 'Primayan Medicare',
'path' => '/report/primayan-medicare',
'permission' => 'report-primayan-medicare',
'urutan' => 10,
],
],
'permission' => null
],
// MASTER
[
'title' => 'MASTER',
'urutan' => 11,
'children' => [
[
'title' => 'Diagnosis',
@@ -242,6 +296,7 @@ class NavigationSeeder extends Seeder
// USER MANAGEMENT
[
'title' => 'USER MANAGEMENT',
'urutan' => 9,
'children' => [
[
'title' => 'User Role',
@@ -259,12 +314,14 @@ class NavigationSeeder extends Seeder
// LINKING TOOLS
[
'title' => 'LINKING TOOLS',
'urutan' => 13,
'path' => '/linking',
'permission' => 'linkking-list'
],
// E-PRESCRIPTION
[
'title' => 'E-PRESCRIPTION',
'urutan' => 14,
'path' => '/e-prescription/live-chat',
'permission' => 'prescription-list'
],
@@ -370,7 +427,8 @@ class NavigationSeeder extends Seeder
'title' => $menuItemData['title'],
'path' => $menuItemData['path'] ?? null,
'icon' => $menuItemData['icon'] ?? null,
'permission' => $menuItemData['permission'] ?? null
'permission' => $menuItemData['permission'] ?? null,
'urutan' => $menuItemData['urutan'] ?? null
]);
if (isset($menuItemData['children'])) {
@@ -384,7 +442,8 @@ class NavigationSeeder extends Seeder
'path' => $childData['path'] ?? null,
'icon' => $childData['icon'] ?? null,
'parent_id' => $menuItem->id,
'permission' => $childData['permission'] ?? null
'permission' => $childData['permission'] ?? null,
'urutan' => $childData['urutan'] ?? null
]);
}
}

View File

@@ -70,6 +70,7 @@ class PermissionTableSeeder extends Seeder
'report-livechat-list',
'report-livechat-payment',
'report-doctor-rating',
'report-primayan-medicare',
'report-doctor-online',
'report-prescription',
'user-role-list',

View File

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

View File

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

View File

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

View File

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