diff --git a/package-lock.json b/package-lock.json
index 338de72..dfe8e82 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2212,18 +2212,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/detect-libc": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
- "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -2928,18 +2916,6 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
- "node_modules/jiti": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
- "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "bin": {
- "jiti": "lib/jiti-cli.mjs"
- }
- },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -3030,257 +3006,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/lightningcss": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
- "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "detect-libc": "^2.0.3"
- },
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "lightningcss-darwin-arm64": "1.30.1",
- "lightningcss-darwin-x64": "1.30.1",
- "lightningcss-freebsd-x64": "1.30.1",
- "lightningcss-linux-arm-gnueabihf": "1.30.1",
- "lightningcss-linux-arm64-gnu": "1.30.1",
- "lightningcss-linux-arm64-musl": "1.30.1",
- "lightningcss-linux-x64-gnu": "1.30.1",
- "lightningcss-linux-x64-musl": "1.30.1",
- "lightningcss-win32-arm64-msvc": "1.30.1",
- "lightningcss-win32-x64-msvc": "1.30.1"
- }
- },
- "node_modules/lightningcss-darwin-arm64": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
- "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-x64": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
- "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-freebsd-x64": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
- "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "peer": true,
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
- "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
- "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
- "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
- "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-musl": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
- "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
- "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
- "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
index 6cac135..1758999 100644
--- a/src/components/Layout.tsx
+++ b/src/components/Layout.tsx
@@ -57,30 +57,6 @@ export default function Layout() {
ClaimGuard Hospital Management
-
-
- {/* Notifications */}
-
-
- {/* Quick Stats */}
-
-
diff --git a/src/pages/BPJSCode.tsx b/src/pages/BPJSCode.tsx
index 0496152..3e137b6 100644
--- a/src/pages/BPJSCode.tsx
+++ b/src/pages/BPJSCode.tsx
@@ -6,17 +6,23 @@ import {
Download,
Eye,
Calendar,
+ Clock,
Code,
Activity,
DollarSign,
TrendingUp,
AlertCircle,
Building2,
+ Wand2,
+ ShieldCheck,
+ CheckCircle2,
+ ClipboardList,
} from "lucide-react";
interface DiagnoseCode {
id: string;
icdCode: string;
+ bpjsCode: string;
description: string;
category: string;
severity: "ringan" | "sedang" | "berat";
@@ -26,14 +32,16 @@ interface DiagnoseCode {
department: string;
}
-interface ProcedureCode {
+// removed legacy ICP-9 procedure model
+
+interface ICD9DiagnoseCode {
id: string;
- icp9Code: string;
+ icd9Code: string;
+ bpjsCode: string;
description: string;
category: string;
- complexity: "sederhana" | "kompleks" | "sangat_kompleks";
+ severity: "ringan" | "sedang" | "berat";
bpjsRate: number;
- duration: number; // in minutes
usageCount: number;
lastUsed: string;
department: string;
@@ -48,12 +56,44 @@ interface CodeUsageStats {
averageClaimValue: number;
}
+interface AssistInput {
+ clinicalNotes: string;
+ labResults: string;
+ procedures: string;
+}
+
+interface RecommendedCode {
+ type: "ICD10" | "ICD9CM";
+ code: string;
+ description: string;
+ confidence: number;
+ rationale: string;
+}
+
+interface InaCbgsMapping {
+ group: string;
+ code: string;
+ description: string;
+ estTariff: number;
+}
+
+interface AssistResult {
+ preprocessed: AssistInput;
+ recommendedCodes: RecommendedCode[];
+ inaCbgs: InaCbgsMapping | null;
+ references: string[];
+}
+
export default function BPJSCode() {
- const [activeTab, setActiveTab] = useState<"diagnose" | "procedure">(
+ const [activeTab, setActiveTab] = useState<"diagnose" | "procedure" | "assist">(
"diagnose"
);
const [searchTerm, setSearchTerm] = useState("");
const [categoryFilter, setCategoryFilter] = useState("all");
+ const [assistInput, setAssistInput] = useState({ clinicalNotes: "", labResults: "", procedures: "" });
+ const [assistLoading, setAssistLoading] = useState(false);
+ const [assistError, setAssistError] = useState(null);
+ const [assistResult, setAssistResult] = useState(null);
const formatDate = (dateString: string) => {
const date = new Date(dateString);
@@ -74,6 +114,92 @@ export default function BPJSCode() {
}).format(amount);
};
+ const formatDateOnly = (dateString: string) => {
+ const date = new Date(dateString);
+ return date.toLocaleDateString("id-ID", {
+ day: "numeric",
+ month: "short",
+ year: "numeric",
+ });
+ };
+
+ const formatTimeOnly = (dateString: string) => {
+ const date = new Date(dateString);
+ return date.toLocaleTimeString("id-ID", {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+ };
+
+ // Mock pipeline: preprocess -> LLM -> RAG -> INA-CBGs mapping
+ const preprocessInput = (input: AssistInput): AssistInput => {
+ const normalize = (s: string) => s.replace(/\s+/g, " ").trim();
+ return {
+ clinicalNotes: normalize(input.clinicalNotes),
+ labResults: normalize(input.labResults),
+ procedures: normalize(input.procedures),
+ };
+ };
+
+ const mockLLMRecommend = async (input: AssistInput): Promise => {
+ const text = `${input.clinicalNotes} ${input.labResults} ${input.procedures}`.toLowerCase();
+ const recs: RecommendedCode[] = [];
+ if (text.includes("hipertensi") || text.includes("bp 150/95") || text.includes("tekanan darah")) {
+ recs.push({ type: "ICD10", code: "I10", description: "Essential (primary) hypertension", confidence: 90, rationale: "Temuan tekanan darah tinggi/hipertensi pada catatan klinis." });
+ }
+ if (text.includes("hba1c") || text.includes("diabetes") || text.includes("glukosa puasa")) {
+ recs.push({ type: "ICD10", code: "E11", description: "Type 2 diabetes mellitus", confidence: 86, rationale: "Hasil lab HbA1c/glukosa dan kata kunci diabetes terdeteksi." });
+ }
+ if (text.includes("pneumonia") || text.includes("infiltrat") || text.includes("nyeri dada batuk demam")) {
+ recs.push({ type: "ICD10", code: "J18.9", description: "Pneumonia, unspecified organism", confidence: 80, rationale: "Gambaran klinis/temuan imaging mendukung pneumonia." });
+ }
+ if (text.includes("endoskopi") || text.includes("kateterisasi") || text.includes("fiksasi")) {
+ recs.push({ type: "ICD9CM", code: "45.13", description: "Endoskopi lambung (contoh)", confidence: 72, rationale: "Prosedur terekstrak dari tindakan/operasi." });
+ }
+ if (recs.length === 0) {
+ recs.push({ type: "ICD10", code: "R69", description: "Illness, unspecified", confidence: 55, rationale: "Tidak ada sinyal kuat; butuh klarifikasi klinis." });
+ }
+ await new Promise((r) => setTimeout(r, 500));
+ return recs.sort((a, b) => b.confidence - a.confidence).slice(0, 6);
+ };
+
+ const mockRAGValidate = async (recs: RecommendedCode[]): Promise<{ validated: RecommendedCode[]; references: string[] }> => {
+ // Anggap melakukan pencarian ke referensi nasional (Kemenkes/BPJS)
+ const refs: string[] = recs.map((r) => `Ref:${r.type}:${r.code}`);
+ await new Promise((r) => setTimeout(r, 300));
+ return { validated: recs, references: refs };
+ };
+
+ const mockMapInaCbgs = async (recs: RecommendedCode[]): Promise => {
+ // Mapping sederhana contoh saja
+ const hasPneumonia = recs.some((r) => r.code.startsWith("J18"));
+ const hasDM = recs.some((r) => r.code.startsWith("E11"));
+ const mapping: InaCbgsMapping | null = hasPneumonia
+ ? { group: "Respiratory", code: "E-4-13-II", description: "Pneumonia", estTariff: 3500000 }
+ : hasDM
+ ? { group: "Endocrine", code: "E-1-10-I", description: "Diabetes Mellitus", estTariff: 2100000 }
+ : null;
+ await new Promise((r) => setTimeout(r, 300));
+ return mapping;
+ };
+
+ const runAssistPipeline = async () => {
+ setAssistLoading(true);
+ setAssistError(null);
+ setAssistResult(null);
+ try {
+ const pre = preprocessInput(assistInput);
+ const llm = await mockLLMRecommend(pre);
+ const rag = await mockRAGValidate(llm);
+ const ina = await mockMapInaCbgs(rag.validated);
+ setAssistResult({ preprocessed: pre, recommendedCodes: rag.validated, inaCbgs: ina, references: rag.references });
+ } catch (e) {
+ setAssistError("Gagal menjalankan Assist Coding. Coba lagi.");
+ } finally {
+ setAssistLoading(false);
+ }
+ };
+
const getSeverityColor = (severity: string) => {
switch (severity) {
case "ringan":
@@ -87,24 +213,14 @@ export default function BPJSCode() {
}
};
- 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";
- }
- };
+ // removed legacy complexity color helper (ICP-9 procedures)
// Sample diagnose codes data
const [diagnoseCodes] = useState([
{
id: "1",
icdCode: "I10",
+ bpjsCode: "BPJS-I10-01",
description: "Hipertensi Esensial",
category: "Penyakit Kardiovaskular",
severity: "sedang",
@@ -116,6 +232,7 @@ export default function BPJSCode() {
{
id: "2",
icdCode: "E11",
+ bpjsCode: "BPJS-E11-02",
description: "Diabetes Mellitus Tipe 2",
category: "Penyakit Endokrin",
severity: "sedang",
@@ -127,6 +244,7 @@ export default function BPJSCode() {
{
id: "3",
icdCode: "J18.9",
+ bpjsCode: "BPJS-J18-03",
description: "Pneumonia",
category: "Penyakit Pernafasan",
severity: "berat",
@@ -138,6 +256,7 @@ export default function BPJSCode() {
{
id: "4",
icdCode: "K29.1",
+ bpjsCode: "BPJS-K29-04",
description: "Gastritis Akut",
category: "Penyakit Pencernaan",
severity: "ringan",
@@ -149,6 +268,7 @@ export default function BPJSCode() {
{
id: "5",
icdCode: "S52.5",
+ bpjsCode: "BPJS-S52-05",
description: "Fraktur Radius",
category: "Cedera dan Keracunan",
severity: "berat",
@@ -160,86 +280,86 @@ export default function BPJSCode() {
]);
// Sample procedure codes data
- const [procedureCodes] = useState([
+ const [icd9DiagnoseCodes] = useState([
{
id: "1",
- icp9Code: "99.04",
- description: "Transfusi Darah",
- category: "Prosedur Hematologi",
- complexity: "sederhana",
- bpjsRate: 450000,
- duration: 120,
- usageCount: 89,
+ icd9Code: "250.00",
+ bpjsCode: "BPJS-250-00",
+ description: "Diabetes mellitus tanpa komplikasi",
+ category: "Penyakit Endokrin",
+ severity: "sedang",
+ bpjsRate: 950000,
+ usageCount: 132,
lastUsed: "2024-01-15T13:20:00Z",
- department: "IGD",
+ department: "Poli Endokrin",
},
{
id: "2",
- icp9Code: "36.10",
- description: "Kateterisasi Jantung",
- category: "Prosedur Kardiovaskular",
- complexity: "sangat_kompleks",
- bpjsRate: 8500000,
- duration: 180,
- usageCount: 23,
+ icd9Code: "486",
+ bpjsCode: "BPJS-486-00",
+ description: "Pneumonia, organisme tidak spesifik",
+ category: "Penyakit Pernafasan",
+ severity: "berat",
+ bpjsRate: 2700000,
+ usageCount: 58,
lastUsed: "2024-01-14T08:45:00Z",
- department: "Kardiologi",
+ department: "Poli Paru",
},
{
id: "3",
- icp9Code: "79.35",
- description: "Open Reduction Fraktur",
- category: "Prosedur Ortopedi",
- complexity: "kompleks",
- bpjsRate: 5200000,
- duration: 240,
- usageCount: 45,
+ icd9Code: "401.9",
+ bpjsCode: "BPJS-401-90",
+ description: "Hipertensi esensial, tidak spesifik",
+ category: "Penyakit Kardiovaskular",
+ severity: "sedang",
+ bpjsRate: 780000,
+ usageCount: 120,
lastUsed: "2024-01-13T14:15:00Z",
- department: "Ortopedi",
+ department: "Poli Dalam",
},
{
id: "4",
- icp9Code: "45.13",
- description: "Endoskopi Lambung",
- category: "Prosedur Pencernaan",
- complexity: "kompleks",
- bpjsRate: 1800000,
- duration: 45,
- usageCount: 67,
+ icd9Code: "530.81",
+ bpjsCode: "BPJS-530-81",
+ description: "Penyakit refluks gastroesofageal",
+ category: "Penyakit Pencernaan",
+ severity: "ringan",
+ bpjsRate: 620000,
+ usageCount: 93,
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,
+ icd9Code: "813.42",
+ bpjsCode: "BPJS-813-42",
+ description: "Fraktur tertutup radius distal",
+ category: "Cedera dan Keracunan",
+ severity: "berat",
+ bpjsRate: 3400000,
+ usageCount: 27,
lastUsed: "2024-01-15T16:45:00Z",
- department: "Radiologi",
+ department: "Ortopedi",
},
]);
// Calculate statistics
const stats: CodeUsageStats = {
totalDiagnoses: diagnoseCodes.length,
- totalProcedures: procedureCodes.length,
+ totalProcedures: icd9DiagnoseCodes.length,
totalRevenue:
diagnoseCodes.reduce((sum, d) => sum + d.bpjsRate * d.usageCount, 0) +
- procedureCodes.reduce((sum, p) => sum + p.bpjsRate * p.usageCount, 0),
+ icd9DiagnoseCodes.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 ||
+ icd9DiagnoseCodes.sort((a, b) => b.usageCount - a.usageCount)[0]?.icd9Code ||
"",
averageClaimValue:
(diagnoseCodes.reduce((sum, d) => sum + d.bpjsRate, 0) +
- procedureCodes.reduce((sum, p) => sum + p.bpjsRate, 0)) /
- (diagnoseCodes.length + procedureCodes.length),
+ icd9DiagnoseCodes.reduce((sum, p) => sum + p.bpjsRate, 0)) /
+ (diagnoseCodes.length + icd9DiagnoseCodes.length),
};
// Filter data based on search and category
@@ -255,9 +375,9 @@ export default function BPJSCode() {
return matchesSearch && matchesCategory;
});
- const filteredProcedureCodes = procedureCodes.filter((code) => {
+ const filteredICD9DiagnoseCodes = icd9DiagnoseCodes.filter((code) => {
const matchesSearch =
- code.icp9Code.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ code.icd9Code.toLowerCase().includes(searchTerm.toLowerCase()) ||
code.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
code.category.toLowerCase().includes(searchTerm.toLowerCase());
@@ -389,78 +509,123 @@ export default function BPJSCode() {
>
- Kode Prosedur (ICP-9)
+ Kode Diagnosis (ICD-9)
+
+
+
- {/* Filters and Search */}
-
-
-
-
-
- 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"
- />
-
-
-
-
+ {/* Filters and Search or Assist Input */}
+ {activeTab !== "assist" ? (
+
+
+
+
+
+ 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"
+ />
+
+
+
+
+
-
+ ) : (
+
+
+
+
+
+
+
+
+
+
+ setAssistInput((s) => ({ ...s, procedures: e.target.value }))}
+ placeholder="Endoskopi lambung, kateterisasi jantung, transfusi, ..."
+ className="w-full rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent p-3"
+ />
+
+
+
+
+ {assistError && (
+
{assistError}
+ )}
+
+
+ )}
- {/* Tables */}
+ {/* Tables / Assist Results */}
{activeTab === "diagnose" ? (
@@ -469,6 +634,9 @@ export default function BPJSCode() {
|
Kode ICD-10
|
+
+ Kode BPJS
+ |
Deskripsi & Kategori
|
@@ -500,6 +668,11 @@ export default function BPJSCode() {
{code.icdCode}
+
+
+ {code.bpjsCode}
+
+ |
@@ -533,9 +706,15 @@ export default function BPJSCode() {
{code.usageCount}x
|
-
-
- {formatDate(code.lastUsed)}
+
+
+
+ {formatDateOnly(code.lastUsed)}
+
+
+
+ {formatTimeOnly(code.lastUsed)}
+
|
@@ -552,13 +731,12 @@ export default function BPJSCode() {
))}
|
- ) : (
+ ) : activeTab === "procedure" ? (
- |
- Kode ICP-9
- |
+ Kode ICD-9 |
+ Kode BPJS |
Deskripsi & Kategori
|
@@ -566,14 +744,11 @@ export default function BPJSCode() {
Departemen
- Kompleksitas
+ Tingkat Keparahan
|
Tarif BPJS
|
-
- Durasi
- |
Usage
|
@@ -586,11 +761,20 @@ export default function BPJSCode() {
- {filteredProcedureCodes.map((code) => (
+ {filteredICD9DiagnoseCodes
+ .filter((code) => {
+ return true;
+ })
+ .map((code) => (
|
- {code.icp9Code}
+ {code.icd9Code}
+
+ |
+
+
+ {code.bpjsCode}
|
@@ -610,31 +794,26 @@ export default function BPJSCode() {
|
-
- {code.complexity
- .replace("_", " ")
- .charAt(0)
- .toUpperCase() +
- code.complexity.replace("_", " ").slice(1)}
-
+
+ {code.severity.charAt(0).toUpperCase() + code.severity.slice(1)}
+
|
{formatCurrency(code.bpjsRate)}
|
-
- {code.duration} menit
- |
{code.usageCount}x
|
-
-
- {formatDate(code.lastUsed)}
+
+
+
+ {formatDateOnly(code.lastUsed)}
+
+
+
+ {formatTimeOnly(code.lastUsed)}
+
|
@@ -651,14 +830,109 @@ export default function BPJSCode() {
))}
|
+ ) : (
+
+ {!assistResult ? (
+
+
+
Belum ada hasil
+
Isi input lalu klik "Jalankan Assist Coding".
+
+ ) : (
+
+
+
Hasil Preprocess
+
+
+
Catatan Medis
+
{assistResult.preprocessed.clinicalNotes || '-'}
+
+
+
Hasil Lab
+
{assistResult.preprocessed.labResults || '-'}
+
+
+
Tindakan
+
{assistResult.preprocessed.procedures || '-'}
+
+
+
+
+
+
+ Rekomendasi Kode & Alasan (LLM + RAG)
+
+
+
+
+ | Jenis |
+ Kode |
+ Deskripsi |
+ Confidence |
+ Alasan |
+
+
+
+ {assistResult.recommendedCodes.map((c) => (
+
+ | {c.type} |
+ {c.code} |
+ {c.description} |
+
+ {c.confidence}%
+
+ |
+ {c.rationale} |
+
+ ))}
+
+
+
Referensi: {assistResult.references.join(", ") || '-'}
+
+
+
+
Mapping INA-CBGs
+ {assistResult.inaCbgs ? (
+
+
Kode: {assistResult.inaCbgs.code} ({assistResult.inaCbgs.group})
+
Deskripsi: {assistResult.inaCbgs.description}
+
Estimasi Tarif: {formatCurrency(assistResult.inaCbgs.estTariff)}
+
+ ) : (
+
Belum ada mapping yang relevan.
+ )}
+
+
+
+
+
+
+
+ )}
+
)}
- {/* Empty State */}
- {((activeTab === "diagnose" && filteredDiagnoseCodes.length === 0) ||
- (activeTab === "procedure" &&
- filteredProcedureCodes.length === 0)) && (
+ {/* Empty State */}
+ {((activeTab === "diagnose" && filteredDiagnoseCodes.length === 0) ||
+ (activeTab === "procedure" && filteredICD9DiagnoseCodes.length === 0)) && (
diff --git a/src/pages/CostRecommendation.tsx b/src/pages/CostRecommendation.tsx
index dd3645d..a292c9f 100644
--- a/src/pages/CostRecommendation.tsx
+++ b/src/pages/CostRecommendation.tsx
@@ -61,7 +61,7 @@ interface CostStats {
approvalRate: number;
}
-export default function CostRecommendation() {
+export function LegacyCostRecommendation() {
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [riskFilter, setRiskFilter] = useState("all");
@@ -679,3 +679,484 @@ export default function CostRecommendation() {
);
}
+
+interface ICDRecommendation {
+ code: string;
+ description: string;
+ confidence: number;
+ reasoning: string;
+}
+
+async function mockRecommendICD(diagnosis: string, procedure: string): Promise
{
+ 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(null);
+ const [results, setResults] = useState([]);
+ const [visitType, setVisitType] = useState("Kontrol Rutin");
+ const [lastVisitDate, setLastVisitDate] = useState("");
+ const [currentVisitDate, setCurrentVisitDate] = useState(() => new Date().toISOString().slice(0, 10));
+
+ 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: string, b: string) => {
+ try {
+ const da = new Date(a + "T00:00:00");
+ const db = new Date(b + "T00:00:00");
+ const diff = Math.floor((db.getTime() - da.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 = {
+ "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 (e) {
+ 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 {}
+ };
+
+ return (
+
+
+
+
Asisten Rekomendasi ICD untuk Dokter
+
+ Masukkan ringkasan diagnosa dan prosedur. Sistem akan mengusulkan kode ICD yang relevan beserta tingkat keyakinan.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setLastVisitDate(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"
+ />
+
+
+
+ setCurrentVisitDate(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"
+ />
+
+
+
+
+ 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"
+ />
+
+
+
+
+
+ {error && (
+
{error}
+ )}
+ {/* Global warnings */}
+ {showInconsistentControlWarning && (
+
+ Kunjungan kontrol < 30 hari dan alasan terdeteksi tidak konsisten dengan tipe kunjungan.
+ {consistency.reasons.length > 0 && (
+
+ {consistency.reasons.map((r, i) => (
+ - {r}
+ ))}
+
+ )}
+
+ )}
+
+
+
+
+
+
Rekomendasi Kode ICD
+
AI Based
+
+
+ {/* Overclaim/interval banner */}
+ {overclaimItems.length > 0 && (
+
+ Peringatan interval: Ditemukan potensi klaim dini pada {overclaimItems.length} kode.
+ {daysSinceLast !== null && (
+
Jarak kunjungan: {daysSinceLast} hari.
+ )}
+
+ )}
+
+ {isLoading ? (
+
Sedang menganalisis...
+ ) : results.length > 0 ? (
+
+
+
+
+ | Kode ICD |
+ Deskripsi |
+ Confidence |
+ Alasan |
+ Aksi |
+
+
+
+ {results.map((r) => (
+
+ |
+
+ {r.code}
+
+ {/* 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 (
+
+ Interval < {String((rule as { minDays: number }).minDays)} hari ({String((rule as { note: string }).note)})
+
+ );
+ })()}
+ |
+
+ {r.description}
+ |
+
+ {r.confidence}%
+
+ |
+
+ {r.reasoning}
+ |
+
+
+ |
+
+ ))}
+
+
+
+ ) : (
+
+
+
Belum ada rekomendasi
+
Isi diagnosa/prosedur lalu klik "Buat Rekomendasi".
+
+ )}
+
+
+ {/* BPJS mapping and cost section */}
+
+
+
Rekomendasi Kode BPJS & Estimasi Biaya
+
Mapping dari ICD
+
+ {isLoading ? (
+
Menyiapkan mapping BPJS...
+ ) : results.length > 0 ? (
+ bpjsRecommendations.length > 0 ? (
+
+
+
+
+ | Kode ICD |
+ Kode BPJS |
+ Deskripsi |
+ Estimasi Tarif |
+
+
+
+ {bpjsRecommendations.map(({ icdCode, mapping }) => (
+
+ |
+
+ {icdCode}
+
+ |
+
+ {mapping.bpjsCode}
+ |
+ {mapping.description} |
+
+
+
+ {formatCurrency(mapping.estTariff)}
+
+ |
+
+ ))}
+
+
+
+ ) : (
+
+
+
Belum ada mapping BPJS untuk rekomendasi ICD saat ini
+
Coba perbarui diagnosa/prosedur untuk melihat rekomendasi BPJS.
+
+ )
+ ) : (
+
+
+
Belum ada rekomendasi
+
Buat rekomendasi ICD terlebih dahulu.
+
+ )}
+
+
+
+ );
+}