fix revision add datatable and date picker

This commit is contained in:
2025-08-13 20:14:43 +07:00
parent 39af7d1692
commit e1ce7edd9e
8 changed files with 1854 additions and 1091 deletions

View File

@@ -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 */}