ux cleanup panel basics
Some checks failed
Deploy Nyla Frontend to Integration / deploy (push) Failing after 53s
Some checks failed
Deploy Nyla Frontend to Integration / deploy (push) Failing after 53s
This commit is contained in:
parent
d579df1c92
commit
766a767d59
26 changed files with 358 additions and 204 deletions
68
src/api.ts
68
src/api.ts
|
|
@ -106,18 +106,27 @@ api.interceptors.request.use(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add a response interceptor to handle token expiration
|
// Silent refresh: attempt token renewal before forcing re-login
|
||||||
|
let _isRefreshing = false;
|
||||||
|
let _refreshSubscribers: Array<(success: boolean) => void> = [];
|
||||||
|
|
||||||
|
function _onRefreshDone(success: boolean) {
|
||||||
|
_refreshSubscribers.forEach(cb => cb(success));
|
||||||
|
_refreshSubscribers = [];
|
||||||
|
}
|
||||||
|
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
async (error) => {
|
async (error) => {
|
||||||
if (error.response?.status === 401) {
|
const originalRequest = error.config;
|
||||||
// Don't redirect to login if the request was to a login endpoint
|
|
||||||
const isLoginEndpoint = error.config?.url?.includes('/login') ||
|
if (error.response?.status === 401) {
|
||||||
error.config?.url?.includes('/api/local/login') ||
|
const isAuthEndpoint = originalRequest?.url?.includes('/login') ||
|
||||||
error.config?.url?.includes('/api/msft/auth/login') ||
|
originalRequest?.url?.includes('/api/local/login') ||
|
||||||
error.config?.url?.includes('/api/google/auth/login');
|
originalRequest?.url?.includes('/api/local/refresh') ||
|
||||||
|
originalRequest?.url?.includes('/api/msft/auth/login') ||
|
||||||
|
originalRequest?.url?.includes('/api/google/auth/login');
|
||||||
|
|
||||||
// Don't redirect if we're on a public auth page (prevents redirect loops and allows public pages to work)
|
|
||||||
const pathname = window.location.pathname;
|
const pathname = window.location.pathname;
|
||||||
const isOnPublicAuthPage = pathname === '/login' ||
|
const isOnPublicAuthPage = pathname === '/login' ||
|
||||||
pathname.startsWith('/login') ||
|
pathname.startsWith('/login') ||
|
||||||
|
|
@ -129,19 +138,52 @@ api.interceptors.response.use(
|
||||||
pathname.startsWith('/password-reset-request') ||
|
pathname.startsWith('/password-reset-request') ||
|
||||||
pathname.startsWith('/invite');
|
pathname.startsWith('/invite');
|
||||||
|
|
||||||
if (!isLoginEndpoint && !isOnPublicAuthPage) {
|
if (isAuthEndpoint || isOnPublicAuthPage) {
|
||||||
// Clear local auth data (httpOnly cookies are cleared by backend)
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt silent refresh (only once per request)
|
||||||
|
if (!originalRequest._retryAfterRefresh) {
|
||||||
|
originalRequest._retryAfterRefresh = true;
|
||||||
|
|
||||||
|
if (!_isRefreshing) {
|
||||||
|
_isRefreshing = true;
|
||||||
|
try {
|
||||||
|
await api.post('/api/local/refresh');
|
||||||
|
_isRefreshing = false;
|
||||||
|
_onRefreshDone(true);
|
||||||
|
return api(originalRequest);
|
||||||
|
} catch {
|
||||||
|
_isRefreshing = false;
|
||||||
|
_onRefreshDone(false);
|
||||||
sessionStorage.removeItem('auth_authority');
|
sessionStorage.removeItem('auth_authority');
|
||||||
clearUserDataCache();
|
clearUserDataCache();
|
||||||
// Redirect to login
|
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Another request is already refreshing; queue this one
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
_refreshSubscribers.push((success: boolean) => {
|
||||||
|
if (success) {
|
||||||
|
resolve(api(originalRequest));
|
||||||
|
} else {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle rate limiting (429) - don't throw, just log and return error
|
// Refresh already failed for this request
|
||||||
|
sessionStorage.removeItem('auth_authority');
|
||||||
|
clearUserDataCache();
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle rate limiting (429)
|
||||||
if (error.response?.status === 429) {
|
if (error.response?.status === 429) {
|
||||||
console.warn('Rate limit exceeded (429). Please wait before making more requests.');
|
console.warn('Rate limit exceeded (429). Please wait before making more requests.');
|
||||||
// Don't cause cascading errors by throwing here
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
|
|
|
||||||
|
|
@ -3332,7 +3332,9 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
colSpan={(selectable ? 1 : 0) + (hasActionColumn ? 1 : 0) + detectedColumns.length}
|
colSpan={(selectable ? 1 : 0) + (hasActionColumn ? 1 : 0) + detectedColumns.length}
|
||||||
style={{ textAlign: 'center', padding: '40px 16px', color: 'var(--text-muted, #64748b)' }}
|
style={{ textAlign: 'center', padding: '40px 16px', color: 'var(--text-muted, #64748b)' }}
|
||||||
>
|
>
|
||||||
{emptyMessage || t('Keine Daten verfügbar')}
|
{supportsBackendPagination && !hookDataProp?.pagination
|
||||||
|
? t('Lade Daten...')
|
||||||
|
: (emptyMessage || t('Keine Daten verfügbar'))}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
* Tab D: Neutralization Mappings — FormGeneratorTable + delete
|
* Tab D: Neutralization Mappings — FormGeneratorTable + delete
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ResponsiveContainer, LineChart, Line, XAxis, YAxis, CartesianGrid,
|
ResponsiveContainer, LineChart, Line, XAxis, YAxis, CartesianGrid,
|
||||||
|
|
@ -195,6 +195,10 @@ export const ComplianceAuditPage: React.FC = () => {
|
||||||
|
|
||||||
const selectedEntryId = searchParams.get('entryId');
|
const selectedEntryId = searchParams.get('entryId');
|
||||||
|
|
||||||
|
const _lastAiLogParams = useRef<any>(undefined);
|
||||||
|
const _lastAuditLogParams = useRef<any>(undefined);
|
||||||
|
const _lastNeutParams = useRef<any>(undefined);
|
||||||
|
|
||||||
// ── Mandate loader ──
|
// ── Mandate loader ──
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -222,6 +226,8 @@ export const ComplianceAuditPage: React.FC = () => {
|
||||||
|
|
||||||
const _loadAiLog = useCallback(async (paginationParams?: any) => {
|
const _loadAiLog = useCallback(async (paginationParams?: any) => {
|
||||||
if (!selectedMandateId) return;
|
if (!selectedMandateId) return;
|
||||||
|
if (paginationParams) _lastAiLogParams.current = paginationParams;
|
||||||
|
else paginationParams = _lastAiLogParams.current;
|
||||||
setAiLoading(true);
|
setAiLoading(true);
|
||||||
try {
|
try {
|
||||||
const page = paginationParams?.page ?? 1;
|
const page = paginationParams?.page ?? 1;
|
||||||
|
|
@ -254,6 +260,8 @@ export const ComplianceAuditPage: React.FC = () => {
|
||||||
|
|
||||||
const _loadAuditLog = useCallback(async (paginationParams?: any) => {
|
const _loadAuditLog = useCallback(async (paginationParams?: any) => {
|
||||||
if (!selectedMandateId) return;
|
if (!selectedMandateId) return;
|
||||||
|
if (paginationParams) _lastAuditLogParams.current = paginationParams;
|
||||||
|
else paginationParams = _lastAuditLogParams.current;
|
||||||
setAuditLoading(true);
|
setAuditLoading(true);
|
||||||
try {
|
try {
|
||||||
const page = paginationParams?.page ?? 1;
|
const page = paginationParams?.page ?? 1;
|
||||||
|
|
@ -301,6 +309,8 @@ export const ComplianceAuditPage: React.FC = () => {
|
||||||
|
|
||||||
const _loadNeutMappings = useCallback(async (paginationParams?: any) => {
|
const _loadNeutMappings = useCallback(async (paginationParams?: any) => {
|
||||||
if (!selectedMandateId) return;
|
if (!selectedMandateId) return;
|
||||||
|
if (paginationParams) _lastNeutParams.current = paginationParams;
|
||||||
|
else paginationParams = _lastNeutParams.current;
|
||||||
setNeutLoading(true);
|
setNeutLoading(true);
|
||||||
try {
|
try {
|
||||||
const page = paginationParams?.page ?? 1;
|
const page = paginationParams?.page ?? 1;
|
||||||
|
|
@ -369,10 +379,7 @@ export const ComplianceAuditPage: React.FC = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedMandateId) return;
|
if (!selectedMandateId) return;
|
||||||
if (activeTab === 'ai-log') void _loadAiLog();
|
if (activeTab === 'stats') void _loadStats({ dateFrom: statsPeriod.fromDate, dateTo: statsPeriod.toDate });
|
||||||
else if (activeTab === 'audit-log') void _loadAuditLog();
|
|
||||||
else if (activeTab === 'stats') void _loadStats({ dateFrom: statsPeriod.fromDate, dateTo: statsPeriod.toDate });
|
|
||||||
else if (activeTab === 'neutralization') void _loadNeutMappings();
|
|
||||||
}, [activeTab, selectedMandateId]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [activeTab, selectedMandateId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// ── Content view handler (modal) ──
|
// ── Content view handler (modal) ──
|
||||||
|
|
@ -866,6 +873,7 @@ export const ComplianceAuditPage: React.FC = () => {
|
||||||
filterable={true}
|
filterable={true}
|
||||||
searchable={true}
|
searchable={true}
|
||||||
selectable={false}
|
selectable={false}
|
||||||
|
initialSort={[{ key: 'timestamp', direction: 'desc' }]}
|
||||||
emptyMessage={t('Keine Audit-Einträge vorhanden.')}
|
emptyMessage={t('Keine Audit-Einträge vorhanden.')}
|
||||||
onRefresh={_loadAuditLog}
|
onRefresh={_loadAuditLog}
|
||||||
hookData={auditLogHookData}
|
hookData={auditLogHookData}
|
||||||
|
|
@ -891,6 +899,7 @@ export const ComplianceAuditPage: React.FC = () => {
|
||||||
filterable={true}
|
filterable={true}
|
||||||
searchable={true}
|
searchable={true}
|
||||||
selectable={false}
|
selectable={false}
|
||||||
|
initialSort={[{ key: 'timestamp', direction: 'desc' }]}
|
||||||
emptyMessage={t('Keine AI-Audit-Einträge vorhanden.')}
|
emptyMessage={t('Keine AI-Audit-Einträge vorhanden.')}
|
||||||
onRefresh={_loadAiLog}
|
onRefresh={_loadAiLog}
|
||||||
hookData={aiLogHookData}
|
hookData={aiLogHookData}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,8 @@ const _downloadJson = (data: unknown, fileName: string, mimeType = 'application/
|
||||||
|
|
||||||
export const GDPRPage: React.FC = () => {
|
export const GDPRPage: React.FC = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const contactEmail = 'p.motsch@poweron.swiss';
|
const contactEmail = 'privacy@poweron.swiss';
|
||||||
|
const deleteWord = t('LOESCHEN');
|
||||||
const [consentInfo, setConsentInfo] = useState<ConsentInfo | null>(null);
|
const [consentInfo, setConsentInfo] = useState<ConsentInfo | null>(null);
|
||||||
const [isLoadingConsent, setIsLoadingConsent] = useState(true);
|
const [isLoadingConsent, setIsLoadingConsent] = useState(true);
|
||||||
const [consentError, setConsentError] = useState<string | null>(null);
|
const [consentError, setConsentError] = useState<string | null>(null);
|
||||||
|
|
@ -123,8 +124,8 @@ export const GDPRPage: React.FC = () => {
|
||||||
|
|
||||||
const handleDeleteAccount = async () => {
|
const handleDeleteAccount = async () => {
|
||||||
setActionMessage(null);
|
setActionMessage(null);
|
||||||
if (deleteConfirmText !== 'LOESCHEN') {
|
if (deleteConfirmText !== deleteWord) {
|
||||||
setActionMessage({ type: 'error', text: t('Bitte geben Sie LOESCHEN ein, um die Löschung zu bestätigen.') });
|
setActionMessage({ type: 'error', text: t('Bitte geben Sie {word} ein, um die Löschung zu bestätigen.', { word: deleteWord }) });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,14 +228,14 @@ export const GDPRPage: React.FC = () => {
|
||||||
<div className={styles.deleteConfirm}>
|
<div className={styles.deleteConfirm}>
|
||||||
<p className={styles.deleteWarning}>
|
<p className={styles.deleteWarning}>
|
||||||
{t('Diese Aktion ist unwiderruflich. Geben Sie {word} ein, um zu bestätigen.', {
|
{t('Diese Aktion ist unwiderruflich. Geben Sie {word} ein, um zu bestätigen.', {
|
||||||
word: 'LOESCHEN',
|
word: deleteWord,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
className={styles.deleteInput}
|
className={styles.deleteInput}
|
||||||
value={deleteConfirmText}
|
value={deleteConfirmText}
|
||||||
onChange={(event) => setDeleteConfirmText(event.target.value)}
|
onChange={(event) => setDeleteConfirmText(event.target.value)}
|
||||||
placeholder="LOESCHEN"
|
placeholder={deleteWord}
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
/>
|
/>
|
||||||
<div className={styles.deleteActions}>
|
<div className={styles.deleteActions}>
|
||||||
|
|
@ -251,7 +252,7 @@ export const GDPRPage: React.FC = () => {
|
||||||
<button
|
<button
|
||||||
className={styles.dangerButton}
|
className={styles.dangerButton}
|
||||||
onClick={handleDeleteAccount}
|
onClick={handleDeleteAccount}
|
||||||
disabled={isDeleting || deleteConfirmText !== 'LOESCHEN'}
|
disabled={isDeleting || deleteConfirmText !== deleteWord}
|
||||||
>
|
>
|
||||||
{isDeleting ? (
|
{isDeleting ? (
|
||||||
<span className={styles.buttonSpinner}>
|
<span className={styles.buttonSpinner}>
|
||||||
|
|
@ -290,44 +291,34 @@ export const GDPRPage: React.FC = () => {
|
||||||
<div className={styles.infoBlock}>
|
<div className={styles.infoBlock}>
|
||||||
<h3>{t('Gesammelte Daten')}</h3>
|
<h3>{t('Gesammelte Daten')}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{Object.entries(consentInfo.dataCollected || {}).map(([key, value]) => (
|
<li><strong>{t('Profil')}:</strong> {t('Name, E-Mail, Benutzername, Spracheinstellungen')}</li>
|
||||||
<li key={key}>
|
<li><strong>{t('Authentifizierung')}:</strong> {t('Login-Zeitstempel, Authentifizierungsanbieter')}</li>
|
||||||
<strong>{key}:</strong> {value}
|
<li><strong>{t('Mitgliedschaften')}:</strong> {t('Mandanten- und Feature-Zugriffseinträge')}</li>
|
||||||
</li>
|
<li><strong>{t('Aktivität')}:</strong> {t('Audit-Logs für sicherheitsrelevante Aktionen')}</li>
|
||||||
))}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.infoBlock}>
|
<div className={styles.infoBlock}>
|
||||||
<h3>{t('Verarbeitung')}</h3>
|
<h3>{t('Verarbeitung')}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{Object.entries(consentInfo.dataProcessing || {}).map(([key, value]) => (
|
<li><strong>{t('Zweck')}:</strong> {t('Bereitstellung von Multi-Tenant-Plattformdiensten')}</li>
|
||||||
<li key={key}>
|
<li><strong>{t('Rechtsgrundlage')}:</strong> {t('Vertragserfüllung und berechtigtes Interesse')}</li>
|
||||||
<strong>{key}:</strong> {value}
|
<li><strong>{t('Aufbewahrung')}:</strong> {t('Daten werden aufbewahrt, solange das Konto aktiv ist, und bei Kontolöschung gelöscht')}</li>
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.infoBlock}>
|
<div className={styles.infoBlock}>
|
||||||
<h3>{t('Ihre Rechte')}</h3>
|
<h3>{t('Ihre Rechte')}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{Object.entries(consentInfo.userRights || {}).map(([key, value]) => (
|
<li><strong>{t('Auskunft')}:</strong> {t('Datenexport (Artikel 15)')}</li>
|
||||||
<li key={key}>
|
<li><strong>{t('Datenübertragbarkeit')}:</strong> {t('Portabler Export (Artikel 20)')}</li>
|
||||||
<strong>{key}:</strong> {value}
|
<li><strong>{t('Löschung')}:</strong> {t('Kontolöschung (Artikel 17)')}</li>
|
||||||
</li>
|
<li><strong>{t('Berichtigung')}:</strong> {t('Profildaten bearbeiten (Artikel 16)')}</li>
|
||||||
))}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.infoBlock}>
|
<div className={styles.infoBlock}>
|
||||||
<h3>{t('Kontakt')}</h3>
|
<h3>{t('Kontakt')}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{Object.entries({
|
<li><strong>{t('E-Mail')}:</strong> {contactEmail}</li>
|
||||||
...(consentInfo.contact || {}),
|
<li><strong>{t('Hinweis')}:</strong> {t('Bei Datenschutzanfragen kontaktieren Sie uns bitte mit Ihrer Benutzer-ID.')}</li>
|
||||||
email: contactEmail,
|
|
||||||
}).map(([key, value]) => (
|
|
||||||
<li key={key}>
|
|
||||||
<strong>{key}:</strong> {value}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ import { useSearchParams } from 'react-router-dom';
|
||||||
import { useLanguage } from '../providers/language/LanguageContext';
|
import { useLanguage } from '../providers/language/LanguageContext';
|
||||||
import { useApiRequest } from '../hooks/useApi';
|
import { useApiRequest } from '../hooks/useApi';
|
||||||
import { useConfirm } from '../hooks/useConfirm';
|
import { useConfirm } from '../hooks/useConfirm';
|
||||||
import type { RagInventoryDto, RagConnectionDto, RagFeatureInstanceDto } from '../api/connectionApi';
|
import type { RagInventoryDto, RagConnectionDto, RagDataSourceDto, RagFeatureInstanceDto } from '../api/connectionApi';
|
||||||
|
import api from '../api';
|
||||||
import { FaDatabase, FaStop, FaSync, FaToggleOn, FaToggleOff, FaRedo, FaExclamationTriangle, FaCheckCircle, FaSlidersH, FaCubes } from 'react-icons/fa';
|
import { FaDatabase, FaStop, FaSync, FaToggleOn, FaToggleOff, FaRedo, FaExclamationTriangle, FaCheckCircle, FaSlidersH, FaCubes } from 'react-icons/fa';
|
||||||
import { mandateDisplayLabel } from '../utils/mandateDisplayUtils';
|
import { mandateDisplayLabel } from '../utils/mandateDisplayUtils';
|
||||||
import { DataSourceSettingsModal } from '../components/UnifiedDataBar/DataSourceSettingsModal';
|
import { DataSourceSettingsModal } from '../components/UnifiedDataBar/DataSourceSettingsModal';
|
||||||
|
|
@ -169,6 +170,15 @@ export const RagInventoryPage: React.FC = () => {
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _handleDsRagToggle = async (connectionId: string, ds: RagDataSourceDto) => {
|
||||||
|
const nodeKey = `ds|${connectionId}|${ds.sourceType}|${ds.path}`;
|
||||||
|
const newValue = !ds.ragIndexEnabled;
|
||||||
|
try {
|
||||||
|
await api.post(`/api/udb/node/${encodeURIComponent(nodeKey)}/flag/ragIndexEnabled`, { value: newValue });
|
||||||
|
_fetchInventory();
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
const _handleConsentToggle = async (connectionId: string, currentEnabled: boolean) => {
|
const _handleConsentToggle = async (connectionId: string, currentEnabled: boolean) => {
|
||||||
if (currentEnabled) {
|
if (currentEnabled) {
|
||||||
const ok = await confirm(t('Alle indexierten Inhalte dieser Verbindung werden entfernt. Fortfahren?'), { variant: 'danger', confirmLabel: t('Fortfahren') });
|
const ok = await confirm(t('Alle indexierten Inhalte dieser Verbindung werden entfernt. Fortfahren?'), { variant: 'danger', confirmLabel: t('Fortfahren') });
|
||||||
|
|
@ -309,6 +319,14 @@ export const RagInventoryPage: React.FC = () => {
|
||||||
{t('{f} Dateien · {c} Chunks', { f: conn.totalFiles, c: conn.totalChunks })}
|
{t('{f} Dateien · {c} Chunks', { f: conn.totalFiles, c: conn.totalChunks })}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
className={styles.reindexBtn}
|
||||||
|
onClick={() => _openSettingsForConnection(conn)}
|
||||||
|
title={t('Einstellungen')}
|
||||||
|
style={{ marginLeft: 'auto' }}
|
||||||
|
>
|
||||||
|
<FaSlidersH size={14} />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className={styles.consentToggle}
|
className={styles.consentToggle}
|
||||||
onClick={() => _handleConsentToggle(conn.id, conn.knowledgeIngestionEnabled)}
|
onClick={() => _handleConsentToggle(conn.id, conn.knowledgeIngestionEnabled)}
|
||||||
|
|
@ -425,7 +443,14 @@ export const RagInventoryPage: React.FC = () => {
|
||||||
>
|
>
|
||||||
{ds.fileCount} {t('Dateien')} · {ds.chunkCount} {t('Chunks')}
|
{ds.fileCount} {t('Dateien')} · {ds.chunkCount} {t('Chunks')}
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.dsIndex}>{ds.ragIndexEnabled ? '\uD83E\uDDE0' : '\u2014'}</span>
|
<button
|
||||||
|
className={styles.dsIndex}
|
||||||
|
onClick={() => _handleDsRagToggle(conn.id, ds)}
|
||||||
|
title={ds.ragIndexEnabled ? t('Indexierung deaktivieren') : t('Indexierung aktivieren')}
|
||||||
|
style={{ cursor: 'pointer', background: 'none', border: 'none', padding: '2px 4px', fontSize: 'inherit' }}
|
||||||
|
>
|
||||||
|
{ds.ragIndexEnabled ? '\uD83E\uDDE0' : '\u2014'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{conn.dataSources.length === 0 && (
|
{conn.dataSources.length === 0 && (
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
const [createFeatureCode, setCreateFeatureCode] = useState<string>('');
|
const [createFeatureCode, setCreateFeatureCode] = useState<string>('');
|
||||||
const [createLabel, setCreateLabel] = useState<string>(''); // Label field value
|
const [createLabel, setCreateLabel] = useState<string>(''); // Label field value
|
||||||
|
|
||||||
// Ref to track form data for featureCode detection
|
|
||||||
const formDataRef = useRef<Record<string, any>>({});
|
const formDataRef = useRef<Record<string, any>>({});
|
||||||
|
const _lastTableParams = useRef<any>(undefined);
|
||||||
|
|
||||||
// Load features, mandates, and attributes on mount
|
// Load features, mandates, and attributes on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -82,12 +82,22 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
}).catch(() => setBackendAttributes([]));
|
}).catch(() => setBackendAttributes([]));
|
||||||
}, [fetchFeatures, fetchMandates]);
|
}, [fetchFeatures, fetchMandates]);
|
||||||
|
|
||||||
// Load instances when mandate changes
|
const _refetchInstances = React.useCallback(async (paginationParams?: any) => {
|
||||||
|
if (!selectedMandateId) return;
|
||||||
|
if (paginationParams && typeof paginationParams === 'object') {
|
||||||
|
_lastTableParams.current = paginationParams;
|
||||||
|
} else {
|
||||||
|
paginationParams = _lastTableParams.current;
|
||||||
|
}
|
||||||
|
if (paginationParams) return fetchInstances(paginationParams);
|
||||||
|
return fetchInstances(selectedMandateId);
|
||||||
|
}, [selectedMandateId, fetchInstances]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedMandateId) {
|
if (selectedMandateId) {
|
||||||
fetchInstances(selectedMandateId);
|
_lastTableParams.current = undefined;
|
||||||
}
|
}
|
||||||
}, [selectedMandateId, fetchInstances]);
|
}, [selectedMandateId]);
|
||||||
|
|
||||||
const _rawColumns: ColumnConfig[] = useMemo(() => [
|
const _rawColumns: ColumnConfig[] = useMemo(() => [
|
||||||
{ key: 'label', label: t('Name'), sortable: true, filterable: true, searchable: true, width: 200 },
|
{ key: 'label', label: t('Name'), sortable: true, filterable: true, searchable: true, width: 200 },
|
||||||
|
|
@ -147,8 +157,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
setCreateFeatureCode('');
|
setCreateFeatureCode('');
|
||||||
setCreateLabel('');
|
setCreateLabel('');
|
||||||
formDataRef.current = {};
|
formDataRef.current = {};
|
||||||
fetchInstances(selectedMandateId);
|
_refetchInstances();
|
||||||
loadFeatures(); // Refresh global navigation cache
|
loadFeatures();
|
||||||
showSuccess(t('Feature-Instanz erstellt'), t('Die Instanz "{name}" wurde erfolgreich erstellt.', { name: createLabel }));
|
showSuccess(t('Feature-Instanz erstellt'), t('Die Instanz "{name}" wurde erfolgreich erstellt.', { name: createLabel }));
|
||||||
} else {
|
} else {
|
||||||
showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Feature-Instanz'));
|
showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Feature-Instanz'));
|
||||||
|
|
@ -185,8 +195,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
setEditingInstance(null);
|
setEditingInstance(null);
|
||||||
fetchInstances(selectedMandateId);
|
_refetchInstances();
|
||||||
loadFeatures(); // Refresh global navigation cache
|
loadFeatures();
|
||||||
showSuccess(t('Feature-Instanz aktualisiert'), t('Die Instanz "{name}" wurde erfolgreich aktualisiert.', { name: data.label }));
|
showSuccess(t('Feature-Instanz aktualisiert'), t('Die Instanz "{name}" wurde erfolgreich aktualisiert.', { name: data.label }));
|
||||||
} else {
|
} else {
|
||||||
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Feature-Instanz'));
|
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Feature-Instanz'));
|
||||||
|
|
@ -315,7 +325,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
<div className={styles.headerActions}>
|
<div className={styles.headerActions}>
|
||||||
<button
|
<button
|
||||||
className={styles.secondaryButton}
|
className={styles.secondaryButton}
|
||||||
onClick={() => fetchInstances(selectedMandateId)}
|
onClick={() => _refetchInstances()}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||||
|
|
@ -426,7 +436,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
hookData={{
|
hookData={{
|
||||||
refetch: fetchInstances,
|
refetch: _refetchInstances,
|
||||||
pagination: instancesPagination,
|
pagination: instancesPagination,
|
||||||
handleDelete: handleDeleteInstance,
|
handleDelete: handleDeleteInstance,
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* Allows adding, removing, and updating user roles within feature instances.
|
* Allows adding, removing, and updating user roles within feature instances.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||||
import { useFeatureAccess, type FeatureAccessUser, type FeatureInstanceRole, type PaginationParams, type PaginationMetadata } from '../../hooks/useFeatureAccess';
|
import { useFeatureAccess, type FeatureAccessUser, type FeatureInstanceRole, type PaginationParams, type PaginationMetadata } from '../../hooks/useFeatureAccess';
|
||||||
import { useUserMandates } from '../../hooks/useUserMandates';
|
import { useUserMandates } from '../../hooks/useUserMandates';
|
||||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||||
|
|
@ -70,6 +70,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||||
const [, setIsSubmitting] = useState(false);
|
const [, setIsSubmitting] = useState(false);
|
||||||
const [usersLoading, setUsersLoading] = useState(false);
|
const [usersLoading, setUsersLoading] = useState(false);
|
||||||
const [usersPagination, setUsersPagination] = useState<PaginationMetadata | null>(null);
|
const [usersPagination, setUsersPagination] = useState<PaginationMetadata | null>(null);
|
||||||
|
const _lastTableParams = useRef<any>(undefined);
|
||||||
|
|
||||||
// Extract mandateId and instanceId from combined key
|
// Extract mandateId and instanceId from combined key
|
||||||
const selectedMandateId = useMemo(() => {
|
const selectedMandateId = useMemo(() => {
|
||||||
|
|
@ -137,21 +138,13 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||||
loadCombinedOptions();
|
loadCombinedOptions();
|
||||||
}, [fetchFeatures, fetchMandates]);
|
}, [fetchFeatures, fetchMandates]);
|
||||||
|
|
||||||
// Load users and roles when instance changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedMandateId && selectedInstanceId) {
|
if (selectedMandateId && selectedInstanceId) {
|
||||||
setUsersLoading(true);
|
_lastTableParams.current = undefined;
|
||||||
Promise.all([
|
fetchInstanceRoles(selectedMandateId, selectedInstanceId)
|
||||||
fetchInstanceUsers(selectedMandateId, selectedInstanceId),
|
.then(setInstanceRoles);
|
||||||
fetchInstanceRoles(selectedMandateId, selectedInstanceId),
|
|
||||||
]).then(([users, roles]) => {
|
|
||||||
setInstanceUsers(users);
|
|
||||||
setInstanceRoles(roles);
|
|
||||||
}).finally(() => {
|
|
||||||
setUsersLoading(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [selectedMandateId, selectedInstanceId, fetchInstanceUsers, fetchInstanceRoles]);
|
}, [selectedMandateId, selectedInstanceId, fetchInstanceRoles]);
|
||||||
|
|
||||||
// Load mandate members for the add modal (only users who are members of the selected mandate)
|
// Load mandate members for the add modal (only users who are members of the selected mandate)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -172,12 +165,15 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||||
}).catch(() => setAllUsers([]));
|
}).catch(() => setAllUsers([]));
|
||||||
}, [selectedMandateId]);
|
}, [selectedMandateId]);
|
||||||
|
|
||||||
// Refresh instance users with optional pagination
|
|
||||||
const refreshUsers = useCallback(async (paginationParams?: PaginationParams) => {
|
const refreshUsers = useCallback(async (paginationParams?: PaginationParams) => {
|
||||||
if (selectedMandateId && selectedInstanceId) {
|
if (selectedMandateId && selectedInstanceId) {
|
||||||
|
if (paginationParams && Object.keys(paginationParams).length > 0) {
|
||||||
|
_lastTableParams.current = paginationParams;
|
||||||
|
} else {
|
||||||
|
paginationParams = _lastTableParams.current;
|
||||||
|
}
|
||||||
setUsersLoading(true);
|
setUsersLoading(true);
|
||||||
try {
|
try {
|
||||||
// Build query params
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (paginationParams && Object.keys(paginationParams).length > 0) {
|
if (paginationParams && Object.keys(paginationParams).length > 0) {
|
||||||
params.append('pagination', JSON.stringify(paginationParams));
|
params.append('pagination', JSON.stringify(paginationParams));
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
* - Actions: Create feature role, edit description, manage AccessRules, delete role
|
* - Actions: Create feature role, edit description, manage AccessRules, delete role
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||||
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
|
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
|
||||||
import { AccessRulesEditor } from '../../components/AccessRules';
|
import { AccessRulesEditor } from '../../components/AccessRules';
|
||||||
|
|
@ -66,6 +66,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
const [editingRole, setEditingRole] = useState<FeatureRole | null>(null);
|
const [editingRole, setEditingRole] = useState<FeatureRole | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [permissionsRole, setPermissionsRole] = useState<FeatureRole | null>(null);
|
const [permissionsRole, setPermissionsRole] = useState<FeatureRole | null>(null);
|
||||||
|
const _lastTableParams = useRef<any>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAttributes(request, 'Role')
|
fetchAttributes(request, 'Role')
|
||||||
|
|
@ -96,12 +97,13 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
||||||
|
|
||||||
const [pagination, setPagination] = useState<any>(null);
|
const [pagination, setPagination] = useState<any>(null);
|
||||||
|
|
||||||
// Load roles when feature changes
|
|
||||||
const fetchRoles = useCallback(async (params?: any) => {
|
const fetchRoles = useCallback(async (params?: any) => {
|
||||||
if (!selectedFeatureCode) {
|
if (!selectedFeatureCode) {
|
||||||
setRoles([]);
|
setRoles([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (params) _lastTableParams.current = params;
|
||||||
|
else params = _lastTableParams.current;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* Allows creating, viewing, and revoking invitations.
|
* Allows creating, viewing, and revoking invitations.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||||
import { useInvitations, type Invitation, type InvitationCreate } from '../../hooks/useInvitations';
|
import { useInvitations, type Invitation, type InvitationCreate } from '../../hooks/useInvitations';
|
||||||
import { useUserMandates, type Mandate, type Role } from '../../hooks/useUserMandates';
|
import { useUserMandates, type Mandate, type Role } from '../../hooks/useUserMandates';
|
||||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||||
|
|
@ -53,6 +53,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
||||||
const [_isSubmitting, setIsSubmitting] = useState(false); // Prefixed with _ to suppress warning
|
const [_isSubmitting, setIsSubmitting] = useState(false); // Prefixed with _ to suppress warning
|
||||||
const [copySuccess, setCopySuccess] = useState(false);
|
const [copySuccess, setCopySuccess] = useState(false);
|
||||||
const [backendAttributes, setBackendAttributes] = useState<AttributeDefinition[]>([]);
|
const [backendAttributes, setBackendAttributes] = useState<AttributeDefinition[]>([]);
|
||||||
|
const _lastTableParams = useRef<any>(undefined);
|
||||||
|
|
||||||
// Load mandates and attributes on mount
|
// Load mandates and attributes on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -69,13 +70,24 @@ export const AdminInvitationsPage: React.FC = () => {
|
||||||
.catch(() => setBackendAttributes([]));
|
.catch(() => setBackendAttributes([]));
|
||||||
}, [fetchMandates, request]);
|
}, [fetchMandates, request]);
|
||||||
|
|
||||||
// Load invitations and roles when mandate changes (same roles as AdminUserMandatesPage: user, viewer, admin)
|
const _refetchInvitations = useCallback(async (paginationParams?: any) => {
|
||||||
|
if (!selectedMandateId) return;
|
||||||
|
if (paginationParams && typeof paginationParams === 'object' && !Array.isArray(paginationParams) && typeof paginationParams !== 'string') {
|
||||||
|
_lastTableParams.current = paginationParams;
|
||||||
|
} else {
|
||||||
|
paginationParams = _lastTableParams.current;
|
||||||
|
}
|
||||||
|
if (paginationParams) return fetchInvitations(paginationParams, { includeExpired: showExpired, includeUsed: showUsed });
|
||||||
|
return fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed });
|
||||||
|
}, [selectedMandateId, showExpired, showUsed, fetchInvitations]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedMandateId) {
|
if (selectedMandateId) {
|
||||||
fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed });
|
_lastTableParams.current = undefined;
|
||||||
|
_refetchInvitations();
|
||||||
fetchRoles(selectedMandateId).then(setRoles);
|
fetchRoles(selectedMandateId).then(setRoles);
|
||||||
}
|
}
|
||||||
}, [selectedMandateId, showExpired, showUsed, fetchInvitations, fetchRoles]);
|
}, [selectedMandateId, showExpired, showUsed, _refetchInvitations, fetchRoles]);
|
||||||
|
|
||||||
// Format timestamp (used by URL modal only).
|
// Format timestamp (used by URL modal only).
|
||||||
const formatDate = (timestamp: number) => {
|
const formatDate = (timestamp: number) => {
|
||||||
|
|
@ -165,7 +177,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
setShowUrlModal(result.data);
|
setShowUrlModal(result.data);
|
||||||
fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed });
|
_refetchInvitations();
|
||||||
} else {
|
} else {
|
||||||
showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Einladung'));
|
showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Einladung'));
|
||||||
}
|
}
|
||||||
|
|
@ -276,7 +288,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
||||||
<div className={styles.headerActions}>
|
<div className={styles.headerActions}>
|
||||||
<button
|
<button
|
||||||
className={styles.secondaryButton}
|
className={styles.secondaryButton}
|
||||||
onClick={() => fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed })}
|
onClick={() => _refetchInvitations()}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||||
|
|
@ -332,7 +344,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
||||||
]}
|
]}
|
||||||
hookData={{
|
hookData={{
|
||||||
handleDelete: handleDeleteInvitation,
|
handleDelete: handleDeleteInvitation,
|
||||||
refetch: (params?: any) => fetchInvitations(params || selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed }),
|
refetch: _refetchInvitations,
|
||||||
pagination,
|
pagination,
|
||||||
}}
|
}}
|
||||||
emptyMessage={t('Keine Einladungen gefunden')}
|
emptyMessage={t('Keine Einladungen gefunden')}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
||||||
// Store current filter state for refetch
|
// Store current filter state for refetch
|
||||||
const currentScopeFilterRef = useRef(scopeFilter);
|
const currentScopeFilterRef = useRef(scopeFilter);
|
||||||
currentScopeFilterRef.current = scopeFilter;
|
currentScopeFilterRef.current = scopeFilter;
|
||||||
|
const _lastTableParams = useRef<any>(undefined);
|
||||||
|
|
||||||
// Load mandates and attributes on mount
|
// Load mandates and attributes on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -81,24 +82,25 @@ export const AdminMandateRolesPage: React.FC = () => {
|
||||||
.catch(() => setBackendAttributes([]));
|
.catch(() => setBackendAttributes([]));
|
||||||
}, [fetchMandates, request]);
|
}, [fetchMandates, request]);
|
||||||
|
|
||||||
// Load roles when mandate or scopeFilter changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedMandateId) {
|
|
||||||
fetchRoles(selectedMandateId, { scopeFilter });
|
|
||||||
}
|
|
||||||
}, [selectedMandateId, scopeFilter, fetchRoles]);
|
|
||||||
|
|
||||||
// Refetch wrapper that accepts pagination params from FormGeneratorTable
|
|
||||||
// and includes the current mandateId and scopeFilter
|
|
||||||
const refetchWithParams = useCallback(async (paginationParams?: PaginationParams) => {
|
const refetchWithParams = useCallback(async (paginationParams?: PaginationParams) => {
|
||||||
if (!selectedMandateId) return;
|
if (!selectedMandateId) return;
|
||||||
// Merge pagination params with current filter state
|
if (paginationParams && Object.keys(paginationParams).length > 0) {
|
||||||
|
_lastTableParams.current = paginationParams;
|
||||||
|
} else {
|
||||||
|
paginationParams = _lastTableParams.current;
|
||||||
|
}
|
||||||
return fetchRoles(selectedMandateId, {
|
return fetchRoles(selectedMandateId, {
|
||||||
...paginationParams,
|
...paginationParams,
|
||||||
scopeFilter: currentScopeFilterRef.current
|
scopeFilter: currentScopeFilterRef.current
|
||||||
});
|
});
|
||||||
}, [selectedMandateId, fetchRoles]);
|
}, [selectedMandateId, fetchRoles]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedMandateId) {
|
||||||
|
refetchWithParams();
|
||||||
|
}
|
||||||
|
}, [selectedMandateId, scopeFilter, refetchWithParams]);
|
||||||
|
|
||||||
const getDescriptionText = (desc: any) => {
|
const getDescriptionText = (desc: any) => {
|
||||||
if (!desc) return '-';
|
if (!desc) return '-';
|
||||||
if (typeof desc === 'string') return desc;
|
if (typeof desc === 'string') return desc;
|
||||||
|
|
@ -183,7 +185,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
await fetchRoles(selectedMandateId, { scopeFilter });
|
await refetchWithParams();
|
||||||
} else {
|
} else {
|
||||||
showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Rolle'));
|
showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Rolle'));
|
||||||
}
|
}
|
||||||
|
|
@ -212,9 +214,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setEditingRole(null);
|
setEditingRole(null);
|
||||||
if (selectedMandateId) {
|
await refetchWithParams();
|
||||||
await fetchRoles(selectedMandateId, { scopeFilter });
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Rolle'));
|
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Rolle'));
|
||||||
}
|
}
|
||||||
|
|
@ -235,8 +235,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
||||||
|
|
||||||
const result = await deleteRole(role.id);
|
const result = await deleteRole(role.id);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Refetch to update the list
|
await refetchWithParams();
|
||||||
await fetchRoles(selectedMandateId, { scopeFilter });
|
|
||||||
} else {
|
} else {
|
||||||
showError(t('Fehler'), result.error || t('Fehler beim Löschen der Rolle'));
|
showError(t('Fehler'), result.error || t('Fehler beim Löschen der Rolle'));
|
||||||
}
|
}
|
||||||
|
|
@ -332,7 +331,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
||||||
<div className={styles.headerActions}>
|
<div className={styles.headerActions}>
|
||||||
<button
|
<button
|
||||||
className={styles.secondaryButton}
|
className={styles.secondaryButton}
|
||||||
onClick={() => fetchRoles(selectedMandateId, { scopeFilter })}
|
onClick={() => refetchWithParams()}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
* Admin page for managing Mandates (tenants) using FormGeneratorTable.
|
* Admin page for managing Mandates (tenants) using FormGeneratorTable.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo, useCallback, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAdminMandates, useMandateFormAttributes, type Mandate } from '../../hooks/useMandates';
|
import { useAdminMandates, useMandateFormAttributes, type Mandate } from '../../hooks/useMandates';
|
||||||
import { useApiRequest } from '../../hooks/useApi';
|
import { useApiRequest } from '../../hooks/useApi';
|
||||||
|
|
@ -60,6 +60,12 @@ export const AdminMandatesPage: React.FC = () => {
|
||||||
} = useMandateFormAttributes();
|
} = useMandateFormAttributes();
|
||||||
|
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const _lastTableParams = useRef<any>(undefined);
|
||||||
|
const _tableRefetch = useCallback(async (paginationParams?: any) => {
|
||||||
|
if (paginationParams) _lastTableParams.current = paginationParams;
|
||||||
|
else paginationParams = _lastTableParams.current;
|
||||||
|
return refetch(paginationParams);
|
||||||
|
}, [refetch]);
|
||||||
/** Mandate row merged with billing fields for FormGenerator */
|
/** Mandate row merged with billing fields for FormGenerator */
|
||||||
const [editingFormData, setEditingFormData] = useState<Record<string, unknown> | null>(null);
|
const [editingFormData, setEditingFormData] = useState<Record<string, unknown> | null>(null);
|
||||||
const [editingBillingWarning, setEditingBillingWarning] = useState<string | null>(null);
|
const [editingBillingWarning, setEditingBillingWarning] = useState<string | null>(null);
|
||||||
|
|
@ -192,7 +198,7 @@ export const AdminMandatesPage: React.FC = () => {
|
||||||
<p className={styles.errorMessage}>
|
<p className={styles.errorMessage}>
|
||||||
{t('Fehler beim Laden der Mandanten')}: {error}
|
{t('Fehler beim Laden der Mandanten')}: {error}
|
||||||
</p>
|
</p>
|
||||||
<button className={styles.secondaryButton} onClick={() => refetch()}>
|
<button className={styles.secondaryButton} onClick={() => _tableRefetch()}>
|
||||||
<FaSync /> {t('Erneut versuchen')}
|
<FaSync /> {t('Erneut versuchen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -229,7 +235,7 @@ export const AdminMandatesPage: React.FC = () => {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={styles.secondaryButton}
|
className={styles.secondaryButton}
|
||||||
onClick={() => refetch()}
|
onClick={() => _tableRefetch()}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||||
|
|
@ -282,7 +288,7 @@ export const AdminMandatesPage: React.FC = () => {
|
||||||
}] : []}
|
}] : []}
|
||||||
onDelete={handleDeleteMandate}
|
onDelete={handleDeleteMandate}
|
||||||
hookData={{
|
hookData={{
|
||||||
refetch,
|
refetch: _tableRefetch,
|
||||||
permissions,
|
permissions,
|
||||||
pagination,
|
pagination,
|
||||||
handleDelete,
|
handleDelete,
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
||||||
|
|
||||||
// Store current mandateId for refetch
|
// Store current mandateId for refetch
|
||||||
const currentMandateIdRef = useRef<string>('');
|
const currentMandateIdRef = useRef<string>('');
|
||||||
|
const _lastTableParams = useRef<any>(undefined);
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const [mandates, setMandates] = useState<Mandate[]>([]);
|
const [mandates, setMandates] = useState<Mandate[]>([]);
|
||||||
|
|
@ -72,23 +73,23 @@ export const AdminUserMandatesPage: React.FC = () => {
|
||||||
.catch(() => setBackendAttributes([]));
|
.catch(() => setBackendAttributes([]));
|
||||||
}, [fetchMandates, request]);
|
}, [fetchMandates, request]);
|
||||||
|
|
||||||
// Load users when mandate changes
|
// Load roles when mandate changes — table auto-reloads via filterScopeKey
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedMandateId) {
|
if (selectedMandateId) {
|
||||||
currentMandateIdRef.current = selectedMandateId;
|
currentMandateIdRef.current = selectedMandateId;
|
||||||
fetchMandateUsers(selectedMandateId);
|
_lastTableParams.current = undefined;
|
||||||
fetchRoles(selectedMandateId).then(setRoles);
|
fetchRoles(selectedMandateId).then(setRoles);
|
||||||
}
|
}
|
||||||
}, [selectedMandateId, fetchMandateUsers, fetchRoles]);
|
}, [selectedMandateId, fetchRoles]);
|
||||||
|
|
||||||
// Refetch wrapper that accepts pagination params from FormGeneratorTable
|
|
||||||
const refetchWithParams = useCallback(async (paginationParams?: PaginationParams) => {
|
const refetchWithParams = useCallback(async (paginationParams?: PaginationParams) => {
|
||||||
const mandateId = currentMandateIdRef.current;
|
const mandateId = currentMandateIdRef.current;
|
||||||
if (!mandateId) return;
|
if (!mandateId) return;
|
||||||
// If pagination params provided, pass them; otherwise just use mandateId
|
|
||||||
if (paginationParams && Object.keys(paginationParams).length > 0) {
|
if (paginationParams && Object.keys(paginationParams).length > 0) {
|
||||||
|
_lastTableParams.current = paginationParams;
|
||||||
return fetchMandateUsers(paginationParams);
|
return fetchMandateUsers(paginationParams);
|
||||||
}
|
}
|
||||||
|
if (_lastTableParams.current) return fetchMandateUsers(_lastTableParams.current);
|
||||||
return fetchMandateUsers(mandateId);
|
return fetchMandateUsers(mandateId);
|
||||||
}, [fetchMandateUsers]);
|
}, [fetchMandateUsers]);
|
||||||
|
|
||||||
|
|
@ -214,7 +215,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
||||||
const result = await addUserToMandate(selectedMandateId, data);
|
const result = await addUserToMandate(selectedMandateId, data);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setShowAddModal(false);
|
setShowAddModal(false);
|
||||||
fetchMandateUsers(selectedMandateId);
|
refetchWithParams();
|
||||||
} else {
|
} else {
|
||||||
showError(t('Fehler'), result.error || t('Fehler beim Hinzufügen des Benutzers'));
|
showError(t('Fehler'), result.error || t('Fehler beim Hinzufügen des Benutzers'));
|
||||||
}
|
}
|
||||||
|
|
@ -231,7 +232,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
||||||
const result = await updateUserRoles(selectedMandateId, editingUser.userId, data.roleIds);
|
const result = await updateUserRoles(selectedMandateId, editingUser.userId, data.roleIds);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setEditingUser(null);
|
setEditingUser(null);
|
||||||
fetchMandateUsers(selectedMandateId);
|
refetchWithParams();
|
||||||
} else {
|
} else {
|
||||||
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Rollen'));
|
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Rollen'));
|
||||||
}
|
}
|
||||||
|
|
@ -309,7 +310,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
||||||
<div className={styles.headerActions}>
|
<div className={styles.headerActions}>
|
||||||
<button
|
<button
|
||||||
className={styles.secondaryButton}
|
className={styles.secondaryButton}
|
||||||
onClick={() => fetchMandateUsers(selectedMandateId)}
|
onClick={() => refetchWithParams()}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
* Admin page for managing Users using FormGeneratorTable.
|
* Admin page for managing Users using FormGeneratorTable.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo, useCallback, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useOrgUsers, useUserOperations } from '../../hooks/useUsers';
|
import { useOrgUsers, useUserOperations } from '../../hooks/useUsers';
|
||||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||||
|
|
@ -61,6 +61,12 @@ export const AdminUsersPage: React.FC = () => {
|
||||||
|
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||||
|
const _lastTableParams = useRef<any>(undefined);
|
||||||
|
const _tableRefetch = useCallback(async (paginationParams?: any) => {
|
||||||
|
if (paginationParams) _lastTableParams.current = paginationParams;
|
||||||
|
else paginationParams = _lastTableParams.current;
|
||||||
|
return refetch(paginationParams);
|
||||||
|
}, [refetch]);
|
||||||
|
|
||||||
// Generate columns from attributes; types from backend via resolveColumnTypes
|
// Generate columns from attributes; types from backend via resolveColumnTypes
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
|
|
@ -96,7 +102,7 @@ export const AdminUsersPage: React.FC = () => {
|
||||||
const result = await createUser(data as Omit<User, 'id'>);
|
const result = await createUser(data as Omit<User, 'id'>);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
refetch(); // Refresh the list
|
_tableRefetch();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -106,7 +112,7 @@ export const AdminUsersPage: React.FC = () => {
|
||||||
const result = await updateUser(editingUser.id, data);
|
const result = await updateUser(editingUser.id, data);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setEditingUser(null);
|
setEditingUser(null);
|
||||||
refetch(); // Refresh the list
|
_tableRefetch();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -114,7 +120,7 @@ export const AdminUsersPage: React.FC = () => {
|
||||||
const handleDeleteUser = async (user: User) => {
|
const handleDeleteUser = async (user: User) => {
|
||||||
const success = await deleteUser(user.id);
|
const success = await deleteUser(user.id);
|
||||||
if (success) {
|
if (success) {
|
||||||
refetch(); // Refresh the list
|
_tableRefetch();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -169,7 +175,7 @@ export const AdminUsersPage: React.FC = () => {
|
||||||
<p className={styles.errorMessage}>
|
<p className={styles.errorMessage}>
|
||||||
{t('Fehler beim Laden der Benutzer')}: {error}
|
{t('Fehler beim Laden der Benutzer')}: {error}
|
||||||
</p>
|
</p>
|
||||||
<button className={styles.secondaryButton} onClick={() => refetch()}>
|
<button className={styles.secondaryButton} onClick={() => _tableRefetch()}>
|
||||||
<FaSync /> {t('Erneut versuchen')}
|
<FaSync /> {t('Erneut versuchen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -207,7 +213,7 @@ export const AdminUsersPage: React.FC = () => {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={styles.secondaryButton}
|
className={styles.secondaryButton}
|
||||||
onClick={() => refetch()}
|
onClick={() => _tableRefetch()}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||||
|
|
@ -257,7 +263,7 @@ export const AdminUsersPage: React.FC = () => {
|
||||||
] : []}
|
] : []}
|
||||||
onDelete={handleDeleteUser}
|
onDelete={handleDeleteUser}
|
||||||
hookData={{
|
hookData={{
|
||||||
refetch,
|
refetch: _tableRefetch,
|
||||||
permissions,
|
permissions,
|
||||||
pagination,
|
pagination,
|
||||||
handleDelete: deleteUser,
|
handleDelete: deleteUser,
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,10 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMandates().then(setMandates);
|
fetchMandates().then((data: Mandate[]) => {
|
||||||
|
data.sort((a, b) => getMandateName(a).localeCompare(getMandateName(b), undefined, { sensitivity: 'base' }));
|
||||||
|
setMandates(data);
|
||||||
|
});
|
||||||
}, [fetchMandates]);
|
}, [fetchMandates]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -144,14 +147,18 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
||||||
if (inviteType === 'mandate') {
|
if (inviteType === 'mandate') {
|
||||||
fetchRoles(selectedMandate.id).then((r: Role[]) => {
|
fetchRoles(selectedMandate.id).then((r: Role[]) => {
|
||||||
const mandateRoles = r.filter((role: Role) => !role.featureInstanceId);
|
const mandateRoles = r.filter((role: Role) => !role.featureInstanceId);
|
||||||
setRoles(mandateRoles.map((role: Role) => ({ id: role.id, roleLabel: role.roleLabel })));
|
const mapped = mandateRoles.map((role: Role) => ({ id: role.id, roleLabel: role.roleLabel }));
|
||||||
|
mapped.sort((a, b) => a.roleLabel.localeCompare(b.roleLabel, undefined, { sensitivity: 'base' }));
|
||||||
|
setRoles(mapped);
|
||||||
});
|
});
|
||||||
fetchMandateUsers(selectedMandate.id).then((users: { userId: string }[]) => {
|
fetchMandateUsers(selectedMandate.id).then((users: { userId: string }[]) => {
|
||||||
setExistingAccessUserIds(new Set(users.map(u => u.userId)));
|
setExistingAccessUserIds(new Set(users.map(u => u.userId)));
|
||||||
}).catch(() => setExistingAccessUserIds(new Set()));
|
}).catch(() => setExistingAccessUserIds(new Set()));
|
||||||
} else if (inviteType === 'featureInstance' && selectedInstance) {
|
} else if (inviteType === 'featureInstance' && selectedInstance) {
|
||||||
fetchInstanceRoles(selectedMandate.id, selectedInstance.id).then((r: FeatureInstanceRole[]) => {
|
fetchInstanceRoles(selectedMandate.id, selectedInstance.id).then((r: FeatureInstanceRole[]) => {
|
||||||
setRoles(r.map(role => ({ id: role.id, roleLabel: role.roleLabel })));
|
const mapped = r.map(role => ({ id: role.id, roleLabel: role.roleLabel }));
|
||||||
|
mapped.sort((a, b) => a.roleLabel.localeCompare(b.roleLabel, undefined, { sensitivity: 'base' }));
|
||||||
|
setRoles(mapped);
|
||||||
});
|
});
|
||||||
fetchInstanceUsers(selectedMandate.id, selectedInstance.id).then((users: { userId: string }[]) => {
|
fetchInstanceUsers(selectedMandate.id, selectedInstance.id).then((users: { userId: string }[]) => {
|
||||||
setExistingAccessUserIds(new Set(users.map(u => u.userId)));
|
setExistingAccessUserIds(new Set(users.map(u => u.userId)));
|
||||||
|
|
@ -160,7 +167,10 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
||||||
setRoles([]);
|
setRoles([]);
|
||||||
setExistingAccessUserIds(new Set());
|
setExistingAccessUserIds(new Set());
|
||||||
}
|
}
|
||||||
fetchAllUsers().then(setAllSystemUsers);
|
fetchAllUsers().then((data: any[]) => {
|
||||||
|
data.sort((a: any, b: any) => (a.username || '').localeCompare(b.username || '', undefined, { sensitivity: 'base' }));
|
||||||
|
setAllSystemUsers(data);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [step, selectedMandate, selectedInstance, inviteType, fetchRoles, fetchInstanceRoles, fetchAllUsers, fetchMandateUsers, fetchInstanceUsers]);
|
}, [step, selectedMandate, selectedInstance, inviteType, fetchRoles, fetchInstanceRoles, fetchAllUsers, fetchMandateUsers, fetchInstanceUsers]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
||||||
const loadMandates = useCallback(async () => {
|
const loadMandates = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await fetchMandatesList();
|
const data = await fetchMandatesList();
|
||||||
|
data.sort((a: Mandate, b: Mandate) => getMandateName(a).localeCompare(getMandateName(b), undefined, { sensitivity: 'base' }));
|
||||||
setMandates(data);
|
setMandates(data);
|
||||||
} catch {
|
} catch {
|
||||||
setError(t('Fehler beim Laden der Mandanten'));
|
setError(t('Fehler beim Laden der Mandanten'));
|
||||||
|
|
@ -162,13 +163,16 @@ export const AdminMandateWizardPage: React.FC = () => {
|
||||||
|
|
||||||
const loadAllSystemUsers = useCallback(async () => {
|
const loadAllSystemUsers = useCallback(async () => {
|
||||||
const data = await fetchAllUsers();
|
const data = await fetchAllUsers();
|
||||||
|
data.sort((a: any, b: any) => getUserDisplayName(a).localeCompare(getUserDisplayName(b), undefined, { sensitivity: 'base' }));
|
||||||
setAllSystemUsers(data);
|
setAllSystemUsers(data);
|
||||||
}, [fetchAllUsers]);
|
}, [fetchAllUsers]);
|
||||||
|
|
||||||
const loadMandateRoles = useCallback(async () => {
|
const loadMandateRoles = useCallback(async () => {
|
||||||
if (!selectedMandate) return;
|
if (!selectedMandate) return;
|
||||||
const data = await fetchMandateRolesList(selectedMandate.id);
|
const data = await fetchMandateRolesList(selectedMandate.id);
|
||||||
setMandateRoles(data.map((r: Role) => ({ id: r.id, roleLabel: r.roleLabel })));
|
const mapped = data.map((r: Role) => ({ id: r.id, roleLabel: r.roleLabel }));
|
||||||
|
mapped.sort((a: RoleOption, b: RoleOption) => a.roleLabel.localeCompare(b.roleLabel, undefined, { sensitivity: 'base' }));
|
||||||
|
setMandateRoles(mapped);
|
||||||
}, [selectedMandate, fetchMandateRolesList]);
|
}, [selectedMandate, fetchMandateRolesList]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -200,7 +204,9 @@ export const AdminMandateWizardPage: React.FC = () => {
|
||||||
const loadInstanceRoles = useCallback(async () => {
|
const loadInstanceRoles = useCallback(async () => {
|
||||||
if (!selectedInstance || !selectedMandate) return;
|
if (!selectedInstance || !selectedMandate) return;
|
||||||
const data = await fetchInstanceRolesList(selectedMandate.id, selectedInstance.id);
|
const data = await fetchInstanceRolesList(selectedMandate.id, selectedInstance.id);
|
||||||
setInstanceRoles(data.map((r: FeatureInstanceRole) => ({ id: r.id, roleLabel: r.roleLabel })));
|
const mapped = data.map((r: FeatureInstanceRole) => ({ id: r.id, roleLabel: r.roleLabel }));
|
||||||
|
mapped.sort((a: RoleOption, b: RoleOption) => a.roleLabel.localeCompare(b.roleLabel, undefined, { sensitivity: 'base' }));
|
||||||
|
setInstanceRoles(mapped);
|
||||||
}, [selectedInstance, selectedMandate, fetchInstanceRolesList]);
|
}, [selectedInstance, selectedMandate, fetchInstanceRolesList]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -95,17 +95,19 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
|
||||||
fetchInstanceRoles(mandateId, createdInstanceId),
|
fetchInstanceRoles(mandateId, createdInstanceId),
|
||||||
api.get(`/api/mandates/${mandateId}/users`),
|
api.get(`/api/mandates/${mandateId}/users`),
|
||||||
]);
|
]);
|
||||||
setInstanceRoles(Array.isArray(roleList) ? roleList : []);
|
const roles = Array.isArray(roleList) ? [...roleList] : [];
|
||||||
|
roles.sort((a: { roleLabel: string }, b: { roleLabel: string }) => a.roleLabel.localeCompare(b.roleLabel, undefined, { sensitivity: 'base' }));
|
||||||
|
setInstanceRoles(roles);
|
||||||
const data = usersRes.data?.items || usersRes.data || [];
|
const data = usersRes.data?.items || usersRes.data || [];
|
||||||
setMandateUsers(
|
const users = Array.isArray(data)
|
||||||
Array.isArray(data)
|
|
||||||
? data.map((u: { userId: string; username: string; email?: string }) => ({
|
? data.map((u: { userId: string; username: string; email?: string }) => ({
|
||||||
id: u.userId,
|
id: u.userId,
|
||||||
username: u.username,
|
username: u.username,
|
||||||
email: u.email,
|
email: u.email,
|
||||||
}))
|
}))
|
||||||
: []
|
: [];
|
||||||
);
|
users.sort((a: { username: string }, b: { username: string }) => a.username.localeCompare(b.username, undefined, { sensitivity: 'base' }));
|
||||||
|
setMandateUsers(users);
|
||||||
} catch {
|
} catch {
|
||||||
setInstanceRoles([]);
|
setInstanceRoles([]);
|
||||||
setMandateUsers([]);
|
setMandateUsers([]);
|
||||||
|
|
@ -183,7 +185,7 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
|
||||||
<p className={wizardStyles.fieldHint}>{t('Keine Mandanten verfügbar')}</p>
|
<p className={wizardStyles.fieldHint}>{t('Keine Mandanten verfügbar')}</p>
|
||||||
) : (
|
) : (
|
||||||
<div className={wizardStyles.cardGrid}>
|
<div className={wizardStyles.cardGrid}>
|
||||||
{mandates.map((m) => {
|
{[...mandates].sort((a, b) => getMandateName(a).localeCompare(getMandateName(b), undefined, { sensitivity: 'base' })).map((m) => {
|
||||||
const isActive = mandateId === m.id;
|
const isActive = mandateId === m.id;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|
@ -209,7 +211,7 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
|
||||||
<p className={wizardStyles.fieldHint}>{t('Keine Features verfügbar')}</p>
|
<p className={wizardStyles.fieldHint}>{t('Keine Features verfügbar')}</p>
|
||||||
) : (
|
) : (
|
||||||
<div className={wizardStyles.cardGrid}>
|
<div className={wizardStyles.cardGrid}>
|
||||||
{features.map((f) => {
|
{[...features].sort((a, b) => (a.label || a.code).localeCompare(b.label || b.code, undefined, { sensitivity: 'base' })).map((f) => {
|
||||||
const isActive = featureCode === f.code;
|
const isActive = featureCode === f.code;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* Follows the pattern established in AdminUsersPage/WorkflowsPage.
|
* Follows the pattern established in AdminUsersPage/WorkflowsPage.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useMemo, useEffect, useRef } from 'react';
|
import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useConnections, type Connection } from '../../hooks/useConnections';
|
import { useConnections, type Connection } from '../../hooks/useConnections';
|
||||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||||
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
||||||
|
|
@ -63,7 +63,12 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
const [wizardOpen, setWizardOpen] = useState(false);
|
const [wizardOpen, setWizardOpen] = useState(false);
|
||||||
|
|
||||||
const { request } = useApiRequest();
|
const { request } = useApiRequest();
|
||||||
// Banner shown while knowledge bootstrap is running in the background
|
const _lastTableParams = useRef<any>(undefined);
|
||||||
|
const _tableRefetch = useCallback(async (paginationParams?: any) => {
|
||||||
|
if (paginationParams) _lastTableParams.current = paginationParams;
|
||||||
|
else paginationParams = _lastTableParams.current;
|
||||||
|
return refetch(paginationParams);
|
||||||
|
}, [refetch]);
|
||||||
const [syncBanner, setSyncBanner] = useState<{
|
const [syncBanner, setSyncBanner] = useState<{
|
||||||
connector: string;
|
connector: string;
|
||||||
startedAt: number;
|
startedAt: number;
|
||||||
|
|
@ -80,9 +85,8 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
setSyncBanner(null);
|
setSyncBanner(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial fetch
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refetch();
|
_tableRefetch();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Generate columns from attributes - hide internal/redundant fields
|
// Generate columns from attributes - hide internal/redundant fields
|
||||||
|
|
@ -151,7 +155,7 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
|
|
||||||
await handleInlineUpdate(editingConnection.id, updateData, editingConnection);
|
await handleInlineUpdate(editingConnection.id, updateData, editingConnection);
|
||||||
setEditingConnection(null);
|
setEditingConnection(null);
|
||||||
refetch();
|
_tableRefetch();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating connection:', error);
|
console.error('Error updating connection:', error);
|
||||||
}
|
}
|
||||||
|
|
@ -162,7 +166,7 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
setDeletingConnections(prev => new Set(prev).add(connection.id));
|
setDeletingConnections(prev => new Set(prev).add(connection.id));
|
||||||
try {
|
try {
|
||||||
await deleteConnection(connection.id);
|
await deleteConnection(connection.id);
|
||||||
refetch();
|
_tableRefetch();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting connection:', error);
|
console.error('Error deleting connection:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -178,7 +182,7 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
const handleConnect = async (connection: Connection) => {
|
const handleConnect = async (connection: Connection) => {
|
||||||
try {
|
try {
|
||||||
await connectWithPopup(connection.id);
|
await connectWithPopup(connection.id);
|
||||||
refetch();
|
_tableRefetch();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error connecting:', error);
|
console.error('Error connecting:', error);
|
||||||
}
|
}
|
||||||
|
|
@ -193,7 +197,7 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
} else if (connection.authority === 'google') {
|
} else if (connection.authority === 'google') {
|
||||||
await refreshGoogleToken(connection.id);
|
await refreshGoogleToken(connection.id);
|
||||||
}
|
}
|
||||||
refetch();
|
_tableRefetch();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error refreshing token:', error);
|
console.error('Error refreshing token:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -211,7 +215,7 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
setReconnectingConnections(prev => new Set(prev).add(connection.id));
|
setReconnectingConnections(prev => new Set(prev).add(connection.id));
|
||||||
try {
|
try {
|
||||||
await connectWithPopup(connection.id, true);
|
await connectWithPopup(connection.id, true);
|
||||||
refetch();
|
_tableRefetch();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error reconnecting:', error);
|
console.error('Error reconnecting:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -230,7 +234,7 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await createConnectionAndAuth(type, knowledgeEnabled, knowledgePreferences ?? null);
|
await createConnectionAndAuth(type, knowledgeEnabled, knowledgePreferences ?? null);
|
||||||
refetch();
|
_tableRefetch();
|
||||||
if (knowledgeEnabled) {
|
if (knowledgeEnabled) {
|
||||||
const LABELS: Record<ConnectorType, string> = { google: 'Google', msft: 'Microsoft 365', clickup: 'ClickUp', infomaniak: 'Infomaniak' };
|
const LABELS: Record<ConnectorType, string> = { google: 'Google', msft: 'Microsoft 365', clickup: 'ClickUp', infomaniak: 'Infomaniak' };
|
||||||
showSyncBanner(LABELS[type] ?? type);
|
showSyncBanner(LABELS[type] ?? type);
|
||||||
|
|
@ -244,7 +248,7 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
try {
|
try {
|
||||||
const newConn = await createInfomaniakConnection();
|
const newConn = await createInfomaniakConnection();
|
||||||
await submitInfomaniakToken(newConn.id, token);
|
await submitInfomaniakToken(newConn.id, token);
|
||||||
refetch();
|
_tableRefetch();
|
||||||
if (knowledgeEnabled) showSyncBanner('Infomaniak');
|
if (knowledgeEnabled) showSyncBanner('Infomaniak');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating Infomaniak connection:', error);
|
console.error('Error creating Infomaniak connection:', error);
|
||||||
|
|
@ -266,7 +270,7 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
setTogglingConsent(prev => new Set(prev).add(connection.id));
|
setTogglingConsent(prev => new Set(prev).add(connection.id));
|
||||||
try {
|
try {
|
||||||
await patchKnowledgeConsent(request, connection.id, newEnabled);
|
await patchKnowledgeConsent(request, connection.id, newEnabled);
|
||||||
await refetch();
|
await _tableRefetch();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling knowledge consent:', error);
|
console.error('Error toggling knowledge consent:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -298,7 +302,7 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
<div className={styles.errorContainer}>
|
<div className={styles.errorContainer}>
|
||||||
<span className={styles.errorIcon}>⚠️</span>
|
<span className={styles.errorIcon}>⚠️</span>
|
||||||
<p className={styles.errorMessage}>{t('Fehler beim Laden der Verbindungen: {detail}', { detail: String(error) })}</p>
|
<p className={styles.errorMessage}>{t('Fehler beim Laden der Verbindungen: {detail}', { detail: String(error) })}</p>
|
||||||
<button className={styles.secondaryButton} onClick={() => refetch()}>
|
<button className={styles.secondaryButton} onClick={() => _tableRefetch()}>
|
||||||
<FaSync /> {t('Erneut versuchen')}
|
<FaSync /> {t('Erneut versuchen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -324,7 +328,7 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
<div className={styles.headerActions}>
|
<div className={styles.headerActions}>
|
||||||
<button
|
<button
|
||||||
className={styles.secondaryButton}
|
className={styles.secondaryButton}
|
||||||
onClick={() => refetch()}
|
onClick={() => _tableRefetch()}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||||
|
|
@ -444,7 +448,7 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
]}
|
]}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
hookData={{
|
hookData={{
|
||||||
refetch,
|
refetch: _tableRefetch,
|
||||||
permissions,
|
permissions,
|
||||||
pagination,
|
pagination,
|
||||||
groupLayout,
|
groupLayout,
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,10 @@ export const FilesPage: React.FC = () => {
|
||||||
const [treeVisible, setTreeVisible] = useState(true);
|
const [treeVisible, setTreeVisible] = useState(true);
|
||||||
const [tableVisible, setTableVisible] = useState(true);
|
const [tableVisible, setTableVisible] = useState(true);
|
||||||
|
|
||||||
|
const _lastTableParams = useRef<any>(undefined);
|
||||||
const _tableRefetch = useCallback(async (params?: any) => {
|
const _tableRefetch = useCallback(async (params?: any) => {
|
||||||
|
if (params) _lastTableParams.current = params;
|
||||||
|
else params = _lastTableParams.current;
|
||||||
const nextParams = { ...(params || {}) };
|
const nextParams = { ...(params || {}) };
|
||||||
const nextFilters = { ...(nextParams.filters || {}) };
|
const nextFilters = { ...(nextParams.filters || {}) };
|
||||||
const normalizedFolderId = normalizeFolderFilterId(selectedFolderId);
|
const normalizedFolderId = normalizeFolderFilterId(selectedFolderId);
|
||||||
|
|
@ -172,7 +175,7 @@ export const FilesPage: React.FC = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const _refreshAll = useCallback(async () => {
|
const _refreshAll = useCallback(async () => {
|
||||||
await _tableRefetch({ page: 1, pageSize: 25 });
|
await _tableRefetch();
|
||||||
setTreeKey(k => k + 1);
|
setTreeKey(k => k + 1);
|
||||||
}, [_tableRefetch]);
|
}, [_tableRefetch]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* Follows the pattern established in AdminUsersPage/WorkflowsPage.
|
* Follows the pattern established in AdminUsersPage/WorkflowsPage.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
||||||
import { usePrompts, usePromptOperations } from '../../hooks/usePrompts';
|
import { usePrompts, usePromptOperations } from '../../hooks/usePrompts';
|
||||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||||
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
||||||
|
|
@ -58,16 +58,15 @@ export const PromptsPage: React.FC = () => {
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
const [editingPrompt, setEditingPrompt] = useState<Prompt | null>(null);
|
const [editingPrompt, setEditingPrompt] = useState<Prompt | null>(null);
|
||||||
|
|
||||||
// ── Table refetch wrapper (stable signature used by FormGeneratorTable) ──
|
const _lastTableParams = useRef<any>(undefined);
|
||||||
const _tableRefetch = useCallback(async (params?: any) => {
|
const _tableRefetch = useCallback(async (params?: any) => {
|
||||||
|
if (params) _lastTableParams.current = params;
|
||||||
|
else params = _lastTableParams.current;
|
||||||
await refetch(params);
|
await refetch(params);
|
||||||
}, [refetch]);
|
}, [refetch]);
|
||||||
|
|
||||||
// ── Refresh-All for the header "Aktualisieren" button ────────────────────
|
|
||||||
// Forces a paginated request so the cache key matches what the table uses
|
|
||||||
// internally. This guarantees fresh (non-cached) data is pulled in.
|
|
||||||
const _refreshAll = useCallback(async () => {
|
const _refreshAll = useCallback(async () => {
|
||||||
await _tableRefetch({ page: 1, pageSize: 25 });
|
await _tableRefetch();
|
||||||
}, [_tableRefetch]);
|
}, [_tableRefetch]);
|
||||||
|
|
||||||
// Initial fetch
|
// Initial fetch
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Copyright (c) 2026 PowerOn AG
|
// Copyright (c) 2026 PowerOn AG
|
||||||
// All rights reserved.
|
// All rights reserved.
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||||
import { FormGeneratorTable, type ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable';
|
import { FormGeneratorTable, type ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable';
|
||||||
import { StackLayout } from '../../components/Layout/StackLayout';
|
import { StackLayout } from '../../components/Layout/StackLayout';
|
||||||
import { Panel } from '../../components/Layout/Panel';
|
import { Panel } from '../../components/Layout/Panel';
|
||||||
|
|
@ -32,6 +32,12 @@ const AdminSubscriptionsPage: React.FC = () => {
|
||||||
|
|
||||||
const { confirm, ConfirmDialog } = useConfirm();
|
const { confirm, ConfirmDialog } = useConfirm();
|
||||||
const { data: subscriptions, pagination, loading, refetch } = useAdminSubscriptions();
|
const { data: subscriptions, pagination, loading, refetch } = useAdminSubscriptions();
|
||||||
|
const _lastTableParams = useRef<any>(undefined);
|
||||||
|
const _tableRefetch = useCallback(async (paginationParams?: any) => {
|
||||||
|
if (paginationParams) _lastTableParams.current = paginationParams;
|
||||||
|
else paginationParams = _lastTableParams.current;
|
||||||
|
return refetch(paginationParams);
|
||||||
|
}, [refetch]);
|
||||||
|
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [dialogMode, setDialogMode] = useState<EnterpriseDialogMode>('create');
|
const [dialogMode, setDialogMode] = useState<EnterpriseDialogMode>('create');
|
||||||
|
|
@ -106,8 +112,8 @@ const AdminSubscriptionsPage: React.FC = () => {
|
||||||
} else if (mode === 'update') {
|
} else if (mode === 'update') {
|
||||||
await updateEnterprise(request, values as any);
|
await updateEnterprise(request, values as any);
|
||||||
}
|
}
|
||||||
await refetch();
|
await _tableRefetch();
|
||||||
}, [request, refetch]);
|
}, [request, _tableRefetch]);
|
||||||
|
|
||||||
const _rawColumns: ColumnConfig[] = useMemo(() => [
|
const _rawColumns: ColumnConfig[] = useMemo(() => [
|
||||||
{ key: 'mandateName', label: t('Mandant'), sortable: true, filterable: true, width: 180 },
|
{ key: 'mandateName', label: t('Mandant'), sortable: true, filterable: true, width: 180 },
|
||||||
|
|
@ -153,11 +159,11 @@ const AdminSubscriptionsPage: React.FC = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.post('/api/subscription/force-cancel', { subscriptionId: row.id });
|
await api.post('/api/subscription/force-cancel', { subscriptionId: row.id });
|
||||||
await refetch();
|
await _tableRefetch();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Force cancel failed:', err);
|
console.error('Force cancel failed:', err);
|
||||||
}
|
}
|
||||||
}, [confirm, refetch, t]);
|
}, [confirm, _tableRefetch, t]);
|
||||||
|
|
||||||
const _isEnterprise = (row: any) => row.isEnterprise || row.planKey === 'ENTERPRISE';
|
const _isEnterprise = (row: any) => row.isEnterprise || row.planKey === 'ENTERPRISE';
|
||||||
|
|
||||||
|
|
@ -215,7 +221,8 @@ const AdminSubscriptionsPage: React.FC = () => {
|
||||||
pagination={true}
|
pagination={true}
|
||||||
pageSize={50}
|
pageSize={50}
|
||||||
selectable={false}
|
selectable={false}
|
||||||
hookData={{ refetch, pagination }}
|
initialSort={[{ key: 'startedAt', direction: 'desc' }]}
|
||||||
|
hookData={{ refetch: _tableRefetch, pagination }}
|
||||||
customActions={customActions}
|
customActions={customActions}
|
||||||
emptyMessage={t('Keine Abonnements vorhanden')}
|
emptyMessage={t('Keine Abonnements vorhanden')}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -757,6 +757,7 @@ export const BillingDataView: React.FC = () => {
|
||||||
filterable={true}
|
filterable={true}
|
||||||
sortable={true}
|
sortable={true}
|
||||||
selectable={false}
|
selectable={false}
|
||||||
|
initialSort={[{ key: 'sysCreatedAt', direction: 'desc' }]}
|
||||||
emptyMessage={t('Keine Transaktionen vorhanden')}
|
emptyMessage={t('Keine Transaktionen vorhanden')}
|
||||||
hookData={transactionsHookData}
|
hookData={transactionsHookData}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
* 2. Gespraechspartner – Persona CRUD with FormGeneratorTable (apiEndpoint-driven)
|
* 2. Gespraechspartner – Persona CRUD with FormGeneratorTable (apiEndpoint-driven)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||||
import { Link, useSearchParams } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
import { useApiRequest } from '../../../hooks/useApi';
|
import { useApiRequest } from '../../../hooks/useApi';
|
||||||
import { useInstanceId } from '../../../hooks/useCurrentInstance';
|
import { useInstanceId } from '../../../hooks/useCurrentInstance';
|
||||||
|
|
@ -102,9 +102,12 @@ export const CommcoachSettingsView: React.FC = () => {
|
||||||
const [personaError, setPersonaError] = useState<string | null>(null);
|
const [personaError, setPersonaError] = useState<string | null>(null);
|
||||||
|
|
||||||
const personaApiEndpoint = instanceId ? `/api/commcoach/${instanceId}/personas` : undefined;
|
const personaApiEndpoint = instanceId ? `/api/commcoach/${instanceId}/personas` : undefined;
|
||||||
|
const _lastPersonaParams = useRef<any>(undefined);
|
||||||
|
|
||||||
const _refetchPersonas = useCallback(async (params?: any) => {
|
const _refetchPersonas = useCallback(async (params?: any) => {
|
||||||
if (!instanceId) return;
|
if (!instanceId) return;
|
||||||
|
if (params) _lastPersonaParams.current = params;
|
||||||
|
else params = _lastPersonaParams.current || { page: 1, pageSize: 50 };
|
||||||
setPersonasLoading(true);
|
setPersonasLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await fetchPersonasPaginated(request, instanceId, params);
|
const data = await fetchPersonasPaginated(request, instanceId, params);
|
||||||
|
|
@ -125,7 +128,7 @@ export const CommcoachSettingsView: React.FC = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab === 'personas' && instanceId) {
|
if (activeTab === 'personas' && instanceId) {
|
||||||
_refetchPersonas({ page: 1, pageSize: 50 });
|
_refetchPersonas();
|
||||||
}
|
}
|
||||||
}, [activeTab, instanceId, _refetchPersonas]);
|
}, [activeTab, instanceId, _refetchPersonas]);
|
||||||
|
|
||||||
|
|
@ -159,7 +162,7 @@ export const CommcoachSettingsView: React.FC = () => {
|
||||||
});
|
});
|
||||||
setPersonaForm({ label: '', description: '', gender: '' });
|
setPersonaForm({ label: '', description: '', gender: '' });
|
||||||
setShowCreatePersona(false);
|
setShowCreatePersona(false);
|
||||||
await _refetchPersonas({ page: 1, pageSize: 50 });
|
await _refetchPersonas();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setPersonaError(err.message || 'Fehler beim Erstellen');
|
setPersonaError(err.message || 'Fehler beim Erstellen');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -179,7 +182,7 @@ export const CommcoachSettingsView: React.FC = () => {
|
||||||
});
|
});
|
||||||
setEditingPersona(null);
|
setEditingPersona(null);
|
||||||
setPersonaForm({ label: '', description: '', gender: '' });
|
setPersonaForm({ label: '', description: '', gender: '' });
|
||||||
await _refetchPersonas({ page: 1, pageSize: 50 });
|
await _refetchPersonas();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setPersonaError(err.message || 'Fehler beim Speichern');
|
setPersonaError(err.message || 'Fehler beim Speichern');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
* Embedded — NO own StackLayout. Regions: Panel `toolbar` + Panel `table`.
|
* Embedded — NO own StackLayout. Regions: Panel `toolbar` + Panel `table`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useTrusteeDocuments, useTrusteeDocumentOperations, TrusteeDocument } from '../../../hooks/useTrustee';
|
import { useTrusteeDocuments, useTrusteeDocumentOperations, TrusteeDocument } from '../../../hooks/useTrustee';
|
||||||
import { useInstanceId } from '../../../hooks/useCurrentInstance';
|
import { useInstanceId } from '../../../hooks/useCurrentInstance';
|
||||||
import { FormGeneratorTable } from '../../../components/FormGenerator/FormGeneratorTable';
|
import { FormGeneratorTable } from '../../../components/FormGenerator/FormGeneratorTable';
|
||||||
|
|
@ -58,12 +58,20 @@ export const TrusteeDocumentsView: React.FC = () => {
|
||||||
const [editingDocument, setEditingDocument] = useState<TrusteeDocument | null>(null);
|
const [editingDocument, setEditingDocument] = useState<TrusteeDocument | null>(null);
|
||||||
const [isCreateMode, setIsCreateMode] = useState(false);
|
const [isCreateMode, setIsCreateMode] = useState(false);
|
||||||
const [downloadingId, setDownloadingId] = useState<string | null>(null);
|
const [downloadingId, setDownloadingId] = useState<string | null>(null);
|
||||||
|
const _lastTableParams = useRef<any>(undefined);
|
||||||
|
|
||||||
|
const _tableRefetch = useCallback(async (paginationParams?: any) => {
|
||||||
|
if (paginationParams) _lastTableParams.current = paginationParams;
|
||||||
|
else paginationParams = _lastTableParams.current;
|
||||||
|
return refetch(paginationParams);
|
||||||
|
}, [refetch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (instanceId) {
|
if (instanceId) {
|
||||||
refetch();
|
_lastTableParams.current = undefined;
|
||||||
|
_tableRefetch();
|
||||||
}
|
}
|
||||||
}, [instanceId, refetch]);
|
}, [instanceId, _tableRefetch]);
|
||||||
|
|
||||||
const documentColumnOrder = ['sysCreatedAt'];
|
const documentColumnOrder = ['sysCreatedAt'];
|
||||||
|
|
||||||
|
|
@ -113,13 +121,13 @@ export const TrusteeDocumentsView: React.FC = () => {
|
||||||
const result = await handleCreate(data);
|
const result = await handleCreate(data);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setIsCreateMode(false);
|
setIsCreateMode(false);
|
||||||
refetch();
|
_tableRefetch();
|
||||||
}
|
}
|
||||||
} else if (editingDocument) {
|
} else if (editingDocument) {
|
||||||
const result = await handleUpdate(editingDocument.id, data);
|
const result = await handleUpdate(editingDocument.id, data);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setEditingDocument(null);
|
setEditingDocument(null);
|
||||||
refetch();
|
_tableRefetch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -128,7 +136,7 @@ export const TrusteeDocumentsView: React.FC = () => {
|
||||||
removeOptimistically(doc.id);
|
removeOptimistically(doc.id);
|
||||||
const success = await handleDelete(doc.id);
|
const success = await handleDelete(doc.id);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
refetch();
|
_tableRefetch();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -173,7 +181,7 @@ export const TrusteeDocumentsView: React.FC = () => {
|
||||||
updateOptimistically(itemId, updateData);
|
updateOptimistically(itemId, updateData);
|
||||||
const result = await handleUpdate(itemId, { ...row, ...updateData });
|
const result = await handleUpdate(itemId, { ...row, ...updateData });
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
refetch();
|
_tableRefetch();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -189,7 +197,7 @@ export const TrusteeDocumentsView: React.FC = () => {
|
||||||
<p className={styles.errorMessage}>
|
<p className={styles.errorMessage}>
|
||||||
{t('Fehler beim Laden der Dokumente: {detail}', { detail: String(error) })}
|
{t('Fehler beim Laden der Dokumente: {detail}', { detail: String(error) })}
|
||||||
</p>
|
</p>
|
||||||
<button type="button" className={styles.secondaryButton} onClick={() => refetch()}>
|
<button type="button" className={styles.secondaryButton} onClick={() => _tableRefetch()}>
|
||||||
<FaSync /> {t('Erneut versuchen')}
|
<FaSync /> {t('Erneut versuchen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -204,7 +212,7 @@ export const TrusteeDocumentsView: React.FC = () => {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.secondaryButton}
|
className={styles.secondaryButton}
|
||||||
onClick={() => refetch()}
|
onClick={() => _tableRefetch()}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||||
|
|
@ -258,7 +266,7 @@ export const TrusteeDocumentsView: React.FC = () => {
|
||||||
]}
|
]}
|
||||||
onDelete={handleDeleteDoc}
|
onDelete={handleDeleteDoc}
|
||||||
hookData={{
|
hookData={{
|
||||||
refetch,
|
refetch: _tableRefetch,
|
||||||
permissions,
|
permissions,
|
||||||
pagination,
|
pagination,
|
||||||
handleDelete,
|
handleDelete,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
* Embedded — NO own StackLayout. Regions: Panel `toolbar` + Panel `table`.
|
* Embedded — NO own StackLayout. Regions: Panel `toolbar` + Panel `table`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useTrusteePositions, useTrusteePositionOperations, TrusteePosition } from '../../../hooks/useTrustee';
|
import { useTrusteePositions, useTrusteePositionOperations, TrusteePosition } from '../../../hooks/useTrustee';
|
||||||
import { useInstanceId } from '../../../hooks/useCurrentInstance';
|
import { useInstanceId } from '../../../hooks/useCurrentInstance';
|
||||||
import { useApiRequest } from '../../../hooks/useApi';
|
import { useApiRequest } from '../../../hooks/useApi';
|
||||||
|
|
@ -63,12 +63,20 @@ export const TrusteePositionsView: React.FC = () => {
|
||||||
|
|
||||||
const [editingPosition, setEditingPosition] = useState<TrusteePosition | null>(null);
|
const [editingPosition, setEditingPosition] = useState<TrusteePosition | null>(null);
|
||||||
const [isCreateMode, setIsCreateMode] = useState(false);
|
const [isCreateMode, setIsCreateMode] = useState(false);
|
||||||
|
const _lastTableParams = useRef<any>(undefined);
|
||||||
|
|
||||||
|
const _tableRefetch = useCallback(async (paginationParams?: any) => {
|
||||||
|
if (paginationParams) _lastTableParams.current = paginationParams;
|
||||||
|
else paginationParams = _lastTableParams.current;
|
||||||
|
return refetch(paginationParams);
|
||||||
|
}, [refetch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (instanceId) {
|
if (instanceId) {
|
||||||
refetch();
|
_lastTableParams.current = undefined;
|
||||||
|
_tableRefetch();
|
||||||
}
|
}
|
||||||
}, [instanceId, refetch]);
|
}, [instanceId, _tableRefetch]);
|
||||||
|
|
||||||
const handleBatchSyncToAccounting = useCallback(
|
const handleBatchSyncToAccounting = useCallback(
|
||||||
async (rows: TrusteePosition[]) => {
|
async (rows: TrusteePosition[]) => {
|
||||||
|
|
@ -91,7 +99,7 @@ export const TrusteePositionsView: React.FC = () => {
|
||||||
firstError?.errorMessage || t('{count} Fehler.', { count: res.errors }),
|
firstError?.errorMessage || t('{count} Fehler.', { count: res.errors }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
refetch();
|
_tableRefetch();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showError(
|
showError(
|
||||||
t('Sync fehlgeschlagen'),
|
t('Sync fehlgeschlagen'),
|
||||||
|
|
@ -105,7 +113,7 @@ export const TrusteePositionsView: React.FC = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[instanceId, request, refetch, showSuccess, showError, t],
|
[instanceId, request, _tableRefetch, showSuccess, showError, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSingleSyncToAccounting = useCallback(
|
const handleSingleSyncToAccounting = useCallback(
|
||||||
|
|
@ -290,13 +298,13 @@ export const TrusteePositionsView: React.FC = () => {
|
||||||
const result = await handleCreate(processedData);
|
const result = await handleCreate(processedData);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setIsCreateMode(false);
|
setIsCreateMode(false);
|
||||||
refetch();
|
_tableRefetch();
|
||||||
}
|
}
|
||||||
} else if (editingPosition) {
|
} else if (editingPosition) {
|
||||||
const result = await handleUpdate(editingPosition.id, processedData);
|
const result = await handleUpdate(editingPosition.id, processedData);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setEditingPosition(null);
|
setEditingPosition(null);
|
||||||
refetch();
|
_tableRefetch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -305,7 +313,7 @@ export const TrusteePositionsView: React.FC = () => {
|
||||||
removeOptimistically(pos.id);
|
removeOptimistically(pos.id);
|
||||||
const success = await handleDelete(pos.id);
|
const success = await handleDelete(pos.id);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
refetch();
|
_tableRefetch();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -323,7 +331,7 @@ export const TrusteePositionsView: React.FC = () => {
|
||||||
updateOptimistically(itemId, updateData);
|
updateOptimistically(itemId, updateData);
|
||||||
const result = await handleUpdate(itemId, { ...row, ...updateData });
|
const result = await handleUpdate(itemId, { ...row, ...updateData });
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
refetch();
|
_tableRefetch();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -339,7 +347,7 @@ export const TrusteePositionsView: React.FC = () => {
|
||||||
<p className={styles.errorMessage}>
|
<p className={styles.errorMessage}>
|
||||||
{t('Fehler beim Laden der Positionen: {detail}', { detail: String(error) })}
|
{t('Fehler beim Laden der Positionen: {detail}', { detail: String(error) })}
|
||||||
</p>
|
</p>
|
||||||
<button type="button" className={styles.secondaryButton} onClick={() => refetch()}>
|
<button type="button" className={styles.secondaryButton} onClick={() => _tableRefetch()}>
|
||||||
<FaSync /> {t('Erneut versuchen')}
|
<FaSync /> {t('Erneut versuchen')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -354,7 +362,7 @@ export const TrusteePositionsView: React.FC = () => {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.secondaryButton}
|
className={styles.secondaryButton}
|
||||||
onClick={() => refetch()}
|
onClick={() => _tableRefetch()}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||||
|
|
@ -416,7 +424,7 @@ export const TrusteePositionsView: React.FC = () => {
|
||||||
]}
|
]}
|
||||||
onDelete={handleDeletePos}
|
onDelete={handleDeletePos}
|
||||||
hookData={{
|
hookData={{
|
||||||
refetch,
|
refetch: _tableRefetch,
|
||||||
permissions,
|
permissions,
|
||||||
pagination,
|
pagination,
|
||||||
handleDelete,
|
handleDelete,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
* Embedded in WorkflowAutomationHubPage via TemplatesTab (embedded=true).
|
* Embedded in WorkflowAutomationHubPage via TemplatesTab (embedded=true).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { FaCopy, FaSync, FaShareAlt, FaPen } from 'react-icons/fa';
|
import { FaCopy, FaSync, FaShareAlt, FaPen } from 'react-icons/fa';
|
||||||
import { usePrompt } from '../../../hooks/usePrompt';
|
import { usePrompt } from '../../../hooks/usePrompt';
|
||||||
|
|
@ -89,6 +89,7 @@ export const WorkflowTemplatesPage: React.FC<WorkflowTemplatesPageProps> = ({
|
||||||
|
|
||||||
const [paginationMeta, setPaginationMeta] = useState<any>(null);
|
const [paginationMeta, setPaginationMeta] = useState<any>(null);
|
||||||
const [backendAttributes, setBackendAttributes] = useState<AttributeDefinition[]>([]);
|
const [backendAttributes, setBackendAttributes] = useState<AttributeDefinition[]>([]);
|
||||||
|
const _lastTableParams = useRef<any>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAttributes(request, 'AutoWorkflowView')
|
fetchAttributes(request, 'AutoWorkflowView')
|
||||||
|
|
@ -100,6 +101,8 @@ export const WorkflowTemplatesPage: React.FC<WorkflowTemplatesPageProps> = ({
|
||||||
|
|
||||||
const load = useCallback(async (paginationParams?: any) => {
|
const load = useCallback(async (paginationParams?: any) => {
|
||||||
if (!instanceId) return;
|
if (!instanceId) return;
|
||||||
|
if (paginationParams) _lastTableParams.current = paginationParams;
|
||||||
|
else paginationParams = _lastTableParams.current;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const scope = activeScope === 'all' ? undefined : activeScope;
|
const scope = activeScope === 'all' ? undefined : activeScope;
|
||||||
|
|
|
||||||
|
|
@ -278,9 +278,8 @@ export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputPro
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (_sttPrefsLoaded.current) return;
|
if (_sttPrefsLoaded.current) return;
|
||||||
_sttPrefsLoaded.current = true;
|
_sttPrefsLoaded.current = true;
|
||||||
fetch('/api/voice/preferences', { credentials: 'include' })
|
api.get('/api/voice/preferences')
|
||||||
.then(r => r.ok ? r.json() : null)
|
.then(res => { if (res.data?.sttLanguage) setVoiceLanguage(res.data.sttLanguage); })
|
||||||
.then(data => { if (data?.sttLanguage) setVoiceLanguage(data.sttLanguage); })
|
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -872,7 +871,7 @@ export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputPro
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setVoiceLanguage(lang.bcp47);
|
setVoiceLanguage(lang.bcp47);
|
||||||
setShowLangPicker(false);
|
setShowLangPicker(false);
|
||||||
fetch('/api/voice/preferences', { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sttLanguage: lang.bcp47 }) }).catch(() => {});
|
api.put('/api/voice/preferences', { sttLanguage: lang.bcp47 }).catch(() => {});
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
padding: '8px 12px', cursor: 'pointer', fontSize: 13,
|
padding: '8px 12px', cursor: 'pointer', fontSize: 13,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue