import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { connectorsApi } from '../../api/settingsApi';
import type { ConnectorInfo, ConnectorLog } from '../../api/settingsApi';
import {
Plug,
Loader2,
RefreshCw,
ChevronDown,
ChevronUp,
Clock,
CheckCircle2,
AlertCircle,
CircleHelp,
ExternalLink,
Trash2,
FileText,
} from 'lucide-react';
/* βββ Styling constants (match SettingsPage / BillingTab) ββββββββββββ */
const btnClass =
'rounded-lg bg-brand-600 py-2 px-4 text-sm font-semibold text-white hover:bg-brand-700 disabled:opacity-50';
const btnOutlineClass =
'rounded-lg border border-surface-300 py-2 px-4 text-sm font-medium text-surface-700 hover:bg-surface-50 disabled:opacity-50';
const btnDangerClass =
'rounded-lg border border-red-200 py-2 px-4 text-sm font-medium text-red-600 hover:bg-red-50 disabled:opacity-50';
/* βββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
function timeAgo(dateStr: string | null): string {
if (!dateStr) return 'Never';
const diffMs = Date.now() - new Date(dateStr).getTime();
const seconds = Math.floor(diffMs / 1000);
if (seconds < 60) return 'Just now';
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
function statusBadge(status: string) {
switch (status) {
case 'connected':
case 'success':
return (
<span className="inline-flex items-center gap-1 rounded-full bg-green-50 px-2.5 py-1 text-xs font-medium text-green-700">
<CheckCircle2 className="h-3 w-3" /> Connected
</span>
);
case 'error':
case 'failed':
return (
<span className="inline-flex items-center gap-1 rounded-full bg-red-50 px-2.5 py-1 text-xs font-medium text-red-700">
<AlertCircle className="h-3 w-3" /> Failed
</span>
);
case 'syncing':
return (
<span className="inline-flex items-center gap-1 rounded-full bg-brand-50 px-2.5 py-1 text-xs font-medium text-brand-700">
<Loader2 className="h-3 w-3 animate-spin" /> Syncing
</span>
);
default:
return (
<span className="inline-flex items-center gap-1 rounded-full bg-surface-100 px-2.5 py-1 text-xs font-medium text-surface-600">
<CircleHelp className="h-3 w-3" /> Never synced
</span>
);
}
}
function logStatusBadge(status: string) {
switch (status) {
case 'success':
return <span className="text-green-600">Success</span>;
case 'failed':
case 'error':
return <span className="text-red-600">Failed</span>;
case 'pending':
case 'syncing':
return <span className="text-brand-600">Running</span>;
default:
return <span className="text-surface-500">{status}</span>;
}
}
/* βββ Sync Logs Collapsible ββββββββββββββββββββββββββββββββββββββββββ */
interface SyncLogsProps {
connectorId: string;
serviceName: string;
}
function SyncLogs({ connectorId, serviceName }: SyncLogsProps) {
const [isOpen, setIsOpen] = useState(false);
const { data, isLoading } = useQuery({
queryKey: ['connector-logs', connectorId],
queryFn: () => connectorsApi.getConnectorLogs(connectorId),
enabled: isOpen,
staleTime: 30_000,
refetchOnWindowFocus: false,
});
return (
<div className="mt-3 rounded-lg border border-surface-200 bg-surface-50">
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="flex w-full items-center justify-between px-4 py-2.5 text-sm font-medium text-surface-700 hover:bg-surface-100"
>
<span className="flex items-center gap-2">
<FileText className="h-4 w-4 text-surface-400" />
Sync Logs for {serviceName}
{data && <span className="text-xs text-surface-400">({data.logs.length} entries)</span>}
</span>
{isOpen ? (
<ChevronUp className="h-4 w-4 text-surface-400" />
) : (
<ChevronDown className="h-4 w-4 text-surface-400" />
)}
</button>
{isOpen && (
<div className="max-h-48 overflow-y-auto border-t border-surface-200 px-4 py-2">
{isLoading ? (
<div className="flex items-center gap-2 py-2 text-sm text-surface-500">
<Loader2 className="h-4 w-4 animate-spin" />
Loading logsβ¦
</div>
) : !data?.logs.length ? (
<p className="py-2 text-sm text-surface-400">No sync logs available.</p>
) : (
<div className="space-y-1.5">
{data.logs.map((log: ConnectorLog) => (
<div
key={log.id}
className="flex items-center justify-between rounded-md bg-white px-3 py-1.5 text-xs"
>
<div className="flex items-center gap-2">
{logStatusBadge(log.status)}
<span className="text-surface-600">{log.event_type}</span>
{log.record_count > 0 && (
<span className="text-surface-400">{log.record_count} records</span>
)}
{log.duration_ms && (
<span className="text-surface-400">{log.duration_ms}ms</span>
)}
</div>
<div className="flex items-center gap-1.5">
{log.error_message && (
<span className="text-red-500 truncate max-w-[150px]" title={log.error_message}>
{log.error_message}
</span>
)}
<span className="shrink-0 text-surface-400">
{timeAgo(log.created_at)}
</span>
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
);
}
/* βββ Connected Integration Card βββββββββββββββββββββββββββββββββββββ */
interface ConnectedCardProps {
connector: ConnectorInfo;
onDisconnect: (id: string) => void;
onSync: (id: string) => void;
disconnectingIds: string[];
syncingIds: string[];
}
function ConnectedCard({ connector, onDisconnect, onSync, disconnectingIds, syncingIds }: ConnectedCardProps) {
return (
<div className="rounded-xl border border-surface-200 bg-white p-6">
<div className="mb-3 flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-surface-100">
<Plug className="h-5 w-5 text-surface-500" />
</div>
<div>
<h4 className="text-sm font-semibold text-surface-900">{connector.serviceDisplayName}</h4>
<div className="mt-0.5 flex items-center gap-2">
{statusBadge(connector.status)}
<span className="flex items-center gap-1 text-xs text-surface-400">
<Clock className="h-3 w-3" />
{timeAgo(connector.last_sync_at)}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => onSync(connector.id)}
disabled={disconnectingIds.includes(connector.id) || syncingIds.includes(connector.id)}
className={btnOutlineClass}
title="Sync now"
>
{syncingIds.includes(connector.id) ? (
<span className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Syncingβ¦
</span>
) : (
<span className="flex items-center gap-2">
<RefreshCw className="h-4 w-4" />
Sync Now
</span>
)}
</button>
<button
type="button"
onClick={() => onDisconnect(connector.id)}
disabled={disconnectingIds.includes(connector.id) || syncingIds.includes(connector.id)}
className={btnDangerClass}
title="Disconnect"
>
{disconnectingIds.includes(connector.id) ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<span className="flex items-center gap-2">
<Trash2 className="h-4 w-4" />
Disconnect
</span>
)}
</button>
</div>
</div>
{connector.error_message && (
<div className="mb-3 flex items-start gap-2 rounded-lg bg-red-50 p-3 text-xs text-red-600">
<AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<span>{connector.error_message}</span>
</div>
)}
<div className="flex items-center gap-4 text-xs text-surface-500">
<span>Sync: {connector.sync_frequency}</span>
<span>Auth: {connector.auth_type === 'oauth2' ? 'OAuth 2.0' : 'Manual'}</span>
<span>Connected: {timeAgo(connector.created_at).replace(' ago', '')}</span>
</div>
{/* Sync Logs */}
<SyncLogs connectorId={connector.id} serviceName={connector.serviceDisplayName} />
</div>
);
}
/* βββ Available Integration Card βββββββββββββββββββββββββββββββββββββ */
interface AvailableCardProps {
service: string;
displayName: string;
description: string;
onConnect: (service: string) => void;
connectingService: string | null;
}
function AvailableCard({ service, displayName, description, onConnect, connectingService }: AvailableCardProps) {
return (
<div className="rounded-xl border border-surface-200 bg-white p-5">
<div className="mb-3 flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-surface-100">
<Plug className="h-5 w-5 text-surface-500" />
</div>
<div>
<h4 className="text-sm font-semibold text-surface-900">{displayName}</h4>
<p className="mt-0.5 text-xs text-surface-500 line-clamp-2">{description}</p>
</div>
</div>
<button
type="button"
onClick={() => onConnect(service)}
disabled={connectingService === service}
className={btnClass}
>
{connectingService === service ? (
<span className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Redirectingβ¦
</span>
) : (
<span className="flex items-center gap-2">
<ExternalLink className="h-4 w-4" />
Connect
</span>
)}
</button>
</div>
);
}
/* βββ IntegrationsTab ββββββββββββββββββββββββββββββββββββββββββββββββ */
export default function IntegrationsTab() {
const queryClient = useQueryClient();
const [connectingService, setConnectingService] = useState<string | null>(null);
const [disconnectingIds, setDisconnectingIds] = useState<string[]>([]);
const [syncingIds, setSyncingIds] = useState<string[]>([]);
// βββ Fetch data ββββββββββββββββββββββββββββββββββββββββββββββββββ
const { data: availableData, isLoading: availableLoading } = useQuery({
queryKey: ['available-integrations'],
queryFn: connectorsApi.getAvailableIntegrations,
staleTime: 300_000,
refetchOnWindowFocus: false,
});
const { data: connectedData, isLoading: connectedLoading } = useQuery({
queryKey: ['connected-integrations'],
queryFn: connectorsApi.getConnectedIntegrations,
staleTime: 60_000,
refetchOnWindowFocus: false,
});
// βββ Mutations βββββββββββββββββββββββββββββββββββββββββββββββββββ
const disconnectMutation = useMutation({
mutationFn: (connectorId: string) => connectorsApi.disconnectIntegration(connectorId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['connected-integrations'] });
},
});
const syncMutation = useMutation({
mutationFn: (connectorId: string) => connectorsApi.triggerSync(connectorId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['connected-integrations'] });
queryClient.invalidateQueries({ queryKey: ['connector-logs'] });
},
});
const connectMutation = useMutation({
mutationFn: (service: string) => connectorsApi.connectIntegration(service),
onSuccess: (data) => {
if (data.redirect_url) {
window.location.href = data.redirect_url;
}
},
onSettled: () => {
setConnectingService(null);
},
});
// βββ Handlers ββββββββββββββββββββββββββββββββββββββββββββββββββββ
const handleConnect = (service: string) => {
setConnectingService(service);
connectMutation.mutate(service);
};
const handleDisconnect = (connectorId: string) => {
if (!window.confirm('Disconnect this integration? This will remove all synced data.')) return;
setDisconnectingIds(prev => [...prev, connectorId]);
disconnectMutation.mutate(connectorId, {
onSettled: () => {
setDisconnectingIds(prev => prev.filter(id => id !== connectorId));
},
});
};
const handleSync = (connectorId: string) => {
setSyncingIds(prev => [...prev, connectorId]);
syncMutation.mutate(connectorId, {
onSettled: () => {
setSyncingIds(prev => prev.filter(id => id !== connectorId));
},
});
};
// βββ Derived state βββββββββββββββββββββββββββββββββββββββββββββββ
const connectedServices = new Set(connectedData?.connectors.map(c => c.service) || []);
const availableIntegrations = availableData || [];
const connectedConnectors = connectedData?.connectors || [];
const unconnectedIntegrations = availableIntegrations.filter(i => !connectedServices.has(i.service));
const isLoading = availableLoading || connectedLoading;
// βββ Render ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (isLoading) {
return (
<div className="rounded-xl border border-surface-200 bg-white p-6">
<div className="flex items-center gap-3 text-surface-500">
<Loader2 className="h-5 w-5 animate-spin" />
<span className="text-sm">Loading integrationsβ¦</span>
</div>
</div>
);
}
return (
<div className="space-y-8">
{/* βββ Section header ββββββββββββββββββββββββββββββββββββββββ */}
<div>
<h2 className="text-lg font-semibold text-surface-900">Integrations</h2>
<p className="mt-1 text-sm text-surface-500">
Connect your business tools to automate data sync and unlock revenue insights.
</p>
</div>
{/* βββ Connected Integrations ββββββββββββββββββββββββββββββββ */}
<section>
<h3 className="mb-3 text-sm font-semibold text-surface-700">
Connected Integrations{' '}
<span className="text-xs font-normal text-surface-400">
({connectedConnectors.length} connected)
</span>
</h3>
{connectedConnectors.length === 0 ? (
<div className="rounded-xl border border-surface-200 bg-white p-8 text-center">
<Plug className="mx-auto h-8 w-8 text-surface-300" />
<p className="mt-3 text-sm font-medium text-surface-600">No integrations connected</p>
<p className="mt-1 text-xs text-surface-400">
Connect a service below to get started.
</p>
</div>
) : (
<div className="space-y-3">
{connectedConnectors.map(connector => (
<ConnectedCard
key={connector.id}
connector={connector}
onDisconnect={handleDisconnect}
onSync={handleSync}
disconnectingIds={disconnectingIds}
syncingIds={syncingIds}
/>
))}
</div>
)}
</section>
{/* βββ Available Integrations ββββββββββββββββββββββββββββββββ */}
<section>
<h3 className="mb-3 text-sm font-semibold text-surface-700">
Available Integrations{' '}
<span className="text-xs font-normal text-surface-400">
({unconnectedIntegrations.length} available)
</span>
</h3>
{unconnectedIntegrations.length === 0 ? (
<div className="rounded-xl border border-surface-200 bg-white p-6 text-center">
<CheckCircle2 className="mx-auto h-8 w-8 text-green-400" />
<p className="mt-3 text-sm font-medium text-surface-600">All integrations connected</p>
<p className="mt-1 text-xs text-surface-400">
You have connected all available integrations.
</p>
</div>
) : (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{unconnectedIntegrations.map(integration => (
<AvailableCard
key={integration.service}
service={integration.service}
displayName={integration.display_name}
description={integration.description}
onConnect={handleConnect}
connectingService={connectingService}
/>
))}
</div>
)}
</section>
</div>
);
}