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

675
src/pages/BPJSCode.tsx Normal file
View File

@@ -0,0 +1,675 @@
import { useState } from "react";
import {
FileText,
Search,
Filter,
Download,
Eye,
Calendar,
Code,
Activity,
DollarSign,
TrendingUp,
AlertCircle,
Building2,
} from "lucide-react";
interface DiagnoseCode {
id: string;
icdCode: string;
description: string;
category: string;
severity: "ringan" | "sedang" | "berat";
bpjsRate: number;
usageCount: number;
lastUsed: string;
department: string;
}
interface ProcedureCode {
id: string;
icp9Code: string;
description: string;
category: string;
complexity: "sederhana" | "kompleks" | "sangat_kompleks";
bpjsRate: number;
duration: number; // in minutes
usageCount: number;
lastUsed: string;
department: string;
}
interface CodeUsageStats {
totalDiagnoses: number;
totalProcedures: number;
totalRevenue: number;
mostUsedDiagnose: string;
mostUsedProcedure: string;
averageClaimValue: number;
}
export default function BPJSCode() {
const [activeTab, setActiveTab] = useState<"diagnose" | "procedure">(
"diagnose"
);
const [searchTerm, setSearchTerm] = useState("");
const [categoryFilter, setCategoryFilter] = 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 getSeverityColor = (severity: string) => {
switch (severity) {
case "ringan":
return "bg-green-100 text-green-800";
case "sedang":
return "bg-yellow-100 text-yellow-800";
case "berat":
return "bg-red-100 text-red-800";
default:
return "bg-gray-100 text-gray-800";
}
};
const getComplexityColor = (complexity: string) => {
switch (complexity) {
case "sederhana":
return "bg-green-100 text-green-800";
case "kompleks":
return "bg-yellow-100 text-yellow-800";
case "sangat_kompleks":
return "bg-red-100 text-red-800";
default:
return "bg-gray-100 text-gray-800";
}
};
// Sample diagnose codes data
const [diagnoseCodes] = useState<DiagnoseCode[]>([
{
id: "1",
icdCode: "I10",
description: "Hipertensi Esensial",
category: "Penyakit Kardiovaskular",
severity: "sedang",
bpjsRate: 850000,
usageCount: 145,
lastUsed: "2024-01-15T14:30:00Z",
department: "Poli Dalam",
},
{
id: "2",
icdCode: "E11",
description: "Diabetes Mellitus Tipe 2",
category: "Penyakit Endokrin",
severity: "sedang",
bpjsRate: 1200000,
usageCount: 98,
lastUsed: "2024-01-15T10:45:00Z",
department: "Poli Endokrin",
},
{
id: "3",
icdCode: "J18.9",
description: "Pneumonia",
category: "Penyakit Pernafasan",
severity: "berat",
bpjsRate: 2500000,
usageCount: 67,
lastUsed: "2024-01-14T16:20:00Z",
department: "Poli Paru",
},
{
id: "4",
icdCode: "K29.1",
description: "Gastritis Akut",
category: "Penyakit Pencernaan",
severity: "ringan",
bpjsRate: 650000,
usageCount: 112,
lastUsed: "2024-01-13T09:15:00Z",
department: "Poli Dalam",
},
{
id: "5",
icdCode: "S52.5",
description: "Fraktur Radius",
category: "Cedera dan Keracunan",
severity: "berat",
bpjsRate: 3200000,
usageCount: 34,
lastUsed: "2024-01-12T11:30:00Z",
department: "Ortopedi",
},
]);
// Sample procedure codes data
const [procedureCodes] = useState<ProcedureCode[]>([
{
id: "1",
icp9Code: "99.04",
description: "Transfusi Darah",
category: "Prosedur Hematologi",
complexity: "sederhana",
bpjsRate: 450000,
duration: 120,
usageCount: 89,
lastUsed: "2024-01-15T13:20:00Z",
department: "IGD",
},
{
id: "2",
icp9Code: "36.10",
description: "Kateterisasi Jantung",
category: "Prosedur Kardiovaskular",
complexity: "sangat_kompleks",
bpjsRate: 8500000,
duration: 180,
usageCount: 23,
lastUsed: "2024-01-14T08:45:00Z",
department: "Kardiologi",
},
{
id: "3",
icp9Code: "79.35",
description: "Open Reduction Fraktur",
category: "Prosedur Ortopedi",
complexity: "kompleks",
bpjsRate: 5200000,
duration: 240,
usageCount: 45,
lastUsed: "2024-01-13T14:15:00Z",
department: "Ortopedi",
},
{
id: "4",
icp9Code: "45.13",
description: "Endoskopi Lambung",
category: "Prosedur Pencernaan",
complexity: "kompleks",
bpjsRate: 1800000,
duration: 45,
usageCount: 67,
lastUsed: "2024-01-12T10:30:00Z",
department: "Gastroenterologi",
},
{
id: "5",
icp9Code: "87.44",
description: "CT Scan Thorax",
category: "Prosedur Radiologi",
complexity: "sederhana",
bpjsRate: 750000,
duration: 30,
usageCount: 156,
lastUsed: "2024-01-15T16:45:00Z",
department: "Radiologi",
},
]);
// Calculate statistics
const stats: CodeUsageStats = {
totalDiagnoses: diagnoseCodes.length,
totalProcedures: procedureCodes.length,
totalRevenue:
diagnoseCodes.reduce((sum, d) => sum + d.bpjsRate * d.usageCount, 0) +
procedureCodes.reduce((sum, p) => sum + p.bpjsRate * p.usageCount, 0),
mostUsedDiagnose:
diagnoseCodes.sort((a, b) => b.usageCount - a.usageCount)[0]?.icdCode ||
"",
mostUsedProcedure:
procedureCodes.sort((a, b) => b.usageCount - a.usageCount)[0]?.icp9Code ||
"",
averageClaimValue:
(diagnoseCodes.reduce((sum, d) => sum + d.bpjsRate, 0) +
procedureCodes.reduce((sum, p) => sum + p.bpjsRate, 0)) /
(diagnoseCodes.length + procedureCodes.length),
};
// Filter data based on search and category
const filteredDiagnoseCodes = diagnoseCodes.filter((code) => {
const matchesSearch =
code.icdCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
code.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
code.category.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory =
categoryFilter === "all" || code.category === categoryFilter;
return matchesSearch && matchesCategory;
});
const filteredProcedureCodes = procedureCodes.filter((code) => {
const matchesSearch =
code.icp9Code.toLowerCase().includes(searchTerm.toLowerCase()) ||
code.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
code.category.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory =
categoryFilter === "all" || code.category === categoryFilter;
return matchesSearch && matchesCategory;
});
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">
<Code className="h-8 w-8 text-purple-600 mr-3" />
BPJS Code - Diagnosis & Procedure
</h1>
<p className="text-gray-600 mt-1">
Kelola kode diagnosis ICD-10 dan prosedur ICP-9 untuk klaim BPJS
</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 Data</span>
</button>
<button className="btn-primary flex items-center space-x-2">
<FileText className="h-4 w-4" />
<span>Tambah Code</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 Diagnosis
</p>
<p className="text-2xl font-bold text-blue-600">
{stats.totalDiagnoses}
</p>
</div>
<div className="p-3 bg-blue-100 rounded-lg">
<Activity 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">
Total Prosedur
</p>
<p className="text-2xl font-bold text-green-600">
{stats.totalProcedures}
</p>
</div>
<div className="p-3 bg-green-100 rounded-lg">
<Code 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">
Total Revenue
</p>
<p className="text-2xl font-bold text-purple-600">
{formatCurrency(stats.totalRevenue)}
</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">
Rata-rata Klaim
</p>
<p className="text-2xl font-bold text-orange-600">
{formatCurrency(stats.averageClaimValue)}
</p>
</div>
<div className="p-3 bg-orange-100 rounded-lg">
<TrendingUp className="h-6 w-6 text-orange-600" />
</div>
</div>
</div>
</div>
{/* Tabs */}
<div className="bg-white rounded-lg shadow-sm border mb-6">
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8 px-6">
<button
onClick={() => setActiveTab("diagnose")}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === "diagnose"
? "border-purple-500 text-purple-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
<div className="flex items-center space-x-2">
<Activity className="h-4 w-4" />
<span>Kode Diagnosis (ICD-10)</span>
</div>
</button>
<button
onClick={() => setActiveTab("procedure")}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === "procedure"
? "border-purple-500 text-purple-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
<div className="flex items-center space-x-2">
<Code className="h-4 w-4" />
<span>Kode Prosedur (ICP-9)</span>
</div>
</button>
</nav>
</div>
{/* Filters and Search */}
<div className="p-6 border-b border-gray-200">
<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 kode, deskripsi, atau kategori..."
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-purple-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={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="all">Semua Kategori</option>
{activeTab === "diagnose" ? (
<>
<option value="Penyakit Kardiovaskular">
Penyakit Kardiovaskular
</option>
<option value="Penyakit Endokrin">
Penyakit Endokrin
</option>
<option value="Penyakit Pernafasan">
Penyakit Pernafasan
</option>
<option value="Penyakit Pencernaan">
Penyakit Pencernaan
</option>
<option value="Cedera dan Keracunan">
Cedera dan Keracunan
</option>
</>
) : (
<>
<option value="Prosedur Hematologi">
Prosedur Hematologi
</option>
<option value="Prosedur Kardiovaskular">
Prosedur Kardiovaskular
</option>
<option value="Prosedur Ortopedi">
Prosedur Ortopedi
</option>
<option value="Prosedur Pencernaan">
Prosedur Pencernaan
</option>
<option value="Prosedur Radiologi">
Prosedur Radiologi
</option>
</>
)}
</select>
</div>
</div>
</div>
</div>
{/* Tables */}
<div className="overflow-x-auto">
{activeTab === "diagnose" ? (
<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-10
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Deskripsi & Kategori
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Departemen
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tingkat Keparahan
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tarif BPJS
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Usage
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Terakhir Digunakan
</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">
{filteredDiagnoseCodes.map((code) => (
<tr key={code.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{code.icdCode}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900">
{code.description}
</div>
<div className="text-sm text-gray-500">
{code.category}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-500">
<Building2 className="h-4 w-4 mr-1" />
{code.department}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getSeverityColor(
code.severity
)}`}
>
{code.severity.charAt(0).toUpperCase() +
code.severity.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{formatCurrency(code.bpjsRate)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{code.usageCount}x
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-500">
<Calendar className="h-4 w-4 mr-1" />
{formatDate(code.lastUsed)}
</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>
) : (
<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 ICP-9
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Deskripsi & Kategori
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Departemen
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Kompleksitas
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tarif BPJS
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Durasi
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Usage
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Terakhir Digunakan
</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">
{filteredProcedureCodes.map((code) => (
<tr key={code.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{code.icp9Code}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900">
{code.description}
</div>
<div className="text-sm text-gray-500">
{code.category}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-500">
<Building2 className="h-4 w-4 mr-1" />
{code.department}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getComplexityColor(
code.complexity
)}`}
>
{code.complexity
.replace("_", " ")
.charAt(0)
.toUpperCase() +
code.complexity.replace("_", " ").slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{formatCurrency(code.bpjsRate)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{code.duration} menit
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{code.usageCount}x
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-500">
<Calendar className="h-4 w-4 mr-1" />
{formatDate(code.lastUsed)}
</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 */}
{((activeTab === "diagnose" && filteredDiagnoseCodes.length === 0) ||
(activeTab === "procedure" &&
filteredProcedureCodes.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 kode ditemukan
</h3>
<p className="mt-1 text-sm text-gray-500">
Tidak ada kode yang sesuai dengan kriteria pencarian.
</p>
</div>
)}
</div>
</div>
);
}