feat(sdk): Multi-Projekt-Architektur — mehrere Projekte pro Tenant
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 33s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 19s
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 33s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 19s
Jeder Tenant kann jetzt mehrere Compliance-Projekte anlegen (z.B. verschiedene Produkte, Tochterunternehmen). CompanyProfile ist pro Projekt kopierbar und danach unabhaengig editierbar. Multi-Tab-Support via separater BroadcastChannel und localStorage Keys pro Projekt. - Migration 039: compliance_projects Tabelle, sdk_states.project_id - Backend: FastAPI CRUD-Routes fuer Projekte mit Tenant-Isolation - Frontend: ProjectSelector UI, SDKProvider mit projectId, URL ?project= - State API: UPSERT auf (tenant_id, project_id) mit Abwaertskompatibilitaet - Tests: pytest fuer Model-Validierung, Row-Konvertierung, Tenant-Isolation - Docs: MKDocs Seite, CLAUDE.md, Backend README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
ImportedDocument,
|
||||
GapAnalysis,
|
||||
SDKPackageId,
|
||||
ProjectInfo,
|
||||
SDK_STEPS,
|
||||
SDK_PACKAGES,
|
||||
getStepById,
|
||||
@@ -57,6 +58,10 @@ const initialState: SDKState = {
|
||||
userId: '',
|
||||
subscription: 'PROFESSIONAL',
|
||||
|
||||
// Project Context
|
||||
projectId: '',
|
||||
projectInfo: null,
|
||||
|
||||
// Customer Type
|
||||
customerType: null,
|
||||
|
||||
@@ -548,6 +553,13 @@ interface SDKContextValue {
|
||||
// Command Bar
|
||||
isCommandBarOpen: boolean
|
||||
setCommandBarOpen: (open: boolean) => void
|
||||
|
||||
// Project Management
|
||||
projectId: string | undefined
|
||||
createProject: (name: string, customerType: CustomerType, copyFromProjectId?: string) => Promise<ProjectInfo>
|
||||
listProjects: () => Promise<ProjectInfo[]>
|
||||
switchProject: (projectId: string) => void
|
||||
archiveProject: (projectId: string) => Promise<void>
|
||||
}
|
||||
|
||||
const SDKContext = createContext<SDKContextValue | null>(null)
|
||||
@@ -562,6 +574,7 @@ interface SDKProviderProps {
|
||||
children: React.ReactNode
|
||||
tenantId?: string
|
||||
userId?: string
|
||||
projectId?: string
|
||||
enableBackendSync?: boolean
|
||||
}
|
||||
|
||||
@@ -569,6 +582,7 @@ export function SDKProvider({
|
||||
children,
|
||||
tenantId = 'default',
|
||||
userId = 'default',
|
||||
projectId,
|
||||
enableBackendSync = false,
|
||||
}: SDKProviderProps) {
|
||||
const router = useRouter()
|
||||
@@ -577,6 +591,7 @@ export function SDKProvider({
|
||||
...initialState,
|
||||
tenantId,
|
||||
userId,
|
||||
projectId: projectId || '',
|
||||
})
|
||||
const [isCommandBarOpen, setCommandBarOpen] = React.useState(false)
|
||||
const [isInitialized, setIsInitialized] = React.useState(false)
|
||||
@@ -597,7 +612,7 @@ export function SDKProvider({
|
||||
// Initialize API client and sync manager
|
||||
useEffect(() => {
|
||||
if (enableBackendSync && typeof window !== 'undefined') {
|
||||
apiClientRef.current = getSDKApiClient(tenantId)
|
||||
apiClientRef.current = getSDKApiClient(tenantId, projectId)
|
||||
|
||||
syncManagerRef.current = createStateSyncManager(
|
||||
apiClientRef.current,
|
||||
@@ -640,7 +655,8 @@ export function SDKProvider({
|
||||
setIsOnline(true)
|
||||
setSyncState(prev => ({ ...prev, status: 'idle' }))
|
||||
},
|
||||
}
|
||||
},
|
||||
projectId
|
||||
)
|
||||
}
|
||||
|
||||
@@ -654,7 +670,7 @@ export function SDKProvider({
|
||||
apiClientRef.current = null
|
||||
}
|
||||
}
|
||||
}, [enableBackendSync, tenantId])
|
||||
}, [enableBackendSync, tenantId, projectId])
|
||||
|
||||
// Sync current step with URL
|
||||
useEffect(() => {
|
||||
@@ -666,12 +682,17 @@ export function SDKProvider({
|
||||
}
|
||||
}, [pathname, state.currentStep])
|
||||
|
||||
// Storage key — per tenant+project
|
||||
const storageKey = projectId
|
||||
? `${SDK_STORAGE_KEY}-${tenantId}-${projectId}`
|
||||
: `${SDK_STORAGE_KEY}-${tenantId}`
|
||||
|
||||
// Load state on mount (localStorage first, then server)
|
||||
useEffect(() => {
|
||||
const loadInitialState = async () => {
|
||||
try {
|
||||
// First, try loading from localStorage
|
||||
const stored = localStorage.getItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
||||
const stored = localStorage.getItem(storageKey)
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored)
|
||||
if (parsed.lastModified) {
|
||||
@@ -699,7 +720,7 @@ export function SDKProvider({
|
||||
}
|
||||
|
||||
loadInitialState()
|
||||
}, [tenantId, enableBackendSync])
|
||||
}, [tenantId, projectId, enableBackendSync, storageKey])
|
||||
|
||||
// Auto-save to localStorage and sync to server
|
||||
useEffect(() => {
|
||||
@@ -707,8 +728,8 @@ export function SDKProvider({
|
||||
|
||||
const saveTimeout = setTimeout(() => {
|
||||
try {
|
||||
// Save to localStorage
|
||||
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(state))
|
||||
// Save to localStorage (per tenant+project)
|
||||
localStorage.setItem(storageKey, JSON.stringify(state))
|
||||
|
||||
// Sync to server if backend sync is enabled
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
@@ -720,7 +741,7 @@ export function SDKProvider({
|
||||
}, 1000)
|
||||
|
||||
return () => clearTimeout(saveTimeout)
|
||||
}, [state, tenantId, isInitialized, enableBackendSync])
|
||||
}, [state, tenantId, projectId, isInitialized, enableBackendSync, storageKey])
|
||||
|
||||
// Keyboard shortcut for Command Bar
|
||||
useEffect(() => {
|
||||
@@ -746,10 +767,11 @@ export function SDKProvider({
|
||||
const step = getStepById(stepId)
|
||||
if (step) {
|
||||
dispatch({ type: 'SET_CURRENT_STEP', payload: stepId })
|
||||
router.push(step.url)
|
||||
const url = projectId ? `${step.url}?project=${projectId}` : step.url
|
||||
router.push(url)
|
||||
}
|
||||
},
|
||||
[router]
|
||||
[router, projectId]
|
||||
)
|
||||
|
||||
const goToNextStep = useCallback(() => {
|
||||
@@ -992,7 +1014,7 @@ export function SDKProvider({
|
||||
}
|
||||
|
||||
// Also save to localStorage for immediate availability
|
||||
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(demoState))
|
||||
localStorage.setItem(storageKey, JSON.stringify(demoState))
|
||||
|
||||
// Update local state
|
||||
dispatch({ type: 'LOAD_DEMO_DATA', payload: demoState })
|
||||
@@ -1005,7 +1027,7 @@ export function SDKProvider({
|
||||
message: error instanceof Error ? error.message : 'Unbekannter Fehler beim Laden der Demo-Daten',
|
||||
}
|
||||
}
|
||||
}, [tenantId, userId, enableBackendSync])
|
||||
}, [tenantId, userId, enableBackendSync, storageKey])
|
||||
|
||||
// Clear demo data
|
||||
const clearDemoData = useCallback(async (): Promise<boolean> => {
|
||||
@@ -1016,7 +1038,7 @@ export function SDKProvider({
|
||||
}
|
||||
|
||||
// Clear localStorage
|
||||
localStorage.removeItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
||||
localStorage.removeItem(storageKey)
|
||||
|
||||
// Reset local state
|
||||
dispatch({ type: 'RESET_STATE' })
|
||||
@@ -1026,7 +1048,7 @@ export function SDKProvider({
|
||||
console.error('Failed to clear demo data:', error)
|
||||
return false
|
||||
}
|
||||
}, [tenantId, enableBackendSync])
|
||||
}, [storageKey, enableBackendSync])
|
||||
|
||||
// Check if demo data is loaded (has use cases with demo- prefix)
|
||||
const isDemoDataLoaded = useMemo(() => {
|
||||
@@ -1036,7 +1058,7 @@ export function SDKProvider({
|
||||
// Persistence
|
||||
const saveState = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(state))
|
||||
localStorage.setItem(storageKey, JSON.stringify(state))
|
||||
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
await syncManagerRef.current.forcSync(state)
|
||||
@@ -1045,7 +1067,7 @@ export function SDKProvider({
|
||||
console.error('Failed to save SDK state:', error)
|
||||
throw error
|
||||
}
|
||||
}, [state, tenantId, enableBackendSync])
|
||||
}, [state, storageKey, enableBackendSync])
|
||||
|
||||
const loadState = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
@@ -1058,7 +1080,7 @@ export function SDKProvider({
|
||||
}
|
||||
|
||||
// Fall back to localStorage
|
||||
const stored = localStorage.getItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
||||
const stored = localStorage.getItem(storageKey)
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored)
|
||||
dispatch({ type: 'SET_STATE', payload: parsed })
|
||||
@@ -1067,7 +1089,7 @@ export function SDKProvider({
|
||||
console.error('Failed to load SDK state:', error)
|
||||
throw error
|
||||
}
|
||||
}, [tenantId, enableBackendSync])
|
||||
}, [storageKey, enableBackendSync])
|
||||
|
||||
// Force sync to server
|
||||
const forceSyncToServer = useCallback(async (): Promise<void> => {
|
||||
@@ -1076,6 +1098,49 @@ export function SDKProvider({
|
||||
}
|
||||
}, [state, enableBackendSync])
|
||||
|
||||
// Project Management
|
||||
const createProject = useCallback(
|
||||
async (name: string, customerType: CustomerType, copyFromProjectId?: string): Promise<ProjectInfo> => {
|
||||
if (!apiClientRef.current) {
|
||||
throw new Error('Backend sync not enabled')
|
||||
}
|
||||
return apiClientRef.current.createProject({
|
||||
name,
|
||||
customer_type: customerType,
|
||||
copy_from_project_id: copyFromProjectId,
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const listProjectsFn = useCallback(async (): Promise<ProjectInfo[]> => {
|
||||
if (!apiClientRef.current) {
|
||||
return []
|
||||
}
|
||||
const result = await apiClientRef.current.listProjects()
|
||||
return result.projects
|
||||
}, [])
|
||||
|
||||
const switchProject = useCallback(
|
||||
(newProjectId: string) => {
|
||||
// Navigate to the SDK dashboard with the new project
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
params.set('project', newProjectId)
|
||||
router.push(`/sdk?${params.toString()}`)
|
||||
},
|
||||
[router]
|
||||
)
|
||||
|
||||
const archiveProjectFn = useCallback(
|
||||
async (archiveId: string): Promise<void> => {
|
||||
if (!apiClientRef.current) {
|
||||
throw new Error('Backend sync not enabled')
|
||||
}
|
||||
await apiClientRef.current.archiveProject(archiveId)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
// Export
|
||||
const exportState = useCallback(
|
||||
async (format: 'json' | 'pdf' | 'zip'): Promise<Blob> => {
|
||||
@@ -1136,6 +1201,11 @@ export function SDKProvider({
|
||||
exportState,
|
||||
isCommandBarOpen,
|
||||
setCommandBarOpen,
|
||||
projectId,
|
||||
createProject,
|
||||
listProjects: listProjectsFn,
|
||||
switchProject,
|
||||
archiveProject: archiveProjectFn,
|
||||
}
|
||||
|
||||
return <SDKContext.Provider value={value}>{children}</SDKContext.Provider>
|
||||
|
||||
Reference in New Issue
Block a user