This commit is contained in:
2025-08-12 17:51:40 +07:00
commit 2e396f32b9
35 changed files with 11038 additions and 0 deletions

View File

@@ -0,0 +1,681 @@
import { useState } from "react";
import {
TrendingUp,
DollarSign,
Calendar,
Search,
Filter,
Eye,
Download,
AlertCircle,
User,
FileText,
Calculator,
Target,
Code,
Stethoscope,
} from "lucide-react";
interface CostRecommendation {
id: string;
patientId: string;
patientName: string;
age: number;
gender: string;
diagnosis: string;
icdCode: string;
procedureCode?: string;
medicalRecordNumber: string;
admissionDate: string;
dischargeDate?: string;
roomType: string;
currentTreatment: string;
estimatedCost: number;
recommendedTreatment: string;
recommendedCost: number;
potentialSavings: number;
riskLevel: "low" | "medium" | "high";
confidence: number;
department: string;
doctor: string;
createdDate: string;
status: "pending" | "approved" | "rejected" | "implemented";
medicalHistory?: string;
allergies?: string;
currentMedications?: string;
vitalSigns?: {
bloodPressure: string;
heartRate: number;
temperature: number;
oxygenSaturation: number;
};
labResults?: string;
notes?: string;
}
interface CostStats {
totalRecommendations: number;
potentialSavings: number;
implementedSavings: number;
avgSavingsPerCase: number;
approvalRate: number;
}
export default function CostRecommendation() {
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [riskFilter, setRiskFilter] = useState("all");
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString("id-ID", {
day: "numeric",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
}).format(amount);
};
const getRiskColor = (risk: string) => {
switch (risk) {
case "low":
return "bg-green-100 text-green-800";
case "medium":
return "bg-yellow-100 text-yellow-800";
case "high":
return "bg-red-100 text-red-800";
default:
return "bg-gray-100 text-gray-800";
}
};
const getStatusColor = (status: string) => {
switch (status) {
case "approved":
return "bg-green-100 text-green-800";
case "pending":
return "bg-yellow-100 text-yellow-800";
case "rejected":
return "bg-red-100 text-red-800";
case "implemented":
return "bg-blue-100 text-blue-800";
default:
return "bg-gray-100 text-gray-800";
}
};
const getStatusText = (status: string) => {
switch (status) {
case "approved":
return "Disetujui";
case "pending":
return "Menunggu";
case "rejected":
return "Ditolak";
case "implemented":
return "Diterapkan";
default:
return status;
}
};
const getRiskText = (risk: string) => {
switch (risk) {
case "low":
return "Rendah";
case "medium":
return "Sedang";
case "high":
return "Tinggi";
default:
return risk;
}
};
// Sample cost recommendations data
const [recommendations] = useState<CostRecommendation[]>([
{
id: "1",
patientId: "P001",
patientName: "Ahmad Santoso",
age: 45,
gender: "Laki-laki",
diagnosis: "Hipertensi Esensial",
icdCode: "I10",
procedureCode: "99213",
medicalRecordNumber: "MR2024001",
admissionDate: "2024-01-10T08:00:00Z",
dischargeDate: "2024-01-11T16:00:00Z",
roomType: "Ruang Rawat Jalan",
currentTreatment: "Amlodipine 10mg + Monitoring Harian",
estimatedCost: 2500000,
recommendedTreatment: "Amlodipine 5mg + Monitoring Mingguan",
recommendedCost: 1800000,
potentialSavings: 700000,
riskLevel: "low",
confidence: 92,
department: "Poli Dalam",
doctor: "Dr. Sarah Wijaya",
createdDate: "2024-01-15T14:30:00Z",
status: "pending",
medicalHistory: "Riwayat hipertensi keluarga, tidak ada riwayat stroke",
allergies: "Tidak ada alergi obat yang diketahui",
currentMedications: "Amlodipine 10mg 1x1, Aspirin 80mg 1x1",
vitalSigns: {
bloodPressure: "140/90",
heartRate: 78,
temperature: 36.5,
oxygenSaturation: 98,
},
labResults: "Kolesterol Total: 220 mg/dl, HDL: 45 mg/dl, LDL: 140 mg/dl",
notes: "Pasien menunjukkan respons baik dengan dosis rendah",
},
{
id: "2",
patientId: "P002",
patientName: "Siti Nurhaliza",
age: 52,
gender: "Perempuan",
diagnosis: "Diabetes Mellitus Tipe 2",
icdCode: "E11",
procedureCode: "99214",
medicalRecordNumber: "MR2024002",
admissionDate: "2024-01-12T09:30:00Z",
dischargeDate: "2024-01-14T14:00:00Z",
roomType: "Ruang Rawat Inap Kelas II",
currentTreatment: "Insulin + Metformin + Konsultasi Harian",
estimatedCost: 4200000,
recommendedTreatment: "Metformin + Diet Program + Konsultasi Mingguan",
recommendedCost: 2800000,
potentialSavings: 1400000,
riskLevel: "medium",
confidence: 87,
department: "Poli Endokrin",
doctor: "Dr. Rahman Hidayat",
createdDate: "2024-01-15T10:15:00Z",
status: "approved",
medicalHistory: "DM Tipe 2 sejak 5 tahun, riwayat hipertensi",
allergies: "Alergi sulfa",
currentMedications: "Insulin Lantus 20 unit, Metformin 500mg 2x1",
vitalSigns: {
bloodPressure: "130/85",
heartRate: 82,
temperature: 36.8,
oxygenSaturation: 96,
},
labResults: "HbA1c: 8.2%, Glukosa Puasa: 180 mg/dl, Kreatinin: 1.1 mg/dl",
notes: "Pasien cocok dengan program diet terstruktur",
},
{
id: "3",
patientId: "P003",
patientName: "Budi Prasetyo",
age: 38,
gender: "Laki-laki",
diagnosis: "Pneumonia",
icdCode: "J18.9",
procedureCode: "99223",
medicalRecordNumber: "MR2024003",
admissionDate: "2024-01-11T15:00:00Z",
dischargeDate: "2024-01-13T10:30:00Z",
roomType: "Ruang Rawat Inap Kelas I",
currentTreatment: "Antibiotik IV + Rawat Inap 7 hari",
estimatedCost: 8500000,
recommendedTreatment: "Antibiotik Oral + Rawat Jalan",
recommendedCost: 3200000,
potentialSavings: 5300000,
riskLevel: "high",
confidence: 78,
department: "Poli Paru",
doctor: "Dr. Linda Sari",
createdDate: "2024-01-14T16:45:00Z",
status: "implemented",
medicalHistory: "Riwayat merokok 15 tahun, berhenti 2 tahun lalu",
allergies: "Alergi penisilin",
currentMedications: "Ceftriaxone 2g IV, Azithromycin 500mg",
vitalSigns: {
bloodPressure: "120/80",
heartRate: 92,
temperature: 38.2,
oxygenSaturation: 94,
},
labResults: "Leukosit: 12,000/μL, CRP: 15 mg/L, Procalcitonin: 0.8 ng/mL",
notes: "Monitoring ketat diperlukan untuk rawat jalan",
},
{
id: "4",
patientId: "P004",
patientName: "Maria Lopez",
age: 29,
gender: "Perempuan",
diagnosis: "Gastritis Akut",
icdCode: "K29.1",
procedureCode: "99212",
medicalRecordNumber: "MR2024004",
admissionDate: "2024-01-13T08:15:00Z",
roomType: "Ruang Rawat Jalan",
currentTreatment: "PPI + Antasida + Konsultasi Harian",
estimatedCost: 1500000,
recommendedTreatment: "H2 Blocker + Diet + Konsultasi Mingguan",
recommendedCost: 800000,
potentialSavings: 700000,
riskLevel: "low",
confidence: 94,
department: "Poli Dalam",
doctor: "Dr. Sarah Wijaya",
createdDate: "2024-01-13T11:20:00Z",
status: "rejected",
medicalHistory: "Tidak ada riwayat penyakit kronis",
allergies: "Alergi H2 blocker (ranitidin)",
currentMedications: "Omeprazole 20mg 1x1, Antasida 3x1",
vitalSigns: {
bloodPressure: "110/70",
heartRate: 75,
temperature: 36.4,
oxygenSaturation: 99,
},
labResults: "Hemoglobin: 12.5 g/dl, H. pylori: Negatif",
notes: "Pasien memiliki riwayat alergi terhadap H2 blocker",
},
{
id: "5",
patientId: "P005",
patientName: "Andi Kusuma",
age: 67,
gender: "Laki-laki",
diagnosis: "Osteoarthritis",
icdCode: "M15.9",
procedureCode: "99215",
medicalRecordNumber: "MR2024005",
admissionDate: "2024-01-12T07:45:00Z",
roomType: "Ruang Rawat Jalan",
currentTreatment: "NSAID + Fisioterapi Intensif",
estimatedCost: 3200000,
recommendedTreatment: "Paracetamol + Fisioterapi Standar + Olahraga",
recommendedCost: 1900000,
potentialSavings: 1300000,
riskLevel: "low",
confidence: 89,
department: "Ortopedi",
doctor: "Dr. Kevin Tan",
createdDate: "2024-01-12T09:30:00Z",
status: "approved",
medicalHistory: "Osteoarthritis bilateral knee sejak 10 tahun",
allergies: "Tidak ada alergi obat yang diketahui",
currentMedications: "Diclofenac 50mg 2x1, Glucosamine 500mg 2x1",
vitalSigns: {
bloodPressure: "135/85",
heartRate: 68,
temperature: 36.3,
oxygenSaturation: 97,
},
labResults: "Fungsi ginjal normal, tidak ada tanda inflamasi sistemik",
notes: "Pasien menunjukkan respons baik dengan terapi konservatif",
},
]);
// Calculate statistics
const stats: CostStats = {
totalRecommendations: recommendations.length,
potentialSavings: recommendations.reduce(
(sum, r) => sum + r.potentialSavings,
0
),
implementedSavings: recommendations
.filter((r) => r.status === "implemented")
.reduce((sum, r) => sum + r.potentialSavings, 0),
avgSavingsPerCase:
recommendations.reduce((sum, r) => sum + r.potentialSavings, 0) /
recommendations.length,
approvalRate:
(recommendations.filter(
(r) => r.status === "approved" || r.status === "implemented"
).length /
recommendations.length) *
100,
};
// Filter recommendations
const filteredRecommendations = recommendations.filter((rec) => {
const matchesSearch =
rec.patientName.toLowerCase().includes(searchTerm.toLowerCase()) ||
rec.diagnosis.toLowerCase().includes(searchTerm.toLowerCase()) ||
rec.icdCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
rec.doctor.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === "all" || rec.status === statusFilter;
const matchesRisk = riskFilter === "all" || rec.riskLevel === riskFilter;
return matchesSearch && matchesStatus && matchesRisk;
});
return (
<div className="p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center">
<TrendingUp className="h-8 w-8 text-green-600 mr-3" />
Cost Recommendation
</h1>
<p className="text-gray-600 mt-1">
Rekomendasi optimalisasi biaya perawatan berdasarkan analisis
medis
</p>
</div>
<div className="flex space-x-3">
<button className="btn-secondary flex items-center space-x-2">
<Download className="h-4 w-4" />
<span>Export Report</span>
</button>
<button className="btn-primary flex items-center space-x-2">
<Calculator className="h-4 w-4" />
<span>Analisis Baru</span>
</button>
</div>
</div>
</div>
{/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Total Rekomendasi
</p>
<p className="text-2xl font-bold text-blue-600">
{stats.totalRecommendations}
</p>
</div>
<div className="p-3 bg-blue-100 rounded-lg">
<FileText className="h-6 w-6 text-blue-600" />
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Potensi Penghematan
</p>
<p className="text-2xl font-bold text-green-600">
{formatCurrency(stats.potentialSavings)}
</p>
</div>
<div className="p-3 bg-green-100 rounded-lg">
<TrendingUp className="h-6 w-6 text-green-600" />
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Penghematan Terealisasi
</p>
<p className="text-2xl font-bold text-purple-600">
{formatCurrency(stats.implementedSavings)}
</p>
</div>
<div className="p-3 bg-purple-100 rounded-lg">
<DollarSign className="h-6 w-6 text-purple-600" />
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Tingkat Persetujuan
</p>
<p className="text-2xl font-bold text-orange-600">
{Math.round(stats.approvalRate)}%
</p>
</div>
<div className="p-3 bg-orange-100 rounded-lg">
<Target className="h-6 w-6 text-orange-600" />
</div>
</div>
</div>
</div>
{/* Filters and Search */}
<div className="bg-white p-6 rounded-lg shadow-sm border mb-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
<div className="flex items-center space-x-4">
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Cari pasien, diagnosis, atau dokter..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent w-80"
/>
</div>
<div className="flex items-center space-x-2">
<Filter className="h-4 w-4 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-green-500 focus:border-transparent"
>
<option value="all">Semua Status</option>
<option value="pending">Menunggu</option>
<option value="approved">Disetujui</option>
<option value="rejected">Ditolak</option>
<option value="implemented">Diterapkan</option>
</select>
<select
value={riskFilter}
onChange={(e) => setRiskFilter(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-green-500 focus:border-transparent"
>
<option value="all">Semua Risiko</option>
<option value="low">Risiko Rendah</option>
<option value="medium">Risiko Sedang</option>
<option value="high">Risiko Tinggi</option>
</select>
</div>
</div>
</div>
</div>
{/* Recommendations Table */}
<div className="bg-white rounded-lg shadow-sm border overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">
Rekomendasi Optimalisasi Biaya
</h3>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Pasien & Medical Record
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Medical Code
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Treatment Saat Ini
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Rekomendasi
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Potensi Penghematan
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status & Risiko
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tanggal
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Aksi
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredRecommendations.map((rec) => (
<tr key={rec.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<div className="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
<User className="h-5 w-5 text-gray-600" />
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
{rec.patientName}
</div>
<div className="text-sm text-gray-500">
{rec.diagnosis}
</div>
<div className="text-xs text-gray-400 flex items-center">
<FileText className="h-3 w-3 mr-1" />
MR: {rec.medicalRecordNumber}
</div>
<div className="text-xs text-gray-400">
{rec.age} th, {rec.gender} | {rec.roomType}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-1">
<div className="flex items-center text-sm font-medium text-gray-900">
<Code className="h-4 w-4 mr-2 text-blue-600" />
ICD: {rec.icdCode}
</div>
{rec.procedureCode && (
<div className="flex items-center text-sm text-gray-600">
<Stethoscope className="h-4 w-4 mr-2 text-green-600" />
CPT: {rec.procedureCode}
</div>
)}
<div className="text-xs text-gray-500">
Dept: {rec.department}
</div>
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900">
{rec.currentTreatment}
</div>
<div className="text-sm text-gray-500">
Estimasi: {formatCurrency(rec.estimatedCost)}
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900">
{rec.recommendedTreatment}
</div>
<div className="text-sm text-green-600">
Biaya: {formatCurrency(rec.recommendedCost)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-green-600">
{formatCurrency(rec.potentialSavings)}
</div>
<div className="text-xs text-gray-500">
{Math.round(
(rec.potentialSavings / rec.estimatedCost) * 100
)}
% penghematan
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex flex-col space-y-2">
<span
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(
rec.status
)}`}
>
{getStatusText(rec.status)}
</span>
<div className="flex items-center space-x-2">
<span
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getRiskColor(
rec.riskLevel
)}`}
>
{getRiskText(rec.riskLevel)}
</span>
<div className="text-xs text-gray-600">
{rec.confidence}%
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-1">
<div className="flex items-center text-sm text-gray-500">
<Calendar className="h-4 w-4 mr-1" />
{formatDate(rec.createdDate)}
</div>
<div className="text-xs text-gray-400">
Admission: {formatDate(rec.admissionDate)}
</div>
{rec.dischargeDate && (
<div className="text-xs text-gray-400">
Discharge: {formatDate(rec.dischargeDate)}
</div>
)}
<div className="text-xs text-gray-500">
Dr. {rec.doctor.replace("Dr. ", "")}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center space-x-2">
<button className="text-blue-600 hover:text-blue-900">
<Eye className="h-4 w-4" />
</button>
<button className="text-green-600 hover:text-green-900">
<Download className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Empty State */}
{filteredRecommendations.length === 0 && (
<div className="text-center py-12">
<AlertCircle className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">
Tidak ada rekomendasi ditemukan
</h3>
<p className="mt-1 text-sm text-gray-500">
Tidak ada rekomendasi yang sesuai dengan kriteria pencarian.
</p>
</div>
)}
</div>
</div>
);
}