fix revision add datatable and date picker
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import {
|
||||
Shield,
|
||||
Calendar,
|
||||
Filter,
|
||||
AlertCircle,
|
||||
@@ -10,7 +9,18 @@ import {
|
||||
Upload,
|
||||
Database,
|
||||
Building2,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
interface BPJSSyncLog {
|
||||
id: string;
|
||||
@@ -35,15 +45,34 @@ interface BPJSSyncStats {
|
||||
}
|
||||
|
||||
export default function BPJSSync() {
|
||||
// Helper function to get default dates (1 month back)
|
||||
const getDefaultDates = () => {
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setMonth(startDate.getMonth() - 1);
|
||||
return { startDate, endDate };
|
||||
};
|
||||
|
||||
const [statusInput, setStatusInput] = useState("all");
|
||||
const [appliedStatus, setAppliedStatus] = useState("all");
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [startDateInput, setStartDateInput] = useState("");
|
||||
const [endDateInput, setEndDateInput] = useState("");
|
||||
const [appliedStartDate, setAppliedStartDate] = useState<string | null>(null);
|
||||
const [appliedEndDate, setAppliedEndDate] = useState<string | null>(null);
|
||||
const [hasDateFiltered, setHasDateFiltered] = useState(false);
|
||||
|
||||
const { startDate: defaultStartDate, endDate: defaultEndDate } =
|
||||
getDefaultDates();
|
||||
const [startDateInput, setStartDateInput] = useState<Date | undefined>(
|
||||
defaultStartDate
|
||||
);
|
||||
const [endDateInput, setEndDateInput] = useState<Date | undefined>(
|
||||
defaultEndDate
|
||||
);
|
||||
const [appliedStartDate, setAppliedStartDate] = useState<Date | undefined>(
|
||||
defaultStartDate
|
||||
);
|
||||
const [appliedEndDate, setAppliedEndDate] = useState<Date | undefined>(
|
||||
defaultEndDate
|
||||
);
|
||||
const [hasDateFiltered, setHasDateFiltered] = useState(true); // Start with default filter applied
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
@@ -56,65 +85,105 @@ export default function BPJSSync() {
|
||||
});
|
||||
};
|
||||
|
||||
// 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,
|
||||
},
|
||||
]);
|
||||
// Sample BPJS sync logs data with additional entries for pagination
|
||||
const [syncLogs] = useState<BPJSSyncLog[]>(() => {
|
||||
const baseLogs: 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,
|
||||
},
|
||||
];
|
||||
|
||||
// Generate additional logs for pagination
|
||||
const generated: BPJSSyncLog[] = Array.from({ length: 20 }).map(
|
||||
(_, idx) => {
|
||||
const n = idx + 6;
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - Math.floor(Math.random() * 30));
|
||||
date.setHours(
|
||||
Math.floor(Math.random() * 24),
|
||||
Math.floor(Math.random() * 60)
|
||||
);
|
||||
|
||||
return {
|
||||
id: String(n),
|
||||
timestamp: date.toISOString(),
|
||||
type: Math.random() > 0.5 ? "sync" : "import",
|
||||
status:
|
||||
Math.random() > 0.8
|
||||
? "failed"
|
||||
: Math.random() > 0.1
|
||||
? "success"
|
||||
: "in_progress",
|
||||
claimsProcessed: Math.floor(Math.random() * 500) + 50,
|
||||
claimsSuccess: Math.floor(Math.random() * 450) + 40,
|
||||
claimsFailed: Math.floor(Math.random() * 20),
|
||||
source: [
|
||||
"BPJS Kesehatan API",
|
||||
"Hospital Billing System",
|
||||
"External Claims System",
|
||||
"Pharmacy System",
|
||||
][Math.floor(Math.random() * 4)],
|
||||
duration: Math.floor(Math.random() * 60) + 10,
|
||||
...(Math.random() > 0.9 && { errorMessage: "Connection timeout" }),
|
||||
} as BPJSSyncLog;
|
||||
}
|
||||
);
|
||||
|
||||
return [...baseLogs, ...generated];
|
||||
});
|
||||
|
||||
// Calculate statistics
|
||||
const stats: BPJSSyncStats = {
|
||||
@@ -134,22 +203,185 @@ export default function BPJSSync() {
|
||||
};
|
||||
|
||||
// Filter sync logs based on date and status (status applied via button)
|
||||
const filteredLogs = syncLogs.filter((log) => {
|
||||
const matchesStatus =
|
||||
appliedStatus === "all" || log.status === appliedStatus;
|
||||
let matchesDate = true;
|
||||
if (hasDateFiltered) {
|
||||
const ts = new Date(log.timestamp).getTime();
|
||||
const startOk =
|
||||
!appliedStartDate ||
|
||||
ts >= new Date(appliedStartDate + "T00:00:00").getTime();
|
||||
const endOk =
|
||||
!appliedEndDate ||
|
||||
ts <= new Date(appliedEndDate + "T23:59:59").getTime();
|
||||
matchesDate = startOk && endOk;
|
||||
}
|
||||
const filteredLogs = useMemo(() => {
|
||||
return syncLogs.filter((log) => {
|
||||
const matchesStatus =
|
||||
appliedStatus === "all" || log.status === appliedStatus;
|
||||
|
||||
return matchesStatus && matchesDate;
|
||||
let matchesDate = true;
|
||||
if (hasDateFiltered && (appliedStartDate || appliedEndDate)) {
|
||||
const logDate = new Date(log.timestamp);
|
||||
const startOk = !appliedStartDate || logDate >= appliedStartDate;
|
||||
const endOk = !appliedEndDate || logDate <= appliedEndDate;
|
||||
matchesDate = startOk && endOk;
|
||||
}
|
||||
|
||||
return matchesStatus && matchesDate;
|
||||
});
|
||||
}, [
|
||||
syncLogs,
|
||||
appliedStatus,
|
||||
hasDateFiltered,
|
||||
appliedStartDate,
|
||||
appliedEndDate,
|
||||
]);
|
||||
|
||||
const columnHelper = createColumnHelper<BPJSSyncLog>();
|
||||
const columns: ColumnDef<BPJSSyncLog, unknown>[] = useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "timeAndType",
|
||||
header: "Waktu & Type",
|
||||
cell: (info) => {
|
||||
const log = info.row.original;
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "source",
|
||||
header: "Source System",
|
||||
cell: (info) => {
|
||||
const log = info.row.original;
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "status",
|
||||
header: "Status",
|
||||
cell: (info) => {
|
||||
const log = info.row.original;
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "claimsProcessed",
|
||||
header: "Claims Processed",
|
||||
cell: (info) => {
|
||||
const log = info.row.original;
|
||||
return (
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "successRate",
|
||||
header: "Success Rate",
|
||||
cell: (info) => {
|
||||
const log = info.row.original;
|
||||
const successRate =
|
||||
log.claimsProcessed > 0
|
||||
? Math.round((log.claimsSuccess / log.claimsProcessed) * 100)
|
||||
: 0;
|
||||
const displayRate = Math.min(100, successRate); // Batasi maksimal 100%
|
||||
return (
|
||||
<div>
|
||||
<div className="text-sm text-gray-900">{successRate}%</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-green-600 h-2 rounded-full"
|
||||
style={{ width: `${displayRate}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "duration",
|
||||
header: "Duration",
|
||||
cell: (info) => {
|
||||
const log = info.row.original;
|
||||
return (
|
||||
<div className="text-sm text-gray-900">
|
||||
{log.duration > 0 ? `${log.duration}s` : "-"}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}),
|
||||
],
|
||||
[columnHelper]
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredLogs,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getRowId: (row) => row.id,
|
||||
initialState: { pagination: { pageIndex: 0, pageSize: 10 } },
|
||||
enableRowSelection: false,
|
||||
debugTable: false,
|
||||
});
|
||||
|
||||
const handleImport = async () => {
|
||||
@@ -247,15 +479,13 @@ export default function BPJSSync() {
|
||||
<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 className="text-sm font-medium text-gray-600">Total Gagal</p>
|
||||
<p className="text-2xl font-bold text-red-600">
|
||||
{stats.failedSyncs}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-purple-100 rounded-lg">
|
||||
<Shield className="h-6 w-6 text-purple-600" />
|
||||
<div className="p-3 bg-red-100 rounded-lg">
|
||||
<XCircle className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,18 +514,27 @@ export default function BPJSSync() {
|
||||
{/* Date range */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Calendar className="h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
type="date"
|
||||
value={startDateInput}
|
||||
onChange={(e) => setStartDateInput(e.target.value)}
|
||||
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
<DatePicker
|
||||
selected={startDateInput}
|
||||
onChange={(date) => setStartDateInput(date || undefined)}
|
||||
selectsStart
|
||||
startDate={startDateInput}
|
||||
endDate={endDateInput}
|
||||
placeholderText="Start Date"
|
||||
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent w-36"
|
||||
dateFormat="dd MMM yyyy"
|
||||
/>
|
||||
<span className="text-gray-400 text-sm">s/d</span>
|
||||
<input
|
||||
type="date"
|
||||
value={endDateInput}
|
||||
onChange={(e) => setEndDateInput(e.target.value)}
|
||||
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
<DatePicker
|
||||
selected={endDateInput}
|
||||
onChange={(date) => setEndDateInput(date || undefined)}
|
||||
selectsEnd
|
||||
startDate={startDateInput}
|
||||
endDate={endDateInput}
|
||||
minDate={startDateInput}
|
||||
placeholderText="End Date"
|
||||
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent w-36"
|
||||
dateFormat="dd MMM yyyy"
|
||||
/>
|
||||
</div>
|
||||
{/* Status */}
|
||||
@@ -316,8 +555,8 @@ export default function BPJSSync() {
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setAppliedStartDate(startDateInput || null);
|
||||
setAppliedEndDate(endDateInput || null);
|
||||
setAppliedStartDate(startDateInput);
|
||||
setAppliedEndDate(endDateInput);
|
||||
setHasDateFiltered(Boolean(startDateInput || endDateInput));
|
||||
setAppliedStatus(statusInput);
|
||||
}}
|
||||
@@ -327,11 +566,12 @@ export default function BPJSSync() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setStartDateInput("");
|
||||
setEndDateInput("");
|
||||
setAppliedStartDate(null);
|
||||
setAppliedEndDate(null);
|
||||
setHasDateFiltered(false);
|
||||
const { startDate, endDate } = getDefaultDates();
|
||||
setStartDateInput(startDate);
|
||||
setEndDateInput(endDate);
|
||||
setAppliedStartDate(startDate);
|
||||
setAppliedEndDate(endDate);
|
||||
setHasDateFiltered(true);
|
||||
setStatusInput("all");
|
||||
setAppliedStatus("all");
|
||||
}}
|
||||
@@ -354,139 +594,100 @@ export default function BPJSSync() {
|
||||
<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>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th
|
||||
key={header.id}
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</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>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<tr key={row.id} className="hover:bg-gray-50">
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td key={cell.id} className="px-6 py-4 whitespace-nowrap">
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
{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>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination Controls */}
|
||||
<div className="bg-gray-50 px-6 py-4 border-t border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600 flex items-center space-x-3">
|
||||
<span>Page</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={Math.max(1, table.getPageCount())}
|
||||
value={table.getState().pagination.pageIndex + 1}
|
||||
onChange={(e) => {
|
||||
const page = Number(e.target.value) - 1;
|
||||
if (!Number.isNaN(page)) table.setPageIndex(page);
|
||||
}}
|
||||
className="w-16 border border-gray-300 rounded px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
<span>of {table.getPageCount() || 1}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
className="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
« First
|
||||
</button>
|
||||
<button
|
||||
className="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
‹ Prev
|
||||
</button>
|
||||
<select
|
||||
className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
|
||||
value={table.getState().pagination.pageSize}
|
||||
onChange={(e) => table.setPageSize(Number(e.target.value))}
|
||||
>
|
||||
{[10, 20, 30, 50].map((ps) => (
|
||||
<option key={ps} value={ps}>
|
||||
{ps}/page
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
Next ›
|
||||
</button>
|
||||
<button
|
||||
className="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
Last »
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Empty State */}
|
||||
|
||||
Reference in New Issue
Block a user