initial
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user