Fix: remove some and add some

This commit is contained in:
Friday6661
2025-08-13 13:34:37 +07:00
parent 2e396f32b9
commit 5d9195d903
4 changed files with 917 additions and 461 deletions

275
package-lock.json generated
View File

@@ -2212,18 +2212,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/didyoumean": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -2928,18 +2916,6 @@
"@pkgjs/parseargs": "^0.11.0" "@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": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -3030,257 +3006,6 @@
"node": ">= 0.8.0" "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": { "node_modules/lilconfig": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",

View File

@@ -57,30 +57,6 @@ export default function Layout() {
ClaimGuard Hospital Management ClaimGuard Hospital Management
</div> </div>
</div> </div>
<div className="flex items-center space-x-4">
{/* Notifications */}
<button className="relative p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg">
<Bell className="h-5 w-5" />
<span className="absolute top-1 right-1 h-2 w-2 bg-red-500 rounded-full"></span>
</button>
{/* Quick Stats */}
<div className="hidden md:flex items-center space-x-4 pl-4 border-l border-gray-200">
<div className="text-center">
<p className="text-sm font-semibold text-gray-900">24</p>
<p className="text-xs text-gray-500">Pasien Hari Ini</p>
</div>
<div className="text-center">
<p className="text-sm font-semibold text-green-600">12</p>
<p className="text-xs text-gray-500">Klaim Aktif</p>
</div>
<div className="text-center">
<p className="text-sm font-semibold text-orange-600">3</p>
<p className="text-xs text-gray-500">Alert</p>
</div>
</div>
</div>
</div> </div>
</header> </header>

View File

@@ -6,17 +6,23 @@ import {
Download, Download,
Eye, Eye,
Calendar, Calendar,
Clock,
Code, Code,
Activity, Activity,
DollarSign, DollarSign,
TrendingUp, TrendingUp,
AlertCircle, AlertCircle,
Building2, Building2,
Wand2,
ShieldCheck,
CheckCircle2,
ClipboardList,
} from "lucide-react"; } from "lucide-react";
interface DiagnoseCode { interface DiagnoseCode {
id: string; id: string;
icdCode: string; icdCode: string;
bpjsCode: string;
description: string; description: string;
category: string; category: string;
severity: "ringan" | "sedang" | "berat"; severity: "ringan" | "sedang" | "berat";
@@ -26,14 +32,16 @@ interface DiagnoseCode {
department: string; department: string;
} }
interface ProcedureCode { // removed legacy ICP-9 procedure model
interface ICD9DiagnoseCode {
id: string; id: string;
icp9Code: string; icd9Code: string;
bpjsCode: string;
description: string; description: string;
category: string; category: string;
complexity: "sederhana" | "kompleks" | "sangat_kompleks"; severity: "ringan" | "sedang" | "berat";
bpjsRate: number; bpjsRate: number;
duration: number; // in minutes
usageCount: number; usageCount: number;
lastUsed: string; lastUsed: string;
department: string; department: string;
@@ -48,12 +56,44 @@ interface CodeUsageStats {
averageClaimValue: number; 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() { export default function BPJSCode() {
const [activeTab, setActiveTab] = useState<"diagnose" | "procedure">( const [activeTab, setActiveTab] = useState<"diagnose" | "procedure" | "assist">(
"diagnose" "diagnose"
); );
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [categoryFilter, setCategoryFilter] = useState("all"); const [categoryFilter, setCategoryFilter] = useState("all");
const [assistInput, setAssistInput] = useState<AssistInput>({ clinicalNotes: "", labResults: "", procedures: "" });
const [assistLoading, setAssistLoading] = useState(false);
const [assistError, setAssistError] = useState<string | null>(null);
const [assistResult, setAssistResult] = useState<AssistResult | null>(null);
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
const date = new Date(dateString); const date = new Date(dateString);
@@ -74,6 +114,92 @@ export default function BPJSCode() {
}).format(amount); }).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<RecommendedCode[]> => {
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<InaCbgsMapping | null> => {
// 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) => { const getSeverityColor = (severity: string) => {
switch (severity) { switch (severity) {
case "ringan": case "ringan":
@@ -87,24 +213,14 @@ export default function BPJSCode() {
} }
}; };
const getComplexityColor = (complexity: string) => { // removed legacy complexity color helper (ICP-9 procedures)
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 // Sample diagnose codes data
const [diagnoseCodes] = useState<DiagnoseCode[]>([ const [diagnoseCodes] = useState<DiagnoseCode[]>([
{ {
id: "1", id: "1",
icdCode: "I10", icdCode: "I10",
bpjsCode: "BPJS-I10-01",
description: "Hipertensi Esensial", description: "Hipertensi Esensial",
category: "Penyakit Kardiovaskular", category: "Penyakit Kardiovaskular",
severity: "sedang", severity: "sedang",
@@ -116,6 +232,7 @@ export default function BPJSCode() {
{ {
id: "2", id: "2",
icdCode: "E11", icdCode: "E11",
bpjsCode: "BPJS-E11-02",
description: "Diabetes Mellitus Tipe 2", description: "Diabetes Mellitus Tipe 2",
category: "Penyakit Endokrin", category: "Penyakit Endokrin",
severity: "sedang", severity: "sedang",
@@ -127,6 +244,7 @@ export default function BPJSCode() {
{ {
id: "3", id: "3",
icdCode: "J18.9", icdCode: "J18.9",
bpjsCode: "BPJS-J18-03",
description: "Pneumonia", description: "Pneumonia",
category: "Penyakit Pernafasan", category: "Penyakit Pernafasan",
severity: "berat", severity: "berat",
@@ -138,6 +256,7 @@ export default function BPJSCode() {
{ {
id: "4", id: "4",
icdCode: "K29.1", icdCode: "K29.1",
bpjsCode: "BPJS-K29-04",
description: "Gastritis Akut", description: "Gastritis Akut",
category: "Penyakit Pencernaan", category: "Penyakit Pencernaan",
severity: "ringan", severity: "ringan",
@@ -149,6 +268,7 @@ export default function BPJSCode() {
{ {
id: "5", id: "5",
icdCode: "S52.5", icdCode: "S52.5",
bpjsCode: "BPJS-S52-05",
description: "Fraktur Radius", description: "Fraktur Radius",
category: "Cedera dan Keracunan", category: "Cedera dan Keracunan",
severity: "berat", severity: "berat",
@@ -160,86 +280,86 @@ export default function BPJSCode() {
]); ]);
// Sample procedure codes data // Sample procedure codes data
const [procedureCodes] = useState<ProcedureCode[]>([ const [icd9DiagnoseCodes] = useState<ICD9DiagnoseCode[]>([
{ {
id: "1", id: "1",
icp9Code: "99.04", icd9Code: "250.00",
description: "Transfusi Darah", bpjsCode: "BPJS-250-00",
category: "Prosedur Hematologi", description: "Diabetes mellitus tanpa komplikasi",
complexity: "sederhana", category: "Penyakit Endokrin",
bpjsRate: 450000, severity: "sedang",
duration: 120, bpjsRate: 950000,
usageCount: 89, usageCount: 132,
lastUsed: "2024-01-15T13:20:00Z", lastUsed: "2024-01-15T13:20:00Z",
department: "IGD", department: "Poli Endokrin",
}, },
{ {
id: "2", id: "2",
icp9Code: "36.10", icd9Code: "486",
description: "Kateterisasi Jantung", bpjsCode: "BPJS-486-00",
category: "Prosedur Kardiovaskular", description: "Pneumonia, organisme tidak spesifik",
complexity: "sangat_kompleks", category: "Penyakit Pernafasan",
bpjsRate: 8500000, severity: "berat",
duration: 180, bpjsRate: 2700000,
usageCount: 23, usageCount: 58,
lastUsed: "2024-01-14T08:45:00Z", lastUsed: "2024-01-14T08:45:00Z",
department: "Kardiologi", department: "Poli Paru",
}, },
{ {
id: "3", id: "3",
icp9Code: "79.35", icd9Code: "401.9",
description: "Open Reduction Fraktur", bpjsCode: "BPJS-401-90",
category: "Prosedur Ortopedi", description: "Hipertensi esensial, tidak spesifik",
complexity: "kompleks", category: "Penyakit Kardiovaskular",
bpjsRate: 5200000, severity: "sedang",
duration: 240, bpjsRate: 780000,
usageCount: 45, usageCount: 120,
lastUsed: "2024-01-13T14:15:00Z", lastUsed: "2024-01-13T14:15:00Z",
department: "Ortopedi", department: "Poli Dalam",
}, },
{ {
id: "4", id: "4",
icp9Code: "45.13", icd9Code: "530.81",
description: "Endoskopi Lambung", bpjsCode: "BPJS-530-81",
category: "Prosedur Pencernaan", description: "Penyakit refluks gastroesofageal",
complexity: "kompleks", category: "Penyakit Pencernaan",
bpjsRate: 1800000, severity: "ringan",
duration: 45, bpjsRate: 620000,
usageCount: 67, usageCount: 93,
lastUsed: "2024-01-12T10:30:00Z", lastUsed: "2024-01-12T10:30:00Z",
department: "Gastroenterologi", department: "Gastroenterologi",
}, },
{ {
id: "5", id: "5",
icp9Code: "87.44", icd9Code: "813.42",
description: "CT Scan Thorax", bpjsCode: "BPJS-813-42",
category: "Prosedur Radiologi", description: "Fraktur tertutup radius distal",
complexity: "sederhana", category: "Cedera dan Keracunan",
bpjsRate: 750000, severity: "berat",
duration: 30, bpjsRate: 3400000,
usageCount: 156, usageCount: 27,
lastUsed: "2024-01-15T16:45:00Z", lastUsed: "2024-01-15T16:45:00Z",
department: "Radiologi", department: "Ortopedi",
}, },
]); ]);
// Calculate statistics // Calculate statistics
const stats: CodeUsageStats = { const stats: CodeUsageStats = {
totalDiagnoses: diagnoseCodes.length, totalDiagnoses: diagnoseCodes.length,
totalProcedures: procedureCodes.length, totalProcedures: icd9DiagnoseCodes.length,
totalRevenue: totalRevenue:
diagnoseCodes.reduce((sum, d) => sum + d.bpjsRate * d.usageCount, 0) + 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: mostUsedDiagnose:
diagnoseCodes.sort((a, b) => b.usageCount - a.usageCount)[0]?.icdCode || diagnoseCodes.sort((a, b) => b.usageCount - a.usageCount)[0]?.icdCode ||
"", "",
mostUsedProcedure: mostUsedProcedure:
procedureCodes.sort((a, b) => b.usageCount - a.usageCount)[0]?.icp9Code || icd9DiagnoseCodes.sort((a, b) => b.usageCount - a.usageCount)[0]?.icd9Code ||
"", "",
averageClaimValue: averageClaimValue:
(diagnoseCodes.reduce((sum, d) => sum + d.bpjsRate, 0) + (diagnoseCodes.reduce((sum, d) => sum + d.bpjsRate, 0) +
procedureCodes.reduce((sum, p) => sum + p.bpjsRate, 0)) / icd9DiagnoseCodes.reduce((sum, p) => sum + p.bpjsRate, 0)) /
(diagnoseCodes.length + procedureCodes.length), (diagnoseCodes.length + icd9DiagnoseCodes.length),
}; };
// Filter data based on search and category // Filter data based on search and category
@@ -255,9 +375,9 @@ export default function BPJSCode() {
return matchesSearch && matchesCategory; return matchesSearch && matchesCategory;
}); });
const filteredProcedureCodes = procedureCodes.filter((code) => { const filteredICD9DiagnoseCodes = icd9DiagnoseCodes.filter((code) => {
const matchesSearch = const matchesSearch =
code.icp9Code.toLowerCase().includes(searchTerm.toLowerCase()) || code.icd9Code.toLowerCase().includes(searchTerm.toLowerCase()) ||
code.description.toLowerCase().includes(searchTerm.toLowerCase()) || code.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
code.category.toLowerCase().includes(searchTerm.toLowerCase()); code.category.toLowerCase().includes(searchTerm.toLowerCase());
@@ -389,13 +509,27 @@ export default function BPJSCode() {
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Code className="h-4 w-4" /> <Code className="h-4 w-4" />
<span>Kode Prosedur (ICP-9)</span> <span>Kode Diagnosis (ICD-9)</span>
</div>
</button>
<button
onClick={() => setActiveTab("assist")}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === "assist"
? "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">
<Wand2 className="h-4 w-4" />
<span>Assist Coding</span>
</div> </div>
</button> </button>
</nav> </nav>
</div> </div>
{/* Filters and Search */} {/* Filters and Search or Assist Input */}
{activeTab !== "assist" ? (
<div className="p-6 border-b border-gray-200"> <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 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="flex items-center space-x-4">
@@ -419,48 +553,79 @@ export default function BPJSCode() {
<option value="all">Semua Kategori</option> <option value="all">Semua Kategori</option>
{activeTab === "diagnose" ? ( {activeTab === "diagnose" ? (
<> <>
<option value="Penyakit Kardiovaskular"> <option value="Penyakit Kardiovaskular">Penyakit Kardiovaskular</option>
Penyakit Kardiovaskular <option value="Penyakit Endokrin">Penyakit Endokrin</option>
</option> <option value="Penyakit Pernafasan">Penyakit Pernafasan</option>
<option value="Penyakit Endokrin"> <option value="Penyakit Pencernaan">Penyakit Pencernaan</option>
Penyakit Endokrin <option value="Cedera dan Keracunan">Cedera dan Keracunan</option>
</option>
<option value="Penyakit Pernafasan">
Penyakit Pernafasan
</option>
<option value="Penyakit Pencernaan">
Penyakit Pencernaan
</option>
<option value="Cedera dan Keracunan">
Cedera dan Keracunan
</option>
</> </>
) : ( ) : activeTab === "procedure" ? (
<> <>
<option value="Prosedur Hematologi"> <option value="Penyakit Kardiovaskular">Penyakit Kardiovaskular</option>
Prosedur Hematologi <option value="Penyakit Endokrin">Penyakit Endokrin</option>
</option> <option value="Penyakit Pernafasan">Penyakit Pernafasan</option>
<option value="Prosedur Kardiovaskular"> <option value="Penyakit Pencernaan">Penyakit Pencernaan</option>
Prosedur Kardiovaskular <option value="Cedera dan Keracunan">Cedera dan Keracunan</option>
</option>
<option value="Prosedur Ortopedi">
Prosedur Ortopedi
</option>
<option value="Prosedur Pencernaan">
Prosedur Pencernaan
</option>
<option value="Prosedur Radiologi">
Prosedur Radiologi
</option>
</> </>
)} ) : null}
</select> </select>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
) : (
<div className="p-6 border-b border-gray-200">
<div className="grid grid-cols-1 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 flex items-center">
<ClipboardList className="h-4 w-4 mr-2 text-purple-600" /> Catatan Medis
</label>
<textarea
rows={4}
value={assistInput.clinicalNotes}
onChange={(e) => setAssistInput((s) => ({ ...s, clinicalNotes: e.target.value }))}
placeholder="Riwayat penyakit, gejala, temuan fisik..."
className="w-full rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent p-3"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Hasil Lab</label>
<textarea
rows={3}
value={assistInput.labResults}
onChange={(e) => setAssistInput((s) => ({ ...s, labResults: e.target.value }))}
placeholder="HbA1c 8.2%, CRP 15 mg/L, Leukosit 12.000/µL, ..."
className="w-full rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent p-3"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Tindakan/Prosedur</label>
<input
type="text"
value={assistInput.procedures}
onChange={(e) => 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"
/>
</div>
<div className="flex items-center space-x-3">
<button
onClick={runAssistPipeline}
disabled={assistLoading || (!assistInput.clinicalNotes && !assistInput.labResults && !assistInput.procedures)}
className="btn-primary flex items-center space-x-2 disabled:opacity-60"
>
<Wand2 className="h-4 w-4" />
<span>{assistLoading ? "Memproses..." : "Jalankan Assist Coding"}</span>
</button>
</div>
{assistError && (
<div className="text-sm text-red-600">{assistError}</div>
)}
</div>
</div>
)}
{/* Tables */} {/* Tables / Assist Results */}
<div className="overflow-x-auto"> <div className="overflow-x-auto">
{activeTab === "diagnose" ? ( {activeTab === "diagnose" ? (
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-gray-200">
@@ -469,6 +634,9 @@ export default function BPJSCode() {
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Kode ICD-10 Kode ICD-10
</th> </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"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Deskripsi & Kategori Deskripsi & Kategori
</th> </th>
@@ -500,6 +668,11 @@ export default function BPJSCode() {
{code.icdCode} {code.icdCode}
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{code.bpjsCode}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div> <div>
<div className="text-sm font-medium text-gray-900"> <div className="text-sm font-medium text-gray-900">
@@ -533,9 +706,15 @@ export default function BPJSCode() {
{code.usageCount}x {code.usageCount}x
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-500"> <div className="flex flex-col space-y-1 text-sm text-gray-500">
<div className="flex items-center">
<Calendar className="h-4 w-4 mr-1" /> <Calendar className="h-4 w-4 mr-1" />
{formatDate(code.lastUsed)} {formatDateOnly(code.lastUsed)}
</div>
<div className="flex items-center">
<Clock className="h-4 w-4 mr-1" />
{formatTimeOnly(code.lastUsed)}
</div>
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium"> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
@@ -552,13 +731,12 @@ export default function BPJSCode() {
))} ))}
</tbody> </tbody>
</table> </table>
) : ( ) : activeTab === "procedure" ? (
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Kode ICD-9</th>
Kode ICP-9 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Kode BPJS</th>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Deskripsi & Kategori Deskripsi & Kategori
</th> </th>
@@ -566,14 +744,11 @@ export default function BPJSCode() {
Departemen Departemen
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Kompleksitas Tingkat Keparahan
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tarif BPJS Tarif BPJS
</th> </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"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Usage Usage
</th> </th>
@@ -586,11 +761,20 @@ export default function BPJSCode() {
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{filteredProcedureCodes.map((code) => ( {filteredICD9DiagnoseCodes
.filter((code) => {
return true;
})
.map((code) => (
<tr key={code.id} className="hover:bg-gray-50"> <tr key={code.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900"> <div className="text-sm font-medium text-gray-900">
{code.icp9Code} {code.icd9Code}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{code.bpjsCode}
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
@@ -610,31 +794,26 @@ export default function BPJSCode() {
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<span <span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getSeverityColor(code.severity)}`}>
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getComplexityColor( {code.severity.charAt(0).toUpperCase() + code.severity.slice(1)}
code.complexity
)}`}
>
{code.complexity
.replace("_", " ")
.charAt(0)
.toUpperCase() +
code.complexity.replace("_", " ").slice(1)}
</span> </span>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{formatCurrency(code.bpjsRate)} {formatCurrency(code.bpjsRate)}
</td> </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"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{code.usageCount}x {code.usageCount}x
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-500"> <div className="flex flex-col space-y-1 text-sm text-gray-500">
<div className="flex items-center">
<Calendar className="h-4 w-4 mr-1" /> <Calendar className="h-4 w-4 mr-1" />
{formatDate(code.lastUsed)} {formatDateOnly(code.lastUsed)}
</div>
<div className="flex items-center">
<Clock className="h-4 w-4 mr-1" />
{formatTimeOnly(code.lastUsed)}
</div>
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium"> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
@@ -651,14 +830,109 @@ export default function BPJSCode() {
))} ))}
</tbody> </tbody>
</table> </table>
) : (
<div>
{!assistResult ? (
<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 hasil</h3>
<p className="mt-1 text-sm text-gray-500">Isi input lalu klik "Jalankan Assist Coding".</p>
</div>
) : (
<div className="p-6 space-y-6">
<div className="bg-white border rounded-lg p-4">
<div className="text-sm font-medium text-gray-900 mb-2">Hasil Preprocess</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm text-gray-700">
<div>
<div className="font-medium">Catatan Medis</div>
<div className="text-gray-600 whitespace-pre-wrap">{assistResult.preprocessed.clinicalNotes || '-'}</div>
</div>
<div>
<div className="font-medium">Hasil Lab</div>
<div className="text-gray-600 whitespace-pre-wrap">{assistResult.preprocessed.labResults || '-'}</div>
</div>
<div>
<div className="font-medium">Tindakan</div>
<div className="text-gray-600 whitespace-pre-wrap">{assistResult.preprocessed.procedures || '-'}</div>
</div>
</div>
</div>
<div className="bg-white border rounded-lg p-4">
<div className="text-sm font-medium text-gray-900 mb-3 flex items-center">
<ShieldCheck className="h-4 w-4 mr-2 text-green-600" /> Rekomendasi Kode & Alasan (LLM + RAG)
</div>
<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">Jenis</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Kode</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>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{assistResult.recommendedCodes.map((c) => (
<tr key={`${c.type}-${c.code}`} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">{c.type}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{c.code}</td>
<td className="px-6 py-4 text-sm text-gray-900">{c.description}</td>
<td className="px-6 py-4 w-48">
<div className="text-sm text-gray-700 mb-1">{c.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, c.confidence))}%` }} />
</div>
</td>
<td className="px-6 py-4 text-sm text-gray-600">{c.rationale}</td>
</tr>
))}
</tbody>
</table>
<div className="text-xs text-gray-500 mt-2">Referensi: {assistResult.references.join(", ") || '-'}</div>
</div>
<div className="bg-white border rounded-lg p-4">
<div className="text-sm font-medium text-gray-900 mb-2">Mapping INA-CBGs</div>
{assistResult.inaCbgs ? (
<div className="text-sm text-gray-700">
<div>Kode: <span className="font-medium">{assistResult.inaCbgs.code}</span> ({assistResult.inaCbgs.group})</div>
<div>Deskripsi: {assistResult.inaCbgs.description}</div>
<div>Estimasi Tarif: <span className="font-medium">{formatCurrency(assistResult.inaCbgs.estTariff)}</span></div>
</div>
) : (
<div className="text-sm text-gray-500">Belum ada mapping yang relevan.</div>
)}
</div>
<div className="flex items-center space-x-3">
<button className="btn-primary flex items-center space-x-2">
<CheckCircle2 className="h-4 w-4" />
<span>Verifikasi & Approve</span>
</button>
<button className="btn-secondary flex items-center space-x-2" onClick={() => {
const blob = new Blob([JSON.stringify(assistResult, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'klaim_siap_kirim.json';
a.click();
URL.revokeObjectURL(url);
}}>
<Download className="h-4 w-4" />
<span>Export Klaim</span>
</button>
</div>
</div>
)}
</div>
)} )}
</div> </div>
</div> </div>
{/* Empty State */} {/* Empty State */}
{((activeTab === "diagnose" && filteredDiagnoseCodes.length === 0) || {((activeTab === "diagnose" && filteredDiagnoseCodes.length === 0) ||
(activeTab === "procedure" && (activeTab === "procedure" && filteredICD9DiagnoseCodes.length === 0)) && (
filteredProcedureCodes.length === 0)) && (
<div className="text-center py-12"> <div className="text-center py-12">
<AlertCircle className="mx-auto h-12 w-12 text-gray-400" /> <AlertCircle className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900"> <h3 className="mt-2 text-sm font-medium text-gray-900">

View File

@@ -61,7 +61,7 @@ interface CostStats {
approvalRate: number; approvalRate: number;
} }
export default function CostRecommendation() { export function LegacyCostRecommendation() {
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all");
const [riskFilter, setRiskFilter] = useState("all"); const [riskFilter, setRiskFilter] = useState("all");
@@ -679,3 +679,484 @@ export default function CostRecommendation() {
</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("");
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<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 (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 (
<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="block 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</label>
<input
type="date"
value={lastVisitDate}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Tanggal Kunjungan Saat Ini</label>
<input
type="date"
value={currentVisitDate}
onChange={(e) => 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"
/>
</div>
</div>
<div>
<label className="block 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)}
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 &lt; 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 &lt; {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>
);
}