initial
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
69
README.md
Normal file
69
README.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default tseslint.config([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
|
||||||
|
// Remove tseslint.configs.recommended and replace with this
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
...tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
|
|
||||||
|
// Other configs...
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default tseslint.config([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
// Enable lint rules for React
|
||||||
|
reactX.configs['recommended-typescript'],
|
||||||
|
// Enable lint rules for React DOM
|
||||||
|
reactDom.configs.recommended,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
23
eslint.config.js
Normal file
23
eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default tseslint.config([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs['recommended-latest'],
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
15
index.html
Normal file
15
index.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/claim-guard.ico" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="/claim-guard.ico" />
|
||||||
|
<!-- <link rel="icon" type="image/svg+xml" href="/claim-guard.svg" /> -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>ClaimGuard - Hospital Management System</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4798
package-lock.json
generated
Normal file
4798
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
package.json
Normal file
36
package.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "claim-guard-fe",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^6.30.0",
|
||||||
|
"react-hook-form": "^7.54.2",
|
||||||
|
"lucide-react": "^0.469.0",
|
||||||
|
"clsx": "^2.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.33.0",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"eslint": "^9.33.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
|
"globals": "^16.3.0",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"typescript-eslint": "^8.39.1",
|
||||||
|
"vite": "^7.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
BIN
public/claim-guard.ico
Normal file
BIN
public/claim-guard.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
1
public/claim-guard.svg
Normal file
1
public/claim-guard.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="140.000000pt" height="152.000000pt" viewBox="0 0 140.000000 152.000000" preserveAspectRatio="xMidYMid meet"> <g transform="translate(0.000000,152.000000) scale(0.100000,-0.100000)" fill="#2e9aff" stroke="none"> <path d="M480 1385 c-143 -47 -169 -59 -197 -89 l-33 -36 0 -213 c0 -239 7 -281 66 -378 64 -106 161 -189 296 -255 88 -42 99 -41 215 23 116 65 210 153 255 240 53 101 60 149 56 385 -4 268 12 246 -244 328 -101 32 -173 50 -203 49 -26 0 -117 -24 -211 -54z m392 -46 c134 -41 168 -56 191 -81 l29 -30 -4 -212 c-3 -186 -6 -217 -25 -263 -29 -73 -85 -146 -151 -197 -64 -48 -189 -116 -215 -116 -10 0 -41 12 -70 26 -180 91 -289 217 -317 363 -16 87 -13 386 5 421 13 25 32 34 177 80 90 28 168 53 173 55 24 8 52 2 207 -46z"/> <path d="M455 1204 c-13 -13 -15 -54 -15 -265 l0 -251 29 -29 29 -29 194 0 c257 0 238 -19 238 241 l0 194 -77 77 -78 76 -152 1 c-124 0 -155 -3 -168 -15z m297 -101 l3 -58 63 0 62 0 -2 -180 -3 -180 -183 -3 c-141 -2 -183 0 -187 10 -2 7 -6 115 -7 241 l-3 227 127 0 127 0 3 -57z m66 27 c6 0 18 -13 26 -28 l15 -29 -39 0 c-38 0 -39 1 -36 34 1 18 -2 33 -6 33 -5 0 -6 5 -2 12 5 8 11 6 19 -5 7 -10 17 -17 23 -17z"/> <path d="M600 1055 c0 -20 -5 -25 -25 -25 -20 0 -25 -5 -25 -25 0 -20 5 -25 25 -25 18 0 25 -5 25 -20 0 -15 7 -20 25 -20 18 0 25 5 25 20 0 15 7 20 25 20 20 0 25 5 25 25 0 20 -5 25 -25 25 -20 0 -25 5 -25 25 0 20 -5 25 -25 25 -20 0 -25 -5 -25 -25z m45 -55 c-10 -17 -42 -7 -37 12 3 14 8 15 23 7 11 -5 17 -14 14 -19z"/> <path d="M558 868 c-13 -34 3 -38 133 -38 l129 0 0 25 0 25 -129 0 c-94 0 -130 -3 -133 -12z"/> <path d="M560 765 l0 -25 130 0 130 0 0 25 0 25 -130 0 -130 0 0 -25z"/> <path d="M163 260 c-50 -20 -60 -105 -17 -139 36 -28 60 -26 92 6 30 30 14 47 -18 18 -10 -9 -29 -15 -42 -13 -19 2 -24 10 -26 41 -5 59 36 90 68 52 16 -19 30 -19 30 -1 0 29 -51 50 -87 36z"/> <path d="M722 258 c-29 -15 -47 -73 -33 -111 12 -31 53 -51 85 -40 11 4 27 6 34 5 8 -2 12 11 12 38 0 38 -2 40 -30 40 -21 0 -30 -5 -30 -16 0 -11 5 -14 16 -10 11 4 14 1 12 -12 -4 -21 -44 -28 -64 -12 -17 15 -19 71 -2 88 17 17 44 15 73 -4 27 -17 33 -8 13 19 -17 21 -58 29 -86 15z"/> <path d="M280 179 c0 -105 20 -102 23 4 2 57 -1 77 -10 77 -10 0 -13 -22 -13 -81z"/> <path d="M1240 236 c0 -19 -4 -22 -25 -18 -14 2 -32 -1 -40 -8 -16 -13 -20 -67 -7 -86 10 -16 49 -27 56 -15 3 5 14 7 24 5 15 -5 17 3 17 70 0 54 -4 76 -12 76 -7 0 -13 -11 -13 -24z m-10 -51 c19 -23 8 -50 -20 -50 -28 0 -39 27 -20 50 16 19 24 19 40 0z"/> <path d="M353 219 c-34 -12 -28 -29 8 -27 18 2 34 -2 36 -9 3 -7 -9 -13 -29 -15 -27 -2 -34 -8 -36 -29 -3 -30 19 -44 46 -30 9 4 23 6 30 4 10 -4 12 8 10 47 -3 58 -22 75 -65 59z m47 -68 c0 -5 -7 -14 -15 -21 -12 -10 -19 -10 -28 -1 -9 9 -9 14 3 21 19 12 40 13 40 1z"/> <path d="M540 220 c-7 -4 -19 -6 -27 -3 -10 4 -13 -9 -13 -57 0 -45 3 -61 13 -57 7 2 11 16 9 33 -3 33 19 71 36 60 7 -4 12 -27 12 -52 0 -24 5 -44 10 -44 6 0 10 15 10 34 0 44 9 66 26 66 14 0 15 -3 18 -57 2 -51 21 -39 21 13 0 55 -22 77 -55 56 -11 -7 -20 -8 -20 -3 0 13 -25 20 -40 11z"/> <path d="M988 219 c-26 -15 -22 -27 10 -25 38 2 44 -18 7 -24 -23 -4 -31 -11 -33 -31 -3 -30 19 -44 46 -30 9 4 23 6 30 4 13 -5 17 73 4 93 -11 17 -45 24 -64 13z m52 -68 c0 -5 -7 -14 -15 -21 -18 -15 -42 1 -31 19 8 13 46 15 46 2z"/> <path d="M1126 221 c-3 -5 -14 -7 -24 -5 -15 4 -17 -3 -16 -52 0 -35 5 -59 12 -62 8 -2 12 11 12 43 0 38 4 48 20 52 11 3 20 9 20 14 0 13 -18 20 -24 10z"/> <path d="M450 166 c0 -30 5 -58 10 -61 6 -4 10 17 10 54 0 34 -4 61 -10 61 -5 0 -10 -24 -10 -54z"/> <path d="M852 168 l3 -53 42 -3 41 -3 4 55 c2 39 -1 56 -9 56 -7 0 -13 -18 -15 -42 -2 -35 -7 -43 -23 -43 -16 0 -21 8 -23 43 -2 23 -8 42 -13 42 -6 0 -9 -22 -7 -52z"/> </g> </svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
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" />
|
||||||
30
tailwind.config.js
Normal file
30
tailwind.config.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "#059669", // green-600
|
||||||
|
foreground: "#ffffff",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "#f3f4f6", // gray-100
|
||||||
|
foreground: "#111827", // gray-900
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "#dc2626", // red-600
|
||||||
|
foreground: "#ffffff",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "#f9fafb", // gray-50
|
||||||
|
foreground: "#6b7280", // gray-500
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["Inter", "system-ui", "sans-serif"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
27
tsconfig.app.json
Normal file
27
tsconfig.app.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
25
tsconfig.node.json
Normal file
25
tsconfig.node.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user