initial
This commit is contained in:
59
src/App.tsx
Normal file
59
src/App.tsx
Normal 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
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
94
src/components/Layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
15
src/components/ProtectedRoute.tsx
Normal file
15
src/components/ProtectedRoute.tsx
Normal 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
308
src/components/Sidebar.tsx
Normal 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
44
src/index.css
Normal 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
10
src/main.tsx
Normal 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
675
src/pages/BPJSCode.tsx
Normal 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
461
src/pages/BPJSSync.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
681
src/pages/CostRecommendation.tsx
Normal file
681
src/pages/CostRecommendation.tsx
Normal 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
339
src/pages/Dashboard.tsx
Normal 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
318
src/pages/Login.tsx
Normal 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
470
src/pages/MedicalRecord.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
552
src/pages/MedicalRecordSync.tsx
Normal file
552
src/pages/MedicalRecordSync.tsx
Normal 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
158
src/pages/NotFound.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
114
src/pages/NotFoundProtected.tsx
Normal file
114
src/pages/NotFoundProtected.tsx
Normal 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
284
src/pages/Patients.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
464
src/pages/RoleManagement.tsx
Normal file
464
src/pages/RoleManagement.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
523
src/pages/UserManagement.tsx
Normal file
523
src/pages/UserManagement.tsx
Normal 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
400
src/types/roles.ts
Normal 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
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user