import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { ChevronLeft, ChevronRight, ExternalLink, FileSpreadsheet, LoaderCircle, Search, Unplug } from 'lucide-react'; import { ApiError, api, get, post, relativeTime } from '@/lib/api'; import { Badge, Button, Card, CardContent, CardHeader, CardTitle, EmptyState, Input, Skeleton } from '@/components/ui'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; export interface GoogleParsedTable { fileName: string; sheetName: string | null; headers: string[]; rows: string[][]; warnings: string[]; } interface ConnectionStatus { configured: boolean; connected: boolean; connectedAt: string | null; scopes: string[]; } interface DriveFile { id: string; name: string; modifiedTime: string | null; } interface FilePage { files: DriveFile[]; nextPageToken: string | null; incomplete: boolean; } interface SpreadsheetMetadata { title: string; sheets: { sheetId: number; title: string; rowCount: number; columnCount: number }[]; } export function GoogleSheetsSource({ onLoaded }: { onLoaded(table: GoogleParsedTable): void }) { const queryClient = useQueryClient(); const [searchDraft, setSearchDraft] = useState(''); const [search, setSearch] = useState(''); const [pageToken, setPageToken] = useState(null); const [previousTokens, setPreviousTokens] = useState<(string | null)[]>([]); const [spreadsheetId, setSpreadsheetId] = useState(''); const [sheetId, setSheetId] = useState(''); const [range, setRange] = useState('A1:Z2001'); const { data: status, isLoading: statusLoading } = useQuery({ queryKey: ['google-sheets', 'status'], queryFn: () => get('/api/imports/google/status'), }); const connect = useMutation({ mutationFn: () => post<{ authorizationUrl: string }>('/api/imports/google/connect', {}), onSuccess: ({ authorizationUrl }) => window.location.assign(authorizationUrl), }); const disconnect = useMutation({ mutationFn: () => api('/api/imports/google/connection', { method: 'DELETE' }), onSuccess: async () => { setSpreadsheetId(''); setSheetId(''); await queryClient.invalidateQueries({ queryKey: ['google-sheets'] }); }, }); const files = useQuery({ queryKey: ['google-sheets', 'files', search, pageToken], queryFn: () => { const parameters = new URLSearchParams(); if (search) parameters.set('search', search); if (pageToken) parameters.set('pageToken', pageToken); return get(`/api/imports/google/files?${parameters}`); }, enabled: status?.connected === true, }); const metadata = useQuery({ queryKey: ['google-sheets', 'metadata', spreadsheetId], queryFn: () => get(`/api/imports/google/spreadsheets/${encodeURIComponent(spreadsheetId)}/sheets`), enabled: Boolean(spreadsheetId), }); const load = useMutation({ mutationFn: () => post('/api/imports/google/table', { spreadsheetId, sheetId: Number(sheetId), range, }), onSuccess: onLoaded, }); if (statusLoading) return ; if (!status?.configured) { return } title="Google Sheets is not configured" description="An operator must configure the Google OAuth client and PIG settings encryption key before connecting." />; } if (!status.connected) { return ( Connect Google Sheets

PIG requests read-only spreadsheet values and Drive metadata only when you start an import. Tokens remain encrypted on the server and are never returned to this page.

{connect.isError ? : null}
); } const selectedSheet = metadata.data?.sheets.find((sheet) => String(sheet.sheetId) === sheetId); return (

Google Sheets connected

Read only

{status.connectedAt ? `Connected ${relativeTime(status.connectedAt)}` : 'Encrypted server-side connection'} · selecting a range only stages a preview

{disconnect.isError ? : null} Choose a spreadsheet
{ event.preventDefault(); setSearch(searchDraft.trim()); setPageToken(null); setPreviousTokens([]); }}> setSearchDraft(event.target.value)} placeholder="Search spreadsheet names" />
{files.isLoading ? : files.isError ? : files.data?.files.length === 0 ? : (
{files.data?.files.map((file) => ( ))}
)} {files.data?.incomplete ?

Google reported an incomplete Drive search. Narrow the spreadsheet name and search again.

: null}
Up to 50 per page
{spreadsheetId ? ( Choose a sheet and bounded range

A rectangular A1 range must include one header row and at most 2,000 data rows by 100 columns.

{metadata.isError ?
: null} {selectedSheet ?

Selected grid: {selectedSheet.rowCount} rows × {selectedSheet.columnCount} columns. Range limits are enforced again by the server.

: null} {load.isError ?
: null}
) : null}
); } function ErrorText({ error }: { error: unknown }) { return

{error instanceof ApiError || error instanceof Error ? error.message : 'Google Sheets request failed.'}

; }