logical fixes

This commit is contained in:
ValueOn AG 2026-02-09 23:45:05 +01:00
parent 75125e3f58
commit 2a1cd2ef64
30 changed files with 449 additions and 170 deletions

View file

@ -22,6 +22,7 @@ export interface Automation {
_updatedAt?: number;
_createdByUserName?: string;
mandateName?: string;
featureInstanceName?: string;
[key: string]: any;
}

View file

@ -37,6 +37,43 @@
white-space: nowrap;
}
/* CSV Export Button */
.csvExportButton {
display: inline-flex;
align-items: center;
gap: 5px;
height: 40px;
padding: 0 14px;
border: 1px solid var(--color-primary);
border-radius: 25px;
background: var(--color-bg);
color: var(--color-text);
font-size: 12px;
font-family: var(--font-family);
font-weight: 400;
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
flex-shrink: 0;
}
.csvExportButton:hover:not(:disabled) {
background: var(--color-secondary);
color: var(--color-bg);
border-color: var(--color-secondary);
}
.csvExportButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.csvExportIcon {
font-size: 13px;
display: flex;
align-items: center;
}
.refreshButton {
display: flex;
align-items: center;

View file

@ -3,7 +3,7 @@ import { useLanguage } from '../../../providers/language/LanguageContext';
import styles from './FormGeneratorControls.module.css';
import { Button } from '../../UiComponents/Button';
import { IoIosRefresh } from "react-icons/io";
import { FaTrash } from "react-icons/fa";
import { FaTrash, FaDownload } from "react-icons/fa";
import type { AttributeType } from '../../../utils/attributeTypeMapper';
// Generic field/column config interface
@ -62,6 +62,9 @@ export interface FormGeneratorControlsProps {
onPageSizeChange?: (pageSize: number) => void;
supportsBackendPagination?: boolean;
hookData?: any;
// CSV Export
onCsvExport?: () => void;
csvExporting?: boolean;
}
export function FormGeneratorControls({
@ -87,7 +90,9 @@ export function FormGeneratorControls({
onPageChange,
onPageSizeChange,
supportsBackendPagination = false,
hookData: _hookData // Reserved for future use
hookData: _hookData, // Reserved for future use
onCsvExport,
csvExporting = false
}: FormGeneratorControlsProps) {
void _hookData; // Suppress unused variable warning
const { t } = useLanguage();
@ -147,6 +152,17 @@ export function FormGeneratorControls({
{activeFiltersCount} {t('formgen.filter.active', 'filter(s)')}
</span>
)}
{onCsvExport && (
<button
onClick={onCsvExport}
className={styles.csvExportButton}
title={t('formgen.export.csv', 'Export all data as CSV')}
disabled={csvExporting}
>
<span className={styles.csvExportIcon}><FaDownload /></span>
{csvExporting ? t('formgen.export.exporting', 'Exporting...') : 'CSV'}
</button>
)}
{onRefresh && (
<button
onClick={onRefresh}

View file

@ -110,7 +110,7 @@
/* Use separate borders for sticky header support */
border-collapse: separate;
border-spacing: 0;
font-size: 14px;
font-size: 12px;
background: var(--color-bg);
table-layout: fixed;
word-wrap: break-word;
@ -151,9 +151,10 @@
.th {
background: var(--color-bg);
padding: 10px 16px;
padding: 6px 10px;
text-align: left;
font-weight: 400;
font-size: 12px;
color: var(--color-text);
white-space: normal;
word-wrap: break-word;
@ -163,7 +164,6 @@
overflow: visible;
/* Border separates header from scrolled content */
border-bottom: 2px solid var(--color-primary);
/* Shadow on the row, not individual cells */
}
.th.actionsColumn {
@ -339,9 +339,11 @@
}
.td {
padding: 12px 16px;
padding: 4px 10px;
border-top: 1px solid var(--color-primary);
color: var(--color-text);
font-weight: 400;
font-size: 12px;
vertical-align: middle;
word-wrap: break-word;
overflow-wrap: break-word;
@ -376,7 +378,7 @@
/* Selection Column */
.selectColumn {
text-align: center;
padding: 8px !important;
padding: 4px !important;
background: var(--color-bg);
position: relative;
}
@ -393,9 +395,9 @@ tbody .selectColumn {
.selectColumn input[type="checkbox"] {
cursor: pointer;
transform: scale(1.3);
width: 16px;
height: 16px;
transform: scale(1.1);
width: 14px;
height: 14px;
accent-color: var(--color-secondary);
margin: 0;
padding: 0;
@ -427,7 +429,7 @@ tbody .selectColumn {
.actionsColumn {
white-space: nowrap;
text-align: center;
padding: 8px !important;
padding: 4px !important;
font-weight: 400;
box-sizing: border-box;
background: var(--color-bg);
@ -463,17 +465,17 @@ tbody .actionsColumn {
display: flex;
align-items: center;
justify-content: center;
padding: 6px;
padding: 4px;
border: none;
border-radius: 50%;
font-size: 12px;
font-size: 11px;
font-family: var(--font-family);
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
position: relative;
min-width: 28px;
min-height: 28px;
min-width: 24px;
min-height: 24px;
background: var(--color-secondary);
color: var(--color-bg);
}
@ -483,9 +485,9 @@ tbody .actionsColumn {
}
.actionIcon {
font-size: 16px;
height: 16px;
width: 16px;
font-size: 14px;
height: 14px;
width: 14px;
display: flex;
align-items: center;
justify-content: center;
@ -698,18 +700,20 @@ tbody .actionsColumn {
.th,
.td {
padding: 8px 12px;
font-size: 13px;
padding: 4px 8px;
font-size: 11px;
}
.actionButtons {
flex-direction: column;
gap: 4px;
gap: 2px;
}
.actionButton {
padding: 4px 8px;
font-size: 11px;
padding: 3px;
font-size: 10px;
min-width: 22px;
min-height: 22px;
}
.pagination {
@ -916,3 +920,4 @@ tbody .actionsColumn {
50% { opacity: 1; }
}

View file

@ -155,6 +155,8 @@ export interface FormGeneratorTableProps<T = any> {
hookData?: any; // Contains all hook data: refetch, operations, loading states, etc.
// Custom empty message when table is empty
emptyMessage?: string;
// API endpoint for CSV export (e.g. "/api/users/"). If provided, the CSV export button is shown.
apiEndpoint?: string;
}
export function FormGeneratorTable<T extends Record<string, any>>({
@ -185,7 +187,8 @@ export function FormGeneratorTable<T extends Record<string, any>>({
className = '',
getRowDataAttributes,
hookData,
emptyMessage
emptyMessage,
apiEndpoint
}: FormGeneratorTableProps<T>) {
const { t } = useLanguage();
// Get current language from localStorage or default to 'en'
@ -347,9 +350,14 @@ export function FormGeneratorTable<T extends Record<string, any>>({
const [containerWidth, setContainerWidth] = useState<number>(0);
// Calculate default actions column width and track container width
// Minimum width always fits 4 icons (4 * 26px button + 3 * 2px gap + 8px padding = 122px)
const MIN_ACTIONS_WIDTH_FOR_4_ICONS = 122;
const defaultActionsWidth = useMemo(() => {
return actionButtons.length > 0 ? Math.max(60, actionButtons.length * 32 + 16) : 0;
}, [actionButtons.length]);
if (actionButtons.length === 0 && customActions.length === 0) return 0;
const totalButtons = actionButtons.length + customActions.length;
const calculatedWidth = totalButtons * 26 + (totalButtons - 1) * 2 + 8;
return Math.max(MIN_ACTIONS_WIDTH_FOR_4_ICONS, calculatedWidth);
}, [actionButtons.length, customActions.length]);
// Current actions column width (user-defined or default)
const currentActionsWidth = actionsColumnWidth ?? defaultActionsWidth;
@ -853,7 +861,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
const cWidth = tableContainer.clientWidth;
// Calculate actions column width dynamically: ~32px per button + padding
const actionsColWidth = currentActionsWidth;
const selectColumnWidth = selectable ? 50 : 0;
const selectColumnWidth = selectable ? 40 : 0;
const fixedWidth = actionsColWidth + selectColumnWidth;
// Maximum allowed width - simple calculation to prevent overflow
@ -1000,6 +1008,128 @@ export function FormGeneratorTable<T extends Record<string, any>>({
// Track which cells are currently being updated (for loading state)
const [updatingCells, setUpdatingCells] = useState<Set<string>>(new Set());
// CSV Export state
const [csvExporting, setCsvExporting] = useState(false);
// CSV Export: fetch ALL data via direct API call (no state update, no table flicker)
// Fetches in batches of 1000 (backend max pageSize) until all data is loaded.
const handleCsvExport = useCallback(async () => {
if (csvExporting || detectedColumns.length === 0 || !apiEndpoint) return;
setCsvExporting(true);
try {
const batchSize = 1000; // Backend max pageSize
let allData: T[] = [];
let currentPage = 1;
let hasMore = true;
while (hasMore) {
const response = await api.get(apiEndpoint, {
params: { pagination: JSON.stringify({ page: currentPage, pageSize: batchSize }) }
});
let batchItems: T[];
if (response.data && typeof response.data === 'object' && 'items' in response.data) {
batchItems = Array.isArray(response.data.items) ? response.data.items : [];
// Use pagination metadata to determine if more pages exist
const totalPages = response.data.pagination?.totalPages || 1;
hasMore = currentPage < totalPages;
} else if (Array.isArray(response.data)) {
batchItems = response.data;
hasMore = false; // Non-paginated response — all data in one shot
} else {
batchItems = [];
hasMore = false;
}
allData = allData.concat(batchItems);
currentPage++;
// Safety: stop after 100 pages (100k records)
if (currentPage > 100) {
hasMore = false;
}
}
if (allData.length === 0) {
setCsvExporting(false);
return;
}
// Build CSV content
const separator = ';';
const _escapeCsvValue = (val: any): string => {
if (val === null || val === undefined) return '';
let str: string;
if (typeof val === 'boolean') {
str = val ? 'true' : 'false';
} else if (typeof val === 'object') {
if (isTextMultilingual(val)) {
str = formatTextMultilingual(val, currentLanguage);
} else {
try { str = JSON.stringify(val); } catch { str = String(val); }
}
} else {
str = String(val);
}
// Escape quotes and wrap in quotes if needed
if (str.includes(separator) || str.includes('"') || str.includes('\n') || str.includes('\r')) {
str = '"' + str.replace(/"/g, '""') + '"';
}
return str;
};
// Header row
const headerRow = detectedColumns.map(col => _escapeCsvValue(col.label)).join(separator);
// Data rows
const dataRows = allData.map(row => {
return detectedColumns.map(col => {
let cellValue = row[col.key];
// FK resolution
if (col.fkSource && typeof cellValue === 'string' && cellValue.length > 0) {
const resolved = fkCache[col.fkSource]?.[cellValue];
if (resolved) cellValue = resolved;
}
// Timestamp formatting
const isTimestampField = /(at|date|time|timestamp|created|updated|expires|checked|valuta)$/i.test(col.key);
const isExplicitDateType = col.type && isDateTimeType(col.type);
if ((isTimestampField || isExplicitDateType) && typeof cellValue === 'number') {
try {
let ts = cellValue < 10000000000 ? cellValue : cellValue / 1000;
const formatted = formatUnixTimestamp(ts, undefined, {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
});
cellValue = `${formatted.time} ${formatted.timezone}`;
} catch { /* keep original */ }
}
return _escapeCsvValue(cellValue);
}).join(separator);
});
const csvContent = '\ufeff' + [headerRow, ...dataRows].join('\r\n'); // BOM for Excel
// Trigger download
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `export_${new Date().toISOString().slice(0, 10)}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error('CSV export failed:', error);
} finally {
setCsvExporting(false);
}
}, [csvExporting, detectedColumns, apiEndpoint, currentLanguage, fkCache]);
// Check if inline editing is allowed for a column (based on RBAC permissions)
const canInlineEdit = useMemo(() => {
// Auto-enable if inlineEditable is explicitly true OR if handleInlineUpdate is available
@ -1416,6 +1546,8 @@ export function FormGeneratorTable<T extends Record<string, any>>({
onPageSizeChange={handlePageSizeChange}
supportsBackendPagination={supportsBackendPagination}
hookData={hookData}
onCsvExport={apiEndpoint ? handleCsvExport : undefined}
csvExporting={csvExporting}
/>
)}
@ -1452,7 +1584,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
<thead>
<tr>
{selectable && (
<th className={styles.selectColumn} style={{ width: '50px', minWidth: '50px', maxWidth: '50px' }}>
<th className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
<input
type="checkbox"
checked={(() => {
@ -1610,7 +1742,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
)}
>
{selectable && (
<td className={styles.selectColumn} style={{ width: '50px', minWidth: '50px', maxWidth: '50px' }}>
<td className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
<input
type="checkbox"
checked={selectedRows.has(index)}

View file

@ -104,7 +104,7 @@ export function useAutomations() {
// Fetch permissions from backend
const fetchPermissions = useCallback(async () => {
try {
const perms = await checkPermission('DATA', 'AutomationDefinition');
const perms = await checkPermission('DATA', 'data.automation.AutomationDefinition');
setPermissions(perms);
return perms;
} catch (error: any) {
@ -507,7 +507,7 @@ export function useAutomationTemplates() {
const fetchPermissions = useCallback(async () => {
try {
const perms = await checkPermission('DATA', 'AutomationTemplate');
const perms = await checkPermission('DATA', 'data.automation.AutomationTemplate');
setPermissions(perms);
return perms;
} catch (e: any) {

View file

@ -524,15 +524,12 @@ export function usePromptOperations() {
}
};
const handlePromptUpdate = async (promptId: string, updateData: { name: string; content: string }, _originalData?: any) => {
const handlePromptUpdate = async (promptId: string, updateData: Record<string, any>, _originalData?: any) => {
setUpdateError(null);
try {
// mandateId wird nicht mehr vom Client gesendet
const requestBody = {
name: updateData.name,
content: updateData.content
};
// Pass all provided fields (supports partial inline updates like isSystem toggle)
const { id, mandateId, _createdBy, _createdAt, _modifiedAt, _permissions, ...requestBody } = updateData;
const updatedPrompt = await updatePromptApi(request, promptId, requestBody);
@ -555,8 +552,8 @@ export function usePromptOperations() {
};
// Generic inline update handler for FormGeneratorTable
const handleInlineUpdate = async (promptId: string, changes: Partial<{ name: string; content: string }>) => {
const result = await handlePromptUpdate(promptId, changes as { name: string; content: string });
const handleInlineUpdate = async (promptId: string, changes: Record<string, any>) => {
const result = await handlePromptUpdate(promptId, changes);
if (!result.success) {
throw new Error(result.error || 'Failed to update');
}

View file

@ -3,55 +3,44 @@
*
* System-Übersicht für den User.
* Zeigt alle verfügbaren Feature-Instanzen als Karten an.
* Daten kommen vom Backend via GET /api/navigation.
*/
import React from 'react';
import { Link } from 'react-router-dom';
import { useMandates, useFeatureStore } from '../stores/featureStore';
import { getLabel, FEATURE_REGISTRY } from '../types/mandate';
import type { FeatureInstance } from '../types/mandate';
import { FaBriefcase, FaRobot, FaPlay, FaArrowRight } from 'react-icons/fa';
import useNavigation from '../hooks/useNavigation';
import type { NavigationMandate, MandateFeature, FeatureInstance as NavFeatureInstance } from '../hooks/useNavigation';
import { getPageIcon } from '../config/pageRegistry';
import { FaArrowRight } from 'react-icons/fa';
import styles from './Dashboard.module.css';
// =============================================================================
// FEATURE ICONS
// =============================================================================
const FEATURE_ICONS: Record<string, React.ReactNode> = {
trustee: <FaBriefcase size={24} />,
chatbot: <FaRobot size={24} />,
chatworkflow: <FaPlay size={24} />,
};
// =============================================================================
// INSTANCE CARD
// =============================================================================
interface InstanceCardProps {
instance: FeatureInstance;
featureLabel: string;
instance: NavFeatureInstance;
feature: MandateFeature;
mandateLabel: string;
}
const InstanceCard: React.FC<InstanceCardProps> = ({ instance, featureLabel }) => {
const basePath = `/mandates/${instance.mandateId}/${instance.featureCode}/${instance.id}`;
// Ersten verfügbaren View finden
const featureConfig = FEATURE_REGISTRY[instance.featureCode];
const firstView = featureConfig?.views?.[0];
const targetPath = firstView ? `${basePath}/${firstView.path}` : basePath;
const InstanceCard: React.FC<InstanceCardProps> = ({ instance, feature, mandateLabel }) => {
// Ersten verfügbaren View-Pfad vom Backend nehmen
const targetPath = instance.views.length > 0 ? instance.views[0].uiPath : undefined;
if (!targetPath) return null;
return (
<Link to={targetPath} className={styles.instanceCard}>
<div className={styles.cardIcon}>
{FEATURE_ICONS[instance.featureCode] || <FaBriefcase size={24} />}
{getPageIcon(feature.uiComponent)}
</div>
<div className={styles.cardContent}>
<div className={styles.cardHeader}>
<span className={styles.featureLabel}>{featureLabel}</span>
<span className={styles.roleBadge}>{instance.userRoles?.join(', ') || '-'}</span>
<span className={styles.featureLabel}>{feature.uiLabel}</span>
</div>
<h3 className={styles.instanceLabel}>{instance.instanceLabel}</h3>
<p className={styles.mandateName}>{instance.mandateName}</p>
<h3 className={styles.instanceLabel}>{instance.uiLabel}</h3>
<p className={styles.mandateName}>{mandateLabel}</p>
</div>
<div className={styles.cardArrow}>
<FaArrowRight />
@ -78,59 +67,80 @@ const EmptyState: React.FC = () => (
// =============================================================================
export const DashboardPage: React.FC = () => {
const mandates = useMandates();
const { hasAnyInstance, getAllInstances } = useFeatureStore();
// Alle Instanzen sammeln für Übersicht
const allInstances = getAllInstances();
// Gruppiere nach Feature
const instancesByFeature = allInstances.reduce((acc, instance) => {
const featureCode = instance.featureCode;
if (!acc[featureCode]) {
acc[featureCode] = [];
}
acc[featureCode].push(instance);
return acc;
}, {} as Record<string, FeatureInstance[]>);
if (!hasAnyInstance()) {
const { dynamicBlock, loading } = useNavigation();
// Alle Mandate und deren Features/Instanzen aus der Navigation
const mandates: NavigationMandate[] = dynamicBlock?.mandates || [];
// Gesamtzahl Instanzen und Mandate berechnen
let totalInstances = 0;
const totalMandates = mandates.length;
mandates.forEach(m => m.features.forEach(f => {
totalInstances += f.instances.length;
}));
if (loading) {
return (
<div className={styles.dashboard}>
<header className={styles.header}>
<h1>Übersicht</h1>
<p className={styles.subtitle}>Lade...</p>
</header>
</div>
);
}
if (totalInstances === 0) {
return <EmptyState />;
}
// Gruppiere Instanzen nach Feature (über alle Mandate)
const featureGroups: { feature: MandateFeature; instances: { instance: NavFeatureInstance; mandateLabel: string }[] }[] = [];
const featureMap = new Map<string, typeof featureGroups[0]>();
for (const mandate of mandates) {
for (const feature of mandate.features) {
const key = feature.uiComponent;
let group = featureMap.get(key);
if (!group) {
group = { feature, instances: [] };
featureMap.set(key, group);
featureGroups.push(group);
}
for (const instance of feature.instances) {
group.instances.push({ instance, mandateLabel: mandate.uiLabel });
}
}
}
return (
<div className={styles.dashboard}>
<header className={styles.header}>
<h1>Übersicht</h1>
<p className={styles.subtitle}>
Du hast Zugriff auf {allInstances.length} Feature-Instanz{allInstances.length !== 1 ? 'en' : ''}
in {mandates.length} Mandant{mandates.length !== 1 ? 'en' : ''}.
Du hast Zugriff auf {totalInstances} Feature-Instanz{totalInstances !== 1 ? 'en' : ''} in {totalMandates} Mandant{totalMandates !== 1 ? 'en' : ''}.
</p>
</header>
<main className={styles.content}>
{Object.entries(instancesByFeature).map(([featureCode, instances]) => {
const featureConfig = FEATURE_REGISTRY[featureCode];
const featureLabel = featureConfig ? getLabel(featureConfig.label) : featureCode;
return (
<section key={featureCode} className={styles.featureSection}>
<h2 className={styles.sectionTitle}>
{FEATURE_ICONS[featureCode]}
<span>{featureLabel}</span>
</h2>
<div className={styles.instanceGrid}>
{instances.map(instance => (
<InstanceCard
key={instance.id}
instance={instance}
featureLabel={featureLabel}
/>
))}
</div>
</section>
);
})}
{featureGroups.map(({ feature, instances }) => (
<section key={feature.uiComponent} className={styles.featureSection}>
<h2 className={styles.sectionTitle}>
{getPageIcon(feature.uiComponent)}
<span>{feature.uiLabel}</span>
</h2>
<div className={styles.instanceGrid}>
{instances.map(({ instance, mandateLabel }) => (
<InstanceCard
key={instance.id}
instance={instance}
feature={feature}
mandateLabel={mandateLabel}
/>
))}
</div>
</section>
))}
</main>
</div>
);

View file

@ -8,7 +8,7 @@
import React from 'react';
import { useCurrentInstance } from '../hooks/useCurrentInstance';
import { useCanViewFeatureView } from '../hooks/useInstancePermissions';
import { getLabel, FEATURE_REGISTRY } from '../types/mandate';
import useNavigation from '../hooks/useNavigation';
// Trustee Views
// Note: TrusteeOrganisationsView and TrusteeContractsView removed - Feature-Instanz = Organisation
@ -129,31 +129,13 @@ interface FeatureViewPageProps {
}
export const FeatureViewPage: React.FC<FeatureViewPageProps> = ({ view }) => {
const { instance, featureCode, isValid } = useCurrentInstance();
const { instance, featureCode, mandateId, isValid } = useCurrentInstance();
const { dynamicBlock } = useNavigation();
// Berechtigungs-Check
const viewCode = `${featureCode}-${view}`;
const canView = useCanViewFeatureView(viewCode);
// DEBUG: Log permission check for chatbot
if (featureCode === 'chatbot') {
console.log('🔍 [DEBUG] FeatureView Permission Check:', {
featureCode,
view,
viewCode,
instanceId: instance?.id,
instanceLabel: instance?.instanceLabel,
isValid,
canView,
permissions: instance?.permissions,
views: instance?.permissions?.views,
viewKeys: instance?.permissions?.views ? Object.keys(instance.permissions.views) : [],
hasLegacyView: instance?.permissions?.views?.[viewCode],
hasFullObjectKey: instance?.permissions?.views?.[`ui.feature.${featureCode}.${view}`],
hasWildcard: instance?.permissions?.views?.['_all'],
});
}
// Nicht valider Kontext
if (!isValid || !featureCode || !instance) {
return <NotFound />;
@ -175,10 +157,17 @@ export const FeatureViewPage: React.FC<FeatureViewPageProps> = ({ view }) => {
return <NotFound />;
}
// View-Info aus Registry
const featureConfig = FEATURE_REGISTRY[featureCode];
const viewConfig = featureConfig?.views?.find(v => v.code === view);
const viewLabel = viewConfig ? getLabel(viewConfig.label) : view;
// View-Label aus Backend-Navigation ermitteln
let viewLabel = view;
if (dynamicBlock) {
const navMandate = dynamicBlock.mandates.find(m => m.id === mandateId);
const navFeature = navMandate?.features.find(f => f.uiComponent.includes(featureCode));
const navInstance = navFeature?.instances.find(i => i.id === instance.id);
const navView = navInstance?.views.find(v => v.uiComponent.includes(view));
if (navView) {
viewLabel = navView.uiLabel;
}
}
return (
<div className={styles.featureView}>

View file

@ -439,6 +439,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
<FormGeneratorTable
data={instances}
columns={columns}
apiEndpoint="/api/features/instances"
loading={loading}
pagination={true}
pageSize={25}

View file

@ -528,6 +528,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
<FormGeneratorTable
data={instanceUsers}
columns={columns}
apiEndpoint={selectedInstanceId ? `/api/features/instances/${selectedInstanceId}/users` : undefined}
loading={usersLoading}
pagination={true}
pageSize={25}

View file

@ -353,6 +353,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
<FormGeneratorTable
data={roles}
columns={columns}
apiEndpoint="/api/features/templates/roles"
loading={loading}
pagination={true}
pageSize={25}

View file

@ -349,6 +349,7 @@ export const AdminInvitationsPage: React.FC = () => {
<FormGeneratorTable
data={invitations}
columns={columns}
apiEndpoint="/api/invitations/"
loading={loading}
pagination={true}
pageSize={25}

View file

@ -433,6 +433,7 @@ export const AdminMandateRolesPage: React.FC = () => {
<FormGeneratorTable
data={roles}
columns={columns}
apiEndpoint="/api/rbac/roles"
loading={loading}
pagination={true}
pageSize={25}

View file

@ -162,6 +162,7 @@ export const AdminMandatesPage: React.FC = () => {
<FormGeneratorTable
data={mandates}
columns={columns}
apiEndpoint="/api/mandates/"
loading={loading}
pagination={true}
pageSize={25}

View file

@ -188,7 +188,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
{overview.mandates.length === 0 ? (
<p className={styles.emptyHint}>Keine Mandate-Zuordnungen vorhanden.</p>
) : (
<div className={styles.rolesList}>
<div className={styles.rolesList} style={{ flex: 'none', overflow: 'visible' }}>
{overview.mandates.map(mandate => (
<div key={mandate.id} className={styles.roleCard}>
<div
@ -250,7 +250,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
{overview.roles.length === 0 ? (
<p className={styles.emptyHint}>Keine Rollen zugewiesen.</p>
) : (
<div className={styles.rolesList}>
<div className={styles.rolesList} style={{ flex: 'none', overflow: 'visible' }}>
{overview.roles.map(role => (
<div key={role.id} className={styles.roleCard}>
<div
@ -590,7 +590,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
) : overview ? (
<>
{/* User Info */}
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
<div className={styles.infoBox} style={{ marginBottom: '1rem', flexShrink: 0 }}>
<strong>{overview.user.fullName || overview.user.username}</strong>
<span style={{ margin: '0 1rem', color: 'var(--text-secondary)' }}>|</span>
<span>{overview.user.email}</span>
@ -610,7 +610,8 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
gap: '0.5rem',
marginBottom: '1rem',
borderBottom: '1px solid var(--border-color)',
paddingBottom: '0.5rem'
paddingBottom: '0.5rem',
flexShrink: 0
}}>
<button
className={activeTab === 'overview' ? styles.primaryButton : styles.secondaryButton}

View file

@ -352,6 +352,7 @@ export const AdminUserMandatesPage: React.FC = () => {
<FormGeneratorTable
data={users}
columns={columns}
apiEndpoint={selectedMandateId ? `/api/mandates/${selectedMandateId}/users` : undefined}
loading={loading}
pagination={true}
pageSize={25}

View file

@ -203,6 +203,7 @@ export const AdminUsersPage: React.FC = () => {
<FormGeneratorTable
data={users}
columns={columns}
apiEndpoint="/api/users/"
loading={loading}
pagination={true}
pageSize={25}

View file

@ -43,19 +43,36 @@ export const ConnectionsPage: React.FC = () => {
refetch();
}, []);
// Generate columns from attributes
// Generate columns from attributes - hide internal/redundant fields
const columns = useMemo(() => {
return (attributes || []).map(attr => ({
key: attr.name,
label: attr.label || attr.name,
type: attr.type as any,
sortable: attr.sortable !== false,
filterable: attr.filterable !== false,
searchable: attr.searchable !== false,
width: attr.width || 150,
minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400,
}));
const hiddenColumns = ['id', 'externalId', 'tokenStatus', 'tokenExpiresAt', 'grantedScopes'];
return (attributes || [])
.filter(attr => !hiddenColumns.includes(attr.name))
.map(attr => {
const col: any = {
key: attr.name,
label: attr.label || attr.name,
type: attr.type as any,
sortable: attr.sortable !== false,
filterable: attr.filterable !== false,
searchable: attr.searchable !== false,
width: attr.width || 150,
minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400,
fkSource: (attr as any).fkSource,
fkDisplayField: (attr as any).fkDisplayField,
};
// Resolve userId to username via FK
if (attr.name === 'userId') {
col.fkSource = '/api/users/';
col.fkDisplayField = 'username';
col.label = 'User';
}
return col;
});
}, [attributes]);
// Check permissions
@ -258,6 +275,7 @@ export const ConnectionsPage: React.FC = () => {
<FormGeneratorTable
data={connections}
columns={columns}
apiEndpoint="/api/connections/"
loading={loading}
pagination={true}
pageSize={25}

View file

@ -60,19 +60,40 @@ export const FilesPage: React.FC = () => {
refetch();
}, []);
// Generate columns from attributes
// Generate columns from attributes - hide internal fields
const columns = useMemo(() => {
return (attributes || []).map(attr => ({
key: attr.name,
label: attr.label || attr.name,
type: attr.type as any,
sortable: attr.sortable !== false,
filterable: attr.filterable !== false,
searchable: attr.searchable !== false,
width: attr.width || 150,
minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400,
}));
const hiddenColumns = ['id', 'mandateId', 'featureInstanceId', 'fileHash'];
const cols = (attributes || [])
.filter(attr => !hiddenColumns.includes(attr.name))
.map(attr => ({
key: attr.name,
label: attr.label || attr.name,
type: attr.type as any,
sortable: attr.sortable !== false,
filterable: attr.filterable !== false,
searchable: attr.searchable !== false,
width: attr.width || 150,
minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400,
}));
// Add _createdBy column with FK resolution to show username
cols.push({
key: '_createdBy',
label: 'Created By',
type: 'text' as any,
sortable: true,
filterable: false,
searchable: false,
width: 150,
minWidth: 100,
maxWidth: 250,
fkSource: '/api/users/',
fkDisplayField: 'username',
});
return cols;
}, [attributes]);
// Check permissions
@ -255,6 +276,7 @@ export const FilesPage: React.FC = () => {
<FormGeneratorTable
data={files}
columns={columns}
apiEndpoint="/api/files/list"
loading={loading}
pagination={true}
pageSize={25}

View file

@ -53,9 +53,9 @@ export const PromptsPage: React.FC = () => {
// Generate columns from attributes - exclude ID fields from display
const columns = useMemo(() => {
// Fields to hide in table view
const hiddenColumns = ['id', 'mandateId', '_createdBy', '_createdAt', '_modifiedAt', '_hideDelete'];
const hiddenColumns = ['id', 'mandateId', '_createdAt', '_modifiedAt', '_hideDelete', '_permissions'];
return (attributes || [])
const cols = (attributes || [])
.filter(attr => !hiddenColumns.includes(attr.name))
.map(attr => ({
key: attr.name,
@ -67,7 +67,26 @@ export const PromptsPage: React.FC = () => {
width: attr.name === 'content' ? 300 : attr.width || 150,
minWidth: attr.minWidth || 100,
maxWidth: attr.name === 'content' ? 500 : attr.maxWidth || 400,
fkSource: (attr as any).fkSource,
fkDisplayField: (attr as any).fkDisplayField,
}));
// Add _createdBy column with FK resolution to show username
cols.push({
key: '_createdBy',
label: 'Created By',
type: 'text' as any,
sortable: true,
filterable: false,
searchable: false,
width: 150,
minWidth: 100,
maxWidth: 250,
fkSource: '/api/users/',
fkDisplayField: 'username',
});
return cols;
}, [attributes]);
// Check permissions
@ -118,7 +137,7 @@ export const PromptsPage: React.FC = () => {
// Form attributes for create/edit modal
const formAttributes = useMemo(() => {
const excludedFields = ['id', 'mandateId', '_createdBy', '_createdAt', '_modifiedAt', '_hideDelete'];
const excludedFields = ['id', 'mandateId', 'isSystem', '_createdBy', '_createdAt', '_modifiedAt', '_hideDelete', '_permissions'];
return (attributes || [])
.filter(attr => !excludedFields.includes(attr.name));
}, [attributes]);
@ -189,6 +208,7 @@ export const PromptsPage: React.FC = () => {
<FormGeneratorTable
data={prompts}
columns={columns}
apiEndpoint="/api/prompts"
loading={loading}
pagination={true}
pageSize={25}

View file

@ -474,6 +474,7 @@ export const BillingDataView: React.FC = () => {
<FormGeneratorTable
data={transactions}
columns={columns}
apiEndpoint="/api/billing/balance"
loading={transactionsLoading}
pagination={true}
pageSize={25}

View file

@ -185,6 +185,7 @@ export const RealEstateParcelsView: React.FC = () => {
<FormGeneratorTable
data={parcels}
columns={columns}
apiEndpoint={instanceId ? `/api/realestate/${instanceId}/parcels` : undefined}
loading={loading}
pagination={true}
pageSize={25}

View file

@ -169,6 +169,7 @@ export const RealEstateProjectsView: React.FC = () => {
<FormGeneratorTable
data={projects}
columns={columns}
apiEndpoint={instanceId ? `/api/realestate/${instanceId}/projects` : undefined}
loading={loading}
pagination={true}
pageSize={25}

View file

@ -228,6 +228,7 @@ export const TrusteeDocumentsView: React.FC = () => {
<FormGeneratorTable
data={documents}
columns={columns}
apiEndpoint={instanceId ? `/api/trustee/${instanceId}/documents` : undefined}
loading={loading}
pagination={true}
pageSize={25}

View file

@ -201,6 +201,7 @@ export const TrusteePositionDocumentsView: React.FC = () => {
<FormGeneratorTable
data={links}
columns={columns}
apiEndpoint={instanceId ? `/api/trustee/${instanceId}/position-documents` : undefined}
loading={loading}
pagination={true}
pageSize={25}

View file

@ -222,6 +222,7 @@ export const TrusteePositionsView: React.FC = () => {
<FormGeneratorTable
data={positions}
columns={columns}
apiEndpoint={instanceId ? `/api/trustee/${instanceId}/positions` : undefined}
loading={loading}
pagination={true}
pageSize={25}

View file

@ -170,6 +170,7 @@ export const AutomationTemplatesPage: React.FC = () => {
<FormGeneratorTable
data={templates as any[]}
columns={columns}
apiEndpoint="/api/automation-templates"
loading={loading}
pagination={true}
pageSize={25}

View file

@ -125,11 +125,16 @@ export const AutomationsPage: React.FC = () => {
}
}, [executionModal.logs]);
// Generate columns from attributes - exclude ID fields from display
// Generate columns from attributes - exclude internal fields, add enriched display columns
const columns = useMemo(() => {
const hiddenColumns = ['id', 'mandateId', '_createdBy', '_createdAt', '_modifiedAt', 'template', 'executionLogs', 'placeholders'];
const hiddenColumns = [
'id', 'mandateId', 'featureInstanceId', '_createdBy', '_createdAt', '_modifiedAt',
'template', 'executionLogs', 'placeholders',
// Hide enriched fields from attribute list (added manually below)
'_createdByUserName', 'mandateName', 'featureInstanceName',
];
return (attributes || [])
const attrColumns = (attributes || [])
.filter(attr => !hiddenColumns.includes(attr.name))
.map(attr => ({
key: attr.name,
@ -142,6 +147,15 @@ export const AutomationsPage: React.FC = () => {
minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400,
}));
// Add enriched display columns (from backend enrichment)
const enrichedColumns = [
{ key: 'mandateName', label: 'Mandant', type: 'text' as any, sortable: true, filterable: true, searchable: true, width: 150, minWidth: 100, maxWidth: 250 },
{ key: 'featureInstanceName', label: 'Feature-Instanz', type: 'text' as any, sortable: true, filterable: true, searchable: true, width: 160, minWidth: 100, maxWidth: 250 },
{ key: '_createdByUserName', label: 'Erstellt von', type: 'text' as any, sortable: true, filterable: true, searchable: true, width: 150, minWidth: 100, maxWidth: 250 },
];
return [...attrColumns, ...enrichedColumns];
}, [attributes]);
// Check permissions
@ -583,6 +597,7 @@ export const AutomationsPage: React.FC = () => {
<FormGeneratorTable
data={automations as any[]}
columns={columns}
apiEndpoint="/api/automations"
loading={loading}
pagination={true}
pageSize={25}

View file

@ -173,6 +173,7 @@ export const WorkflowsPage: React.FC = () => {
<FormGeneratorTable
data={workflows}
columns={columns}
apiEndpoint="/api/workflows/"
loading={loading}
pagination={true}
pageSize={25}