Files
breakpilot-compliance/admin-compliance/components/sdk/ProjectSelector/ProjectSelector.tsx
Sharang Parnerkar ada50f0466 refactor(admin): split AIUseCaseModuleEditor, DataPointCatalog, ProjectSelector components
AIUseCaseModuleEditor (698 LOC) → thin orchestrator (187) + constants (29) +
barrel tabs (4) + tabs implementation split into SystemData (261), PurposeAct
(149), RisksReview (219). DataPointCatalog (658 LOC) → main (291) + helpers
(190) + CategoryGroup (124) + Row (108). ProjectSelector (656 LOC) → main
(211) + CreateProjectDialog (169) + ProjectActionDialog (140) + ProjectCard
(128). All files now under 300 LOC soft target and 500 LOC hard cap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 09:16:21 +02:00

212 lines
7.9 KiB
TypeScript

'use client'
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useSDK } from '@/lib/sdk'
import type { ProjectInfo } from '@/lib/sdk/types'
import { CreateProjectDialog, normalizeProject } from './CreateProjectDialog'
import { ProjectActionDialog } from './ProjectActionDialog'
import { ProjectCard } from './ProjectCard'
export function ProjectSelector() {
const router = useRouter()
const { listProjects, archiveProject, restoreProject, permanentlyDeleteProject } = useSDK()
const [projects, setProjects] = useState<ProjectInfo[]>([])
const [archivedProjects, setArchivedProjects] = useState<ProjectInfo[]>([])
const [loading, setLoading] = useState(true)
const [showCreateDialog, setShowCreateDialog] = useState(false)
const [actionTarget, setActionTarget] = useState<ProjectInfo | null>(null)
const [isProcessing, setIsProcessing] = useState(false)
const [showArchived, setShowArchived] = useState(false)
useEffect(() => {
loadProjects()
}, [])
const loadProjects = async () => {
setLoading(true)
try {
const result = await listProjects()
const all = result.map(normalizeProject)
setProjects(all.filter(p => p.status === 'active'))
setArchivedProjects(all.filter(p => p.status === 'archived'))
} catch (error) {
console.error('Failed to load projects:', error)
} finally {
setLoading(false)
}
}
const handleProjectClick = (project: ProjectInfo) => {
router.push(`/sdk?project=${project.id}`)
}
const handleProjectCreated = (project: ProjectInfo) => {
router.push(`/sdk?project=${project.id}`)
}
const handleArchive = async () => {
if (!actionTarget) return
setIsProcessing(true)
try {
await archiveProject(actionTarget.id)
setProjects(prev => prev.filter(p => p.id !== actionTarget.id))
setArchivedProjects(prev => [...prev, { ...actionTarget, status: 'archived' as const }])
setActionTarget(null)
} catch (error) {
console.error('Failed to archive project:', error)
} finally {
setIsProcessing(false)
}
}
const handlePermanentDelete = async () => {
if (!actionTarget) return
setIsProcessing(true)
try {
await permanentlyDeleteProject(actionTarget.id)
setProjects(prev => prev.filter(p => p.id !== actionTarget.id))
setArchivedProjects(prev => prev.filter(p => p.id !== actionTarget.id))
setActionTarget(null)
} catch (error) {
console.error('Failed to delete project:', error)
} finally {
setIsProcessing(false)
}
}
const handleRestore = async (project: ProjectInfo) => {
try {
await restoreProject(project.id)
setArchivedProjects(prev => prev.filter(p => p.id !== project.id))
setProjects(prev => [...prev, { ...project, status: 'active' as const }])
} catch (error) {
console.error('Failed to restore project:', error)
}
}
return (
<div className="max-w-4xl mx-auto py-12 px-4">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-2xl font-bold text-gray-900">Ihre Projekte</h1>
<p className="mt-1 text-gray-500">
Waehlen Sie ein Compliance-Projekt oder erstellen Sie ein neues.
</p>
</div>
<button
onClick={() => setShowCreateDialog(true)}
className="flex items-center gap-2 px-4 py-2.5 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium text-sm"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Neues Projekt
</button>
</div>
{/* Loading */}
{loading && (
<div className="flex items-center justify-center py-16">
<div className="w-8 h-8 border-4 border-purple-200 border-t-purple-600 rounded-full animate-spin" />
</div>
)}
{/* Empty State */}
{!loading && projects.length === 0 && archivedProjects.length === 0 && (
<div className="text-center py-16">
<div className="w-16 h-16 mx-auto mb-4 bg-purple-100 rounded-2xl flex items-center justify-center">
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h2 className="text-lg font-semibold text-gray-900">Noch keine Projekte</h2>
<p className="mt-2 text-gray-500 max-w-md mx-auto">
Erstellen Sie Ihr erstes Compliance-Projekt, um mit der DSGVO- und AI-Act-Konformitaet zu beginnen.
</p>
<button
onClick={() => setShowCreateDialog(true)}
className="mt-6 inline-flex items-center gap-2 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Erstes Projekt erstellen
</button>
</div>
)}
{/* Active Projects */}
{!loading && projects.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{projects.map(project => (
<ProjectCard
key={project.id}
project={project}
onClick={() => handleProjectClick(project)}
onDelete={() => setActionTarget(project)}
/>
))}
</div>
)}
{/* Archived Projects Section */}
{!loading && archivedProjects.length > 0 && (
<div className="mt-8">
<button
onClick={() => setShowArchived(!showArchived)}
className="flex items-center gap-2 text-sm text-gray-500 hover:text-gray-700 transition-colors mb-4"
>
<svg
className={`w-4 h-4 transition-transform ${showArchived ? 'rotate-90' : ''}`}
fill="none" stroke="currentColor" viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
</svg>
Archivierte Projekte ({archivedProjects.length})
</button>
{showArchived && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{archivedProjects.map(project => (
<ProjectCard
key={project.id}
project={project}
onClick={() => handleProjectClick(project)}
onRestore={() => handleRestore(project)}
onDelete={() => setActionTarget(project)}
/>
))}
</div>
)}
</div>
)}
{/* Create Dialog */}
<CreateProjectDialog
open={showCreateDialog}
onClose={() => setShowCreateDialog(false)}
onCreated={handleProjectCreated}
existingProjects={projects}
/>
{/* Action Dialog (Archive / Delete) */}
{actionTarget && (
<ProjectActionDialog
project={actionTarget}
onArchive={handleArchive}
onPermanentDelete={handlePermanentDelete}
onCancel={() => setActionTarget(null)}
isProcessing={isProcessing}
/>
)}
</div>
)
}