partial update revision simplify feature

This commit is contained in:
2025-08-13 17:51:30 +07:00
parent 5d9195d903
commit 39af7d1692
12 changed files with 1209 additions and 1734 deletions

View File

@@ -6,8 +6,8 @@ import {
} from "react-router-dom";
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
import Patients from "./pages/Patients";
import MedicalRecord from "./pages/MedicalRecord";
import CostRecommendation from "./pages/CostRecommendation";
import BPJSSync from "./pages/BPJSSync";
import BPJSCode from "./pages/BPJSCode";
@@ -34,8 +34,8 @@ function App() {
>
<Route path="dashboard" element={<Dashboard />} />
<Route path="cost-recommendation" element={<CostRecommendation />} />
<Route path="patients" element={<Patients />} />
<Route path="medical-record" element={<MedicalRecord />} />
<Route path="patients/medical-record" element={<MedicalRecord />} />
<Route path="patients/bpjs-code" element={<BPJSCode />} />
<Route path="integration/bpjs" element={<BPJSSync />} />
<Route

View File

@@ -1,11 +1,13 @@
import { useState } from "react";
import { Outlet } from "react-router-dom";
import Sidebar from "./Sidebar";
import { Bell, Menu } from "lucide-react";
import { Menu, LogOut } from "lucide-react";
import { useNavigate } from "react-router-dom";
export default function Layout() {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const navigate = useNavigate();
const toggleSidebar = () => {
setSidebarCollapsed(!sidebarCollapsed);
@@ -15,6 +17,11 @@ export default function Layout() {
setMobileMenuOpen(!mobileMenuOpen);
};
const handleLogout = () => {
localStorage.removeItem("isAuthenticated");
navigate("/login");
};
return (
<div className="flex h-screen bg-gray-50">
{/* Desktop Sidebar */}
@@ -57,6 +64,27 @@ export default function Layout() {
ClaimGuard Hospital Management
</div>
</div>
<div className="flex items-center space-x-3">
<div className="hidden sm:flex items-center">
<div className="h-8 w-8 bg-green-600 rounded-full flex items-center justify-center mr-2">
<span className="text-xs font-medium text-white">A</span>
</div>
<div className="text-sm">
<div className="font-medium text-gray-900 leading-4">
Dr. Admin
</div>
<div className="text-xs text-gray-500">Administrator</div>
</div>
</div>
<button
onClick={handleLogout}
className="inline-flex items-center px-3 py-2 rounded-md text-red-600 hover:bg-red-50 border border-red-200"
title="Keluar"
>
<LogOut className="h-4 w-4 mr-2" />
<span className="hidden sm:inline">Keluar</span>
</button>
</div>
</div>
</header>

View File

@@ -1,10 +1,9 @@
import { useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { Link, useLocation } from "react-router-dom";
import {
LayoutDashboard,
Users,
FileText,
LogOut,
ChevronLeft,
ChevronRight,
UserCog,
@@ -26,12 +25,6 @@ export default function Sidebar({
onToggleCollapse,
}: SidebarProps) {
const location = useLocation();
const navigate = useNavigate();
const handleLogout = () => {
localStorage.removeItem("isAuthenticated");
navigate("/login");
};
const menuItems = [
{
@@ -64,11 +57,10 @@ export default function Sidebar({
icon: Users,
color: "text-orange-600",
submenu: [
{ title: "Manajemen Pasien", icon: Users, path: "/patients" },
{
title: "Medical Record Pasien",
icon: FileText,
path: "/medical-record",
path: "/patients/medical-record",
},
{ title: "BPJS Code", icon: Shield, path: "/patients/bpjs-code" },
],
@@ -263,46 +255,8 @@ export default function Sidebar({
</div>
</nav>
{/* User Profile & Logout */}
<div className="border-t border-gray-200 p-4">
{!isCollapsed ? (
<div className="space-y-3">
<div className="flex items-center">
<div className="h-8 w-8 bg-green-600 rounded-full flex items-center justify-center">
<span className="text-xs font-medium text-white">A</span>
</div>
<div className="ml-3 flex-1">
<p className="text-sm font-medium text-gray-900">Dr. Admin</p>
<p className="text-xs text-gray-500">Administrator</p>
</div>
</div>
<button
onClick={handleLogout}
className="w-full flex items-center px-3 py-2 rounded-lg text-sm font-medium text-red-600 hover:bg-red-50 transition-colors"
>
<LogOut className="h-5 w-5 mr-3" />
<span>Keluar</span>
</button>
</div>
) : (
<div className="space-y-2">
<div
className="h-8 w-8 bg-green-600 rounded-full flex items-center justify-center mx-auto"
title="Dr. Admin - Administrator"
>
<span className="text-xs font-medium text-white">A</span>
</div>
<button
onClick={handleLogout}
title="Keluar dari sistem"
className="w-full flex justify-center p-2 rounded-lg text-red-600 hover:bg-red-50 transition-colors"
>
<LogOut className="h-5 w-5" />
</button>
</div>
)}
</div>
{/* Footer space */}
<div className="border-t border-gray-200 p-4"></div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@ import { useState } from "react";
import {
Shield,
Calendar,
Search,
Filter,
AlertCircle,
CheckCircle,
@@ -36,10 +35,15 @@ interface BPJSSyncStats {
}
export default function BPJSSync() {
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [statusInput, setStatusInput] = useState("all");
const [appliedStatus, setAppliedStatus] = useState("all");
const [isImporting, setIsImporting] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
const [startDateInput, setStartDateInput] = useState("");
const [endDateInput, setEndDateInput] = useState("");
const [appliedStartDate, setAppliedStartDate] = useState<string | null>(null);
const [appliedEndDate, setAppliedEndDate] = useState<string | null>(null);
const [hasDateFiltered, setHasDateFiltered] = useState(false);
const formatDate = (dateString: string) => {
const date = new Date(dateString);
@@ -129,17 +133,23 @@ export default function BPJSSync() {
syncLogs.filter((log) => log.duration > 0).length || 0,
};
// Filter sync logs based on search and status
// Filter sync logs based on date and status (status applied via button)
const filteredLogs = syncLogs.filter((log) => {
const matchesSearch =
log.source.toLowerCase().includes(searchTerm.toLowerCase()) ||
log.type.toLowerCase().includes(searchTerm.toLowerCase()) ||
(log.errorMessage &&
log.errorMessage.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesStatus =
appliedStatus === "all" || log.status === appliedStatus;
let matchesDate = true;
if (hasDateFiltered) {
const ts = new Date(log.timestamp).getTime();
const startOk =
!appliedStartDate ||
ts >= new Date(appliedStartDate + "T00:00:00").getTime();
const endOk =
!appliedEndDate ||
ts <= new Date(appliedEndDate + "T23:59:59").getTime();
matchesDate = startOk && endOk;
}
const matchesStatus = statusFilter === "all" || log.status === statusFilter;
return matchesSearch && matchesStatus;
return matchesStatus && matchesDate;
});
const handleImport = async () => {
@@ -267,25 +277,33 @@ export default function BPJSSync() {
</div>
</div>
{/* Filters and Search */}
{/* Filters */}
<div className="bg-white p-6 rounded-lg shadow-sm border mb-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
<div className="flex items-center space-x-4">
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<div className="flex items-center space-x-3">
{/* Date range */}
<div className="flex items-center space-x-2">
<Calendar className="h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Cari source, type, atau error message..."
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-blue-500 focus:border-transparent w-80"
type="date"
value={startDateInput}
onChange={(e) => setStartDateInput(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<span className="text-gray-400 text-sm">s/d</span>
<input
type="date"
value={endDateInput}
onChange={(e) => setEndDateInput(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
{/* Status */}
<div className="flex items-center space-x-2">
<Filter className="h-4 w-4 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
value={statusInput}
onChange={(e) => setStatusInput(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Semua Status</option>
@@ -294,6 +312,34 @@ export default function BPJSSync() {
<option value="in_progress">Berlangsung</option>
</select>
</div>
{/* Buttons */}
<div className="flex items-center space-x-2">
<button
onClick={() => {
setAppliedStartDate(startDateInput || null);
setAppliedEndDate(endDateInput || null);
setHasDateFiltered(Boolean(startDateInput || endDateInput));
setAppliedStatus(statusInput);
}}
className="btn-primary px-3 py-2"
>
Filter
</button>
<button
onClick={() => {
setStartDateInput("");
setEndDateInput("");
setAppliedStartDate(null);
setAppliedEndDate(null);
setHasDateFiltered(false);
setStatusInput("all");
setAppliedStatus("all");
}}
className="btn-secondary px-3 py-2 border border-gray-300 rounded-md"
>
Reset
</button>
</div>
</div>
</div>
</div>

View File

@@ -687,7 +687,10 @@ interface ICDRecommendation {
reasoning: string;
}
async function mockRecommendICD(diagnosis: string, procedure: string): Promise<ICDRecommendation[]> {
async function mockRecommendICD(
diagnosis: string,
procedure: string
): Promise<ICDRecommendation[]> {
const lowerDx = diagnosis.toLowerCase();
const lowerProc = procedure.toLowerCase();
@@ -738,7 +741,10 @@ async function mockRecommendICD(diagnosis: string, procedure: string): Promise<I
});
}
if (lowerProc.includes("insulin") && !suggestions.find((s) => s.code.startsWith("E1"))) {
if (
lowerProc.includes("insulin") &&
!suggestions.find((s) => s.code.startsWith("E1"))
) {
suggestions.push({
code: "E10-E14",
description: "Diabetes mellitus (range, sesuaikan subkategori)",
@@ -747,7 +753,10 @@ async function mockRecommendICD(diagnosis: string, procedure: string): Promise<I
});
}
if (lowerProc.includes("antibiotik") && !suggestions.find((s) => s.code.startsWith("J"))) {
if (
lowerProc.includes("antibiotik") &&
!suggestions.find((s) => s.code.startsWith("J"))
) {
suggestions.push({
code: "J15-J18",
description: "Bacterial/unspecified pneumonia (range)",
@@ -762,7 +771,8 @@ async function mockRecommendICD(diagnosis: string, procedure: string): Promise<I
code: "R69",
description: "Illness, unspecified",
confidence: 55,
reasoning: "Tidak ada kata kunci spesifik terdeteksi. Perlu klarifikasi klinis.",
reasoning:
"Tidak ada kata kunci spesifik terdeteksi. Perlu klarifikasi klinis.",
},
{
code: "Z00.0",
@@ -786,7 +796,9 @@ export default function CostRecommendation() {
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));
const [currentVisitDate, setCurrentVisitDate] = useState(() =>
new Date().toISOString().slice(0, 10)
);
type BpjsMapping = {
icdCode: string;
@@ -796,11 +808,36 @@ export default function CostRecommendation() {
};
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 },
{
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 => {
@@ -808,18 +845,25 @@ export default function CostRecommendation() {
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;
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 }[];
.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));
const diff = Math.floor(
(db.getTime() - da.getTime()) / (1000 * 60 * 60 * 24)
);
return isNaN(diff) ? null : Math.max(diff, 0);
} catch {
return null;
@@ -829,8 +873,21 @@ export default function CostRecommendation() {
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 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[] = [];
@@ -838,37 +895,62 @@ export default function CostRecommendation() {
if (t.includes("kontrol") && foundAcute) {
inconsistent = true;
reasons.push("Terdapat indikasi kondisi akut pada keterangan, namun tipe kunjungan adalah kontrol.");
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'.");
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 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") {
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 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 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);
.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", {
@@ -884,7 +966,7 @@ export default function CostRecommendation() {
try {
const recs = await mockRecommendICD(diagnosis, procedure);
setResults(recs);
} catch (e) {
} catch {
setError("Gagal membuat rekomendasi. Coba lagi.");
} finally {
setIsLoading(false);
@@ -892,7 +974,9 @@ export default function CostRecommendation() {
};
const exportJSON = () => {
const blob = new Blob([JSON.stringify(results, null, 2)], { type: "application/json" });
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;
@@ -904,24 +988,30 @@ export default function CostRecommendation() {
const copyCode = async (code: string) => {
try {
await navigator.clipboard.writeText(code);
} catch {}
} catch {
// ignore clipboard errors in non-secure contexts
}
};
return (
<div className="p-6">
<div className="max-w-5xl mx-auto">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Asisten Rekomendasi ICD untuk Dokter</h1>
<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.
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 className="text-sm font-medium text-gray-700 mb-1 flex items-center">
<FileText className="h-4 w-4 mr-2 text-blue-600" /> Diagnosa
Klinis
</label>
<textarea
value={diagnosis}
@@ -933,7 +1023,9 @@ export default function CostRecommendation() {
</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>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tipe Kunjungan
</label>
<select
value={visitType}
onChange={(e) => setVisitType(e.target.value)}
@@ -945,7 +1037,9 @@ export default function CostRecommendation() {
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Tanggal Kunjungan Terakhir</label>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tanggal Kunjungan Terakhir
</label>
<input
type="date"
value={lastVisitDate}
@@ -954,7 +1048,9 @@ export default function CostRecommendation() {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Tanggal Kunjungan Saat Ini</label>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tanggal Kunjungan Saat Ini
</label>
<input
type="date"
value={currentVisitDate}
@@ -964,8 +1060,9 @@ export default function CostRecommendation() {
</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 className="text-sm font-medium text-gray-700 mb-1 flex items-center">
<Stethoscope className="h-4 w-4 mr-2 text-green-600" />{" "}
Prosedur/Tatalaksana Utama
</label>
<input
type="text"
@@ -982,7 +1079,9 @@ export default function CostRecommendation() {
className="btn-primary flex items-center space-x-2 disabled:opacity-60"
>
<Search className="h-4 w-4" />
<span>{isLoading ? "Menghasilkan..." : "Buat Rekomendasi"}</span>
<span>
{isLoading ? "Menghasilkan..." : "Buat Rekomendasi"}
</span>
</button>
<button
onClick={exportJSON}
@@ -993,13 +1092,12 @@ export default function CostRecommendation() {
<span>Export JSON</span>
</button>
</div>
{error && (
<div className="text-sm text-red-600">{error}</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.
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) => (
@@ -1014,32 +1112,49 @@ export default function CostRecommendation() {
<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>
<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.
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 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>
<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>
<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">
@@ -1047,34 +1162,52 @@ export default function CostRecommendation() {
<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}
<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;
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)})
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>
<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="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))}%` }}
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>
<div className="text-sm text-gray-600">
{r.reasoning}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<button
@@ -1092,8 +1225,12 @@ export default function CostRecommendation() {
) : (
<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>
<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>
@@ -1101,35 +1238,55 @@ export default function CostRecommendation() {
{/* 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>
<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>
<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>
<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">
<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}
<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>
<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 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" />
@@ -1144,15 +1301,24 @@ export default function CostRecommendation() {
) : (
<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>
<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>
<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>

View File

@@ -246,93 +246,6 @@ export default function Dashboard() {
</div>
</div>
</div>
{/* Additional Analytics Row */}
<div className="mt-8 grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="card p-6">
<h4 className="text-lg font-semibold text-gray-900 mb-4">
Pasien Datang Hari Ini
</h4>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Rawat Jalan</span>
<span className="text-sm font-medium text-blue-600">52</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Rawat Inap</span>
<span className="text-sm font-medium text-green-600">23</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">IGD</span>
<span className="text-sm font-medium text-red-600">14</span>
</div>
<div className="border-t pt-2 mt-3">
<div className="flex justify-between items-center">
<span className="text-sm font-semibold text-gray-800">
Total
</span>
<span className="text-lg font-bold text-gray-900">89</span>
</div>
</div>
</div>
</div>
<div className="card p-6">
<h4 className="text-lg font-semibold text-gray-900 mb-4">
Potential Overclaim
</h4>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">High Risk</span>
<span className="text-sm font-medium text-red-600">3</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Medium Risk</span>
<span className="text-sm font-medium text-orange-600">4</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Low Risk</span>
<span className="text-sm font-medium text-yellow-600">8</span>
</div>
<div className="border-t pt-2 mt-3">
<div className="flex justify-between items-center">
<span className="text-sm font-semibold text-gray-800">
Perlu Review
</span>
<span className="text-lg font-bold text-red-600">7</span>
</div>
</div>
</div>
</div>
<div className="card p-6">
<h4 className="text-lg font-semibold text-gray-900 mb-4">
Verifikasi Klaim
</h4>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Disetujui</span>
<span className="text-sm font-medium text-green-600">89</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Pending</span>
<span className="text-sm font-medium text-yellow-600">23</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Ditolak</span>
<span className="text-sm font-medium text-red-600">5</span>
</div>
<div className="border-t pt-2 mt-3">
<div className="flex justify-between items-center">
<span className="text-sm font-semibold text-gray-800">
Total Klaim
</span>
<span className="text-lg font-bold text-gray-900">117</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);

View File

@@ -2,18 +2,12 @@ import { useState } from "react";
import {
FileText,
Search,
Filter,
Plus,
Eye,
Edit,
Calendar,
User,
Stethoscope,
Heart,
Activity,
ClipboardList,
Download,
Printer,
} from "lucide-react";
interface MedicalRecord {
@@ -37,6 +31,14 @@ interface MedicalRecord {
};
}
const dateDaysAgo = (days: number, hour = 10, minute = 30) => {
const d = new Date();
d.setHours(0, 0, 0, 0);
d.setDate(d.getDate() - days);
d.setHours(hour, minute, 0, 0);
return d.toISOString();
};
const sampleMedicalRecords: MedicalRecord[] = [
{
id: "MR001",
@@ -44,7 +46,7 @@ const sampleMedicalRecords: MedicalRecord[] = [
patientName: "Ahmad Rizki",
patientAge: 45,
patientGender: "Laki-laki",
recordDate: "2024-01-15T10:30:00Z",
recordDate: dateDaysAgo(5, 10, 30),
diagnosis: "Hipertensi Grade 2",
icdCode: "I10",
treatment: "Amlodipine 10mg 1x1, Diet rendah garam",
@@ -64,7 +66,7 @@ const sampleMedicalRecords: MedicalRecord[] = [
patientName: "Maria Lopez",
patientAge: 32,
patientGender: "Perempuan",
recordDate: "2024-01-15T14:15:00Z",
recordDate: dateDaysAgo(12, 14, 15),
diagnosis: "Gastritis Akut",
icdCode: "K29.0",
treatment: "Omeprazole 20mg 2x1, Antasida 3x1",
@@ -84,7 +86,7 @@ const sampleMedicalRecords: MedicalRecord[] = [
patientName: "Dewi Sartika",
patientAge: 28,
patientGender: "Perempuan",
recordDate: "2024-01-15T09:45:00Z",
recordDate: dateDaysAgo(20, 9, 45),
diagnosis: "Kehamilan Normal G1P0A0",
icdCode: "Z34.0",
treatment: "Asam folat 1x1, Vitamin prenatal",
@@ -104,7 +106,7 @@ const sampleMedicalRecords: MedicalRecord[] = [
patientName: "Joko Widodo",
patientAge: 56,
patientGender: "Laki-laki",
recordDate: "2024-01-15T16:20:00Z",
recordDate: dateDaysAgo(28, 16, 20),
diagnosis: "Diabetes Mellitus Tipe 2",
icdCode: "E11.9",
treatment: "Metformin 500mg 2x1, Diet DM",
@@ -118,37 +120,64 @@ const sampleMedicalRecords: MedicalRecord[] = [
weight: 82,
},
},
{
id: "MR005",
patientId: "P004",
patientName: "Joko Widodo",
patientAge: 56,
patientGender: "Laki-laki",
recordDate: dateDaysAgo(7, 11, 10),
diagnosis: "Kontrol DM tipe 2, evaluasi terapi",
icdCode: "E11.9",
treatment: "Metformin 850mg 2x1, Edukasi diet & aktivitas",
doctor: "Dr. Budi Santoso",
department: "Endocrinology",
status: "reviewed",
vital: {
bloodPressure: "130/85",
heartRate: 78,
temperature: 36.6,
weight: 81,
},
},
];
export default function MedicalRecord() {
const [records] = useState<MedicalRecord[]>(sampleMedicalRecords);
const [searchTerm, setSearchTerm] = useState("");
const [selectedDepartment, setSelectedDepartment] = useState("");
const [selectedStatus, setSelectedStatus] = useState("");
const [nameInput, setNameInput] = useState("");
const [idInput, setIdInput] = useState("");
const [appliedName, setAppliedName] = useState("");
const [appliedId, setAppliedId] = useState("");
const [hasSearched, setHasSearched] = useState(false);
const [detailId, setDetailId] = useState<string | null>(null);
const departments = [
"Cardiology",
"Internal Medicine",
"Obstetrics & Gynecology",
"Endocrinology",
"Emergency",
"Surgery",
"Pediatrics",
];
// departments removed (unused)
const handleSearch = () => {
setAppliedName(nameInput.trim());
setAppliedId(idInput.trim());
setHasSearched(true);
};
const handleReset = () => {
setNameInput("");
setIdInput("");
setAppliedName("");
setAppliedId("");
setHasSearched(false);
};
const filteredRecords = records.filter((record) => {
const matchesSearch =
record.patientName.toLowerCase().includes(searchTerm.toLowerCase()) ||
record.diagnosis.toLowerCase().includes(searchTerm.toLowerCase()) ||
record.icdCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
record.doctor.toLowerCase().includes(searchTerm.toLowerCase());
const matchesDepartment =
!selectedDepartment || record.department === selectedDepartment;
const matchesStatus = !selectedStatus || record.status === selectedStatus;
return matchesSearch && matchesDepartment && matchesStatus;
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const inLast30Days = new Date(record.recordDate) >= thirtyDaysAgo;
if (!hasSearched) return inLast30Days;
const matchName =
!appliedName ||
record.patientName.toLowerCase().includes(appliedName.toLowerCase());
const matchId =
!appliedId || record.patientId.toLowerCase() === appliedId.toLowerCase();
return inLast30Days && matchName && matchId;
});
const getStatusColor = (status: string) => {
@@ -207,109 +236,62 @@ export default function MedicalRecord() {
</p>
</div>
</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">
<Plus className="h-4 w-4" />
<span>Tambah Record</span>
</button>
</div>
<div />
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div className="card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Total Records
</p>
<p className="text-2xl font-bold text-gray-900">
{records.length}
</p>
</div>
<FileText className="h-8 w-8 text-green-500" />
</div>
</div>
<div className="card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Completed</p>
<p className="text-2xl font-bold text-green-600">
{records.filter((r) => r.status === "completed").length}
</p>
</div>
<ClipboardList className="h-8 w-8 text-green-500" />
</div>
</div>
<div className="card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Reviewed</p>
<p className="text-2xl font-bold text-blue-600">
{records.filter((r) => r.status === "reviewed").length}
</p>
</div>
<Eye className="h-8 w-8 text-blue-500" />
</div>
</div>
<div className="card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Draft</p>
<p className="text-2xl font-bold text-yellow-600">
{records.filter((r) => r.status === "draft").length}
</p>
</div>
<Edit className="h-8 w-8 text-yellow-500" />
</div>
</div>
</div>
{/* Filters and Search */}
{/* Filters: Search by Patient Name and Patient ID with trigger button */}
<div className="bg-white p-6 rounded-lg shadow-sm border mb-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
<div className="flex items-center space-x-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Cari Nama Pasien
</label>
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Cari pasien, diagnosa, ICD code, atau dokter..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent w-80"
placeholder="Contoh: Ahmad Rizki"
value={nameInput}
onChange={(e) => setNameInput(e.target.value)}
className="pl-9 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent w-full"
/>
</div>
<div className="flex items-center space-x-2">
<Filter className="h-4 w-4 text-gray-400" />
<select
value={selectedDepartment}
onChange={(e) => setSelectedDepartment(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-green-500 focus:border-transparent"
>
<option value="">Semua Department</option>
{departments.map((dept) => (
<option key={dept} value={dept}>
{dept}
</option>
))}
</select>
<select
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-green-500 focus:border-transparent"
>
<option value="">Semua Status</option>
<option value="draft">Draft</option>
<option value="completed">Selesai</option>
<option value="reviewed">Direview</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Cari ID Pasien
</label>
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Contoh: P001"
value={idInput}
onChange={(e) => setIdInput(e.target.value)}
className="pl-9 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent w-full"
/>
</div>
</div>
<div className="flex items-end">
<button
onClick={handleSearch}
disabled={!nameInput && !idInput}
className="btn-primary flex items-center space-x-2 disabled:opacity-60"
>
<Search className="h-4 w-4" />
<span>Cari</span>
</button>
<button
onClick={handleReset}
className="ml-3 btn-secondary px-3 py-2 rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50"
>
Reset
</button>
</div>
</div>
<div className="text-xs text-gray-500 mt-3">
Menampilkan riwayat medical record 30 hari terakhir sesuai kriteria.
</div>
</div>
@@ -331,14 +313,11 @@ export default function MedicalRecord() {
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Dokter & Dept
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tanggal
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Aksi
Detail
</th>
</tr>
</thead>
@@ -347,13 +326,6 @@ export default function MedicalRecord() {
<tr key={record.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="h-10 w-10 rounded-full bg-gradient-to-r from-green-500 to-blue-600 flex items-center justify-center text-white font-medium">
{record.patientName
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()}
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
{record.patientName}
@@ -408,21 +380,9 @@ export default function MedicalRecord() {
<div className="text-sm font-medium text-gray-900">
{record.doctor}
</div>
<div className="text-xs text-gray-500">
{record.department}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(
record.status
)}`}
>
{getStatusText(record.status)}
</span>
</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" />
@@ -431,18 +391,13 @@ export default function MedicalRecord() {
</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">
<button
onClick={() => setDetailId(record.id)}
className="text-blue-600 hover:text-blue-900"
title="Lihat detail rekam medis"
>
<Eye className="h-4 w-4" />
</button>
<button className="text-green-600 hover:text-green-900">
<Edit className="h-4 w-4" />
</button>
<button className="text-purple-600 hover:text-purple-900">
<Download className="h-4 w-4" />
</button>
<button className="text-gray-600 hover:text-gray-900">
<Printer className="h-4 w-4" />
</button>
</div>
</td>
</tr>
@@ -452,8 +407,302 @@ export default function MedicalRecord() {
</div>
</div>
{/* Detail Modal */}
{detailId && (
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4">
<div className="bg-white w-full max-w-3xl rounded-lg shadow-lg">
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<div className="text-lg font-semibold text-gray-900">
Detail Rekam Medis
</div>
<button
onClick={() => setDetailId(null)}
className="text-gray-500 hover:text-gray-700 text-sm"
>
Tutup
</button>
</div>
{(() => {
const rec = records.find((r) => r.id === detailId);
if (!rec)
return (
<div className="p-6 text-sm">Data tidak ditemukan.</div>
);
// Dummy detail menyerupai rekam medis standar RS (hasil olahan LLM)
const details = (() => {
switch (rec.icdCode) {
case "I10":
return {
chiefComplaint: "Pusing dan tekanan darah tinggi",
hpi: "Keluhan sejak 3 hari, pusing berdenyut terutama pagi hari. Tidak ada nyeri dada/sesak.",
pmh: ["Hipertensi 2 tahun", "Dislipidemia"],
meds: ["Amlodipine 10mg 1x1", "Atorvastatin 20mg 1x1"],
allergies: ["Tidak ada"],
labs: [
"Profil lipid: LDL 140 mg/dl",
"Fungsi ginjal normal",
],
imaging: ["Foto toraks dalam batas normal"],
procedures: [
"Edukasi diet rendah garam",
"Monitoring TD rumah",
],
plan: [
"Lanjut obat, evaluasi 2 minggu",
"Olahraga 30 menit/hari",
"Kontrol tekanan darah harian",
],
discharge:
"Pulang kondisi stabil, edukasi dan rencana kontrol diberikan.",
} as const;
case "K29.0":
return {
chiefComplaint: "Nyeri ulu hati dan mual",
hpi: "Nyeri ulu hati terutama setelah makan pedas/asam, mual tanpa muntah.",
pmh: ["Gastritis episodik"],
meds: ["Omeprazole 20mg 2x1", "Antasida 3x1"],
allergies: ["Tidak ada"],
labs: ["Hb normal", "Helicobacter pylori: negatif"],
imaging: ["Tidak dilakukan"],
procedures: ["Edukasi diet lambung", "Hindari NSAID"],
plan: [
"PPI 2 minggu",
"Diet lunak, porsi kecil",
"Kontrol bila nyeri menetap",
],
discharge: "Pulang, kontrol bila keluhan berlanjut.",
} as const;
case "E11.9":
return {
chiefComplaint: "Sering haus dan berkemih",
hpi: "Keluhan polidipsia dan poliuria 1 minggu, gula darah rumah meningkat.",
pmh: ["DM tipe 2 3 tahun"],
meds: ["Metformin 500mg 2x1"],
allergies: ["Tidak ada"],
labs: ["GDP 180 mg/dl", "HbA1c 8.1%"],
imaging: ["Tidak dilakukan"],
procedures: ["Edukasi diet DM", "Jurnal gula harian"],
plan: [
"Optimasi metformin",
"Rencana edukasi diet dan aktivitas",
"Kontrol 2-4 minggu",
],
discharge:
"Pulang, monitoring gula dan kontrol terjadwal.",
} as const;
default:
return {
chiefComplaint: "Keluhan sesuai diagnosa utama",
hpi: "Riwayat penyakit sekarang sesuai catatan klinis.",
pmh: ["Tidak ada yang menonjol"],
meds: [rec.treatment],
allergies: ["Tidak diketahui"],
labs: ["Dalam batas normal"],
imaging: ["Tidak ada temuan bermakna"],
procedures: ["Observasi dan edukasi"],
plan: ["Kontrol sesuai kebutuhan"],
discharge: "Pulang dalam kondisi stabil.",
} as const;
}
})();
return (
<div className="p-6 text-sm text-gray-800 max-h-[75vh] overflow-y-auto">
<div className="space-y-6">
<div className="rounded-lg border border-gray-200">
<div className="px-4 py-2 border-b border-gray-200 bg-gray-50 text-sm font-semibold text-gray-900">
Identitas Pasien & Kunjungan
</div>
<div className="p-4 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-1">
<div className="text-xs uppercase tracking-wide text-gray-500">
Nama / Jenis Kelamin / Umur
</div>
<div className="text-gray-900 font-medium">
{rec.patientName} ({rec.patientGender},{" "}
{rec.patientAge} th)
</div>
<div className="text-gray-500">
ID Pasien: {rec.patientId}
</div>
</div>
<div className="space-y-1">
<div className="text-xs uppercase tracking-wide text-gray-500">
Tanggal / Dokter / Dept
</div>
<div className="text-gray-900 font-medium">
{formatDate(rec.recordDate)}
</div>
<div className="text-gray-500">
{rec.doctor} {rec.department}
</div>
</div>
</div>
</div>
<div className="rounded-lg border border-gray-200">
<div className="px-4 py-2 border-b border-gray-200 bg-gray-50 text-sm font-semibold text-gray-900">
Anamnesis
</div>
<div className="p-4 grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div className="text-xs uppercase tracking-wide text-gray-500">
Keluhan Utama
</div>
<div className="mt-1 text-gray-900">
{details.chiefComplaint}
</div>
</div>
<div>
<div className="text-xs uppercase tracking-wide text-gray-500">
Riwayat Penyakit Sekarang
</div>
<div className="mt-1 text-gray-900 leading-relaxed">
{details.hpi}
</div>
</div>
</div>
</div>
<div className="rounded-lg border border-gray-200">
<div className="px-4 py-2 border-b border-gray-200 bg-gray-50 text-sm font-semibold text-gray-900">
Riwayat / Obat / Alergi
</div>
<div className="p-4 grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<div className="text-xs uppercase tracking-wide text-gray-500 mb-1">
Riwayat Penyakit Dahulu
</div>
<ul className="list-disc pl-5 space-y-1">
{details.pmh.map((x, i) => (
<li key={i}>{x}</li>
))}
</ul>
</div>
<div>
<div className="text-xs uppercase tracking-wide text-gray-500 mb-1">
Obat
</div>
<ul className="list-disc pl-5 space-y-1">
{details.meds.map((x, i) => (
<li key={i}>{x}</li>
))}
</ul>
</div>
<div>
<div className="text-xs uppercase tracking-wide text-gray-500 mb-1">
Alergi
</div>
<ul className="list-disc pl-5 space-y-1">
{details.allergies.map((x, i) => (
<li key={i}>{x}</li>
))}
</ul>
</div>
</div>
</div>
<div className="rounded-lg border border-gray-200">
<div className="px-4 py-2 border-b border-gray-200 bg-gray-50 text-sm font-semibold text-gray-900">
Penunjang
</div>
<div className="p-4 grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div className="text-xs uppercase tracking-wide text-gray-500 mb-1">
Laboratorium
</div>
<ul className="list-disc pl-5 space-y-1">
{details.labs.map((x, i) => (
<li key={i}>{x}</li>
))}
</ul>
</div>
<div>
<div className="text-xs uppercase tracking-wide text-gray-500 mb-1">
Imaging
</div>
<ul className="list-disc pl-5 space-y-1">
{details.imaging.map((x, i) => (
<li key={i}>{x}</li>
))}
</ul>
</div>
</div>
</div>
<div className="rounded-lg border border-gray-200">
<div className="px-4 py-2 border-b border-gray-200 bg-gray-50 text-sm font-semibold text-gray-900">
Diagnosis & Tindakan
</div>
<div className="p-4 grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div className="text-xs uppercase tracking-wide text-gray-500 mb-1">
Diagnosa & ICD
</div>
<div className="text-gray-900">{rec.diagnosis}</div>
<div className="text-xs font-mono inline-block mt-1 bg-gray-100 px-2 py-1 rounded">
{rec.icdCode}
</div>
</div>
<div>
<div className="text-xs uppercase tracking-wide text-gray-500 mb-1">
Prosedur/Tindakan
</div>
<ul className="list-disc pl-5 space-y-1">
{details.procedures.map((x, i) => (
<li key={i}>{x}</li>
))}
</ul>
</div>
</div>
</div>
<div className="rounded-lg border border-gray-200">
<div className="px-4 py-2 border-b border-gray-200 bg-gray-50 text-sm font-semibold text-gray-900">
Tanda Vital & Rencana
</div>
<div className="p-4 grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<ul className="mt-1 list-disc pl-5 space-y-1">
<li>TD: {rec.vital.bloodPressure} mmHg</li>
<li>Nadi: {rec.vital.heartRate} bpm</li>
<li>Suhu: {rec.vital.temperature}°C</li>
<li>Berat: {rec.vital.weight} kg</li>
</ul>
</div>
<div>
<ul className="list-disc pl-5 space-y-1">
{details.plan.map((x, i) => (
<li key={i}>{x}</li>
))}
</ul>
</div>
</div>
</div>
<div className="flex items-center justify-between">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(
rec.status
)}`}
>
Status: {getStatusText(rec.status)}
</span>
<div className="text-xs text-gray-600">
Ringkasan Pulang: {details.discharge}
</div>
</div>
</div>
</div>
);
})()}
</div>
</div>
)}
{/* Empty State */}
{filteredRecords.length === 0 && (
{hasSearched && filteredRecords.length === 0 && (
<div className="text-center py-12">
<FileText className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">

View File

@@ -3,7 +3,6 @@ import {
Database,
RefreshCw,
Calendar,
Search,
Filter,
CheckCircle,
XCircle,
@@ -45,10 +44,15 @@ interface SyncStats {
}
export default function MedicalRecordSync() {
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [statusInput, setStatusInput] = useState("all");
const [appliedStatus, setAppliedStatus] = useState("all");
const [isImporting, setIsImporting] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
const [startDateInput, setStartDateInput] = useState("");
const [endDateInput, setEndDateInput] = useState("");
const [appliedStartDate, setAppliedStartDate] = useState<string | null>(null);
const [appliedEndDate, setAppliedEndDate] = useState<string | null>(null);
const [hasDateFiltered, setHasDateFiltered] = useState(false);
const formatDate = (dateString: string) => {
const date = new Date(dateString);
@@ -207,17 +211,23 @@ export default function MedicalRecordSync() {
syncLogs.filter((log) => log.duration > 0).length || 0,
};
// Filter logs based on search and status
// Filter logs based on date and status (status applied via button)
const filteredLogs = syncLogs.filter((log) => {
const matchesSearch =
log.source.toLowerCase().includes(searchTerm.toLowerCase()) ||
log.type.toLowerCase().includes(searchTerm.toLowerCase()) ||
(log.errorMessage &&
log.errorMessage.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesStatus =
appliedStatus === "all" || log.status === appliedStatus;
let matchesDate = true;
if (hasDateFiltered) {
const ts = new Date(log.timestamp).getTime();
const startOk =
!appliedStartDate ||
ts >= new Date(appliedStartDate + "T00:00:00").getTime();
const endOk =
!appliedEndDate ||
ts <= new Date(appliedEndDate + "T23:59:59").getTime();
matchesDate = startOk && endOk;
}
const matchesStatus = statusFilter === "all" || log.status === statusFilter;
return matchesSearch && matchesStatus;
return matchesStatus && matchesDate;
});
const handleImport = async () => {
@@ -346,25 +356,33 @@ export default function MedicalRecordSync() {
</div>
</div>
{/* Filters and Search */}
{/* Filters */}
<div className="bg-white p-6 rounded-lg shadow-sm border mb-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
<div className="flex items-center space-x-4">
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<div className="flex items-center space-x-3">
{/* Date range */}
<div className="flex items-center space-x-2">
<Calendar className="h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Cari source, type, atau error message..."
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-blue-500 focus:border-transparent w-80"
type="date"
value={startDateInput}
onChange={(e) => setStartDateInput(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<span className="text-gray-400 text-sm">s/d</span>
<input
type="date"
value={endDateInput}
onChange={(e) => setEndDateInput(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
{/* Status */}
<div className="flex items-center space-x-2">
<Filter className="h-4 w-4 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
value={statusInput}
onChange={(e) => setStatusInput(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Semua Status</option>
@@ -373,6 +391,34 @@ export default function MedicalRecordSync() {
<option value="in_progress">Berlangsung</option>
</select>
</div>
{/* Buttons */}
<div className="flex items-center space-x-2">
<button
onClick={() => {
setAppliedStartDate(startDateInput || null);
setAppliedEndDate(endDateInput || null);
setHasDateFiltered(Boolean(startDateInput || endDateInput));
setAppliedStatus(statusInput);
}}
className="btn-primary px-3 py-2"
>
Filter
</button>
<button
onClick={() => {
setStartDateInput("");
setEndDateInput("");
setAppliedStartDate(null);
setAppliedEndDate(null);
setHasDateFiltered(false);
setStatusInput("all");
setAppliedStatus("all");
}}
className="btn-secondary px-3 py-2 border border-gray-300 rounded-md"
>
Reset
</button>
</div>
</div>
</div>
</div>
@@ -406,9 +452,6 @@ export default function MedicalRecordSync() {
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Duration
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Details
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
@@ -507,26 +550,6 @@ export default function MedicalRecordSync() {
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{log.duration > 0 ? `${log.duration}s` : "-"}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-xs text-gray-500">
<div className="flex items-center">
<Users className="h-3 w-3 mr-1" />
Pasien: {log.details.patientsUpdated}
</div>
<div className="flex items-center mt-1">
<Activity className="h-3 w-3 mr-1" />
Diagnosis: {log.details.diagnosesAdded}
</div>
<div className="flex items-center mt-1">
<FileText className="h-3 w-3 mr-1" />
Treatment: {log.details.treatmentsAdded}
</div>
<div className="flex items-center mt-1">
<RefreshCw className="h-3 w-3 mr-1" />
Vitals: {log.details.vitalsUpdated}
</div>
</div>
</td>
</tr>
))}
</tbody>

View File

@@ -1,284 +0,0 @@
import {
Users,
UserPlus,
Search,
Filter,
Calendar,
Eye,
Edit,
Download,
Printer,
User,
Phone,
} from "lucide-react";
export default function Patients() {
const patients = [
{
id: "P001",
name: "Ahmad Rizki",
age: 35,
gender: "Laki-laki",
phone: "+62 812-3456-7890",
lastVisit: "2024-01-15T10:30:00Z",
status: "Active",
},
{
id: "P002",
name: "Siti Nurhaliza",
age: 28,
gender: "Perempuan",
phone: "+62 813-4567-8901",
lastVisit: "2024-01-14T14:15:00Z",
status: "Active",
},
{
id: "P003",
name: "Budi Santoso",
age: 42,
gender: "Laki-laki",
phone: "+62 814-5678-9012",
lastVisit: "2024-01-10T09:45:00Z",
status: "Inactive",
},
{
id: "P004",
name: "Maria Lopez",
age: 29,
gender: "Perempuan",
phone: "+62 815-6789-0123",
lastVisit: "2024-01-13T16:20:00Z",
status: "Active",
},
{
id: "P005",
name: "Dewi Sartika",
age: 31,
gender: "Perempuan",
phone: "+62 816-7890-1234",
lastVisit: "2024-01-12T11:30:00Z",
status: "Active",
},
];
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",
});
};
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 className="flex items-center space-x-3">
<div className="bg-blue-100 p-3 rounded-lg">
<Users className="h-6 w-6 text-blue-600" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900">
Manajemen Pasien
</h1>
<p className="text-gray-600">
Kelola data pasien rumah sakit dan informasi kontak
</p>
</div>
</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">
<UserPlus className="h-4 w-4" />
<span>Tambah Pasien</span>
</button>
</div>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="card p-6">
<div className="flex items-center">
<div className="p-2 rounded-lg bg-blue-100">
<Users className="h-5 w-5 text-blue-600" />
</div>
<div className="ml-4">
<p className="text-2xl font-bold text-gray-900">1,234</p>
<p className="text-sm text-gray-500">Total Pasien</p>
</div>
</div>
</div>
<div className="card p-6">
<div className="flex items-center">
<div className="p-2 rounded-lg bg-green-100">
<Users className="h-5 w-5 text-green-600" />
</div>
<div className="ml-4">
<p className="text-2xl font-bold text-gray-900">45</p>
<p className="text-sm text-gray-500">Pasien Aktif</p>
</div>
</div>
</div>
<div className="card p-6">
<div className="flex items-center">
<div className="p-2 rounded-lg bg-orange-100">
<Users className="h-5 w-5 text-orange-600" />
</div>
<div className="ml-4">
<p className="text-2xl font-bold text-gray-900">24</p>
<p className="text-sm text-gray-500">Hari Ini</p>
</div>
</div>
</div>
<div className="card p-6">
<div className="flex items-center">
<div className="p-2 rounded-lg bg-purple-100">
<Users className="h-5 w-5 text-purple-600" />
</div>
<div className="ml-4">
<p className="text-2xl font-bold text-gray-900">8</p>
<p className="text-sm text-gray-500">Emergency</p>
</div>
</div>
</div>
</div>
{/* Filters and Search */}
<div className="bg-white p-6 rounded-lg shadow-sm border mb-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
<div className="flex items-center space-x-4">
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Cari nama pasien, ID, atau nomor telepon..."
className="pl-9 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-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 className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="">Semua Status</option>
<option value="active">Aktif</option>
<option value="inactive">Tidak Aktif</option>
</select>
<select className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="">Semua Gender</option>
<option value="laki-laki">Laki-laki</option>
<option value="perempuan">Perempuan</option>
</select>
</div>
</div>
</div>
</div>
{/* Patients Table */}
<div className="card">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Pasien
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Kontak
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Kunjungan Terakhir
</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">
{patients.map((patient) => (
<tr key={patient.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="h-10 w-10 rounded-full bg-gradient-to-r from-blue-500 to-green-600 flex items-center justify-center text-white font-medium">
{patient.name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()}
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
{patient.name}
</div>
<div className="text-sm text-gray-500 flex items-center">
<User className="h-3 w-3 mr-1" />
{patient.age} tahun {patient.gender}
</div>
<div className="text-xs text-gray-400">
ID: {patient.id}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-500">
<Phone className="h-4 w-4 mr-1" />
{patient.phone}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
patient.status === "Active"
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{patient.status === "Active" ? "Aktif" : "Tidak Aktif"}
</span>
</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(patient.lastVisit)}
</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">
<Edit className="h-4 w-4" />
</button>
<button className="text-purple-600 hover:text-purple-900">
<Download className="h-4 w-4" />
</button>
<button className="text-gray-600 hover:text-gray-900">
<Printer className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
}

View File

@@ -7,11 +7,8 @@ import {
Search,
Filter,
Users,
CheckCircle,
XCircle,
Settings,
Calendar,
Download,
} from "lucide-react";
import { sampleRoles, MODULES, ACTIONS } from "../types/roles";
import type { IRole } from "../types/roles";
@@ -34,22 +31,28 @@ const getModuleDisplayName = (module: string) => {
export default function RoleManagement() {
const [roles, setRoles] = useState<IRole[]>(sampleRoles);
const [searchTerm, setSearchTerm] = useState("");
const [searchInput, setSearchInput] = useState("");
const [selectedRole, setSelectedRole] = useState<IRole | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalMode, setModalMode] = useState<"create" | "edit">("create");
const [statusFilter, setStatusFilter] = useState("all");
const [permissionFilter, setPermissionFilter] = useState("all");
const [statusInput, setStatusInput] = useState("all");
const [permissionFilter] = useState("all");
const [appliedSearch, setAppliedSearch] = useState("");
const [appliedStatus, setAppliedStatus] = useState("all");
const [hasFiltered, setHasFiltered] = useState(false);
const filteredRoles = roles.filter((role) => {
const matchesSearch =
role.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
role.description.toLowerCase().includes(searchTerm.toLowerCase());
const matchesSearch = !hasFiltered
? true
: !appliedSearch ||
role.name.toLowerCase().includes(appliedSearch.toLowerCase()) ||
role.description.toLowerCase().includes(appliedSearch.toLowerCase());
const statusToUse = hasFiltered ? appliedStatus : "all";
const matchesStatus =
statusFilter === "all" ||
(statusFilter === "active" && role.isActive) ||
(statusFilter === "inactive" && !role.isActive);
statusToUse === "all" ||
(statusToUse === "active" && role.isActive) ||
(statusToUse === "inactive" && !role.isActive);
const matchesPermission =
permissionFilter === "all" ||
@@ -119,10 +122,6 @@ export default function RoleManagement() {
</div>
</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
onClick={handleCreateRole}
className="btn-primary flex items-center space-x-2"
@@ -134,100 +133,58 @@ export default function RoleManagement() {
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div className="card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Roles</p>
<p className="text-2xl font-bold text-gray-900">
{roles.length}
</p>
</div>
<Shield className="h-8 w-8 text-purple-500" />
</div>
</div>
<div className="card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Active Roles
</p>
<p className="text-2xl font-bold text-green-600">
{roles.filter((r) => r.isActive).length}
</p>
</div>
<CheckCircle className="h-8 w-8 text-green-500" />
</div>
</div>
<div className="card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Inactive Roles
</p>
<p className="text-2xl font-bold text-red-600">
{roles.filter((r) => !r.isActive).length}
</p>
</div>
<XCircle className="h-8 w-8 text-red-500" />
</div>
</div>
<div className="card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Avg Permissions
</p>
<p className="text-2xl font-bold text-blue-600">
{Math.round(
roles.reduce(
(acc, role) => acc + role.permissions.length,
0
) / roles.length
)}
</p>
</div>
<Settings className="h-8 w-8 text-blue-500" />
</div>
</div>
</div>
{/* Filters and Search */}
{/* Filters */}
<div className="bg-white p-6 rounded-lg shadow-sm border mb-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-3">
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Cari nama role atau deskripsi..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
value={searchInput}
onChange={(e) => setSearchInput(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>
{/* Status */}
<div className="flex items-center space-x-2">
<Filter className="h-4 w-4 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
value={statusInput}
onChange={(e) => setStatusInput(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 Status</option>
<option value="active">Aktif</option>
<option value="inactive">Tidak Aktif</option>
</select>
<select
value={permissionFilter}
onChange={(e) => setPermissionFilter(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
</div>
{/* Buttons */}
<div className="flex items-center space-x-2">
<button
onClick={() => {
setAppliedSearch(searchInput.trim());
setAppliedStatus(statusInput);
setHasFiltered(true);
}}
className="btn-primary px-3 py-2"
>
<option value="all">Semua Permission</option>
<option value="high">Banyak (8+)</option>
<option value="medium">Sedang (4-7)</option>
<option value="low">Sedikit (1-3)</option>
</select>
Filter
</button>
<button
onClick={() => {
setSearchInput("");
setStatusInput("all");
setAppliedSearch("");
setAppliedStatus("all");
setHasFiltered(false);
}}
className="btn-secondary px-3 py-2 border border-gray-300 rounded-md"
>
Reset
</button>
</div>
</div>
</div>

View File

@@ -8,55 +8,39 @@ import {
Filter,
Mail,
Phone,
Building2,
XCircle,
UserCheck,
Clock,
Shield,
Calendar,
Download,
} from "lucide-react";
import { sampleUsers, sampleRoles } from "../types/roles";
import type { IUser } from "../types/roles";
export default function UserManagement() {
const [users, setUsers] = useState<IUser[]>(sampleUsers);
const [searchTerm, setSearchTerm] = useState("");
const [searchInput, setSearchInput] = useState("");
const [selectedUser, setSelectedUser] = useState<IUser | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalMode, setModalMode] = useState<"create" | "edit">("create");
const [selectedDepartment, setSelectedDepartment] = useState("");
const [selectedStatus, setSelectedStatus] = useState("");
const departments = [
"IT & Administration",
"Administration",
"Cardiology",
"Emergency",
"Finance",
"Pharmacy",
"Laboratory",
"Radiology",
"Surgery",
"Pediatrics",
];
const [statusInput, setStatusInput] = useState("");
const [appliedSearch, setAppliedSearch] = useState("");
const [appliedStatus, setAppliedStatus] = useState("");
const [hasFiltered, setHasFiltered] = useState(false);
const filteredUsers = users.filter((user) => {
const matchesSearch =
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.department.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.role.name.toLowerCase().includes(searchTerm.toLowerCase());
const matchesDepartment =
!selectedDepartment || user.department === selectedDepartment;
const matchesSearch = !hasFiltered
? true
: !appliedSearch ||
user.name.toLowerCase().includes(appliedSearch.toLowerCase()) ||
user.email.toLowerCase().includes(appliedSearch.toLowerCase()) ||
user.role.name.toLowerCase().includes(appliedSearch.toLowerCase());
const statusToUse = hasFiltered ? appliedStatus : "";
const matchesStatus =
!selectedStatus ||
(selectedStatus === "active" && user.isActive) ||
(selectedStatus === "inactive" && !user.isActive);
!statusToUse ||
(statusToUse === "active" && user.isActive) ||
(statusToUse === "inactive" && !user.isActive);
return matchesSearch && matchesDepartment && matchesStatus;
return matchesSearch && matchesStatus;
});
const handleCreateUser = () => {
@@ -121,10 +105,6 @@ export default function UserManagement() {
</div>
</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
onClick={handleCreateUser}
className="btn-primary flex items-center space-x-2"
@@ -136,98 +116,27 @@ export default function UserManagement() {
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div className="card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Users</p>
<p className="text-2xl font-bold text-gray-900">
{users.length}
</p>
</div>
<Users className="h-8 w-8 text-blue-500" />
</div>
</div>
<div className="card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Active Users
</p>
<p className="text-2xl font-bold text-green-600">
{users.filter((u) => u.isActive).length}
</p>
</div>
<UserCheck className="h-8 w-8 text-green-500" />
</div>
</div>
<div className="card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Inactive Users
</p>
<p className="text-2xl font-bold text-red-600">
{users.filter((u) => !u.isActive).length}
</p>
</div>
<XCircle className="h-8 w-8 text-red-500" />
</div>
</div>
<div className="card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Online Today
</p>
<p className="text-2xl font-bold text-purple-600">
{
users.filter((u) => {
if (!u.lastLogin) return false;
const lastLogin = new Date(u.lastLogin);
const today = new Date();
return lastLogin.toDateString() === today.toDateString();
}).length
}
</p>
</div>
<Clock className="h-8 w-8 text-purple-500" />
</div>
</div>
</div>
{/* Filters and Search */}
{/* Filters */}
<div className="bg-white p-6 rounded-lg shadow-sm border mb-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-3">
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Cari nama, email, department, atau role..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Cari nama, email, atau role..."
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
className="pl-9 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent w-80"
/>
</div>
{/* Status */}
<div className="flex items-center space-x-2">
<Filter className="h-4 w-4 text-gray-400" />
<select
value={selectedDepartment}
onChange={(e) => setSelectedDepartment(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">Semua Department</option>
{departments.map((dept) => (
<option key={dept} value={dept}>
{dept}
</option>
))}
</select>
<select
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
value={statusInput}
onChange={(e) => setStatusInput(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">Semua Status</option>
@@ -235,6 +144,31 @@ export default function UserManagement() {
<option value="inactive">Tidak Aktif</option>
</select>
</div>
{/* Buttons */}
<div className="flex items-center space-x-2">
<button
onClick={() => {
setAppliedSearch(searchInput.trim());
setAppliedStatus(statusInput);
setHasFiltered(true);
}}
className="btn-primary px-3 py-2"
>
Filter
</button>
<button
onClick={() => {
setSearchInput("");
setStatusInput("");
setAppliedSearch("");
setAppliedStatus("");
setHasFiltered(false);
}}
className="btn-secondary px-3 py-2 border border-gray-300 rounded-md"
>
Reset
</button>
</div>
</div>
</div>
</div>
@@ -251,9 +185,6 @@ export default function UserManagement() {
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Role
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Department
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
@@ -270,13 +201,6 @@ export default function UserManagement() {
<tr key={user.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="h-10 w-10 rounded-full bg-gradient-to-r from-blue-500 to-purple-600 flex items-center justify-center text-white font-medium">
{user.name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()}
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
{user.name}
@@ -305,14 +229,6 @@ export default function UserManagement() {
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<Building2 className="h-4 w-4 text-gray-400 mr-2" />
<span className="text-sm text-gray-900">
{user.department}
</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<button
onClick={() => handleToggleStatus(user.id)}
@@ -428,23 +344,6 @@ export default function UserManagement() {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Department
</label>
<select
className="input w-full"
defaultValue={selectedUser?.department || ""}
>
<option value="">Pilih Department</option>
{departments.map((dept) => (
<option key={dept} value={dept}>
{dept}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Role