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>
155 lines
7.0 KiB
TypeScript
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>
|
|
)
|
|
}
|