Split 879-LOC page.tsx into 187 LOC with 11 colocated components, _types.ts and _constants.ts for the industry templates module. Behavior preserved. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
188 lines
6.6 KiB
TypeScript
188 lines
6.6 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Branchenspezifische Module (Phase 3.3)
|
|
*
|
|
* Industry-specific compliance template packages:
|
|
* - Browse industry templates (grid view)
|
|
* - View full detail with VVT, TOM, Risk tabs
|
|
* - Apply template packages to current compliance setup
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
import type { IndustrySummary, IndustryTemplate, DetailTab } from './_types'
|
|
import { GridSkeleton, DetailSkeleton } from './_components/Skeletons'
|
|
import { PageHeader } from './_components/PageHeader'
|
|
import { ErrorPanel } from './_components/ErrorPanel'
|
|
import { IndustryGrid } from './_components/IndustryGrid'
|
|
import { DetailHeader } from './_components/DetailHeader'
|
|
import { DetailContent } from './_components/DetailContent'
|
|
import { Toast } from './_components/Toast'
|
|
import { EmptyState } from './_components/EmptyState'
|
|
|
|
export default function IndustryTemplatesPage() {
|
|
const [industries, setIndustries] = useState<IndustrySummary[]>([])
|
|
const [selectedDetail, setSelectedDetail] = useState<IndustryTemplate | null>(null)
|
|
const [selectedSlug, setSelectedSlug] = useState<string | null>(null)
|
|
const [activeTab, setActiveTab] = useState<DetailTab>('vvt')
|
|
const [loading, setLoading] = useState(true)
|
|
const [detailLoading, setDetailLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [detailError, setDetailError] = useState<string | null>(null)
|
|
const [applying, setApplying] = useState(false)
|
|
const [toastMessage, setToastMessage] = useState<string | null>(null)
|
|
|
|
const loadIndustries = useCallback(async () => {
|
|
setLoading(true)
|
|
setError(null)
|
|
try {
|
|
const res = await fetch('/api/sdk/v1/industry/templates')
|
|
if (!res.ok) {
|
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`)
|
|
}
|
|
const data = await res.json()
|
|
setIndustries(Array.isArray(data) ? data : data.industries || data.templates || [])
|
|
} catch (err) {
|
|
console.error('Failed to load industries:', err)
|
|
setError('Branchenvorlagen konnten nicht geladen werden. Bitte pruefen Sie die Verbindung zum Backend.')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [])
|
|
|
|
const loadDetail = useCallback(async (slug: string) => {
|
|
setDetailLoading(true)
|
|
setDetailError(null)
|
|
setSelectedSlug(slug)
|
|
setActiveTab('vvt')
|
|
try {
|
|
const [detailRes, vvtRes, tomRes, risksRes] = await Promise.all([
|
|
fetch(`/api/sdk/v1/industry/templates/${slug}`),
|
|
fetch(`/api/sdk/v1/industry/templates/${slug}/vvt`),
|
|
fetch(`/api/sdk/v1/industry/templates/${slug}/tom`),
|
|
fetch(`/api/sdk/v1/industry/templates/${slug}/risks`),
|
|
])
|
|
|
|
if (!detailRes.ok) {
|
|
throw new Error(`HTTP ${detailRes.status}: ${detailRes.statusText}`)
|
|
}
|
|
|
|
const detail: IndustryTemplate = await detailRes.json()
|
|
|
|
if (vvtRes.ok) {
|
|
const vvtData = await vvtRes.json()
|
|
detail.vvt_templates = vvtData.vvt_templates || vvtData.templates || vvtData || []
|
|
}
|
|
if (tomRes.ok) {
|
|
const tomData = await tomRes.json()
|
|
detail.tom_recommendations = tomData.tom_recommendations || tomData.recommendations || tomData || []
|
|
}
|
|
if (risksRes.ok) {
|
|
const risksData = await risksRes.json()
|
|
detail.risk_scenarios = risksData.risk_scenarios || risksData.scenarios || risksData || []
|
|
}
|
|
|
|
setSelectedDetail(detail)
|
|
} catch (err) {
|
|
console.error('Failed to load industry detail:', err)
|
|
setDetailError('Details konnten nicht geladen werden. Bitte versuchen Sie es erneut.')
|
|
} finally {
|
|
setDetailLoading(false)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
loadIndustries()
|
|
}, [loadIndustries])
|
|
|
|
const handleBackToGrid = useCallback(() => {
|
|
setSelectedSlug(null)
|
|
setSelectedDetail(null)
|
|
setDetailError(null)
|
|
}, [])
|
|
|
|
const handleApplyPackage = useCallback(async () => {
|
|
if (!selectedDetail) return
|
|
setApplying(true)
|
|
try {
|
|
await new Promise((resolve) => setTimeout(resolve, 1500))
|
|
setToastMessage(
|
|
`Branchenpaket "${selectedDetail.name}" wurde erfolgreich angewendet. ` +
|
|
`${selectedDetail.vvt_templates?.length || 0} VVT-Vorlagen, ` +
|
|
`${selectedDetail.tom_recommendations?.length || 0} TOM-Empfehlungen und ` +
|
|
`${selectedDetail.risk_scenarios?.length || 0} Risiko-Szenarien wurden importiert.`
|
|
)
|
|
} catch {
|
|
setToastMessage('Fehler beim Anwenden des Branchenpakets. Bitte versuchen Sie es erneut.')
|
|
} finally {
|
|
setApplying(false)
|
|
}
|
|
}, [selectedDetail])
|
|
|
|
useEffect(() => {
|
|
if (!toastMessage) return
|
|
const timer = setTimeout(() => setToastMessage(null), 6000)
|
|
return () => clearTimeout(timer)
|
|
}, [toastMessage])
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<style>{`
|
|
@keyframes slide-up {
|
|
from { opacity: 0; transform: translateY(16px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
.animate-slide-up {
|
|
animation: slide-up 0.3s ease-out;
|
|
}
|
|
`}</style>
|
|
|
|
<PageHeader />
|
|
|
|
{error && <ErrorPanel message={error} onRetry={loadIndustries} />}
|
|
|
|
{loading ? (
|
|
selectedSlug ? <DetailSkeleton /> : <GridSkeleton />
|
|
) : selectedSlug ? (
|
|
<div className="space-y-6">
|
|
{detailLoading ? (
|
|
<DetailSkeleton />
|
|
) : detailError ? (
|
|
<>
|
|
<button
|
|
onClick={handleBackToGrid}
|
|
className="inline-flex items-center gap-1.5 text-sm text-slate-500 hover:text-emerald-600 transition-colors"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
Zurueck zur Uebersicht
|
|
</button>
|
|
<ErrorPanel message={detailError} onRetry={() => loadDetail(selectedSlug)} />
|
|
</>
|
|
) : selectedDetail ? (
|
|
<>
|
|
<DetailHeader detail={selectedDetail} onBack={handleBackToGrid} />
|
|
<DetailContent
|
|
detail={selectedDetail}
|
|
activeTab={activeTab}
|
|
onTabChange={setActiveTab}
|
|
applying={applying}
|
|
onApply={handleApplyPackage}
|
|
/>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
) : industries.length === 0 && !error ? (
|
|
<EmptyState onReload={loadIndustries} />
|
|
) : (
|
|
<IndustryGrid industries={industries} onSelect={loadDetail} />
|
|
)}
|
|
|
|
{toastMessage && (
|
|
<Toast message={toastMessage} onDismiss={() => setToastMessage(null)} />
|
|
)}
|
|
</div>
|
|
)
|
|
}
|