logical fixes
This commit is contained in:
parent
75125e3f58
commit
2a1cd2ef64
30 changed files with 449 additions and 170 deletions
|
|
@ -22,6 +22,7 @@ export interface Automation {
|
|||
_updatedAt?: number;
|
||||
_createdByUserName?: string;
|
||||
mandateName?: string;
|
||||
featureInstanceName?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -439,6 +439,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
<FormGeneratorTable
|
||||
data={instances}
|
||||
columns={columns}
|
||||
apiEndpoint="/api/features/instances"
|
||||
loading={loading}
|
||||
pagination={true}
|
||||
pageSize={25}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -349,6 +349,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
<FormGeneratorTable
|
||||
data={invitations}
|
||||
columns={columns}
|
||||
apiEndpoint="/api/invitations/"
|
||||
loading={loading}
|
||||
pagination={true}
|
||||
pageSize={25}
|
||||
|
|
|
|||
|
|
@ -433,6 +433,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
<FormGeneratorTable
|
||||
data={roles}
|
||||
columns={columns}
|
||||
apiEndpoint="/api/rbac/roles"
|
||||
loading={loading}
|
||||
pagination={true}
|
||||
pageSize={25}
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
<FormGeneratorTable
|
||||
data={mandates}
|
||||
columns={columns}
|
||||
apiEndpoint="/api/mandates/"
|
||||
loading={loading}
|
||||
pagination={true}
|
||||
pageSize={25}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@ export const AdminUsersPage: React.FC = () => {
|
|||
<FormGeneratorTable
|
||||
data={users}
|
||||
columns={columns}
|
||||
apiEndpoint="/api/users/"
|
||||
loading={loading}
|
||||
pagination={true}
|
||||
pageSize={25}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -474,6 +474,7 @@ export const BillingDataView: React.FC = () => {
|
|||
<FormGeneratorTable
|
||||
data={transactions}
|
||||
columns={columns}
|
||||
apiEndpoint="/api/billing/balance"
|
||||
loading={transactionsLoading}
|
||||
pagination={true}
|
||||
pageSize={25}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ export const WorkflowsPage: React.FC = () => {
|
|||
<FormGeneratorTable
|
||||
data={workflows}
|
||||
columns={columns}
|
||||
apiEndpoint="/api/workflows/"
|
||||
loading={loading}
|
||||
pagination={true}
|
||||
pageSize={25}
|
||||
|
|
|
|||
Loading…
Reference in a new issue