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:
@@ -5,7 +5,7 @@
|
||||
* retry logic, and optimistic locking support.
|
||||
*/
|
||||
|
||||
import { SDKState, CheckpointStatus } from './types'
|
||||
import { SDKState, CheckpointStatus, ProjectInfo } from './types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
@@ -73,16 +73,19 @@ const RETRY_DELAYS = [1000, 2000, 4000] // Exponential backoff
|
||||
export class SDKApiClient {
|
||||
private baseUrl: string
|
||||
private tenantId: string
|
||||
private projectId: string | undefined
|
||||
private timeout: number
|
||||
private abortControllers: Map<string, AbortController> = new Map()
|
||||
|
||||
constructor(options: {
|
||||
baseUrl?: string
|
||||
tenantId: string
|
||||
projectId?: string
|
||||
timeout?: number
|
||||
}) {
|
||||
this.baseUrl = options.baseUrl || DEFAULT_BASE_URL
|
||||
this.tenantId = options.tenantId
|
||||
this.projectId = options.projectId
|
||||
this.timeout = options.timeout || DEFAULT_TIMEOUT
|
||||
}
|
||||
|
||||
@@ -188,8 +191,10 @@ export class SDKApiClient {
|
||||
*/
|
||||
async getState(): Promise<StateResponse | null> {
|
||||
try {
|
||||
const params = new URLSearchParams({ tenantId: this.tenantId })
|
||||
if (this.projectId) params.set('projectId', this.projectId)
|
||||
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
|
||||
`${this.baseUrl}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
|
||||
`${this.baseUrl}/state?${params.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -228,6 +233,7 @@ export class SDKApiClient {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId: this.tenantId,
|
||||
projectId: this.projectId,
|
||||
state,
|
||||
version,
|
||||
}),
|
||||
@@ -245,8 +251,10 @@ export class SDKApiClient {
|
||||
* Delete SDK state for the current tenant
|
||||
*/
|
||||
async deleteState(): Promise<void> {
|
||||
const params = new URLSearchParams({ tenantId: this.tenantId })
|
||||
if (this.projectId) params.set('projectId', this.projectId)
|
||||
await this.fetchWithRetry<APIResponse<void>>(
|
||||
`${this.baseUrl}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
|
||||
`${this.baseUrl}/state?${params.toString()}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
@@ -571,6 +579,107 @@ export class SDKApiClient {
|
||||
return this.tenantId
|
||||
}
|
||||
|
||||
/**
|
||||
* Set project ID for multi-project support
|
||||
*/
|
||||
setProjectId(projectId: string | undefined): void {
|
||||
this.projectId = projectId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current project ID
|
||||
*/
|
||||
getProjectId(): string | undefined {
|
||||
return this.projectId
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Project Management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* List all projects for the current tenant
|
||||
*/
|
||||
async listProjects(): Promise<{ projects: ProjectInfo[]; total: number }> {
|
||||
const response = await this.fetchWithRetry<{ projects: ProjectInfo[]; total: number }>(
|
||||
`${this.baseUrl}/projects?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new project
|
||||
*/
|
||||
async createProject(data: {
|
||||
name: string
|
||||
description?: string
|
||||
customer_type?: string
|
||||
copy_from_project_id?: string
|
||||
}): Promise<ProjectInfo> {
|
||||
const response = await this.fetchWithRetry<ProjectInfo>(
|
||||
`${this.baseUrl}/projects?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
tenant_id: this.tenantId,
|
||||
}),
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing project
|
||||
*/
|
||||
async updateProject(projectId: string, data: {
|
||||
name?: string
|
||||
description?: string
|
||||
}): Promise<ProjectInfo> {
|
||||
const response = await this.fetchWithRetry<ProjectInfo>(
|
||||
`${this.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
tenant_id: this.tenantId,
|
||||
}),
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive (soft-delete) a project
|
||||
*/
|
||||
async archiveProject(projectId: string): Promise<void> {
|
||||
await this.fetchWithRetry<{ success: boolean }>(
|
||||
`${this.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check
|
||||
*/
|
||||
@@ -594,19 +703,23 @@ export class SDKApiClient {
|
||||
|
||||
let clientInstance: SDKApiClient | null = null
|
||||
|
||||
export function getSDKApiClient(tenantId?: string): SDKApiClient {
|
||||
export function getSDKApiClient(tenantId?: string, projectId?: string): SDKApiClient {
|
||||
if (!clientInstance && !tenantId) {
|
||||
throw new Error('SDKApiClient not initialized. Provide tenantId on first call.')
|
||||
}
|
||||
|
||||
if (!clientInstance && tenantId) {
|
||||
clientInstance = new SDKApiClient({ tenantId })
|
||||
clientInstance = new SDKApiClient({ tenantId, projectId })
|
||||
}
|
||||
|
||||
if (tenantId && clientInstance && clientInstance.getTenantId() !== tenantId) {
|
||||
clientInstance.setTenantId(tenantId)
|
||||
}
|
||||
|
||||
if (clientInstance) {
|
||||
clientInstance.setProjectId(projectId)
|
||||
}
|
||||
|
||||
return clientInstance!
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -59,6 +59,7 @@ const DEFAULT_MAX_RETRIES = 3
|
||||
export class StateSyncManager {
|
||||
private apiClient: SDKApiClient
|
||||
private tenantId: string
|
||||
private projectId: string | undefined
|
||||
private options: Required<SyncOptions>
|
||||
private callbacks: SyncCallbacks
|
||||
private syncState: SyncState
|
||||
@@ -71,10 +72,12 @@ export class StateSyncManager {
|
||||
apiClient: SDKApiClient,
|
||||
tenantId: string,
|
||||
options: SyncOptions = {},
|
||||
callbacks: SyncCallbacks = {}
|
||||
callbacks: SyncCallbacks = {},
|
||||
projectId?: string
|
||||
) {
|
||||
this.apiClient = apiClient
|
||||
this.tenantId = tenantId
|
||||
this.projectId = projectId
|
||||
this.callbacks = callbacks
|
||||
this.options = {
|
||||
debounceMs: options.debounceMs ?? DEFAULT_DEBOUNCE_MS,
|
||||
@@ -105,7 +108,10 @@ export class StateSyncManager {
|
||||
}
|
||||
|
||||
try {
|
||||
this.broadcastChannel = new BroadcastChannel(`${SYNC_CHANNEL}-${this.tenantId}`)
|
||||
const channelName = this.projectId
|
||||
? `${SYNC_CHANNEL}-${this.tenantId}-${this.projectId}`
|
||||
: `${SYNC_CHANNEL}-${this.tenantId}`
|
||||
this.broadcastChannel = new BroadcastChannel(channelName)
|
||||
this.broadcastChannel.onmessage = this.handleBroadcastMessage.bind(this)
|
||||
} catch (error) {
|
||||
console.warn('BroadcastChannel not available:', error)
|
||||
@@ -209,7 +215,9 @@ export class StateSyncManager {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private getStorageKey(): string {
|
||||
return `${STORAGE_KEY_PREFIX}-${this.tenantId}`
|
||||
return this.projectId
|
||||
? `${STORAGE_KEY_PREFIX}-${this.tenantId}-${this.projectId}`
|
||||
: `${STORAGE_KEY_PREFIX}-${this.tenantId}`
|
||||
}
|
||||
|
||||
saveToLocalStorage(state: SDKState): void {
|
||||
@@ -476,7 +484,8 @@ export function createStateSyncManager(
|
||||
apiClient: SDKApiClient,
|
||||
tenantId: string,
|
||||
options?: SyncOptions,
|
||||
callbacks?: SyncCallbacks
|
||||
callbacks?: SyncCallbacks,
|
||||
projectId?: string
|
||||
): StateSyncManager {
|
||||
return new StateSyncManager(apiClient, tenantId, options, callbacks)
|
||||
return new StateSyncManager(apiClient, tenantId, options, callbacks, projectId)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,22 @@ export type SDKPackageId = 'vorbereitung' | 'analyse' | 'dokumentation' | 'recht
|
||||
|
||||
export type CustomerType = 'new' | 'existing'
|
||||
|
||||
// =============================================================================
|
||||
// PROJECT INFO (Multi-Projekt-Architektur)
|
||||
// =============================================================================
|
||||
|
||||
export interface ProjectInfo {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
customerType: CustomerType
|
||||
status: 'active' | 'archived'
|
||||
projectVersion: number
|
||||
completionPercentage: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMPANY PROFILE (Business Context - collected before use cases)
|
||||
// =============================================================================
|
||||
@@ -1497,6 +1513,10 @@ export interface SDKState {
|
||||
userId: string
|
||||
subscription: SubscriptionTier
|
||||
|
||||
// Project Context (Multi-Projekt)
|
||||
projectId: string
|
||||
projectInfo: ProjectInfo | null
|
||||
|
||||
// Customer Type (new vs existing)
|
||||
customerType: CustomerType | null
|
||||
|
||||
|
||||
Reference in New Issue
Block a user