/**
* Instance Permission Hooks
*
* Hooks für Berechtigungsprüfungen basierend auf der aktuellen Feature-Instanz.
* Die Berechtigungen werden summarisch pro Instanz geladen (kein einzelner API-Call pro Check).
*/
import { useMemo } from 'react';
import { useCurrentInstance } from './useCurrentInstance';
import type {
TablePermission,
FieldPermission,
AccessLevel,
InstancePermissions,
} from '../types/mandate';
import { canAccessRecord, hasAccess } from '../types/mandate';
// =============================================================================
// DEFAULT PERMISSIONS (Kein Zugriff)
// =============================================================================
const NO_ACCESS_TABLE: TablePermission = {
view: false,
read: 'n',
create: 'n',
update: 'n',
delete: 'n',
};
// =============================================================================
// TABLE PERMISSION HOOKS
// =============================================================================
/**
* Hook für Tabellen-Berechtigungen
*
* Verwendung:
* ```tsx
* function ContractList() {
* const { canCreate, canUpdate, canDelete, read } = useTablePermission('TrusteeContract');
*
* return (
*
* {canCreate && }
* {contracts.map(c => (
*
* {canUpdate(c) && }
* {canDelete(c) && }
*
* ))}
*
* );
* }
* ```
*/
export function useTablePermission(tableName: string) {
const { instance } = useCurrentInstance();
const permission = useMemo((): TablePermission => {
if (!instance?.permissions?.tables) {
return NO_ACCESS_TABLE;
}
return instance.permissions.tables[tableName] ?? NO_ACCESS_TABLE;
}, [instance, tableName]);
// Kontext für Record-basierte Prüfungen
const userId = ''; // TODO: Aus Auth-Store holen
return {
// Raw permission levels
view: permission.view,
read: permission.read,
create: permission.create,
update: permission.update,
delete: permission.delete,
// Convenience Booleans
canView: permission.view,
canRead: hasAccess(permission.read),
canCreate: hasAccess(permission.create),
canUpdate: hasAccess(permission.update),
canDelete: hasAccess(permission.delete),
// Record-basierte Prüfungen
canReadRecord: (record: { _createdBy?: string }) =>
canAccessRecord(permission.read, record, userId),
canUpdateRecord: (record: { _createdBy?: string }) =>
canAccessRecord(permission.update, record, userId),
canDeleteRecord: (record: { _createdBy?: string }) =>
canAccessRecord(permission.delete, record, userId),
};
}
/**
* Vereinfachter Hook - prüft nur ob Tabelle sichtbar ist
*/
export function useCanViewTable(tableName: string): boolean {
const { canView } = useTablePermission(tableName);
return canView;
}
// =============================================================================
// VIEW PERMISSION HOOKS
// =============================================================================
/**
* Hook für View-Berechtigungen (Navigation)
*
* Verwendung:
* ```tsx
* function Navigation() {
* const canViewContracts = useCanViewFeatureView('trustee-contracts');
*
* return (
*
* );
* }
* ```
*
* Supports both legacy format (e.g., "trustee-dashboard") and
* fully qualified objectKey format (e.g., "ui.feature.trustee.dashboard")
*/
export function useCanViewFeatureView(viewCode: string): boolean {
const { instance, featureCode } = useCurrentInstance();
if (!instance?.permissions?.views) {
// DEBUG: Log for chatbot
if (featureCode === 'chatbot') {
console.log('🔍 [DEBUG] useCanViewFeatureView: No views permissions', {
viewCode,
featureCode,
instanceId: instance?.id,
hasPermissions: !!instance?.permissions,
hasViews: !!instance?.permissions?.views,
});
}
return false;
}
const views = instance.permissions.views;
// DEBUG: Log for chatbot
if (featureCode === 'chatbot') {
const parts = viewCode.split('-');
const viewName = parts.length >= 2 ? parts.slice(1).join('-') : '';
const fullObjectKey = `ui.feature.${featureCode}.${viewName}`;
console.log('🔍 [DEBUG] useCanViewFeatureView: Checking permissions', {
viewCode,
featureCode,
viewName,
fullObjectKey,
instanceId: instance.id,
viewKeys: Object.keys(views),
hasWildcard: !!views["_all"],
hasLegacyView: !!views[viewCode],
hasFullObjectKey: !!views[fullObjectKey],
wildcardValue: views["_all"],
legacyValue: views[viewCode],
fullObjectKeyValue: views[fullObjectKey],
});
}
// Check for wildcard "_all" permission first (item=None in backend = all views)
if (views["_all"]) {
return true;
}
// Check legacy format directly (e.g., "trustee-dashboard")
if (views[viewCode]) {
return true;
}
// Check fully qualified objectKey format (e.g., "ui.feature.trustee.dashboard")
// Convert viewCode "trustee-dashboard" to "ui.feature.trustee.dashboard"
const parts = viewCode.split('-');
if (parts.length >= 2 && featureCode) {
const viewName = parts.slice(1).join('-'); // e.g., "dashboard" or "position-documents"
const fullObjectKey = `ui.feature.${featureCode}.${viewName}`;
if (views[fullObjectKey]) {
return true;
}
}
return false;
}
/**
* Hook für mehrere View-Berechtigungen gleichzeitig
* Supports both legacy format and fully qualified objectKey format
*/
export function useViewPermissions(viewCodes: string[]): Record {
const { instance, featureCode } = useCurrentInstance();
return useMemo(() => {
const result: Record = {};
const views = instance?.permissions?.views;
if (!views) {
viewCodes.forEach(code => {
result[code] = false;
});
return result;
}
// Check for wildcard permission
const hasAllViews = views["_all"] ?? false;
viewCodes.forEach(code => {
if (hasAllViews) {
result[code] = true;
return;
}
// Check legacy format
if (views[code]) {
result[code] = true;
return;
}
// Check fully qualified objectKey format
const parts = code.split('-');
if (parts.length >= 2 && featureCode) {
const viewName = parts.slice(1).join('-');
const fullObjectKey = `ui.feature.${featureCode}.${viewName}`;
if (views[fullObjectKey]) {
result[code] = true;
return;
}
}
result[code] = false;
});
return result;
}, [instance, featureCode, viewCodes]);
}
// =============================================================================
// FIELD PERMISSION HOOKS
// =============================================================================
/**
* Hook für Feld-Berechtigungen
*
* Verwendung:
* ```tsx
* function ContractForm() {
* const { canRead, canWrite } = useFieldPermission('TrusteeContract', 'salary');
*
* return (
*
* );
* }
* ```
*/
export function useFieldPermission(tableName: string, fieldName: string): FieldPermission {
const { instance } = useCurrentInstance();
return useMemo(() => {
const fieldPermissions = instance?.permissions?.fields?.[tableName];
if (!fieldPermissions) {
// Wenn keine Feld-Level Einschränkungen, erlaube alles
return { read: true, write: true };
}
return fieldPermissions[fieldName] ?? { read: true, write: true };
}, [instance, tableName, fieldName]);
}
// =============================================================================
// GENERIC PERMISSION CHECK
// =============================================================================
/**
* Generischer Hook für beliebige Berechtigungsprüfungen
*/
export function useInstancePermissions(): InstancePermissions | undefined {
const { instance } = useCurrentInstance();
return instance?.permissions;
}
/**
* Hook der prüft ob ein Record bearbeitet werden darf
* Kombiniert Tabellen-Permission mit Record-Owner-Check
*/
export function useCanEditRecord(
tableName: string,
record: { _createdBy?: string } | undefined,
userId: string
): boolean {
const { update } = useTablePermission(tableName);
if (!record) return false;
return canAccessRecord(update, record, userId);
}
/**
* Hook der prüft ob ein Record gelöscht werden darf
*/
export function useCanDeleteRecord(
tableName: string,
record: { _createdBy?: string } | undefined,
userId: string
): boolean {
const { delete: deleteLevel } = useTablePermission(tableName);
if (!record) return false;
return canAccessRecord(deleteLevel, record, userId);
}
// =============================================================================
// PERMISSION GATE COMPONENT
// =============================================================================
interface PermissionGateProps {
table?: string;
view?: string;
action?: 'view' | 'read' | 'create' | 'update' | 'delete';
record?: { _createdBy?: string };
children: React.ReactNode;
fallback?: React.ReactNode;
}
/**
* Komponente für bedingte Anzeige basierend auf Berechtigungen
*
* Verwendung:
* ```tsx
*
*
*
*
* }>
*
*
* ```
*/
export function PermissionGate({
table,
view,
action = 'view',
record,
children,
fallback = null,
}: PermissionGateProps): React.ReactElement | null {
const { instance } = useCurrentInstance();
const userId = ''; // TODO: Aus Auth-Store holen
let hasPermission = false;
if (view) {
// View-basierte Prüfung
hasPermission = instance?.permissions?.views?.[view] ?? false;
} else if (table) {
// Tabellen-basierte Prüfung
const tablePermission = instance?.permissions?.tables?.[table];
if (!tablePermission) {
hasPermission = false;
} else if (action === 'view') {
hasPermission = tablePermission.view;
} else {
const level = tablePermission[action] as AccessLevel;
if (record) {
hasPermission = canAccessRecord(level, record, userId);
} else {
hasPermission = hasAccess(level);
}
}
}
return hasPermission ? <>{children}> : <>{fallback}>;
}