feat:finished admin pages
This commit is contained in:
parent
eaf69f41e9
commit
64d14af8d5
19 changed files with 3399 additions and 2461 deletions
144
.cursor/plans/implement_rbac_roles_page_8dd9fac6.plan.md
Normal file
144
.cursor/plans/implement_rbac_roles_page_8dd9fac6.plan.md
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
# Implement RBAC Roles Page
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Implement the RBAC roles admin page following the exact pattern used in `mandates.ts`. This includes creating the API file, custom hook for state management, updating the page configuration with CreateButton header button, and adding translations in all three languages (German, English, French).
|
||||||
|
|
||||||
|
## Files to Create/Modify
|
||||||
|
|
||||||
|
### 1. Create API File: `frontend_nyla/src/api/roleApi.ts`
|
||||||
|
- Follow the pattern from `mandateApi.ts`
|
||||||
|
- Implement all required endpoints:
|
||||||
|
- `fetchRoles()` - GET /api/rbac/roles (with pagination support)
|
||||||
|
- `fetchRoleById()` - GET /api/rbac/roles/{roleId}
|
||||||
|
- `fetchRoleOptions()` - GET /api/rbac/roles/options
|
||||||
|
- `createRole()` - POST /api/rbac/roles
|
||||||
|
- `updateRole()` - PUT /api/rbac/roles/{roleId}
|
||||||
|
- `deleteRole()` - DELETE /api/rbac/roles/{roleId}
|
||||||
|
- Include TypeScript types: `Role`, `RoleUpdateData`, `PaginationParams`, `PaginatedResponse`
|
||||||
|
|
||||||
|
### 2. Create Hook: `frontend_nyla/src/hooks/useAdminRbacRoles.ts`
|
||||||
|
- Follow the exact pattern from `useAdminMandates.ts`
|
||||||
|
- Create two hooks:
|
||||||
|
- `useRbacRoles()` - Main hook for data fetching and state management
|
||||||
|
- Fetch roles with pagination support
|
||||||
|
- Fetch attributes from `/api/attributes/Role` using `fetchAttributes(request, 'Role')`
|
||||||
|
- Fetch permissions using `checkPermission('DATA', 'Role')`
|
||||||
|
- Implement `generateEditFieldsFromAttributes()` using `attributeTypeMapper` utilities
|
||||||
|
- Implement `generateCreateFieldsFromAttributes()` using `attributeTypeMapper` utilities
|
||||||
|
- Implement `ensureAttributesLoaded()` for EditActionButton
|
||||||
|
- Implement optimistic updates (`removeOptimistically`, `updateOptimistically`)
|
||||||
|
- Return pagination info, attributes, permissions, and all required functions
|
||||||
|
- `useRbacRoleOperations()` - Operations hook for CRUD
|
||||||
|
- `handleRoleDelete()` - Delete with loading state tracking
|
||||||
|
- `handleRoleCreate()` - Create with error handling
|
||||||
|
- `handleRoleUpdate()` - Update with error handling
|
||||||
|
- Track loading states in Sets (deletingRoles, editingRoles, creatingRole)
|
||||||
|
- Return error states (deleteError, createError, updateError)
|
||||||
|
|
||||||
|
### 3. Update Page Configuration: `frontend_nyla/src/core/PageManager/data/pages/admin/rbac-role.ts`
|
||||||
|
- Follow the exact structure from `mandates.ts`
|
||||||
|
- Import `FaPlus` from `react-icons/fa` for the create button icon
|
||||||
|
- Create `createRbacRolesHook()` factory function that:
|
||||||
|
- Uses `useRbacRoles()` and `useRbacRoleOperations()`
|
||||||
|
- Converts attributes to columns using `attributesToColumns()` helper
|
||||||
|
- Implements `handleDeleteSingle` and `handleDeleteMultiple` callbacks
|
||||||
|
- Returns all required data for FormGeneratorTable
|
||||||
|
- Update `rbacRolePageData`:
|
||||||
|
- Add header button with `FaPlus` icon for creating roles (following mandates.ts pattern):
|
||||||
|
```typescript
|
||||||
|
headerButtons: [
|
||||||
|
{
|
||||||
|
id: 'add-role',
|
||||||
|
label: 'admin.rbac-role.new_button',
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'md',
|
||||||
|
icon: FaPlus,
|
||||||
|
formConfig: {
|
||||||
|
fields: [], // Empty array - fields will be generated dynamically from attributes
|
||||||
|
popupTitle: 'admin.rbac-role.modal.create.title',
|
||||||
|
popupSize: 'medium',
|
||||||
|
createOperationName: 'handleRoleCreate',
|
||||||
|
successMessage: 'admin.rbac-role.create.success',
|
||||||
|
errorMessage: 'admin.rbac-role.create.error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
- Add table content section with:
|
||||||
|
- `hookFactory: createRbacRolesHook`
|
||||||
|
- Action buttons: edit and delete (following mandates pattern)
|
||||||
|
- Configure edit button with `fetchItemFunctionName: 'fetchRoleById'`
|
||||||
|
- Configure delete button with proper operation names
|
||||||
|
- Add permission-based disabled logic
|
||||||
|
- Keep existing privilege checker (sysadmin only)
|
||||||
|
|
||||||
|
### 4. Update Translations: All three locale files
|
||||||
|
- **German (`frontend_nyla/src/locales/de.ts`)**: Add missing translations after line 756:
|
||||||
|
- `'admin.rbac-role.new_button': 'Rolle hinzufügen'`
|
||||||
|
- `'admin.rbac-role.action.edit': 'Bearbeiten'`
|
||||||
|
- `'admin.rbac-role.action.delete': 'Löschen'`
|
||||||
|
- `'admin.rbac-role.modal.create.title': 'Neue Rolle erstellen'`
|
||||||
|
- `'admin.rbac-role.create.success': 'Rolle erfolgreich erstellt'`
|
||||||
|
- `'admin.rbac-role.create.error': 'Fehler beim Erstellen der Rolle'`
|
||||||
|
|
||||||
|
- **English (`frontend_nyla/src/locales/en.ts`)**: Add missing translations after line 756:
|
||||||
|
- `'admin.rbac-role.new_button': 'Add Role'`
|
||||||
|
- `'admin.rbac-role.action.edit': 'Edit'`
|
||||||
|
- `'admin.rbac-role.action.delete': 'Delete'`
|
||||||
|
- `'admin.rbac-role.modal.create.title': 'Create New Role'`
|
||||||
|
- `'admin.rbac-role.create.success': 'Role created successfully'`
|
||||||
|
- `'admin.rbac-role.create.error': 'Error creating role'`
|
||||||
|
|
||||||
|
- **French (`frontend_nyla/src/locales/fr.ts`)**: Add missing translations after line 756:
|
||||||
|
- `'admin.rbac-role.new_button': 'Ajouter un rôle'`
|
||||||
|
- `'admin.rbac-role.action.edit': 'Modifier'`
|
||||||
|
- `'admin.rbac-role.action.delete': 'Supprimer'`
|
||||||
|
- `'admin.rbac-role.modal.create.title': 'Créer un nouveau rôle'`
|
||||||
|
- `'admin.rbac-role.create.success': 'Rôle créé avec succès'`
|
||||||
|
- `'admin.rbac-role.create.error': 'Erreur lors de la création du rôle'`
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### API File Structure
|
||||||
|
- Use `ApiRequestFunction` type from `useApi`
|
||||||
|
- Support pagination parameters (page, pageSize, sort, filters, search)
|
||||||
|
- Handle both paginated and non-paginated responses
|
||||||
|
- Use `/api/rbac/roles` as base URL
|
||||||
|
- Use `/api/attributes/Role` for attributes endpoint
|
||||||
|
|
||||||
|
### Hook Pattern
|
||||||
|
- Use `useApiRequest` hook for API calls
|
||||||
|
- Use `usePermissions` hook for permission checking
|
||||||
|
- Use `getUserDataCache()` to check authentication before fetching
|
||||||
|
- Implement attribute type mapping using utilities from `attributeTypeMapper.ts`:
|
||||||
|
- `isCheckboxType()`, `isSelectType()`, `isMultiselectType()`, `isDateTimeType()`, `isTextareaType()`
|
||||||
|
- Filter out non-editable fields (id, readonly fields, etc.)
|
||||||
|
- Handle options arrays and option references
|
||||||
|
|
||||||
|
### Page Configuration Pattern
|
||||||
|
- Use `attributesToColumns()` helper to convert attributes to column config
|
||||||
|
- Disable filtering for date/timestamp fields using `isDateTimeType()`
|
||||||
|
- Configure action buttons with proper field mappings and operation names
|
||||||
|
- Use permission-based disabled logic for buttons
|
||||||
|
- Set `entityType: 'Role'` for EditActionButton
|
||||||
|
- Add header button using CreateButton component pattern (via formConfig in headerButtons)
|
||||||
|
|
||||||
|
## Key Dependencies
|
||||||
|
- `useApiRequest` from `hooks/useApi`
|
||||||
|
- `usePermissions` from `hooks/usePermissions`
|
||||||
|
- `fetchAttributes` from `api/attributesApi`
|
||||||
|
- `attributeTypeMapper` utilities from `utils/attributeTypeMapper`
|
||||||
|
- `FormGeneratorTable` component
|
||||||
|
- `EditActionButton` and `DeleteActionButton` components
|
||||||
|
- `CreateButton` component (rendered via PageRenderer from headerButtons formConfig)
|
||||||
|
- `FaPlus` icon from `react-icons/fa`
|
||||||
|
|
||||||
|
## Testing Considerations
|
||||||
|
- Verify all API endpoints are called correctly
|
||||||
|
- Ensure attributes are fetched from `/api/attributes/Role`
|
||||||
|
- Verify permission checks work correctly
|
||||||
|
- Test create, edit, delete operations
|
||||||
|
- Verify optimistic updates work
|
||||||
|
- Check that date/timestamp fields are not filterable
|
||||||
|
- Verify CreateButton appears in header and opens create modal
|
||||||
|
- Verify translations work in all three languages
|
||||||
136
src/api/mandateApi.ts
Normal file
136
src/api/mandateApi.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TYPES & INTERFACES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface Mandate {
|
||||||
|
id: string;
|
||||||
|
[key: string]: any; // Allow additional properties from backend
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MandateUpdateData = Partial<Omit<Mandate, 'id'>>;
|
||||||
|
|
||||||
|
export interface PaginationParams {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
|
||||||
|
filters?: Record<string, any>;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
items: T[];
|
||||||
|
pagination?: {
|
||||||
|
currentPage: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type for the request function passed to API functions
|
||||||
|
export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<any>;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// API REQUEST FUNCTIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch list of mandates with optional pagination
|
||||||
|
* Endpoint: GET /api/mandates/
|
||||||
|
*/
|
||||||
|
export async function fetchMandates(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
params?: PaginationParams
|
||||||
|
): Promise<PaginatedResponse<Mandate> | Mandate[]> {
|
||||||
|
const requestParams: any = {};
|
||||||
|
|
||||||
|
// Build pagination object if provided
|
||||||
|
if (params) {
|
||||||
|
const paginationObj: any = {};
|
||||||
|
|
||||||
|
if (params.page !== undefined) paginationObj.page = params.page;
|
||||||
|
if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
|
||||||
|
if (params.sort) paginationObj.sort = params.sort;
|
||||||
|
if (params.filters) paginationObj.filters = params.filters;
|
||||||
|
if (params.search) paginationObj.search = params.search;
|
||||||
|
|
||||||
|
if (Object.keys(paginationObj).length > 0) {
|
||||||
|
requestParams.pagination = JSON.stringify(paginationObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await request({
|
||||||
|
url: '/api/mandates/',
|
||||||
|
method: 'get',
|
||||||
|
params: requestParams
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single mandate by ID
|
||||||
|
* Endpoint: GET /api/mandates/{mandateId}
|
||||||
|
*/
|
||||||
|
export async function fetchMandateById(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
mandateId: string
|
||||||
|
): Promise<Mandate | null> {
|
||||||
|
try {
|
||||||
|
const data = await request({
|
||||||
|
url: `/api/mandates/${mandateId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
return data || null;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching mandate by ID:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a mandate
|
||||||
|
* Endpoint: PUT /api/mandates/{mandateId}
|
||||||
|
*/
|
||||||
|
export async function updateMandate(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
mandateId: string,
|
||||||
|
updateData: MandateUpdateData
|
||||||
|
): Promise<Mandate> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/mandates/${mandateId}`,
|
||||||
|
method: 'put',
|
||||||
|
data: updateData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new mandate
|
||||||
|
* Endpoint: POST /api/mandates/
|
||||||
|
*/
|
||||||
|
export async function createMandate(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
mandateData: Partial<Mandate>
|
||||||
|
): Promise<Mandate> {
|
||||||
|
return await request({
|
||||||
|
url: '/api/mandates/',
|
||||||
|
method: 'post',
|
||||||
|
data: mandateData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a mandate
|
||||||
|
* Endpoint: DELETE /api/mandates/{mandateId}
|
||||||
|
*/
|
||||||
|
export async function deleteMandate(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
mandateId: string
|
||||||
|
): Promise<void> {
|
||||||
|
await request({
|
||||||
|
url: `/api/mandates/${mandateId}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
136
src/api/rbacRulesApi.ts
Normal file
136
src/api/rbacRulesApi.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TYPES & INTERFACES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface RbacRule {
|
||||||
|
id: string;
|
||||||
|
[key: string]: any; // Allow additional properties from backend
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RbacRuleUpdateData = Partial<Omit<RbacRule, 'id'>>;
|
||||||
|
|
||||||
|
export interface PaginationParams {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
|
||||||
|
filters?: Record<string, any>;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
items: T[];
|
||||||
|
pagination?: {
|
||||||
|
currentPage: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type for the request function passed to API functions
|
||||||
|
export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<any>;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// API REQUEST FUNCTIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch list of RBAC rules with optional pagination
|
||||||
|
* Endpoint: GET /api/rbac/rules
|
||||||
|
*/
|
||||||
|
export async function fetchRbacRules(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
params?: PaginationParams
|
||||||
|
): Promise<PaginatedResponse<RbacRule> | RbacRule[]> {
|
||||||
|
const requestParams: any = {};
|
||||||
|
|
||||||
|
// Build pagination object if provided
|
||||||
|
if (params) {
|
||||||
|
const paginationObj: any = {};
|
||||||
|
|
||||||
|
if (params.page !== undefined) paginationObj.page = params.page;
|
||||||
|
if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
|
||||||
|
if (params.sort) paginationObj.sort = params.sort;
|
||||||
|
if (params.filters) paginationObj.filters = params.filters;
|
||||||
|
if (params.search) paginationObj.search = params.search;
|
||||||
|
|
||||||
|
if (Object.keys(paginationObj).length > 0) {
|
||||||
|
requestParams.pagination = JSON.stringify(paginationObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await request({
|
||||||
|
url: '/api/rbac/rules',
|
||||||
|
method: 'get',
|
||||||
|
params: requestParams
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single RBAC rule by ID
|
||||||
|
* Endpoint: GET /api/rbac/rules/{ruleId}
|
||||||
|
*/
|
||||||
|
export async function fetchRbacRuleById(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
ruleId: string
|
||||||
|
): Promise<RbacRule | null> {
|
||||||
|
try {
|
||||||
|
const data = await request({
|
||||||
|
url: `/api/rbac/rules/${ruleId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
return data || null;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching RBAC rule by ID:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a RBAC rule
|
||||||
|
* Endpoint: PUT /api/rbac/rules/{ruleId}
|
||||||
|
*/
|
||||||
|
export async function updateRbacRule(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
ruleId: string,
|
||||||
|
updateData: RbacRuleUpdateData
|
||||||
|
): Promise<RbacRule> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/rbac/rules/${ruleId}`,
|
||||||
|
method: 'put',
|
||||||
|
data: updateData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new RBAC rule
|
||||||
|
* Endpoint: POST /api/rbac/rules
|
||||||
|
*/
|
||||||
|
export async function createRbacRule(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
ruleData: Partial<RbacRule>
|
||||||
|
): Promise<RbacRule> {
|
||||||
|
return await request({
|
||||||
|
url: '/api/rbac/rules',
|
||||||
|
method: 'post',
|
||||||
|
data: ruleData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a RBAC rule
|
||||||
|
* Endpoint: DELETE /api/rbac/rules/{ruleId}
|
||||||
|
*/
|
||||||
|
export async function deleteRbacRule(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
ruleId: string
|
||||||
|
): Promise<void> {
|
||||||
|
await request({
|
||||||
|
url: `/api/rbac/rules/${ruleId}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
201
src/api/roleApi.ts
Normal file
201
src/api/roleApi.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TYPES & INTERFACES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface Role {
|
||||||
|
id: string;
|
||||||
|
[key: string]: any; // Allow additional properties from backend
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RoleUpdateData = Partial<Omit<Role, 'id'>>;
|
||||||
|
|
||||||
|
export interface PaginationParams {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
|
||||||
|
filters?: Record<string, any>;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
items: T[];
|
||||||
|
pagination?: {
|
||||||
|
currentPage: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type for the request function passed to API functions
|
||||||
|
export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<any>;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// API REQUEST FUNCTIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch list of roles with optional pagination
|
||||||
|
* Endpoint: GET /api/rbac/roles
|
||||||
|
* Query parameter: pagination (optional, JSON-encoded string)
|
||||||
|
* Example: /api/rbac/roles?pagination={"page":1,"pageSize":10}
|
||||||
|
*/
|
||||||
|
export async function fetchRoles(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
params?: PaginationParams
|
||||||
|
): Promise<PaginatedResponse<Role> | Role[]> {
|
||||||
|
const requestParams: any = {};
|
||||||
|
|
||||||
|
// Build pagination object if provided
|
||||||
|
if (params) {
|
||||||
|
const paginationObj: any = {};
|
||||||
|
|
||||||
|
if (params.page !== undefined) paginationObj.page = params.page;
|
||||||
|
if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
|
||||||
|
if (params.sort) paginationObj.sort = params.sort;
|
||||||
|
if (params.filters) paginationObj.filters = params.filters;
|
||||||
|
if (params.search) paginationObj.search = params.search;
|
||||||
|
|
||||||
|
if (Object.keys(paginationObj).length > 0) {
|
||||||
|
requestParams.pagination = JSON.stringify(paginationObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await request({
|
||||||
|
url: '/api/rbac/roles',
|
||||||
|
method: 'get',
|
||||||
|
params: requestParams
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single role by ID
|
||||||
|
* Endpoint: GET /api/rbac/roles/{roleId}
|
||||||
|
*/
|
||||||
|
export async function fetchRoleById(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
roleId: string
|
||||||
|
): Promise<Role | null> {
|
||||||
|
try {
|
||||||
|
const data = await request({
|
||||||
|
url: `/api/rbac/roles/${roleId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
return data || null;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching role by ID:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch role options
|
||||||
|
* Endpoint: GET /api/rbac/roles/options
|
||||||
|
*/
|
||||||
|
export async function fetchRoleOptions(
|
||||||
|
request: ApiRequestFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const data = await request({
|
||||||
|
url: '/api/rbac/roles/options',
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
return data || null;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching role options:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new role
|
||||||
|
* Endpoint: POST /api/rbac/roles
|
||||||
|
* Request body: Role object
|
||||||
|
* Required fields:
|
||||||
|
* - roleLabel: string (e.g., "admin", "user")
|
||||||
|
* - description: TextMultilingual object (at least en is required)
|
||||||
|
*/
|
||||||
|
export async function createRole(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
roleData: Partial<Role>
|
||||||
|
): Promise<Role> {
|
||||||
|
console.log('🔵 createRole - Complete request structure:', {
|
||||||
|
url: '/api/rbac/roles',
|
||||||
|
method: 'post',
|
||||||
|
requestOptions: {
|
||||||
|
url: '/api/rbac/roles',
|
||||||
|
method: 'post',
|
||||||
|
data: roleData
|
||||||
|
},
|
||||||
|
roleData: roleData,
|
||||||
|
roleDataKeys: Object.keys(roleData || {}),
|
||||||
|
roleDataValues: Object.entries(roleData || {}).map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
type: typeof value,
|
||||||
|
isObject: typeof value === 'object' && value !== null,
|
||||||
|
isArray: Array.isArray(value),
|
||||||
|
stringified: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)
|
||||||
|
})),
|
||||||
|
dataStringified: JSON.stringify(roleData, null, 2),
|
||||||
|
dataStringifiedCompact: JSON.stringify(roleData)
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await request({
|
||||||
|
url: '/api/rbac/roles',
|
||||||
|
method: 'post',
|
||||||
|
data: roleData
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ createRole - Response:', result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ createRole - Request failed:', {
|
||||||
|
error,
|
||||||
|
errorMessage: error.message,
|
||||||
|
errorResponse: error.response,
|
||||||
|
errorResponseData: error.response?.data,
|
||||||
|
errorResponseStatus: error.response?.status,
|
||||||
|
errorResponseHeaders: error.response?.headers,
|
||||||
|
errorRequest: error.request,
|
||||||
|
errorConfig: error.config
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a role
|
||||||
|
* Endpoint: PUT /api/rbac/roles/{roleId}
|
||||||
|
*/
|
||||||
|
export async function updateRole(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
roleId: string,
|
||||||
|
updateData: RoleUpdateData
|
||||||
|
): Promise<Role> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/rbac/roles/${roleId}`,
|
||||||
|
method: 'put',
|
||||||
|
data: updateData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a role
|
||||||
|
* Endpoint: DELETE /api/rbac/roles/{roleId}
|
||||||
|
*/
|
||||||
|
export async function deleteRole(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
roleId: string
|
||||||
|
): Promise<void> {
|
||||||
|
await request({
|
||||||
|
url: `/api/rbac/roles/${roleId}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,47 @@ import {
|
||||||
} from '../../../utils/attributeTypeMapper';
|
} from '../../../utils/attributeTypeMapper';
|
||||||
import type { AttributeType } from '../../../utils/attributeTypeMapper';
|
import type { AttributeType } from '../../../utils/attributeTypeMapper';
|
||||||
|
|
||||||
|
// Helper function to detect TextMultilingual objects
|
||||||
|
// TextMultilingual has structure: { en: string, ge?: string, fr?: string, it?: string }
|
||||||
|
const isTextMultilingual = (value: any): boolean => {
|
||||||
|
if (!value || typeof value !== 'object' || Array.isArray(value) || value instanceof Date) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check if it has 'en' property (required) and optionally other language codes
|
||||||
|
return 'en' in value && typeof value.en === 'string';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to check if a field name suggests it's a multilingual field
|
||||||
|
// Only specific fields should be multilingual, not fields that just contain these words
|
||||||
|
const isMultilingualFieldName = (fieldName: string): boolean => {
|
||||||
|
const lowerFieldName = fieldName.toLowerCase();
|
||||||
|
|
||||||
|
// Exact matches for multilingual fields
|
||||||
|
const exactMultilingualFields = ['description'];
|
||||||
|
|
||||||
|
// Fields that end with these patterns (but not roleLabel, etc.)
|
||||||
|
const multilingualPatterns = [
|
||||||
|
/^description$/i,
|
||||||
|
/^label$/i, // Only exact "label", not "roleLabel"
|
||||||
|
/^title$/i, // Only exact "title"
|
||||||
|
/^name$/i // Only exact "name", not field names containing "name"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check exact matches first
|
||||||
|
if (exactMultilingualFields.includes(lowerFieldName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check patterns - but exclude fields like "roleLabel" which should be strings
|
||||||
|
const excludedFields = ['rolelabel', 'role_label', 'rolename', 'role_name', 'username', 'user_name'];
|
||||||
|
if (excludedFields.includes(lowerFieldName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it matches multilingual patterns (exact match, not contains)
|
||||||
|
return multilingualPatterns.some(pattern => pattern.test(fieldName));
|
||||||
|
};
|
||||||
|
|
||||||
// Attribute definition interface (matches backend structure)
|
// Attribute definition interface (matches backend structure)
|
||||||
export interface AttributeDefinition {
|
export interface AttributeDefinition {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -180,13 +221,30 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
// Initialize form data with defaults
|
// Initialize form data with defaults
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({ ...data });
|
// Ensure TextMultilingual fields are properly initialized
|
||||||
|
const processedData: any = { ...data };
|
||||||
|
const filteredAttrs = getFilteredAttributes();
|
||||||
|
filteredAttrs.forEach(attr => {
|
||||||
|
if (isMultilingualFieldName(attr.name) && processedData[attr.name]) {
|
||||||
|
// If it's already a TextMultilingual object, keep it
|
||||||
|
if (!isTextMultilingual(processedData[attr.name])) {
|
||||||
|
// If it's a string, convert to TextMultilingual
|
||||||
|
if (typeof processedData[attr.name] === 'string') {
|
||||||
|
processedData[attr.name] = { en: processedData[attr.name] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setFormData(processedData as T);
|
||||||
} else {
|
} else {
|
||||||
const filteredAttrs = getFilteredAttributes();
|
const filteredAttrs = getFilteredAttributes();
|
||||||
const initialData: any = {};
|
const initialData: any = {};
|
||||||
filteredAttrs.forEach(attr => {
|
filteredAttrs.forEach(attr => {
|
||||||
if (attr.default !== undefined) {
|
if (attr.default !== undefined) {
|
||||||
initialData[attr.name] = attr.default;
|
initialData[attr.name] = attr.default;
|
||||||
|
} else if (isMultilingualFieldName(attr.name)) {
|
||||||
|
// Initialize TextMultilingual fields with empty object
|
||||||
|
initialData[attr.name] = { en: '' };
|
||||||
} else {
|
} else {
|
||||||
initialData[attr.name] = getDefaultValueForType(attr.type);
|
initialData[attr.name] = getDefaultValueForType(attr.type);
|
||||||
}
|
}
|
||||||
|
|
@ -321,10 +379,18 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
const value = formData[attr.name];
|
const value = formData[attr.name];
|
||||||
|
|
||||||
// Check required fields
|
// Check required fields
|
||||||
if (attr.required && (value === undefined || value === null || value === '' ||
|
if (attr.required) {
|
||||||
(Array.isArray(value) && value.length === 0))) {
|
// Special handling for TextMultilingual fields
|
||||||
newErrors[attr.name] = t('formgen.form.required', `${attr.label} is required`);
|
if (isMultilingualFieldName(attr.name) && isTextMultilingual(value)) {
|
||||||
return;
|
if (!value.en || typeof value.en !== 'string' || value.en.trim() === '') {
|
||||||
|
newErrors[attr.name] = t('formgen.form.required', `${attr.label} (English) is required`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (value === undefined || value === null || value === '' ||
|
||||||
|
(Array.isArray(value) && value.length === 0)) {
|
||||||
|
newErrors[attr.name] = t('formgen.form.required', `${attr.label} is required`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type-specific validation
|
// Type-specific validation
|
||||||
|
|
@ -410,9 +476,53 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare form data - ensure roleLabel is a string if it exists
|
||||||
|
const preparedFormData = { ...formData };
|
||||||
|
|
||||||
|
// Ensure roleLabel is a string (not object/array/null/undefined)
|
||||||
|
if ('roleLabel' in preparedFormData) {
|
||||||
|
const roleLabelValue = (preparedFormData as any).roleLabel;
|
||||||
|
if (typeof roleLabelValue !== 'string') {
|
||||||
|
// Convert to string if it's not already
|
||||||
|
if (roleLabelValue === null || roleLabelValue === undefined) {
|
||||||
|
// Remove if null/undefined - let validation handle it
|
||||||
|
delete (preparedFormData as any).roleLabel;
|
||||||
|
} else if (typeof roleLabelValue === 'object') {
|
||||||
|
// If it's an object, try to extract a string value or remove it
|
||||||
|
console.warn('⚠️ roleLabel is an object, removing it:', roleLabelValue);
|
||||||
|
delete (preparedFormData as any).roleLabel;
|
||||||
|
} else {
|
||||||
|
// Convert to string
|
||||||
|
(preparedFormData as any).roleLabel = String(roleLabelValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📤 FormGeneratorForm - handleSubmit - Complete formData structure:', {
|
||||||
|
originalFormData: formData,
|
||||||
|
preparedFormData: preparedFormData,
|
||||||
|
formDataKeys: Object.keys(preparedFormData),
|
||||||
|
formDataEntries: Object.entries(preparedFormData).map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
valueType: typeof value,
|
||||||
|
valueIsObject: typeof value === 'object' && value !== null && !Array.isArray(value),
|
||||||
|
valueIsArray: Array.isArray(value),
|
||||||
|
valueIsNull: value === null,
|
||||||
|
valueIsUndefined: value === undefined,
|
||||||
|
valueStringified: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)
|
||||||
|
})),
|
||||||
|
formDataStringified: JSON.stringify(preparedFormData, null, 2),
|
||||||
|
formDataStringifiedCompact: JSON.stringify(preparedFormData),
|
||||||
|
hasRoleLabel: 'roleLabel' in preparedFormData,
|
||||||
|
roleLabelValue: (preparedFormData as any).roleLabel,
|
||||||
|
roleLabelType: typeof (preparedFormData as any).roleLabel,
|
||||||
|
roleLabelIsString: typeof (preparedFormData as any).roleLabel === 'string'
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
await onSubmit(formData);
|
await onSubmit(preparedFormData);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Form submission error:', error);
|
console.error('Form submission error:', error);
|
||||||
// Handle backend validation errors
|
// Handle backend validation errors
|
||||||
|
|
@ -448,12 +558,84 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Render multilingual field
|
||||||
|
const renderMultilingualField = (attr: AttributeDefinition) => {
|
||||||
|
const value = formData[attr.name] || { en: '' };
|
||||||
|
const hasError = errors[attr.name];
|
||||||
|
const isReadonly = mode === 'display' || attr.readonly || attr.editable === false;
|
||||||
|
|
||||||
|
// Ensure value is a TextMultilingual object
|
||||||
|
const multilingualValue = isTextMultilingual(value) ? value : { en: typeof value === 'string' ? value : '' };
|
||||||
|
|
||||||
|
const languages = [
|
||||||
|
{ code: 'en', label: 'English', required: true },
|
||||||
|
{ code: 'ge', label: 'German', required: false },
|
||||||
|
{ code: 'fr', label: 'French', required: false },
|
||||||
|
{ code: 'it', label: 'Italian', required: false }
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleMultilingualChange = (langCode: string, langValue: string) => {
|
||||||
|
const newValue = { ...multilingualValue, [langCode]: langValue };
|
||||||
|
handleFieldChange(attr.name, newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isReadonly) {
|
||||||
|
// Display mode - show all languages
|
||||||
|
const displayValues = languages
|
||||||
|
.filter(lang => multilingualValue[lang.code] && multilingualValue[lang.code].trim())
|
||||||
|
.map(lang => `${lang.label}: ${multilingualValue[lang.code]}`)
|
||||||
|
.join(' | ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.fieldGroup} key={attr.name}>
|
||||||
|
<div className={styles.readonlyField}>
|
||||||
|
{displayValues || t('common.na', 'N/A')}
|
||||||
|
</div>
|
||||||
|
<label className={styles.focusedLabel}>
|
||||||
|
{attr.label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.fieldGroup} key={attr.name}>
|
||||||
|
<label className={styles.fieldLabel}>
|
||||||
|
{attr.label}
|
||||||
|
{attr.required && <span className={styles.required}>*</span>}
|
||||||
|
</label>
|
||||||
|
{languages.map(lang => (
|
||||||
|
<div className={styles.floatingLabelInput} key={lang.code}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={multilingualValue[lang.code] || ''}
|
||||||
|
onChange={(e) => handleMultilingualChange(lang.code, e.target.value)}
|
||||||
|
onFocus={() => handleFieldFocus(`${attr.name}.${lang.code}`, true)}
|
||||||
|
onBlur={() => handleFieldFocus(`${attr.name}.${lang.code}`, false)}
|
||||||
|
className={`${styles.fieldInput} ${hasError && lang.code === 'en' ? styles.fieldError : ''}`}
|
||||||
|
/>
|
||||||
|
<label className={getLabelClass(`${attr.name}.${lang.code}`, multilingualValue[lang.code])}>
|
||||||
|
{lang.label}
|
||||||
|
{lang.required && <span className={styles.required}>*</span>}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{hasError && <span className={styles.errorText}>{hasError}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Render field based on attribute type
|
// Render field based on attribute type
|
||||||
const renderField = (attr: AttributeDefinition) => {
|
const renderField = (attr: AttributeDefinition) => {
|
||||||
const value = formData[attr.name];
|
const value = formData[attr.name];
|
||||||
const hasError = errors[attr.name];
|
const hasError = errors[attr.name];
|
||||||
const isReadonly = mode === 'display' || attr.readonly || attr.editable === false;
|
const isReadonly = mode === 'display' || attr.readonly || attr.editable === false;
|
||||||
|
|
||||||
|
// Check if this is a multilingual field
|
||||||
|
if (isMultilingualFieldName(attr.name) && (isTextMultilingual(value) || value === undefined || value === null || value === '')) {
|
||||||
|
return renderMultilingualField(attr);
|
||||||
|
}
|
||||||
|
|
||||||
// Readonly/Display field
|
// Readonly/Display field
|
||||||
if (isReadonly) {
|
if (isReadonly) {
|
||||||
let displayValue = value;
|
let displayValue = value;
|
||||||
|
|
@ -621,7 +803,6 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
onBlur={() => handleFieldFocus(attr.name, false)}
|
onBlur={() => handleFieldFocus(attr.name, false)}
|
||||||
className={textareaClassName}
|
className={textareaClassName}
|
||||||
rows={minRows}
|
rows={minRows}
|
||||||
placeholder={attr.placeholder}
|
|
||||||
ref={(textarea) => {
|
ref={(textarea) => {
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
textarea.style.setProperty('min-height', `${minHeight}px`, 'important');
|
textarea.style.setProperty('min-height', `${minHeight}px`, 'important');
|
||||||
|
|
@ -681,7 +862,6 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
onFocus={() => handleFieldFocus(attr.name, true)}
|
onFocus={() => handleFieldFocus(attr.name, true)}
|
||||||
onBlur={() => handleFieldFocus(attr.name, false)}
|
onBlur={() => handleFieldFocus(attr.name, false)}
|
||||||
className={`${styles.fieldInput} ${hasError ? styles.fieldError : ''}`}
|
className={`${styles.fieldInput} ${hasError ? styles.fieldError : ''}`}
|
||||||
placeholder={attr.placeholder}
|
|
||||||
/>
|
/>
|
||||||
<label className={getLabelClass(attr.name, value)}>
|
<label className={getLabelClass(attr.name, value)}>
|
||||||
{attr.label}
|
{attr.label}
|
||||||
|
|
|
||||||
|
|
@ -355,19 +355,25 @@ tbody .actionsColumn {
|
||||||
}
|
}
|
||||||
|
|
||||||
.paginationButton {
|
.paginationButton {
|
||||||
padding: 8px 12px;
|
width: 36px;
|
||||||
border: 1px solid var(--color-gray-disabled);
|
height: 36px;
|
||||||
background: var(--color-bg);
|
padding: 0;
|
||||||
color: var(--color-text);
|
border: none;
|
||||||
border-radius: 4px;
|
background: var(--color-secondary);
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paginationButton:hover:not(:disabled) {
|
.paginationButton:hover:not(:disabled) {
|
||||||
background: var(--color-gray-disabled);
|
background: var(--color-secondary-hover);
|
||||||
border-color: var(--color-secondary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.paginationButton:disabled {
|
.paginationButton:disabled {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,54 @@ import {
|
||||||
} from '../../../utils/attributeTypeMapper';
|
} from '../../../utils/attributeTypeMapper';
|
||||||
import type { AttributeType } from '../../../utils/attributeTypeMapper';
|
import type { AttributeType } from '../../../utils/attributeTypeMapper';
|
||||||
|
|
||||||
|
// Helper function to detect TextMultilingual objects
|
||||||
|
// TextMultilingual has structure: { en: string, ge?: string, fr?: string, it?: string }
|
||||||
|
const isTextMultilingual = (value: any): boolean => {
|
||||||
|
if (!value || typeof value !== 'object' || Array.isArray(value) || value instanceof Date) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check if it has 'en' property (required) and optionally other language codes
|
||||||
|
return 'en' in value && typeof value.en === 'string';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to format TextMultilingual for display
|
||||||
|
const formatTextMultilingual = (value: any, currentLanguage?: string): string => {
|
||||||
|
if (!isTextMultilingual(value)) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map language codes (backend uses 'ge' for German, frontend might use 'de')
|
||||||
|
const languageMap: Record<string, string> = {
|
||||||
|
'de': 'ge',
|
||||||
|
'en': 'en',
|
||||||
|
'fr': 'fr',
|
||||||
|
'it': 'it'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to get value for current language
|
||||||
|
if (currentLanguage) {
|
||||||
|
const backendLang = languageMap[currentLanguage] || currentLanguage;
|
||||||
|
if (value[backendLang] && typeof value[backendLang] === 'string' && value[backendLang].trim()) {
|
||||||
|
return value[backendLang];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to English (required field)
|
||||||
|
if (value.en && typeof value.en === 'string' && value.en.trim()) {
|
||||||
|
return value.en;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no English, try other languages
|
||||||
|
const languages = ['ge', 'fr', 'it'];
|
||||||
|
for (const lang of languages) {
|
||||||
|
if (value[lang] && typeof value[lang] === 'string' && value[lang].trim()) {
|
||||||
|
return value[lang];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '-';
|
||||||
|
};
|
||||||
|
|
||||||
// Types for the FormGeneratorTable
|
// Types for the FormGeneratorTable
|
||||||
export interface ColumnConfig {
|
export interface ColumnConfig {
|
||||||
key: string;
|
key: string;
|
||||||
|
|
@ -111,6 +159,20 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
emptyMessage
|
emptyMessage
|
||||||
}: FormGeneratorTableProps<T>) {
|
}: FormGeneratorTableProps<T>) {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
// Get current language from localStorage or default to 'en'
|
||||||
|
const currentLanguage = useMemo(() => {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem('language') || localStorage.getItem('app-language');
|
||||||
|
if (stored) {
|
||||||
|
// Map frontend language codes to backend codes
|
||||||
|
const langMap: Record<string, string> = { 'de': 'ge', 'en': 'en', 'fr': 'fr', 'it': 'it' };
|
||||||
|
return langMap[stored] || stored;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore localStorage errors
|
||||||
|
}
|
||||||
|
return 'en'; // Default to English
|
||||||
|
}, []);
|
||||||
// Use provided columns (from attributes) if available, otherwise auto-detect from data
|
// Use provided columns (from attributes) if available, otherwise auto-detect from data
|
||||||
// Columns should persist even when data is empty (e.g., after filtering)
|
// Columns should persist even when data is empty (e.g., after filtering)
|
||||||
// Use a ref to cache columns so they persist across data changes
|
// Use a ref to cache columns so they persist across data changes
|
||||||
|
|
@ -307,11 +369,44 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
|
|
||||||
// Get pagination info from backend
|
// Get pagination info from backend
|
||||||
const totalPages = useMemo(() => {
|
const totalPages = useMemo(() => {
|
||||||
if (!supportsBackendPagination || !hookData?.pagination) {
|
if (!supportsBackendPagination) {
|
||||||
return 1; // No pagination if backend doesn't support it
|
return 1; // No pagination if backend doesn't support it
|
||||||
}
|
}
|
||||||
return hookData.pagination.totalPages || 1;
|
|
||||||
}, [supportsBackendPagination, hookData?.pagination]);
|
// If pagination object exists, use totalPages from backend
|
||||||
|
if (hookData?.pagination) {
|
||||||
|
// Debug logging
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('📊 FormGeneratorTable pagination data:', {
|
||||||
|
totalPages: hookData.pagination.totalPages,
|
||||||
|
totalItems: hookData.pagination.totalItems,
|
||||||
|
currentPageSize,
|
||||||
|
calculatedPages: hookData.pagination.totalItems && currentPageSize
|
||||||
|
? Math.ceil(hookData.pagination.totalItems / currentPageSize)
|
||||||
|
: 'N/A'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hookData.pagination.totalPages) {
|
||||||
|
return hookData.pagination.totalPages;
|
||||||
|
}
|
||||||
|
// Fallback: calculate totalPages from totalItems and pageSize
|
||||||
|
if (hookData.pagination.totalItems && currentPageSize) {
|
||||||
|
const calculated = Math.ceil(hookData.pagination.totalItems / currentPageSize);
|
||||||
|
return calculated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have data and pagination is enabled, assume there might be more pages
|
||||||
|
// Show pagination controls if we have at least one page of data
|
||||||
|
if (displayData.length > 0 && displayData.length === currentPageSize) {
|
||||||
|
// If we got a full page of data, there might be more pages
|
||||||
|
// Return a minimum of 2 pages to show pagination controls
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}, [supportsBackendPagination, hookData?.pagination, currentPageSize, displayData.length]);
|
||||||
|
|
||||||
// Handle sorting
|
// Handle sorting
|
||||||
const handleSort = (key: string) => {
|
const handleSort = (key: string) => {
|
||||||
|
|
@ -526,13 +621,21 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
// Handle objects/arrays (e.g., references to other entities)
|
// Handle objects/arrays (e.g., references to other entities)
|
||||||
// Check if value is an object (but not Date, Array, or null)
|
// Check if value is an object (but not Date, Array, or null)
|
||||||
if (typeof value === 'object' && value !== null && !(value instanceof Date) && !Array.isArray(value)) {
|
if (typeof value === 'object' && value !== null && !(value instanceof Date) && !Array.isArray(value)) {
|
||||||
|
// Check if this is a TextMultilingual object first
|
||||||
|
if (isTextMultilingual(value)) {
|
||||||
|
return formatTextMultilingual(value, currentLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
// Try to find a display field in common order: label, name, title, id
|
// Try to find a display field in common order: label, name, title, id
|
||||||
const displayFields = ['label', 'name', 'title', 'id', 'value', 'text'];
|
const displayFields = ['label', 'name', 'title', 'id', 'value', 'text'];
|
||||||
for (const field of displayFields) {
|
for (const field of displayFields) {
|
||||||
if (value[field] !== undefined && value[field] !== null) {
|
if (value[field] !== undefined && value[field] !== null) {
|
||||||
const displayValue = value[field];
|
const displayValue = value[field];
|
||||||
// If the display value is itself an object, try to stringify it nicely
|
// If the display value is itself an object, check if it's TextMultilingual
|
||||||
if (typeof displayValue === 'object' && displayValue !== null) {
|
if (typeof displayValue === 'object' && displayValue !== null && !Array.isArray(displayValue) && !(displayValue instanceof Date)) {
|
||||||
|
if (isTextMultilingual(displayValue)) {
|
||||||
|
return formatTextMultilingual(displayValue, currentLanguage);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(displayValue);
|
return JSON.stringify(displayValue);
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -968,7 +1071,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{totalPages > 1 && (
|
{(totalPages > 1 || (supportsBackendPagination && displayData.length >= currentPageSize)) && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage(1)}
|
onClick={() => setCurrentPage(1)}
|
||||||
|
|
@ -990,7 +1093,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
<span className={styles.paginationInfo}>
|
<span className={styles.paginationInfo}>
|
||||||
{t('formgen.pagination.info')
|
{t('formgen.pagination.info')
|
||||||
.replace('{page}', currentPage.toString())
|
.replace('{page}', currentPage.toString())
|
||||||
.replace('{total}', totalPages.toString())
|
.replace('{total}', totalPages > 1 ? totalPages.toString() : '?')
|
||||||
.replace('{count}', supportsBackendPagination && hookData?.pagination
|
.replace('{count}', supportsBackendPagination && hookData?.pagination
|
||||||
? hookData.pagination.totalItems.toString()
|
? hookData.pagination.totalItems.toString()
|
||||||
: displayData.length.toString())}
|
: displayData.length.toString())}
|
||||||
|
|
@ -998,15 +1101,22 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage(currentPage + 1)}
|
onClick={() => setCurrentPage(currentPage + 1)}
|
||||||
disabled={currentPage === totalPages}
|
disabled={currentPage >= totalPages && totalPages > 1}
|
||||||
className={styles.paginationButton}
|
className={styles.paginationButton}
|
||||||
title={t('formgen.pagination.next')}
|
title={t('formgen.pagination.next')}
|
||||||
>
|
>
|
||||||
»
|
»
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage(totalPages)}
|
onClick={() => {
|
||||||
disabled={currentPage === totalPages}
|
if (totalPages > 1) {
|
||||||
|
setCurrentPage(totalPages);
|
||||||
|
} else {
|
||||||
|
// If we don't know totalPages, just increment to see if there's more data
|
||||||
|
setCurrentPage(currentPage + 1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={currentPage >= totalPages && totalPages > 1}
|
||||||
className={styles.paginationButton}
|
className={styles.paginationButton}
|
||||||
title={t('formgen.pagination.last')}
|
title={t('formgen.pagination.last')}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -2155,19 +2155,85 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (createOperation) {
|
if (createOperation) {
|
||||||
// Use generateEditFieldsFromAttributes from backend (required, no fallback)
|
// Use generateCreateFieldsFromAttributes from backend if available, otherwise fall back to generateEditFieldsFromAttributes
|
||||||
const hookDataAny = hookData as any;
|
const hookDataAny = hookData as any;
|
||||||
|
|
||||||
if (!hookDataAny.generateEditFieldsFromAttributes || typeof hookDataAny.generateEditFieldsFromAttributes !== 'function') {
|
// Prefer generateCreateFieldsFromAttributes for create forms
|
||||||
console.error('Create button requires generateEditFieldsFromAttributes function in hookData');
|
const generateFieldsFunction = hookDataAny.generateCreateFieldsFromAttributes || hookDataAny.generateEditFieldsFromAttributes;
|
||||||
|
|
||||||
|
if (!generateFieldsFunction || typeof generateFieldsFunction !== 'function') {
|
||||||
|
console.error('Create button requires generateCreateFieldsFromAttributes or generateEditFieldsFromAttributes function in hookData');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a wrapper for onCreate that ensures attributes are loaded (define before use)
|
||||||
|
const wrappedCreateOperation = async (formData: any) => {
|
||||||
|
// Ensure attributes are loaded before creating (if function exists)
|
||||||
|
if (hookDataAny.ensureAttributesLoaded && typeof hookDataAny.ensureAttributesLoaded === 'function') {
|
||||||
|
await hookDataAny.ensureAttributesLoaded();
|
||||||
|
}
|
||||||
|
return await createOperation(formData);
|
||||||
|
};
|
||||||
|
|
||||||
// Use dynamic fields from backend attributes
|
// Use dynamic fields from backend attributes
|
||||||
const generatedFields = hookDataAny.generateEditFieldsFromAttributes();
|
const generatedFields = generateFieldsFunction();
|
||||||
|
|
||||||
|
// Check if attributes are still loading
|
||||||
|
const attributes = hookDataAny.attributes;
|
||||||
|
const isLoadingAttributes = hookDataAny.loading || (attributes === undefined);
|
||||||
|
|
||||||
|
// If attributes are loading, show button but disable it
|
||||||
|
// If attributes loaded but empty, still show button (might be a backend issue)
|
||||||
|
// Only hide if we're sure attributes won't load (attributes is null/empty and not loading)
|
||||||
if (!generatedFields || generatedFields.length === 0) {
|
if (!generatedFields || generatedFields.length === 0) {
|
||||||
console.error('No fields generated from backend attributes');
|
// If attributes are still loading, show button disabled
|
||||||
return null;
|
if (isLoadingAttributes) {
|
||||||
|
return (
|
||||||
|
<CreateButton
|
||||||
|
key={button.id}
|
||||||
|
onCreate={wrappedCreateOperation}
|
||||||
|
fields={[]}
|
||||||
|
popupTitle={resolveLanguageText(button.formConfig.popupTitle || 'Create New Item', t)}
|
||||||
|
popupSize={button.formConfig.popupSize || 'medium'}
|
||||||
|
variant={button.variant || 'primary'}
|
||||||
|
size={button.size || 'md'}
|
||||||
|
icon={button.icon}
|
||||||
|
disabled={true}
|
||||||
|
multiStep={button.formConfig.multiStep || false}
|
||||||
|
onSuccess={() => {
|
||||||
|
if (hookData.refetch) {
|
||||||
|
hookData.refetch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{resolveLanguageText(button.label, t)}
|
||||||
|
</CreateButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attributes loaded but no fields - log warning but still show button disabled
|
||||||
|
console.warn('No fields generated from backend attributes. Button will be disabled.');
|
||||||
|
return (
|
||||||
|
<CreateButton
|
||||||
|
key={button.id}
|
||||||
|
onCreate={wrappedCreateOperation}
|
||||||
|
fields={[]}
|
||||||
|
popupTitle={resolveLanguageText(button.formConfig.popupTitle || 'Create New Item', t)}
|
||||||
|
popupSize={button.formConfig.popupSize || 'medium'}
|
||||||
|
variant={button.variant || 'primary'}
|
||||||
|
size={button.size || 'md'}
|
||||||
|
icon={button.icon}
|
||||||
|
disabled={true}
|
||||||
|
multiStep={button.formConfig.multiStep || false}
|
||||||
|
onSuccess={() => {
|
||||||
|
if (hookData.refetch) {
|
||||||
|
hookData.refetch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{resolveLanguageText(button.label, t)}
|
||||||
|
</CreateButton>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve language text for generated fields
|
// Resolve language text for generated fields
|
||||||
|
|
@ -2177,15 +2243,6 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
placeholder: field.placeholder ? resolveLanguageText(field.placeholder, t) : undefined
|
placeholder: field.placeholder ? resolveLanguageText(field.placeholder, t) : undefined
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Create a wrapper for onCreate that ensures attributes are loaded
|
|
||||||
const wrappedCreateOperation = async (formData: any) => {
|
|
||||||
// Ensure attributes are loaded before creating (if function exists)
|
|
||||||
if (hookDataAny.ensureAttributesLoaded && typeof hookDataAny.ensureAttributesLoaded === 'function') {
|
|
||||||
await hookDataAny.ensureAttributesLoaded();
|
|
||||||
}
|
|
||||||
return await createOperation(formData);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Evaluate disabled property if it's a function
|
// Evaluate disabled property if it's a function
|
||||||
const isDisabled = typeof button.disabled === 'function'
|
const isDisabled = typeof button.disabled === 'function'
|
||||||
? button.disabled(hookData)
|
? button.disabled(hookData)
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import { GenericPageData } from '../../../pageInterface';
|
|
||||||
import { FaCog } from 'react-icons/fa';
|
|
||||||
import { getUserDataCache } from '../../../../../utils/userCache';
|
|
||||||
|
|
||||||
export const adminSettingsPageData: GenericPageData = {
|
|
||||||
id: 'admin-admin-settings',
|
|
||||||
path: 'admin/admin-settings',
|
|
||||||
name: 'admin.admin-settings.title',
|
|
||||||
description: 'admin.admin-settings.description',
|
|
||||||
|
|
||||||
// Parent page
|
|
||||||
parentPath: 'admin',
|
|
||||||
showInSidebar: true,
|
|
||||||
|
|
||||||
// Visual
|
|
||||||
icon: FaCog,
|
|
||||||
title: 'admin.admin-settings.title',
|
|
||||||
subtitle: 'admin.admin-settings.subtitle',
|
|
||||||
|
|
||||||
// Header buttons
|
|
||||||
headerButtons: [],
|
|
||||||
|
|
||||||
// Content sections
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
id: 'admin-settings-description',
|
|
||||||
type: 'paragraph',
|
|
||||||
content: 'admin.admin-settings.description_text'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
// Page behavior
|
|
||||||
persistent: false,
|
|
||||||
preload: false,
|
|
||||||
preserveState: true,
|
|
||||||
moduleEnabled: true,
|
|
||||||
|
|
||||||
// Privilege checker: only allow sysadmin role
|
|
||||||
privilegeChecker: async () => {
|
|
||||||
const userData = getUserDataCache();
|
|
||||||
const roleLabels = Array.isArray(userData?.roleLabels) ? userData.roleLabels : [];
|
|
||||||
// Only allow access if user has "sysadmin" role
|
|
||||||
return roleLabels.includes('sysadmin');
|
|
||||||
},
|
|
||||||
|
|
||||||
// Lifecycle hooks
|
|
||||||
onActivate: async () => {
|
|
||||||
if (import.meta.env.DEV) console.log('Admin Settings page activated');
|
|
||||||
},
|
|
||||||
onDeactivate: async () => {
|
|
||||||
if (import.meta.env.DEV) console.log('Admin Settings page deactivated');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,6 +1,123 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
import { GenericPageData } from '../../../pageInterface';
|
import { GenericPageData } from '../../../pageInterface';
|
||||||
import { FaFileContract } from 'react-icons/fa';
|
import { FaFileContract, FaPlus } from 'react-icons/fa';
|
||||||
import { getUserDataCache } from '../../../../../utils/userCache';
|
import { getUserDataCache } from '../../../../../utils/userCache';
|
||||||
|
import { useMandates, useMandateOperations } from '../../../../../hooks/useAdminMandates';
|
||||||
|
import { isDateTimeType, type AttributeType } from '../../../../../utils/attributeTypeMapper';
|
||||||
|
import type { AttributeDefinition } from '../../../../../api/attributesApi';
|
||||||
|
|
||||||
|
// Helper function to convert attribute definitions to column config
|
||||||
|
const attributesToColumns = (attributes: AttributeDefinition[]) => {
|
||||||
|
return attributes.map(attr => {
|
||||||
|
// Use attributeTypeMapper to check if this is a date/timestamp field - disable filtering for these
|
||||||
|
const isDateField = isDateTimeType(attr.type as AttributeType);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: attr.name,
|
||||||
|
label: attr.label || attr.name,
|
||||||
|
type: attr.type || 'string',
|
||||||
|
width: attr.width || 200,
|
||||||
|
minWidth: attr.minWidth || 100,
|
||||||
|
maxWidth: attr.maxWidth || 400,
|
||||||
|
sortable: attr.sortable !== false,
|
||||||
|
// Disable filtering for date/timestamp fields
|
||||||
|
filterable: isDateField ? false : (attr.filterable !== false),
|
||||||
|
searchable: attr.searchable !== false,
|
||||||
|
filterOptions: attr.filterOptions
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook factory function for mandates data
|
||||||
|
const createMandatesHook = () => {
|
||||||
|
return () => {
|
||||||
|
const {
|
||||||
|
data: mandates,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
fetchMandateById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
} = useMandates();
|
||||||
|
const {
|
||||||
|
handleMandateDelete,
|
||||||
|
handleMandateCreate,
|
||||||
|
handleMandateUpdate,
|
||||||
|
deletingMandates,
|
||||||
|
editingMandates,
|
||||||
|
deleteError,
|
||||||
|
updateError
|
||||||
|
} = useMandateOperations();
|
||||||
|
|
||||||
|
const generatedColumns = attributes && attributes.length > 0
|
||||||
|
? attributesToColumns(attributes)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Handle single mandate deletion for FormGenerator
|
||||||
|
const handleDeleteSingle = useCallback(async (mandate: any) => {
|
||||||
|
const success = await handleMandateDelete(mandate.id);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleMandateDelete, refetch]);
|
||||||
|
|
||||||
|
// Handle multiple mandate deletion for FormGenerator
|
||||||
|
const handleDeleteMultiple = useCallback(async (selectedMandates: any[]) => {
|
||||||
|
const mandateIds = selectedMandates.map(mandate => mandate.id);
|
||||||
|
const results = await Promise.all(
|
||||||
|
mandateIds.map(id => handleMandateDelete(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
const allSuccessful = results.every((result: boolean) => result);
|
||||||
|
|
||||||
|
if (allSuccessful) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleMandateDelete, refetch]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: mandates,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
// Operations
|
||||||
|
handleDelete: handleMandateDelete,
|
||||||
|
handleDeleteMultiple,
|
||||||
|
handleMandateUpdate,
|
||||||
|
// FormGenerator specific handlers
|
||||||
|
onDelete: handleDeleteSingle,
|
||||||
|
onDeleteMultiple: handleDeleteMultiple,
|
||||||
|
// Loading states
|
||||||
|
deletingMandates,
|
||||||
|
editingMandates,
|
||||||
|
// Error states
|
||||||
|
deleteError,
|
||||||
|
updateError,
|
||||||
|
// Attributes and permissions for dynamic column/button generation
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
columns: generatedColumns,
|
||||||
|
// Functions for EditActionButton
|
||||||
|
fetchMandateById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded,
|
||||||
|
// Entity type for EditActionButton
|
||||||
|
entityType: 'Mandate',
|
||||||
|
// Create operation for CreateButton
|
||||||
|
handleMandateCreate
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const mandatesPageData: GenericPageData = {
|
export const mandatesPageData: GenericPageData = {
|
||||||
id: 'admin-mandates',
|
id: 'admin-mandates',
|
||||||
|
|
@ -18,14 +135,71 @@ export const mandatesPageData: GenericPageData = {
|
||||||
subtitle: 'admin.mandates.subtitle',
|
subtitle: 'admin.mandates.subtitle',
|
||||||
|
|
||||||
// Header buttons
|
// Header buttons
|
||||||
headerButtons: [],
|
headerButtons: [
|
||||||
|
{
|
||||||
|
id: 'add-mandate',
|
||||||
|
label: 'admin.mandates.new_button',
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'md',
|
||||||
|
icon: FaPlus,
|
||||||
|
formConfig: {
|
||||||
|
// Fields will be generated dynamically from attributes via generateCreateFieldsFromAttributes
|
||||||
|
// PageRenderer will use generateCreateFieldsFromAttributes if available, otherwise generateEditFieldsFromAttributes
|
||||||
|
fields: [], // Empty array - fields will be generated dynamically from attributes
|
||||||
|
popupTitle: 'admin.mandates.modal.create.title',
|
||||||
|
popupSize: 'medium',
|
||||||
|
createOperationName: 'handleMandateCreate',
|
||||||
|
successMessage: 'admin.mandates.create.success',
|
||||||
|
errorMessage: 'admin.mandates.create.error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
// Content sections
|
// Content sections - using generic table approach
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
id: 'mandates-description',
|
id: 'mandates-table',
|
||||||
type: 'paragraph',
|
type: 'table',
|
||||||
content: 'admin.mandates.description_text'
|
tableConfig: {
|
||||||
|
hookFactory: createMandatesHook,
|
||||||
|
// Columns are generated dynamically from attributes via hookData.columns
|
||||||
|
actionButtons: [
|
||||||
|
{
|
||||||
|
type: 'edit',
|
||||||
|
title: 'admin.mandates.action.edit',
|
||||||
|
idField: 'id',
|
||||||
|
nameField: 'id',
|
||||||
|
operationName: 'handleMandateUpdate',
|
||||||
|
loadingStateName: 'editingMandates',
|
||||||
|
fetchItemFunctionName: 'fetchMandateById',
|
||||||
|
disabled: (hookData: any) => {
|
||||||
|
if (!hookData?.permissions) return { disabled: false };
|
||||||
|
const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view;
|
||||||
|
return { disabled: !hasUpdate, message: 'No permission to edit mandates' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'delete',
|
||||||
|
title: 'admin.mandates.action.delete',
|
||||||
|
idField: 'id',
|
||||||
|
operationName: 'handleDelete',
|
||||||
|
loadingStateName: 'deletingMandates',
|
||||||
|
// Only show if user has delete permission (permissions.delete !== 'n')
|
||||||
|
disabled: (hookData: any) => {
|
||||||
|
if (!hookData?.permissions) return { disabled: false };
|
||||||
|
const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view;
|
||||||
|
return { disabled: !hasDelete, message: 'No permission to delete mandates' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
searchable: true,
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
resizable: true,
|
||||||
|
pagination: true,
|
||||||
|
pageSize: 10,
|
||||||
|
className: 'mandates-table'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,132 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
import { GenericPageData } from '../../../pageInterface';
|
import { GenericPageData } from '../../../pageInterface';
|
||||||
import { FaUserShield } from 'react-icons/fa';
|
import { FaUserShield, FaPlus } from 'react-icons/fa';
|
||||||
import { getUserDataCache } from '../../../../../utils/userCache';
|
import { getUserDataCache } from '../../../../../utils/userCache';
|
||||||
|
import { useRbacRoles, useRbacRoleOperations } from '../../../../../hooks/useAdminRbacRoles';
|
||||||
|
import { isDateTimeType, type AttributeType } from '../../../../../utils/attributeTypeMapper';
|
||||||
|
import type { AttributeDefinition } from '../../../../../api/attributesApi';
|
||||||
|
|
||||||
|
// Helper function to convert attribute definitions to column config
|
||||||
|
const attributesToColumns = (attributes: AttributeDefinition[]) => {
|
||||||
|
return attributes.map(attr => {
|
||||||
|
// Use attributeTypeMapper to check if this is a date/timestamp field - disable filtering for these
|
||||||
|
const isDateField = isDateTimeType(attr.type as AttributeType);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: attr.name,
|
||||||
|
label: attr.label || attr.name,
|
||||||
|
type: attr.type || 'string',
|
||||||
|
width: attr.width || 200,
|
||||||
|
minWidth: attr.minWidth || 100,
|
||||||
|
maxWidth: attr.maxWidth || 400,
|
||||||
|
sortable: attr.sortable !== false,
|
||||||
|
// Disable filtering for date/timestamp fields
|
||||||
|
filterable: isDateField ? false : (attr.filterable !== false),
|
||||||
|
searchable: attr.searchable !== false,
|
||||||
|
filterOptions: attr.filterOptions
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook factory function for rbac roles data
|
||||||
|
const createRbacRolesHook = () => {
|
||||||
|
return () => {
|
||||||
|
const {
|
||||||
|
data: roles,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
fetchRoleById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
} = useRbacRoles();
|
||||||
|
const {
|
||||||
|
handleRoleDelete,
|
||||||
|
handleRoleCreate,
|
||||||
|
handleRoleUpdate,
|
||||||
|
deletingRoles,
|
||||||
|
editingRoles,
|
||||||
|
deleteError,
|
||||||
|
updateError
|
||||||
|
} = useRbacRoleOperations();
|
||||||
|
|
||||||
|
const generatedColumns = attributes && attributes.length > 0
|
||||||
|
? attributesToColumns(attributes)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Handle single role deletion for FormGenerator
|
||||||
|
const handleDeleteSingle = useCallback(async (role: any) => {
|
||||||
|
const success = await handleRoleDelete(role.id);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleRoleDelete, refetch]);
|
||||||
|
|
||||||
|
// Handle multiple role deletion for FormGenerator
|
||||||
|
const handleDeleteMultiple = useCallback(async (selectedRoles: any[]) => {
|
||||||
|
const roleIds = selectedRoles.map(role => role.id);
|
||||||
|
const results = await Promise.all(
|
||||||
|
roleIds.map(id => handleRoleDelete(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
const allSuccessful = results.every((result: boolean) => result);
|
||||||
|
|
||||||
|
if (allSuccessful) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleRoleDelete, refetch]);
|
||||||
|
|
||||||
|
// Wrapper for create operation that refetches after success
|
||||||
|
const handleRoleCreateWithRefetch = useCallback(async (roleData: any) => {
|
||||||
|
const result = await handleRoleCreate(roleData);
|
||||||
|
if (result?.success) {
|
||||||
|
await refetch();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [handleRoleCreate, refetch]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: roles,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
// Operations
|
||||||
|
handleDelete: handleRoleDelete,
|
||||||
|
handleDeleteMultiple,
|
||||||
|
handleRoleUpdate,
|
||||||
|
// FormGenerator specific handlers
|
||||||
|
onDelete: handleDeleteSingle,
|
||||||
|
onDeleteMultiple: handleDeleteMultiple,
|
||||||
|
// Loading states
|
||||||
|
deletingRoles,
|
||||||
|
editingRoles,
|
||||||
|
// Error states
|
||||||
|
deleteError,
|
||||||
|
updateError,
|
||||||
|
// Attributes and permissions for dynamic column/button generation
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
columns: generatedColumns,
|
||||||
|
// Functions for EditActionButton
|
||||||
|
fetchRoleById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded,
|
||||||
|
// Entity type for EditActionButton
|
||||||
|
entityType: 'Role',
|
||||||
|
// Create operation for CreateButton (with refetch)
|
||||||
|
handleRoleCreate: handleRoleCreateWithRefetch
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const rbacRolePageData: GenericPageData = {
|
export const rbacRolePageData: GenericPageData = {
|
||||||
id: 'admin-rbac-role',
|
id: 'admin-rbac-role',
|
||||||
|
|
@ -18,14 +144,76 @@ export const rbacRolePageData: GenericPageData = {
|
||||||
subtitle: 'admin.rbac-role.subtitle',
|
subtitle: 'admin.rbac-role.subtitle',
|
||||||
|
|
||||||
// Header buttons
|
// Header buttons
|
||||||
headerButtons: [],
|
headerButtons: [
|
||||||
|
{
|
||||||
|
id: 'add-role',
|
||||||
|
label: 'admin.rbac-role.new_button',
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'md',
|
||||||
|
icon: FaPlus,
|
||||||
|
formConfig: {
|
||||||
|
// Fields will be generated dynamically from attributes via generateCreateFieldsFromAttributes
|
||||||
|
// PageRenderer will use generateCreateFieldsFromAttributes if available, otherwise generateEditFieldsFromAttributes
|
||||||
|
fields: [], // Empty array - fields will be generated dynamically from attributes
|
||||||
|
popupTitle: 'admin.rbac-role.modal.create.title',
|
||||||
|
popupSize: 'medium',
|
||||||
|
createOperationName: 'handleRoleCreate',
|
||||||
|
successMessage: 'admin.rbac-role.create.success',
|
||||||
|
errorMessage: 'admin.rbac-role.create.error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
// Content sections
|
// Content sections - using generic table approach
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
id: 'rbac-role-description',
|
id: 'rbac-role-description',
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: 'admin.rbac-role.description_text'
|
content: 'admin.rbac-role.description_text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rbac-roles-table',
|
||||||
|
type: 'table',
|
||||||
|
tableConfig: {
|
||||||
|
hookFactory: createRbacRolesHook,
|
||||||
|
// Columns are generated dynamically from attributes via hookData.columns
|
||||||
|
actionButtons: [
|
||||||
|
{
|
||||||
|
type: 'edit',
|
||||||
|
title: 'admin.rbac-role.action.edit',
|
||||||
|
idField: 'id',
|
||||||
|
nameField: 'id',
|
||||||
|
operationName: 'handleRoleUpdate',
|
||||||
|
loadingStateName: 'editingRoles',
|
||||||
|
fetchItemFunctionName: 'fetchRoleById',
|
||||||
|
disabled: (hookData: any) => {
|
||||||
|
if (!hookData?.permissions) return { disabled: false };
|
||||||
|
const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view;
|
||||||
|
return { disabled: !hasUpdate, message: 'No permission to edit roles' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'delete',
|
||||||
|
title: 'admin.rbac-role.action.delete',
|
||||||
|
idField: 'id',
|
||||||
|
operationName: 'handleDelete',
|
||||||
|
loadingStateName: 'deletingRoles',
|
||||||
|
// Only show if user has delete permission (permissions.delete !== 'n')
|
||||||
|
disabled: (hookData: any) => {
|
||||||
|
if (!hookData?.permissions) return { disabled: false };
|
||||||
|
const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view;
|
||||||
|
return { disabled: !hasDelete, message: 'No permission to delete roles' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
searchable: true,
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
resizable: true,
|
||||||
|
pagination: true,
|
||||||
|
pageSize: 10,
|
||||||
|
className: 'rbac-roles-table'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,126 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
import { GenericPageData } from '../../../pageInterface';
|
import { GenericPageData } from '../../../pageInterface';
|
||||||
import { FaShieldAlt } from 'react-icons/fa';
|
import { FaShieldAlt, FaPlus } from 'react-icons/fa';
|
||||||
import { getUserDataCache } from '../../../../../utils/userCache';
|
import { getUserDataCache } from '../../../../../utils/userCache';
|
||||||
|
import { useRbacRules, useRbacRuleOperations } from '../../../../../hooks/useAdminRbacRules';
|
||||||
|
import { isDateTimeType, type AttributeType } from '../../../../../utils/attributeTypeMapper';
|
||||||
|
import type { AttributeDefinition } from '../../../../../api/attributesApi';
|
||||||
|
|
||||||
|
// Helper function to convert attribute definitions to column config
|
||||||
|
const attributesToColumns = (attributes: AttributeDefinition[]) => {
|
||||||
|
return attributes.map(attr => {
|
||||||
|
// Use attributeTypeMapper to check if this is a date/timestamp field - disable filtering for these
|
||||||
|
const isDateField = isDateTimeType(attr.type as AttributeType);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: attr.name,
|
||||||
|
label: attr.label || attr.name,
|
||||||
|
type: attr.type || 'string',
|
||||||
|
width: attr.width || 200,
|
||||||
|
minWidth: attr.minWidth || 100,
|
||||||
|
maxWidth: attr.maxWidth || 400,
|
||||||
|
sortable: attr.sortable !== false,
|
||||||
|
// Disable filtering for date/timestamp fields
|
||||||
|
filterable: isDateField ? false : (attr.filterable !== false),
|
||||||
|
searchable: attr.searchable !== false,
|
||||||
|
filterOptions: attr.filterOptions
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook factory function for RBAC rules data
|
||||||
|
const createRbacRulesHook = () => {
|
||||||
|
return () => {
|
||||||
|
const {
|
||||||
|
data: rbacRules,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
pagination,
|
||||||
|
fetchRbacRuleById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
} = useRbacRules();
|
||||||
|
const {
|
||||||
|
handleRbacRuleDelete,
|
||||||
|
handleRbacRuleCreate,
|
||||||
|
handleRbacRuleUpdate,
|
||||||
|
deletingRbacRules,
|
||||||
|
editingRbacRules,
|
||||||
|
deleteError,
|
||||||
|
updateError
|
||||||
|
} = useRbacRuleOperations();
|
||||||
|
|
||||||
|
const generatedColumns = attributes && attributes.length > 0
|
||||||
|
? attributesToColumns(attributes)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Handle single RBAC rule deletion for FormGenerator
|
||||||
|
const handleDeleteSingle = useCallback(async (rule: any) => {
|
||||||
|
const success = await handleRbacRuleDelete(rule.id);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleRbacRuleDelete, refetch]);
|
||||||
|
|
||||||
|
// Handle multiple RBAC rule deletion for FormGenerator
|
||||||
|
const handleDeleteMultiple = useCallback(async (selectedRules: any[]) => {
|
||||||
|
const ruleIds = selectedRules.map(rule => rule.id);
|
||||||
|
const results = await Promise.all(
|
||||||
|
ruleIds.map(id => handleRbacRuleDelete(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
const allSuccessful = results.every((result: boolean) => result);
|
||||||
|
|
||||||
|
if (allSuccessful) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleRbacRuleDelete, refetch]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: rbacRules,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
// Operations
|
||||||
|
handleDelete: handleRbacRuleDelete,
|
||||||
|
handleDeleteMultiple,
|
||||||
|
handleRbacRuleUpdate,
|
||||||
|
// FormGenerator specific handlers
|
||||||
|
onDelete: handleDeleteSingle,
|
||||||
|
onDeleteMultiple: handleDeleteMultiple,
|
||||||
|
// Loading states
|
||||||
|
deletingRbacRules,
|
||||||
|
editingRbacRules,
|
||||||
|
// Error states
|
||||||
|
deleteError,
|
||||||
|
updateError,
|
||||||
|
// Attributes and permissions for dynamic column/button generation
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
columns: generatedColumns,
|
||||||
|
// Functions for EditActionButton
|
||||||
|
fetchRbacRuleById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded,
|
||||||
|
// Entity type for EditActionButton
|
||||||
|
entityType: 'AccessRule',
|
||||||
|
// Create operation for CreateButton
|
||||||
|
handleRbacRuleCreate,
|
||||||
|
// Pagination info for FormGeneratorTable
|
||||||
|
pagination
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const rbacRulesPageData: GenericPageData = {
|
export const rbacRulesPageData: GenericPageData = {
|
||||||
id: 'admin-rbac-rules',
|
id: 'admin-rbac-rules',
|
||||||
|
|
@ -18,14 +138,71 @@ export const rbacRulesPageData: GenericPageData = {
|
||||||
subtitle: 'admin.rbac-rules.subtitle',
|
subtitle: 'admin.rbac-rules.subtitle',
|
||||||
|
|
||||||
// Header buttons
|
// Header buttons
|
||||||
headerButtons: [],
|
headerButtons: [
|
||||||
|
{
|
||||||
|
id: 'add-rbac-rule',
|
||||||
|
label: 'admin.rbac-rules.new_button',
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'md',
|
||||||
|
icon: FaPlus,
|
||||||
|
formConfig: {
|
||||||
|
// Fields will be generated dynamically from attributes via generateCreateFieldsFromAttributes
|
||||||
|
// PageRenderer will use generateCreateFieldsFromAttributes if available, otherwise generateEditFieldsFromAttributes
|
||||||
|
fields: [], // Empty array - fields will be generated dynamically from attributes
|
||||||
|
popupTitle: 'admin.rbac-rules.modal.create.title',
|
||||||
|
popupSize: 'medium',
|
||||||
|
createOperationName: 'handleRbacRuleCreate',
|
||||||
|
successMessage: 'admin.rbac-rules.create.success',
|
||||||
|
errorMessage: 'admin.rbac-rules.create.error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
// Content sections
|
// Content sections - using generic table approach
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
id: 'rbac-rules-description',
|
id: 'rbac-rules-table',
|
||||||
type: 'paragraph',
|
type: 'table',
|
||||||
content: 'admin.rbac-rules.description_text'
|
tableConfig: {
|
||||||
|
hookFactory: createRbacRulesHook,
|
||||||
|
// Columns are generated dynamically from attributes via hookData.columns
|
||||||
|
actionButtons: [
|
||||||
|
{
|
||||||
|
type: 'edit',
|
||||||
|
title: 'admin.rbac-rules.action.edit',
|
||||||
|
idField: 'id',
|
||||||
|
nameField: 'id',
|
||||||
|
operationName: 'handleRbacRuleUpdate',
|
||||||
|
loadingStateName: 'editingRbacRules',
|
||||||
|
fetchItemFunctionName: 'fetchRbacRuleById',
|
||||||
|
disabled: (hookData: any) => {
|
||||||
|
if (!hookData?.permissions) return { disabled: false };
|
||||||
|
const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view;
|
||||||
|
return { disabled: !hasUpdate, message: 'No permission to edit RBAC rules' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'delete',
|
||||||
|
title: 'admin.rbac-rules.action.delete',
|
||||||
|
idField: 'id',
|
||||||
|
operationName: 'handleDelete',
|
||||||
|
loadingStateName: 'deletingRbacRules',
|
||||||
|
// Only show if user has delete permission (permissions.delete !== 'n')
|
||||||
|
disabled: (hookData: any) => {
|
||||||
|
if (!hookData?.permissions) return { disabled: false };
|
||||||
|
const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view;
|
||||||
|
return { disabled: !hasDelete, message: 'No permission to delete RBAC rules' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
searchable: true,
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
resizable: true,
|
||||||
|
pagination: true,
|
||||||
|
pageSize: 10,
|
||||||
|
className: 'rbac-rules-table'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
||||||
563
src/hooks/useAdminMandates.ts
Normal file
563
src/hooks/useAdminMandates.ts
Normal file
|
|
@ -0,0 +1,563 @@
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useApiRequest } from './useApi';
|
||||||
|
import { getUserDataCache } from '../utils/userCache';
|
||||||
|
import { usePermissions, type UserPermissions } from './usePermissions';
|
||||||
|
import {
|
||||||
|
fetchMandates as fetchMandatesApi,
|
||||||
|
fetchMandateById as fetchMandateByIdApi,
|
||||||
|
createMandate as createMandateApi,
|
||||||
|
updateMandate as updateMandateApi,
|
||||||
|
deleteMandate as deleteMandateApi,
|
||||||
|
type Mandate,
|
||||||
|
type MandateUpdateData,
|
||||||
|
type PaginationParams
|
||||||
|
} from '../api/mandateApi';
|
||||||
|
import { fetchAttributes } from '../api/attributesApi';
|
||||||
|
import type { AttributeDefinition } from '../api/attributesApi';
|
||||||
|
import {
|
||||||
|
isCheckboxType,
|
||||||
|
isSelectType,
|
||||||
|
isMultiselectType,
|
||||||
|
isDateTimeType,
|
||||||
|
isTextareaType,
|
||||||
|
type AttributeType
|
||||||
|
} from '../utils/attributeTypeMapper';
|
||||||
|
|
||||||
|
// Re-export types for backward compatibility
|
||||||
|
export type { Mandate, MandateUpdateData, AttributeDefinition, PaginationParams };
|
||||||
|
|
||||||
|
// Organization mandates hook (list, update, delete) - following users pattern
|
||||||
|
export function useMandates() {
|
||||||
|
const [mandates, setMandates] = useState<Mandate[]>([]);
|
||||||
|
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
|
||||||
|
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
|
||||||
|
const [pagination, setPagination] = useState<{
|
||||||
|
currentPage: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
} | null>(null);
|
||||||
|
const { request, isLoading: loading, error } = useApiRequest<null, Mandate[]>();
|
||||||
|
const { checkPermission } = usePermissions();
|
||||||
|
|
||||||
|
// Fetch attributes from backend
|
||||||
|
const fetchAttributesData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const attrs = await fetchAttributes(request, 'Mandate');
|
||||||
|
setAttributes(attrs);
|
||||||
|
return attrs;
|
||||||
|
} catch (error: any) {
|
||||||
|
// Don't log 429 errors as errors (they're rate limit warnings)
|
||||||
|
if (error.response?.status === 429) {
|
||||||
|
console.warn('Rate limit exceeded while fetching mandate attributes. Please wait.');
|
||||||
|
} else if (error.response?.status !== 401) {
|
||||||
|
// Only log non-auth errors (401 is expected when not logged in)
|
||||||
|
console.error('Error fetching attributes:', error);
|
||||||
|
}
|
||||||
|
setAttributes([]);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
// Fetch permissions from backend
|
||||||
|
const fetchPermissions = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const perms = await checkPermission('DATA', 'Mandate');
|
||||||
|
setPermissions(perms);
|
||||||
|
return perms;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching permissions:', error);
|
||||||
|
const defaultPerms: UserPermissions = {
|
||||||
|
view: false,
|
||||||
|
read: 'n',
|
||||||
|
create: 'n',
|
||||||
|
update: 'n',
|
||||||
|
delete: 'n',
|
||||||
|
};
|
||||||
|
setPermissions(defaultPerms);
|
||||||
|
return defaultPerms;
|
||||||
|
}
|
||||||
|
}, [checkPermission]);
|
||||||
|
|
||||||
|
const fetchMandates = useCallback(async (params?: PaginationParams) => {
|
||||||
|
try {
|
||||||
|
const requestParams: any = {};
|
||||||
|
|
||||||
|
// Build pagination object if provided
|
||||||
|
if (params) {
|
||||||
|
const paginationObj: any = {};
|
||||||
|
|
||||||
|
if (params.page !== undefined) paginationObj.page = params.page;
|
||||||
|
if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
|
||||||
|
if (params.sort) paginationObj.sort = params.sort;
|
||||||
|
if (params.filters) paginationObj.filters = params.filters;
|
||||||
|
if (params.search) paginationObj.search = params.search;
|
||||||
|
|
||||||
|
if (Object.keys(paginationObj).length > 0) {
|
||||||
|
requestParams.pagination = JSON.stringify(paginationObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fetchMandatesApi(request, params);
|
||||||
|
|
||||||
|
// Handle paginated response
|
||||||
|
if (data && typeof data === 'object' && 'items' in data) {
|
||||||
|
const items = Array.isArray(data.items) ? data.items : [];
|
||||||
|
setMandates(items);
|
||||||
|
if (data.pagination) {
|
||||||
|
setPagination(data.pagination);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle non-paginated response (backward compatibility)
|
||||||
|
const items = Array.isArray(data) ? data : [];
|
||||||
|
setMandates(items);
|
||||||
|
setPagination(null);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// Error is already handled by useApiRequest
|
||||||
|
setMandates([]);
|
||||||
|
setPagination(null);
|
||||||
|
}
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
// Optimistically remove a mandate from the local state
|
||||||
|
const removeOptimistically = (mandateId: string) => {
|
||||||
|
setMandates(prevMandates => prevMandates.filter(mandate => mandate.id !== mandateId));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optimistically update a mandate in the local state
|
||||||
|
const updateOptimistically = (mandateId: string, updateData: Partial<Mandate>) => {
|
||||||
|
setMandates(prevMandates =>
|
||||||
|
prevMandates.map(mandate =>
|
||||||
|
mandate.id === mandateId
|
||||||
|
? { ...mandate, ...updateData }
|
||||||
|
: mandate
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch a single mandate by ID
|
||||||
|
const fetchMandateById = useCallback(async (mandateId: string): Promise<Mandate | null> => {
|
||||||
|
return await fetchMandateByIdApi(request, mandateId);
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
// Generate edit fields from attributes dynamically using attributeTypeMapper utilities
|
||||||
|
const generateEditFieldsFromAttributes = useCallback((): Array<{
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly';
|
||||||
|
editable?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
validator?: (value: any) => string | null;
|
||||||
|
minRows?: number;
|
||||||
|
maxRows?: number;
|
||||||
|
options?: Array<{ value: string | number; label: string }>;
|
||||||
|
optionsReference?: string; // For options that need to be fetched (e.g., "user.role")
|
||||||
|
}> => {
|
||||||
|
if (!attributes || attributes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const editableFields = attributes
|
||||||
|
.filter(attr => {
|
||||||
|
// Filter out non-editable fields based on readonly/editable flags
|
||||||
|
if (attr.readonly === true || attr.editable === false) {
|
||||||
|
return false; // Don't show readonly fields in edit form
|
||||||
|
}
|
||||||
|
// Also filter out common non-editable fields
|
||||||
|
const nonEditableFields = ['id', 'mandateId', '_createdBy', '_hideDelete'];
|
||||||
|
return !nonEditableFields.includes(attr.name);
|
||||||
|
})
|
||||||
|
.map(attr => {
|
||||||
|
// Map backend attribute type to form field type using attributeTypeMapper utilities
|
||||||
|
let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string';
|
||||||
|
let options: Array<{ value: string | number; label: string }> | undefined = undefined;
|
||||||
|
let optionsReference: string | undefined = undefined;
|
||||||
|
|
||||||
|
const attrType = attr.type as AttributeType;
|
||||||
|
|
||||||
|
// Use attributeTypeMapper utilities to determine field type
|
||||||
|
if (isCheckboxType(attrType)) {
|
||||||
|
fieldType = 'boolean';
|
||||||
|
} else if (attrType === 'email') {
|
||||||
|
fieldType = 'email';
|
||||||
|
} else if (isDateTimeType(attrType)) {
|
||||||
|
fieldType = 'date';
|
||||||
|
} else if (isSelectType(attrType)) {
|
||||||
|
fieldType = 'enum';
|
||||||
|
// Handle options - can be array or string reference
|
||||||
|
const attrOptions = (attr as any).options;
|
||||||
|
if (Array.isArray(attrOptions)) {
|
||||||
|
options = attrOptions.map((opt: any) => {
|
||||||
|
const labelValue = typeof opt.label === 'string'
|
||||||
|
? opt.label
|
||||||
|
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||||
|
return {
|
||||||
|
value: opt.value,
|
||||||
|
label: labelValue
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (typeof attrOptions === 'string') {
|
||||||
|
// Options reference (e.g., "user.role", "auth.authority")
|
||||||
|
optionsReference = attrOptions;
|
||||||
|
}
|
||||||
|
} else if (isMultiselectType(attrType)) {
|
||||||
|
fieldType = 'multiselect';
|
||||||
|
// Handle options - can be array or string reference
|
||||||
|
const attrOptions = (attr as any).options;
|
||||||
|
if (Array.isArray(attrOptions)) {
|
||||||
|
options = attrOptions.map((opt: any) => {
|
||||||
|
const labelValue = typeof opt.label === 'string'
|
||||||
|
? opt.label
|
||||||
|
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||||
|
return {
|
||||||
|
value: opt.value,
|
||||||
|
label: labelValue
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (typeof attrOptions === 'string') {
|
||||||
|
// Options reference (e.g., "user.role", "auth.authority")
|
||||||
|
optionsReference = attrOptions;
|
||||||
|
}
|
||||||
|
} else if (isTextareaType(attrType)) {
|
||||||
|
fieldType = 'textarea';
|
||||||
|
} else if (attrType === 'readonly') {
|
||||||
|
fieldType = 'readonly';
|
||||||
|
} else {
|
||||||
|
fieldType = 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define validators and required fields
|
||||||
|
let required = attr.required === true;
|
||||||
|
let validator: ((value: any) => string | null) | undefined = undefined;
|
||||||
|
let minRows: number | undefined = undefined;
|
||||||
|
let maxRows: number | undefined = undefined;
|
||||||
|
|
||||||
|
// Email validation
|
||||||
|
if (fieldType === 'email') {
|
||||||
|
validator = (value: string) => {
|
||||||
|
if (required && (!value || value.trim() === '')) {
|
||||||
|
return 'Email cannot be empty';
|
||||||
|
}
|
||||||
|
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
||||||
|
return 'Invalid email format';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Textarea settings
|
||||||
|
else if (fieldType === 'textarea') {
|
||||||
|
minRows = 4;
|
||||||
|
maxRows = 8;
|
||||||
|
if (attr.name.toLowerCase().includes('content')) {
|
||||||
|
minRows = 6;
|
||||||
|
maxRows = 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Multiselect validation
|
||||||
|
else if (fieldType === 'multiselect' && required) {
|
||||||
|
validator = (value: any[]) => {
|
||||||
|
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||||
|
return `${attr.label} is required`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: attr.name,
|
||||||
|
label: attr.label || attr.name,
|
||||||
|
type: fieldType,
|
||||||
|
editable: (attr as any).editable !== false && (attr as any).readonly !== true,
|
||||||
|
required,
|
||||||
|
validator,
|
||||||
|
minRows,
|
||||||
|
maxRows,
|
||||||
|
options,
|
||||||
|
optionsReference
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return editableFields;
|
||||||
|
}, [attributes]);
|
||||||
|
|
||||||
|
// Generate create fields from attributes dynamically using attributeTypeMapper utilities
|
||||||
|
const generateCreateFieldsFromAttributes = useCallback((): Array<{
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly';
|
||||||
|
required?: boolean;
|
||||||
|
validator?: (value: any) => string | null;
|
||||||
|
minRows?: number;
|
||||||
|
maxRows?: number;
|
||||||
|
options?: Array<{ value: string | number; label: string }>;
|
||||||
|
optionsReference?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
}> => {
|
||||||
|
if (!attributes || attributes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const createFields = attributes
|
||||||
|
.filter(attr => {
|
||||||
|
// Filter out non-editable fields and auto-generated fields for create forms
|
||||||
|
if (attr.readonly === true || attr.editable === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Filter out ID fields and other auto-generated fields
|
||||||
|
const nonEditableFields = ['id', 'mandateId', '_createdBy', '_hideDelete'];
|
||||||
|
return !nonEditableFields.includes(attr.name);
|
||||||
|
})
|
||||||
|
.map(attr => {
|
||||||
|
// Map backend attribute type to form field type using attributeTypeMapper utilities
|
||||||
|
let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string';
|
||||||
|
let options: Array<{ value: string | number; label: string }> | undefined = undefined;
|
||||||
|
let optionsReference: string | undefined = undefined;
|
||||||
|
|
||||||
|
const attrType = attr.type as AttributeType;
|
||||||
|
|
||||||
|
// Use attributeTypeMapper utilities to determine field type
|
||||||
|
if (isCheckboxType(attrType)) {
|
||||||
|
fieldType = 'boolean';
|
||||||
|
} else if (attrType === 'email') {
|
||||||
|
fieldType = 'email';
|
||||||
|
} else if (isDateTimeType(attrType)) {
|
||||||
|
fieldType = 'date';
|
||||||
|
} else if (isSelectType(attrType)) {
|
||||||
|
fieldType = 'enum';
|
||||||
|
const attrOptions = (attr as any).options;
|
||||||
|
if (Array.isArray(attrOptions)) {
|
||||||
|
options = attrOptions.map((opt: any) => {
|
||||||
|
const labelValue = typeof opt.label === 'string'
|
||||||
|
? opt.label
|
||||||
|
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||||
|
return {
|
||||||
|
value: opt.value,
|
||||||
|
label: labelValue
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (typeof attrOptions === 'string') {
|
||||||
|
optionsReference = attrOptions;
|
||||||
|
}
|
||||||
|
} else if (isMultiselectType(attrType)) {
|
||||||
|
fieldType = 'multiselect';
|
||||||
|
const attrOptions = (attr as any).options;
|
||||||
|
if (Array.isArray(attrOptions)) {
|
||||||
|
options = attrOptions.map((opt: any) => {
|
||||||
|
const labelValue = typeof opt.label === 'string'
|
||||||
|
? opt.label
|
||||||
|
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||||
|
return {
|
||||||
|
value: opt.value,
|
||||||
|
label: labelValue
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (typeof attrOptions === 'string') {
|
||||||
|
optionsReference = attrOptions;
|
||||||
|
}
|
||||||
|
} else if (isTextareaType(attrType)) {
|
||||||
|
fieldType = 'textarea';
|
||||||
|
} else {
|
||||||
|
fieldType = 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define validators and required fields
|
||||||
|
let required = attr.required === true;
|
||||||
|
let validator: ((value: any) => string | null) | undefined = undefined;
|
||||||
|
let minRows: number | undefined = undefined;
|
||||||
|
let maxRows: number | undefined = undefined;
|
||||||
|
|
||||||
|
// Email validation
|
||||||
|
if (fieldType === 'email') {
|
||||||
|
validator = (value: string) => {
|
||||||
|
if (required && (!value || value.trim() === '')) {
|
||||||
|
return 'Email cannot be empty';
|
||||||
|
}
|
||||||
|
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
||||||
|
return 'Invalid email format';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Textarea settings
|
||||||
|
else if (fieldType === 'textarea') {
|
||||||
|
minRows = 4;
|
||||||
|
maxRows = 8;
|
||||||
|
if (attr.name.toLowerCase().includes('content')) {
|
||||||
|
minRows = 6;
|
||||||
|
maxRows = 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Multiselect validation
|
||||||
|
else if (fieldType === 'multiselect' && required) {
|
||||||
|
validator = (value: any[]) => {
|
||||||
|
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||||
|
return `${attr.label} is required`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// String validation for required fields
|
||||||
|
else if (fieldType === 'string' && required) {
|
||||||
|
validator = (value: string) => {
|
||||||
|
if (!value || value.trim() === '') {
|
||||||
|
return `${attr.label} is required`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: attr.name,
|
||||||
|
label: attr.label || attr.name,
|
||||||
|
type: fieldType,
|
||||||
|
required,
|
||||||
|
validator,
|
||||||
|
minRows,
|
||||||
|
maxRows,
|
||||||
|
options,
|
||||||
|
optionsReference,
|
||||||
|
placeholder: (attr as any).placeholder
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return createFields;
|
||||||
|
}, [attributes]);
|
||||||
|
|
||||||
|
// Ensure attributes are loaded - can be called by EditActionButton
|
||||||
|
const ensureAttributesLoaded = useCallback(async () => {
|
||||||
|
// Don't fetch attributes if user is not authenticated (prevents 401 errors)
|
||||||
|
const currentUser = getUserDataCache();
|
||||||
|
if (!currentUser) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes && attributes.length > 0) {
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchedAttributes = await fetchAttributesData();
|
||||||
|
return fetchedAttributes;
|
||||||
|
}, [attributes, fetchAttributesData]);
|
||||||
|
|
||||||
|
// Fetch attributes and permissions on mount (only if user is authenticated)
|
||||||
|
useEffect(() => {
|
||||||
|
const currentUser = getUserDataCache();
|
||||||
|
if (currentUser) {
|
||||||
|
fetchAttributesData();
|
||||||
|
fetchPermissions();
|
||||||
|
}
|
||||||
|
}, [fetchAttributesData, fetchPermissions]);
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMandates();
|
||||||
|
}, [fetchMandates]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: mandates,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch: fetchMandates,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
pagination,
|
||||||
|
fetchMandateById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mandate operations hook
|
||||||
|
export function useMandateOperations() {
|
||||||
|
const [deletingMandates, setDeletingMandates] = useState<Set<string>>(new Set());
|
||||||
|
const [editingMandates, setEditingMandates] = useState<Set<string>>(new Set());
|
||||||
|
const [creatingMandate, setCreatingMandate] = useState(false);
|
||||||
|
const { request, isLoading } = useApiRequest();
|
||||||
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||||
|
const [createError, setCreateError] = useState<string | null>(null);
|
||||||
|
const [updateError, setUpdateError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleMandateDelete = async (mandateId: string) => {
|
||||||
|
setDeleteError(null);
|
||||||
|
setDeletingMandates(prev => new Set(prev).add(mandateId));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteMandateApi(request, mandateId);
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
setDeleteError(error.message);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setDeletingMandates(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(mandateId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMandateCreate = async (mandateData: Partial<Mandate>) => {
|
||||||
|
setCreateError(null);
|
||||||
|
setCreatingMandate(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newMandate = await createMandateApi(request, mandateData);
|
||||||
|
|
||||||
|
return { success: true, mandateData: newMandate };
|
||||||
|
} catch (error: any) {
|
||||||
|
setCreateError(error.message);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
} finally {
|
||||||
|
setCreatingMandate(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMandateUpdate = async (mandateId: string, updateData: MandateUpdateData, _originalData?: any) => {
|
||||||
|
setUpdateError(null);
|
||||||
|
setEditingMandates(prev => new Set(prev).add(mandateId));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedMandate = await updateMandateApi(request, mandateId, updateData);
|
||||||
|
|
||||||
|
return { success: true, mandateData: updatedMandate };
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMessage = error.response?.data?.message || error.message || 'Failed to update mandate';
|
||||||
|
const statusCode = error.response?.status;
|
||||||
|
|
||||||
|
setUpdateError(errorMessage);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
statusCode,
|
||||||
|
isPermissionError: statusCode === 403,
|
||||||
|
isValidationError: statusCode === 400
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
setEditingMandates(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(mandateId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletingMandates,
|
||||||
|
editingMandates,
|
||||||
|
creatingMandate,
|
||||||
|
deleteError,
|
||||||
|
createError,
|
||||||
|
updateError,
|
||||||
|
handleMandateDelete,
|
||||||
|
handleMandateCreate,
|
||||||
|
handleMandateUpdate,
|
||||||
|
isLoading
|
||||||
|
};
|
||||||
|
}
|
||||||
665
src/hooks/useAdminRbacRoles.ts
Normal file
665
src/hooks/useAdminRbacRoles.ts
Normal file
|
|
@ -0,0 +1,665 @@
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useApiRequest } from './useApi';
|
||||||
|
import { getUserDataCache } from '../utils/userCache';
|
||||||
|
import { usePermissions, type UserPermissions } from './usePermissions';
|
||||||
|
import {
|
||||||
|
fetchRoles as fetchRolesApi,
|
||||||
|
fetchRoleById as fetchRoleByIdApi,
|
||||||
|
createRole as createRoleApi,
|
||||||
|
updateRole as updateRoleApi,
|
||||||
|
deleteRole as deleteRoleApi,
|
||||||
|
type Role,
|
||||||
|
type RoleUpdateData,
|
||||||
|
type PaginationParams
|
||||||
|
} from '../api/roleApi';
|
||||||
|
import { fetchAttributes } from '../api/attributesApi';
|
||||||
|
import type { AttributeDefinition } from '../api/attributesApi';
|
||||||
|
import {
|
||||||
|
isCheckboxType,
|
||||||
|
isSelectType,
|
||||||
|
isMultiselectType,
|
||||||
|
isDateTimeType,
|
||||||
|
isTextareaType,
|
||||||
|
type AttributeType
|
||||||
|
} from '../utils/attributeTypeMapper';
|
||||||
|
|
||||||
|
// Re-export types for backward compatibility
|
||||||
|
export type { Role, RoleUpdateData, AttributeDefinition, PaginationParams };
|
||||||
|
|
||||||
|
// Helper function to detect TextMultilingual objects
|
||||||
|
// TextMultilingual has structure: { en: string, ge?: string, fr?: string, it?: string }
|
||||||
|
const isTextMultilingual = (value: any): boolean => {
|
||||||
|
if (!value || typeof value !== 'object' || Array.isArray(value) || value instanceof Date) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check if it has 'en' property (required) and optionally other language codes
|
||||||
|
return 'en' in value && typeof value.en === 'string';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to check if a field name suggests it's a multilingual field
|
||||||
|
// Only specific fields should be multilingual, not fields that just contain these words
|
||||||
|
const isMultilingualFieldName = (fieldName: string): boolean => {
|
||||||
|
const lowerFieldName = fieldName.toLowerCase();
|
||||||
|
|
||||||
|
// Exact matches for multilingual fields
|
||||||
|
const exactMultilingualFields = ['description'];
|
||||||
|
|
||||||
|
// Fields that end with these patterns (but not roleLabel, etc.)
|
||||||
|
const multilingualPatterns = [
|
||||||
|
/^description$/i,
|
||||||
|
/^label$/i, // Only exact "label", not "roleLabel"
|
||||||
|
/^title$/i, // Only exact "title"
|
||||||
|
/^name$/i // Only exact "name", not field names containing "name"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check exact matches first
|
||||||
|
if (exactMultilingualFields.includes(lowerFieldName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check patterns - but exclude fields like "roleLabel" which should be strings
|
||||||
|
const excludedFields = ['rolelabel', 'role_label', 'rolename', 'role_name', 'username', 'user_name'];
|
||||||
|
if (excludedFields.includes(lowerFieldName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it matches multilingual patterns (exact match, not contains)
|
||||||
|
return multilingualPatterns.some(pattern => pattern.test(fieldName));
|
||||||
|
};
|
||||||
|
|
||||||
|
// RBAC roles hook (list, update, delete) - following mandates pattern
|
||||||
|
export function useRbacRoles() {
|
||||||
|
const [roles, setRoles] = useState<Role[]>([]);
|
||||||
|
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
|
||||||
|
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
|
||||||
|
const [pagination, setPagination] = useState<{
|
||||||
|
currentPage: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
} | null>(null);
|
||||||
|
const { request, isLoading: loading, error } = useApiRequest<null, Role[]>();
|
||||||
|
const { checkPermission } = usePermissions();
|
||||||
|
|
||||||
|
// Fetch attributes from backend
|
||||||
|
const fetchAttributesData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const attrs = await fetchAttributes(request, 'Role');
|
||||||
|
setAttributes(attrs);
|
||||||
|
return attrs;
|
||||||
|
} catch (error: any) {
|
||||||
|
// Don't log 429 errors as errors (they're rate limit warnings)
|
||||||
|
if (error.response?.status === 429) {
|
||||||
|
console.warn('Rate limit exceeded while fetching role attributes. Please wait.');
|
||||||
|
} else if (error.response?.status !== 401) {
|
||||||
|
// Only log non-auth errors (401 is expected when not logged in)
|
||||||
|
console.error('Error fetching attributes:', error);
|
||||||
|
}
|
||||||
|
setAttributes([]);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
// Fetch permissions from backend
|
||||||
|
const fetchPermissions = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const perms = await checkPermission('DATA', 'Role');
|
||||||
|
setPermissions(perms);
|
||||||
|
return perms;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching permissions:', error);
|
||||||
|
const defaultPerms: UserPermissions = {
|
||||||
|
view: false,
|
||||||
|
read: 'n',
|
||||||
|
create: 'n',
|
||||||
|
update: 'n',
|
||||||
|
delete: 'n',
|
||||||
|
};
|
||||||
|
setPermissions(defaultPerms);
|
||||||
|
return defaultPerms;
|
||||||
|
}
|
||||||
|
}, [checkPermission]);
|
||||||
|
|
||||||
|
const fetchRoles = useCallback(async (params?: PaginationParams) => {
|
||||||
|
try {
|
||||||
|
const requestParams: any = {};
|
||||||
|
|
||||||
|
// Build pagination object if provided
|
||||||
|
if (params) {
|
||||||
|
const paginationObj: any = {};
|
||||||
|
|
||||||
|
if (params.page !== undefined) paginationObj.page = params.page;
|
||||||
|
if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
|
||||||
|
if (params.sort) paginationObj.sort = params.sort;
|
||||||
|
if (params.filters) paginationObj.filters = params.filters;
|
||||||
|
if (params.search) paginationObj.search = params.search;
|
||||||
|
|
||||||
|
if (Object.keys(paginationObj).length > 0) {
|
||||||
|
requestParams.pagination = JSON.stringify(paginationObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fetchRolesApi(request, params);
|
||||||
|
|
||||||
|
// Handle paginated response
|
||||||
|
if (data && typeof data === 'object' && 'items' in data) {
|
||||||
|
const items = Array.isArray(data.items) ? data.items : [];
|
||||||
|
setRoles(items);
|
||||||
|
if (data.pagination) {
|
||||||
|
setPagination(data.pagination);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle non-paginated response (backward compatibility)
|
||||||
|
const items = Array.isArray(data) ? data : [];
|
||||||
|
setRoles(items);
|
||||||
|
setPagination(null);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// Error is already handled by useApiRequest
|
||||||
|
setRoles([]);
|
||||||
|
setPagination(null);
|
||||||
|
}
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
// Optimistically remove a role from the local state
|
||||||
|
const removeOptimistically = (roleId: string) => {
|
||||||
|
setRoles(prevRoles => prevRoles.filter(role => role.id !== roleId));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optimistically update a role in the local state
|
||||||
|
const updateOptimistically = (roleId: string, updateData: Partial<Role>) => {
|
||||||
|
setRoles(prevRoles =>
|
||||||
|
prevRoles.map(role =>
|
||||||
|
role.id === roleId
|
||||||
|
? { ...role, ...updateData }
|
||||||
|
: role
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch a single role by ID
|
||||||
|
const fetchRoleById = useCallback(async (roleId: string): Promise<Role | null> => {
|
||||||
|
return await fetchRoleByIdApi(request, roleId);
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
// Generate edit fields from attributes dynamically using attributeTypeMapper utilities
|
||||||
|
const generateEditFieldsFromAttributes = useCallback((): Array<{
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly';
|
||||||
|
editable?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
validator?: (value: any) => string | null;
|
||||||
|
minRows?: number;
|
||||||
|
maxRows?: number;
|
||||||
|
options?: Array<{ value: string | number; label: string }>;
|
||||||
|
optionsReference?: string; // For options that need to be fetched (e.g., "user.role")
|
||||||
|
}> => {
|
||||||
|
if (!attributes || attributes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const editableFields = attributes
|
||||||
|
.filter(attr => {
|
||||||
|
// Filter out non-editable fields based on readonly/editable flags
|
||||||
|
if (attr.readonly === true || attr.editable === false) {
|
||||||
|
return false; // Don't show readonly fields in edit form
|
||||||
|
}
|
||||||
|
// Also filter out common non-editable fields
|
||||||
|
const nonEditableFields = ['id', 'roleId', '_createdBy', '_hideDelete'];
|
||||||
|
return !nonEditableFields.includes(attr.name);
|
||||||
|
})
|
||||||
|
.map(attr => {
|
||||||
|
// Map backend attribute type to form field type using attributeTypeMapper utilities
|
||||||
|
let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string';
|
||||||
|
let options: Array<{ value: string | number; label: string }> | undefined = undefined;
|
||||||
|
let optionsReference: string | undefined = undefined;
|
||||||
|
|
||||||
|
const attrType = attr.type as AttributeType;
|
||||||
|
|
||||||
|
// Use attributeTypeMapper utilities to determine field type
|
||||||
|
if (isCheckboxType(attrType)) {
|
||||||
|
fieldType = 'boolean';
|
||||||
|
} else if (attrType === 'email') {
|
||||||
|
fieldType = 'email';
|
||||||
|
} else if (isDateTimeType(attrType)) {
|
||||||
|
fieldType = 'date';
|
||||||
|
} else if (isSelectType(attrType)) {
|
||||||
|
fieldType = 'enum';
|
||||||
|
// Handle options - can be array or string reference
|
||||||
|
const attrOptions = (attr as any).options;
|
||||||
|
if (Array.isArray(attrOptions)) {
|
||||||
|
options = attrOptions.map((opt: any) => {
|
||||||
|
const labelValue = typeof opt.label === 'string'
|
||||||
|
? opt.label
|
||||||
|
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||||
|
return {
|
||||||
|
value: opt.value,
|
||||||
|
label: labelValue
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (typeof attrOptions === 'string') {
|
||||||
|
// Options reference (e.g., "user.role", "auth.authority")
|
||||||
|
optionsReference = attrOptions;
|
||||||
|
}
|
||||||
|
} else if (isMultiselectType(attrType)) {
|
||||||
|
fieldType = 'multiselect';
|
||||||
|
// Handle options - can be array or string reference
|
||||||
|
const attrOptions = (attr as any).options;
|
||||||
|
if (Array.isArray(attrOptions)) {
|
||||||
|
options = attrOptions.map((opt: any) => {
|
||||||
|
const labelValue = typeof opt.label === 'string'
|
||||||
|
? opt.label
|
||||||
|
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||||
|
return {
|
||||||
|
value: opt.value,
|
||||||
|
label: labelValue
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (typeof attrOptions === 'string') {
|
||||||
|
// Options reference (e.g., "user.role", "auth.authority")
|
||||||
|
optionsReference = attrOptions;
|
||||||
|
}
|
||||||
|
} else if (isTextareaType(attrType)) {
|
||||||
|
fieldType = 'textarea';
|
||||||
|
} else if (attrType === 'readonly') {
|
||||||
|
fieldType = 'readonly';
|
||||||
|
} else {
|
||||||
|
fieldType = 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define validators and required fields
|
||||||
|
let required = attr.required === true;
|
||||||
|
let validator: ((value: any) => string | null) | undefined = undefined;
|
||||||
|
let minRows: number | undefined = undefined;
|
||||||
|
let maxRows: number | undefined = undefined;
|
||||||
|
|
||||||
|
// Email validation
|
||||||
|
if (fieldType === 'email') {
|
||||||
|
validator = (value: string) => {
|
||||||
|
if (required && (!value || value.trim() === '')) {
|
||||||
|
return 'Email cannot be empty';
|
||||||
|
}
|
||||||
|
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
||||||
|
return 'Invalid email format';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Textarea settings
|
||||||
|
else if (fieldType === 'textarea') {
|
||||||
|
minRows = 4;
|
||||||
|
maxRows = 8;
|
||||||
|
if (attr.name.toLowerCase().includes('content')) {
|
||||||
|
minRows = 6;
|
||||||
|
maxRows = 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Multiselect validation
|
||||||
|
else if (fieldType === 'multiselect' && required) {
|
||||||
|
validator = (value: any[]) => {
|
||||||
|
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||||
|
return `${attr.label} is required`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: attr.name,
|
||||||
|
label: attr.label || attr.name,
|
||||||
|
type: fieldType,
|
||||||
|
editable: (attr as any).editable !== false && (attr as any).readonly !== true,
|
||||||
|
required,
|
||||||
|
validator,
|
||||||
|
minRows,
|
||||||
|
maxRows,
|
||||||
|
options,
|
||||||
|
optionsReference
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return editableFields;
|
||||||
|
}, [attributes]);
|
||||||
|
|
||||||
|
// Generate create fields from attributes dynamically using attributeTypeMapper utilities
|
||||||
|
const generateCreateFieldsFromAttributes = useCallback((): Array<{
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly';
|
||||||
|
required?: boolean;
|
||||||
|
validator?: (value: any) => string | null;
|
||||||
|
minRows?: number;
|
||||||
|
maxRows?: number;
|
||||||
|
options?: Array<{ value: string | number; label: string }>;
|
||||||
|
optionsReference?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
}> => {
|
||||||
|
if (!attributes || attributes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const createFields = attributes
|
||||||
|
.filter(attr => {
|
||||||
|
// Filter out non-editable fields and auto-generated fields for create forms
|
||||||
|
if (attr.readonly === true || attr.editable === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Filter out ID fields and other auto-generated fields
|
||||||
|
const nonEditableFields = ['id', 'roleId', '_createdBy', '_hideDelete'];
|
||||||
|
return !nonEditableFields.includes(attr.name);
|
||||||
|
})
|
||||||
|
.map(attr => {
|
||||||
|
// Map backend attribute type to form field type using attributeTypeMapper utilities
|
||||||
|
let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string';
|
||||||
|
let options: Array<{ value: string | number; label: string }> | undefined = undefined;
|
||||||
|
let optionsReference: string | undefined = undefined;
|
||||||
|
|
||||||
|
const attrType = attr.type as AttributeType;
|
||||||
|
|
||||||
|
// Use attributeTypeMapper utilities to determine field type
|
||||||
|
if (isCheckboxType(attrType)) {
|
||||||
|
fieldType = 'boolean';
|
||||||
|
} else if (attrType === 'email') {
|
||||||
|
fieldType = 'email';
|
||||||
|
} else if (isDateTimeType(attrType)) {
|
||||||
|
fieldType = 'date';
|
||||||
|
} else if (isSelectType(attrType)) {
|
||||||
|
fieldType = 'enum';
|
||||||
|
const attrOptions = (attr as any).options;
|
||||||
|
if (Array.isArray(attrOptions)) {
|
||||||
|
options = attrOptions.map((opt: any) => {
|
||||||
|
const labelValue = typeof opt.label === 'string'
|
||||||
|
? opt.label
|
||||||
|
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||||
|
return {
|
||||||
|
value: opt.value,
|
||||||
|
label: labelValue
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (typeof attrOptions === 'string') {
|
||||||
|
optionsReference = attrOptions;
|
||||||
|
}
|
||||||
|
} else if (isMultiselectType(attrType)) {
|
||||||
|
fieldType = 'multiselect';
|
||||||
|
const attrOptions = (attr as any).options;
|
||||||
|
if (Array.isArray(attrOptions)) {
|
||||||
|
options = attrOptions.map((opt: any) => {
|
||||||
|
const labelValue = typeof opt.label === 'string'
|
||||||
|
? opt.label
|
||||||
|
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||||
|
return {
|
||||||
|
value: opt.value,
|
||||||
|
label: labelValue
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (typeof attrOptions === 'string') {
|
||||||
|
optionsReference = attrOptions;
|
||||||
|
}
|
||||||
|
} else if (isTextareaType(attrType)) {
|
||||||
|
fieldType = 'textarea';
|
||||||
|
} else {
|
||||||
|
fieldType = 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define validators and required fields
|
||||||
|
let required = attr.required === true;
|
||||||
|
let validator: ((value: any) => string | null) | undefined = undefined;
|
||||||
|
let minRows: number | undefined = undefined;
|
||||||
|
let maxRows: number | undefined = undefined;
|
||||||
|
|
||||||
|
// Email validation
|
||||||
|
if (fieldType === 'email') {
|
||||||
|
validator = (value: string) => {
|
||||||
|
if (required && (!value || value.trim() === '')) {
|
||||||
|
return 'Email cannot be empty';
|
||||||
|
}
|
||||||
|
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
||||||
|
return 'Invalid email format';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Textarea settings
|
||||||
|
else if (fieldType === 'textarea') {
|
||||||
|
minRows = 4;
|
||||||
|
maxRows = 8;
|
||||||
|
if (attr.name.toLowerCase().includes('content')) {
|
||||||
|
minRows = 6;
|
||||||
|
maxRows = 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Multiselect validation
|
||||||
|
else if (fieldType === 'multiselect' && required) {
|
||||||
|
validator = (value: any[]) => {
|
||||||
|
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||||
|
return `${attr.label} is required`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// String validation for required fields
|
||||||
|
else if (fieldType === 'string' && required) {
|
||||||
|
validator = (value: any) => {
|
||||||
|
// Check if this is a multilingual field (TextMultilingual object)
|
||||||
|
if (isMultilingualFieldName(attr.name)) {
|
||||||
|
// Handle TextMultilingual object
|
||||||
|
if (isTextMultilingual(value)) {
|
||||||
|
if (!value.en || typeof value.en !== 'string' || value.en.trim() === '') {
|
||||||
|
return `${attr.label} (English) is required`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// If it's a multilingual field but value is not yet a TextMultilingual object,
|
||||||
|
// check if it's an empty object or needs initialization
|
||||||
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
// Empty object or object without 'en' property
|
||||||
|
if (!value.en || typeof value.en !== 'string' || value.en.trim() === '') {
|
||||||
|
return `${attr.label} (English) is required`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// If it's a string, that's also valid (will be converted to TextMultilingual)
|
||||||
|
if (typeof value === 'string' && value.trim() !== '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Empty or invalid value
|
||||||
|
return `${attr.label} (English) is required`;
|
||||||
|
}
|
||||||
|
// Regular string validation for non-multilingual fields
|
||||||
|
if (typeof value !== 'string' || !value || value.trim() === '') {
|
||||||
|
return `${attr.label} is required`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: attr.name,
|
||||||
|
label: attr.label || attr.name,
|
||||||
|
type: fieldType,
|
||||||
|
required,
|
||||||
|
validator,
|
||||||
|
minRows,
|
||||||
|
maxRows,
|
||||||
|
options,
|
||||||
|
optionsReference,
|
||||||
|
placeholder: (attr as any).placeholder
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return createFields;
|
||||||
|
}, [attributes]);
|
||||||
|
|
||||||
|
// Ensure attributes are loaded - can be called by EditActionButton
|
||||||
|
const ensureAttributesLoaded = useCallback(async () => {
|
||||||
|
// Don't fetch attributes if user is not authenticated (prevents 401 errors)
|
||||||
|
const currentUser = getUserDataCache();
|
||||||
|
if (!currentUser) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes && attributes.length > 0) {
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchedAttributes = await fetchAttributesData();
|
||||||
|
return fetchedAttributes;
|
||||||
|
}, [attributes, fetchAttributesData]);
|
||||||
|
|
||||||
|
// Fetch attributes and permissions on mount (only if user is authenticated)
|
||||||
|
useEffect(() => {
|
||||||
|
const currentUser = getUserDataCache();
|
||||||
|
if (currentUser) {
|
||||||
|
fetchAttributesData();
|
||||||
|
fetchPermissions();
|
||||||
|
}
|
||||||
|
}, [fetchAttributesData, fetchPermissions]);
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRoles();
|
||||||
|
}, [fetchRoles]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: roles,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch: fetchRoles,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
pagination,
|
||||||
|
fetchRoleById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role operations hook
|
||||||
|
export function useRbacRoleOperations() {
|
||||||
|
const [deletingRoles, setDeletingRoles] = useState<Set<string>>(new Set());
|
||||||
|
const [editingRoles, setEditingRoles] = useState<Set<string>>(new Set());
|
||||||
|
const [creatingRole, setCreatingRole] = useState(false);
|
||||||
|
const { request, isLoading } = useApiRequest();
|
||||||
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||||
|
const [createError, setCreateError] = useState<string | null>(null);
|
||||||
|
const [updateError, setUpdateError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleRoleDelete = async (roleId: string) => {
|
||||||
|
setDeleteError(null);
|
||||||
|
setDeletingRoles(prev => new Set(prev).add(roleId));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteRoleApi(request, roleId);
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
setDeleteError(error.message);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setDeletingRoles(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(roleId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRoleCreate = async (roleData: Partial<Role>) => {
|
||||||
|
console.log('🟢 handleRoleCreate - Complete input structure:', {
|
||||||
|
roleData,
|
||||||
|
roleDataType: typeof roleData,
|
||||||
|
roleDataIsArray: Array.isArray(roleData),
|
||||||
|
roleDataIsNull: roleData === null,
|
||||||
|
roleDataIsUndefined: roleData === undefined,
|
||||||
|
roleDataKeys: roleData ? Object.keys(roleData) : [],
|
||||||
|
roleDataEntries: roleData ? Object.entries(roleData).map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
valueType: typeof value,
|
||||||
|
valueIsObject: typeof value === 'object' && value !== null && !Array.isArray(value),
|
||||||
|
valueIsArray: Array.isArray(value),
|
||||||
|
valueIsNull: value === null,
|
||||||
|
valueIsUndefined: value === undefined,
|
||||||
|
valueStringified: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)
|
||||||
|
})) : [],
|
||||||
|
roleDataStringified: JSON.stringify(roleData, null, 2),
|
||||||
|
roleDataStringifiedCompact: JSON.stringify(roleData)
|
||||||
|
});
|
||||||
|
|
||||||
|
setCreateError(null);
|
||||||
|
setCreatingRole(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newRole = await createRoleApi(request, roleData);
|
||||||
|
console.log('✅ handleRoleCreate - Success, newRole:', newRole);
|
||||||
|
|
||||||
|
return { success: true, roleData: newRole };
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ handleRoleCreate - Error:', error);
|
||||||
|
console.error('❌ handleRoleCreate - Complete error details:', {
|
||||||
|
error,
|
||||||
|
message: error.message,
|
||||||
|
response: error.response,
|
||||||
|
responseData: error.response?.data,
|
||||||
|
responseStatus: error.response?.status,
|
||||||
|
responseStatusText: error.response?.statusText,
|
||||||
|
responseHeaders: error.response?.headers,
|
||||||
|
request: error.request,
|
||||||
|
config: error.config,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
setCreateError(error.message);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
} finally {
|
||||||
|
setCreatingRole(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRoleUpdate = async (roleId: string, updateData: RoleUpdateData, _originalData?: any) => {
|
||||||
|
setUpdateError(null);
|
||||||
|
setEditingRoles(prev => new Set(prev).add(roleId));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedRole = await updateRoleApi(request, roleId, updateData);
|
||||||
|
|
||||||
|
return { success: true, roleData: updatedRole };
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMessage = error.response?.data?.message || error.message || 'Failed to update role';
|
||||||
|
const statusCode = error.response?.status;
|
||||||
|
|
||||||
|
setUpdateError(errorMessage);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
statusCode,
|
||||||
|
isPermissionError: statusCode === 403,
|
||||||
|
isValidationError: statusCode === 400
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
setEditingRoles(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(roleId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletingRoles,
|
||||||
|
editingRoles,
|
||||||
|
creatingRole,
|
||||||
|
deleteError,
|
||||||
|
createError,
|
||||||
|
updateError,
|
||||||
|
handleRoleDelete,
|
||||||
|
handleRoleCreate,
|
||||||
|
handleRoleUpdate,
|
||||||
|
isLoading
|
||||||
|
};
|
||||||
|
}
|
||||||
580
src/hooks/useAdminRbacRules.ts
Normal file
580
src/hooks/useAdminRbacRules.ts
Normal file
|
|
@ -0,0 +1,580 @@
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useApiRequest } from './useApi';
|
||||||
|
import { getUserDataCache } from '../utils/userCache';
|
||||||
|
import { usePermissions, type UserPermissions } from './usePermissions';
|
||||||
|
import {
|
||||||
|
fetchRbacRules as fetchRbacRulesApi,
|
||||||
|
fetchRbacRuleById as fetchRbacRuleByIdApi,
|
||||||
|
createRbacRule as createRbacRuleApi,
|
||||||
|
updateRbacRule as updateRbacRuleApi,
|
||||||
|
deleteRbacRule as deleteRbacRuleApi,
|
||||||
|
type RbacRule,
|
||||||
|
type RbacRuleUpdateData,
|
||||||
|
type PaginationParams
|
||||||
|
} from '../api/rbacRulesApi';
|
||||||
|
import { fetchAttributes } from '../api/attributesApi';
|
||||||
|
import type { AttributeDefinition } from '../api/attributesApi';
|
||||||
|
import {
|
||||||
|
isCheckboxType,
|
||||||
|
isSelectType,
|
||||||
|
isMultiselectType,
|
||||||
|
isDateTimeType,
|
||||||
|
isTextareaType,
|
||||||
|
type AttributeType
|
||||||
|
} from '../utils/attributeTypeMapper';
|
||||||
|
|
||||||
|
// Re-export types for backward compatibility
|
||||||
|
export type { RbacRule, RbacRuleUpdateData, AttributeDefinition, PaginationParams };
|
||||||
|
|
||||||
|
// RBAC rules hook (list, update, delete) - following mandates pattern
|
||||||
|
export function useRbacRules() {
|
||||||
|
const [rbacRules, setRbacRules] = useState<RbacRule[]>([]);
|
||||||
|
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
|
||||||
|
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
|
||||||
|
const [pagination, setPagination] = useState<{
|
||||||
|
currentPage: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
} | null>(null);
|
||||||
|
const { request, isLoading: loading, error } = useApiRequest<null, RbacRule[]>();
|
||||||
|
const { checkPermission } = usePermissions();
|
||||||
|
|
||||||
|
// Fetch attributes from backend
|
||||||
|
const fetchAttributesData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const attrs = await fetchAttributes(request, 'AccessRule');
|
||||||
|
setAttributes(attrs);
|
||||||
|
return attrs;
|
||||||
|
} catch (error: any) {
|
||||||
|
// Don't log 429 errors as errors (they're rate limit warnings)
|
||||||
|
if (error.response?.status === 429) {
|
||||||
|
console.warn('Rate limit exceeded while fetching RBAC rule attributes. Please wait.');
|
||||||
|
} else if (error.response?.status !== 401) {
|
||||||
|
// Only log non-auth errors (401 is expected when not logged in)
|
||||||
|
console.error('Error fetching attributes:', error);
|
||||||
|
}
|
||||||
|
setAttributes([]);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
// Fetch permissions from backend
|
||||||
|
const fetchPermissions = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const perms = await checkPermission('DATA', 'RbacRule');
|
||||||
|
setPermissions(perms);
|
||||||
|
return perms;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching permissions:', error);
|
||||||
|
const defaultPerms: UserPermissions = {
|
||||||
|
view: false,
|
||||||
|
read: 'n',
|
||||||
|
create: 'n',
|
||||||
|
update: 'n',
|
||||||
|
delete: 'n',
|
||||||
|
};
|
||||||
|
setPermissions(defaultPerms);
|
||||||
|
return defaultPerms;
|
||||||
|
}
|
||||||
|
}, [checkPermission]);
|
||||||
|
|
||||||
|
const fetchRbacRules = useCallback(async (params?: PaginationParams) => {
|
||||||
|
try {
|
||||||
|
const requestParams: any = {};
|
||||||
|
|
||||||
|
// Build pagination object if provided
|
||||||
|
if (params) {
|
||||||
|
const paginationObj: any = {};
|
||||||
|
|
||||||
|
if (params.page !== undefined) paginationObj.page = params.page;
|
||||||
|
if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
|
||||||
|
if (params.sort) paginationObj.sort = params.sort;
|
||||||
|
if (params.filters) paginationObj.filters = params.filters;
|
||||||
|
if (params.search) paginationObj.search = params.search;
|
||||||
|
|
||||||
|
if (Object.keys(paginationObj).length > 0) {
|
||||||
|
requestParams.pagination = JSON.stringify(paginationObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fetchRbacRulesApi(request, params);
|
||||||
|
|
||||||
|
// Debug logging for pagination
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('📊 useRbacRules: Backend response:', {
|
||||||
|
hasItems: data && typeof data === 'object' && 'items' in data,
|
||||||
|
itemsCount: data && typeof data === 'object' && 'items' in data ? (data.items as any[]).length : 'N/A',
|
||||||
|
pagination: data && typeof data === 'object' && 'pagination' in data ? data.pagination : 'N/A',
|
||||||
|
fullData: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle paginated response
|
||||||
|
if (data && typeof data === 'object' && 'items' in data) {
|
||||||
|
const items = Array.isArray(data.items) ? data.items : [];
|
||||||
|
setRbacRules(items);
|
||||||
|
if (data.pagination) {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('📊 useRbacRules: Setting pagination:', data.pagination);
|
||||||
|
}
|
||||||
|
setPagination(data.pagination);
|
||||||
|
} else {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.warn('⚠️ useRbacRules: No pagination object in response');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle non-paginated response (backward compatibility)
|
||||||
|
const items = Array.isArray(data) ? data : [];
|
||||||
|
setRbacRules(items);
|
||||||
|
setPagination(null);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// Error is already handled by useApiRequest
|
||||||
|
setRbacRules([]);
|
||||||
|
setPagination(null);
|
||||||
|
}
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
// Optimistically remove a RBAC rule from the local state
|
||||||
|
const removeOptimistically = (ruleId: string) => {
|
||||||
|
setRbacRules(prevRules => prevRules.filter(rule => rule.id !== ruleId));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optimistically update a RBAC rule in the local state
|
||||||
|
const updateOptimistically = (ruleId: string, updateData: Partial<RbacRule>) => {
|
||||||
|
setRbacRules(prevRules =>
|
||||||
|
prevRules.map(rule =>
|
||||||
|
rule.id === ruleId
|
||||||
|
? { ...rule, ...updateData }
|
||||||
|
: rule
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch a single RBAC rule by ID
|
||||||
|
const fetchRbacRuleById = useCallback(async (ruleId: string): Promise<RbacRule | null> => {
|
||||||
|
return await fetchRbacRuleByIdApi(request, ruleId);
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
// Generate edit fields from attributes dynamically using attributeTypeMapper utilities
|
||||||
|
const generateEditFieldsFromAttributes = useCallback((): Array<{
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly';
|
||||||
|
editable?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
validator?: (value: any) => string | null;
|
||||||
|
minRows?: number;
|
||||||
|
maxRows?: number;
|
||||||
|
options?: Array<{ value: string | number; label: string }>;
|
||||||
|
optionsReference?: string; // For options that need to be fetched (e.g., "user.role")
|
||||||
|
}> => {
|
||||||
|
if (!attributes || attributes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const editableFields = attributes
|
||||||
|
.filter(attr => {
|
||||||
|
// Filter out non-editable fields based on readonly/editable flags
|
||||||
|
if (attr.readonly === true || attr.editable === false) {
|
||||||
|
return false; // Don't show readonly fields in edit form
|
||||||
|
}
|
||||||
|
// Also filter out common non-editable fields
|
||||||
|
const nonEditableFields = ['id', 'ruleId', '_createdBy', '_hideDelete'];
|
||||||
|
return !nonEditableFields.includes(attr.name);
|
||||||
|
})
|
||||||
|
.map(attr => {
|
||||||
|
// Map backend attribute type to form field type using attributeTypeMapper utilities
|
||||||
|
let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string';
|
||||||
|
let options: Array<{ value: string | number; label: string }> | undefined = undefined;
|
||||||
|
let optionsReference: string | undefined = undefined;
|
||||||
|
|
||||||
|
const attrType = attr.type as AttributeType;
|
||||||
|
|
||||||
|
// Use attributeTypeMapper utilities to determine field type
|
||||||
|
if (isCheckboxType(attrType)) {
|
||||||
|
fieldType = 'boolean';
|
||||||
|
} else if (attrType === 'email') {
|
||||||
|
fieldType = 'email';
|
||||||
|
} else if (isDateTimeType(attrType)) {
|
||||||
|
fieldType = 'date';
|
||||||
|
} else if (isSelectType(attrType)) {
|
||||||
|
fieldType = 'enum';
|
||||||
|
// Handle options - can be array or string reference
|
||||||
|
const attrOptions = (attr as any).options;
|
||||||
|
if (Array.isArray(attrOptions)) {
|
||||||
|
options = attrOptions.map((opt: any) => {
|
||||||
|
const labelValue = typeof opt.label === 'string'
|
||||||
|
? opt.label
|
||||||
|
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||||
|
return {
|
||||||
|
value: opt.value,
|
||||||
|
label: labelValue
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (typeof attrOptions === 'string') {
|
||||||
|
// Options reference (e.g., "user.role", "auth.authority")
|
||||||
|
optionsReference = attrOptions;
|
||||||
|
}
|
||||||
|
} else if (isMultiselectType(attrType)) {
|
||||||
|
fieldType = 'multiselect';
|
||||||
|
// Handle options - can be array or string reference
|
||||||
|
const attrOptions = (attr as any).options;
|
||||||
|
if (Array.isArray(attrOptions)) {
|
||||||
|
options = attrOptions.map((opt: any) => {
|
||||||
|
const labelValue = typeof opt.label === 'string'
|
||||||
|
? opt.label
|
||||||
|
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||||
|
return {
|
||||||
|
value: opt.value,
|
||||||
|
label: labelValue
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (typeof attrOptions === 'string') {
|
||||||
|
// Options reference (e.g., "user.role", "auth.authority")
|
||||||
|
optionsReference = attrOptions;
|
||||||
|
}
|
||||||
|
} else if (isTextareaType(attrType)) {
|
||||||
|
fieldType = 'textarea';
|
||||||
|
} else if (attrType === 'readonly') {
|
||||||
|
fieldType = 'readonly';
|
||||||
|
} else {
|
||||||
|
fieldType = 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define validators and required fields
|
||||||
|
let required = attr.required === true;
|
||||||
|
let validator: ((value: any) => string | null) | undefined = undefined;
|
||||||
|
let minRows: number | undefined = undefined;
|
||||||
|
let maxRows: number | undefined = undefined;
|
||||||
|
|
||||||
|
// Email validation
|
||||||
|
if (fieldType === 'email') {
|
||||||
|
validator = (value: string) => {
|
||||||
|
if (required && (!value || value.trim() === '')) {
|
||||||
|
return 'Email cannot be empty';
|
||||||
|
}
|
||||||
|
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
||||||
|
return 'Invalid email format';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Textarea settings
|
||||||
|
else if (fieldType === 'textarea') {
|
||||||
|
minRows = 4;
|
||||||
|
maxRows = 8;
|
||||||
|
if (attr.name.toLowerCase().includes('content')) {
|
||||||
|
minRows = 6;
|
||||||
|
maxRows = 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Multiselect validation
|
||||||
|
else if (fieldType === 'multiselect' && required) {
|
||||||
|
validator = (value: any[]) => {
|
||||||
|
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||||
|
return `${attr.label} is required`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: attr.name,
|
||||||
|
label: attr.label || attr.name,
|
||||||
|
type: fieldType,
|
||||||
|
editable: (attr as any).editable !== false && (attr as any).readonly !== true,
|
||||||
|
required,
|
||||||
|
validator,
|
||||||
|
minRows,
|
||||||
|
maxRows,
|
||||||
|
options,
|
||||||
|
optionsReference
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return editableFields;
|
||||||
|
}, [attributes]);
|
||||||
|
|
||||||
|
// Generate create fields from attributes dynamically using attributeTypeMapper utilities
|
||||||
|
const generateCreateFieldsFromAttributes = useCallback((): Array<{
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly';
|
||||||
|
required?: boolean;
|
||||||
|
validator?: (value: any) => string | null;
|
||||||
|
minRows?: number;
|
||||||
|
maxRows?: number;
|
||||||
|
options?: Array<{ value: string | number; label: string }>;
|
||||||
|
optionsReference?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
}> => {
|
||||||
|
if (!attributes || attributes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const createFields = attributes
|
||||||
|
.filter(attr => {
|
||||||
|
// Filter out non-editable fields and auto-generated fields for create forms
|
||||||
|
if (attr.readonly === true || attr.editable === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Filter out ID fields and other auto-generated fields
|
||||||
|
const nonEditableFields = ['id', 'ruleId', '_createdBy', '_hideDelete'];
|
||||||
|
return !nonEditableFields.includes(attr.name);
|
||||||
|
})
|
||||||
|
.map(attr => {
|
||||||
|
// Map backend attribute type to form field type using attributeTypeMapper utilities
|
||||||
|
let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string';
|
||||||
|
let options: Array<{ value: string | number; label: string }> | undefined = undefined;
|
||||||
|
let optionsReference: string | undefined = undefined;
|
||||||
|
|
||||||
|
const attrType = attr.type as AttributeType;
|
||||||
|
|
||||||
|
// Use attributeTypeMapper utilities to determine field type
|
||||||
|
if (isCheckboxType(attrType)) {
|
||||||
|
fieldType = 'boolean';
|
||||||
|
} else if (attrType === 'email') {
|
||||||
|
fieldType = 'email';
|
||||||
|
} else if (isDateTimeType(attrType)) {
|
||||||
|
fieldType = 'date';
|
||||||
|
} else if (isSelectType(attrType)) {
|
||||||
|
fieldType = 'enum';
|
||||||
|
const attrOptions = (attr as any).options;
|
||||||
|
if (Array.isArray(attrOptions)) {
|
||||||
|
options = attrOptions.map((opt: any) => {
|
||||||
|
const labelValue = typeof opt.label === 'string'
|
||||||
|
? opt.label
|
||||||
|
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||||
|
return {
|
||||||
|
value: opt.value,
|
||||||
|
label: labelValue
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (typeof attrOptions === 'string') {
|
||||||
|
optionsReference = attrOptions;
|
||||||
|
}
|
||||||
|
} else if (isMultiselectType(attrType)) {
|
||||||
|
fieldType = 'multiselect';
|
||||||
|
const attrOptions = (attr as any).options;
|
||||||
|
if (Array.isArray(attrOptions)) {
|
||||||
|
options = attrOptions.map((opt: any) => {
|
||||||
|
const labelValue = typeof opt.label === 'string'
|
||||||
|
? opt.label
|
||||||
|
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||||
|
return {
|
||||||
|
value: opt.value,
|
||||||
|
label: labelValue
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (typeof attrOptions === 'string') {
|
||||||
|
optionsReference = attrOptions;
|
||||||
|
}
|
||||||
|
} else if (isTextareaType(attrType)) {
|
||||||
|
fieldType = 'textarea';
|
||||||
|
} else {
|
||||||
|
fieldType = 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define validators and required fields
|
||||||
|
let required = attr.required === true;
|
||||||
|
let validator: ((value: any) => string | null) | undefined = undefined;
|
||||||
|
let minRows: number | undefined = undefined;
|
||||||
|
let maxRows: number | undefined = undefined;
|
||||||
|
|
||||||
|
// Email validation
|
||||||
|
if (fieldType === 'email') {
|
||||||
|
validator = (value: string) => {
|
||||||
|
if (required && (!value || value.trim() === '')) {
|
||||||
|
return 'Email cannot be empty';
|
||||||
|
}
|
||||||
|
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
||||||
|
return 'Invalid email format';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Textarea settings
|
||||||
|
else if (fieldType === 'textarea') {
|
||||||
|
minRows = 4;
|
||||||
|
maxRows = 8;
|
||||||
|
if (attr.name.toLowerCase().includes('content')) {
|
||||||
|
minRows = 6;
|
||||||
|
maxRows = 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Multiselect validation
|
||||||
|
else if (fieldType === 'multiselect' && required) {
|
||||||
|
validator = (value: any[]) => {
|
||||||
|
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||||
|
return `${attr.label} is required`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// String validation for required fields
|
||||||
|
else if (fieldType === 'string' && required) {
|
||||||
|
validator = (value: string) => {
|
||||||
|
if (!value || value.trim() === '') {
|
||||||
|
return `${attr.label} is required`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: attr.name,
|
||||||
|
label: attr.label || attr.name,
|
||||||
|
type: fieldType,
|
||||||
|
required,
|
||||||
|
validator,
|
||||||
|
minRows,
|
||||||
|
maxRows,
|
||||||
|
options,
|
||||||
|
optionsReference,
|
||||||
|
placeholder: (attr as any).placeholder
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return createFields;
|
||||||
|
}, [attributes]);
|
||||||
|
|
||||||
|
// Ensure attributes are loaded - can be called by EditActionButton
|
||||||
|
const ensureAttributesLoaded = useCallback(async () => {
|
||||||
|
// Don't fetch attributes if user is not authenticated (prevents 401 errors)
|
||||||
|
const currentUser = getUserDataCache();
|
||||||
|
if (!currentUser) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes && attributes.length > 0) {
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchedAttributes = await fetchAttributesData();
|
||||||
|
return fetchedAttributes;
|
||||||
|
}, [attributes, fetchAttributesData]);
|
||||||
|
|
||||||
|
// Fetch attributes and permissions on mount (only if user is authenticated)
|
||||||
|
useEffect(() => {
|
||||||
|
const currentUser = getUserDataCache();
|
||||||
|
if (currentUser) {
|
||||||
|
fetchAttributesData();
|
||||||
|
fetchPermissions();
|
||||||
|
}
|
||||||
|
}, [fetchAttributesData, fetchPermissions]);
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRbacRules();
|
||||||
|
}, [fetchRbacRules]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: rbacRules,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch: fetchRbacRules,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
pagination,
|
||||||
|
fetchRbacRuleById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// RBAC rule operations hook
|
||||||
|
export function useRbacRuleOperations() {
|
||||||
|
const [deletingRbacRules, setDeletingRbacRules] = useState<Set<string>>(new Set());
|
||||||
|
const [editingRbacRules, setEditingRbacRules] = useState<Set<string>>(new Set());
|
||||||
|
const [creatingRbacRule, setCreatingRbacRule] = useState(false);
|
||||||
|
const { request, isLoading } = useApiRequest();
|
||||||
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||||
|
const [createError, setCreateError] = useState<string | null>(null);
|
||||||
|
const [updateError, setUpdateError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleRbacRuleDelete = async (ruleId: string) => {
|
||||||
|
setDeleteError(null);
|
||||||
|
setDeletingRbacRules(prev => new Set(prev).add(ruleId));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteRbacRuleApi(request, ruleId);
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
setDeleteError(error.message);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setDeletingRbacRules(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(ruleId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRbacRuleCreate = async (ruleData: Partial<RbacRule>) => {
|
||||||
|
setCreateError(null);
|
||||||
|
setCreatingRbacRule(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newRule = await createRbacRuleApi(request, ruleData);
|
||||||
|
|
||||||
|
return { success: true, ruleData: newRule };
|
||||||
|
} catch (error: any) {
|
||||||
|
setCreateError(error.message);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
} finally {
|
||||||
|
setCreatingRbacRule(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRbacRuleUpdate = async (ruleId: string, updateData: RbacRuleUpdateData, _originalData?: any) => {
|
||||||
|
setUpdateError(null);
|
||||||
|
setEditingRbacRules(prev => new Set(prev).add(ruleId));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedRule = await updateRbacRuleApi(request, ruleId, updateData);
|
||||||
|
|
||||||
|
return { success: true, ruleData: updatedRule };
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMessage = error.response?.data?.message || error.message || 'Failed to update RBAC rule';
|
||||||
|
const statusCode = error.response?.status;
|
||||||
|
|
||||||
|
setUpdateError(errorMessage);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
statusCode,
|
||||||
|
isPermissionError: statusCode === 403,
|
||||||
|
isValidationError: statusCode === 400
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
setEditingRbacRules(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(ruleId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletingRbacRules,
|
||||||
|
editingRbacRules,
|
||||||
|
creatingRbacRule,
|
||||||
|
deleteError,
|
||||||
|
createError,
|
||||||
|
updateError,
|
||||||
|
handleRbacRuleDelete,
|
||||||
|
handleRbacRuleCreate,
|
||||||
|
handleRbacRuleUpdate,
|
||||||
|
isLoading
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -81,7 +81,33 @@ export function useApiRequest<RequestData = any, ResponseData = any>() {
|
||||||
return await requestCache.get(cacheKey)!;
|
return await requestCache.get(cacheKey)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🔧 useApiRequest: Making request', { url, method, hasData: !!data, hasParams: !!params, cacheKey });
|
console.log('🔧 useApiRequest: Making request', {
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
hasData: !!data,
|
||||||
|
hasParams: !!params,
|
||||||
|
cacheKey,
|
||||||
|
dataStructure: data ? {
|
||||||
|
data,
|
||||||
|
dataType: typeof data,
|
||||||
|
dataKeys: Object.keys(data),
|
||||||
|
dataEntries: Object.entries(data).map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
valueType: typeof value,
|
||||||
|
valueIsObject: typeof value === 'object' && value !== null && !Array.isArray(value),
|
||||||
|
valueStringified: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)
|
||||||
|
})),
|
||||||
|
dataStringified: JSON.stringify(data, null, 2)
|
||||||
|
} : null,
|
||||||
|
fullRequestConfig: {
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
data,
|
||||||
|
params,
|
||||||
|
...additionalConfig
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Create the request promise
|
// Create the request promise
|
||||||
const requestPromise = api({
|
const requestPromise = api({
|
||||||
|
|
|
||||||
|
|
@ -1,784 +0,0 @@
|
||||||
export default {
|
|
||||||
// Navigation
|
|
||||||
'nav.dashboard': 'Zentrale',
|
|
||||||
'nav.files': 'Dateien',
|
|
||||||
'nav.team': 'Team-Bereich',
|
|
||||||
'nav.connections': 'Verbindungen',
|
|
||||||
'nav.workflows': 'Workflows',
|
|
||||||
'nav.settings': 'Einstellungen',
|
|
||||||
'nav.testSharepoint': 'SharePoint Test',
|
|
||||||
'nav.speech': 'Sprache',
|
|
||||||
'nav.transcript_management': 'Transkriptverwaltung',
|
|
||||||
|
|
||||||
// Settings page
|
|
||||||
'settings.title': 'Einstellungen',
|
|
||||||
'settings.appearance': 'Darstellung',
|
|
||||||
'settings.language': 'Sprache',
|
|
||||||
'settings.about': 'Über',
|
|
||||||
'settings.version': 'Version',
|
|
||||||
'settings.theme': 'Theme',
|
|
||||||
'settings.theme.description': 'Wechseln Sie zwischen hellem und dunklem Modus',
|
|
||||||
'settings.language.description': 'Wählen Sie Ihre bevorzugte Sprache',
|
|
||||||
'settings.theme.light': 'Hell',
|
|
||||||
'settings.theme.dark': 'Dunkel',
|
|
||||||
'settings.theme.toggle.light': 'Zu hellem Modus wechseln',
|
|
||||||
'settings.theme.toggle.dark': 'Zu dunklem Modus wechseln',
|
|
||||||
'settings.userinfo': 'Benutzerinformationen',
|
|
||||||
'settings.userinfo.description': 'Verwalten Sie Ihre Kontoinformationen',
|
|
||||||
'settings.userinfo.username': 'Benutzername',
|
|
||||||
'settings.userinfo.fullname': 'Vollständiger Name',
|
|
||||||
'settings.userinfo.email': 'E-Mail-Adresse',
|
|
||||||
'settings.userinfo.phone_name': 'Rufname am Telefon',
|
|
||||||
'settings.userinfo.phone_name.description': 'Wie möchten Sie am Telefon genannt werden?',
|
|
||||||
'settings.userinfo.language': 'Sprache',
|
|
||||||
'settings.userinfo.privilege': 'Berechtigungsstufe',
|
|
||||||
'settings.userinfo.enabled': 'Kontostatus',
|
|
||||||
'settings.userinfo.auth_authority': 'Authentifizierungsanbieter',
|
|
||||||
'settings.userinfo.enabled.true': 'Aktiv',
|
|
||||||
'settings.userinfo.enabled.false': 'Inaktiv',
|
|
||||||
'settings.userinfo.loading': 'Benutzerinformationen werden geladen...',
|
|
||||||
'settings.userinfo.error': 'Fehler beim Laden der Benutzerinformationen',
|
|
||||||
'settings.userinfo.save': 'Änderungen speichern',
|
|
||||||
'settings.userinfo.saving': 'Speichern...',
|
|
||||||
'settings.userinfo.success': 'Benutzerinformationen erfolgreich aktualisiert',
|
|
||||||
'settings.userinfo.update_error': 'Fehler beim Aktualisieren der Benutzerinformationen',
|
|
||||||
'settings.userinfo.managed_by': 'Verwaltet von {provider}',
|
|
||||||
'settings.userinfo.managed_note': 'Dieses Feld wird von {provider} verwaltet und kann nicht geändert werden',
|
|
||||||
|
|
||||||
// Languages
|
|
||||||
'language.german': 'Deutsch',
|
|
||||||
'language.english': 'English',
|
|
||||||
'language.french': 'Français',
|
|
||||||
|
|
||||||
// Common
|
|
||||||
'common.loading': 'Laden...',
|
|
||||||
'common.error': 'Fehler',
|
|
||||||
'common.success': 'Erfolgreich',
|
|
||||||
'common.cancel': 'Abbrechen',
|
|
||||||
'common.save': 'Speichern',
|
|
||||||
'common.delete': 'Löschen',
|
|
||||||
'common.edit': 'Bearbeiten',
|
|
||||||
'common.close': 'Schließen',
|
|
||||||
'common.retry': 'Wiederholen',
|
|
||||||
'common.create': 'Erstellen',
|
|
||||||
'common.creating': 'Erstellen...',
|
|
||||||
|
|
||||||
// Auth
|
|
||||||
'auth.login': 'Anmelden',
|
|
||||||
'auth.register': 'Registrieren',
|
|
||||||
'auth.logout': 'Abmelden',
|
|
||||||
'auth.email': 'E-Mail',
|
|
||||||
'auth.password': 'Passwort',
|
|
||||||
|
|
||||||
// Dashboard
|
|
||||||
'dashboard.prompt.template': 'Prompt Vorlage',
|
|
||||||
'dashboard.prompt.settings': 'Einstellungen',
|
|
||||||
'dashboard.chat.area': 'Chatbereich',
|
|
||||||
'dashboard.chat.history': 'Workflow-Verlauf',
|
|
||||||
'dashboard.log.title': 'Log',
|
|
||||||
'dashboard.log.workflow': 'Workflow',
|
|
||||||
'dashboard.log.no_workflow': 'Kein Workflow ausgewählt',
|
|
||||||
'dashboard.log.loading': 'Logs werden geladen...',
|
|
||||||
'dashboard.log.error': 'Fehler beim Laden der Logs',
|
|
||||||
'dashboard.log.no_logs': 'Keine Logs für diesen Workflow verfügbar',
|
|
||||||
'dashboard.log.waiting': 'Workflow läuft... Warte auf Logs...',
|
|
||||||
'dashboard.log.fetch_failed': 'Logs konnten nicht geladen werden',
|
|
||||||
'dashboard.log.level.info': 'INFO',
|
|
||||||
'dashboard.workflow_dropdown.loading': 'Laden...',
|
|
||||||
'dashboard.workflow_dropdown.error': 'Fehler',
|
|
||||||
'dashboard.workflow_dropdown.select_workflow': 'Workflow auswählen',
|
|
||||||
'dashboard.workflow_dropdown.available_workflows': 'Verfügbare Workflows',
|
|
||||||
'dashboard.workflow_dropdown.no_workflows': 'Keine Workflows verfügbar',
|
|
||||||
|
|
||||||
// Workflow Stats
|
|
||||||
'dashboard.stats.workflow': 'Workflow',
|
|
||||||
'dashboard.stats.status': 'Status',
|
|
||||||
'dashboard.stats.rounds': 'Runden',
|
|
||||||
'dashboard.stats.messages': 'Nachrichten',
|
|
||||||
'dashboard.stats.files': 'Dateien',
|
|
||||||
'dashboard.stats.tokens': 'Token',
|
|
||||||
'dashboard.stats.data_sent': 'Daten gesendet',
|
|
||||||
'dashboard.stats.data_received': 'Daten empfangen',
|
|
||||||
'dashboard.stats.success_rate': 'Erfolgsrate',
|
|
||||||
'dashboard.stats.errors': 'Fehler',
|
|
||||||
'dashboard.stats.started': 'Gestartet',
|
|
||||||
|
|
||||||
// Prompt Set
|
|
||||||
'promptset.loading': 'Prompts werden geladen...',
|
|
||||||
'promptset.error.loading': 'Fehler beim Laden der Prompts',
|
|
||||||
'promptset.retry': 'Erneut versuchen',
|
|
||||||
'promptset.new_prompt': 'Neuer Prompt',
|
|
||||||
'promptset.prompt_count': 'Prompt',
|
|
||||||
'promptset.prompt_count_plural': 'Prompts',
|
|
||||||
'promptset.no_prompts': 'Keine Prompts verfügbar',
|
|
||||||
'promptset.created': 'Erstellt',
|
|
||||||
'promptset.run_tooltip': 'Prompt ausführen',
|
|
||||||
'promptset.share_tooltip': 'Prompt teilen',
|
|
||||||
'promptset.delete_tooltip': 'Prompt löschen',
|
|
||||||
'promptset.confirm_delete': 'Klicken Sie erneut zum Bestätigen',
|
|
||||||
'promptset.deleting': 'Löschen...',
|
|
||||||
'promptset.confirm_click': 'Zum Bestätigen klicken',
|
|
||||||
'promptset.delete_error': 'Fehler beim Löschen',
|
|
||||||
'promptset.deleting_message': 'Prompt wird gelöscht...',
|
|
||||||
|
|
||||||
// Connections
|
|
||||||
'connections.title': 'Verbindungen',
|
|
||||||
'connections.subtitle': 'Verwalten Sie Ihre Service-Verbindungen',
|
|
||||||
'connections.connect_google': 'Google verbinden',
|
|
||||||
'connections.connect_microsoft': 'Microsoft verbinden',
|
|
||||||
'connections.add_google_button': 'Google-Verbindung hinzufügen',
|
|
||||||
'connections.add_microsoft_button': 'Microsoft-Verbindung hinzufügen',
|
|
||||||
'connections.create_google_title': 'Google-Verbindung erstellen',
|
|
||||||
'connections.create_microsoft_title': 'Microsoft-Verbindung erstellen',
|
|
||||||
'connections.edit_connection_title': '{authority} Verbindung bearbeiten',
|
|
||||||
'connections.update_connection': 'Verbindung aktualisieren',
|
|
||||||
'connections.service_connections': 'Service-Verbindungen',
|
|
||||||
'connections.error': 'Fehler',
|
|
||||||
'connections.connection_error': 'Verbindungsfehler',
|
|
||||||
'connections.disconnect_error': 'Trennungsfehler',
|
|
||||||
'connections.unknown': 'Unbekannt',
|
|
||||||
'connections.not_available': 'Nicht verfügbar',
|
|
||||||
'connections.invalid_date': 'Ungültiges Datum',
|
|
||||||
'connections.confirm_delete': 'Sind Sie sicher, dass Sie die {service} Verbindung löschen möchten?',
|
|
||||||
'connections.confirm_delete_multiple': 'Sind Sie sicher, dass Sie {count} Verbindungen löschen möchten?',
|
|
||||||
|
|
||||||
// Connection Fields
|
|
||||||
'connections.field.service': 'Service',
|
|
||||||
'connections.field.status': 'Status',
|
|
||||||
'connections.field.external_username': 'Externer Benutzername',
|
|
||||||
'connections.field.external_email': 'Externe E-Mail',
|
|
||||||
'connections.field.connected_at': 'Verbunden am',
|
|
||||||
'connections.field.last_checked': 'Zuletzt geprüft',
|
|
||||||
'connections.field.expires_at': 'Läuft ab am',
|
|
||||||
|
|
||||||
// Connection Columns
|
|
||||||
'connections.column.username': 'Benutzername',
|
|
||||||
'connections.column.email': 'E-Mail',
|
|
||||||
'connections.column.authority': 'Service',
|
|
||||||
'connections.column.status': 'Status',
|
|
||||||
'connections.column.connectedat': 'Verbunden am',
|
|
||||||
'connections.column.lastchecked': 'Zuletzt geprüft',
|
|
||||||
'connections.column.expiresat': 'Läuft ab am',
|
|
||||||
|
|
||||||
// Connection Services
|
|
||||||
'connections.service.google': 'Google',
|
|
||||||
'connections.service.microsoft': 'Microsoft',
|
|
||||||
'connections.service.local': 'Lokal',
|
|
||||||
|
|
||||||
// Connection Placeholders
|
|
||||||
'connections.placeholder.external_username': 'Externen Benutzernamen eingeben',
|
|
||||||
'connections.placeholder.external_email': 'Externe E-Mail-Adresse eingeben',
|
|
||||||
|
|
||||||
// Connection Actions
|
|
||||||
'connections.action.edit': 'Bearbeiten',
|
|
||||||
'connections.action.update': 'Aktualisieren',
|
|
||||||
'connections.action.delete': 'Löschen',
|
|
||||||
'connections.action.connect': 'Verbinden',
|
|
||||||
'connections.action.refresh': 'Aktualisieren',
|
|
||||||
|
|
||||||
// Prompt Modal
|
|
||||||
'modal.create_prompt': 'Neuen Prompt erstellen',
|
|
||||||
'modal.name_required': 'Name ist erforderlich',
|
|
||||||
'modal.content_required': 'Inhalt ist erforderlich',
|
|
||||||
'modal.create_error': 'Fehler beim Erstellen des Prompts',
|
|
||||||
'modal.name_label': 'Name',
|
|
||||||
'modal.content_label': 'Inhalt',
|
|
||||||
'modal.name_placeholder': 'Geben Sie einen Namen für den Prompt ein',
|
|
||||||
'modal.content_placeholder': 'Geben Sie den Inhalt des Prompts ein',
|
|
||||||
'modal.cancel': 'Abbrechen',
|
|
||||||
'modal.creating': 'Erstellen...',
|
|
||||||
'modal.create': 'Prompt erstellen',
|
|
||||||
|
|
||||||
// Share Modal
|
|
||||||
'share_modal.title': 'Prompt teilen',
|
|
||||||
'share_modal.select_users': 'Benutzer auswählen',
|
|
||||||
'share_modal.select_all': 'Alle auswählen',
|
|
||||||
'share_modal.deselect_all': 'Alle abwählen',
|
|
||||||
'share_modal.loading_users': 'Benutzer werden geladen...',
|
|
||||||
'share_modal.error_loading_users': 'Fehler beim Laden der Benutzer',
|
|
||||||
'share_modal.no_users_available': 'Keine Benutzer verfügbar',
|
|
||||||
'share_modal.no_users_selected': 'Bitte wählen Sie mindestens einen Benutzer aus',
|
|
||||||
'share_modal.one_user_selected': '1 Benutzer ausgewählt',
|
|
||||||
'share_modal.multiple_users_selected': '{count} Benutzer ausgewählt',
|
|
||||||
'share_modal.custom_title': 'Benutzerdefinierter Titel (optional)',
|
|
||||||
'share_modal.title_placeholder': 'Geben Sie einen benutzerdefinierten Titel ein',
|
|
||||||
'share_modal.message': 'Nachricht (optional)',
|
|
||||||
'share_modal.message_placeholder': 'Fügen Sie eine Nachricht für die Empfänger hinzu',
|
|
||||||
'share_modal.share': 'Teilen',
|
|
||||||
'share_modal.sharing': 'Wird geteilt...',
|
|
||||||
'share_modal.share_error': 'Fehler beim Teilen des Prompts',
|
|
||||||
|
|
||||||
// Prompt Settings
|
|
||||||
'prompt_settings.title': 'Prompt Einstellungen',
|
|
||||||
'prompt_settings.content_placeholder': 'Einstellungen werden in zukünftigen Updates hinzugefügt.',
|
|
||||||
|
|
||||||
// Chat Area
|
|
||||||
'chat.continue_conversation': 'Gespräch fortsetzen...',
|
|
||||||
'chat.enter_message': 'Nachricht eingeben...',
|
|
||||||
'chat.remove_file': 'Datei entfernen',
|
|
||||||
'chat.attach_file': 'Datei anhängen',
|
|
||||||
'chat.you': 'You',
|
|
||||||
'chat.click_to_open': 'Klicken Sie, um zu öffnen',
|
|
||||||
'chat.preview_document': 'Dokument vorschauen',
|
|
||||||
'chat.download_document': 'Dokument herunterladen',
|
|
||||||
'chat.workflow_failed': 'Workflow fehlgeschlagen.',
|
|
||||||
'chat.retry_workflow': 'Nochmal versuchen',
|
|
||||||
'chat.sending_followup': 'Folgenachricht wird gesendet...',
|
|
||||||
'chat.sending_message': 'Nachricht wird gesendet...',
|
|
||||||
'chat.error_prefix': 'Fehler:',
|
|
||||||
'chat.error_loading_messages': 'Fehler beim Laden der Nachrichten:',
|
|
||||||
'chat.loading_workflow_messages': 'Workflow-Nachrichten werden geladen...',
|
|
||||||
'chat.start_conversation': 'Beginne ein Gespräch, indem du eine Nachricht eingibst, eine Vorlage auswählst oder einen vorherigen Workflow fortsetzt …',
|
|
||||||
|
|
||||||
// Chat Input Area
|
|
||||||
'chat.input.continue_workflow': 'Gespräch fortsetzen...',
|
|
||||||
'chat.input.enter_message': 'Oder geben Sie Ihre Nachricht ein...',
|
|
||||||
'chat.input.continuing_workflow': 'Workflow wird fortgesetzt',
|
|
||||||
'chat.input.workflow': 'Workflow',
|
|
||||||
'chat.input.files_attached': 'Datei',
|
|
||||||
'chat.input.files_attached_plural': 'Dateien',
|
|
||||||
'chat.input.files_attached_label': 'angehängt',
|
|
||||||
'chat.input.error_prefix': 'Fehler:',
|
|
||||||
'chat.input.attach_files': 'Dateien anhängen',
|
|
||||||
'chat.input.sending': 'Wird gesendet...',
|
|
||||||
'chat.input.processing': 'Wird verarbeitet...',
|
|
||||||
'chat.input.continue': 'Fortsetzen',
|
|
||||||
'chat.input.send': 'Senden',
|
|
||||||
'chat.input.stop': 'Stoppen',
|
|
||||||
'chat.input.stopping': 'Wird gestoppt...',
|
|
||||||
'chat.input.drop_files_here': 'Dateien hier ablegen zum Anhängen',
|
|
||||||
'chat.input.drop_disabled': 'Datei-Ablage während Workflow deaktiviert',
|
|
||||||
'chat.input.new_chat': 'Chat leeren...',
|
|
||||||
'chat.input.using_prompt': 'Verwende Vorlage:',
|
|
||||||
'chat.input.select_prompt': 'Prompt auswählen...',
|
|
||||||
'chat.input.loading_prompts': 'Prompts werden geladen...',
|
|
||||||
'chat.input.clear_prompt': 'Prompt löschen',
|
|
||||||
|
|
||||||
// File Preview
|
|
||||||
'file_preview.loading': 'Vorschau wird geladen...',
|
|
||||||
'file_preview.error': 'Fehler',
|
|
||||||
'file_preview.no_preview': 'Keine Vorschau verfügbar',
|
|
||||||
'file_preview.close_preview': 'Vorschau schließen',
|
|
||||||
'file_preview.python': 'Python',
|
|
||||||
|
|
||||||
// Chat History
|
|
||||||
'chat_history.loading': 'Workflows werden geladen...',
|
|
||||||
'chat_history.error_loading': 'Fehler beim Laden der Workflows:',
|
|
||||||
'chat_history.try_again': 'Nochmal versuchen',
|
|
||||||
'chat_history.title': 'Workflow-Verlauf',
|
|
||||||
'chat_history.workflow_count': 'Workflow',
|
|
||||||
'chat_history.workflow_count_plural': 'Workflows',
|
|
||||||
'chat_history.empty_state': 'Keine Workflows verfügbar',
|
|
||||||
'chat_history.confirm_delete': 'Sind Sie sicher, dass Sie Workflow "{id}..." löschen möchten?',
|
|
||||||
'chat_history.no_message_content': 'Kein Nachrichteninhalt verfügbar',
|
|
||||||
'chat_history.unknown_date': 'Unbekanntes Datum',
|
|
||||||
'chat_history.invalid_date': 'Ungültiges Datum',
|
|
||||||
'chat_history.started': 'Gestartet:',
|
|
||||||
'chat_history.last_activity': 'Letzte Aktivität:',
|
|
||||||
'chat_history.round': 'Runde',
|
|
||||||
'chat_history.resume_tooltip': 'Workflow fortsetzen',
|
|
||||||
'chat_history.delete_tooltip': 'Workflow löschen',
|
|
||||||
'chat_history.deleting': 'Workflow wird gelöscht...',
|
|
||||||
|
|
||||||
// Chat Messages
|
|
||||||
'chat.messages.no_workflow_selected': 'Noch keinen Workflow ausgewählt',
|
|
||||||
'chat.messages.no_workflow_selected_description': 'Wähle einen Workflow aus der Liste aus oder starte einen neuen Workflow',
|
|
||||||
'chat.messages.loading_progress': 'Lade Fortschritt...',
|
|
||||||
'chat.messages.tasks': 'Aufgaben',
|
|
||||||
'chat.messages.workflow_progress': 'Workflow Fortschritt',
|
|
||||||
'chat.messages.analyzing_workflow': 'Analysiere Workflow...',
|
|
||||||
'chat.messages.scroll_to_bottom_btn': 'Nach unten scrollen',
|
|
||||||
// Workflow Status
|
|
||||||
'status.error': 'FEHLER',
|
|
||||||
'status.failed': 'FEHLGESCHLAGEN',
|
|
||||||
'status.stopped': 'GESTOPPT',
|
|
||||||
'status.cancelled': 'ABGEBROCHEN',
|
|
||||||
'status.running': 'LÄUFT',
|
|
||||||
'status.processing': 'VERARBEITUNG',
|
|
||||||
'status.completed': 'ABGESCHLOSSEN',
|
|
||||||
'status.pending': 'WARTEND',
|
|
||||||
|
|
||||||
// Files
|
|
||||||
'files.unknown_size': 'Unbekannte Größe',
|
|
||||||
'files.unknown_date': 'Unbekanntes Datum',
|
|
||||||
'files.source.uploaded': 'Hochgeladen',
|
|
||||||
'files.source.ai_created': 'KI-erstellt',
|
|
||||||
'files.source.shared': 'Geteilt',
|
|
||||||
'files.source.unknown': 'Unbekannt',
|
|
||||||
'files.preview_tooltip': 'Datei vorschauen',
|
|
||||||
'files.download_tooltip': 'Datei herunterladen',
|
|
||||||
'files.delete_tooltip': 'Datei löschen',
|
|
||||||
'files.delete_confirm_tooltip': 'Klicken Sie erneut zum Bestätigen der Löschung',
|
|
||||||
'files.downloading': 'Laden...',
|
|
||||||
'files.deleting': 'Löschen...',
|
|
||||||
'files.delete_confirm': 'Zum Bestätigen klicken...',
|
|
||||||
'files.no_files': 'Keine Dateien gefunden.',
|
|
||||||
'files.no_shared_files': 'Keine mit Ihnen geteilten Dateien gefunden.',
|
|
||||||
'files.no_ai_files': 'Keine von der KI erstellten Dateien gefunden.',
|
|
||||||
'files.no_uploaded_files': 'Keine hochgeladenen Dateien gefunden.',
|
|
||||||
'files.header.name': 'Name',
|
|
||||||
'files.header.type': 'Typ',
|
|
||||||
'files.header.size': 'Größe',
|
|
||||||
'files.header.date': 'Datum',
|
|
||||||
'files.selector.title': 'Dateien auswählen',
|
|
||||||
'files.selector.tab.all': 'Alle Dateien',
|
|
||||||
'files.selector.tab.uploads': 'Hochgeladen',
|
|
||||||
'files.selector.tab.created': 'KI-erstellt',
|
|
||||||
'files.selector.tab.shared': 'Geteilt',
|
|
||||||
'files.selector.select_all': 'Alle auswählen',
|
|
||||||
'files.selector.deselect_all': 'Alle abwählen',
|
|
||||||
'files.selector.file_selected': 'Datei',
|
|
||||||
'files.selector.files_selected': 'Dateien',
|
|
||||||
'files.selector.selected_suffix': 'ausgewählt',
|
|
||||||
'files.selector.upload_new': 'Neue Datei hochladen',
|
|
||||||
'files.selector.loading': 'Dateien werden geladen...',
|
|
||||||
'files.selector.error_loading': 'Fehler beim Laden der Dateien:',
|
|
||||||
'files.upload.title': 'Datei hochladen',
|
|
||||||
'files.upload.drop_here': 'Datei hier ablegen...',
|
|
||||||
'files.upload.uploading': 'Lädt hoch...',
|
|
||||||
'files.upload.drag_files': 'Dateien hierher ziehen',
|
|
||||||
'files.upload.or': 'oder',
|
|
||||||
'files.upload.browse': 'Durchsuchen',
|
|
||||||
'files.upload.selected_file': 'Ausgewählte Datei:',
|
|
||||||
'files.upload.upload_button': 'Hochladen',
|
|
||||||
'files.upload.uploading_button': 'Wird hochgeladen...',
|
|
||||||
'files.upload.success': 'Datei erfolgreich hochgeladen!',
|
|
||||||
'files.upload.error': 'Beim Hochladen ist ein Fehler aufgetreten.',
|
|
||||||
'files.upload.unexpected_error': 'Beim Hochladen ist ein unerwarteter Fehler aufgetreten.',
|
|
||||||
|
|
||||||
// Files Page Upload Actions
|
|
||||||
'files.drop_zone': 'Dateien hier ablegen',
|
|
||||||
'files.upload_button': 'Dateien hochladen',
|
|
||||||
'files.uploading_button': 'Wird hochgeladen...',
|
|
||||||
'files.upload_aria_label': 'Dateien hochladen',
|
|
||||||
|
|
||||||
// Files Page
|
|
||||||
'files.title': 'Dateien',
|
|
||||||
'files.table.title': 'Dateien',
|
|
||||||
'files.error.loading': 'Fehler beim Laden der Dateien:',
|
|
||||||
'files.button.retry': 'Wiederholen',
|
|
||||||
'files.page.tab.all': 'Alle Dateien',
|
|
||||||
'files.page.tab.uploads': 'Meine Uploads',
|
|
||||||
'files.page.tab.created': 'Erstellte Dateien',
|
|
||||||
'files.page.tab.shared': 'Geteilte Dateien',
|
|
||||||
'files.page.add_file': 'Datei hinzufügen',
|
|
||||||
'files.page.loading': 'Dateien werden geladen...',
|
|
||||||
'files.page.error': 'Fehler:',
|
|
||||||
|
|
||||||
// File Table Columns
|
|
||||||
'files.column.name': 'Name',
|
|
||||||
'files.column.filename': 'Dateiname',
|
|
||||||
'files.column.type': 'Typ',
|
|
||||||
'files.column.mimetype': 'MIME-Typ',
|
|
||||||
'files.column.size': 'Größe',
|
|
||||||
'files.column.filesize': 'Dateigröße',
|
|
||||||
'files.column.created': 'Erstellt',
|
|
||||||
'files.column.creationdate': 'Erstellungsdatum',
|
|
||||||
'files.column.source': 'Quelle',
|
|
||||||
|
|
||||||
// File Types
|
|
||||||
'files.type.image': 'Bild',
|
|
||||||
'files.type.pdf': 'PDF',
|
|
||||||
'files.type.document': 'Dokument',
|
|
||||||
'files.type.spreadsheet': 'Tabelle',
|
|
||||||
'files.type.text': 'Text',
|
|
||||||
'files.type.video': 'Video',
|
|
||||||
'files.type.audio': 'Audio',
|
|
||||||
'files.type.file': 'Datei',
|
|
||||||
|
|
||||||
// File Actions
|
|
||||||
'files.action.preview': 'Vorschau',
|
|
||||||
'files.action.download': 'Herunterladen',
|
|
||||||
'files.action.delete': 'Löschen',
|
|
||||||
'files.delete.confirm': 'Sind Sie sicher, dass Sie die Datei "{name}" löschen möchten?',
|
|
||||||
|
|
||||||
// File Preview
|
|
||||||
'files.preview.title': 'Dateivorschau',
|
|
||||||
'files.preview.loading': 'Vorschau wird geladen...',
|
|
||||||
'files.preview.unsupported': 'Vorschau für diesen Dateityp nicht verfügbar',
|
|
||||||
'files.preview.error': 'Fehler beim Laden der Vorschau',
|
|
||||||
'files.preview.textInPdfFile': 'Textvorschau',
|
|
||||||
'files.preview.pdfFileCorrupted': 'Diese Datei scheint beschädigt zu sein. Sie hat eine PDF-Erweiterung, enthält aber Textinhalte. Bitte laden Sie die Datei erneut hoch, falls möglich.',
|
|
||||||
|
|
||||||
// Workflows Page
|
|
||||||
'workflows.title': 'Workflows',
|
|
||||||
'workflows.table.title': 'Workflows',
|
|
||||||
'workflows.error.loading': 'Fehler beim Laden der Workflows:',
|
|
||||||
'workflows.button.retry': 'Wiederholen',
|
|
||||||
'workflows.table.empty': 'Keine Workflows gefunden',
|
|
||||||
|
|
||||||
// Workflow Table Columns
|
|
||||||
'workflows.column.id': 'ID',
|
|
||||||
'workflows.column.name': 'Name',
|
|
||||||
'workflows.column.status': 'Status',
|
|
||||||
'workflows.column.round': 'Runde',
|
|
||||||
'workflows.column.started': 'Gestartet',
|
|
||||||
'workflows.column.lastActivity': 'Letzte Aktivität',
|
|
||||||
'workflows.column.messages': 'Nachrichten',
|
|
||||||
|
|
||||||
// Workflow Status
|
|
||||||
'workflows.status.running': 'Läuft',
|
|
||||||
'workflows.status.completed': 'Abgeschlossen',
|
|
||||||
'workflows.status.failed': 'Fehlgeschlagen',
|
|
||||||
'workflows.status.stopped': 'Gestoppt',
|
|
||||||
'workflows.status.pending': 'Wartend',
|
|
||||||
|
|
||||||
// Workflow Actions
|
|
||||||
'workflows.action.stop': 'Stoppen',
|
|
||||||
'workflows.action.delete': 'Löschen',
|
|
||||||
'workflows.action.stop.tooltip': 'Workflow stoppen',
|
|
||||||
'workflows.action.delete.tooltip': 'Workflow löschen',
|
|
||||||
|
|
||||||
// Workflow Messages
|
|
||||||
'workflows.unnamed': 'Unbenannter Workflow',
|
|
||||||
'workflows.delete.confirm': 'Sind Sie sicher, dass Sie den Workflow "{name}" löschen möchten?',
|
|
||||||
'workflows.loading': 'Workflows werden geladen...',
|
|
||||||
|
|
||||||
// FormGenerator
|
|
||||||
'formgen.search.placeholder': 'Suchen...',
|
|
||||||
'formgen.refresh.tooltip': 'Daten aktualisieren',
|
|
||||||
'formgen.filter.yes': 'Ja',
|
|
||||||
'formgen.filter.no': 'Nein',
|
|
||||||
'formgen.filter.clear': 'Filter löschen',
|
|
||||||
'formgen.filter.placeholder': '{column} filtern',
|
|
||||||
'formgen.actions.column': 'Aktionen',
|
|
||||||
'formgen.pagination.info': 'Seite {page} von {total} ({count} Einträge)',
|
|
||||||
'formgen.pagination.pageSize': 'Einträge pro Seite:',
|
|
||||||
'formgen.pagination.first': 'Erste Seite',
|
|
||||||
'formgen.pagination.prev': 'Vorherige Seite',
|
|
||||||
'formgen.pagination.next': 'Nächste Seite',
|
|
||||||
'formgen.pagination.last': 'Letzte Seite',
|
|
||||||
'formgen.select.all': 'Alle Elemente auswählen',
|
|
||||||
'formgen.select.item': 'Dieses Element auswählen',
|
|
||||||
'formgen.select.disabled': 'Dieses Element kann nicht ausgewählt werden',
|
|
||||||
'formgen.delete.multiple': 'Löschen ({count})',
|
|
||||||
'formgen.delete.single': 'Löschen',
|
|
||||||
'formgen.delete.confirm': 'Sind Sie sicher, dass Sie die {count} ausgewählten Elemente löschen möchten?',
|
|
||||||
'formgen.delete.confirm_multiple': 'Sind Sie sicher, dass Sie die {count} ausgewählten Elemente löschen möchten?',
|
|
||||||
'formgen.delete.confirm_single': 'Sind Sie sicher, dass Sie das ausgewählte Element löschen möchten?',
|
|
||||||
|
|
||||||
// Prompts
|
|
||||||
'prompts.title': 'Prompts',
|
|
||||||
'prompts.subtitle': 'Prompts verwalten',
|
|
||||||
'prompts.description': 'Prompts für Ihren KI-Assistenten erstellen und verwalten',
|
|
||||||
'prompts.new_button': 'Neuer Prompt',
|
|
||||||
'prompts.addNew': 'Prompt hinzufügen',
|
|
||||||
'prompts.creating': 'Erstellen...',
|
|
||||||
'prompts.column.name': 'Name',
|
|
||||||
'prompts.column.content': 'Inhalt',
|
|
||||||
'prompts.column.mandateId': 'Mandat-ID',
|
|
||||||
'prompts.unnamed': 'Unbenannt',
|
|
||||||
'prompts.action.edit': 'Bearbeiten',
|
|
||||||
'prompts.action.copy': 'Kopieren',
|
|
||||||
'prompts.action.delete': 'Löschen',
|
|
||||||
'prompts.action.delete.disabled': 'Keine Berechtigung zum Löschen des Prompts',
|
|
||||||
'prompts.delete.confirm': 'Sind Sie sicher, dass Sie "{name}" löschen möchten?',
|
|
||||||
'prompts.delete.confirmMultiple': 'Sind Sie sicher, dass Sie {count} Prompts löschen möchten?',
|
|
||||||
'prompts.field.name': 'Prompt-Name',
|
|
||||||
'prompts.field.content': 'Prompt-Inhalt',
|
|
||||||
'prompts.validation.nameRequired': 'Prompt-Name darf nicht leer sein',
|
|
||||||
'prompts.validation.nameTooLong': 'Prompt-Name darf 100 Zeichen nicht überschreiten',
|
|
||||||
'prompts.validation.contentRequired': 'Prompt-Inhalt darf nicht leer sein',
|
|
||||||
'prompts.validation.contentTooLong': 'Prompt-Inhalt darf 10.000 Zeichen nicht überschreiten',
|
|
||||||
'prompts.error.loading': 'Fehler beim Laden der Prompts:',
|
|
||||||
'prompts.modal.edit.title': 'Prompt bearbeiten',
|
|
||||||
'prompts.modal.edit.save': 'Änderungen speichern',
|
|
||||||
'prompts.modal.create.title': 'Neuen Prompt erstellen',
|
|
||||||
'prompts.modal.create.save': 'Prompt erstellen',
|
|
||||||
'prompts.create.success': 'Prompt erfolgreich erstellt',
|
|
||||||
'prompts.create.error': 'Fehler beim Erstellen des Prompts',
|
|
||||||
|
|
||||||
// Users/Members
|
|
||||||
'users.title': 'Benutzer',
|
|
||||||
'users.column.username': 'Benutzername',
|
|
||||||
'users.column.name': 'Name',
|
|
||||||
'users.column.email': 'E-Mail',
|
|
||||||
'users.column.password': 'Passwort',
|
|
||||||
'users.column.language': 'Sprache',
|
|
||||||
'users.column.privilege': 'Berechtigung',
|
|
||||||
'users.column.enabled': 'Aktiviert',
|
|
||||||
'users.column.authAuthority': 'Auth-Anbieter',
|
|
||||||
'users.password.placeholder': 'Passwort eingeben',
|
|
||||||
'users.noUsername': 'Kein Benutzername',
|
|
||||||
'users.noName': 'Kein Name',
|
|
||||||
'users.noEmail': 'Keine E-Mail',
|
|
||||||
'users.noLanguage': 'Keine Sprache',
|
|
||||||
'users.noPrivilege': 'Keine Berechtigung',
|
|
||||||
'users.noAuthAuthority': 'Kein Auth-Anbieter',
|
|
||||||
'users.privilege.viewer': 'Betrachter',
|
|
||||||
'users.privilege.user': 'Benutzer',
|
|
||||||
'users.privilege.admin': 'Administrator',
|
|
||||||
'users.privilege.sysadmin': 'Systemadministrator',
|
|
||||||
'users.enabled.yes': 'Ja',
|
|
||||||
'users.enabled.no': 'Nein',
|
|
||||||
'users.auth.local': 'Lokal',
|
|
||||||
'users.auth.msft': 'Microsoft',
|
|
||||||
'users.actions.edit': 'Bearbeiten',
|
|
||||||
'users.actions.delete': 'Löschen',
|
|
||||||
'users.edit.title': 'Benutzer bearbeiten',
|
|
||||||
'users.add.title': 'Benutzer hinzufügen',
|
|
||||||
'users.add.button': 'Benutzer hinzufügen',
|
|
||||||
'users.add.create': 'Benutzer erstellen',
|
|
||||||
'users.delete.title': 'Benutzer löschen',
|
|
||||||
'users.delete.message': 'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?',
|
|
||||||
'users.delete.confirm': 'Sind Sie sicher, dass Sie "{name}" löschen möchten?',
|
|
||||||
'users.delete.warning': 'Diese Aktion kann nicht rückgängig gemacht werden.',
|
|
||||||
'users.action.edit': 'Bearbeiten',
|
|
||||||
'users.action.delete': 'Löschen',
|
|
||||||
'users.delete.confirmMultiple': 'Sind Sie sicher, dass Sie {count} Benutzer löschen möchten?',
|
|
||||||
'users.error.loading': 'Fehler beim Laden der Benutzer:',
|
|
||||||
|
|
||||||
// Team Members
|
|
||||||
'team-members.title': 'Team-Mitglieder',
|
|
||||||
'team-members.subtitle': 'Team-Mitglieder verwalten',
|
|
||||||
'team-members.description': 'Team-Mitglieder verwalten, Berechtigungen festlegen und Zusammenarbeitseinstellungen konfigurieren',
|
|
||||||
'team-members.new_button': 'Mitglied hinzufügen',
|
|
||||||
'team-members.action.edit': 'Bearbeiten',
|
|
||||||
'team-members.action.delete': 'Löschen',
|
|
||||||
'team-members.field.username': 'Benutzername',
|
|
||||||
'team-members.field.email': 'E-Mail',
|
|
||||||
'team-members.field.password': 'Passwort',
|
|
||||||
'team-members.field.fullName': 'Vollständiger Name',
|
|
||||||
'team-members.field.privilege': 'Berechtigung',
|
|
||||||
'team-members.modal.create.title': 'Neues Team-Mitglied erstellen',
|
|
||||||
'team-members.create.success': 'Team-Mitglied erfolgreich erstellt',
|
|
||||||
'team-members.create.error': 'Fehler beim Erstellen des Team-Mitglieds',
|
|
||||||
|
|
||||||
// SharePoint Test
|
|
||||||
'sharepoint.title': 'SharePoint Test',
|
|
||||||
'sharepoint.table.title': 'SharePoint Dokumente',
|
|
||||||
'sharepoint.error.loading': 'Fehler beim Laden der SharePoint Dokumente:',
|
|
||||||
'sharepoint.button.retry': 'Wiederholen',
|
|
||||||
'sharepoint.button.testConnection': 'Verbindung testen',
|
|
||||||
'sharepoint.button.listDocuments': 'Dokumente auflisten',
|
|
||||||
'sharepoint.button.discoverSites': 'Sites entdecken',
|
|
||||||
'sharepoint.column.documentName': 'Dokumentname',
|
|
||||||
'sharepoint.column.mimeType': 'MIME-Typ',
|
|
||||||
'sharepoint.column.size': 'Größe',
|
|
||||||
'sharepoint.column.path': 'Pfad',
|
|
||||||
'sharepoint.action.view': 'Anzeigen',
|
|
||||||
'sharepoint.action.download': 'Herunterladen',
|
|
||||||
'sharepoint.connections.title': 'Microsoft Verbindungen',
|
|
||||||
'sharepoint.connections.noConnections': 'Keine Microsoft-Verbindungen gefunden. Bitte erstellen Sie zuerst eine Verbindung.',
|
|
||||||
'sharepoint.connections.loading': 'Verbindungen werden geladen...',
|
|
||||||
'sharepoint.sites.discovered': 'Entdeckte Sites',
|
|
||||||
'sharepoint.sites.noSites': 'Keine SharePoint-Sites gefunden',
|
|
||||||
'sharepoint.sites.authError': 'Authentifizierungstoken abgelaufen oder ungültig. Bitte verbinden Sie Ihr Microsoft-Konto erneut.',
|
|
||||||
'sharepoint.sites.retryConnection': 'Versuchen Sie, Ihr Microsoft-Konto auf der Verbindungsseite erneut zu verbinden.',
|
|
||||||
'sharepoint.form.siteUrl': 'SharePoint Site URL',
|
|
||||||
'sharepoint.form.folderPaths': 'Ordnerpfade',
|
|
||||||
|
|
||||||
// Speech
|
|
||||||
'speech.title': 'Sprach Integration',
|
|
||||||
'speech.subtitle': 'Unterstützt von',
|
|
||||||
'speech.signup.title': 'Sprach Integration',
|
|
||||||
'speech.signup.subtitle': 'Unterstützt von',
|
|
||||||
|
|
||||||
'speech.info.va': 'Virtual Assistant (VA)',
|
|
||||||
'speech.info.va_description': 'Geben Sie Kunden einen schnellen und effizienten Selbstservice für Sprach- und Textanfragen, der 24/7 verfügbar ist.',
|
|
||||||
'speech.info.sa': 'Speech Analytics (SA)',
|
|
||||||
'speech.info.sa_description': 'Überwachen Sie automatisch 100% der Gespräche, um wertvolle Einblicke für Ihr Unternehmen zu erhalten.',
|
|
||||||
'speech.info.vb': 'Voice Biometrics (VB)',
|
|
||||||
'speech.info.vb_description': 'Identifizieren und authentifizieren Sie Anrufer in Sekunden mit kontinuierlicher Verifizierung und Sicherheit.',
|
|
||||||
'speech.info.ka': 'Knowledge Agent (KA)',
|
|
||||||
'speech.info.ka_description': 'Vereinheitlichen und liefern Sie Informationen an Ihre Kunden und Mitarbeiter, wann und wo sie sie benötigen.',
|
|
||||||
'speech.info.cp': 'Chat Platform (CP)',
|
|
||||||
'speech.info.cp_description': 'Bieten Sie Unterstützung im Live-Chat und setzen Sie intelligente Chatbots in allen Kanälen ein.',
|
|
||||||
'speech.info.aa': 'Agent Assist (AA)',
|
|
||||||
'speech.info.aa_description': 'Stellen Sie alles, was Ihre Agenten benötigen, in ihren Händen bereit, mit einem einheitlichen Agent-Desktop.',
|
|
||||||
|
|
||||||
'speech.info.about': 'Revolutionäre Telefonie-Integration mit Spitch.ai',
|
|
||||||
'speech.info.about_intro': 'Erleben Sie die Zukunft der Mandantenkommunikation durch unsere strategische Partnerschaft mit Spitch.ai. Diese bahnbrechende Integration verwandelt Ihre PowerOn-Plattform in ein intelligentes Telefonie-System, das externe Mandanten nahtlos mit Unternehmen verbindet.',
|
|
||||||
'speech.info.workflow_title': 'Nahtloser Mandanten-Workflow:',
|
|
||||||
'speech.info.workflow_description': 'Von der Registrierung bis zur technischen Einrichtung - Ihr Mandant registriert sich bei PowerOn für Telefonie-Services, lädt Dokumente hoch und erhält automatisch eine technische SIP-Nummer von Spitch. Die Call-Weiterleitung kann jederzeit aktiviert oder deaktiviert werden, was maximale Flexibilität und BCM-Sicherheit gewährleistet.',
|
|
||||||
'speech.info.ai_title': 'KI-gestützte Dokumentengenerierung:',
|
|
||||||
'speech.info.ai_description': 'Unsere bereits aktive Dokumenten-Extraktions-Engine generiert automatisch personalisierte Dokumente für Spitch, basierend auf Mandantenspezifischen Daten. Die KI nutzt FAQ-Datenbanken, Mitarbeiterinformationen und Service-Details, um jeden Anruf kontextuell und hochpersonalisiert zu gestalten.',
|
|
||||||
'speech.info.sync_title': 'Echtzeit-Datensynchronisation:',
|
|
||||||
'speech.info.sync_description': 'Spitch prüft vor jedem Anruf die Mandantenberechtigung bei PowerOn, während alle Datenänderungen zentral von PowerOn initiiert werden. Call-Transkripte werden in Echtzeit in Ihrer PowerOn-Datenbank gespeichert, mit vollständiger Mandantenisolation und Sicherheit. Bei Ausfällen werden Anrufe automatisch blockiert, um die Integrität zu gewährleisten.',
|
|
||||||
'speech.info.cost_title': 'Kosteneinsparungen & Effizienz:',
|
|
||||||
'speech.info.cost_description': 'Mandanten können jederzeit auf die technische SIP-Nummer umstellen und dabei erhebliche Telefoniekosten sparen. Die Integration funktioniert wie ein weiterer Connector (Outlook, SharePoint) und wird nahtlos in Ihren bestehenden Workflow integriert.',
|
|
||||||
'speech.info.about_link': 'Mehr erfahren',
|
|
||||||
|
|
||||||
'speech.signup.button': 'Verbinden',
|
|
||||||
'speech.signup.back': 'Zurück zur Sprach Integration',
|
|
||||||
'speech.signup.submit': 'Mandat erstellen',
|
|
||||||
'speech.signup.cancel': 'Abbrechen',
|
|
||||||
|
|
||||||
'speech.signup.company_info': 'Unternehmensinformationen',
|
|
||||||
'speech.signup.company_name': 'Firmenname',
|
|
||||||
'speech.signup.company_name_placeholder': 'Geben Sie Ihren Firmennamen ein',
|
|
||||||
'speech.signup.industry': 'Branche',
|
|
||||||
'speech.signup.industry_placeholder': 'z.B. Finanzdienstleistungen, Technologie, etc.',
|
|
||||||
'speech.signup.business_hours': 'Geschäftszeiten',
|
|
||||||
'speech.signup.timezone': 'Zeitzone',
|
|
||||||
|
|
||||||
'speech.signup.contact_info': 'Kontaktinformationen',
|
|
||||||
'speech.signup.email': 'E-Mail-Adresse',
|
|
||||||
'speech.signup.email_placeholder': 'kontakt@firma.com',
|
|
||||||
'speech.signup.phone': 'Telefonnummer',
|
|
||||||
'speech.signup.phone_placeholder': '+41 123 456 789',
|
|
||||||
'speech.signup.street': 'Straße',
|
|
||||||
'speech.signup.postal_code': 'Postleitzahl',
|
|
||||||
'speech.signup.city': 'Stadt',
|
|
||||||
'speech.signup.country': 'Land',
|
|
||||||
|
|
||||||
'speech.signup.contacts_setup': 'Kontakte einrichten',
|
|
||||||
'speech.signup.contacts_description': 'Möchten Sie jetzt Kontakte für Ihr Mandat einrichten? Sie können dies auch später in den Einstellungen tun.',
|
|
||||||
'speech.signup.setup_contacts': 'Kontakte einrichten',
|
|
||||||
'speech.signup.skip_for_now': 'Jetzt überspringen',
|
|
||||||
|
|
||||||
'speech.signup.company_required': 'Firmenname ist erforderlich',
|
|
||||||
'speech.signup.industry_required': 'Branche ist erforderlich',
|
|
||||||
'speech.signup.email_required': 'E-Mail-Adresse ist erforderlich',
|
|
||||||
'speech.signup.email_invalid': 'Bitte geben Sie eine gültige E-Mail-Adresse ein',
|
|
||||||
'speech.signup.phone_required': 'Telefonnummer ist erforderlich',
|
|
||||||
'speech.signup.street_required': 'Straße ist erforderlich',
|
|
||||||
'speech.signup.postal_code_required': 'Postleitzahl ist erforderlich',
|
|
||||||
'speech.signup.city_required': 'Stadt ist erforderlich',
|
|
||||||
'speech.signup.country_required': 'Land ist erforderlich',
|
|
||||||
|
|
||||||
'speech.status.submitted': '✓ Mandat eingereicht',
|
|
||||||
'speech.status.reset': 'Neu starten',
|
|
||||||
|
|
||||||
'speech.confirmation.title': 'Mandat erfolgreich eingereicht!',
|
|
||||||
'speech.confirmation.message': 'Vielen Dank für Ihr Interesse an unserer Sprach Integration powered by Spitch.ai. Wir haben Ihr Mandat erhalten und werden es in Kürze überprüfen.',
|
|
||||||
'speech.confirmation.submitted_data': 'Eingereichte Daten:',
|
|
||||||
'speech.confirmation.company': 'Firma',
|
|
||||||
'speech.confirmation.industry': 'Branche',
|
|
||||||
'speech.confirmation.email': 'E-Mail',
|
|
||||||
'speech.confirmation.phone': 'Telefon',
|
|
||||||
'speech.confirmation.address': 'Adresse',
|
|
||||||
'speech.confirmation.timezone': 'Zeitzone',
|
|
||||||
'speech.confirmation.back': 'Zurück zur Sprach Integration',
|
|
||||||
'speech.confirmation.reset': 'Neu starten',
|
|
||||||
'speech.confirmation.next_steps': 'Was passiert als nächstes?',
|
|
||||||
'speech.confirmation.email_confirmation': 'E-Mail-Bestätigung',
|
|
||||||
'speech.confirmation.email_confirmation_desc': 'Sie erhalten in den nächsten Minuten eine Bestätigungs-E-Mail.',
|
|
||||||
'speech.confirmation.review_process': 'Überprüfungsprozess',
|
|
||||||
'speech.confirmation.review_process_desc': 'Unser Team wird Ihr Mandat innerhalb von 1-2 Werktagen überprüfen.',
|
|
||||||
'speech.confirmation.setup_call': 'Einrichtungsanruf',
|
|
||||||
'speech.confirmation.setup_call_desc': 'Bei Genehmigung planen wir einen Einrichtungsanruf zur Konfiguration Ihrer Integration.',
|
|
||||||
'speech.confirmation.questions': 'Fragen?',
|
|
||||||
'speech.confirmation.questions_desc': 'Falls Sie Fragen zu Ihrem Mandat oder dem Integrationsprozess haben, zögern Sie nicht, unser Support-Team zu kontaktieren.',
|
|
||||||
'speech.confirmation.transcript_management': 'Transkriptverwaltung',
|
|
||||||
'speech.confirmation.speech_settings': 'Sprach-Einstellungen',
|
|
||||||
|
|
||||||
'speech.transcripts.title': 'Transkriptverwaltung',
|
|
||||||
'speech.transcripts.new_transcript': 'Neues Transkript',
|
|
||||||
'speech.transcripts.recent_transcripts': 'Aktuelle Transkripte',
|
|
||||||
'speech.transcripts.no_transcripts': 'Keine Transkripte vorhanden',
|
|
||||||
'speech.transcripts.date': 'Datum',
|
|
||||||
'speech.transcripts.duration': 'Dauer',
|
|
||||||
'speech.transcripts.status': 'Status',
|
|
||||||
'speech.transcripts.transcript': 'Transkript',
|
|
||||||
'speech.transcripts.processing': 'Transkript wird verarbeitet...',
|
|
||||||
'speech.transcripts.status.completed': 'Abgeschlossen',
|
|
||||||
'speech.transcripts.status.processing': 'Verarbeitung',
|
|
||||||
'speech.transcripts.status.failed': 'Fehlgeschlagen',
|
|
||||||
'speech.transcripts.access_denied_title': 'Zugriff verweigert',
|
|
||||||
'speech.transcripts.access_denied_message': 'Sie müssen sich zuerst für die Sprach-Integration anmelden, um auf die Transkriptverwaltung zuzugreifen.',
|
|
||||||
'speech.transcripts.sign_up_now': 'Jetzt anmelden',
|
|
||||||
'speech.transcripts.subject': 'Betreff',
|
|
||||||
'speech.transcripts.start_time': 'Startzeit',
|
|
||||||
'speech.transcripts.end_time': 'Endzeit',
|
|
||||||
'speech.transcripts.caller': 'Anrufer',
|
|
||||||
'speech.transcripts.recipient': 'Empfänger',
|
|
||||||
'speech.transcripts.tags': 'Tags',
|
|
||||||
'speech.transcripts.created': 'Erstellt',
|
|
||||||
'speech.transcripts.view': 'Anzeigen',
|
|
||||||
'speech.transcripts.download': 'Herunterladen',
|
|
||||||
|
|
||||||
'speech.settings.title': 'Sprach-Integration Einstellungen',
|
|
||||||
'speech.settings.description': 'Verwalten Sie Ihre Sprach-Integrations-Konfiguration und Einstellungen.',
|
|
||||||
'speech.settings.company_info': 'Unternehmensinformationen',
|
|
||||||
'speech.settings.contact_info': 'Kontaktinformationen',
|
|
||||||
'speech.settings.business_hours': 'Geschäftszeiten & Zeitzone',
|
|
||||||
'speech.settings.save': 'Änderungen speichern',
|
|
||||||
'speech.settings.saving': 'Speichern...',
|
|
||||||
'speech.settings.save_success': 'Einstellungen erfolgreich gespeichert!',
|
|
||||||
'speech.settings.save_error': 'Fehler beim Speichern der Einstellungen. Bitte versuchen Sie es erneut.',
|
|
||||||
'speech.settings.reset': 'Auf Standard zurücksetzen',
|
|
||||||
'speech.settings.reset_confirm': 'Sind Sie sicher, dass Sie alle Sprach-Integrations-Einstellungen zurücksetzen möchten? Diese Aktion kann nicht rückgängig gemacht werden.',
|
|
||||||
'speech.settings.reset_success': 'Einstellungen wurden erfolgreich zurückgesetzt.',
|
|
||||||
'speech.settings.no_data': 'Keine Sprach-Integrations-Daten gefunden. Bitte melden Sie sich zuerst an, um auf die Einstellungen zuzugreifen.',
|
|
||||||
'speech.settings.sign_up_now': 'Jetzt anmelden',
|
|
||||||
|
|
||||||
// Message Overlay Types
|
|
||||||
'message.success.title': 'Erfolgreich',
|
|
||||||
'message.success.upload': 'Datei erfolgreich hochgeladen!',
|
|
||||||
'message.info.title': 'Information',
|
|
||||||
'message.info.processing': 'Ihre Anfrage wird verarbeitet...',
|
|
||||||
'message.error.title': 'Fehler',
|
|
||||||
'message.error.upload_failed': 'Upload fehlgeschlagen. Bitte versuchen Sie es erneut.',
|
|
||||||
|
|
||||||
// Warning Messages
|
|
||||||
'warning.duplicate_file.title': 'Datei bereits vorhanden',
|
|
||||||
'warning.duplicate_file.message': 'Die Datei "{fileName}" existiert bereits mit identischem Inhalt. Die vorhandene Datei wird wiederverwendet.',
|
|
||||||
|
|
||||||
// Administration
|
|
||||||
'administration.title': 'Verwaltung',
|
|
||||||
'administration.description': 'Verwaltungs- und Management-Tools',
|
|
||||||
'administration.subtitle': 'Verwaltungs- und Management-Tools',
|
|
||||||
'administration.intro.description': 'Dieser Bereich enthält alle Verwaltungs- und Management-Tools für Ihren Arbeitsbereich.',
|
|
||||||
'administration.features.title': 'Verfügbare Tools',
|
|
||||||
'administration.features.description': 'Management-Tools umfassen:',
|
|
||||||
'administration.features.file_management': 'Dateiverwaltung - Dokumente hochladen und organisieren',
|
|
||||||
'administration.features.user_management': 'Benutzerverwaltung - Teammitglieder und Berechtigungen verwalten',
|
|
||||||
'administration.features.system_settings': 'Systemeinstellungen - Arbeitsbereich-Einstellungen konfigurieren',
|
|
||||||
'administration.features.data_management': 'Datenverwaltung - Datenimporte und -exporte verwalten',
|
|
||||||
|
|
||||||
// Admin pages
|
|
||||||
'admin.mandates.title': 'Mandate',
|
|
||||||
'admin.mandates.subtitle': 'Mandate und Berechtigungen verwalten',
|
|
||||||
'admin.mandates.description': 'Mandatsverwaltung',
|
|
||||||
'admin.mandates.description_text': 'Verwalten Sie Mandate und deren zugehörige Berechtigungen.',
|
|
||||||
|
|
||||||
'admin.rbac-rules.title': 'RBAC-Regeln',
|
|
||||||
'admin.rbac-rules.subtitle': 'Rollenbasierte Zugriffssteuerungsregeln',
|
|
||||||
'admin.rbac-rules.description': 'RBAC-Regelverwaltung',
|
|
||||||
'admin.rbac-rules.description_text': 'Konfigurieren und verwalten Sie rollenbasierte Zugriffssteuerungsregeln.',
|
|
||||||
|
|
||||||
'admin.rbac-role.title': 'RBAC-Rollen',
|
|
||||||
'admin.rbac-role.subtitle': 'Rollenverwaltung',
|
|
||||||
'admin.rbac-role.description': 'RBAC-Rollenverwaltung',
|
|
||||||
'admin.rbac-role.description_text': 'Erstellen und verwalten Sie RBAC-Rollen und deren Berechtigungen.',
|
|
||||||
|
|
||||||
'admin.admin-settings.title': 'Admin-Einstellungen',
|
|
||||||
'admin.admin-settings.subtitle': 'Administrative Einstellungen',
|
|
||||||
'admin.admin-settings.description': 'Administrative Einstellungen',
|
|
||||||
'admin.admin-settings.description_text': 'Konfigurieren Sie administrative Einstellungen und Systempräferenzen.',
|
|
||||||
|
|
||||||
// Start page
|
|
||||||
'start.title': 'Start',
|
|
||||||
'start.description': 'Willkommen in Ihrem Arbeitsbereich',
|
|
||||||
'start.subtitle': 'Willkommen in Ihrem Arbeitsbereich',
|
|
||||||
'start.intro.description': 'Dies ist Ihr Ausgangspunkt für den Zugriff auf alle Arbeitsbereich-Features und -Tools.',
|
|
||||||
'start.features.title': 'Schnellzugriff',
|
|
||||||
'start.features.description': 'Beginnen Sie mit:',
|
|
||||||
'start.features.quick_access': 'Schnellzugriff - Springen Sie zu häufig verwendeten Features',
|
|
||||||
'start.features.recent_activities': 'Letzte Aktivitäten - Sehen Sie Ihre neueste Arbeit',
|
|
||||||
'start.features.overview': 'Übersicht - Sehen Sie den Arbeitsbereich-Status und Updates',
|
|
||||||
'start.features.navigation': 'Navigation - Erkunden Sie alle verfügbaren Tools',
|
|
||||||
|
|
||||||
// Projects page
|
|
||||||
'projects.title': 'Projekte',
|
|
||||||
'projects.subtitle': 'Projektverwaltung',
|
|
||||||
'projects.description': 'Projektverwaltung und -organisation',
|
|
||||||
'projects.description_text': 'Suchen Sie nach Standorten über Adresse oder Koordinaten, oder verwenden Sie natürliche Sprache, um Projekte zu erstellen und zu verwalten.',
|
|
||||||
'projects.command.placeholder': 'Befehl eingeben (z.B., "Erstelle ein neues Projekt namens \'Hauptstrasse 42\'")',
|
|
||||||
'projects.command.empty': 'Noch keine Befehle ausgeführt. Senden Sie einen Befehl, um Ergebnisse hier zu sehen.',
|
|
||||||
|
|
||||||
// Data Management page
|
|
||||||
'data-management.title': 'Datenverwaltung',
|
|
||||||
'data-management.subtitle': 'Datenverwaltung',
|
|
||||||
'data-management.description': 'Datenverwaltung mit Tabellen',
|
|
||||||
'data-management.description_text': 'Verwalten Sie Daten über Tabellen. Wählen Sie eine Tabelle aus oder verwenden Sie natürliche Sprache, um Befehle auszuführen.',
|
|
||||||
'data-management.command.placeholder': 'Befehl eingeben (z.B., "Erstelle ein neues Projekt namens \'Hauptstrasse 42\'")',
|
|
||||||
'data-management.command.empty': 'Noch keine Befehle ausgeführt. Senden Sie einen Befehl, um Ergebnisse hier zu sehen.',
|
|
||||||
|
|
||||||
// Drag and Drop
|
|
||||||
'dragdrop.overlay.default_text': 'Dateien hier ablegen',
|
|
||||||
'dragdrop.overlay.default_subtext': 'Sie können auch auf den Upload-Button klicken',
|
|
||||||
'dragdrop.overlay.processing': 'Dateien werden verarbeitet...',
|
|
||||||
'dragdrop.overlay.error': 'Fehler beim Verarbeiten der Dateien',
|
|
||||||
};
|
|
||||||
|
|
@ -1,784 +0,0 @@
|
||||||
export default {
|
|
||||||
// Navigation
|
|
||||||
'nav.dashboard': 'Dashboard',
|
|
||||||
'nav.files': 'Files',
|
|
||||||
'nav.team': 'Team Area',
|
|
||||||
'nav.workflows': 'Workflows',
|
|
||||||
'nav.connections': 'Connections',
|
|
||||||
'nav.settings': 'Settings',
|
|
||||||
'nav.testSharepoint': 'Test SharePoint',
|
|
||||||
'nav.speech': 'Speech',
|
|
||||||
'nav.transcript_management': 'Transcript Management',
|
|
||||||
|
|
||||||
// Settings page
|
|
||||||
'settings.title': 'Settings',
|
|
||||||
'settings.appearance': 'Appearance',
|
|
||||||
'settings.language': 'Language',
|
|
||||||
'settings.about': 'About',
|
|
||||||
'settings.version': 'Version',
|
|
||||||
'settings.theme': 'Theme',
|
|
||||||
'settings.theme.description': 'Switch between light and dark mode',
|
|
||||||
'settings.language.description': 'Choose your preferred language',
|
|
||||||
'settings.theme.light': 'Light',
|
|
||||||
'settings.theme.dark': 'Dark',
|
|
||||||
'settings.theme.toggle.light': 'Switch to light mode',
|
|
||||||
'settings.theme.toggle.dark': 'Switch to dark mode',
|
|
||||||
'settings.userinfo': 'User Information',
|
|
||||||
'settings.userinfo.description': 'Manage your account information',
|
|
||||||
'settings.userinfo.username': 'Username',
|
|
||||||
'settings.userinfo.fullname': 'Full Name',
|
|
||||||
'settings.userinfo.email': 'Email Address',
|
|
||||||
'settings.userinfo.phone_name': 'Phone Name',
|
|
||||||
'settings.userinfo.phone_name.description': 'How would you like to be called on the phone?',
|
|
||||||
'settings.userinfo.language': 'Language',
|
|
||||||
'settings.userinfo.privilege': 'Privilege Level',
|
|
||||||
'settings.userinfo.enabled': 'Account Status',
|
|
||||||
'settings.userinfo.auth_authority': 'Authentication Provider',
|
|
||||||
'settings.userinfo.enabled.true': 'Active',
|
|
||||||
'settings.userinfo.enabled.false': 'Inactive',
|
|
||||||
'settings.userinfo.loading': 'Loading user information...',
|
|
||||||
'settings.userinfo.error': 'Error loading user information',
|
|
||||||
'settings.userinfo.save': 'Save Changes',
|
|
||||||
'settings.userinfo.saving': 'Saving...',
|
|
||||||
'settings.userinfo.success': 'User information updated successfully',
|
|
||||||
'settings.userinfo.update_error': 'Error updating user information',
|
|
||||||
'settings.userinfo.managed_by': 'Managed by {provider}',
|
|
||||||
'settings.userinfo.managed_note': 'This field is managed by {provider} and cannot be changed',
|
|
||||||
|
|
||||||
// Languages
|
|
||||||
'language.german': 'Deutsch',
|
|
||||||
'language.english': 'English',
|
|
||||||
'language.french': 'Français',
|
|
||||||
|
|
||||||
// Common
|
|
||||||
'common.loading': 'Loading...',
|
|
||||||
'common.error': 'Error',
|
|
||||||
'common.success': 'Success',
|
|
||||||
'common.cancel': 'Cancel',
|
|
||||||
'common.save': 'Save',
|
|
||||||
'common.delete': 'Delete',
|
|
||||||
'common.edit': 'Edit',
|
|
||||||
'common.close': 'Close',
|
|
||||||
'common.retry': 'Retry',
|
|
||||||
'common.create': 'Create',
|
|
||||||
'common.creating': 'Creating...',
|
|
||||||
|
|
||||||
// Auth
|
|
||||||
'auth.login': 'Login',
|
|
||||||
'auth.register': 'Register',
|
|
||||||
'auth.logout': 'Logout',
|
|
||||||
'auth.email': 'Email',
|
|
||||||
'auth.password': 'Password',
|
|
||||||
|
|
||||||
// Dashboard
|
|
||||||
'dashboard.prompt.template': 'Prompt Template',
|
|
||||||
'dashboard.prompt.settings': 'Settings',
|
|
||||||
'dashboard.chat.area': 'Chat Area',
|
|
||||||
'dashboard.chat.history': 'Workflow History',
|
|
||||||
'dashboard.log.title': 'Log',
|
|
||||||
'dashboard.log.workflow': 'Workflow',
|
|
||||||
'dashboard.log.no_workflow': 'No workflow selected',
|
|
||||||
'dashboard.log.loading': 'Loading logs...',
|
|
||||||
'dashboard.log.error': 'Error loading logs',
|
|
||||||
'dashboard.log.no_logs': 'No logs available for this workflow',
|
|
||||||
'dashboard.log.waiting': 'Workflow running... Waiting for logs...',
|
|
||||||
'dashboard.log.fetch_failed': 'Failed to fetch logs',
|
|
||||||
'dashboard.log.level.info': 'INFO',
|
|
||||||
'dashboard.workflow_dropdown.loading': 'Loading...',
|
|
||||||
'dashboard.workflow_dropdown.error': 'Error',
|
|
||||||
'dashboard.workflow_dropdown.select_workflow': 'Select Workflow',
|
|
||||||
'dashboard.workflow_dropdown.available_workflows': 'Available Workflows',
|
|
||||||
'dashboard.workflow_dropdown.no_workflows': 'No workflows available',
|
|
||||||
|
|
||||||
// Workflow Stats
|
|
||||||
'dashboard.stats.workflow': 'Workflow',
|
|
||||||
'dashboard.stats.status': 'Status',
|
|
||||||
'dashboard.stats.rounds': 'Rounds',
|
|
||||||
'dashboard.stats.messages': 'Messages',
|
|
||||||
'dashboard.stats.files': 'Files',
|
|
||||||
'dashboard.stats.tokens': 'Tokens',
|
|
||||||
'dashboard.stats.data_sent': 'Data Sent',
|
|
||||||
'dashboard.stats.data_received': 'Data Received',
|
|
||||||
'dashboard.stats.success_rate': 'Success Rate',
|
|
||||||
'dashboard.stats.errors': 'Errors',
|
|
||||||
'dashboard.stats.started': 'Started',
|
|
||||||
|
|
||||||
// Prompt Set
|
|
||||||
'promptset.loading': 'Loading prompts...',
|
|
||||||
'promptset.error.loading': 'Error loading prompts',
|
|
||||||
'promptset.retry': 'Try again',
|
|
||||||
'promptset.new_prompt': 'New Prompt',
|
|
||||||
'promptset.prompt_count': 'Prompt',
|
|
||||||
'promptset.prompt_count_plural': 'Prompts',
|
|
||||||
'promptset.no_prompts': 'No prompts available',
|
|
||||||
'promptset.created': 'Created',
|
|
||||||
'promptset.run_tooltip': 'Run prompt',
|
|
||||||
'promptset.share_tooltip': 'Share prompt',
|
|
||||||
'promptset.delete_tooltip': 'Delete prompt',
|
|
||||||
'promptset.confirm_delete': 'Click again to confirm',
|
|
||||||
'promptset.deleting': 'Deleting...',
|
|
||||||
'promptset.confirm_click': 'Click to confirm',
|
|
||||||
'promptset.delete_error': 'Error deleting',
|
|
||||||
'promptset.deleting_message': 'Deleting prompt...',
|
|
||||||
|
|
||||||
// Connections
|
|
||||||
'connections.title': 'Connections',
|
|
||||||
'connections.subtitle': 'Manage your service connections',
|
|
||||||
'connections.connect_google': 'Connect Google',
|
|
||||||
'connections.connect_microsoft': 'Connect Microsoft',
|
|
||||||
'connections.add_google_button': 'Add Google Connection',
|
|
||||||
'connections.add_microsoft_button': 'Add Microsoft Connection',
|
|
||||||
'connections.create_google_title': 'Create Google Connection',
|
|
||||||
'connections.create_microsoft_title': 'Create Microsoft Connection',
|
|
||||||
'connections.edit_connection_title': 'Edit {authority} Connection',
|
|
||||||
'connections.update_connection': 'Update Connection',
|
|
||||||
'connections.service_connections': 'Service Connections',
|
|
||||||
'connections.error': 'Error',
|
|
||||||
'connections.connection_error': 'Connection Error',
|
|
||||||
'connections.disconnect_error': 'Disconnect Error',
|
|
||||||
'connections.unknown': 'Unknown',
|
|
||||||
'connections.not_available': 'N/A',
|
|
||||||
'connections.invalid_date': 'Invalid Date',
|
|
||||||
'connections.confirm_delete': 'Are you sure you want to delete the {service} connection?',
|
|
||||||
'connections.confirm_delete_multiple': 'Are you sure you want to delete {count} connections?',
|
|
||||||
|
|
||||||
// Connection Fields
|
|
||||||
'connections.field.service': 'Service',
|
|
||||||
'connections.field.status': 'Status',
|
|
||||||
'connections.field.external_username': 'External Username',
|
|
||||||
'connections.field.external_email': 'External Email',
|
|
||||||
'connections.field.connected_at': 'Connected At',
|
|
||||||
'connections.field.last_checked': 'Last Checked',
|
|
||||||
'connections.field.expires_at': 'Expires At',
|
|
||||||
|
|
||||||
// Connection Columns
|
|
||||||
'connections.column.username': 'Username',
|
|
||||||
'connections.column.email': 'Email',
|
|
||||||
'connections.column.authority': 'Service',
|
|
||||||
'connections.column.status': 'Status',
|
|
||||||
'connections.column.connectedat': 'Connected At',
|
|
||||||
'connections.column.lastchecked': 'Last Checked',
|
|
||||||
'connections.column.expiresat': 'Expires At',
|
|
||||||
|
|
||||||
// Connection Services
|
|
||||||
'connections.service.google': 'Google',
|
|
||||||
'connections.service.microsoft': 'Microsoft',
|
|
||||||
'connections.service.local': 'Local',
|
|
||||||
|
|
||||||
// Connection Placeholders
|
|
||||||
'connections.placeholder.external_username': 'Enter external username',
|
|
||||||
'connections.placeholder.external_email': 'Enter external email address',
|
|
||||||
|
|
||||||
// Connection Actions
|
|
||||||
'connections.action.edit': 'Edit',
|
|
||||||
'connections.action.update': 'Update',
|
|
||||||
'connections.action.delete': 'Delete',
|
|
||||||
'connections.action.connect': 'Connect',
|
|
||||||
'connections.action.refresh': 'Refresh',
|
|
||||||
|
|
||||||
|
|
||||||
// Prompt Modal
|
|
||||||
'modal.create_prompt': 'Create New Prompt',
|
|
||||||
'modal.name_required': 'Name is required',
|
|
||||||
'modal.content_required': 'Content is required',
|
|
||||||
'modal.create_error': 'Error creating prompt',
|
|
||||||
'modal.name_label': 'Name',
|
|
||||||
'modal.content_label': 'Content',
|
|
||||||
'modal.name_placeholder': 'Enter a name for the prompt',
|
|
||||||
'modal.content_placeholder': 'Enter the prompt content',
|
|
||||||
'modal.cancel': 'Cancel',
|
|
||||||
'modal.creating': 'Creating...',
|
|
||||||
'modal.create': 'Create Prompt',
|
|
||||||
|
|
||||||
// Share Modal
|
|
||||||
'share_modal.title': 'Share Prompt',
|
|
||||||
'share_modal.select_users': 'Select Users',
|
|
||||||
'share_modal.select_all': 'Select All',
|
|
||||||
'share_modal.deselect_all': 'Deselect All',
|
|
||||||
'share_modal.loading_users': 'Loading users...',
|
|
||||||
'share_modal.error_loading_users': 'Error loading users',
|
|
||||||
'share_modal.no_users_available': 'No users available',
|
|
||||||
'share_modal.no_users_selected': 'Please select at least one user',
|
|
||||||
'share_modal.one_user_selected': '1 user selected',
|
|
||||||
'share_modal.multiple_users_selected': '{count} users selected',
|
|
||||||
'share_modal.custom_title': 'Custom Title (optional)',
|
|
||||||
'share_modal.title_placeholder': 'Enter a custom title',
|
|
||||||
'share_modal.message': 'Message (optional)',
|
|
||||||
'share_modal.message_placeholder': 'Add a message for recipients',
|
|
||||||
'share_modal.share': 'Share',
|
|
||||||
'share_modal.sharing': 'Sharing...',
|
|
||||||
'share_modal.share_error': 'Error sharing prompt',
|
|
||||||
|
|
||||||
// Prompt Settings
|
|
||||||
'prompt_settings.title': 'Prompt Settings',
|
|
||||||
'prompt_settings.content_placeholder': 'Settings content will be added here in future updates.',
|
|
||||||
|
|
||||||
// Chat Area
|
|
||||||
'chat.continue_conversation': 'Continue conversation...',
|
|
||||||
'chat.enter_message': 'Enter message...',
|
|
||||||
'chat.remove_file': 'Remove file',
|
|
||||||
'chat.attach_file': 'Attach file',
|
|
||||||
'chat.you': 'You',
|
|
||||||
'chat.click_to_open': 'Click to open',
|
|
||||||
'chat.preview_document': 'Preview document',
|
|
||||||
'chat.download_document': 'Download document',
|
|
||||||
'chat.workflow_failed': 'Workflow failed.',
|
|
||||||
'chat.retry_workflow': 'Try again',
|
|
||||||
'chat.sending_followup': 'Sending follow-up message...',
|
|
||||||
'chat.sending_message': 'Sending message...',
|
|
||||||
'chat.error_prefix': 'Error:',
|
|
||||||
'chat.error_loading_messages': 'Error loading messages:',
|
|
||||||
'chat.loading_workflow_messages': 'Loading workflow messages...',
|
|
||||||
'chat.start_conversation': 'Start a conversation by entering a message, selecting a template, or continuing a previous workflow...',
|
|
||||||
|
|
||||||
// Chat Input Area
|
|
||||||
'chat.input.continue_workflow': 'Continue the conversation...',
|
|
||||||
'chat.input.enter_message': 'Or enter your message...',
|
|
||||||
'chat.input.continuing_workflow': 'Continuing workflow',
|
|
||||||
'chat.input.workflow': 'Workflow',
|
|
||||||
'chat.input.files_attached': 'file',
|
|
||||||
'chat.input.files_attached_plural': 'files',
|
|
||||||
'chat.input.files_attached_label': 'attached',
|
|
||||||
'chat.input.error_prefix': 'Error:',
|
|
||||||
'chat.input.attach_files': 'Attach Files',
|
|
||||||
'chat.input.sending': 'Sending...',
|
|
||||||
'chat.input.processing': 'Processing...',
|
|
||||||
'chat.input.continue': 'Continue',
|
|
||||||
'chat.input.send': 'Send',
|
|
||||||
'chat.input.stop': 'Stop',
|
|
||||||
'chat.input.stopping': 'Stopping...',
|
|
||||||
'chat.input.drop_files_here': 'Drop files here to attach',
|
|
||||||
'chat.input.drop_disabled': 'File drop disabled during workflow',
|
|
||||||
'chat.input.new_chat': 'New Chat',
|
|
||||||
'chat.input.using_prompt': 'Using prompt:',
|
|
||||||
'chat.input.select_prompt': 'Select a prompt...',
|
|
||||||
'chat.input.loading_prompts': 'Loading prompts...',
|
|
||||||
'chat.input.clear_prompt': 'Clear prompt',
|
|
||||||
|
|
||||||
// File Preview
|
|
||||||
'file_preview.loading': 'Loading preview...',
|
|
||||||
'file_preview.error': 'Error',
|
|
||||||
'file_preview.no_preview': 'No preview available',
|
|
||||||
'file_preview.close_preview': 'Close preview',
|
|
||||||
'file_preview.python': 'Python',
|
|
||||||
|
|
||||||
// Chat History
|
|
||||||
'chat_history.loading': 'Loading workflows...',
|
|
||||||
'chat_history.error_loading': 'Error loading workflows:',
|
|
||||||
'chat_history.try_again': 'Try Again',
|
|
||||||
'chat_history.title': 'Workflow History',
|
|
||||||
'chat_history.workflow_count': 'Workflow',
|
|
||||||
'chat_history.workflow_count_plural': 'Workflows',
|
|
||||||
'chat_history.empty_state': 'No workflows available',
|
|
||||||
'chat_history.confirm_delete': 'Are you sure you want to delete workflow "{id}..."?',
|
|
||||||
'chat_history.no_message_content': 'No message content available',
|
|
||||||
'chat_history.unknown_date': 'Unknown date',
|
|
||||||
'chat_history.invalid_date': 'Invalid date',
|
|
||||||
'chat_history.started': 'Started:',
|
|
||||||
'chat_history.last_activity': 'Last Activity:',
|
|
||||||
'chat_history.round': 'Round',
|
|
||||||
'chat_history.resume_tooltip': 'Resume workflow',
|
|
||||||
'chat_history.delete_tooltip': 'Delete workflow',
|
|
||||||
'chat_history.deleting': 'Deleting workflow...',
|
|
||||||
|
|
||||||
// Chat Messages
|
|
||||||
'chat.messages.no_workflow_selected': 'No workflow selected',
|
|
||||||
'chat.messages.no_workflow_selected_description': 'Select a workflow from the list or start a new workflow',
|
|
||||||
'chat.messages.loading_progress': 'Loading progress...',
|
|
||||||
'chat.messages.tasks': 'Tasks',
|
|
||||||
'chat.messages.workflow_progress': 'Workflow Progress',
|
|
||||||
'chat.messages.analyzing_workflow': 'Analyzing workflow...',
|
|
||||||
'chat.messages.scroll_to_bottom_btn': 'Scroll to bottom',
|
|
||||||
// Workflow Status
|
|
||||||
'status.error': 'ERROR',
|
|
||||||
'status.failed': 'FAILED',
|
|
||||||
'status.stopped': 'STOPPED',
|
|
||||||
'status.cancelled': 'CANCELLED',
|
|
||||||
'status.running': 'RUNNING',
|
|
||||||
'status.processing': 'PROCESSING',
|
|
||||||
'status.completed': 'COMPLETED',
|
|
||||||
'status.pending': 'PENDING',
|
|
||||||
|
|
||||||
// Files
|
|
||||||
'files.unknown_size': 'Unknown Size',
|
|
||||||
'files.unknown_date': 'Unknown Date',
|
|
||||||
'files.source.uploaded': 'Uploaded',
|
|
||||||
'files.source.ai_created': 'AI-created',
|
|
||||||
'files.source.shared': 'Shared',
|
|
||||||
'files.source.unknown': 'Unknown',
|
|
||||||
'files.preview_tooltip': 'Preview file',
|
|
||||||
'files.download_tooltip': 'Download file',
|
|
||||||
'files.delete_tooltip': 'Delete file',
|
|
||||||
'files.delete_confirm_tooltip': 'Click again to confirm deletion',
|
|
||||||
'files.downloading': 'Downloading...',
|
|
||||||
'files.deleting': 'Deleting...',
|
|
||||||
'files.delete_confirm': 'Click to confirm...',
|
|
||||||
'files.no_files': 'No files found.',
|
|
||||||
'files.no_shared_files': 'No shared files found.',
|
|
||||||
'files.no_ai_files': 'No AI-created files found.',
|
|
||||||
'files.no_uploaded_files': 'No uploaded files found.',
|
|
||||||
'files.header.name': 'Name',
|
|
||||||
'files.header.type': 'Type',
|
|
||||||
'files.header.size': 'Size',
|
|
||||||
'files.header.date': 'Date',
|
|
||||||
'files.selector.title': 'Select files',
|
|
||||||
'files.selector.tab.all': 'All files',
|
|
||||||
'files.selector.tab.uploads': 'Uploaded',
|
|
||||||
'files.selector.tab.created': 'AI-created',
|
|
||||||
'files.selector.tab.shared': 'Shared',
|
|
||||||
'files.selector.select_all': 'Select all',
|
|
||||||
'files.selector.deselect_all': 'Deselect all',
|
|
||||||
'files.selector.file_selected': 'File',
|
|
||||||
'files.selector.files_selected': 'Files',
|
|
||||||
'files.selector.selected_suffix': 'selected',
|
|
||||||
'files.selector.upload_new': 'Upload new file',
|
|
||||||
'files.selector.loading': 'Loading files...',
|
|
||||||
'files.selector.error_loading': 'Error loading files:',
|
|
||||||
'files.upload.title': 'Upload file',
|
|
||||||
'files.upload.drop_here': 'Drop file here...',
|
|
||||||
'files.upload.uploading': 'Uploading...',
|
|
||||||
'files.upload.drag_files': 'Drag files here',
|
|
||||||
'files.upload.or': 'or',
|
|
||||||
'files.upload.browse': 'Browse',
|
|
||||||
'files.upload.selected_file': 'Selected file:',
|
|
||||||
'files.upload.upload_button': 'Upload',
|
|
||||||
'files.upload.uploading_button': 'Uploading...',
|
|
||||||
'files.upload.success': 'File uploaded successfully!',
|
|
||||||
'files.upload.error': 'An error occurred while uploading.',
|
|
||||||
'files.upload.unexpected_error': 'An unexpected error occurred while uploading.',
|
|
||||||
|
|
||||||
// Files Page Upload Actions
|
|
||||||
'files.drop_zone': 'Drop files here',
|
|
||||||
'files.upload_button': 'Upload Files',
|
|
||||||
'files.uploading_button': 'Uploading...',
|
|
||||||
'files.upload_aria_label': 'Upload files',
|
|
||||||
|
|
||||||
// Files Page
|
|
||||||
'files.title': 'Files',
|
|
||||||
'files.table.title': 'Files',
|
|
||||||
'files.error.loading': 'Error loading files:',
|
|
||||||
'files.button.retry': 'Retry',
|
|
||||||
'files.page.tab.all': 'All Files',
|
|
||||||
'files.page.tab.uploads': 'My Uploads',
|
|
||||||
'files.page.tab.created': 'Created Files',
|
|
||||||
'files.page.tab.shared': 'Shared Files',
|
|
||||||
'files.page.add_file': 'Add File',
|
|
||||||
'files.page.loading': 'Loading files...',
|
|
||||||
'files.page.error': 'Error:',
|
|
||||||
|
|
||||||
// File Table Columns
|
|
||||||
'files.column.name': 'Name',
|
|
||||||
'files.column.filename': 'Filename',
|
|
||||||
'files.column.type': 'Type',
|
|
||||||
'files.column.mimetype': 'MIME Type',
|
|
||||||
'files.column.size': 'Size',
|
|
||||||
'files.column.filesize': 'File Size',
|
|
||||||
'files.column.created': 'Created',
|
|
||||||
'files.column.creationdate': 'Creation Date',
|
|
||||||
'files.column.source': 'Source',
|
|
||||||
|
|
||||||
// File Types
|
|
||||||
'files.type.image': 'Image',
|
|
||||||
'files.type.pdf': 'PDF',
|
|
||||||
'files.type.document': 'Document',
|
|
||||||
'files.type.spreadsheet': 'Spreadsheet',
|
|
||||||
'files.type.text': 'Text',
|
|
||||||
'files.type.video': 'Video',
|
|
||||||
'files.type.audio': 'Audio',
|
|
||||||
'files.type.file': 'File',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// File Actions
|
|
||||||
'files.action.preview': 'Preview',
|
|
||||||
'files.action.download': 'Download',
|
|
||||||
'files.action.delete': 'Delete',
|
|
||||||
'files.delete.confirm': 'Are you sure you want to delete the file "{name}"?',
|
|
||||||
|
|
||||||
// File Preview
|
|
||||||
'files.preview.title': 'File Preview',
|
|
||||||
'files.preview.loading': 'Loading preview...',
|
|
||||||
'files.preview.unsupported': 'Preview not available for this file type',
|
|
||||||
'files.preview.error': 'Error loading preview',
|
|
||||||
'files.preview.textInPdfFile': 'Text Preview',
|
|
||||||
'files.preview.pdfFileCorrupted': 'This file appears to be corrupted. It has a PDF extension but contains text content. Please re-upload the file if possible.',
|
|
||||||
|
|
||||||
// Workflows Page
|
|
||||||
'workflows.title': 'Workflows',
|
|
||||||
'workflows.table.title': 'Workflows',
|
|
||||||
'workflows.error.loading': 'Error loading workflows:',
|
|
||||||
'workflows.button.retry': 'Retry',
|
|
||||||
'workflows.table.empty': 'No workflows found',
|
|
||||||
|
|
||||||
// Workflow Table Columns
|
|
||||||
'workflows.column.id': 'ID',
|
|
||||||
'workflows.column.name': 'Name',
|
|
||||||
'workflows.column.status': 'Status',
|
|
||||||
'workflows.column.round': 'Round',
|
|
||||||
'workflows.column.started': 'Started',
|
|
||||||
'workflows.column.lastActivity': 'Last Activity',
|
|
||||||
'workflows.column.messages': 'Messages',
|
|
||||||
|
|
||||||
// Workflow Status
|
|
||||||
'workflows.status.running': 'Running',
|
|
||||||
'workflows.status.completed': 'Completed',
|
|
||||||
'workflows.status.failed': 'Failed',
|
|
||||||
'workflows.status.stopped': 'Stopped',
|
|
||||||
'workflows.status.pending': 'Pending',
|
|
||||||
|
|
||||||
// Workflow Actions
|
|
||||||
'workflows.action.stop': 'Stop',
|
|
||||||
'workflows.action.delete': 'Delete',
|
|
||||||
'workflows.action.stop.tooltip': 'Stop workflow',
|
|
||||||
'workflows.action.delete.tooltip': 'Delete workflow',
|
|
||||||
|
|
||||||
// Workflow Messages
|
|
||||||
'workflows.unnamed': 'Unnamed Workflow',
|
|
||||||
'workflows.delete.confirm': 'Are you sure you want to delete workflow "{name}"?',
|
|
||||||
'workflows.loading': 'Loading workflows...',
|
|
||||||
|
|
||||||
// FormGenerator
|
|
||||||
'formgen.search.placeholder': 'Search...',
|
|
||||||
'formgen.refresh.tooltip': 'Refresh data',
|
|
||||||
'formgen.filter.yes': 'Yes',
|
|
||||||
'formgen.filter.no': 'No',
|
|
||||||
'formgen.filter.clear': 'Clear filter',
|
|
||||||
'formgen.filter.placeholder': 'Filter {column}',
|
|
||||||
'formgen.actions.column': 'Actions',
|
|
||||||
'formgen.pagination.info': 'Page {page} of {total} ({count} items)',
|
|
||||||
'formgen.pagination.pageSize': 'Items per page:',
|
|
||||||
'formgen.pagination.first': 'First page',
|
|
||||||
'formgen.pagination.prev': 'Previous page',
|
|
||||||
'formgen.pagination.next': 'Next page',
|
|
||||||
'formgen.pagination.last': 'Last page',
|
|
||||||
'formgen.select.all': 'Select all items',
|
|
||||||
'formgen.select.item': 'Select this item',
|
|
||||||
'formgen.select.disabled': 'This item cannot be selected',
|
|
||||||
'formgen.delete.multiple': 'Delete ({count})',
|
|
||||||
'formgen.delete.confirm_multiple': 'Are you sure you want to delete the {count} selected items?',
|
|
||||||
|
|
||||||
// Prompts
|
|
||||||
'prompts.title': 'Prompts',
|
|
||||||
'prompts.subtitle': 'Manage your prompts',
|
|
||||||
'prompts.description': 'Create and manage prompts for your AI assistant',
|
|
||||||
'prompts.new_button': 'New Prompt',
|
|
||||||
'prompts.addNew': 'Add Prompt',
|
|
||||||
'prompts.creating': 'Creating...',
|
|
||||||
'prompts.column.name': 'Name',
|
|
||||||
'prompts.column.content': 'Content',
|
|
||||||
'prompts.column.mandateId': 'Mandate ID',
|
|
||||||
'prompts.unnamed': 'Unnamed',
|
|
||||||
'prompts.action.edit': 'Edit',
|
|
||||||
'prompts.action.copy': 'Copy',
|
|
||||||
'prompts.action.delete': 'Delete',
|
|
||||||
'prompts.action.delete.disabled': 'No permission to delete prompt',
|
|
||||||
'prompts.delete.confirm': 'Are you sure you want to delete "{name}"?',
|
|
||||||
'prompts.delete.confirmMultiple': 'Are you sure you want to delete {count} prompts?',
|
|
||||||
'prompts.field.name': 'Prompt Name',
|
|
||||||
'prompts.field.content': 'Prompt Content',
|
|
||||||
'prompts.validation.nameRequired': 'Prompt name cannot be empty',
|
|
||||||
'prompts.validation.nameTooLong': 'Prompt name cannot exceed 100 characters',
|
|
||||||
'prompts.validation.contentRequired': 'Prompt content cannot be empty',
|
|
||||||
'prompts.validation.contentTooLong': 'Prompt content cannot exceed 10,000 characters',
|
|
||||||
'prompts.error.loading': 'Error loading prompts:',
|
|
||||||
'prompts.modal.edit.title': 'Edit Prompt',
|
|
||||||
'prompts.modal.edit.save': 'Save Changes',
|
|
||||||
'prompts.modal.create.title': 'Create New Prompt',
|
|
||||||
'prompts.modal.create.save': 'Create Prompt',
|
|
||||||
'prompts.create.success': 'Prompt created successfully',
|
|
||||||
'prompts.create.error': 'Error creating prompt',
|
|
||||||
|
|
||||||
// Users/Members
|
|
||||||
'users.title': 'Users',
|
|
||||||
'users.column.username': 'Username',
|
|
||||||
'users.column.name': 'Name',
|
|
||||||
'users.column.email': 'Email',
|
|
||||||
'users.column.password': 'Password',
|
|
||||||
'users.column.language': 'Language',
|
|
||||||
'users.column.privilege': 'Privilege',
|
|
||||||
'users.column.enabled': 'Enabled',
|
|
||||||
'users.column.authAuthority': 'Auth Authority',
|
|
||||||
'users.password.placeholder': 'Enter password',
|
|
||||||
'users.noUsername': 'No Username',
|
|
||||||
'users.noName': 'No Name',
|
|
||||||
'users.noEmail': 'No Email',
|
|
||||||
'users.noLanguage': 'No Language',
|
|
||||||
'users.noPrivilege': 'No Privilege',
|
|
||||||
'users.noAuthAuthority': 'No Auth Authority',
|
|
||||||
'users.privilege.viewer': 'Viewer',
|
|
||||||
'users.privilege.user': 'User',
|
|
||||||
'users.privilege.admin': 'Admin',
|
|
||||||
'users.privilege.sysadmin': 'Sysadmin',
|
|
||||||
'users.enabled.yes': 'Yes',
|
|
||||||
'users.enabled.no': 'No',
|
|
||||||
'users.auth.local': 'Local',
|
|
||||||
'users.auth.msft': 'Microsoft',
|
|
||||||
'users.actions.edit': 'Edit',
|
|
||||||
'users.actions.delete': 'Delete',
|
|
||||||
'users.edit.title': 'Edit User',
|
|
||||||
'users.add.title': 'Add User',
|
|
||||||
'users.add.button': 'Add User',
|
|
||||||
'users.add.create': 'Create User',
|
|
||||||
'users.delete.title': 'Delete User',
|
|
||||||
'users.delete.message': 'Are you sure you want to delete this user?',
|
|
||||||
'users.delete.confirm': 'Are you sure you want to delete "{name}"?',
|
|
||||||
'users.delete.warning': 'This action cannot be undone.',
|
|
||||||
'users.action.edit': 'Edit',
|
|
||||||
'users.action.delete': 'Delete',
|
|
||||||
'users.delete.confirmMultiple': 'Are you sure you want to delete {count} users?',
|
|
||||||
'users.error.loading': 'Error loading users:',
|
|
||||||
|
|
||||||
// Team Members
|
|
||||||
'team-members.title': 'Team Members',
|
|
||||||
'team-members.subtitle': 'Manage your team members',
|
|
||||||
'team-members.description': 'Manage team members, set permissions, and configure collaboration settings',
|
|
||||||
'team-members.new_button': 'Add Member',
|
|
||||||
'team-members.action.edit': 'Edit',
|
|
||||||
'team-members.action.delete': 'Delete',
|
|
||||||
'team-members.field.username': 'Username',
|
|
||||||
'team-members.field.email': 'Email',
|
|
||||||
'team-members.field.password': 'Password',
|
|
||||||
'team-members.field.fullName': 'Full Name',
|
|
||||||
'team-members.field.privilege': 'Privilege',
|
|
||||||
'team-members.modal.create.title': 'Create New Team Member',
|
|
||||||
'team-members.create.success': 'Team member created successfully',
|
|
||||||
'team-members.create.error': 'Error creating team member',
|
|
||||||
|
|
||||||
// SharePoint Test
|
|
||||||
'sharepoint.title': 'SharePoint Test',
|
|
||||||
'sharepoint.table.title': 'SharePoint Documents',
|
|
||||||
'sharepoint.error.loading': 'Error loading SharePoint documents:',
|
|
||||||
'sharepoint.button.retry': 'Retry',
|
|
||||||
'sharepoint.button.testConnection': 'Test Connection',
|
|
||||||
'sharepoint.button.listDocuments': 'List Documents',
|
|
||||||
'sharepoint.button.discoverSites': 'Discover Sites',
|
|
||||||
'sharepoint.column.documentName': 'Document Name',
|
|
||||||
'sharepoint.column.mimeType': 'MIME Type',
|
|
||||||
'sharepoint.column.size': 'Size',
|
|
||||||
'sharepoint.column.path': 'Path',
|
|
||||||
'sharepoint.action.view': 'View',
|
|
||||||
'sharepoint.action.download': 'Download',
|
|
||||||
'sharepoint.connections.title': 'Microsoft Connections',
|
|
||||||
'sharepoint.connections.noConnections': 'No Microsoft connections found. Please create a connection first.',
|
|
||||||
'sharepoint.connections.loading': 'Loading connections...',
|
|
||||||
'sharepoint.sites.discovered': 'Discovered Sites',
|
|
||||||
'sharepoint.sites.noSites': 'No SharePoint sites found',
|
|
||||||
'sharepoint.sites.authError': 'Authentication token expired or invalid. Please reconnect your Microsoft account.',
|
|
||||||
'sharepoint.sites.retryConnection': 'Try reconnecting your Microsoft account in the Connections page.',
|
|
||||||
'sharepoint.form.siteUrl': 'SharePoint Site URL',
|
|
||||||
'sharepoint.form.folderPaths': 'Folder Paths',
|
|
||||||
|
|
||||||
// Speech
|
|
||||||
'speech.title': 'Speech Integration',
|
|
||||||
'speech.subtitle': 'Powered by',
|
|
||||||
'speech.signup.title': 'Speech Integration',
|
|
||||||
'speech.signup.subtitle': 'Powered by',
|
|
||||||
|
|
||||||
'speech.info.va': 'Virtual Assistant (VA)',
|
|
||||||
'speech.info.va_description': 'Give customers a fast and efficient self-service for voice and text queries that\'s available 24/7.',
|
|
||||||
'speech.info.sa': 'Speech Analytics (SA)',
|
|
||||||
'speech.info.sa_description': 'Automatically monitor 100% of conversations to get valuable insights for your business.',
|
|
||||||
'speech.info.vb': 'Voice Biometrics (VB)',
|
|
||||||
'speech.info.vb_description': 'Identify and authenticate callers in seconds with continuous verification and security.',
|
|
||||||
'speech.info.ka': 'Knowledge Agent (KA)',
|
|
||||||
'speech.info.ka_description': 'Unify and deliver info to your customers and staff wherever and whenever they need it.',
|
|
||||||
'speech.info.cp': 'Chat Platform (CP)',
|
|
||||||
'speech.info.cp_description': 'Deliver assistance in live chat and deploy intelligent chatbots in all channels.',
|
|
||||||
'speech.info.aa': 'Agent Assist (AA)',
|
|
||||||
'speech.info.aa_description': 'Put everything your agents need at their fingertips, with a unified agent desktop.',
|
|
||||||
|
|
||||||
'speech.info.about': 'Revolutionary Telephony Integration with Spitch.ai',
|
|
||||||
'speech.info.about_intro': 'Experience the future of client communication through our strategic partnership with Spitch.ai. This groundbreaking integration transforms your PowerOn platform into an intelligent telephony system that seamlessly connects external clients with companies.',
|
|
||||||
'speech.info.workflow_title': 'Seamless Client Workflow:',
|
|
||||||
'speech.info.workflow_description': 'From registration to technical setup - your client registers with PowerOn for telephony services, uploads documents, and automatically receives a technical SIP number from Spitch. Call forwarding can be activated or deactivated at any time, ensuring maximum flexibility and BCM safety.',
|
|
||||||
'speech.info.ai_title': 'AI-Powered Document Generation:',
|
|
||||||
'speech.info.ai_description': 'Our already active document extraction engine automatically generates personalized documents for Spitch based on client-specific data. The AI uses FAQ databases, employee information, and service details to make every call contextual and highly personalized.',
|
|
||||||
'speech.info.sync_title': 'Real-time Data Synchronization:',
|
|
||||||
'speech.info.sync_description': 'Spitch checks client authorization with PowerOn before each call, while all data changes are centrally initiated by PowerOn. Call transcripts are stored in real-time in your PowerOn database with complete client isolation and security. In case of failures, calls are automatically blocked to ensure integrity.',
|
|
||||||
'speech.info.cost_title': 'Cost Savings & Efficiency:',
|
|
||||||
'speech.info.cost_description': 'Clients can switch to the technical SIP number at any time and save significant telephony costs. The integration works like another connector (Outlook, SharePoint) and is seamlessly integrated into your existing workflow.',
|
|
||||||
'speech.info.about_link': 'Learn more',
|
|
||||||
|
|
||||||
'speech.signup.button': 'Connect',
|
|
||||||
'speech.signup.back': 'Back to Speech Integration',
|
|
||||||
'speech.signup.submit': 'Create Mandate',
|
|
||||||
'speech.signup.cancel': 'Cancel',
|
|
||||||
|
|
||||||
'speech.signup.company_info': 'Company Information',
|
|
||||||
'speech.signup.company_name': 'Company Name',
|
|
||||||
'speech.signup.company_name_placeholder': 'Enter your company name',
|
|
||||||
'speech.signup.industry': 'Industry',
|
|
||||||
'speech.signup.industry_placeholder': 'e.g. Financial Services, Technology, etc.',
|
|
||||||
'speech.signup.business_hours': 'Business Hours',
|
|
||||||
'speech.signup.timezone': 'Timezone',
|
|
||||||
|
|
||||||
'speech.signup.contact_info': 'Contact Information',
|
|
||||||
'speech.signup.email': 'Email Address',
|
|
||||||
'speech.signup.email_placeholder': 'contact@company.com',
|
|
||||||
'speech.signup.phone': 'Phone Number',
|
|
||||||
'speech.signup.phone_placeholder': '+41 123 456 789',
|
|
||||||
'speech.signup.street': 'Street',
|
|
||||||
'speech.signup.postal_code': 'Postal Code',
|
|
||||||
'speech.signup.city': 'City',
|
|
||||||
'speech.signup.country': 'Country',
|
|
||||||
|
|
||||||
'speech.signup.contacts_setup': 'Setup Contacts',
|
|
||||||
'speech.signup.contacts_description': 'Would you like to setup contacts for your mandate now? You can also do this later in settings.',
|
|
||||||
'speech.signup.setup_contacts': 'Setup Contacts',
|
|
||||||
'speech.signup.skip_for_now': 'Skip for Now',
|
|
||||||
|
|
||||||
'speech.signup.company_required': 'Company name is required',
|
|
||||||
'speech.signup.industry_required': 'Industry is required',
|
|
||||||
'speech.signup.email_required': 'Email address is required',
|
|
||||||
'speech.signup.email_invalid': 'Please enter a valid email address',
|
|
||||||
'speech.signup.phone_required': 'Phone number is required',
|
|
||||||
'speech.signup.street_required': 'Street is required',
|
|
||||||
'speech.signup.postal_code_required': 'Postal code is required',
|
|
||||||
'speech.signup.city_required': 'City is required',
|
|
||||||
'speech.signup.country_required': 'Country is required',
|
|
||||||
|
|
||||||
'speech.status.submitted': '✓ Mandate Submitted',
|
|
||||||
'speech.status.reset': 'Start Over',
|
|
||||||
|
|
||||||
'speech.confirmation.title': 'Mandate Submitted Successfully!',
|
|
||||||
'speech.confirmation.message': 'Thank you for your interest in our Speech Integration powered by Spitch.ai. We have received your mandate and will review it shortly.',
|
|
||||||
'speech.confirmation.submitted_data': 'Submitted Data:',
|
|
||||||
'speech.confirmation.company': 'Company',
|
|
||||||
'speech.confirmation.industry': 'Industry',
|
|
||||||
'speech.confirmation.email': 'Email',
|
|
||||||
'speech.confirmation.phone': 'Phone',
|
|
||||||
'speech.confirmation.address': 'Address',
|
|
||||||
'speech.confirmation.timezone': 'Timezone',
|
|
||||||
'speech.confirmation.back': 'Back to Speech Integration',
|
|
||||||
'speech.confirmation.reset': 'Start Over',
|
|
||||||
'speech.confirmation.next_steps': 'What happens next?',
|
|
||||||
'speech.confirmation.email_confirmation': 'Email Confirmation',
|
|
||||||
'speech.confirmation.email_confirmation_desc': 'You will receive a confirmation email within the next few minutes.',
|
|
||||||
'speech.confirmation.review_process': 'Review Process',
|
|
||||||
'speech.confirmation.review_process_desc': 'Our team will review your mandate within 1-2 business days.',
|
|
||||||
'speech.confirmation.setup_call': 'Setup Call',
|
|
||||||
'speech.confirmation.setup_call_desc': 'If approved, we\'ll schedule a setup call to configure your integration.',
|
|
||||||
'speech.confirmation.questions': 'Questions?',
|
|
||||||
'speech.confirmation.questions_desc': 'If you have any questions about your mandate or the integration process, please don\'t hesitate to contact our support team.',
|
|
||||||
'speech.confirmation.transcript_management': 'Transcript Management',
|
|
||||||
'speech.confirmation.speech_settings': 'Speech Settings',
|
|
||||||
|
|
||||||
'speech.transcripts.title': 'Transcript Management',
|
|
||||||
'speech.transcripts.new_transcript': 'New Transcript',
|
|
||||||
'speech.transcripts.recent_transcripts': 'Recent Transcripts',
|
|
||||||
'speech.transcripts.no_transcripts': 'No transcripts available',
|
|
||||||
'speech.transcripts.date': 'Date',
|
|
||||||
'speech.transcripts.duration': 'Duration',
|
|
||||||
'speech.transcripts.status': 'Status',
|
|
||||||
'speech.transcripts.transcript': 'Transcript',
|
|
||||||
'speech.transcripts.processing': 'Processing transcript...',
|
|
||||||
'speech.transcripts.status.completed': 'Completed',
|
|
||||||
'speech.transcripts.status.processing': 'Processing',
|
|
||||||
'speech.transcripts.status.failed': 'Failed',
|
|
||||||
'speech.transcripts.access_denied_title': 'Access Denied',
|
|
||||||
'speech.transcripts.access_denied_message': 'You must first sign up for speech integration to access transcript management.',
|
|
||||||
'speech.transcripts.sign_up_now': 'Sign Up Now',
|
|
||||||
'speech.transcripts.subject': 'Subject',
|
|
||||||
'speech.transcripts.start_time': 'Start Time',
|
|
||||||
'speech.transcripts.end_time': 'End Time',
|
|
||||||
'speech.transcripts.caller': 'Caller',
|
|
||||||
'speech.transcripts.recipient': 'Recipient',
|
|
||||||
'speech.transcripts.tags': 'Tags',
|
|
||||||
'speech.transcripts.created': 'Created',
|
|
||||||
'speech.transcripts.view': 'View',
|
|
||||||
'speech.transcripts.download': 'Download',
|
|
||||||
|
|
||||||
'speech.settings.title': 'Speech Integration Settings',
|
|
||||||
'speech.settings.description': 'Manage your speech integration configuration and preferences.',
|
|
||||||
'speech.settings.company_info': 'Company Information',
|
|
||||||
'speech.settings.contact_info': 'Contact Information',
|
|
||||||
'speech.settings.business_hours': 'Business Hours & Timezone',
|
|
||||||
'speech.settings.save': 'Save Changes',
|
|
||||||
'speech.settings.saving': 'Saving...',
|
|
||||||
'speech.settings.save_success': 'Settings saved successfully!',
|
|
||||||
'speech.settings.save_error': 'Failed to save settings. Please try again.',
|
|
||||||
'speech.settings.reset': 'Reset to Default',
|
|
||||||
'speech.settings.reset_confirm': 'Are you sure you want to reset all speech integration settings? This action cannot be undone.',
|
|
||||||
'speech.settings.reset_success': 'Settings have been reset successfully.',
|
|
||||||
'speech.settings.no_data': 'No speech integration data found. Please sign up first to access settings.',
|
|
||||||
'speech.settings.sign_up_now': 'Sign Up Now',
|
|
||||||
|
|
||||||
// Message Overlay Types
|
|
||||||
'message.success.title': 'Success',
|
|
||||||
'message.success.upload': 'File uploaded successfully!',
|
|
||||||
'message.info.title': 'Information',
|
|
||||||
'message.info.processing': 'Processing your request...',
|
|
||||||
'message.error.title': 'Error',
|
|
||||||
'message.error.upload_failed': 'Upload failed. Please try again.',
|
|
||||||
|
|
||||||
// Warning Messages
|
|
||||||
'warning.duplicate_file.title': 'File Already Exists',
|
|
||||||
'warning.duplicate_file.message': 'The file "{fileName}" already exists with identical content. The existing file will be reused.',
|
|
||||||
|
|
||||||
// Administration
|
|
||||||
'administration.title': 'Administration',
|
|
||||||
'administration.description': 'Administration and management tools',
|
|
||||||
'administration.subtitle': 'Administration and management tools',
|
|
||||||
'administration.intro.description': 'This section contains all administration and management tools for your workspace.',
|
|
||||||
'administration.features.title': 'Available Tools',
|
|
||||||
'administration.features.description': 'Management tools include:',
|
|
||||||
'administration.features.file_management': 'File Management - Upload and organize documents',
|
|
||||||
'administration.features.user_management': 'User Management - Manage team members and permissions',
|
|
||||||
'administration.features.system_settings': 'System Settings - Configure workspace settings',
|
|
||||||
'administration.features.data_management': 'Data Management - Handle data imports and exports',
|
|
||||||
|
|
||||||
// Admin pages
|
|
||||||
'admin.mandates.title': 'Mandates',
|
|
||||||
'admin.mandates.subtitle': 'Manage mandates and permissions',
|
|
||||||
'admin.mandates.description': 'Mandate management',
|
|
||||||
'admin.mandates.description_text': 'Manage mandates and their associated permissions.',
|
|
||||||
|
|
||||||
'admin.rbac-rules.title': 'RBAC Rules',
|
|
||||||
'admin.rbac-rules.subtitle': 'Role-Based Access Control rules',
|
|
||||||
'admin.rbac-rules.description': 'RBAC rules management',
|
|
||||||
'admin.rbac-rules.description_text': 'Configure and manage Role-Based Access Control rules.',
|
|
||||||
|
|
||||||
'admin.rbac-role.title': 'RBAC Roles',
|
|
||||||
'admin.rbac-role.subtitle': 'Role management',
|
|
||||||
'admin.rbac-role.description': 'RBAC role management',
|
|
||||||
'admin.rbac-role.description_text': 'Create and manage RBAC roles and their permissions.',
|
|
||||||
|
|
||||||
'admin.admin-settings.title': 'Admin Settings',
|
|
||||||
'admin.admin-settings.subtitle': 'Administrative settings',
|
|
||||||
'admin.admin-settings.description': 'Administrative settings',
|
|
||||||
'admin.admin-settings.description_text': 'Configure administrative settings and system preferences.',
|
|
||||||
|
|
||||||
// Start page
|
|
||||||
'start.title': 'Start',
|
|
||||||
'start.description': 'Welcome to your workspace',
|
|
||||||
'start.subtitle': 'Welcome to your workspace',
|
|
||||||
'start.intro.description': 'This is your starting point for accessing all workspace features and tools.',
|
|
||||||
'start.features.title': 'Quick Access',
|
|
||||||
'start.features.description': 'Get started with:',
|
|
||||||
'start.features.quick_access': 'Quick Access - Jump to frequently used features',
|
|
||||||
'start.features.recent_activities': 'Recent Activities - View your latest work',
|
|
||||||
'start.features.overview': 'Overview - See workspace status and updates',
|
|
||||||
'start.features.navigation': 'Navigation - Explore all available tools',
|
|
||||||
|
|
||||||
// Projects page
|
|
||||||
'projects.title': 'Projects',
|
|
||||||
'projects.subtitle': 'Project Management',
|
|
||||||
'projects.description': 'Project management and organization',
|
|
||||||
'projects.description_text': 'Search for locations by address or coordinates, or use natural language to create and manage projects.',
|
|
||||||
'projects.command.placeholder': 'Enter a command (e.g., "Create a new project named \'Main Street 42\'")',
|
|
||||||
'projects.command.empty': 'No commands executed yet. Send a command to see results here.',
|
|
||||||
|
|
||||||
// Data Management page
|
|
||||||
'data-management.title': 'Data Management',
|
|
||||||
'data-management.subtitle': 'Data Management',
|
|
||||||
'data-management.description': 'Data management with tables',
|
|
||||||
'data-management.description_text': 'Manage data through tables. Select a table or use natural language to execute commands.',
|
|
||||||
'data-management.command.placeholder': 'Enter a command (e.g., "Create a new project named \'Main Street 42\'")',
|
|
||||||
'data-management.command.empty': 'No commands executed yet. Send a command to see results here.',
|
|
||||||
|
|
||||||
// Drag and Drop
|
|
||||||
'dragdrop.overlay.default_text': 'Drop files here',
|
|
||||||
'dragdrop.overlay.default_subtext': 'You can also click the upload button',
|
|
||||||
'dragdrop.overlay.processing': 'Processing files...',
|
|
||||||
'dragdrop.overlay.error': 'Error processing files',
|
|
||||||
};
|
|
||||||
|
|
@ -1,784 +0,0 @@
|
||||||
export default {
|
|
||||||
// Navigation
|
|
||||||
'nav.dashboard': 'Centre d\'activité',
|
|
||||||
'nav.files': 'Fichiers',
|
|
||||||
'nav.team': 'Espace équipe',
|
|
||||||
'nav.workflows': 'Workflows',
|
|
||||||
'nav.connections': 'Connections',
|
|
||||||
'nav.settings': 'Paramètres',
|
|
||||||
'nav.testSharepoint': 'Test SharePoint',
|
|
||||||
'nav.speech': 'Parole',
|
|
||||||
'nav.transcript_management': 'Gestion des Transcriptions',
|
|
||||||
|
|
||||||
// Settings page
|
|
||||||
'settings.title': 'Paramètres',
|
|
||||||
'settings.appearance': 'Apparence',
|
|
||||||
'settings.language': 'Langue',
|
|
||||||
'settings.about': 'À propos',
|
|
||||||
'settings.version': 'Version',
|
|
||||||
'settings.theme': 'Thème',
|
|
||||||
'settings.theme.description': 'Basculer entre le mode clair et sombre',
|
|
||||||
'settings.language.description': 'Choisissez votre langue préférée',
|
|
||||||
'settings.theme.light': 'Clair',
|
|
||||||
'settings.theme.dark': 'Sombre',
|
|
||||||
'settings.theme.toggle.light': 'Passer en mode clair',
|
|
||||||
'settings.theme.toggle.dark': 'Passer en mode sombre',
|
|
||||||
'settings.userinfo': 'Informations utilisateur',
|
|
||||||
'settings.userinfo.description': 'Gérez vos informations de compte',
|
|
||||||
'settings.userinfo.username': 'Nom d\'utilisateur',
|
|
||||||
'settings.userinfo.fullname': 'Nom complet',
|
|
||||||
'settings.userinfo.email': 'Adresse e-mail',
|
|
||||||
'settings.userinfo.phone_name': 'Nom au téléphone',
|
|
||||||
'settings.userinfo.phone_name.description': 'Comment souhaitez-vous être appelé au téléphone ?',
|
|
||||||
'settings.userinfo.language': 'Langue',
|
|
||||||
'settings.userinfo.privilege': 'Niveau de privilège',
|
|
||||||
'settings.userinfo.enabled': 'Statut du compte',
|
|
||||||
'settings.userinfo.auth_authority': 'Fournisseur d\'authentification',
|
|
||||||
'settings.userinfo.enabled.true': 'Actif',
|
|
||||||
'settings.userinfo.enabled.false': 'Inactif',
|
|
||||||
'settings.userinfo.loading': 'Chargement des informations utilisateur...',
|
|
||||||
'settings.userinfo.error': 'Erreur lors du chargement des informations utilisateur',
|
|
||||||
'settings.userinfo.save': 'Enregistrer les modifications',
|
|
||||||
'settings.userinfo.saving': 'Enregistrement...',
|
|
||||||
'settings.userinfo.success': 'Informations utilisateur mises à jour avec succès',
|
|
||||||
'settings.userinfo.update_error': 'Erreur lors de la mise à jour des informations utilisateur',
|
|
||||||
'settings.userinfo.managed_by': 'Géré par {provider}',
|
|
||||||
'settings.userinfo.managed_note': 'Ce champ est géré par {provider} et ne peut pas être modifié',
|
|
||||||
|
|
||||||
// Languages
|
|
||||||
'language.german': 'Deutsch',
|
|
||||||
'language.english': 'English',
|
|
||||||
'language.french': 'Français',
|
|
||||||
|
|
||||||
// Common
|
|
||||||
'common.loading': 'Chargement...',
|
|
||||||
'common.error': 'Erreur',
|
|
||||||
'common.success': 'Succès',
|
|
||||||
'common.cancel': 'Annuler',
|
|
||||||
'common.save': 'Enregistrer',
|
|
||||||
'common.delete': 'Supprimer',
|
|
||||||
'common.edit': 'Modifier',
|
|
||||||
'common.close': 'Fermer',
|
|
||||||
'common.retry': 'Réessayer',
|
|
||||||
'common.create': 'Créer',
|
|
||||||
'common.creating': 'Création...',
|
|
||||||
|
|
||||||
// Auth
|
|
||||||
'auth.login': 'Se connecter',
|
|
||||||
'auth.register': 'S\'inscrire',
|
|
||||||
'auth.logout': 'Se déconnecter',
|
|
||||||
'auth.email': 'E-mail',
|
|
||||||
'auth.password': 'Mot de passe',
|
|
||||||
|
|
||||||
// Dashboard
|
|
||||||
'dashboard.prompt.template': 'Modèle de prompt',
|
|
||||||
'dashboard.prompt.settings': 'Paramètres',
|
|
||||||
'dashboard.chat.area': 'Zone de chat',
|
|
||||||
'dashboard.chat.history': 'Historique des workflows',
|
|
||||||
'dashboard.log.title': 'Journal',
|
|
||||||
'dashboard.log.workflow': 'Workflow',
|
|
||||||
'dashboard.log.no_workflow': 'Aucun workflow sélectionné',
|
|
||||||
'dashboard.log.loading': 'Chargement des logs...',
|
|
||||||
'dashboard.log.error': 'Erreur lors du chargement des logs',
|
|
||||||
'dashboard.log.no_logs': 'Aucun log disponible pour ce workflow',
|
|
||||||
'dashboard.log.waiting': 'Workflow en cours... En attente des logs...',
|
|
||||||
'dashboard.log.fetch_failed': 'Échec du chargement des logs',
|
|
||||||
'dashboard.log.level.info': 'INFO',
|
|
||||||
'dashboard.workflow_dropdown.loading': 'Chargement...',
|
|
||||||
'dashboard.workflow_dropdown.error': 'Erreur',
|
|
||||||
'dashboard.workflow_dropdown.select_workflow': 'Sélectionner un workflow',
|
|
||||||
'dashboard.workflow_dropdown.available_workflows': 'Workflows disponibles',
|
|
||||||
'dashboard.workflow_dropdown.no_workflows': 'Aucun workflow disponible',
|
|
||||||
|
|
||||||
// Workflow Stats
|
|
||||||
'dashboard.stats.workflow': 'Workflow',
|
|
||||||
'dashboard.stats.status': 'Statut',
|
|
||||||
'dashboard.stats.rounds': 'Tours',
|
|
||||||
'dashboard.stats.messages': 'Messages',
|
|
||||||
'dashboard.stats.files': 'Fichiers',
|
|
||||||
'dashboard.stats.tokens': 'Jetons',
|
|
||||||
'dashboard.stats.data_sent': 'Données envoyées',
|
|
||||||
'dashboard.stats.data_received': 'Données reçues',
|
|
||||||
'dashboard.stats.success_rate': 'Taux de succès',
|
|
||||||
'dashboard.stats.errors': 'Erreurs',
|
|
||||||
'dashboard.stats.started': 'Démarré',
|
|
||||||
|
|
||||||
// Prompt Set
|
|
||||||
'promptset.loading': 'Chargement des prompts...',
|
|
||||||
'promptset.error.loading': 'Erreur lors du chargement des prompts',
|
|
||||||
'promptset.retry': 'Réessayer',
|
|
||||||
'promptset.new_prompt': 'Nouveau prompt',
|
|
||||||
'promptset.prompt_count': 'Prompt',
|
|
||||||
'promptset.prompt_count_plural': 'Prompts',
|
|
||||||
'promptset.no_prompts': 'Aucun prompt disponible',
|
|
||||||
'promptset.created': 'Créé',
|
|
||||||
'promptset.run_tooltip': 'Exécuter le prompt',
|
|
||||||
'promptset.share_tooltip': 'Partager le prompt',
|
|
||||||
'promptset.delete_tooltip': 'Supprimer le prompt',
|
|
||||||
'promptset.confirm_delete': 'Cliquez à nouveau pour confirmer',
|
|
||||||
'promptset.deleting': 'Suppression...',
|
|
||||||
'promptset.confirm_click': 'Cliquez pour confirmer',
|
|
||||||
'promptset.delete_error': 'Erreur lors de la suppression',
|
|
||||||
'promptset.deleting_message': 'Suppression du prompt...',
|
|
||||||
|
|
||||||
// Connections
|
|
||||||
'connections.title': 'Connexions',
|
|
||||||
'connections.subtitle': 'Gérez vos connexions de service',
|
|
||||||
'connections.connect_google': 'Connecter Google',
|
|
||||||
'connections.connect_microsoft': 'Connecter Microsoft',
|
|
||||||
'connections.add_google_button': 'Ajouter une connexion Google',
|
|
||||||
'connections.add_microsoft_button': 'Ajouter une connexion Microsoft',
|
|
||||||
'connections.create_google_title': 'Créer une connexion Google',
|
|
||||||
'connections.create_microsoft_title': 'Créer une connexion Microsoft',
|
|
||||||
'connections.edit_connection_title': 'Modifier la connexion {authority}',
|
|
||||||
'connections.update_connection': 'Mettre à jour la connexion',
|
|
||||||
'connections.service_connections': 'Connexions de service',
|
|
||||||
'connections.error': 'Erreur',
|
|
||||||
'connections.connection_error': 'Erreur de connexion',
|
|
||||||
'connections.disconnect_error': 'Erreur de déconnexion',
|
|
||||||
'connections.unknown': 'Inconnu',
|
|
||||||
'connections.not_available': 'N/D',
|
|
||||||
'connections.invalid_date': 'Date invalide',
|
|
||||||
'connections.confirm_delete': 'Êtes-vous sûr de vouloir supprimer la connexion {service} ?',
|
|
||||||
'connections.confirm_delete_multiple': 'Êtes-vous sûr de vouloir supprimer {count} connexions ?',
|
|
||||||
|
|
||||||
// Connection Fields
|
|
||||||
'connections.field.service': 'Service',
|
|
||||||
'connections.field.status': 'Statut',
|
|
||||||
'connections.field.external_username': 'Nom d\'utilisateur externe',
|
|
||||||
'connections.field.external_email': 'E-mail externe',
|
|
||||||
'connections.field.connected_at': 'Connecté le',
|
|
||||||
'connections.field.last_checked': 'Dernière vérification',
|
|
||||||
'connections.field.expires_at': 'Expire le',
|
|
||||||
|
|
||||||
// Connection Columns
|
|
||||||
'connections.column.username': 'Nom d\'utilisateur',
|
|
||||||
'connections.column.email': 'E-mail',
|
|
||||||
'connections.column.authority': 'Service',
|
|
||||||
'connections.column.status': 'Statut',
|
|
||||||
'connections.column.connectedat': 'Connecté le',
|
|
||||||
'connections.column.lastchecked': 'Dernière vérification',
|
|
||||||
'connections.column.expiresat': 'Expire le',
|
|
||||||
|
|
||||||
// Connection Services
|
|
||||||
'connections.service.google': 'Google',
|
|
||||||
'connections.service.microsoft': 'Microsoft',
|
|
||||||
'connections.service.local': 'Local',
|
|
||||||
|
|
||||||
// Connection Placeholders
|
|
||||||
'connections.placeholder.external_username': 'Entrez le nom d\'utilisateur externe',
|
|
||||||
'connections.placeholder.external_email': 'Entrez l\'adresse e-mail externe',
|
|
||||||
|
|
||||||
// Connection Actions
|
|
||||||
'connections.action.edit': 'Modifier',
|
|
||||||
'connections.action.update': 'Mettre à jour',
|
|
||||||
'connections.action.delete': 'Supprimer',
|
|
||||||
'connections.action.connect': 'Connecter',
|
|
||||||
'connections.action.refresh': 'Actualiser',
|
|
||||||
|
|
||||||
// Prompt Modal
|
|
||||||
'modal.create_prompt': 'Créer un nouveau prompt',
|
|
||||||
'modal.name_required': 'Le nom est requis',
|
|
||||||
'modal.content_required': 'Le contenu est requis',
|
|
||||||
'modal.create_error': 'Erreur lors de la création du prompt',
|
|
||||||
'modal.name_label': 'Nom',
|
|
||||||
'modal.content_label': 'Contenu',
|
|
||||||
'modal.name_placeholder': 'Entrez un nom pour le prompt',
|
|
||||||
'modal.content_placeholder': 'Entrez le contenu du prompt',
|
|
||||||
'modal.cancel': 'Annuler',
|
|
||||||
'modal.creating': 'Création...',
|
|
||||||
'modal.create': 'Créer le prompt',
|
|
||||||
|
|
||||||
// Share Modal
|
|
||||||
'share_modal.title': 'Partager le prompt',
|
|
||||||
'share_modal.select_users': 'Sélectionner les utilisateurs',
|
|
||||||
'share_modal.select_all': 'Tout sélectionner',
|
|
||||||
'share_modal.deselect_all': 'Tout désélectionner',
|
|
||||||
'share_modal.loading_users': 'Chargement des utilisateurs...',
|
|
||||||
'share_modal.error_loading_users': 'Erreur lors du chargement des utilisateurs',
|
|
||||||
'share_modal.no_users_available': 'Aucun utilisateur disponible',
|
|
||||||
'share_modal.no_users_selected': 'Veuillez sélectionner au moins un utilisateur',
|
|
||||||
'share_modal.one_user_selected': '1 utilisateur sélectionné',
|
|
||||||
'share_modal.multiple_users_selected': '{count} utilisateurs sélectionnés',
|
|
||||||
'share_modal.custom_title': 'Titre personnalisé (facultatif)',
|
|
||||||
'share_modal.title_placeholder': 'Entrez un titre personnalisé',
|
|
||||||
'share_modal.message': 'Message (facultatif)',
|
|
||||||
'share_modal.message_placeholder': 'Ajoutez un message pour les destinataires',
|
|
||||||
'share_modal.share': 'Partager',
|
|
||||||
'share_modal.sharing': 'Partage en cours...',
|
|
||||||
'share_modal.share_error': 'Erreur lors du partage du prompt',
|
|
||||||
|
|
||||||
// Prompt Settings
|
|
||||||
'prompt_settings.title': 'Paramètres de prompt',
|
|
||||||
'prompt_settings.content_placeholder': 'Le contenu des paramètres sera ajouté dans les futures mises à jour.',
|
|
||||||
|
|
||||||
// Chat Area
|
|
||||||
'chat.continue_conversation': 'Continuer la conversation...',
|
|
||||||
'chat.enter_message': 'Entrez votre message...',
|
|
||||||
'chat.remove_file': 'Supprimer le fichier',
|
|
||||||
'chat.attach_file': 'Joindre un fichier',
|
|
||||||
'chat.you': 'Vous',
|
|
||||||
'chat.click_to_open': 'Cliquez pour ouvrir',
|
|
||||||
'chat.preview_document': 'Aperçu du document',
|
|
||||||
'chat.download_document': 'Télécharger le document',
|
|
||||||
'chat.workflow_failed': 'Échec du workflow.',
|
|
||||||
'chat.retry_workflow': 'Réessayer',
|
|
||||||
'chat.sending_followup': 'Envoi du message de suivi...',
|
|
||||||
'chat.sending_message': 'Envoi du message...',
|
|
||||||
'chat.error_prefix': 'Erreur:',
|
|
||||||
'chat.error_loading_messages': 'Erreur lors du chargement des messages:',
|
|
||||||
'chat.loading_workflow_messages': 'Chargement des messages de workflow...',
|
|
||||||
'chat.start_conversation': 'Commencez une conversation en entrant un message, en sélectionnant un modèle ou en continuant un workflow précédent...',
|
|
||||||
|
|
||||||
// Chat Input Area
|
|
||||||
'chat.input.continue_workflow': 'Continuer la conversation...',
|
|
||||||
'chat.input.enter_message': 'Ou entrez votre message...',
|
|
||||||
'chat.input.continuing_workflow': 'Workflow en cours',
|
|
||||||
'chat.input.workflow': 'Workflow',
|
|
||||||
'chat.input.files_attached': 'fichier',
|
|
||||||
'chat.input.files_attached_plural': 'fichiers',
|
|
||||||
'chat.input.files_attached_label': 'attaché',
|
|
||||||
'chat.input.error_prefix': 'Erreur:',
|
|
||||||
'chat.input.attach_files': 'Joindre des fichiers',
|
|
||||||
'chat.input.sending': 'Envoi...',
|
|
||||||
'chat.input.processing': 'Traitement...',
|
|
||||||
'chat.input.continue': 'Continuer',
|
|
||||||
'chat.input.send': 'Envoyer',
|
|
||||||
'chat.input.stop': 'Arrêter',
|
|
||||||
'chat.input.stopping': 'Arrêt...',
|
|
||||||
'chat.input.drop_files_here': 'Déposez les fichiers ici pour les joindre',
|
|
||||||
'chat.input.drop_disabled': 'Dépôt de fichiers désactivé pendant le workflow',
|
|
||||||
'chat.input.new_chat': 'Nouveau Chat',
|
|
||||||
'chat.input.using_prompt': 'Utilisation du modèle:',
|
|
||||||
'chat.input.select_prompt': 'Sélectionner un prompt...',
|
|
||||||
'chat.input.loading_prompts': 'Chargement des prompts...',
|
|
||||||
'chat.input.clear_prompt': 'Effacer le prompt',
|
|
||||||
|
|
||||||
// File Preview
|
|
||||||
'file_preview.loading': 'Chargement de l\'aperçu...',
|
|
||||||
'file_preview.error': 'Erreur',
|
|
||||||
'file_preview.no_preview': 'Aucun aperçu disponible',
|
|
||||||
'file_preview.close_preview': 'Fermer l\'aperçu',
|
|
||||||
'file_preview.python': 'Python',
|
|
||||||
|
|
||||||
// Chat History
|
|
||||||
'chat_history.loading': 'Chargement des workflows...',
|
|
||||||
'chat_history.error_loading': 'Erreur lors du chargement des workflows:',
|
|
||||||
'chat_history.try_again': 'Réessayer',
|
|
||||||
'chat_history.title': 'Historique des workflows',
|
|
||||||
'chat_history.workflow_count': 'Workflow',
|
|
||||||
'chat_history.workflow_count_plural': 'Workflows',
|
|
||||||
'chat_history.empty_state': 'Aucun workflow disponible',
|
|
||||||
'chat_history.confirm_delete': 'Êtes-vous sûr de vouloir supprimer le workflow "{id}..."?',
|
|
||||||
'chat_history.no_message_content': 'Aucun contenu de message disponible',
|
|
||||||
'chat_history.unknown_date': 'Date inconnue',
|
|
||||||
'chat_history.invalid_date': 'Date invalide',
|
|
||||||
'chat_history.started': 'Démarré:',
|
|
||||||
'chat_history.last_activity': 'Dernière activité:',
|
|
||||||
'chat_history.round': 'Tour',
|
|
||||||
'chat_history.resume_tooltip': 'Reprendre le workflow',
|
|
||||||
'chat_history.delete_tooltip': 'Supprimer le workflow',
|
|
||||||
'chat_history.deleting': 'Suppression du workflow...',
|
|
||||||
|
|
||||||
// Chat Messages
|
|
||||||
'chat.messages.no_workflow_selected': 'Aucun workflow sélectionné',
|
|
||||||
'chat.messages.no_workflow_selected_description': 'Sélectionnez un workflow dans la liste ou démarrez un nouveau workflow',
|
|
||||||
'chat.messages.loading_progress': 'Chargement du progrès...',
|
|
||||||
'chat.messages.tasks': 'Tâches',
|
|
||||||
'chat.messages.workflow_progress': 'Progression du workflow',
|
|
||||||
'chat.messages.analyzing_workflow': 'Analyse du workflow...',
|
|
||||||
'chat.messages.scroll_to_bottom_btn': 'Faire défiler vers le bas',
|
|
||||||
|
|
||||||
// Workflow Status
|
|
||||||
'status.error': 'ERREUR',
|
|
||||||
'status.failed': 'ÉCHEC',
|
|
||||||
'status.stopped': 'ARRÊTÉ',
|
|
||||||
'status.cancelled': 'ANNULÉ',
|
|
||||||
'status.running': 'EN COURS',
|
|
||||||
'status.processing': 'TRAITEMENT',
|
|
||||||
'status.completed': 'TERMINÉ',
|
|
||||||
'status.pending': 'EN ATTENTE',
|
|
||||||
|
|
||||||
// Files
|
|
||||||
'files.unknown_size': 'Taille inconnue',
|
|
||||||
'files.unknown_date': 'Date inconnue',
|
|
||||||
'files.source.uploaded': 'Téléchargé',
|
|
||||||
'files.source.ai_created': 'Créé par IA',
|
|
||||||
'files.source.shared': 'Partagé',
|
|
||||||
'files.source.unknown': 'Inconnu',
|
|
||||||
'files.preview_tooltip': 'Aperçu du fichier',
|
|
||||||
'files.download_tooltip': 'Télécharger le fichier',
|
|
||||||
'files.delete_tooltip': 'Supprimer le fichier',
|
|
||||||
'files.delete_confirm_tooltip': 'Cliquez à nouveau pour confirmer la suppression',
|
|
||||||
'files.downloading': 'Téléchargement...',
|
|
||||||
'files.deleting': 'Suppression...',
|
|
||||||
'files.delete_confirm': 'Cliquez pour confirmer...',
|
|
||||||
'files.no_files': 'Aucun fichier trouvé.',
|
|
||||||
'files.no_shared_files': 'Aucun fichier partagé trouvé.',
|
|
||||||
'files.no_ai_files': 'Aucun fichier créé par IA trouvé.',
|
|
||||||
'files.no_uploaded_files': 'Aucun fichier téléchargé trouvé.',
|
|
||||||
'files.header.name': 'Nom',
|
|
||||||
'files.header.type': 'Type',
|
|
||||||
'files.header.size': 'Taille',
|
|
||||||
'files.header.date': 'Date',
|
|
||||||
'files.selector.title': 'Sélectionner des fichiers',
|
|
||||||
'files.selector.tab.all': 'Tous les fichiers',
|
|
||||||
'files.selector.tab.uploads': 'Téléchargés',
|
|
||||||
'files.selector.tab.created': 'Créés par IA',
|
|
||||||
'files.selector.tab.shared': 'Partagés',
|
|
||||||
'files.selector.select_all': 'Tout sélectionner',
|
|
||||||
'files.selector.deselect_all': 'Tout désélectionner',
|
|
||||||
'files.selector.file_selected': 'Fichier',
|
|
||||||
'files.selector.files_selected': 'Fichiers',
|
|
||||||
'files.selector.selected_suffix': 'sélectionné(s)',
|
|
||||||
'files.selector.upload_new': 'Télécharger un nouveau fichier',
|
|
||||||
'files.selector.loading': 'Chargement des fichiers...',
|
|
||||||
'files.selector.error_loading': 'Erreur lors du chargement des fichiers:',
|
|
||||||
'files.upload.title': 'Télécharger un fichier',
|
|
||||||
'files.upload.drop_here': 'Déposer le fichier ici...',
|
|
||||||
'files.upload.uploading': 'Téléchargement...',
|
|
||||||
'files.upload.drag_files': 'Glisser les fichiers ici',
|
|
||||||
'files.upload.or': 'ou',
|
|
||||||
'files.upload.browse': 'Parcourir',
|
|
||||||
'files.upload.selected_file': 'Fichier sélectionné:',
|
|
||||||
'files.upload.upload_button': 'Télécharger',
|
|
||||||
'files.upload.uploading_button': 'Téléchargement...',
|
|
||||||
'files.upload.success': 'Fichier téléchargé avec succès!',
|
|
||||||
'files.upload.error': 'Une erreur s\'est produite lors du téléchargement.',
|
|
||||||
'files.upload.unexpected_error': 'Une erreur inattendue s\'est produite lors du téléchargement.',
|
|
||||||
|
|
||||||
// Files Page Upload Actions
|
|
||||||
'files.drop_zone': 'Déposer les fichiers ici',
|
|
||||||
'files.upload_button': 'Télécharger des fichiers',
|
|
||||||
'files.uploading_button': 'Téléchargement...',
|
|
||||||
'files.upload_aria_label': 'Télécharger des fichiers',
|
|
||||||
|
|
||||||
// Files Page
|
|
||||||
'files.title': 'Fichiers',
|
|
||||||
'files.table.title': 'Fichiers',
|
|
||||||
'files.error.loading': 'Erreur lors du chargement des fichiers:',
|
|
||||||
'files.button.retry': 'Réessayer',
|
|
||||||
'files.page.tab.all': 'Tous les fichiers',
|
|
||||||
'files.page.tab.uploads': 'Mes téléchargements',
|
|
||||||
'files.page.tab.created': 'Fichiers créés',
|
|
||||||
'files.page.tab.shared': 'Fichiers partagés',
|
|
||||||
'files.page.add_file': 'Ajouter un fichier',
|
|
||||||
'files.page.loading': 'Chargement des fichiers...',
|
|
||||||
'files.page.error': 'Erreur:',
|
|
||||||
|
|
||||||
// File Table Columns
|
|
||||||
'files.column.name': 'Nom',
|
|
||||||
'files.column.filename': 'Nom de fichier',
|
|
||||||
'files.column.type': 'Type',
|
|
||||||
'files.column.mimetype': 'Type MIME',
|
|
||||||
'files.column.size': 'Taille',
|
|
||||||
'files.column.filesize': 'Taille du fichier',
|
|
||||||
'files.column.created': 'Créé',
|
|
||||||
'files.column.creationdate': 'Date de création',
|
|
||||||
'files.column.source': 'Source',
|
|
||||||
|
|
||||||
// File Types
|
|
||||||
'files.type.image': 'Image',
|
|
||||||
'files.type.pdf': 'PDF',
|
|
||||||
'files.type.document': 'Document',
|
|
||||||
'files.type.spreadsheet': 'Feuille de calcul',
|
|
||||||
'files.type.text': 'Texte',
|
|
||||||
'files.type.video': 'Vidéo',
|
|
||||||
'files.type.audio': 'Audio',
|
|
||||||
'files.type.file': 'Fichier',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// File Actions
|
|
||||||
'files.action.preview': 'Aperçu',
|
|
||||||
'files.action.download': 'Télécharger',
|
|
||||||
'files.action.delete': 'Supprimer',
|
|
||||||
'files.delete.confirm': 'Êtes-vous sûr de vouloir supprimer le fichier "{name}"?',
|
|
||||||
|
|
||||||
// File Preview
|
|
||||||
'files.preview.title': 'Aperçu du fichier',
|
|
||||||
'files.preview.loading': 'Chargement de l\'aperçu...',
|
|
||||||
'files.preview.unsupported': 'Aperçu non disponible pour ce type de fichier',
|
|
||||||
'files.preview.error': 'Erreur lors du chargement de l\'aperçu',
|
|
||||||
'files.preview.textInPdfFile': 'Aperçu du texte',
|
|
||||||
'files.preview.pdfFileCorrupted': 'Ce fichier semble être corrompu. Il a une extension PDF mais contient du contenu texte. Veuillez le télécharger à nouveau si possible.',
|
|
||||||
|
|
||||||
// Workflows Page
|
|
||||||
'workflows.title': 'Workflows',
|
|
||||||
'workflows.table.title': 'Workflows',
|
|
||||||
'workflows.error.loading': 'Erreur lors du chargement des workflows:',
|
|
||||||
'workflows.button.retry': 'Réessayer',
|
|
||||||
'workflows.table.empty': 'Aucun workflow trouvé',
|
|
||||||
|
|
||||||
// Workflow Table Columns
|
|
||||||
'workflows.column.id': 'ID',
|
|
||||||
'workflows.column.name': 'Nom',
|
|
||||||
'workflows.column.status': 'Statut',
|
|
||||||
'workflows.column.round': 'Tour',
|
|
||||||
'workflows.column.started': 'Démarré',
|
|
||||||
'workflows.column.lastActivity': 'Dernière activité',
|
|
||||||
'workflows.column.messages': 'Messages',
|
|
||||||
|
|
||||||
// Workflow Status
|
|
||||||
'workflows.status.running': 'En cours',
|
|
||||||
'workflows.status.completed': 'Terminé',
|
|
||||||
'workflows.status.failed': 'Échoué',
|
|
||||||
'workflows.status.stopped': 'Arrêté',
|
|
||||||
'workflows.status.pending': 'En attente',
|
|
||||||
|
|
||||||
// Workflow Actions
|
|
||||||
'workflows.action.stop': 'Arrêter',
|
|
||||||
'workflows.action.delete': 'Supprimer',
|
|
||||||
'workflows.action.stop.tooltip': 'Arrêter le workflow',
|
|
||||||
'workflows.action.delete.tooltip': 'Supprimer le workflow',
|
|
||||||
|
|
||||||
// Workflow Messages
|
|
||||||
'workflows.unnamed': 'Workflow sans nom',
|
|
||||||
'workflows.delete.confirm': 'Êtes-vous sûr de vouloir supprimer le workflow "{name}"?',
|
|
||||||
'workflows.loading': 'Chargement des workflows...',
|
|
||||||
|
|
||||||
// FormGenerator
|
|
||||||
'formgen.search.placeholder': 'Rechercher...',
|
|
||||||
'formgen.refresh.tooltip': 'Actualiser les données',
|
|
||||||
'formgen.filter.yes': 'Oui',
|
|
||||||
'formgen.filter.no': 'Non',
|
|
||||||
'formgen.filter.clear': 'Effacer le filtre',
|
|
||||||
'formgen.filter.placeholder': 'Filtrer {column}',
|
|
||||||
'formgen.actions.column': 'Actions',
|
|
||||||
'formgen.pagination.info': 'Page {page} sur {total} ({count} éléments)',
|
|
||||||
'formgen.pagination.pageSize': 'Éléments par page:',
|
|
||||||
'formgen.pagination.first': 'Première page',
|
|
||||||
'formgen.pagination.prev': 'Page précédente',
|
|
||||||
'formgen.pagination.next': 'Page suivante',
|
|
||||||
'formgen.pagination.last': 'Dernière page',
|
|
||||||
'formgen.select.all': 'Sélectionner tous les éléments',
|
|
||||||
'formgen.select.item': 'Sélectionner cet élément',
|
|
||||||
'formgen.select.disabled': 'Cet élément ne peut pas être sélectionné',
|
|
||||||
'formgen.delete.multiple': 'Supprimer ({count})',
|
|
||||||
'formgen.delete.confirm_multiple': 'Êtes-vous sûr de vouloir supprimer les {count} éléments sélectionnés ?',
|
|
||||||
|
|
||||||
// Prompts
|
|
||||||
'prompts.title': 'Prompts',
|
|
||||||
'prompts.subtitle': 'Gérer vos prompts',
|
|
||||||
'prompts.description': 'Créer et gérer des prompts pour votre assistant IA',
|
|
||||||
'prompts.new_button': 'Nouveau prompt',
|
|
||||||
'prompts.addNew': 'Ajouter un prompt',
|
|
||||||
'prompts.creating': 'Création...',
|
|
||||||
'prompts.column.name': 'Nom',
|
|
||||||
'prompts.column.content': 'Contenu',
|
|
||||||
'prompts.column.mandateId': 'ID Mandat',
|
|
||||||
'prompts.unnamed': 'Sans nom',
|
|
||||||
'prompts.action.edit': 'Modifier',
|
|
||||||
'prompts.action.copy': 'Copier',
|
|
||||||
'prompts.action.delete': 'Supprimer',
|
|
||||||
'prompts.action.delete.disabled': 'Aucune permission de supprimer l\'invite',
|
|
||||||
'prompts.delete.confirm': 'Êtes-vous sûr de vouloir supprimer "{name}" ?',
|
|
||||||
'prompts.delete.confirmMultiple': 'Êtes-vous sûr de vouloir supprimer {count} prompts ?',
|
|
||||||
'prompts.field.name': 'Nom du prompt',
|
|
||||||
'prompts.field.content': 'Contenu du prompt',
|
|
||||||
'prompts.validation.nameRequired': 'Le nom du prompt ne peut pas être vide',
|
|
||||||
'prompts.validation.nameTooLong': 'Le nom du prompt ne peut pas dépasser 100 caractères',
|
|
||||||
'prompts.validation.contentRequired': 'Le contenu du prompt ne peut pas être vide',
|
|
||||||
'prompts.validation.contentTooLong': 'Le contenu du prompt ne peut pas dépasser 10 000 caractères',
|
|
||||||
'prompts.error.loading': 'Erreur lors du chargement des prompts:',
|
|
||||||
'prompts.modal.edit.title': 'Modifier le prompt',
|
|
||||||
'prompts.modal.edit.save': 'Enregistrer les modifications',
|
|
||||||
'prompts.modal.create.title': 'Créer un nouveau prompt',
|
|
||||||
'prompts.modal.create.save': 'Créer le prompt',
|
|
||||||
'prompts.create.success': 'Prompt créé avec succès',
|
|
||||||
'prompts.create.error': 'Erreur lors de la création du prompt',
|
|
||||||
|
|
||||||
// Users/Members
|
|
||||||
'users.title': 'Utilisateurs',
|
|
||||||
'users.column.username': 'Nom d\'utilisateur',
|
|
||||||
'users.column.name': 'Nom',
|
|
||||||
'users.column.email': 'E-mail',
|
|
||||||
'users.column.password': 'Mot de passe',
|
|
||||||
'users.column.language': 'Langue',
|
|
||||||
'users.column.privilege': 'Privilège',
|
|
||||||
'users.column.enabled': 'Activé',
|
|
||||||
'users.column.authAuthority': 'Autorité d\'authentification',
|
|
||||||
'users.password.placeholder': 'Entrez le mot de passe',
|
|
||||||
'users.noUsername': 'Aucun nom d\'utilisateur',
|
|
||||||
'users.noName': 'Aucun nom',
|
|
||||||
'users.noEmail': 'Aucun e-mail',
|
|
||||||
'users.noLanguage': 'Aucune langue',
|
|
||||||
'users.noPrivilege': 'Aucun privilège',
|
|
||||||
'users.noAuthAuthority': 'Aucune autorité d\'authentification',
|
|
||||||
'users.privilege.viewer': 'Observateur',
|
|
||||||
'users.privilege.user': 'Utilisateur',
|
|
||||||
'users.privilege.admin': 'Administrateur',
|
|
||||||
'users.privilege.sysadmin': 'Administrateur système',
|
|
||||||
'users.enabled.yes': 'Oui',
|
|
||||||
'users.enabled.no': 'Non',
|
|
||||||
'users.auth.local': 'Local',
|
|
||||||
'users.auth.msft': 'Microsoft',
|
|
||||||
'users.actions.edit': 'Modifier',
|
|
||||||
'users.actions.delete': 'Supprimer',
|
|
||||||
'users.edit.title': 'Modifier l\'utilisateur',
|
|
||||||
'users.add.title': 'Ajouter un utilisateur',
|
|
||||||
'users.add.button': 'Ajouter un utilisateur',
|
|
||||||
'users.add.create': 'Créer l\'utilisateur',
|
|
||||||
'users.delete.title': 'Supprimer l\'utilisateur',
|
|
||||||
'users.delete.message': 'Êtes-vous sûr de vouloir supprimer cet utilisateur ?',
|
|
||||||
'users.delete.confirm': 'Êtes-vous sûr de vouloir supprimer "{name}" ?',
|
|
||||||
'users.delete.warning': 'Cette action ne peut pas être annulée.',
|
|
||||||
'users.action.edit': 'Modifier',
|
|
||||||
'users.action.delete': 'Supprimer',
|
|
||||||
'users.delete.confirmMultiple': 'Êtes-vous sûr de vouloir supprimer {count} utilisateurs ?',
|
|
||||||
'users.error.loading': 'Erreur lors du chargement des utilisateurs:',
|
|
||||||
|
|
||||||
// Team Members
|
|
||||||
'team-members.title': 'Membres de l\'équipe',
|
|
||||||
'team-members.subtitle': 'Gérer les membres de votre équipe',
|
|
||||||
'team-members.description': 'Gérer les membres de l\'équipe, définir les permissions et configurer les paramètres de collaboration',
|
|
||||||
'team-members.new_button': 'Ajouter un membre',
|
|
||||||
'team-members.action.edit': 'Modifier',
|
|
||||||
'team-members.action.delete': 'Supprimer',
|
|
||||||
'team-members.field.username': 'Nom d\'utilisateur',
|
|
||||||
'team-members.field.email': 'E-mail',
|
|
||||||
'team-members.field.password': 'Mot de passe',
|
|
||||||
'team-members.field.fullName': 'Nom complet',
|
|
||||||
'team-members.field.privilege': 'Privilège',
|
|
||||||
'team-members.modal.create.title': 'Créer un nouveau membre de l\'équipe',
|
|
||||||
'team-members.create.success': 'Membre de l\'équipe créé avec succès',
|
|
||||||
'team-members.create.error': 'Erreur lors de la création du membre de l\'équipe',
|
|
||||||
|
|
||||||
// SharePoint Test
|
|
||||||
'sharepoint.title': 'Test SharePoint',
|
|
||||||
'sharepoint.table.title': 'Documents SharePoint',
|
|
||||||
'sharepoint.error.loading': 'Erreur lors du chargement des documents SharePoint:',
|
|
||||||
'sharepoint.button.retry': 'Réessayer',
|
|
||||||
'sharepoint.button.testConnection': 'Tester la connexion',
|
|
||||||
'sharepoint.button.listDocuments': 'Lister les documents',
|
|
||||||
'sharepoint.button.discoverSites': 'Découvrir les sites',
|
|
||||||
'sharepoint.column.documentName': 'Nom du document',
|
|
||||||
'sharepoint.column.mimeType': 'Type MIME',
|
|
||||||
'sharepoint.column.size': 'Taille',
|
|
||||||
'sharepoint.column.path': 'Chemin',
|
|
||||||
'sharepoint.action.view': 'Voir',
|
|
||||||
'sharepoint.action.download': 'Télécharger',
|
|
||||||
'sharepoint.connections.title': 'Connexions Microsoft',
|
|
||||||
'sharepoint.connections.noConnections': 'Aucune connexion Microsoft trouvée. Veuillez d\'abord créer une connexion.',
|
|
||||||
'sharepoint.connections.loading': 'Chargement des connexions...',
|
|
||||||
'sharepoint.sites.discovered': 'Sites découverts',
|
|
||||||
'sharepoint.sites.noSites': 'Aucun site SharePoint trouvé',
|
|
||||||
'sharepoint.sites.authError': 'Token d\'authentification expiré ou invalide. Veuillez reconnecter votre compte Microsoft.',
|
|
||||||
'sharepoint.sites.retryConnection': 'Essayez de reconnecter votre compte Microsoft dans la page Connexions.',
|
|
||||||
'sharepoint.form.siteUrl': 'URL du site SharePoint',
|
|
||||||
'sharepoint.form.folderPaths': 'Chemins des dossiers',
|
|
||||||
|
|
||||||
// Speech
|
|
||||||
'speech.title': 'Intégration Vocale',
|
|
||||||
'speech.subtitle': 'Alimenté par ',
|
|
||||||
'speech.signup.title': 'Intégration Vocale',
|
|
||||||
'speech.signup.subtitle': 'Alimenté par',
|
|
||||||
|
|
||||||
'speech.info.va': 'Assistant Virtuel (VA)',
|
|
||||||
'speech.info.va_description': 'Offrez aux clients un libre-service rapide et efficace pour les requêtes vocales et textuelles disponible 24h/24.',
|
|
||||||
'speech.info.sa': 'Analyse Vocale (SA)',
|
|
||||||
'speech.info.sa_description': 'Surveillez automatiquement 100% des conversations pour obtenir des insights précieux pour votre entreprise.',
|
|
||||||
'speech.info.vb': 'Biométrie Vocale (VB)',
|
|
||||||
'speech.info.vb_description': 'Identifiez et authentifiez les appelants en quelques secondes avec une vérification et sécurité continues.',
|
|
||||||
'speech.info.ka': 'Agent de Connaissance (KA)',
|
|
||||||
'speech.info.ka_description': 'Unifiez et livrez des informations à vos clients et employés où et quand ils en ont besoin.',
|
|
||||||
'speech.info.cp': 'Plateforme de Chat (CP)',
|
|
||||||
'speech.info.cp_description': 'Offrez une assistance en chat en direct et déployez des chatbots intelligents sur tous les canaux.',
|
|
||||||
'speech.info.aa': 'Assistance Agent (AA)',
|
|
||||||
'speech.info.aa_description': 'Mettez tout ce dont vos agents ont besoin à portée de main, avec un bureau d\'agent unifié.',
|
|
||||||
|
|
||||||
'speech.info.about': 'Intégration Téléphonique Révolutionnaire avec Spitch.ai',
|
|
||||||
'speech.info.about_intro': 'Découvrez l\'avenir de la communication client grâce à notre partenariat stratégique avec Spitch.ai. Cette intégration révolutionnaire transforme votre plateforme PowerOn en un système téléphonique intelligent qui connecte de manière transparente les clients externes avec les entreprises.',
|
|
||||||
'speech.info.workflow_title': 'Workflow Client Transparent:',
|
|
||||||
'speech.info.workflow_description': 'De l\'inscription à la configuration technique - votre client s\'inscrit auprès de PowerOn pour les services téléphoniques, télécharge des documents et reçoit automatiquement un numéro SIP technique de Spitch. Le transfert d\'appel peut être activé ou désactivé à tout moment, garantissant une flexibilité maximale et la sécurité BCM.',
|
|
||||||
'speech.info.ai_title': 'Génération de Documents alimentée par l\'IA:',
|
|
||||||
'speech.info.ai_description': 'Notre moteur d\'extraction de documents déjà actif génère automatiquement des documents personnalisés pour Spitch basés sur les données spécifiques au client. L\'IA utilise les bases de données FAQ, les informations employés et les détails de service pour rendre chaque appel contextuel et hautement personnalisé.',
|
|
||||||
'speech.info.sync_title': 'Synchronisation de Données en Temps Réel:',
|
|
||||||
'speech.info.sync_description': 'Spitch vérifie l\'autorisation client avec PowerOn avant chaque appel, tandis que tous les changements de données sont initiés centralement par PowerOn. Les transcriptions d\'appels sont stockées en temps réel dans votre base de données PowerOn avec une isolation complète du client et la sécurité. En cas de panne, les appels sont automatiquement bloqués pour assurer l\'intégrité.',
|
|
||||||
'speech.info.cost_title': 'Économies de Coûts & Efficacité:',
|
|
||||||
'speech.info.cost_description': 'Les clients peuvent basculer sur le numéro SIP technique à tout moment et économiser des coûts téléphoniques significatifs. L\'intégration fonctionne comme un autre connecteur (Outlook, SharePoint) et est intégrée de manière transparente dans votre workflow existant.',
|
|
||||||
'speech.info.about_link': 'En savoir plus',
|
|
||||||
|
|
||||||
'speech.signup.button': 'Connecter',
|
|
||||||
'speech.signup.back': 'Retour à l\'Intégration Vocale',
|
|
||||||
'speech.signup.submit': 'Créer le Mandat',
|
|
||||||
'speech.signup.cancel': 'Annuler',
|
|
||||||
|
|
||||||
'speech.signup.company_info': 'Informations de l\'Entreprise',
|
|
||||||
'speech.signup.company_name': 'Nom de l\'Entreprise',
|
|
||||||
'speech.signup.company_name_placeholder': 'Entrez le nom de votre entreprise',
|
|
||||||
'speech.signup.industry': 'Secteur d\'Activité',
|
|
||||||
'speech.signup.industry_placeholder': 'ex. Services Financiers, Technologie, etc.',
|
|
||||||
'speech.signup.business_hours': 'Heures d\'Ouverture',
|
|
||||||
'speech.signup.timezone': 'Fuseau Horaire',
|
|
||||||
|
|
||||||
'speech.signup.contact_info': 'Informations de Contact',
|
|
||||||
'speech.signup.email': 'Adresse Email',
|
|
||||||
'speech.signup.email_placeholder': 'contact@entreprise.com',
|
|
||||||
'speech.signup.phone': 'Numéro de Téléphone',
|
|
||||||
'speech.signup.phone_placeholder': '+41 123 456 789',
|
|
||||||
'speech.signup.street': 'Rue',
|
|
||||||
'speech.signup.postal_code': 'Code Postal',
|
|
||||||
'speech.signup.city': 'Ville',
|
|
||||||
'speech.signup.country': 'Pays',
|
|
||||||
|
|
||||||
'speech.signup.contacts_setup': 'Configurer les Contacts',
|
|
||||||
'speech.signup.contacts_description': 'Souhaitez-vous configurer les contacts pour votre mandat maintenant ? Vous pouvez également le faire plus tard dans les paramètres.',
|
|
||||||
'speech.signup.setup_contacts': 'Configurer les Contacts',
|
|
||||||
'speech.signup.skip_for_now': 'Ignorer pour l\'Instant',
|
|
||||||
|
|
||||||
'speech.signup.company_required': 'Le nom de l\'entreprise est requis',
|
|
||||||
'speech.signup.industry_required': 'Le secteur d\'activité est requis',
|
|
||||||
'speech.signup.email_required': 'L\'adresse email est requise',
|
|
||||||
'speech.signup.email_invalid': 'Veuillez entrer une adresse email valide',
|
|
||||||
'speech.signup.phone_required': 'Le numéro de téléphone est requis',
|
|
||||||
'speech.signup.street_required': 'La rue est requise',
|
|
||||||
'speech.signup.postal_code_required': 'Le code postal est requis',
|
|
||||||
'speech.signup.city_required': 'La ville est requise',
|
|
||||||
'speech.signup.country_required': 'Le pays est requis',
|
|
||||||
|
|
||||||
'speech.status.submitted': '✓ Mandat Soumis',
|
|
||||||
'speech.status.reset': 'Recommencer',
|
|
||||||
|
|
||||||
'speech.confirmation.title': 'Mandat Soumis avec Succès !',
|
|
||||||
'speech.confirmation.message': 'Merci pour votre intérêt pour notre Intégration Vocale powered by Spitch.ai. Nous avons reçu votre mandat et l\'examinerons sous peu.',
|
|
||||||
'speech.confirmation.submitted_data': 'Données Soumises :',
|
|
||||||
'speech.confirmation.company': 'Entreprise',
|
|
||||||
'speech.confirmation.industry': 'Secteur',
|
|
||||||
'speech.confirmation.email': 'Email',
|
|
||||||
'speech.confirmation.phone': 'Téléphone',
|
|
||||||
'speech.confirmation.address': 'Adresse',
|
|
||||||
'speech.confirmation.timezone': 'Fuseau Horaire',
|
|
||||||
'speech.confirmation.back': 'Retour à l\'Intégration Vocale',
|
|
||||||
'speech.confirmation.reset': 'Recommencer',
|
|
||||||
'speech.confirmation.next_steps': 'Que se passe-t-il ensuite ?',
|
|
||||||
'speech.confirmation.email_confirmation': 'Confirmation par Email',
|
|
||||||
'speech.confirmation.email_confirmation_desc': 'Vous recevrez un email de confirmation dans les prochaines minutes.',
|
|
||||||
'speech.confirmation.review_process': 'Processus de Révision',
|
|
||||||
'speech.confirmation.review_process_desc': 'Notre équipe examinera votre mandat dans les 1-2 jours ouvrables.',
|
|
||||||
'speech.confirmation.setup_call': 'Appel de Configuration',
|
|
||||||
'speech.confirmation.setup_call_desc': 'Si approuvé, nous planifierons un appel de configuration pour configurer votre intégration.',
|
|
||||||
'speech.confirmation.questions': 'Questions ?',
|
|
||||||
'speech.confirmation.questions_desc': 'Si vous avez des questions sur votre mandat ou le processus d\'intégration, n\'hésitez pas à contacter notre équipe de support.',
|
|
||||||
'speech.confirmation.transcript_management': 'Gestion des Transcriptions',
|
|
||||||
'speech.confirmation.speech_settings': 'Paramètres Vocaux',
|
|
||||||
|
|
||||||
'speech.transcripts.title': 'Gestion des Transcriptions',
|
|
||||||
'speech.transcripts.new_transcript': 'Nouvelle Transcription',
|
|
||||||
'speech.transcripts.recent_transcripts': 'Transcriptions Récentes',
|
|
||||||
'speech.transcripts.no_transcripts': 'Aucune transcription disponible',
|
|
||||||
'speech.transcripts.date': 'Date',
|
|
||||||
'speech.transcripts.duration': 'Durée',
|
|
||||||
'speech.transcripts.status': 'Statut',
|
|
||||||
'speech.transcripts.transcript': 'Transcription',
|
|
||||||
'speech.transcripts.processing': 'Traitement de la transcription...',
|
|
||||||
'speech.transcripts.status.completed': 'Terminé',
|
|
||||||
'speech.transcripts.status.processing': 'En cours',
|
|
||||||
'speech.transcripts.status.failed': 'Échoué',
|
|
||||||
'speech.transcripts.access_denied_title': 'Accès Refusé',
|
|
||||||
'speech.transcripts.access_denied_message': 'Vous devez d\'abord vous inscrire à l\'intégration vocale pour accéder à la gestion des transcriptions.',
|
|
||||||
'speech.transcripts.sign_up_now': 'S\'inscrire Maintenant',
|
|
||||||
'speech.transcripts.subject': 'Sujet',
|
|
||||||
'speech.transcripts.start_time': 'Heure de Début',
|
|
||||||
'speech.transcripts.end_time': 'Heure de Fin',
|
|
||||||
'speech.transcripts.caller': 'Appelant',
|
|
||||||
'speech.transcripts.recipient': 'Destinataire',
|
|
||||||
'speech.transcripts.tags': 'Étiquettes',
|
|
||||||
'speech.transcripts.created': 'Créé',
|
|
||||||
'speech.transcripts.view': 'Voir',
|
|
||||||
'speech.transcripts.download': 'Télécharger',
|
|
||||||
|
|
||||||
'speech.settings.title': 'Paramètres d\'Intégration Vocale',
|
|
||||||
'speech.settings.description': 'Gérez votre configuration et vos préférences d\'intégration vocale.',
|
|
||||||
'speech.settings.company_info': 'Informations de l\'Entreprise',
|
|
||||||
'speech.settings.contact_info': 'Informations de Contact',
|
|
||||||
'speech.settings.business_hours': 'Heures d\'Ouverture et Fuseau Horaire',
|
|
||||||
'speech.settings.save': 'Sauvegarder les Modifications',
|
|
||||||
'speech.settings.saving': 'Sauvegarde...',
|
|
||||||
'speech.settings.save_success': 'Paramètres sauvegardés avec succès !',
|
|
||||||
'speech.settings.save_error': 'Échec de la sauvegarde des paramètres. Veuillez réessayer.',
|
|
||||||
'speech.settings.reset': 'Réinitialiser par Défaut',
|
|
||||||
'speech.settings.reset_confirm': 'Êtes-vous sûr de vouloir réinitialiser tous les paramètres d\'intégration vocale ? Cette action ne peut pas être annulée.',
|
|
||||||
'speech.settings.reset_success': 'Les paramètres ont été réinitialisés avec succès.',
|
|
||||||
'speech.settings.no_data': 'Aucune donnée d\'intégration vocale trouvée. Veuillez d\'abord vous inscrire pour accéder aux paramètres.',
|
|
||||||
'speech.settings.sign_up_now': 'S\'inscrire Maintenant',
|
|
||||||
|
|
||||||
// Message Overlay Types
|
|
||||||
'message.success.title': 'Succès',
|
|
||||||
'message.success.upload': 'Fichier téléchargé avec succès !',
|
|
||||||
'message.info.title': 'Information',
|
|
||||||
'message.info.processing': 'Traitement de votre demande...',
|
|
||||||
'message.error.title': 'Erreur',
|
|
||||||
'message.error.upload_failed': 'Échec du téléchargement. Veuillez réessayer.',
|
|
||||||
|
|
||||||
// Warning Messages
|
|
||||||
'warning.duplicate_file.title': 'Fichier Déjà Existant',
|
|
||||||
'warning.duplicate_file.message': 'Le fichier "{fileName}" existe déjà avec un contenu identique. Le fichier existant sera réutilisé.',
|
|
||||||
|
|
||||||
// Administration
|
|
||||||
'administration.title': 'Administration',
|
|
||||||
'administration.description': 'Outils d\'administration et de gestion',
|
|
||||||
'administration.subtitle': 'Outils d\'administration et de gestion',
|
|
||||||
'administration.intro.description': 'Cette section contient tous les outils d\'administration et de gestion pour votre espace de travail.',
|
|
||||||
'administration.features.title': 'Outils Disponibles',
|
|
||||||
'administration.features.description': 'Les outils de gestion incluent:',
|
|
||||||
'administration.features.file_management': 'Gestion des Fichiers - Télécharger et organiser les documents',
|
|
||||||
'administration.features.user_management': 'Gestion des Utilisateurs - Gérer les membres de l\'équipe et les permissions',
|
|
||||||
'administration.features.system_settings': 'Paramètres Système - Configurer les paramètres de l\'espace de travail',
|
|
||||||
'administration.features.data_management': 'Gestion des Données - Gérer les imports et exports de données',
|
|
||||||
|
|
||||||
// Admin pages
|
|
||||||
'admin.mandates.title': 'Mandats',
|
|
||||||
'admin.mandates.subtitle': 'Gérer les mandats et les permissions',
|
|
||||||
'admin.mandates.description': 'Gestion des mandats',
|
|
||||||
'admin.mandates.description_text': 'Gérez les mandats et leurs permissions associées.',
|
|
||||||
|
|
||||||
'admin.rbac-rules.title': 'Règles RBAC',
|
|
||||||
'admin.rbac-rules.subtitle': 'Règles de contrôle d\'accès basé sur les rôles',
|
|
||||||
'admin.rbac-rules.description': 'Gestion des règles RBAC',
|
|
||||||
'admin.rbac-rules.description_text': 'Configurez et gérez les règles de contrôle d\'accès basé sur les rôles.',
|
|
||||||
|
|
||||||
'admin.rbac-role.title': 'Rôles RBAC',
|
|
||||||
'admin.rbac-role.subtitle': 'Gestion des rôles',
|
|
||||||
'admin.rbac-role.description': 'Gestion des rôles RBAC',
|
|
||||||
'admin.rbac-role.description_text': 'Créez et gérez les rôles RBAC et leurs permissions.',
|
|
||||||
|
|
||||||
'admin.admin-settings.title': 'Paramètres Admin',
|
|
||||||
'admin.admin-settings.subtitle': 'Paramètres administratifs',
|
|
||||||
'admin.admin-settings.description': 'Paramètres administratifs',
|
|
||||||
'admin.admin-settings.description_text': 'Configurez les paramètres administratifs et les préférences système.',
|
|
||||||
|
|
||||||
// Start page
|
|
||||||
'start.title': 'Démarrage',
|
|
||||||
'start.description': 'Bienvenue dans votre espace de travail',
|
|
||||||
'start.subtitle': 'Bienvenue dans votre espace de travail',
|
|
||||||
'start.intro.description': 'Ceci est votre point de départ pour accéder à toutes les fonctionnalités et outils de votre espace de travail.',
|
|
||||||
'start.features.title': 'Accès Rapide',
|
|
||||||
'start.features.description': 'Commencez avec :',
|
|
||||||
'start.features.quick_access': 'Accès Rapide - Accédez rapidement aux fonctionnalités fréquemment utilisées',
|
|
||||||
'start.features.recent_activities': 'Activités Récentes - Consultez votre travail le plus récent',
|
|
||||||
'start.features.overview': 'Aperçu - Consultez le statut et les mises à jour de l\'espace de travail',
|
|
||||||
'start.features.navigation': 'Navigation - Explorez tous les outils disponibles',
|
|
||||||
|
|
||||||
// Projects page
|
|
||||||
'projects.title': 'Projets',
|
|
||||||
'projects.subtitle': 'Gestion de projets',
|
|
||||||
'projects.description': 'Gestion et organisation de projets',
|
|
||||||
'projects.description_text': 'Recherchez des emplacements par adresse ou coordonnées, ou utilisez le langage naturel pour créer et gérer des projets.',
|
|
||||||
'projects.command.placeholder': 'Entrez une commande (par exemple, "Créer un nouveau projet nommé \'Rue Principale 42\'")',
|
|
||||||
'projects.command.empty': 'Aucune commande exécutée pour le moment. Envoyez une commande pour voir les résultats ici.',
|
|
||||||
|
|
||||||
// Data Management page
|
|
||||||
'data-management.title': 'Gestion des données',
|
|
||||||
'data-management.subtitle': 'Gestion des données',
|
|
||||||
'data-management.description': 'Gestion des données avec des tableaux',
|
|
||||||
'data-management.description_text': 'Gérez les données via des tableaux. Sélectionnez un tableau ou utilisez le langage naturel pour exécuter des commandes.',
|
|
||||||
'data-management.command.placeholder': 'Entrez une commande (par exemple, "Créer un nouveau projet nommé \'Rue Principale 42\'")',
|
|
||||||
'data-management.command.empty': 'Aucune commande exécutée pour le moment. Envoyez une commande pour voir les résultats ici.',
|
|
||||||
|
|
||||||
// Drag and Drop
|
|
||||||
'dragdrop.overlay.default_text': 'Déposer les fichiers ici',
|
|
||||||
'dragdrop.overlay.default_subtext': 'Vous pouvez aussi cliquer sur le bouton de téléchargement',
|
|
||||||
'dragdrop.overlay.processing': 'Traitement des fichiers...',
|
|
||||||
'dragdrop.overlay.error': 'Erreur lors du traitement des fichiers',
|
|
||||||
};
|
|
||||||
Loading…
Reference in a new issue