Files
breakpilot-compliance/admin-compliance/app/sdk/roadmap/_components/ImportWizard.tsx
Sharang Parnerkar 6571b580dc refactor(admin): split roadmap page.tsx into colocated components
Split 876-LOC page.tsx into 146 LOC with 7 colocated components
(RoadmapCard, CreateRoadmapModal, CreateItemModal, ImportWizard,
RoadmapDetailView split into header + items table), plus _types.ts,
_constants.ts, and _api.ts. Behavior preserved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 22:52:20 +02:00

155 lines
7.0 KiB
TypeScript

import { useState, useRef } from 'react'
import { api, API_BASE } from '../_api'
import type { ImportJob } from '../_types'
export function ImportWizard({ onClose, onImported }: {
onClose: () => void
onImported: () => void
}) {
const [step, setStep] = useState<'upload' | 'preview' | 'confirm'>('upload')
const [importJob, setImportJob] = useState<ImportJob | null>(null)
const [uploading, setUploading] = useState(false)
const [confirming, setConfirming] = useState(false)
const [roadmapTitle, setRoadmapTitle] = useState('')
const fileRef = useRef<HTMLInputElement>(null)
const handleUpload = async () => {
const file = fileRef.current?.files?.[0]
if (!file) return
setUploading(true)
try {
const formData = new FormData()
formData.append('file', file)
const res = await fetch(`${API_BASE}/import/upload`, {
method: 'POST',
body: formData,
})
if (!res.ok) throw new Error(`Upload failed: ${res.status}`)
const data = await res.json()
const job = await api<ImportJob>(`/import/${data.job_id || data.id}`)
setImportJob(job)
setStep('preview')
} catch (err) {
console.error('Upload error:', err)
} finally {
setUploading(false)
}
}
const handleConfirm = async () => {
if (!importJob) return
setConfirming(true)
try {
await api(`/import/${importJob.id}/confirm`, {
method: 'POST',
body: JSON.stringify({
job_id: importJob.id,
roadmap_title: roadmapTitle || `Import ${new Date().toLocaleDateString('de-DE')}`,
selected_rows: importJob.items.filter(i => i.is_valid).map(i => i.row),
apply_mappings: true,
}),
})
onImported()
} catch (err) {
console.error('Confirm error:', err)
} finally {
setConfirming(false)
}
}
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={onClose}>
<div className="bg-white rounded-2xl p-6 w-full max-w-2xl max-h-[80vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
<h3 className="text-lg font-bold text-gray-900 mb-4">Roadmap importieren</h3>
{step === 'upload' && (
<div className="space-y-4">
<div className="border-2 border-dashed border-gray-300 rounded-xl p-8 text-center">
<input ref={fileRef} type="file" accept=".xlsx,.xls,.csv" className="hidden" onChange={() => {}} />
<svg className="w-12 h-12 mx-auto text-gray-400 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<button onClick={() => fileRef.current?.click()}
className="px-4 py-2 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-700">
Datei auswaehlen
</button>
<p className="text-xs text-gray-500 mt-2">Excel (.xlsx, .xls) oder CSV</p>
</div>
<div className="flex justify-end gap-3">
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg">Abbrechen</button>
<button onClick={handleUpload} disabled={uploading || !fileRef.current?.files?.length}
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50">
{uploading ? 'Lade hoch...' : 'Hochladen'}
</button>
</div>
</div>
)}
{step === 'preview' && importJob && (
<div className="space-y-4">
<div className="grid grid-cols-3 gap-3">
<div className="bg-green-50 rounded-lg p-3 text-center">
<div className="text-xl font-bold text-green-600">{importJob.valid_rows}</div>
<div className="text-xs text-gray-500">Gueltig</div>
</div>
<div className="bg-red-50 rounded-lg p-3 text-center">
<div className="text-xl font-bold text-red-600">{importJob.invalid_rows}</div>
<div className="text-xs text-gray-500">Ungueltig</div>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-xl font-bold text-gray-900">{importJob.total_rows}</div>
<div className="text-xs text-gray-500">Gesamt</div>
</div>
</div>
<div className="max-h-64 overflow-y-auto border rounded-lg">
<table className="w-full text-sm">
<thead className="bg-gray-50 sticky top-0">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Zeile</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Titel</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Kategorie</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{importJob.items?.map(item => (
<tr key={item.row} className={item.is_valid ? '' : 'bg-red-50'}>
<td className="px-3 py-2 text-gray-500">{item.row}</td>
<td className="px-3 py-2 text-gray-900">{item.title}</td>
<td className="px-3 py-2 text-gray-600">{item.category}</td>
<td className="px-3 py-2">
{item.is_valid ? (
<span className="text-green-600 text-xs">OK</span>
) : (
<span className="text-red-600 text-xs">{item.errors?.join(', ')}</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Roadmap-Titel</label>
<input type="text" value={roadmapTitle} onChange={e => setRoadmapTitle(e.target.value)}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500"
placeholder="Name fuer die importierte Roadmap" />
</div>
<div className="flex justify-end gap-3">
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg">Abbrechen</button>
<button onClick={handleConfirm} disabled={confirming || importJob.valid_rows === 0}
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50">
{confirming ? 'Importiere...' : `${importJob.valid_rows} Items importieren`}
</button>
</div>
</div>
)}
</div>
</div>
)
}