update page

This commit is contained in:
2026-02-01 16:38:08 +01:00
parent 1f00856167
commit 39655c2913
11 changed files with 5192 additions and 992 deletions

View File

@@ -12,19 +12,15 @@ import {
CheckCircle,
XCircle,
AlertCircle,
Cloud,
Server,
RefreshCw,
Loader2
} from 'lucide-react';
import { WorkPackage } from '@/types/traceability';
import { parseCSVContent, ParseResult } from '@/lib/csvParser';
import {
fetchFromOpenProject,
fetchFromBackendProxy,
OpenProjectConfig,
DEFAULT_CONFIG
} from '@/services/openProjectService';
const STORAGE_KEY = 'traceability_data';
const SERVER_URL_KEY = 'traceability_server_url';
interface DataUpdateDialogProps {
onDataLoaded: (workPackages: WorkPackage[]) => void;
@@ -40,11 +36,10 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
const [fileName, setFileName] = useState<string>('');
const fileInputRef = useRef<HTMLInputElement>(null);
// Backend proxy config
const [proxyUrl, setProxyUrl] = useState('');
// Direct OpenProject config
const [opConfig, setOpConfig] = useState<OpenProjectConfig>(DEFAULT_CONFIG);
// Server endpoint config - persisted
const [serverUrl, setServerUrl] = useState(() =>
localStorage.getItem(SERVER_URL_KEY) || '/api/traceability'
);
// Drag and drop state
const [isDragging, setIsDragging] = useState(false);
@@ -53,6 +48,15 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
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);
@@ -60,10 +64,13 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
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(result.logs);
setLogs(prev => [...prev, ...result.logs]);
setErrors(result.errors);
} finally {
setIsLoading(false);
@@ -84,66 +91,87 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
if (file) handleFile(file);
};
const handleFetchFromProxy = async () => {
if (!proxyUrl) {
setErrors(['Please enter a backend proxy URL']);
const handleFetchFromServer = async () => {
if (!serverUrl) {
setErrors(['Please enter a server endpoint URL']);
return;
}
// Save URL for next time
localStorage.setItem(SERVER_URL_KEY, serverUrl);
setIsLoading(true);
setLogs([]);
setErrors([]);
setParseResult(null);
try {
const result = await fetchFromBackendProxy(proxyUrl, addLog);
setLogs(result.logs);
setErrors(result.errors);
addLog(`🔍 Connecting to server: ${serverUrl}`);
if (result.success) {
const typeCounts = result.workPackages.reduce((acc, wp) => {
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<string, number>);
setParseResult({
success: true,
workPackages: result.workPackages,
logs: result.logs,
errors: result.errors,
workPackages,
logs: [],
errors: [],
typeCounts
});
} else {
// Try to parse as CSV anyway
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);
}
} finally {
setIsLoading(false);
}
};
const handleFetchFromOpenProject = async () => {
setIsLoading(true);
setLogs([]);
setErrors([]);
setParseResult(null);
try {
const result = await fetchFromOpenProject(opConfig, undefined, addLog);
setLogs(result.logs);
setErrors(result.errors);
if (result.success) {
const typeCounts = result.workPackages.reduce((acc, wp) => {
acc[wp.type] = (acc[wp.type] || 0) + 1;
return acc;
}, {} as Record<string, number>);
setParseResult({
success: true,
workPackages: result.workPackages,
logs: result.logs,
errors: result.errors,
typeCounts
});
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
setErrors([errorMsg]);
addLog(`❌ Error: ${errorMsg}`);
} finally {
setIsLoading(false);
}
@@ -151,6 +179,8 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
const handleApply = () => {
if (parseResult?.success && parseResult.workPackages.length > 0) {
// Persist to localStorage for other users/sessions
persistData(parseResult.workPackages);
onDataLoaded(parseResult.workPackages);
onClose?.();
}
@@ -174,23 +204,19 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
Update Traceability Data
</CardTitle>
<CardDescription>
Choose how to update your data from OpenProject
Upload a CSV file or fetch from your server
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="upload" className="flex items-center gap-2">
<Upload className="h-4 w-4" />
Upload CSV
</TabsTrigger>
<TabsTrigger value="proxy" className="flex items-center gap-2">
<TabsTrigger value="server" className="flex items-center gap-2">
<Server className="h-4 w-4" />
Backend API
</TabsTrigger>
<TabsTrigger value="direct" className="flex items-center gap-2">
<Cloud className="h-4 w-4" />
Direct Fetch
Server Fetch
</TabsTrigger>
</TabsList>
@@ -222,103 +248,46 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
</p>
</div>
<p className="text-xs text-muted-foreground">
Run <code className="bg-muted px-1 rounded">python get_traceability.py</code> locally to generate the CSV
Run <code className="bg-muted px-1 rounded">python get_traceability.py</code> to generate the CSV file
</p>
</TabsContent>
{/* Tab 2: Backend Proxy */}
<TabsContent value="proxy" className="space-y-4">
{/* Tab 2: Server Fetch */}
<TabsContent value="server" className="space-y-4">
<div className="space-y-3">
<div>
<Label htmlFor="proxyUrl">Backend API Endpoint</Label>
<Label htmlFor="serverUrl">Server Endpoint</Label>
<Input
id="proxyUrl"
placeholder="https://your-server.com/api/traceability"
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
id="serverUrl"
placeholder="/api/traceability or https://your-server.com/api/data"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
<p className="text-xs text-muted-foreground mt-1">
Your backend should return JSON or CSV with work package data
Endpoint that runs your Python script and returns CSV or JSON
</p>
</div>
<Button
onClick={handleFetchFromProxy}
disabled={isLoading || !proxyUrl}
onClick={handleFetchFromServer}
disabled={isLoading || !serverUrl}
className="w-full"
>
{isLoading ? (
<><Loader2 className="h-4 w-4 mr-2 animate-spin" /> Fetching...</>
) : (
<><Server className="h-4 w-4 mr-2" /> Fetch from Backend</>
<><Server className="h-4 w-4 mr-2" /> Fetch from Server</>
)}
</Button>
</div>
<div className="bg-muted/50 rounded-lg p-3 text-xs space-y-2">
<p className="font-medium">Backend Setup Guide:</p>
<p className="font-medium">Server Setup:</p>
<ol className="list-decimal ml-4 space-y-1 text-muted-foreground">
<li>Deploy the Python script on your server</li>
<li>Create an API endpoint that runs the script</li>
<li>Return JSON or serve the generated CSV</li>
<li>Create an endpoint that runs <code>get_traceability.py</code></li>
<li>Return the CSV file or JSON with work packages</li>
<li>Example: <code>GET /api/traceability</code> returns CSV</li>
</ol>
</div>
</TabsContent>
{/* Tab 3: Direct OpenProject Fetch */}
<TabsContent value="direct" className="space-y-4">
<div className="space-y-3">
<div>
<Label htmlFor="baseUrl">OpenProject URL</Label>
<Input
id="baseUrl"
placeholder="https://openproject.example.com"
value={opConfig.baseUrl}
onChange={(e) => setOpConfig(prev => ({ ...prev, baseUrl: e.target.value }))}
/>
</div>
<div>
<Label htmlFor="projectId">Project Identifier</Label>
<Input
id="projectId"
placeholder="project-id"
value={opConfig.projectIdentifier}
onChange={(e) => setOpConfig(prev => ({ ...prev, projectIdentifier: e.target.value }))}
/>
</div>
<div>
<Label htmlFor="apiKey">API Key</Label>
<Input
id="apiKey"
type="password"
placeholder="Your OpenProject API Key"
value={opConfig.apiKey}
onChange={(e) => setOpConfig(prev => ({ ...prev, apiKey: e.target.value }))}
/>
</div>
<Button
onClick={handleFetchFromOpenProject}
disabled={isLoading}
className="w-full"
>
{isLoading ? (
<><Loader2 className="h-4 w-4 mr-2 animate-spin" /> Fetching...</>
) : (
<><Cloud className="h-4 w-4 mr-2" /> Fetch from OpenProject</>
)}
</Button>
</div>
<div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-3 text-xs">
<div className="flex items-start gap-2">
<AlertCircle className="h-4 w-4 text-amber-600 mt-0.5" />
<div>
<p className="font-medium text-amber-700">CORS Notice</p>
<p className="text-amber-600 mt-1">
Direct browser fetch may fail due to CORS. Configure your OpenProject server to allow
cross-origin requests, or use the Backend API option.
</p>
</div>
</div>
</div>
</TabsContent>
</Tabs>
{/* Results Section */}
@@ -387,7 +356,7 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
{parseResult?.success && (
<Button onClick={handleApply}>
<CheckCircle className="h-4 w-4 mr-2" />
Apply Data
Apply & Save
</Button>
)}
<Button variant="outline" onClick={handleReset}>