'use client' import { useState, useEffect, useRef, useCallback } from 'react' import { toast } from 'sonner' import { formatDateTime } from '@/lib/utils' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog' import { PlayCircle, Loader2, CheckCircle2, XCircle, Clock, FileText, FolderOpen, AlertTriangle, RefreshCw, Zap, } from 'lucide-react' const DEFAULT_DRIVE_ID = 'b!OYuzIexQkkOvfEPyMJPzzZHfzTrOCOdPhTWgTlzKs6M0ZWVrAc6LR4LjWl4QFEzm' const DEFAULT_FOLDER_PATH = 'Claims/SHAPE Accounts' interface SpDrive { id: string; name: string; type: string } interface SpFolder { id: string; name: string } interface Run { id: string startedAt: string completedAt: string | null status: string dryRun: boolean driveId: string stats: Record | null user?: { displayName: string | null; email: string } | null } interface LogLine { seq: number line: string } function StatusBadge({ status }: { status: string }) { if (status === 'completed') return Completed if (status === 'failed') return Failed if (status === 'running') return Running return Pending } function StatLine({ label, value }: { label: string; value: number | string }) { return (
{label} {value}
) } function formatDate(iso: string) { return formatDateTime(iso, { timeZoneName: undefined }) } export function ShapeImportPanel({ initialRuns }: { initialRuns: Run[] }) { const [mounted, setMounted] = useState(false) useEffect(() => setMounted(true), []) const [runs, setRuns] = useState(initialRuns) // SharePoint picker state const [drives, setDrives] = useState([]) const [folders, setFolders] = useState([]) const [selectedDriveId, setSelectedDriveId] = useState(DEFAULT_DRIVE_ID) const [selectedFolderId, setSelectedFolderId] = useState('') const [folderPath, setFolderPath] = useState(DEFAULT_FOLDER_PATH) const [loadingDrives, setLoadingDrives] = useState(false) const [loadingFolders, setLoadingFolders] = useState(false) const [pickerError, setPickerError] = useState(null) const [activeRunId, setActiveRunId] = useState(null) const [activeRunStatus, setActiveRunStatus] = useState('idle') const [logLines, setLogLines] = useState([]) const [lastSeq, setLastSeq] = useState(-1) const [viewingRunId, setViewingRunId] = useState(null) const [confirmOpen, setConfirmOpen] = useState(false) const [starting, setStarting] = useState(false) const [gapFillRunning, setGapFillRunning] = useState(false) const [gapFillResult, setGapFillResult] = useState | null>(null) const logRef = useRef(null) const pollRef = useRef | null>(null) const scrollToBottom = useCallback(() => { if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight }, []) useEffect(() => { scrollToBottom() }, [logLines, scrollToBottom]) const stopPolling = useCallback(() => { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null } }, []) const pollRun = useCallback(async (runId: string, currentSeq: number) => { try { const res = await fetch(`/api/admin/shape-import/${runId}?after=${currentSeq}`) if (!res.ok) return const data = await res.json() if (data.logs && data.logs.length > 0) { setLogLines((prev) => { const existing = new Set(prev.map((l) => l.seq)) const newLines = (data.logs as LogLine[]).filter((l) => !existing.has(l.seq)) return [...prev, ...newLines] }) const maxSeq = Math.max(...(data.logs as LogLine[]).map((l) => l.seq)) setLastSeq(maxSeq) currentSeq = maxSeq } if (data.run?.status === 'completed' || data.run?.status === 'failed') { setActiveRunStatus(data.run.status) stopPolling() setRuns((prev) => prev.map((r) => (r.id === runId ? { ...r, status: data.run.status, completedAt: data.run.completedAt, stats: data.run.stats } : r)) ) } } catch { // transient — keep polling } }, [stopPolling]) const startPolling = useCallback((runId: string) => { stopPolling() let seq = -1 pollRef.current = setInterval(async () => { try { const res = await fetch(`/api/admin/shape-import/${runId}?after=${seq}`) if (!res.ok) return const data = await res.json() if (data.logs && data.logs.length > 0) { setLogLines((prev) => { const existing = new Set(prev.map((l) => l.seq)) const newLines = (data.logs as LogLine[]).filter((l) => !existing.has(l.seq)) return [...prev, ...newLines] }) seq = Math.max(...(data.logs as LogLine[]).map((l) => l.seq)) setLastSeq(seq) } if (data.run?.status === 'completed' || data.run?.status === 'failed') { setActiveRunStatus(data.run.status) stopPolling() setRuns((prev) => prev.map((r) => (r.id === runId ? { ...r, status: data.run.status, completedAt: data.run.completedAt, stats: data.run.stats } : r)) ) if (data.run.status === 'completed') toast.success('Import completed') else toast.error('Import failed — check the log for details') } } catch { /* keep polling */ } }, 2000) }, [stopPolling]) useEffect(() => () => stopPolling(), [stopPolling]) // Load document libraries on mount (site is fixed to Commercial) useEffect(() => { setLoadingDrives(true) setPickerError(null) fetch('/api/admin/shape-import/browse?level=drives') .then((r) => r.json()) .then((d) => { if (d.drives) setDrives(d.drives); else setPickerError(d.error ?? 'Failed to load document libraries') }) .catch((e) => setPickerError(e.message)) .finally(() => setLoadingDrives(false)) }, []) const handleDriveChange = async (driveId: string) => { setSelectedDriveId(driveId) setSelectedFolderId('') setFolderPath(DEFAULT_FOLDER_PATH) setFolders([]) setLoadingFolders(true) setPickerError(null) try { const res = await fetch(`/api/admin/shape-import/browse?level=folders&driveId=${encodeURIComponent(driveId)}`) const d = await res.json() if (d.folders) setFolders(d.folders) else setPickerError(d.error ?? 'Failed to load folders') } catch (e: any) { setPickerError(e.message) } finally { setLoadingFolders(false) } } const handleFolderChange = async (folderId: string) => { setSelectedFolderId(folderId) const folderName = folders.find((f) => f.id === folderId)?.name ?? '' // Build path: drill one level into selected folder const newPath = folderName ? folderName : DEFAULT_FOLDER_PATH setFolderPath(newPath) // Load subfolders so user can go deeper if needed setLoadingFolders(true) try { const res = await fetch(`/api/admin/shape-import/browse?level=folders&driveId=${encodeURIComponent(selectedDriveId)}&itemId=${encodeURIComponent(folderId)}`) const d = await res.json() if (d.folders && d.folders.length > 0) setFolders(d.folders) } catch { /* ignore */ } finally { setLoadingFolders(false) } } const startRun = async (dryRun: boolean) => { setStarting(true) setLogLines([]) setLastSeq(-1) try { const res = await fetch('/api/admin/shape-import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dryRun, driveId: selectedDriveId, folderPath }), }) const data = await res.json() if (!res.ok) throw new Error(data.error || 'Failed to start') const newRun: Run = { id: data.runId, startedAt: new Date().toISOString(), completedAt: null, status: 'running', dryRun, driveId: selectedDriveId, stats: null } setRuns((prev) => [newRun, ...prev]) setActiveRunId(data.runId) setViewingRunId(data.runId) setActiveRunStatus('running') startPolling(data.runId) toast.info(dryRun ? 'Dry run started — results will appear in the log' : 'Import started') } catch (err: any) { toast.error(err.message || 'Failed to start import') } finally { setStarting(false) } } const loadRunLog = async (runId: string) => { setViewingRunId(runId) setLogLines([]) setLastSeq(-1) try { const res = await fetch(`/api/admin/shape-import/${runId}?after=-1`) if (!res.ok) return const data = await res.json() setLogLines(data.logs || []) if (data.logs?.length) setLastSeq(data.logs[data.logs.length - 1].seq) } catch { /* ignore */ } } const isRunning = activeRunStatus === 'running' && activeRunId !== null const canRun = !!selectedDriveId && !isRunning && !starting return (
{/* Configuration */} SHAPE Historical Import

Import historical task completion data from SHAPE Excel files on SharePoint into Horizon. Always run a dry run first — it reads SharePoint and shows what would be created/updated without writing anything to the database.

{/* SharePoint location picker */}

SharePoint Location

{pickerError && (

{pickerError}

)}
Site: seubert365.sharepoint.com/sites/Commercial
{/* Document library (drive) */}
{/* Folder */} {folders.length > 0 && (
)} {/* Summary line */}

Path: {folderPath}

{gapFillResult && (

Gap Fill complete

Group tasks: {gapFillResult.groupTasksCreated} · Policy tasks: {gapFillResult.policyTasksCreated} · Client tasks: {gapFillResult.clientTasksCreated} · Total: {gapFillResult.totalCreated}

{gapFillResult.errors > 0 &&

{gapFillResult.errors} errors

}
)} {isRunning && (
Import running — log updating live below…
)}
{/* Log viewer */} {viewingRunId && ( Import Log {isRunning && viewingRunId === activeRunId && ( Live )} {(() => { const run = runs.find((r) => r.id === viewingRunId) return run ? : null })()} {/* Stats summary (completed runs) */} {(() => { const run = runs.find((r) => r.id === viewingRunId) if (!run?.stats || run.status === 'running') return null const s = run.stats return (

Clients

Tasks

Other

Issues

) })()} {/* Log */}
              {logLines.length === 0
                ? '(waiting for output...)'
                : logLines.map((l) => l.line).join('\n')}
            
)} {/* Run history */} Run History {runs.length === 0 ? (

No runs yet

) : (
{runs.map((run) => ( ))}
)}
{/* Execute confirmation dialog */} Execute SHAPE Import

This will write data to the database:

  • Create or update tasks for matched SHAPE clients
  • Mark historical tasks as Completed or N/A
  • Assign claims advocates to clients (if not already set)
  • Delete duplicate tasks

Run a dry run first and review the log if you haven't already. This action cannot be automatically undone.

) }