1346 lines
49 KiB
TypeScript
1346 lines
49 KiB
TypeScript
import { useState } from "react";
|
|
import {
|
|
TrendingUp,
|
|
DollarSign,
|
|
Calendar,
|
|
Search,
|
|
Filter,
|
|
Eye,
|
|
Download,
|
|
AlertCircle,
|
|
User,
|
|
FileText,
|
|
Calculator,
|
|
Target,
|
|
Code,
|
|
Stethoscope,
|
|
} from "lucide-react";
|
|
import DatePicker from "react-datepicker";
|
|
import "react-datepicker/dist/react-datepicker.css";
|
|
|
|
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 function LegacyCostRecommendation() {
|
|
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>
|
|
);
|
|
}
|
|
|
|
interface ICDRecommendation {
|
|
code: string;
|
|
description: string;
|
|
confidence: number;
|
|
reasoning: string;
|
|
}
|
|
|
|
async function mockRecommendICD(
|
|
diagnosis: string,
|
|
procedure: string
|
|
): Promise<ICDRecommendation[]> {
|
|
const lowerDx = diagnosis.toLowerCase();
|
|
const lowerProc = procedure.toLowerCase();
|
|
|
|
const suggestions: ICDRecommendation[] = [];
|
|
|
|
if (lowerDx.includes("hipertensi") || lowerDx.includes("hypertension")) {
|
|
suggestions.push({
|
|
code: "I10",
|
|
description: "Essential (primary) hypertension",
|
|
confidence: 92,
|
|
reasoning: "Kata kunci 'hipertensi' terdeteksi pada diagnosis klinis.",
|
|
});
|
|
}
|
|
|
|
if (lowerDx.includes("diabetes") || lowerDx.includes("dm")) {
|
|
suggestions.push({
|
|
code: "E11",
|
|
description: "Type 2 diabetes mellitus",
|
|
confidence: 88,
|
|
reasoning: "Terdapat indikasi DM tipe 2 pada gambaran diagnosis.",
|
|
});
|
|
}
|
|
|
|
if (lowerDx.includes("pneumonia") || lowerDx.includes("infeksi paru")) {
|
|
suggestions.push({
|
|
code: "J18.9",
|
|
description: "Pneumonia, unspecified organism",
|
|
confidence: 82,
|
|
reasoning: "Gejala dan istilah yang mengarah ke pneumonia terdeteksi.",
|
|
});
|
|
}
|
|
|
|
if (lowerDx.includes("gastritis") || lowerDx.includes("dispepsia")) {
|
|
suggestions.push({
|
|
code: "K29.1",
|
|
description: "Acute gastritis",
|
|
confidence: 80,
|
|
reasoning: "Istilah gastritis muncul pada diagnosis.",
|
|
});
|
|
}
|
|
|
|
if (lowerDx.includes("osteoarthritis") || lowerDx.includes("nyeri sendi")) {
|
|
suggestions.push({
|
|
code: "M15.9",
|
|
description: "Polyosteoarthritis, unspecified",
|
|
confidence: 76,
|
|
reasoning: "Keluhan terkait osteoartritis/nyeri sendi teridentifikasi.",
|
|
});
|
|
}
|
|
|
|
if (
|
|
lowerProc.includes("insulin") &&
|
|
!suggestions.find((s) => s.code.startsWith("E1"))
|
|
) {
|
|
suggestions.push({
|
|
code: "E10-E14",
|
|
description: "Diabetes mellitus (range, sesuaikan subkategori)",
|
|
confidence: 70,
|
|
reasoning: "Penggunaan insulin mengarah pada kategori diabetes mellitus.",
|
|
});
|
|
}
|
|
|
|
if (
|
|
lowerProc.includes("antibiotik") &&
|
|
!suggestions.find((s) => s.code.startsWith("J"))
|
|
) {
|
|
suggestions.push({
|
|
code: "J15-J18",
|
|
description: "Bacterial/unspecified pneumonia (range)",
|
|
confidence: 68,
|
|
reasoning: "Terapi antibiotik berkolerasi dengan infeksi respiratori.",
|
|
});
|
|
}
|
|
|
|
if (suggestions.length === 0) {
|
|
suggestions.push(
|
|
{
|
|
code: "R69",
|
|
description: "Illness, unspecified",
|
|
confidence: 55,
|
|
reasoning:
|
|
"Tidak ada kata kunci spesifik terdeteksi. Perlu klarifikasi klinis.",
|
|
},
|
|
{
|
|
code: "Z00.0",
|
|
description: "General medical examination",
|
|
confidence: 48,
|
|
reasoning: "Pertimbangkan evaluasi umum jika kasus bersifat screening.",
|
|
}
|
|
);
|
|
}
|
|
|
|
suggestions.sort((a, b) => b.confidence - a.confidence);
|
|
await new Promise((r) => setTimeout(r, 600));
|
|
return suggestions.slice(0, 6);
|
|
}
|
|
|
|
export default function CostRecommendation() {
|
|
const [diagnosis, setDiagnosis] = useState("");
|
|
const [procedure, setProcedure] = useState("");
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [results, setResults] = useState<ICDRecommendation[]>([]);
|
|
const [visitType, setVisitType] = useState("Kontrol Rutin");
|
|
const [lastVisitDate, setLastVisitDate] = useState<Date | undefined>(
|
|
undefined
|
|
);
|
|
const [currentVisitDate, setCurrentVisitDate] = useState<Date | undefined>(
|
|
new Date()
|
|
);
|
|
|
|
type BpjsMapping = {
|
|
icdCode: string;
|
|
bpjsCode: string;
|
|
description: string;
|
|
estTariff: number;
|
|
};
|
|
|
|
const BPJS_CODE_MAPPINGS: BpjsMapping[] = [
|
|
{
|
|
icdCode: "I10",
|
|
bpjsCode: "BPJS-I10-01",
|
|
description: "Hipertensi Esensial",
|
|
estTariff: 850000,
|
|
},
|
|
{
|
|
icdCode: "E11",
|
|
bpjsCode: "BPJS-E11-02",
|
|
description: "Diabetes Mellitus Tipe 2",
|
|
estTariff: 1200000,
|
|
},
|
|
{
|
|
icdCode: "J18.9",
|
|
bpjsCode: "BPJS-J18-03",
|
|
description: "Pneumonia",
|
|
estTariff: 2500000,
|
|
},
|
|
{
|
|
icdCode: "K29.1",
|
|
bpjsCode: "BPJS-K29-04",
|
|
description: "Gastritis Akut",
|
|
estTariff: 650000,
|
|
},
|
|
{
|
|
icdCode: "M15.9",
|
|
bpjsCode: "BPJS-M15-09",
|
|
description: "Osteoartritis",
|
|
estTariff: 900000,
|
|
},
|
|
];
|
|
|
|
const findBpjsMappingForICD = (code: string): BpjsMapping | null => {
|
|
if (!code) return null;
|
|
const normalized = code === "E10-E14" ? "E11" : code;
|
|
const exact = BPJS_CODE_MAPPINGS.find((m) => m.icdCode === normalized);
|
|
if (exact) return exact;
|
|
return (
|
|
BPJS_CODE_MAPPINGS.find((m) => normalized.startsWith(m.icdCode)) || null
|
|
);
|
|
};
|
|
|
|
const bpjsRecommendations = results
|
|
.map((r) => ({ icdCode: r.code, mapping: findBpjsMappingForICD(r.code) }))
|
|
.filter((x) => x.mapping !== null) as {
|
|
icdCode: string;
|
|
mapping: BpjsMapping;
|
|
}[];
|
|
|
|
const getDaysBetween = (a: Date | undefined, b: Date | undefined) => {
|
|
try {
|
|
if (!a || !b) return null;
|
|
const diff = Math.floor(
|
|
(b.getTime() - a.getTime()) / (1000 * 60 * 60 * 24)
|
|
);
|
|
return isNaN(diff) ? null : Math.max(diff, 0);
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const checkReasonConsistency = (type: string, dx: string, proc: string) => {
|
|
const t = type.toLowerCase();
|
|
const text = `${dx} ${proc}`.toLowerCase();
|
|
const followUpKeywords = [
|
|
"kontrol",
|
|
"follow up",
|
|
"lanjutan",
|
|
"kontrol ulang",
|
|
"monitoring",
|
|
];
|
|
const acuteKeywords = [
|
|
"akut",
|
|
"baru",
|
|
"nyeri hebat",
|
|
"mendadak",
|
|
"demam tinggi",
|
|
"perburukan",
|
|
];
|
|
const foundFollow = followUpKeywords.some((k) => text.includes(k));
|
|
const foundAcute = acuteKeywords.some((k) => text.includes(k));
|
|
const reasons: string[] = [];
|
|
let inconsistent = false;
|
|
|
|
if (t.includes("kontrol") && foundAcute) {
|
|
inconsistent = true;
|
|
reasons.push(
|
|
"Terdapat indikasi kondisi akut pada keterangan, namun tipe kunjungan adalah kontrol."
|
|
);
|
|
}
|
|
if (t.includes("keluhan baru") && foundFollow) {
|
|
inconsistent = true;
|
|
reasons.push(
|
|
"Terdapat indikasi tindak lanjut/kontrol pada keterangan, namun dipilih 'Keluhan Baru'."
|
|
);
|
|
}
|
|
return { inconsistent, reasons };
|
|
};
|
|
|
|
const ICD_INTERVAL_RULES: Record<string, { minDays: number; note: string }> =
|
|
{
|
|
I10: { minDays: 14, note: "Kontrol hipertensi" },
|
|
E11: { minDays: 30, note: "Kontrol DM tipe 2" },
|
|
"J18.9": { minDays: 14, note: "Pneumonia" },
|
|
"K29.1": { minDays: 14, note: "Gastritis akut" },
|
|
"M15.9": { minDays: 30, note: "Osteoartritis" },
|
|
};
|
|
|
|
const getIntervalRuleForCode = (code: string) => {
|
|
if (ICD_INTERVAL_RULES[code as keyof typeof ICD_INTERVAL_RULES])
|
|
return ICD_INTERVAL_RULES[code as keyof typeof ICD_INTERVAL_RULES];
|
|
if (
|
|
code.startsWith("E10") ||
|
|
code.startsWith("E11") ||
|
|
code.startsWith("E12") ||
|
|
code.startsWith("E13") ||
|
|
code.startsWith("E14") ||
|
|
code === "E10-E14"
|
|
) {
|
|
return ICD_INTERVAL_RULES.E11;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
const daysSinceLast =
|
|
lastVisitDate && currentVisitDate
|
|
? getDaysBetween(lastVisitDate, currentVisitDate)
|
|
: null;
|
|
const consistency = checkReasonConsistency(visitType, diagnosis, procedure);
|
|
const showInconsistentControlWarning =
|
|
visitType.toLowerCase().includes("kontrol") &&
|
|
daysSinceLast !== null &&
|
|
daysSinceLast < 30 &&
|
|
consistency.inconsistent;
|
|
const overclaimItems = results
|
|
.map((r) => ({ rec: r, rule: getIntervalRuleForCode(r.code) }))
|
|
.filter(
|
|
(x) =>
|
|
x.rule &&
|
|
daysSinceLast !== null &&
|
|
(daysSinceLast as number) < (x.rule as { minDays: number }).minDays
|
|
);
|
|
|
|
const formatCurrency = (amount: number) => {
|
|
return new Intl.NumberFormat("id-ID", {
|
|
style: "currency",
|
|
currency: "IDR",
|
|
minimumFractionDigits: 0,
|
|
}).format(amount);
|
|
};
|
|
|
|
const handleGenerate = async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
try {
|
|
const recs = await mockRecommendICD(diagnosis, procedure);
|
|
setResults(recs);
|
|
} catch {
|
|
setError("Gagal membuat rekomendasi. Coba lagi.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const exportJSON = () => {
|
|
const blob = new Blob([JSON.stringify(results, null, 2)], {
|
|
type: "application/json",
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = "icd_recommendations.json";
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const copyCode = async (code: string) => {
|
|
try {
|
|
await navigator.clipboard.writeText(code);
|
|
} catch {
|
|
// ignore clipboard errors in non-secure contexts
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<div className="max-w-5xl mx-auto">
|
|
<div className="mb-8">
|
|
<h1 className="text-2xl font-bold text-gray-900">
|
|
Asisten Rekomendasi ICD untuk Dokter
|
|
</h1>
|
|
<p className="text-gray-600 mt-1">
|
|
Masukkan ringkasan diagnosa dan prosedur. Sistem akan mengusulkan
|
|
kode ICD yang relevan beserta tingkat keyakinan.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-white p-6 rounded-lg shadow-sm border mb-6">
|
|
<div className="grid grid-cols-1 gap-4">
|
|
<div>
|
|
<label className="text-sm font-medium text-gray-700 mb-1 flex items-center">
|
|
<FileText className="h-4 w-4 mr-2 text-blue-600" /> Diagnosa
|
|
Klinis
|
|
</label>
|
|
<textarea
|
|
value={diagnosis}
|
|
onChange={(e) => setDiagnosis(e.target.value)}
|
|
rows={4}
|
|
placeholder="Contoh: Pasien dengan hipertensi esensial, keluhan pusing dan tekanan darah 150/95 mmHg."
|
|
className="w-full rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent p-3"
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Tipe Kunjungan
|
|
</label>
|
|
<select
|
|
value={visitType}
|
|
onChange={(e) => setVisitType(e.target.value)}
|
|
className="w-full rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent p-3"
|
|
>
|
|
<option>Kontrol Rutin</option>
|
|
<option>Keluhan Baru</option>
|
|
<option>Tindak Lanjut Prosedur</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Tanggal Kunjungan Terakhir{" "}
|
|
<span className="text-red-500">*</span>
|
|
</label>
|
|
<DatePicker
|
|
selected={lastVisitDate}
|
|
onChange={(date) => setLastVisitDate(date || undefined)}
|
|
placeholderText="Pilih tanggal kunjungan terakhir"
|
|
className="w-full rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent p-3"
|
|
dateFormat="dd MMM yyyy"
|
|
maxDate={new Date()}
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Tanggal Kunjungan Saat Ini{" "}
|
|
<span className="text-red-500">*</span>
|
|
</label>
|
|
<DatePicker
|
|
selected={currentVisitDate}
|
|
onChange={(date) => setCurrentVisitDate(date || undefined)}
|
|
placeholderText="Pilih tanggal kunjungan saat ini"
|
|
className="w-full rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent p-3"
|
|
dateFormat="dd MMM yyyy"
|
|
maxDate={new Date()}
|
|
minDate={lastVisitDate}
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-gray-700 mb-1 flex items-center">
|
|
<Stethoscope className="h-4 w-4 mr-2 text-green-600" />{" "}
|
|
Prosedur/Tatalaksana Utama
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={procedure}
|
|
onChange={(e) => setProcedure(e.target.value)}
|
|
placeholder="Contoh: Terapi antibiotik IV, insulin basal, fisioterapi, dsb."
|
|
className="w-full rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent p-3"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center space-x-3">
|
|
<button
|
|
onClick={handleGenerate}
|
|
disabled={
|
|
isLoading ||
|
|
(!diagnosis && !procedure) ||
|
|
!lastVisitDate ||
|
|
!currentVisitDate
|
|
}
|
|
className="btn-primary flex items-center space-x-2 disabled:opacity-60"
|
|
>
|
|
<Search className="h-4 w-4" />
|
|
<span>
|
|
{isLoading ? "Menghasilkan..." : "Buat Rekomendasi"}
|
|
</span>
|
|
</button>
|
|
<button
|
|
onClick={exportJSON}
|
|
disabled={results.length === 0}
|
|
className="btn-secondary flex items-center space-x-2 disabled:opacity-60"
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
<span>Export JSON</span>
|
|
</button>
|
|
</div>
|
|
{error && <div className="text-sm text-red-600">{error}</div>}
|
|
{/* Global warnings */}
|
|
{showInconsistentControlWarning && (
|
|
<div className="mt-2 rounded-md border border-yellow-300 bg-yellow-50 p-3 text-sm text-yellow-800">
|
|
Kunjungan kontrol < 30 hari dan alasan terdeteksi tidak
|
|
konsisten dengan tipe kunjungan.
|
|
{consistency.reasons.length > 0 && (
|
|
<ul className="list-disc pl-5 mt-1">
|
|
{consistency.reasons.map((r, i) => (
|
|
<li key={i}>{r}</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow-sm border overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
|
<h3 className="text-lg font-medium text-gray-900">
|
|
Rekomendasi Kode ICD
|
|
</h3>
|
|
<div className="text-xs text-gray-500">AI Based</div>
|
|
</div>
|
|
|
|
{/* Overclaim/interval banner */}
|
|
{overclaimItems.length > 0 && (
|
|
<div className="mx-6 my-4 rounded-md border border-red-300 bg-red-50 p-3 text-sm text-red-800">
|
|
Peringatan interval: Ditemukan potensi klaim dini pada{" "}
|
|
{overclaimItems.length} kode.
|
|
{daysSinceLast !== null && (
|
|
<div className="mt-1 text-xs text-red-700">
|
|
Jarak kunjungan: {daysSinceLast} hari.
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{isLoading ? (
|
|
<div className="p-10 text-center text-gray-600">
|
|
Sedang menganalisis...
|
|
</div>
|
|
) : results.length > 0 ? (
|
|
<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">
|
|
Kode ICD
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Deskripsi
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Confidence
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Alasan
|
|
</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">
|
|
{results.map((r) => (
|
|
<tr key={r.code} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center text-sm font-medium text-gray-900">
|
|
<Code className="h-4 w-4 mr-2 text-blue-600" />{" "}
|
|
{r.code}
|
|
</div>
|
|
{/* Per-item overclaim label */}
|
|
{(() => {
|
|
const rule = getIntervalRuleForCode(r.code);
|
|
const isOverclaim =
|
|
rule &&
|
|
daysSinceLast !== null &&
|
|
(daysSinceLast as number) <
|
|
(rule as { minDays: number }).minDays;
|
|
if (!isOverclaim) return null;
|
|
return (
|
|
<div className="mt-1 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">
|
|
Interval <{" "}
|
|
{String((rule as { minDays: number }).minDays)}{" "}
|
|
hari ({String((rule as { note: string }).note)})
|
|
</div>
|
|
);
|
|
})()}
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="text-sm text-gray-900">
|
|
{r.description}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 w-48">
|
|
<div className="text-sm text-gray-700 mb-1">
|
|
{r.confidence}%
|
|
</div>
|
|
<div className="w-full bg-gray-100 rounded-full h-2">
|
|
<div
|
|
className="bg-green-500 h-2 rounded-full"
|
|
style={{
|
|
width: `${Math.max(
|
|
0,
|
|
Math.min(100, r.confidence)
|
|
)}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="text-sm text-gray-600">
|
|
{r.reasoning}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
|
<button
|
|
onClick={() => copyCode(r.code)}
|
|
className="text-blue-600 hover:text-blue-900"
|
|
>
|
|
Salin Kode
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
) : (
|
|
<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">
|
|
Belum ada rekomendasi
|
|
</h3>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
Isi diagnosa/prosedur lalu klik "Buat Rekomendasi".
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* BPJS mapping and cost section */}
|
|
<div className="bg-white rounded-lg shadow-sm border overflow-hidden mt-6">
|
|
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
|
<h3 className="text-lg font-medium text-gray-900">
|
|
Rekomendasi Kode BPJS & Estimasi Biaya
|
|
</h3>
|
|
<div className="text-xs text-gray-500">Mapping dari ICD</div>
|
|
</div>
|
|
{isLoading ? (
|
|
<div className="p-10 text-center text-gray-600">
|
|
Menyiapkan mapping BPJS...
|
|
</div>
|
|
) : results.length > 0 ? (
|
|
bpjsRecommendations.length > 0 ? (
|
|
<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">
|
|
Kode ICD
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Kode BPJS
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Deskripsi
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Estimasi Tarif
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{bpjsRecommendations.map(({ icdCode, mapping }) => (
|
|
<tr
|
|
key={`${icdCode}-${mapping.bpjsCode}`}
|
|
className="hover:bg-gray-50"
|
|
>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center text-sm font-medium text-gray-900">
|
|
<Code className="h-4 w-4 mr-2 text-blue-600" />{" "}
|
|
{icdCode}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm text-gray-900">
|
|
{mapping.bpjsCode}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-gray-900">
|
|
{mapping.description}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
<div className="flex items-center">
|
|
<DollarSign className="h-4 w-4 mr-1 text-green-600" />
|
|
{formatCurrency(mapping.estTariff)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
) : (
|
|
<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">
|
|
Belum ada mapping BPJS untuk rekomendasi ICD saat ini
|
|
</h3>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
Coba perbarui diagnosa/prosedur untuk melihat rekomendasi
|
|
BPJS.
|
|
</p>
|
|
</div>
|
|
)
|
|
) : (
|
|
<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">
|
|
Belum ada rekomendasi
|
|
</h3>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
Buat rekomendasi ICD terlebih dahulu.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|