whisper-money/resources/js/components/pending-operations-dialog.tsx

137 lines
5.3 KiB
TypeScript

import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import type { SyncStatus } from '@/contexts/sync-context';
import type { PendingChange } from '@/lib/dexie-db';
import { formatDistanceToNow } from 'date-fns';
import { RefreshCw } from 'lucide-react';
interface PendingOperationsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
operations: PendingChange[];
onSyncNow: () => void;
syncStatus: SyncStatus;
isOnline: boolean;
}
function getOperationBadgeClass(operation: PendingChange['operation']): string {
switch (operation) {
case 'create':
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
case 'update':
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
case 'delete':
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
default:
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
}
}
function formatDataPreview(data: Record<string, unknown>): string {
if (data.id) {
const preview = `ID: ${String(data.id).slice(0, 8)}...`;
if (data.name) {
return `${preview}, Name: ${data.name}`;
}
if (data.description) {
return `${preview}, ${String(data.description).slice(0, 20)}...`;
}
return preview;
}
return JSON.stringify(data).slice(0, 50) + '...';
}
export function PendingOperationsDialog({
open,
onOpenChange,
operations,
onSyncNow,
syncStatus,
isOnline,
}: PendingOperationsDialogProps) {
const isSyncing = syncStatus === 'syncing';
const canSync = isOnline && !isSyncing;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Pending Operations</DialogTitle>
<DialogDescription>
These operations are waiting to be synced with the
server.
</DialogDescription>
</DialogHeader>
<div className="max-h-96 overflow-y-auto">
{operations.length === 0 ? (
<div className="flex h-32 items-center justify-center text-muted-foreground">
No pending operations
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Store</TableHead>
<TableHead>Operation</TableHead>
<TableHead>Data</TableHead>
<TableHead>Time</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{operations.map((op) => (
<TableRow key={op.id}>
<TableCell className="font-medium capitalize">
{op.store.replace('_', ' ')}
</TableCell>
<TableCell>
<span
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${getOperationBadgeClass(op.operation)}`}
>
{op.operation}
</span>
</TableCell>
<TableCell className="max-w-48 truncate text-xs text-muted-foreground">
{formatDataPreview(op.data)}
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{formatDistanceToNow(
new Date(op.timestamp),
{ addSuffix: true },
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
<div className="flex justify-end pt-2">
<Button
onClick={onSyncNow}
disabled={!canSync}
className="gap-2"
>
<RefreshCw
className={`h-4 w-4 ${isSyncing ? 'animate-spin' : ''}`}
/>
{isSyncing ? 'Syncing...' : 'Sync now'}
</Button>
</div>
</DialogContent>
</Dialog>
);
}