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

59
src/App.tsx Normal file
View File

@@ -0,0 +1,59 @@
import {
BrowserRouter as Router,
Routes,
Route,
Navigate,
} 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";
import MedicalRecordSync from "./pages/MedicalRecordSync";
import UserManagement from "./pages/UserManagement";
import RoleManagement from "./pages/RoleManagement";
import NotFound from "./pages/NotFound";
import NotFoundProtected from "./pages/NotFoundProtected";
import Layout from "./components/Layout";
import ProtectedRoute from "./components/ProtectedRoute";
function App() {
return (
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/"
element={
<ProtectedRoute>
<Layout />
</ProtectedRoute>
}
>
<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/bpjs-code" element={<BPJSCode />} />
<Route path="integration/bpjs" element={<BPJSSync />} />
<Route
path="integration/medical-record"
element={<MedicalRecordSync />}
/>
<Route path="admin/users" element={<UserManagement />} />
<Route path="admin/roles" element={<RoleManagement />} />
<Route index element={<Navigate to="/dashboard" replace />} />
{/* Protected 404 - untuk route di dalam layout sidebar */}
<Route path="*" element={<NotFoundProtected />} />
</Route>
{/* Public 404 - untuk route umum yang tidak dilindungi */}
<Route path="*" element={<NotFound />} />
</Routes>
</Router>
);
}
export default App;

BIN
src/assets/claim-guard.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

94
src/components/Layout.tsx Normal file
View File

@@ -0,0 +1,94 @@
import { useState } from "react";
import { Outlet } from "react-router-dom";
import Sidebar from "./Sidebar";
import { Bell, Menu } from "lucide-react";
export default function Layout() {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const toggleSidebar = () => {
setSidebarCollapsed(!sidebarCollapsed);
};
const toggleMobileMenu = () => {
setMobileMenuOpen(!mobileMenuOpen);
};
return (
<div className="flex h-screen bg-gray-50">
{/* Desktop Sidebar */}
<div className="hidden lg:block">
<Sidebar
isCollapsed={sidebarCollapsed}
onToggleCollapse={toggleSidebar}
/>
</div>
{/* Mobile Sidebar Overlay */}
{mobileMenuOpen && (
<div className="lg:hidden">
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40"
onClick={toggleMobileMenu}
></div>
<div className="fixed inset-y-0 left-0 z-50">
<Sidebar />
</div>
</div>
)}
{/* Main Content */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Top Header */}
<header className="bg-white border-b border-gray-200 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center">
{/* Mobile Menu Button */}
<button
onClick={toggleMobileMenu}
className="lg:hidden p-2 rounded-lg text-gray-500 hover:bg-gray-100 mr-3"
>
<Menu className="h-5 w-5" />
</button>
{/* Page Title Area - Can be expanded later if needed */}
<div className="text-lg font-semibold text-gray-900">
ClaimGuard Hospital Management
</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>
</header>
{/* Page Content */}
<main className="flex-1 overflow-auto">
<Outlet />
</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,15 @@
import { Navigate } from "react-router-dom";
interface ProtectedRouteProps {
children: React.ReactNode;
}
export default function ProtectedRoute({ children }: ProtectedRouteProps) {
const isAuthenticated = localStorage.getItem("isAuthenticated") === "true";
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
}

308
src/components/Sidebar.tsx Normal file
View File

@@ -0,0 +1,308 @@
import { useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import {
LayoutDashboard,
Users,
FileText,
LogOut,
ChevronLeft,
ChevronRight,
UserCog,
Lock,
Database,
Shield,
TrendingUp,
} from "lucide-react";
import { clsx } from "clsx";
import claimGuardLogo from "../assets/claim-guard.jpeg";
interface SidebarProps {
isCollapsed?: boolean;
onToggleCollapse?: () => void;
}
export default function Sidebar({
isCollapsed = false,
onToggleCollapse,
}: SidebarProps) {
const location = useLocation();
const navigate = useNavigate();
const handleLogout = () => {
localStorage.removeItem("isAuthenticated");
navigate("/login");
};
const menuItems = [
{
title: "Dashboard",
icon: LayoutDashboard,
path: "/dashboard",
color: "text-blue-600",
},
{
title: "Cost Recommendation",
icon: TrendingUp,
path: "/cost-recommendation",
color: "text-green-600",
},
{
title: "Integrasi Data",
icon: Database,
color: "text-purple-600",
submenu: [
{ title: "BPJS", icon: Shield, path: "/integration/bpjs" },
{
title: "Medical Record",
icon: FileText,
path: "/integration/medical-record",
},
],
},
{
title: "Pasien",
icon: Users,
color: "text-orange-600",
submenu: [
{ title: "Manajemen Pasien", icon: Users, path: "/patients" },
{
title: "Medical Record Pasien",
icon: FileText,
path: "/medical-record",
},
{ title: "BPJS Code", icon: Shield, path: "/patients/bpjs-code" },
],
},
{
title: "System Administration",
icon: UserCog,
color: "text-red-700",
submenu: [
{
title: "Manajemen User",
icon: Users,
path: "/admin/users",
},
{
title: "Manajemen Role",
icon: Lock,
path: "/admin/roles",
},
],
},
];
const [expandedMenu, setExpandedMenu] = useState<string | null>(null);
const toggleSubmenu = (title: string) => {
setExpandedMenu(expandedMenu === title ? null : title);
};
const isActive = (path: string) => location.pathname === path;
return (
<div
className={clsx(
"bg-white border-r border-gray-200 h-screen flex flex-col transition-all duration-300 ease-in-out",
isCollapsed ? "w-16" : "w-64"
)}
>
{/* Header */}
<div
className={clsx(
"border-b border-gray-200",
isCollapsed ? "p-2" : "p-4"
)}
>
<div className="flex items-center justify-between w-full">
{!isCollapsed ? (
<>
<div className="flex items-center">
<div className="bg-white p-1 rounded-lg mr-3 border border-gray-200">
<img
src={claimGuardLogo}
alt="ClaimGuard Logo"
className="h-8 w-8 object-cover rounded-md"
/>
</div>
<div>
<h1 className="text-lg font-bold text-gray-900">
ClaimGuard
</h1>
<p className="text-xs text-gray-500">Hospital Management</p>
</div>
</div>
{onToggleCollapse && (
<button
onClick={onToggleCollapse}
className="p-1 hover:bg-gray-100 rounded-md transition-colors"
title="Collapse Sidebar"
>
<ChevronLeft className="h-4 w-4 text-gray-500" />
</button>
)}
</>
) : (
<div className="w-full flex justify-center">
<button
onClick={onToggleCollapse}
className="bg-white p-2 rounded-lg hover:bg-gray-50 transition-colors border border-gray-200 shadow-sm"
title="ClaimGuard - Expand Sidebar"
>
<img
src={claimGuardLogo}
alt="ClaimGuard Logo"
className="h-6 w-6 object-cover rounded-sm"
/>
</button>
</div>
)}
</div>
</div>
{/* Navigation */}
<nav className="flex-1 overflow-y-auto py-4">
<div className="px-3 space-y-1">
{menuItems.map((item) => (
<div key={item.title}>
{item.submenu ? (
<div>
<button
onClick={() => {
if (isCollapsed && onToggleCollapse) {
onToggleCollapse();
} else {
toggleSubmenu(item.title);
}
}}
title={
isCollapsed
? `Klik untuk membuka ${item.title}`
: item.title
}
className={clsx(
"w-full flex items-center py-2 rounded-lg text-sm font-medium transition-colors",
"hover:bg-gray-50 text-gray-700",
isCollapsed ? "px-2 justify-center" : "px-3"
)}
>
<div className="relative">
<item.icon
className={clsx(
"h-5 w-5",
item.color,
isCollapsed ? "" : "mr-3"
)}
/>
{isCollapsed && (
<div className="absolute -bottom-1 -right-1 w-2 h-2 bg-blue-500 rounded-full opacity-60"></div>
)}
</div>
{!isCollapsed && (
<>
<span className="flex-1 text-left">{item.title}</span>
<ChevronRight
className={clsx(
"h-4 w-4 transition-transform",
expandedMenu === item.title && "transform rotate-90"
)}
/>
</>
)}
</button>
{!isCollapsed && expandedMenu === item.title && (
<div className="ml-6 mt-1 space-y-1">
{item.submenu.map((subItem) => (
<Link
key={subItem.path}
to={subItem.path}
className={clsx(
"flex items-center px-3 py-2 rounded-lg text-sm transition-colors",
isActive(subItem.path)
? "bg-green-50 text-green-700 border-r-2 border-green-600"
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
)}
>
<subItem.icon className="h-4 w-4 mr-3" />
<span>{subItem.title}</span>
</Link>
))}
</div>
)}
</div>
) : (
<Link
to={item.path}
title={isCollapsed ? item.title : undefined}
className={clsx(
"flex items-center py-2 rounded-lg text-sm font-medium transition-colors",
isActive(item.path)
? "bg-green-50 text-green-700 border-r-2 border-green-600"
: "text-gray-700 hover:bg-gray-50",
isCollapsed ? "px-2 justify-center" : "px-3"
)}
>
<item.icon
className={clsx(
"h-5 w-5",
item.color,
isCollapsed ? "" : "mr-3"
)}
/>
{!isCollapsed && <span>{item.title}</span>}
</Link>
)}
</div>
))}
</div>
{/* Divider */}
<div className="my-4 px-3">
<div className="border-t border-gray-200"></div>
</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>
</div>
);
}

44
src/index.css Normal file
View File

@@ -0,0 +1,44 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
* {
@apply border-gray-200;
}
body {
@apply bg-white text-gray-900 font-sans;
font-family: "Inter", system-ui, -apple-system, sans-serif;
}
}
@layer components {
.card {
@apply rounded-lg border border-gray-200 bg-white text-gray-900 shadow-sm;
}
.btn {
@apply inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-500 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-white;
}
.btn-primary {
@apply btn bg-green-600 text-white hover:bg-green-700 h-10 px-4 py-2;
}
.btn-secondary {
@apply btn bg-gray-100 text-gray-900 hover:bg-gray-200 h-10 px-4 py-2;
}
.input {
@apply flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
}
.label {
@apply text-sm font-medium leading-none text-gray-700;
}
}

10
src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);

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

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

461
src/pages/BPJSSync.tsx Normal file
View File

@@ -0,0 +1,461 @@
import { useState } from "react";
import {
Shield,
Calendar,
Search,
Filter,
AlertCircle,
CheckCircle,
Clock,
RefreshCw,
Upload,
Database,
Building2,
} from "lucide-react";
interface BPJSSyncLog {
id: string;
timestamp: string;
type: "import" | "sync";
status: "success" | "failed" | "in_progress";
claimsProcessed: number;
claimsSuccess: number;
claimsFailed: number;
source: string;
duration: number;
errorMessage?: string;
}
interface BPJSSyncStats {
totalSyncs: number;
successfulSyncs: number;
failedSyncs: number;
lastSyncTime: string;
totalClaimsProcessed: number;
averageDuration: number;
}
export default function BPJSSync() {
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [isImporting, setIsImporting] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
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",
});
};
// Sample BPJS sync logs data
const [syncLogs] = useState<BPJSSyncLog[]>([
{
id: "1",
timestamp: "2024-01-15T14:30:00Z",
type: "sync",
status: "success",
claimsProcessed: 234,
claimsSuccess: 230,
claimsFailed: 4,
source: "BPJS Kesehatan API",
duration: 32,
},
{
id: "2",
timestamp: "2024-01-15T10:15:00Z",
type: "import",
status: "success",
claimsProcessed: 89,
claimsSuccess: 89,
claimsFailed: 0,
source: "Hospital Billing System",
duration: 15,
},
{
id: "3",
timestamp: "2024-01-14T16:45:00Z",
type: "sync",
status: "failed",
claimsProcessed: 0,
claimsSuccess: 0,
claimsFailed: 0,
source: "BPJS Kesehatan API",
duration: 0,
errorMessage: "API rate limit exceeded",
},
{
id: "4",
timestamp: "2024-01-14T09:30:00Z",
type: "import",
status: "success",
claimsProcessed: 156,
claimsSuccess: 150,
claimsFailed: 6,
source: "External Claims System",
duration: 28,
},
{
id: "5",
timestamp: "2024-01-13T13:20:00Z",
type: "sync",
status: "in_progress",
claimsProcessed: 45,
claimsSuccess: 45,
claimsFailed: 0,
source: "BPJS Kesehatan API",
duration: 0,
},
]);
// Calculate statistics
const stats: BPJSSyncStats = {
totalSyncs: syncLogs.length,
successfulSyncs: syncLogs.filter((log) => log.status === "success").length,
failedSyncs: syncLogs.filter((log) => log.status === "failed").length,
lastSyncTime: syncLogs[0]?.timestamp || "",
totalClaimsProcessed: syncLogs.reduce(
(sum, log) => sum + log.claimsProcessed,
0
),
averageDuration:
syncLogs
.filter((log) => log.duration > 0)
.reduce((sum, log) => sum + log.duration, 0) /
syncLogs.filter((log) => log.duration > 0).length || 0,
};
// Filter sync logs based on search and status
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 = statusFilter === "all" || log.status === statusFilter;
return matchesSearch && matchesStatus;
});
const handleImport = async () => {
setIsImporting(true);
// Simulate import process
setTimeout(() => {
setIsImporting(false);
// Add new log entry here
}, 3000);
};
const handleSync = async () => {
setIsSyncing(true);
// Simulate sync process
setTimeout(() => {
setIsSyncing(false);
// Add new log entry here
}, 5000);
};
return (
<div className="p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center">
<Database className="h-8 w-8 text-blue-600 mr-3" />
BPJS Sync
</h1>
<p className="text-gray-600 mt-1">
Sinkronisasi dan integrasi data klaim BPJS dari sistem eksternal
</p>
</div>
<div className="flex space-x-3">
<button
onClick={handleImport}
disabled={isImporting || isSyncing}
className="btn-secondary flex items-center space-x-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isImporting ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
<Upload className="h-4 w-4" />
)}
<span>{isImporting ? "Importing..." : "Import Data"}</span>
</button>
<button
onClick={handleSync}
disabled={isImporting || isSyncing}
className="btn-primary flex items-center space-x-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSyncing ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
<span>{isSyncing ? "Syncing..." : "Sync by API"}</span>
</button>
</div>
</div>
</div>
{/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Sync</p>
<p className="text-2xl font-bold text-blue-600">
{stats.totalSyncs}
</p>
</div>
<div className="p-3 bg-blue-100 rounded-lg">
<RefreshCw className="h-6 w-6 text-blue-600" />
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Berhasil</p>
<p className="text-2xl font-bold text-green-600">
{stats.successfulSyncs}
</p>
</div>
<div className="p-3 bg-green-100 rounded-lg">
<CheckCircle className="h-6 w-6 text-green-600" />
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Total Claims
</p>
<p className="text-2xl font-bold text-purple-600">
{stats.totalClaimsProcessed.toLocaleString()}
</p>
</div>
<div className="p-3 bg-purple-100 rounded-lg">
<Shield className="h-6 w-6 text-purple-600" />
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Avg Duration
</p>
<p className="text-2xl font-bold text-orange-600">
{Math.round(stats.averageDuration)}s
</p>
</div>
<div className="p-3 bg-orange-100 rounded-lg">
<Clock className="h-6 w-6 text-orange-600" />
</div>
</div>
</div>
</div>
{/* Filters and Search */}
<div className="bg-white p-6 rounded-lg shadow-sm border mb-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
<div className="flex items-center space-x-4">
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Cari 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"
/>
</div>
<div className="flex items-center space-x-2">
<Filter className="h-4 w-4 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Semua Status</option>
<option value="success">Berhasil</option>
<option value="failed">Gagal</option>
<option value="in_progress">Berlangsung</option>
</select>
</div>
</div>
</div>
</div>
{/* Sync Logs Table */}
<div className="bg-white rounded-lg shadow-sm border overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">
Log Sinkronisasi BPJS
</h3>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Waktu & Type
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Source System
</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">
Claims Processed
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Success Rate
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Duration
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredLogs.map((log) => (
<tr key={log.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="flex items-center text-sm text-gray-500">
<Calendar className="h-4 w-4 mr-1" />
{formatDate(log.timestamp)}
</div>
<div className="text-sm font-medium text-gray-900 mt-1">
<span
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
log.type === "import"
? "bg-blue-100 text-blue-800"
: "bg-purple-100 text-purple-800"
}`}
>
{log.type === "import" ? "Import" : "Sync"}
</span>
</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" />
<div>
<div className="text-sm font-medium text-gray-900">
{log.source}
</div>
{log.errorMessage && (
<div className="text-sm text-red-600 mt-1">
{log.errorMessage}
</div>
)}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<span
className={`inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full ${
log.status === "success"
? "bg-green-100 text-green-800"
: log.status === "failed"
? "bg-red-100 text-red-800"
: "bg-blue-100 text-blue-800"
}`}
>
{log.status === "success" ? (
<CheckCircle className="h-4 w-4 mr-1" />
) : log.status === "failed" ? (
<AlertCircle className="h-4 w-4 mr-1" />
) : (
<Clock className="h-4 w-4 mr-1" />
)}
{log.status === "success"
? "Berhasil"
: log.status === "failed"
? "Gagal"
: "Berlangsung"}
</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{log.claimsProcessed.toLocaleString()}
</div>
<div className="text-sm text-gray-500">
{log.claimsSuccess > 0 && (
<span className="text-green-600">
{log.claimsSuccess}
</span>
)}
{log.claimsFailed > 0 && (
<span className="text-red-600 ml-2">
{log.claimsFailed}
</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{log.claimsProcessed > 0
? Math.round(
(log.claimsSuccess / log.claimsProcessed) * 100
)
: 0}
%
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-green-600 h-2 rounded-full"
style={{
width:
log.claimsProcessed > 0
? `${
(log.claimsSuccess / log.claimsProcessed) *
100
}%`
: "0%",
}}
></div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{log.duration > 0 ? `${log.duration}s` : "-"}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Empty State */}
{filteredLogs.length === 0 && (
<div className="text-center py-12">
<AlertCircle className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">
Tidak ada log sinkronisasi ditemukan
</h3>
<p className="mt-1 text-sm text-gray-500">
Tidak ada log yang sesuai dengan kriteria pencarian.
</p>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,681 @@
import { useState } from "react";
import {
TrendingUp,
DollarSign,
Calendar,
Search,
Filter,
Eye,
Download,
AlertCircle,
User,
FileText,
Calculator,
Target,
Code,
Stethoscope,
} from "lucide-react";
interface CostRecommendation {
id: string;
patientId: string;
patientName: string;
age: number;
gender: string;
diagnosis: string;
icdCode: string;
procedureCode?: string;
medicalRecordNumber: string;
admissionDate: string;
dischargeDate?: string;
roomType: string;
currentTreatment: string;
estimatedCost: number;
recommendedTreatment: string;
recommendedCost: number;
potentialSavings: number;
riskLevel: "low" | "medium" | "high";
confidence: number;
department: string;
doctor: string;
createdDate: string;
status: "pending" | "approved" | "rejected" | "implemented";
medicalHistory?: string;
allergies?: string;
currentMedications?: string;
vitalSigns?: {
bloodPressure: string;
heartRate: number;
temperature: number;
oxygenSaturation: number;
};
labResults?: string;
notes?: string;
}
interface CostStats {
totalRecommendations: number;
potentialSavings: number;
implementedSavings: number;
avgSavingsPerCase: number;
approvalRate: number;
}
export default function CostRecommendation() {
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [riskFilter, setRiskFilter] = useState("all");
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString("id-ID", {
day: "numeric",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
}).format(amount);
};
const getRiskColor = (risk: string) => {
switch (risk) {
case "low":
return "bg-green-100 text-green-800";
case "medium":
return "bg-yellow-100 text-yellow-800";
case "high":
return "bg-red-100 text-red-800";
default:
return "bg-gray-100 text-gray-800";
}
};
const getStatusColor = (status: string) => {
switch (status) {
case "approved":
return "bg-green-100 text-green-800";
case "pending":
return "bg-yellow-100 text-yellow-800";
case "rejected":
return "bg-red-100 text-red-800";
case "implemented":
return "bg-blue-100 text-blue-800";
default:
return "bg-gray-100 text-gray-800";
}
};
const getStatusText = (status: string) => {
switch (status) {
case "approved":
return "Disetujui";
case "pending":
return "Menunggu";
case "rejected":
return "Ditolak";
case "implemented":
return "Diterapkan";
default:
return status;
}
};
const getRiskText = (risk: string) => {
switch (risk) {
case "low":
return "Rendah";
case "medium":
return "Sedang";
case "high":
return "Tinggi";
default:
return risk;
}
};
// Sample cost recommendations data
const [recommendations] = useState<CostRecommendation[]>([
{
id: "1",
patientId: "P001",
patientName: "Ahmad Santoso",
age: 45,
gender: "Laki-laki",
diagnosis: "Hipertensi Esensial",
icdCode: "I10",
procedureCode: "99213",
medicalRecordNumber: "MR2024001",
admissionDate: "2024-01-10T08:00:00Z",
dischargeDate: "2024-01-11T16:00:00Z",
roomType: "Ruang Rawat Jalan",
currentTreatment: "Amlodipine 10mg + Monitoring Harian",
estimatedCost: 2500000,
recommendedTreatment: "Amlodipine 5mg + Monitoring Mingguan",
recommendedCost: 1800000,
potentialSavings: 700000,
riskLevel: "low",
confidence: 92,
department: "Poli Dalam",
doctor: "Dr. Sarah Wijaya",
createdDate: "2024-01-15T14:30:00Z",
status: "pending",
medicalHistory: "Riwayat hipertensi keluarga, tidak ada riwayat stroke",
allergies: "Tidak ada alergi obat yang diketahui",
currentMedications: "Amlodipine 10mg 1x1, Aspirin 80mg 1x1",
vitalSigns: {
bloodPressure: "140/90",
heartRate: 78,
temperature: 36.5,
oxygenSaturation: 98,
},
labResults: "Kolesterol Total: 220 mg/dl, HDL: 45 mg/dl, LDL: 140 mg/dl",
notes: "Pasien menunjukkan respons baik dengan dosis rendah",
},
{
id: "2",
patientId: "P002",
patientName: "Siti Nurhaliza",
age: 52,
gender: "Perempuan",
diagnosis: "Diabetes Mellitus Tipe 2",
icdCode: "E11",
procedureCode: "99214",
medicalRecordNumber: "MR2024002",
admissionDate: "2024-01-12T09:30:00Z",
dischargeDate: "2024-01-14T14:00:00Z",
roomType: "Ruang Rawat Inap Kelas II",
currentTreatment: "Insulin + Metformin + Konsultasi Harian",
estimatedCost: 4200000,
recommendedTreatment: "Metformin + Diet Program + Konsultasi Mingguan",
recommendedCost: 2800000,
potentialSavings: 1400000,
riskLevel: "medium",
confidence: 87,
department: "Poli Endokrin",
doctor: "Dr. Rahman Hidayat",
createdDate: "2024-01-15T10:15:00Z",
status: "approved",
medicalHistory: "DM Tipe 2 sejak 5 tahun, riwayat hipertensi",
allergies: "Alergi sulfa",
currentMedications: "Insulin Lantus 20 unit, Metformin 500mg 2x1",
vitalSigns: {
bloodPressure: "130/85",
heartRate: 82,
temperature: 36.8,
oxygenSaturation: 96,
},
labResults: "HbA1c: 8.2%, Glukosa Puasa: 180 mg/dl, Kreatinin: 1.1 mg/dl",
notes: "Pasien cocok dengan program diet terstruktur",
},
{
id: "3",
patientId: "P003",
patientName: "Budi Prasetyo",
age: 38,
gender: "Laki-laki",
diagnosis: "Pneumonia",
icdCode: "J18.9",
procedureCode: "99223",
medicalRecordNumber: "MR2024003",
admissionDate: "2024-01-11T15:00:00Z",
dischargeDate: "2024-01-13T10:30:00Z",
roomType: "Ruang Rawat Inap Kelas I",
currentTreatment: "Antibiotik IV + Rawat Inap 7 hari",
estimatedCost: 8500000,
recommendedTreatment: "Antibiotik Oral + Rawat Jalan",
recommendedCost: 3200000,
potentialSavings: 5300000,
riskLevel: "high",
confidence: 78,
department: "Poli Paru",
doctor: "Dr. Linda Sari",
createdDate: "2024-01-14T16:45:00Z",
status: "implemented",
medicalHistory: "Riwayat merokok 15 tahun, berhenti 2 tahun lalu",
allergies: "Alergi penisilin",
currentMedications: "Ceftriaxone 2g IV, Azithromycin 500mg",
vitalSigns: {
bloodPressure: "120/80",
heartRate: 92,
temperature: 38.2,
oxygenSaturation: 94,
},
labResults: "Leukosit: 12,000/μL, CRP: 15 mg/L, Procalcitonin: 0.8 ng/mL",
notes: "Monitoring ketat diperlukan untuk rawat jalan",
},
{
id: "4",
patientId: "P004",
patientName: "Maria Lopez",
age: 29,
gender: "Perempuan",
diagnosis: "Gastritis Akut",
icdCode: "K29.1",
procedureCode: "99212",
medicalRecordNumber: "MR2024004",
admissionDate: "2024-01-13T08:15:00Z",
roomType: "Ruang Rawat Jalan",
currentTreatment: "PPI + Antasida + Konsultasi Harian",
estimatedCost: 1500000,
recommendedTreatment: "H2 Blocker + Diet + Konsultasi Mingguan",
recommendedCost: 800000,
potentialSavings: 700000,
riskLevel: "low",
confidence: 94,
department: "Poli Dalam",
doctor: "Dr. Sarah Wijaya",
createdDate: "2024-01-13T11:20:00Z",
status: "rejected",
medicalHistory: "Tidak ada riwayat penyakit kronis",
allergies: "Alergi H2 blocker (ranitidin)",
currentMedications: "Omeprazole 20mg 1x1, Antasida 3x1",
vitalSigns: {
bloodPressure: "110/70",
heartRate: 75,
temperature: 36.4,
oxygenSaturation: 99,
},
labResults: "Hemoglobin: 12.5 g/dl, H. pylori: Negatif",
notes: "Pasien memiliki riwayat alergi terhadap H2 blocker",
},
{
id: "5",
patientId: "P005",
patientName: "Andi Kusuma",
age: 67,
gender: "Laki-laki",
diagnosis: "Osteoarthritis",
icdCode: "M15.9",
procedureCode: "99215",
medicalRecordNumber: "MR2024005",
admissionDate: "2024-01-12T07:45:00Z",
roomType: "Ruang Rawat Jalan",
currentTreatment: "NSAID + Fisioterapi Intensif",
estimatedCost: 3200000,
recommendedTreatment: "Paracetamol + Fisioterapi Standar + Olahraga",
recommendedCost: 1900000,
potentialSavings: 1300000,
riskLevel: "low",
confidence: 89,
department: "Ortopedi",
doctor: "Dr. Kevin Tan",
createdDate: "2024-01-12T09:30:00Z",
status: "approved",
medicalHistory: "Osteoarthritis bilateral knee sejak 10 tahun",
allergies: "Tidak ada alergi obat yang diketahui",
currentMedications: "Diclofenac 50mg 2x1, Glucosamine 500mg 2x1",
vitalSigns: {
bloodPressure: "135/85",
heartRate: 68,
temperature: 36.3,
oxygenSaturation: 97,
},
labResults: "Fungsi ginjal normal, tidak ada tanda inflamasi sistemik",
notes: "Pasien menunjukkan respons baik dengan terapi konservatif",
},
]);
// Calculate statistics
const stats: CostStats = {
totalRecommendations: recommendations.length,
potentialSavings: recommendations.reduce(
(sum, r) => sum + r.potentialSavings,
0
),
implementedSavings: recommendations
.filter((r) => r.status === "implemented")
.reduce((sum, r) => sum + r.potentialSavings, 0),
avgSavingsPerCase:
recommendations.reduce((sum, r) => sum + r.potentialSavings, 0) /
recommendations.length,
approvalRate:
(recommendations.filter(
(r) => r.status === "approved" || r.status === "implemented"
).length /
recommendations.length) *
100,
};
// Filter recommendations
const filteredRecommendations = recommendations.filter((rec) => {
const matchesSearch =
rec.patientName.toLowerCase().includes(searchTerm.toLowerCase()) ||
rec.diagnosis.toLowerCase().includes(searchTerm.toLowerCase()) ||
rec.icdCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
rec.doctor.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === "all" || rec.status === statusFilter;
const matchesRisk = riskFilter === "all" || rec.riskLevel === riskFilter;
return matchesSearch && matchesStatus && matchesRisk;
});
return (
<div className="p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center">
<TrendingUp className="h-8 w-8 text-green-600 mr-3" />
Cost Recommendation
</h1>
<p className="text-gray-600 mt-1">
Rekomendasi optimalisasi biaya perawatan berdasarkan analisis
medis
</p>
</div>
<div className="flex space-x-3">
<button className="btn-secondary flex items-center space-x-2">
<Download className="h-4 w-4" />
<span>Export Report</span>
</button>
<button className="btn-primary flex items-center space-x-2">
<Calculator className="h-4 w-4" />
<span>Analisis Baru</span>
</button>
</div>
</div>
</div>
{/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Total Rekomendasi
</p>
<p className="text-2xl font-bold text-blue-600">
{stats.totalRecommendations}
</p>
</div>
<div className="p-3 bg-blue-100 rounded-lg">
<FileText className="h-6 w-6 text-blue-600" />
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Potensi Penghematan
</p>
<p className="text-2xl font-bold text-green-600">
{formatCurrency(stats.potentialSavings)}
</p>
</div>
<div className="p-3 bg-green-100 rounded-lg">
<TrendingUp className="h-6 w-6 text-green-600" />
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Penghematan Terealisasi
</p>
<p className="text-2xl font-bold text-purple-600">
{formatCurrency(stats.implementedSavings)}
</p>
</div>
<div className="p-3 bg-purple-100 rounded-lg">
<DollarSign className="h-6 w-6 text-purple-600" />
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Tingkat Persetujuan
</p>
<p className="text-2xl font-bold text-orange-600">
{Math.round(stats.approvalRate)}%
</p>
</div>
<div className="p-3 bg-orange-100 rounded-lg">
<Target className="h-6 w-6 text-orange-600" />
</div>
</div>
</div>
</div>
{/* Filters and Search */}
<div className="bg-white p-6 rounded-lg shadow-sm border mb-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
<div className="flex items-center space-x-4">
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Cari pasien, diagnosis, atau dokter..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent w-80"
/>
</div>
<div className="flex items-center space-x-2">
<Filter className="h-4 w-4 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-green-500 focus:border-transparent"
>
<option value="all">Semua Status</option>
<option value="pending">Menunggu</option>
<option value="approved">Disetujui</option>
<option value="rejected">Ditolak</option>
<option value="implemented">Diterapkan</option>
</select>
<select
value={riskFilter}
onChange={(e) => setRiskFilter(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-green-500 focus:border-transparent"
>
<option value="all">Semua Risiko</option>
<option value="low">Risiko Rendah</option>
<option value="medium">Risiko Sedang</option>
<option value="high">Risiko Tinggi</option>
</select>
</div>
</div>
</div>
</div>
{/* Recommendations Table */}
<div className="bg-white rounded-lg shadow-sm border overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">
Rekomendasi Optimalisasi Biaya
</h3>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Pasien & Medical Record
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Medical Code
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Treatment Saat Ini
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Rekomendasi
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Potensi Penghematan
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status & Risiko
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tanggal
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Aksi
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredRecommendations.map((rec) => (
<tr key={rec.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<div className="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
<User className="h-5 w-5 text-gray-600" />
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
{rec.patientName}
</div>
<div className="text-sm text-gray-500">
{rec.diagnosis}
</div>
<div className="text-xs text-gray-400 flex items-center">
<FileText className="h-3 w-3 mr-1" />
MR: {rec.medicalRecordNumber}
</div>
<div className="text-xs text-gray-400">
{rec.age} th, {rec.gender} | {rec.roomType}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-1">
<div className="flex items-center text-sm font-medium text-gray-900">
<Code className="h-4 w-4 mr-2 text-blue-600" />
ICD: {rec.icdCode}
</div>
{rec.procedureCode && (
<div className="flex items-center text-sm text-gray-600">
<Stethoscope className="h-4 w-4 mr-2 text-green-600" />
CPT: {rec.procedureCode}
</div>
)}
<div className="text-xs text-gray-500">
Dept: {rec.department}
</div>
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900">
{rec.currentTreatment}
</div>
<div className="text-sm text-gray-500">
Estimasi: {formatCurrency(rec.estimatedCost)}
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900">
{rec.recommendedTreatment}
</div>
<div className="text-sm text-green-600">
Biaya: {formatCurrency(rec.recommendedCost)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-green-600">
{formatCurrency(rec.potentialSavings)}
</div>
<div className="text-xs text-gray-500">
{Math.round(
(rec.potentialSavings / rec.estimatedCost) * 100
)}
% penghematan
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex flex-col space-y-2">
<span
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(
rec.status
)}`}
>
{getStatusText(rec.status)}
</span>
<div className="flex items-center space-x-2">
<span
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getRiskColor(
rec.riskLevel
)}`}
>
{getRiskText(rec.riskLevel)}
</span>
<div className="text-xs text-gray-600">
{rec.confidence}%
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-1">
<div className="flex items-center text-sm text-gray-500">
<Calendar className="h-4 w-4 mr-1" />
{formatDate(rec.createdDate)}
</div>
<div className="text-xs text-gray-400">
Admission: {formatDate(rec.admissionDate)}
</div>
{rec.dischargeDate && (
<div className="text-xs text-gray-400">
Discharge: {formatDate(rec.dischargeDate)}
</div>
)}
<div className="text-xs text-gray-500">
Dr. {rec.doctor.replace("Dr. ", "")}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center space-x-2">
<button className="text-blue-600 hover:text-blue-900">
<Eye className="h-4 w-4" />
</button>
<button className="text-green-600 hover:text-green-900">
<Download className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Empty State */}
{filteredRecommendations.length === 0 && (
<div className="text-center py-12">
<AlertCircle className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">
Tidak ada rekomendasi ditemukan
</h3>
<p className="mt-1 text-sm text-gray-500">
Tidak ada rekomendasi yang sesuai dengan kriteria pencarian.
</p>
</div>
)}
</div>
</div>
);
}

339
src/pages/Dashboard.tsx Normal file
View File

@@ -0,0 +1,339 @@
import { Users, TrendingUp, AlertTriangle, Shield } from "lucide-react";
export default function Dashboard() {
const stats = [
{
title: "Pasien Datang",
value: "89",
change: "+12%",
changeColor: "text-green-600",
icon: Users,
color: "text-blue-600",
subtitle: "Hari ini",
},
{
title: "Potential Overclaim",
value: "7",
change: "+2",
changeColor: "text-red-600",
icon: AlertTriangle,
color: "text-red-600",
subtitle: "Perlu review",
},
{
title: "Rata-rata Biaya",
value: "Rp 2.8M",
change: "+5.2%",
changeColor: "text-orange-600",
icon: TrendingUp,
color: "text-orange-600",
subtitle: "Per klaim",
},
{
title: "Kode BPJS Aktif",
value: "156",
change: "8 kode baru",
changeColor: "text-green-600",
icon: Shield,
color: "text-purple-600",
subtitle: "Total kode",
},
];
return (
<div className="p-6">
<div className="max-w-7xl mx-auto">
{/* Welcome Section */}
<div className="mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-2">
Selamat Datang di Dashboard
</h2>
<p className="text-gray-600">
Kelola sistem rumah sakit dengan efisien dan pantau semua aktivitas
secara real-time.
</p>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{stats.map((stat, index) => (
<div key={index} className="card p-6">
<div className="flex items-center justify-between mb-4">
<div className={`p-2 rounded-lg bg-gray-100 ${stat.color}`}>
<stat.icon className="h-5 w-5" />
</div>
<span className={`text-sm font-medium ${stat.changeColor}`}>
{stat.change}
</span>
</div>
<div>
<p className="text-2xl font-bold text-gray-900 mb-1">
{stat.value}
</p>
<p className="text-sm font-semibold text-gray-700 mb-1">
{stat.title}
</p>
<p className="text-xs text-gray-500">{stat.subtitle}</p>
</div>
</div>
))}
</div>
{/* Main Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Tren Biaya Kode Medis */}
<div className="card p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-gray-900">
Tren Biaya Kode Medis
</h3>
<button className="text-sm text-primary hover:text-primary/80">
Lihat Detail
</button>
</div>
<div className="space-y-4">
{[
{
code: "A15.0",
name: "Tuberculosis Paru",
cost: "Rp 3.2M",
change: "+15%",
trend: "up",
count: "12 kasus",
},
{
code: "I21.9",
name: "Acute Myocardial Infarction",
cost: "Rp 8.5M",
change: "+3%",
trend: "up",
count: "4 kasus",
},
{
code: "N18.6",
name: "Chronic Kidney Disease",
cost: "Rp 5.1M",
change: "-8%",
trend: "down",
count: "7 kasus",
},
{
code: "O80.0",
name: "Spontaneous Delivery",
cost: "Rp 2.8M",
change: "+5%",
trend: "up",
count: "23 kasus",
},
].map((item, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div className="flex-1">
<div className="flex items-center space-x-3 mb-1">
<span className="font-mono text-sm font-bold text-gray-900 bg-white px-2 py-1 rounded">
{item.code}
</span>
<span
className={`text-xs font-medium ${
item.trend === "up"
? "text-green-600"
: "text-red-600"
}`}
>
{item.change}
</span>
</div>
<p className="text-sm font-medium text-gray-800 mb-1">
{item.name}
</p>
<p className="text-xs text-gray-500">
{item.count} {item.cost}
</p>
</div>
<div
className={`p-2 rounded-lg ${
item.trend === "up" ? "bg-green-100" : "bg-red-100"
}`}
>
<TrendingUp
className={`h-4 w-4 ${
item.trend === "up"
? "text-green-600"
: "text-red-600 transform rotate-180"
}`}
/>
</div>
</div>
))}
</div>
</div>
{/* Distribusi Kode BPJS */}
<div className="card p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-gray-900">
Distribusi Kode BPJS
</h3>
<button className="text-sm text-primary hover:text-primary/80">
Lihat Semua
</button>
</div>
<div className="space-y-4">
{[
{
category: "Rawat Inap",
codes: "89 kode",
percentage: "57%",
color: "bg-blue-500",
count: "234 klaim",
},
{
category: "Rawat Jalan",
codes: "45 kode",
percentage: "29%",
color: "bg-green-500",
count: "156 klaim",
},
{
category: "IGD",
codes: "22 kode",
percentage: "14%",
color: "bg-red-500",
count: "67 klaim",
},
].map((item, index) => (
<div key={index} className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div
className={`w-3 h-3 rounded-full ${item.color}`}
></div>
<span className="text-sm font-medium text-gray-900">
{item.category}
</span>
</div>
<span className="text-sm font-bold text-gray-900">
{item.percentage}
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${item.color}`}
style={{ width: item.percentage }}
></div>
</div>
<div className="flex justify-between text-xs text-gray-500">
<span>{item.codes}</span>
<span>{item.count}</span>
</div>
</div>
))}
</div>
<div className="mt-6 pt-4 border-t border-gray-200">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600">Total Kode Aktif:</span>
<span className="font-bold text-gray-900">156 kode</span>
</div>
<div className="flex items-center justify-between text-sm mt-1">
<span className="text-gray-600">Total Klaim Bulan Ini:</span>
<span className="font-bold text-gray-900">457 klaim</span>
</div>
</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>
);
}

318
src/pages/Login.tsx Normal file
View File

@@ -0,0 +1,318 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useForm } from "react-hook-form";
import {
Heart,
Shield,
Eye,
EyeOff,
Lock,
User,
Activity,
Stethoscope,
AlertCircle,
} from "lucide-react";
import { clsx } from "clsx";
interface LoginForm {
username: string;
password: string;
remember: boolean;
}
export default function Login() {
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const {
register,
handleSubmit,
formState: { errors },
setError,
} = useForm<LoginForm>({
defaultValues: {
username: "",
password: "",
remember: false,
},
});
const onSubmit = async (data: LoginForm) => {
setIsLoading(true);
try {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1500));
// Simple authentication check (replace with real API)
if (data.username === "admin" && data.password === "admin123") {
localStorage.setItem("isAuthenticated", "true");
navigate("/dashboard");
} else {
setError("root", {
message: "Username atau password tidak valid",
});
}
} catch {
setError("root", {
message: "Terjadi kesalahan saat login. Silakan coba lagi.",
});
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-green-50 flex">
{/* Left Side - Branding */}
<div className="hidden lg:flex lg:w-1/2 bg-gradient-to-br from-primary to-green-600 relative overflow-hidden">
<div className="absolute inset-0 bg-black/10"></div>
{/* Background Pattern */}
<div className="absolute inset-0 opacity-10">
<div className="absolute top-10 left-10">
<Heart className="h-16 w-16 text-white" />
</div>
<div className="absolute top-32 right-20">
<Stethoscope className="h-12 w-12 text-white" />
</div>
<div className="absolute bottom-32 left-20">
<Activity className="h-14 w-14 text-white" />
</div>
<div className="absolute bottom-10 right-10">
<Shield className="h-18 w-18 text-white" />
</div>
</div>
<div className="relative z-10 flex flex-col justify-center px-12 text-white">
<div className="mb-8">
<div className="flex items-center mb-6">
<div className="bg-white/20 p-3 rounded-lg mr-4">
<Heart className="h-8 w-8 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold">ClaimGuard</h1>
<p className="text-green-100">Hospital Management System</p>
</div>
</div>
</div>
<div className="space-y-6">
<h2 className="text-4xl font-bold leading-tight">
Sistem Manajemen
<br />
<span className="text-green-200">Rumah Sakit Terpadu</span>
</h2>
<p className="text-xl text-green-100 max-w-md">
Solusi digital untuk pengelolaan administrasi, klaim asuransi, dan
operasional rumah sakit yang efisien dan aman.
</p>
<div className="space-y-4">
<div className="flex items-center">
<Shield className="h-5 w-5 mr-3 text-green-200" />
<span>Keamanan Data Tingkat Enterprise</span>
</div>
<div className="flex items-center">
<Activity className="h-5 w-5 mr-3 text-green-200" />
<span>Monitoring Real-time</span>
</div>
<div className="flex items-center">
<Heart className="h-5 w-5 mr-3 text-green-200" />
<span>Fokus pada Pelayanan Pasien</span>
</div>
</div>
</div>
</div>
</div>
{/* Right Side - Login Form */}
<div className="w-full lg:w-1/2 flex items-center justify-center px-6 py-12">
<div className="w-full max-w-md">
{/* Mobile Header */}
<div className="lg:hidden text-center mb-8">
<div className="flex items-center justify-center mb-4">
<div className="bg-primary/10 p-3 rounded-lg mr-3">
<Heart className="h-8 w-8 text-primary" />
</div>
<div className="text-left">
<h1 className="text-2xl font-bold text-gray-900">ClaimGuard</h1>
<p className="text-sm text-gray-500">Hospital Management</p>
</div>
</div>
</div>
<div className="card p-8">
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Selamat Datang
</h2>
<p className="text-gray-600">
Silakan masuk ke akun Anda untuk mengakses sistem
</p>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* Username Field */}
<div className="space-y-2">
<label htmlFor="username" className="label">
Username atau Email
</label>
<div className="relative">
<User className="absolute left-3 top-3 h-4 w-4 text-gray-500" />
<input
id="username"
type="text"
{...register("username", {
required: "Username wajib diisi",
minLength: {
value: 3,
message: "Username minimal 3 karakter",
},
})}
className={clsx(
"input pl-10",
errors.username &&
"border-red-500 focus-visible:ring-red-500"
)}
placeholder="Masukkan username atau email"
/>
</div>
{errors.username && (
<p className="text-sm text-red-600 flex items-center">
<AlertCircle className="h-4 w-4 mr-1" />
{errors.username.message}
</p>
)}
</div>
{/* Password Field */}
<div className="space-y-2">
<label htmlFor="password" className="label">
Password
</label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-500" />
<input
id="password"
type={showPassword ? "text" : "password"}
{...register("password", {
required: "Password wajib diisi",
minLength: {
value: 6,
message: "Password minimal 6 karakter",
},
})}
className={clsx(
"input pl-10 pr-10",
errors.password &&
"border-red-500 focus-visible:ring-destructive"
)}
placeholder="Masukkan password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-3 text-gray-500 hover:text-gray-900"
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
</div>
{errors.password && (
<p className="text-sm text-red-600 flex items-center">
<AlertCircle className="h-4 w-4 mr-1" />
{errors.password.message}
</p>
)}
</div>
{/* Remember Me & Forgot Password */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<input
id="remember"
type="checkbox"
{...register("remember")}
className="rounded border-input"
/>
<label htmlFor="remember" className="text-sm text-gray-500">
Ingat saya
</label>
</div>
<button
type="button"
className="text-sm text-primary hover:text-primary/80 font-medium"
>
Lupa password?
</button>
</div>
{/* Error Message */}
{errors.root && (
<div className="bg-red-50 border border-red-200 rounded-md p-3">
<p className="text-sm text-red-600 flex items-center">
<AlertCircle className="h-4 w-4 mr-2" />
{errors.root.message}
</p>
</div>
)}
{/* Login Button */}
<button
type="submit"
disabled={isLoading}
className={clsx(
"btn-primary w-full h-12 text-base font-medium",
isLoading && "opacity-50 cursor-not-allowed"
)}
>
{isLoading ? (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-4 w-4 border-2 border-primary-foreground border-t-transparent mr-2"></div>
Memproses...
</div>
) : (
"Masuk ke Sistem"
)}
</button>
</form>
{/* Demo Credentials */}
<div className="mt-6 p-4 bg-muted/50 rounded-lg">
<p className="text-sm text-gray-500 text-center mb-2">
<strong>Demo Credentials:</strong>
</p>
<div className="text-xs text-gray-500 text-center space-y-1">
<p>
Username:{" "}
<span className="font-mono bg-white px-1 rounded">admin</span>
</p>
<p>
Password:{" "}
<span className="font-mono bg-white px-1 rounded">
admin123
</span>
</p>
</div>
</div>
</div>
{/* Footer */}
<div className="text-center mt-8 text-sm text-gray-500">
<p>
© 2024 ClaimGuard Hospital Management System.
<br />
Semua hak dilindungi.
</p>
</div>
</div>
</div>
</div>
);
}

470
src/pages/MedicalRecord.tsx Normal file
View File

@@ -0,0 +1,470 @@
import { useState } from "react";
import {
FileText,
Search,
Filter,
Plus,
Eye,
Edit,
Calendar,
User,
Stethoscope,
Heart,
Activity,
ClipboardList,
Download,
Printer,
} from "lucide-react";
interface MedicalRecord {
id: string;
patientId: string;
patientName: string;
patientAge: number;
patientGender: string;
recordDate: string;
diagnosis: string;
icdCode: string;
treatment: string;
doctor: string;
department: string;
status: "draft" | "completed" | "reviewed";
vital: {
bloodPressure: string;
heartRate: number;
temperature: number;
weight: number;
};
}
const sampleMedicalRecords: MedicalRecord[] = [
{
id: "MR001",
patientId: "P001",
patientName: "Ahmad Rizki",
patientAge: 45,
patientGender: "Laki-laki",
recordDate: "2024-01-15T10:30:00Z",
diagnosis: "Hipertensi Grade 2",
icdCode: "I10",
treatment: "Amlodipine 10mg 1x1, Diet rendah garam",
doctor: "Dr. Siti Nurhaliza",
department: "Cardiology",
status: "completed",
vital: {
bloodPressure: "160/100",
heartRate: 88,
temperature: 36.5,
weight: 75,
},
},
{
id: "MR002",
patientId: "P002",
patientName: "Maria Lopez",
patientAge: 32,
patientGender: "Perempuan",
recordDate: "2024-01-15T14:15:00Z",
diagnosis: "Gastritis Akut",
icdCode: "K29.0",
treatment: "Omeprazole 20mg 2x1, Antasida 3x1",
doctor: "Dr. Budi Santoso",
department: "Internal Medicine",
status: "reviewed",
vital: {
bloodPressure: "120/80",
heartRate: 76,
temperature: 37.2,
weight: 58,
},
},
{
id: "MR003",
patientId: "P003",
patientName: "Dewi Sartika",
patientAge: 28,
patientGender: "Perempuan",
recordDate: "2024-01-15T09:45:00Z",
diagnosis: "Kehamilan Normal G1P0A0",
icdCode: "Z34.0",
treatment: "Asam folat 1x1, Vitamin prenatal",
doctor: "Dr. Ahmad Rizki",
department: "Obstetrics & Gynecology",
status: "completed",
vital: {
bloodPressure: "110/70",
heartRate: 82,
temperature: 36.8,
weight: 62,
},
},
{
id: "MR004",
patientId: "P004",
patientName: "Joko Widodo",
patientAge: 56,
patientGender: "Laki-laki",
recordDate: "2024-01-15T16:20:00Z",
diagnosis: "Diabetes Mellitus Tipe 2",
icdCode: "E11.9",
treatment: "Metformin 500mg 2x1, Diet DM",
doctor: "Dr. Siti Nurhaliza",
department: "Endocrinology",
status: "draft",
vital: {
bloodPressure: "140/90",
heartRate: 92,
temperature: 36.7,
weight: 82,
},
},
];
export default function MedicalRecord() {
const [records] = useState<MedicalRecord[]>(sampleMedicalRecords);
const [searchTerm, setSearchTerm] = useState("");
const [selectedDepartment, setSelectedDepartment] = useState("");
const [selectedStatus, setSelectedStatus] = useState("");
const departments = [
"Cardiology",
"Internal Medicine",
"Obstetrics & Gynecology",
"Endocrinology",
"Emergency",
"Surgery",
"Pediatrics",
];
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 getStatusColor = (status: string) => {
switch (status) {
case "completed":
return "bg-green-100 text-green-800";
case "reviewed":
return "bg-blue-100 text-blue-800";
case "draft":
return "bg-yellow-100 text-yellow-800";
default:
return "bg-gray-100 text-gray-800";
}
};
const getStatusText = (status: string) => {
switch (status) {
case "completed":
return "Selesai";
case "reviewed":
return "Direview";
case "draft":
return "Draft";
default:
return status;
}
};
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-green-100 p-3 rounded-lg">
<FileText className="h-6 w-6 text-green-600" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900">
Medical Record
</h1>
<p className="text-gray-600">
Kelola rekam medis pasien dan riwayat diagnosa
</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>
{/* 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 */}
<div className="bg-white p-6 rounded-lg shadow-sm border mb-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
<div className="flex items-center space-x-4">
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Cari pasien, 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"
/>
</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>
</div>
</div>
{/* Medical Records 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">
Diagnosa & ICD
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Vital Signs
</th>
<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
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredRecords.map((record) => (
<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}
</div>
<div className="text-sm text-gray-500 flex items-center">
<User className="h-3 w-3 mr-1" />
{record.patientAge} tahun {record.patientGender}
</div>
<div className="text-xs text-gray-400">
ID: {record.patientId}
</div>
</div>
</div>
</td>
<td className="px-6 py-4">
<div>
<div className="text-sm font-medium text-gray-900 mb-1">
{record.diagnosis}
</div>
<div className="text-xs font-mono bg-gray-100 px-2 py-1 rounded w-fit">
{record.icdCode}
</div>
<div className="text-xs text-gray-500 mt-1">
{record.treatment}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-1 text-xs">
<div className="flex items-center">
<Heart className="h-3 w-3 text-red-500 mr-1" />
<span>{record.vital.bloodPressure} mmHg</span>
</div>
<div className="flex items-center">
<Activity className="h-3 w-3 text-blue-500 mr-1" />
<span>{record.vital.heartRate} bpm</span>
</div>
<div className="flex items-center">
<span className="w-3 h-3 bg-orange-500 rounded-full mr-1"></span>
<span>{record.vital.temperature}°C</span>
</div>
<div className="flex items-center">
<span className="w-3 h-3 bg-purple-500 rounded-full mr-1"></span>
<span>{record.vital.weight} kg</span>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<Stethoscope className="h-4 w-4 text-blue-500 mr-2" />
<div>
<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" />
{formatDate(record.recordDate)}
</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>
{/* Empty State */}
{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">
Tidak ada medical record ditemukan
</h3>
<p className="mt-1 text-sm text-gray-500">
Mulai dengan menambahkan medical record baru.
</p>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,552 @@
import { useState } from "react";
import {
Database,
RefreshCw,
Calendar,
Search,
Filter,
CheckCircle,
XCircle,
Clock,
AlertCircle,
FileText,
Activity,
Users,
Building2,
Upload,
} from "lucide-react";
interface SyncLog {
id: string;
timestamp: string;
type: "import" | "sync";
status: "success" | "failed" | "in_progress";
recordsProcessed: number;
recordsSuccess: number;
recordsFailed: number;
source: string;
duration: number; // in seconds
errorMessage?: string;
details: {
patientsUpdated: number;
diagnosesAdded: number;
treatmentsAdded: number;
vitalsUpdated: number;
};
}
interface SyncStats {
totalSyncs: number;
successfulSyncs: number;
failedSyncs: number;
lastSyncTime: string;
totalRecordsProcessed: number;
averageDuration: number;
}
export default function MedicalRecordSync() {
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [isImporting, setIsImporting] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString("id-ID", {
day: "numeric",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
const getStatusColor = (status: string) => {
switch (status) {
case "success":
return "bg-green-100 text-green-800";
case "failed":
return "bg-red-100 text-red-800";
case "in_progress":
return "bg-blue-100 text-blue-800";
default:
return "bg-gray-100 text-gray-800";
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case "success":
return <CheckCircle className="h-4 w-4" />;
case "failed":
return <XCircle className="h-4 w-4" />;
case "in_progress":
return <Clock className="h-4 w-4" />;
default:
return <AlertCircle className="h-4 w-4" />;
}
};
const getStatusText = (status: string) => {
switch (status) {
case "success":
return "Berhasil";
case "failed":
return "Gagal";
case "in_progress":
return "Berlangsung";
default:
return status;
}
};
// Sample sync logs data
const [syncLogs] = useState<SyncLog[]>([
{
id: "1",
timestamp: "2024-01-15T14:30:00Z",
type: "sync",
status: "success",
recordsProcessed: 1250,
recordsSuccess: 1245,
recordsFailed: 5,
source: "Hospital Management System API",
duration: 45,
details: {
patientsUpdated: 89,
diagnosesAdded: 156,
treatmentsAdded: 203,
vitalsUpdated: 797,
},
},
{
id: "2",
timestamp: "2024-01-15T10:15:00Z",
type: "import",
status: "success",
recordsProcessed: 567,
recordsSuccess: 567,
recordsFailed: 0,
source: "External Lab System",
duration: 23,
details: {
patientsUpdated: 0,
diagnosesAdded: 234,
treatmentsAdded: 0,
vitalsUpdated: 333,
},
},
{
id: "3",
timestamp: "2024-01-14T16:45:00Z",
type: "sync",
status: "failed",
recordsProcessed: 0,
recordsSuccess: 0,
recordsFailed: 0,
source: "Radiology System API",
duration: 0,
errorMessage: "Connection timeout - API endpoint tidak merespons",
details: {
patientsUpdated: 0,
diagnosesAdded: 0,
treatmentsAdded: 0,
vitalsUpdated: 0,
},
},
{
id: "4",
timestamp: "2024-01-14T09:30:00Z",
type: "import",
status: "success",
recordsProcessed: 834,
recordsSuccess: 820,
recordsFailed: 14,
source: "Pharmacy System",
duration: 38,
details: {
patientsUpdated: 45,
diagnosesAdded: 67,
treatmentsAdded: 567,
vitalsUpdated: 155,
},
},
{
id: "5",
timestamp: "2024-01-13T13:20:00Z",
type: "sync",
status: "in_progress",
recordsProcessed: 423,
recordsSuccess: 423,
recordsFailed: 0,
source: "Emergency System API",
duration: 0,
details: {
patientsUpdated: 12,
diagnosesAdded: 89,
treatmentsAdded: 134,
vitalsUpdated: 188,
},
},
]);
// Calculate statistics
const stats: SyncStats = {
totalSyncs: syncLogs.length,
successfulSyncs: syncLogs.filter((log) => log.status === "success").length,
failedSyncs: syncLogs.filter((log) => log.status === "failed").length,
lastSyncTime: syncLogs[0]?.timestamp || "",
totalRecordsProcessed: syncLogs.reduce(
(sum, log) => sum + log.recordsProcessed,
0
),
averageDuration:
syncLogs
.filter((log) => log.duration > 0)
.reduce((sum, log) => sum + log.duration, 0) /
syncLogs.filter((log) => log.duration > 0).length || 0,
};
// Filter logs based on search and status
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 = statusFilter === "all" || log.status === statusFilter;
return matchesSearch && matchesStatus;
});
const handleImport = async () => {
setIsImporting(true);
// Simulate import process
setTimeout(() => {
setIsImporting(false);
// Add new log entry here
}, 3000);
};
const handleSync = async () => {
setIsSyncing(true);
// Simulate sync process
setTimeout(() => {
setIsSyncing(false);
// Add new log entry here
}, 5000);
};
return (
<div className="p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center">
<Database className="h-8 w-8 text-blue-600 mr-3" />
Medical Record Sync
</h1>
<p className="text-gray-600 mt-1">
Sinkronisasi dan import data medical record dari sistem
eksternal
</p>
</div>
<div className="flex space-x-3">
<button
onClick={handleImport}
disabled={isImporting || isSyncing}
className="btn-secondary flex items-center space-x-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isImporting ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
<Upload className="h-4 w-4" />
)}
<span>{isImporting ? "Importing..." : "Import Data"}</span>
</button>
<button
onClick={handleSync}
disabled={isImporting || isSyncing}
className="btn-primary flex items-center space-x-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSyncing ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
<span>{isSyncing ? "Syncing..." : "Sync by API"}</span>
</button>
</div>
</div>
</div>
{/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Sync</p>
<p className="text-2xl font-bold text-blue-600">
{stats.totalSyncs}
</p>
</div>
<div className="p-3 bg-blue-100 rounded-lg">
<RefreshCw className="h-6 w-6 text-blue-600" />
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Berhasil</p>
<p className="text-2xl font-bold text-green-600">
{stats.successfulSyncs}
</p>
</div>
<div className="p-3 bg-green-100 rounded-lg">
<CheckCircle className="h-6 w-6 text-green-600" />
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Total Records
</p>
<p className="text-2xl font-bold text-purple-600">
{stats.totalRecordsProcessed.toLocaleString()}
</p>
</div>
<div className="p-3 bg-purple-100 rounded-lg">
<FileText className="h-6 w-6 text-purple-600" />
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">
Avg Duration
</p>
<p className="text-2xl font-bold text-orange-600">
{Math.round(stats.averageDuration)}s
</p>
</div>
<div className="p-3 bg-orange-100 rounded-lg">
<Clock className="h-6 w-6 text-orange-600" />
</div>
</div>
</div>
</div>
{/* Filters and Search */}
<div className="bg-white p-6 rounded-lg shadow-sm border mb-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
<div className="flex items-center space-x-4">
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Cari 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"
/>
</div>
<div className="flex items-center space-x-2">
<Filter className="h-4 w-4 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Semua Status</option>
<option value="success">Berhasil</option>
<option value="failed">Gagal</option>
<option value="in_progress">Berlangsung</option>
</select>
</div>
</div>
</div>
</div>
{/* Sync Logs Table */}
<div className="bg-white rounded-lg shadow-sm border overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">
Log Sinkronisasi Medical Record
</h3>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Waktu & Type
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Source System
</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">
Records Processed
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Success Rate
</th>
<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">
{filteredLogs.map((log) => (
<tr key={log.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="flex items-center text-sm text-gray-500">
<Calendar className="h-4 w-4 mr-1" />
{formatDate(log.timestamp)}
</div>
<div className="text-sm font-medium text-gray-900 mt-1">
<span
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
log.type === "import"
? "bg-blue-100 text-blue-800"
: "bg-purple-100 text-purple-800"
}`}
>
{log.type === "import" ? "Import" : "Sync"}
</span>
</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" />
<div>
<div className="text-sm font-medium text-gray-900">
{log.source}
</div>
{log.errorMessage && (
<div className="text-sm text-red-600 mt-1">
{log.errorMessage}
</div>
)}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<span
className={`inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(
log.status
)}`}
>
{getStatusIcon(log.status)}
<span className="ml-1">
{getStatusText(log.status)}
</span>
</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{log.recordsProcessed.toLocaleString()}
</div>
<div className="text-sm text-gray-500">
{log.recordsSuccess > 0 && (
<span className="text-green-600">
{log.recordsSuccess}
</span>
)}
{log.recordsFailed > 0 && (
<span className="text-red-600 ml-2">
{log.recordsFailed}
</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{log.recordsProcessed > 0
? Math.round(
(log.recordsSuccess / log.recordsProcessed) * 100
)
: 0}
%
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-green-600 h-2 rounded-full"
style={{
width:
log.recordsProcessed > 0
? `${
(log.recordsSuccess /
log.recordsProcessed) *
100
}%`
: "0%",
}}
></div>
</div>
</td>
<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>
</table>
</div>
</div>
{/* Empty State */}
{filteredLogs.length === 0 && (
<div className="text-center py-12">
<AlertCircle className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">
Tidak ada log sinkronisasi ditemukan
</h3>
<p className="mt-1 text-sm text-gray-500">
Tidak ada log yang sesuai dengan kriteria pencarian.
</p>
</div>
)}
</div>
</div>
);
}

158
src/pages/NotFound.tsx Normal file
View File

@@ -0,0 +1,158 @@
import { Link } from "react-router-dom";
import {
Home,
AlertTriangle,
ArrowLeft,
Search,
Heart,
Stethoscope,
Activity,
Phone,
} from "lucide-react";
export default function NotFound() {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-green-50 flex items-center justify-center px-4">
<div className="max-w-2xl mx-auto text-center">
{/* Header */}
<div className="mb-8">
<div className="flex items-center justify-center mb-6">
<div className="bg-red-100 p-4 rounded-full">
<AlertTriangle className="h-12 w-12 text-red-600" />
</div>
</div>
<h1 className="text-6xl font-bold text-gray-900 mb-4">404</h1>
<h2 className="text-3xl font-bold text-gray-800 mb-4">
Halaman Tidak Ditemukan
</h2>
<p className="text-lg text-gray-600 mb-8 max-w-md mx-auto">
Maaf, halaman yang Anda cari tidak dapat ditemukan di sistem
ClaimGuard Hospital Management.
</p>
</div>
{/* Medical Icons Animation */}
<div className="relative mb-12">
<div className="flex items-center justify-center space-x-8 opacity-20">
<div className="animate-pulse">
<Heart className="h-8 w-8 text-green-500" />
</div>
<div className="animate-pulse" style={{ animationDelay: "0.5s" }}>
<Stethoscope className="h-8 w-8 text-blue-500" />
</div>
<div className="animate-pulse" style={{ animationDelay: "1s" }}>
<Activity className="h-8 w-8 text-purple-500" />
</div>
</div>
</div>
{/* Possible Reasons */}
<div className="bg-white rounded-lg shadow-lg p-8 mb-8">
<h3 className="text-xl font-semibold text-gray-800 mb-6">
Kemungkinan Penyebab:
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-left">
<div className="flex items-start space-x-3">
<div className="bg-blue-100 p-2 rounded-lg">
<Search className="h-4 w-4 text-blue-600" />
</div>
<div>
<p className="font-medium text-gray-800">URL Salah</p>
<p className="text-sm text-gray-600">
Periksa kembali alamat URL yang dimasukkan
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="bg-green-100 p-2 rounded-lg">
<Heart className="h-4 w-4 text-green-600" />
</div>
<div>
<p className="font-medium text-gray-800">Halaman Dipindahkan</p>
<p className="text-sm text-gray-600">
Halaman mungkin telah dipindahkan atau dihapus
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="bg-purple-100 p-2 rounded-lg">
<Activity className="h-4 w-4 text-purple-600" />
</div>
<div>
<p className="font-medium text-gray-800">Akses Terbatas</p>
<p className="text-sm text-gray-600">
Halaman mungkin memerlukan izin khusus
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="bg-orange-100 p-2 rounded-lg">
<Phone className="h-4 w-4 text-orange-600" />
</div>
<div>
<p className="font-medium text-gray-800">Maintenance</p>
<p className="text-sm text-gray-600">
Sistem sedang dalam pemeliharaan
</p>
</div>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link
to="/dashboard"
className="btn-primary flex items-center space-x-2 px-6 py-3"
>
<Home className="h-5 w-5" />
<span>Kembali ke Dashboard</span>
</Link>
<button
onClick={() => window.history.back()}
className="btn-secondary flex items-center space-x-2 px-6 py-3"
>
<ArrowLeft className="h-5 w-5" />
<span>Halaman Sebelumnya</span>
</button>
</div>
{/* Help Section */}
<div className="mt-12 p-6 bg-green-50 rounded-lg border border-green-200">
<h4 className="text-lg font-semibold text-green-800 mb-3">
Butuh Bantuan?
</h4>
<p className="text-green-700 mb-4">
Jika Anda yakin halaman ini seharusnya ada, silakan hubungi tim IT
atau administrator sistem.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
<Link
to="/help"
className="text-green-600 hover:text-green-800 font-medium"
>
📞 Hubungi Support
</Link>
<span className="hidden sm:inline text-green-400"></span>
<Link
to="/settings"
className="text-green-600 hover:text-green-800 font-medium"
>
Pengaturan Sistem
</Link>
</div>
</div>
{/* Footer */}
<div className="mt-8 text-sm text-gray-500">
<p>© 2024 ClaimGuard Hospital Management System</p>
<p>Error Code: 404 - Page Not Found</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,114 @@
import { Link } from "react-router-dom";
import { Home, AlertTriangle, ArrowLeft, Search, Settings } from "lucide-react";
export default function NotFoundProtected() {
return (
<div className="p-6">
<div className="max-w-4xl mx-auto">
<div className="text-center py-16">
{/* Icon */}
<div className="flex items-center justify-center mb-8">
<div className="bg-red-100 p-6 rounded-full">
<AlertTriangle className="h-16 w-16 text-red-600" />
</div>
</div>
{/* Error Message */}
<h1 className="text-4xl font-bold text-gray-900 mb-4">404</h1>
<h2 className="text-2xl font-semibold text-gray-800 mb-4">
Halaman Tidak Ditemukan
</h2>
<p className="text-lg text-gray-600 mb-8 max-w-lg mx-auto">
Halaman yang Anda cari tidak tersedia atau telah dipindahkan ke
lokasi lain dalam sistem ClaimGuard.
</p>
{/* Quick Info Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="card p-6">
<div className="bg-blue-100 p-3 rounded-lg mb-4 mx-auto w-fit">
<Search className="h-6 w-6 text-blue-600" />
</div>
<h3 className="font-semibold text-gray-800 mb-2">Periksa URL</h3>
<p className="text-sm text-gray-600">
Pastikan alamat URL yang dimasukkan sudah benar
</p>
</div>
<div className="card p-6">
<div className="bg-green-100 p-3 rounded-lg mb-4 mx-auto w-fit">
<Home className="h-6 w-6 text-green-600" />
</div>
<h3 className="font-semibold text-gray-800 mb-2">Menu Sidebar</h3>
<p className="text-sm text-gray-600">
Gunakan menu sidebar untuk navigasi yang tersedia
</p>
</div>
<div className="card p-6">
<div className="bg-purple-100 p-3 rounded-lg mb-4 mx-auto w-fit">
<Settings className="h-6 w-6 text-purple-600" />
</div>
<h3 className="font-semibold text-gray-800 mb-2">Izin Akses</h3>
<p className="text-sm text-gray-600">
Halaman mungkin memerlukan izin khusus
</p>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-8">
<Link
to="/dashboard"
className="btn-primary flex items-center space-x-2"
>
<Home className="h-4 w-4" />
<span>Dashboard Utama</span>
</Link>
<button
onClick={() => window.history.back()}
className="btn-secondary flex items-center space-x-2"
>
<ArrowLeft className="h-4 w-4" />
<span>Kembali</span>
</button>
</div>
{/* Available Pages */}
<div className="bg-gray-50 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
Halaman yang Tersedia:
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
<Link
to="/dashboard"
className="text-green-600 hover:text-green-800 font-medium"
>
📊 Dashboard
</Link>
<Link
to="/patients"
className="text-green-600 hover:text-green-800 font-medium"
>
👥 Data Pasien
</Link>
<Link
to="/settings"
className="text-green-600 hover:text-green-800 font-medium"
>
Pengaturan
</Link>
<Link
to="/help"
className="text-green-600 hover:text-green-800 font-medium"
>
Bantuan
</Link>
</div>
</div>
</div>
</div>
</div>
);
}

284
src/pages/Patients.tsx Normal file
View File

@@ -0,0 +1,284 @@
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

@@ -0,0 +1,464 @@
import { useState } from "react";
import {
Shield,
Plus,
Edit,
Trash2,
Search,
Filter,
Users,
CheckCircle,
XCircle,
Settings,
Calendar,
Download,
} from "lucide-react";
import { sampleRoles, MODULES, ACTIONS } from "../types/roles";
import type { IRole } from "../types/roles";
// Helper function to get proper module display names
const getModuleDisplayName = (module: string) => {
const moduleNames: Record<string, string> = {
[MODULES.DASHBOARD]: "Dashboard",
[MODULES.COST_RECOMMENDATION]: "Cost Recommendation",
[MODULES.INTEGRASI_DATA_BPJS]: "Integrasi Data - BPJS",
[MODULES.INTEGRASI_DATA_MEDICAL_RECORD]: "Integrasi Data - Medical Record",
[MODULES.PASIEN_MANAJEMEN]: "Pasien - Manajemen Pasien",
[MODULES.PASIEN_MEDICAL_RECORD]: "Pasien - Medical Record Pasien",
[MODULES.PASIEN_BPJS_CODE]: "Pasien - BPJS Code",
[MODULES.USER_MANAGEMENT]: "System Administration - Manajemen User",
[MODULES.ROLE_MANAGEMENT]: "System Administration - Manajemen Role",
};
return moduleNames[module] || module.replace(/_/g, " ");
};
export default function RoleManagement() {
const [roles, setRoles] = useState<IRole[]>(sampleRoles);
const [searchTerm, setSearchTerm] = 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 filteredRoles = roles.filter((role) => {
const matchesSearch =
role.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
role.description.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus =
statusFilter === "all" ||
(statusFilter === "active" && role.isActive) ||
(statusFilter === "inactive" && !role.isActive);
const matchesPermission =
permissionFilter === "all" ||
(permissionFilter === "high" && role.permissions.length >= 8) ||
(permissionFilter === "medium" &&
role.permissions.length >= 4 &&
role.permissions.length <= 7) ||
(permissionFilter === "low" && role.permissions.length <= 3);
return matchesSearch && matchesStatus && matchesPermission;
});
const handleCreateRole = () => {
setSelectedRole(null);
setModalMode("create");
setIsModalOpen(true);
};
const handleEditRole = (role: IRole) => {
setSelectedRole(role);
setModalMode("edit");
setIsModalOpen(true);
};
const handleDeleteRole = (roleId: string) => {
if (confirm("Apakah Anda yakin ingin menghapus role ini?")) {
setRoles(roles.filter((role) => role.id !== roleId));
}
};
const handleToggleStatus = (roleId: string) => {
setRoles(
roles.map((role) =>
role.id === roleId ? { ...role, isActive: !role.isActive } : role
)
);
};
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-purple-100 p-3 rounded-lg">
<Shield className="h-6 w-6 text-purple-600" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900">
Manajemen Role
</h1>
<p className="text-gray-600">
Kelola role dan permission untuk sistem rumah sakit
</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
onClick={handleCreateRole}
className="btn-primary flex items-center space-x-2"
>
<Plus className="h-4 w-4" />
<span>Tambah Role</span>
</button>
</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 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 */}
<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 role atau deskripsi..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500 focus:border-transparent w-80"
/>
</div>
<div className="flex items-center space-x-2">
<Filter className="h-4 w-4 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(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"
>
<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>
</div>
</div>
</div>
</div>
{/* Roles 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">
Role
</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">
Permissions
</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 Dibuat
</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">
{filteredRoles.map((role) => (
<tr key={role.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="bg-purple-100 p-2 rounded-lg mr-3">
<Shield className="h-5 w-5 text-purple-600" />
</div>
<div>
<div className="text-sm font-medium text-gray-900">
{role.name}
</div>
</div>
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900 max-w-xs truncate">
{role.description}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<Users className="h-4 w-4 text-gray-400 mr-1" />
<span className="text-sm font-medium text-gray-900">
{role.permissions.length}
</span>
<span className="text-sm text-gray-500 ml-1">
permissions
</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<button
onClick={() => handleToggleStatus(role.id)}
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
role.isActive
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{role.isActive ? "Active" : "Inactive"}
</button>
</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(role.createdAt)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center space-x-2">
<button
onClick={() => handleEditRole(role)}
className="text-blue-600 hover:text-blue-900"
>
<Edit className="h-4 w-4" />
</button>
<button
onClick={() => handleDeleteRole(role.id)}
className="text-red-600 hover:text-red-900"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Empty State */}
{filteredRoles.length === 0 && (
<div className="text-center py-12">
<Shield className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">
Tidak ada role ditemukan
</h3>
<p className="mt-1 text-sm text-gray-500">
Mulai dengan membuat role baru untuk sistem rumah sakit.
</p>
<div className="mt-6">
<button
onClick={handleCreateRole}
className="btn-primary flex items-center space-x-2 mx-auto"
>
<Plus className="h-4 w-4" />
<span>Tambah Role</span>
</button>
</div>
</div>
)}
{/* Role Modal - Create/Edit */}
{isModalOpen && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
<div className="mt-3">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">
{modalMode === "create" ? "Tambah Role Baru" : "Edit Role"}
</h3>
<button
onClick={() => setIsModalOpen(false)}
className="text-gray-400 hover:text-gray-600"
>
<XCircle className="h-6 w-6" />
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nama Role
</label>
<input
type="text"
className="input w-full"
placeholder="Masukkan nama role"
defaultValue={selectedRole?.name || ""}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Deskripsi
</label>
<textarea
className="input w-full h-20"
placeholder="Deskripsi role"
defaultValue={selectedRole?.description || ""}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-4">
Permissions (CRUD Operations)
</label>
<div className="bg-gray-50 rounded-lg p-4">
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{Object.values(MODULES).map((module) => (
<div
key={module}
className="bg-white border rounded-lg p-4 shadow-sm"
>
<div className="flex items-center mb-3">
<div className="bg-blue-100 p-2 rounded-lg mr-3">
<Shield className="h-4 w-4 text-blue-600" />
</div>
<h4 className="font-semibold text-gray-900 text-sm">
{getModuleDisplayName(module)}
</h4>
</div>
<div className="grid grid-cols-2 gap-2">
{Object.values(ACTIONS).map((action) => (
<label
key={`${module}-${action}`}
className="flex items-center p-2 rounded-md hover:bg-gray-50 cursor-pointer"
>
<input
type="checkbox"
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
defaultChecked={
selectedRole?.permissions.some(
(p) =>
p.module === module &&
p.action === action
) || false
}
/>
<span className="ml-2 text-sm text-gray-700 capitalize font-medium">
{action}
</span>
</label>
))}
</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="flex items-center justify-end space-x-3 mt-6 pt-4 border-t">
<button
onClick={() => setIsModalOpen(false)}
className="btn-secondary"
>
Batal
</button>
<button className="btn-primary">
{modalMode === "create" ? "Simpan Role" : "Update Role"}
</button>
</div>
</div>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,523 @@
import { useState } from "react";
import {
Users,
Plus,
Edit,
Trash2,
Search,
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 [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 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 matchesStatus =
!selectedStatus ||
(selectedStatus === "active" && user.isActive) ||
(selectedStatus === "inactive" && !user.isActive);
return matchesSearch && matchesDepartment && matchesStatus;
});
const handleCreateUser = () => {
setSelectedUser(null);
setModalMode("create");
setIsModalOpen(true);
};
const handleEditUser = (user: IUser) => {
setSelectedUser(user);
setModalMode("edit");
setIsModalOpen(true);
};
const handleDeleteUser = (userId: string) => {
if (confirm("Apakah Anda yakin ingin menghapus user ini?")) {
setUsers(users.filter((user) => user.id !== userId));
}
};
const handleToggleStatus = (userId: string) => {
setUsers(
users.map((user) =>
user.id === userId ? { ...user, isActive: !user.isActive } : user
)
);
};
const getStatusColor = (isActive: boolean) => {
return isActive ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800";
};
const formatLastLogin = (lastLogin?: string) => {
if (!lastLogin) return "Belum pernah login";
const date = new Date(lastLogin);
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 User
</h1>
<p className="text-gray-600">
Kelola user dan akses sistem rumah sakit
</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
onClick={handleCreateUser}
className="btn-primary flex items-center space-x-2"
>
<Plus className="h-4 w-4" />
<span>Tambah User</span>
</button>
</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 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 */}
<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, email, department, atau role..."
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"
/>
</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-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)}
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>
</div>
</div>
</div>
</div>
{/* Users 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">
User
</th>
<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>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Login 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">
{filteredUsers.map((user) => (
<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}
</div>
<div className="text-sm text-gray-500 flex items-center">
<Mail className="h-3 w-3 mr-1" />
{user.email}
</div>
<div className="text-sm text-gray-500 flex items-center">
<Phone className="h-3 w-3 mr-1" />
{user.phone}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<Shield className="h-4 w-4 text-purple-500 mr-2" />
<div>
<div className="text-sm font-medium text-gray-900">
{user.role.name}
</div>
<div className="text-xs text-gray-500">
{user.role.permissions.length} permissions
</div>
</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)}
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(
user.isActive
)}`}
>
{user.isActive ? "Active" : "Inactive"}
</button>
</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" />
{formatLastLogin(user.lastLogin)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center space-x-2">
<button
onClick={() => handleEditUser(user)}
className="text-blue-600 hover:text-blue-900"
>
<Edit className="h-4 w-4" />
</button>
<button
onClick={() => handleDeleteUser(user.id)}
className="text-red-600 hover:text-red-900"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Empty State */}
{filteredUsers.length === 0 && (
<div className="text-center py-12">
<Users className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">
Tidak ada user ditemukan
</h3>
<p className="mt-1 text-sm text-gray-500">
Mulai dengan menambahkan user baru ke sistem.
</p>
<div className="mt-6">
<button
onClick={handleCreateUser}
className="btn-primary flex items-center space-x-2 mx-auto"
>
<Plus className="h-4 w-4" />
<span>Tambah User</span>
</button>
</div>
</div>
)}
{/* User Modal - Create/Edit */}
{isModalOpen && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
<div className="mt-3">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">
{modalMode === "create" ? "Tambah User Baru" : "Edit User"}
</h3>
<button
onClick={() => setIsModalOpen(false)}
className="text-gray-400 hover:text-gray-600"
>
<XCircle className="h-6 w-6" />
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nama Lengkap
</label>
<input
type="text"
className="input w-full"
placeholder="Masukkan nama lengkap"
defaultValue={selectedUser?.name || ""}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<input
type="email"
className="input w-full"
placeholder="user@claimguard.com"
defaultValue={selectedUser?.email || ""}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nomor Telepon
</label>
<input
type="tel"
className="input w-full"
placeholder="+62 812-3456-7890"
defaultValue={selectedUser?.phone || ""}
/>
</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
</label>
<select
className="input w-full"
defaultValue={selectedUser?.role.id || ""}
>
<option value="">Pilih Role</option>
{sampleRoles.map((role) => (
<option key={role.id} value={role.id}>
{role.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Status
</label>
<select
className="input w-full"
defaultValue={selectedUser?.isActive ? "true" : "false"}
>
<option value="true">Active</option>
<option value="false">Inactive</option>
</select>
</div>
{modalMode === "create" && (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Password
</label>
<input
type="password"
className="input w-full"
placeholder="Masukkan password"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Konfirmasi Password
</label>
<input
type="password"
className="input w-full"
placeholder="Konfirmasi password"
/>
</div>
</>
)}
</div>
<div className="flex items-center justify-end space-x-3 mt-6 pt-4 border-t">
<button
onClick={() => setIsModalOpen(false)}
className="btn-secondary"
>
Batal
</button>
<button className="btn-primary">
{modalMode === "create" ? "Simpan User" : "Update User"}
</button>
</div>
</div>
</div>
</div>
)}
</div>
</div>
);
}

400
src/types/roles.ts Normal file
View File

@@ -0,0 +1,400 @@
// Role types and permissions for hospital management system
export interface IRole {
id: string;
name: string;
description: string;
permissions: Permission[];
isActive: boolean;
createdAt: string;
updatedAt: string;
}
export interface Permission {
id: string;
module: string;
action: string;
description: string;
}
export interface IUser {
id: string;
name: string;
email: string;
phone: string;
role: IRole;
department: string;
isActive: boolean;
lastLogin?: string;
createdAt: string;
updatedAt: string;
}
// Predefined hospital roles
export const HOSPITAL_ROLES = {
SUPER_ADMIN: "super_admin",
ADMINISTRATOR: "administrator",
DOCTOR: "doctor",
NURSE: "nurse",
PHARMACIST: "pharmacist",
FINANCE: "finance",
RECEPTION: "reception",
IT_SUPPORT: "it_support",
MEDICAL_RECORD: "medical_record",
LABORATORY: "laboratory",
RADIOLOGY: "radiology",
} as const;
// System modules - Only modules that exist in sidebar
export const MODULES = {
DASHBOARD: "dashboard",
COST_RECOMMENDATION: "cost_recommendation",
INTEGRASI_DATA_BPJS: "integrasi_data_bpjs",
INTEGRASI_DATA_MEDICAL_RECORD: "integrasi_data_medical_record",
PASIEN_MANAJEMEN: "pasien_manajemen",
PASIEN_MEDICAL_RECORD: "pasien_medical_record",
PASIEN_BPJS_CODE: "pasien_bpjs_code",
USER_MANAGEMENT: "user_management",
ROLE_MANAGEMENT: "role_management",
} as const;
// Actions - CRUD Only
export const ACTIONS = {
CREATE: "create",
READ: "read",
UPDATE: "update",
DELETE: "delete",
} as const;
// Sample roles data
export const sampleRoles: IRole[] = [
{
id: "1",
name: "Super Administrator",
description: "Full system access with all permissions",
permissions: [
{
id: "1",
module: MODULES.DASHBOARD,
action: ACTIONS.READ,
description: "View dashboard",
},
{
id: "2",
module: MODULES.COST_RECOMMENDATION,
action: ACTIONS.READ,
description: "View cost recommendations",
},
{
id: "3",
module: MODULES.PASIEN_MANAJEMEN,
action: ACTIONS.CREATE,
description: "Create patients",
},
{
id: "4",
module: MODULES.PASIEN_MANAJEMEN,
action: ACTIONS.READ,
description: "View patients",
},
{
id: "5",
module: MODULES.PASIEN_MEDICAL_RECORD,
action: ACTIONS.READ,
description: "View patient medical records",
},
{
id: "6",
module: MODULES.USER_MANAGEMENT,
action: ACTIONS.CREATE,
description: "Create users",
},
{
id: "7",
module: MODULES.USER_MANAGEMENT,
action: ACTIONS.READ,
description: "View users",
},
{
id: "8",
module: MODULES.USER_MANAGEMENT,
action: ACTIONS.UPDATE,
description: "Update users",
},
{
id: "9",
module: MODULES.USER_MANAGEMENT,
action: ACTIONS.DELETE,
description: "Delete users",
},
{
id: "10",
module: MODULES.ROLE_MANAGEMENT,
action: ACTIONS.CREATE,
description: "Create roles",
},
{
id: "11",
module: MODULES.ROLE_MANAGEMENT,
action: ACTIONS.READ,
description: "View roles",
},
{
id: "12",
module: MODULES.ROLE_MANAGEMENT,
action: ACTIONS.UPDATE,
description: "Update roles",
},
{
id: "13",
module: MODULES.ROLE_MANAGEMENT,
action: ACTIONS.DELETE,
description: "Delete roles",
},
],
isActive: true,
createdAt: "2024-01-01T10:00:00Z",
updatedAt: "2024-01-01T10:00:00Z",
},
{
id: "2",
name: "Administrator",
description: "Hospital administration with patient and BPJS management",
permissions: [
{
id: "1",
module: MODULES.DASHBOARD,
action: ACTIONS.READ,
description: "View dashboard",
},
{
id: "2",
module: MODULES.PASIEN_MANAJEMEN,
action: ACTIONS.CREATE,
description: "Register patients",
},
{
id: "3",
module: MODULES.PASIEN_MANAJEMEN,
action: ACTIONS.READ,
description: "View patients",
},
{
id: "4",
module: MODULES.PASIEN_MANAJEMEN,
action: ACTIONS.UPDATE,
description: "Update patient data",
},
{
id: "5",
module: MODULES.PASIEN_MEDICAL_RECORD,
action: ACTIONS.READ,
description: "View patient medical records",
},
{
id: "6",
module: MODULES.PASIEN_BPJS_CODE,
action: ACTIONS.READ,
description: "View BPJS codes",
},
{
id: "7",
module: MODULES.INTEGRASI_DATA_BPJS,
action: ACTIONS.READ,
description: "View BPJS integration",
},
],
isActive: true,
createdAt: "2024-01-02T14:30:00Z",
updatedAt: "2024-01-02T14:30:00Z",
},
{
id: "3",
name: "Dokter",
description: "Medical practitioners with patient care permissions",
permissions: [
{
id: "1",
module: MODULES.DASHBOARD,
action: ACTIONS.READ,
description: "View dashboard",
},
{
id: "2",
module: MODULES.COST_RECOMMENDATION,
action: ACTIONS.READ,
description: "View cost recommendations",
},
{
id: "3",
module: MODULES.PASIEN_MANAJEMEN,
action: ACTIONS.READ,
description: "View patient data",
},
{
id: "4",
module: MODULES.PASIEN_MEDICAL_RECORD,
action: ACTIONS.CREATE,
description: "Create medical records",
},
{
id: "5",
module: MODULES.PASIEN_MEDICAL_RECORD,
action: ACTIONS.READ,
description: "View medical records",
},
{
id: "6",
module: MODULES.PASIEN_MEDICAL_RECORD,
action: ACTIONS.UPDATE,
description: "Update medical records",
},
{
id: "7",
module: MODULES.PASIEN_BPJS_CODE,
action: ACTIONS.READ,
description: "View BPJS codes",
},
],
isActive: true,
createdAt: "2024-01-03T09:15:00Z",
updatedAt: "2024-01-03T09:15:00Z",
},
{
id: "4",
name: "Perawat",
description: "Nursing staff with patient care support permissions",
permissions: [
{
id: "1",
module: MODULES.DASHBOARD,
action: ACTIONS.READ,
description: "View dashboard",
},
{
id: "2",
module: MODULES.PASIEN_MANAJEMEN,
action: ACTIONS.READ,
description: "View patient data",
},
{
id: "3",
module: MODULES.PASIEN_MEDICAL_RECORD,
action: ACTIONS.READ,
description: "View medical records",
},
{
id: "4",
module: MODULES.PASIEN_MEDICAL_RECORD,
action: ACTIONS.UPDATE,
description: "Update medical records",
},
],
isActive: true,
createdAt: "2024-01-04T11:45:00Z",
updatedAt: "2024-01-04T11:45:00Z",
},
{
id: "5",
name: "Finance",
description: "Financial management and cost analysis permissions",
permissions: [
{
id: "1",
module: MODULES.DASHBOARD,
action: ACTIONS.READ,
description: "View dashboard",
},
{
id: "2",
module: MODULES.COST_RECOMMENDATION,
action: ACTIONS.READ,
description: "View cost recommendations",
},
{
id: "3",
module: MODULES.COST_RECOMMENDATION,
action: ACTIONS.CREATE,
description: "Create cost analysis",
},
{
id: "4",
module: MODULES.INTEGRASI_DATA_BPJS,
action: ACTIONS.READ,
description: "View BPJS integration data",
},
{
id: "5",
module: MODULES.PASIEN_BPJS_CODE,
action: ACTIONS.READ,
description: "View BPJS codes",
},
],
isActive: true,
createdAt: "2024-01-05T16:20:00Z",
updatedAt: "2024-01-05T16:20:00Z",
},
];
// Sample users data
export const sampleUsers: IUser[] = [
{
id: "1",
name: "Dr. Admin Utama",
email: "admin@claimguard.com",
phone: "+62 812-3456-7890",
role: sampleRoles[0],
department: "IT & Administration",
isActive: true,
lastLogin: "2024-01-15T08:30:00Z",
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-15T08:30:00Z",
},
{
id: "2",
name: "Siti Nurhaliza",
email: "siti.admin@claimguard.com",
phone: "+62 813-4567-8901",
role: sampleRoles[1],
department: "Administration",
isActive: true,
lastLogin: "2024-01-15T09:15:00Z",
createdAt: "2024-01-02T00:00:00Z",
updatedAt: "2024-01-15T09:15:00Z",
},
{
id: "3",
name: "Dr. Ahmad Rizki",
email: "ahmad.rizki@claimguard.com",
phone: "+62 814-5678-9012",
role: sampleRoles[2],
department: "Cardiology",
isActive: true,
lastLogin: "2024-01-15T07:45:00Z",
createdAt: "2024-01-03T00:00:00Z",
updatedAt: "2024-01-15T07:45:00Z",
},
{
id: "4",
name: "Maria Lopez",
email: "maria.lopez@claimguard.com",
phone: "+62 815-6789-0123",
role: sampleRoles[3],
department: "Emergency",
isActive: true,
lastLogin: "2024-01-15T06:30:00Z",
createdAt: "2024-01-04T00:00:00Z",
updatedAt: "2024-01-15T06:30:00Z",
},
{
id: "5",
name: "Budi Santoso",
email: "budi.finance@claimguard.com",
phone: "+62 816-7890-1234",
role: sampleRoles[4],
department: "Finance",
isActive: true,
lastLogin: "2024-01-15T08:00:00Z",
createdAt: "2024-01-05T00:00:00Z",
updatedAt: "2024-01-15T08:00:00Z",
},
];

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />