import { useState, useRef } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Upload, FileText, CheckCircle, XCircle, AlertCircle, Server, RefreshCw, Loader2, Download } from 'lucide-react'; import { WorkPackage } from '@/types/traceability'; import { parseCSVContent, ParseResult } from '@/lib/csvParser'; const STORAGE_KEY = 'traceability_data'; const SERVER_URL_KEY = 'traceability_server_url'; // API URL for production deployment const API_URL = import.meta.env.VITE_API_URL || ''; interface DataUpdateDialogProps { onDataLoaded: (workPackages: WorkPackage[]) => void; onClose?: () => void; } export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProps) { const [activeTab, setActiveTab] = useState('upload'); const [isLoading, setIsLoading] = useState(false); const [parseResult, setParseResult] = useState(null); const [logs, setLogs] = useState([]); const [errors, setErrors] = useState([]); const [fileName, setFileName] = useState(''); const fileInputRef = useRef(null); // Server endpoint config - persisted const [serverUrl, setServerUrl] = useState(() => localStorage.getItem(SERVER_URL_KEY) || (API_URL ? `${API_URL}/api/traceability` : '/api/traceability') ); // Drag and drop state const [isDragging, setIsDragging] = useState(false); const addLog = (log: string) => { setLogs(prev => [...prev, log]); }; const persistData = (workPackages: WorkPackage[]) => { const data = { lastUpdated: new Date().toISOString(), workPackages }; localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); addLog(`💾 Data persisted to storage (${workPackages.length} items)`); }; const handleFile = async (file: File) => { setFileName(file.name); setIsLoading(true); setLogs([]); setErrors([]); try { addLog(`📂 Reading file: ${file.name} (${(file.size / 1024).toFixed(1)} KB)`); const text = await file.text(); addLog(`📝 File loaded, ${text.length} characters`); const result = parseCSVContent(text); setParseResult(result); setLogs(prev => [...prev, ...result.logs]); setErrors(result.errors); } finally { setIsLoading(false); } }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); const file = e.dataTransfer.files[0]; if (file && file.name.endsWith('.csv')) { handleFile(file); } }; const handleFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) handleFile(file); }; // Reload from static CSV file const handleReloadFromCSV = async () => { setIsLoading(true); setLogs([]); setErrors([]); setParseResult(null); try { const cacheBuster = `?t=${Date.now()}`; addLog(`🔍 Fetching /data/traceability_export.csv${cacheBuster}`); const response = await fetch(`/data/traceability_export.csv${cacheBuster}`); if (!response.ok) { throw new Error(`Failed to load CSV: ${response.status} ${response.statusText}`); } const csvText = await response.text(); addLog(`📄 Received ${csvText.length} bytes`); const result = parseCSVContent(csvText); setParseResult(result); setLogs(prev => [...prev, ...result.logs]); setErrors(result.errors); } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error'; setErrors([errorMsg]); addLog(`❌ Error: ${errorMsg}`); } finally { setIsLoading(false); } }; // Trigger sync from OpenProject (runs Python script on server) const handleSyncFromServer = async () => { if (!serverUrl) { setErrors(['Please enter a server endpoint URL']); return; } localStorage.setItem(SERVER_URL_KEY, serverUrl); setIsLoading(true); setLogs([]); setErrors([]); setParseResult(null); try { // First, trigger the sync (runs Python script) const syncUrl = serverUrl.replace(/\/api\/traceability\/?$/, '/api/traceability/sync'); addLog(`🔄 Triggering sync at: ${syncUrl}`); const syncResponse = await fetch(syncUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); if (!syncResponse.ok) { const errorData = await syncResponse.json().catch(() => ({})); throw new Error(errorData.error || `Sync failed: ${syncResponse.status}`); } const syncResult = await syncResponse.json(); addLog(`✅ Sync complete: ${syncResult.message}`); if (syncResult.stdout) { syncResult.stdout.split('\n').filter(Boolean).forEach((line: string) => { addLog(`📋 ${line}`); }); } // Now fetch the updated data addLog(`🔍 Fetching updated data from: ${serverUrl}`); const response = await fetch(serverUrl, { method: 'GET', headers: { 'Accept': 'application/json, text/csv' } }); if (!response.ok) { throw new Error(`Server Error: ${response.status} ${response.statusText}`); } const contentType = response.headers.get('content-type') || ''; addLog(`📄 Response content-type: ${contentType}`); if (contentType.includes('text/csv')) { addLog(`📋 Parsing CSV response...`); const csvText = await response.text(); const result = parseCSVContent(csvText); setLogs(prev => [...prev, ...result.logs]); setErrors(result.errors); setParseResult(result); } else if (contentType.includes('application/json')) { addLog(`📋 Parsing JSON response...`); const data = await response.json(); let workPackages: WorkPackage[]; if (Array.isArray(data)) { workPackages = data; } else if (data.workPackages) { workPackages = data.workPackages; } else { throw new Error('Invalid JSON format: expected array or {workPackages: [...]}'); } addLog(`✅ Received ${workPackages.length} work packages`); const typeCounts = workPackages.reduce((acc, wp) => { acc[wp.type] = (acc[wp.type] || 0) + 1; return acc; }, {} as Record); setParseResult({ success: true, workPackages, logs: [], errors: [], typeCounts }); } else { addLog(`⚠️ Unknown content-type, attempting CSV parse...`); const text = await response.text(); const result = parseCSVContent(text); setLogs(prev => [...prev, ...result.logs]); setErrors(result.errors); setParseResult(result); } } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error'; setErrors([errorMsg]); addLog(`❌ Error: ${errorMsg}`); } finally { setIsLoading(false); } }; // Just fetch existing data (no sync) const handleFetchFromServer = async () => { if (!serverUrl) { setErrors(['Please enter a server endpoint URL']); return; } localStorage.setItem(SERVER_URL_KEY, serverUrl); setIsLoading(true); setLogs([]); setErrors([]); setParseResult(null); try { addLog(`🔍 Connecting to server: ${serverUrl}`); const response = await fetch(serverUrl, { method: 'GET', headers: { 'Accept': 'application/json, text/csv' } }); if (!response.ok) { throw new Error(`Server Error: ${response.status} ${response.statusText}`); } const contentType = response.headers.get('content-type') || ''; addLog(`📄 Response content-type: ${contentType}`); if (contentType.includes('text/csv')) { addLog(`📋 Parsing CSV response...`); const csvText = await response.text(); const result = parseCSVContent(csvText); setLogs(prev => [...prev, ...result.logs]); setErrors(result.errors); setParseResult(result); } else if (contentType.includes('application/json')) { addLog(`📋 Parsing JSON response...`); const data = await response.json(); let workPackages: WorkPackage[]; if (Array.isArray(data)) { workPackages = data; } else if (data.workPackages) { workPackages = data.workPackages; } else { throw new Error('Invalid JSON format: expected array or {workPackages: [...]}'); } addLog(`✅ Received ${workPackages.length} work packages`); const typeCounts = workPackages.reduce((acc, wp) => { acc[wp.type] = (acc[wp.type] || 0) + 1; return acc; }, {} as Record); setParseResult({ success: true, workPackages, logs: [], errors: [], typeCounts }); } else { addLog(`⚠️ Unknown content-type, attempting CSV parse...`); const text = await response.text(); const result = parseCSVContent(text); setLogs(prev => [...prev, ...result.logs]); setErrors(result.errors); setParseResult(result); } } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error'; setErrors([errorMsg]); addLog(`❌ Error: ${errorMsg}`); } finally { setIsLoading(false); } }; const handleApply = () => { if (parseResult?.success && parseResult.workPackages.length > 0) { persistData(parseResult.workPackages); onDataLoaded(parseResult.workPackages); onClose?.(); } }; const handleReset = () => { setParseResult(null); setFileName(''); setLogs([]); setErrors([]); if (fileInputRef.current) { fileInputRef.current.value = ''; } }; return ( Update Traceability Data Upload a CSV file, reload from static file, or sync from OpenProject Upload / Reload Server Sync {/* Tab 1: Manual CSV Upload / Reload */}
{ e.preventDefault(); setIsDragging(true); }} onDragLeave={() => setIsDragging(false)} className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors cursor-pointer ${ isDragging ? 'border-primary bg-primary/5' : 'border-muted-foreground/25 hover:border-primary/50' }`} onClick={() => fileInputRef.current?.click()} >

{fileName ? ( {fileName} ) : ( <>Drop CSV here or browse )}

Run python get_traceability.py to generate the CSV file, then reload.

{/* Tab 2: Server Sync */}
setServerUrl(e.target.value)} />

Uses the data service API to sync with OpenProject

How it works:

  • Sync from OpenProject: Runs the Python script on the server to fetch latest data from OpenProject API
  • Fetch Existing Data: Gets the last synced CSV from the server (no OpenProject call)

Note: Server sync only works in deployed environment with the data-service running.

{/* Results Section */} {(parseResult || errors.length > 0) && (
{parseResult?.success ? ( <> Successfully parsed {parseResult.workPackages.length} work packages ) : errors.length > 0 ? ( <> Operation failed ) : null}
{/* Type counts */} {parseResult?.success && Object.keys(parseResult.typeCounts).length > 0 && (
{Object.entries(parseResult.typeCounts) .sort(([, a], [, b]) => b - a) .map(([type, count]) => ( {type}: {count} ))}
)} {/* Errors */} {errors.length > 0 && (
Errors
{errors.map((error, i) => (

{error}

))}
)} {/* Logs */} {logs.length > 0 && (
View logs ({logs.length} entries)
{logs.map((log, i) => (
{log}
))}
)} {/* Actions */}
{parseResult?.success && ( )} {onClose && ( )}
)}
); }