Fixed UI issues:
Data: roles & rules: fixed id and pydantic handling ui+gateway Table generic: Fixed column width changing with persistent user width not overwritten by default in the session Table generic: fixed active item in navbar working with subpaths Table generic: fixed sort and pagination with modified routes Table generic: enhanced logic to select page directly and bar on top of the table Table generic: Always load data with pagination, fixed page counter (not updated), enhanced pagination to access pages directly Table generic: Filter function enhanced directly as dropdown in column headers, sorting enhanced to have cascading sorting Table generic: Added multilingual to AttributeType and handle it in the form renderer, removed explicit field definitions from team-members.ts and prompts.ts to use dynamic generation from backend attributes strictly Table generic: Inline editting of checkboxes and boolean values Table generig: Only generic logic, no explicit logic (e.g. id's) Table generic: Removed all specific parts like action icons. To e a parameter by calling instance.
This commit is contained in:
parent
7d2808d22e
commit
b2c38e75bf
55 changed files with 4975 additions and 1274 deletions
|
|
@ -16,6 +16,12 @@ export interface UserPermissions {
|
||||||
|
|
||||||
export type PermissionContext = 'DATA' | 'UI' | 'RESOURCE';
|
export type PermissionContext = 'DATA' | 'UI' | 'RESOURCE';
|
||||||
|
|
||||||
|
// Response type for bulk permissions fetch
|
||||||
|
export interface BulkPermissionsResponse {
|
||||||
|
ui?: Record<string, UserPermissions>;
|
||||||
|
resource?: Record<string, UserPermissions>;
|
||||||
|
}
|
||||||
|
|
||||||
// Type for the request function passed to API functions
|
// Type for the request function passed to API functions
|
||||||
export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<any>;
|
export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<any>;
|
||||||
|
|
||||||
|
|
@ -38,32 +44,47 @@ export async function fetchPermissions(
|
||||||
params.item = item;
|
params.item = item;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('📡 fetchPermissions: Requesting permissions:', {
|
|
||||||
context,
|
|
||||||
item,
|
|
||||||
params,
|
|
||||||
url: '/api/rbac/permissions'
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await request({
|
const data = await request({
|
||||||
url: '/api/rbac/permissions',
|
url: '/api/rbac/permissions',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params
|
params
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('📥 fetchPermissions: Received permissions response:', {
|
return data;
|
||||||
context,
|
}
|
||||||
item,
|
|
||||||
response: data,
|
/**
|
||||||
view: data?.view,
|
* Fetch all permissions for a given context (UI or RESOURCE)
|
||||||
read: data?.read,
|
* Endpoint: GET /api/rbac/permissions/all
|
||||||
create: data?.create,
|
* Query params: context (optional - if not provided, returns both UI and RESOURCE)
|
||||||
update: data?.update,
|
*
|
||||||
delete: data?.delete,
|
* This is optimized for UI initialization to avoid multiple API calls.
|
||||||
type: typeof data,
|
* Returns a dictionary of item paths to their permissions.
|
||||||
isArray: Array.isArray(data),
|
*/
|
||||||
keys: data ? Object.keys(data) : [],
|
export async function fetchAllPermissions(
|
||||||
fullResponse: JSON.stringify(data, null, 2)
|
request: ApiRequestFunction,
|
||||||
|
context?: 'UI' | 'RESOURCE'
|
||||||
|
): Promise<BulkPermissionsResponse> {
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
if (context) {
|
||||||
|
params.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📡 fetchAllPermissions: Fetching all permissions:', {
|
||||||
|
context: context || 'all',
|
||||||
|
url: '/api/rbac/permissions/all'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await request({
|
||||||
|
url: '/api/rbac/permissions/all',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📥 fetchAllPermissions: Received bulk permissions:', {
|
||||||
|
context: context || 'all',
|
||||||
|
uiItemCount: data?.ui ? Object.keys(data.ui).length : 0,
|
||||||
|
resourceItemCount: data?.resource ? Object.keys(data.resource).length : 0
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|
|
||||||
660
src/api/trusteeApi.ts
Normal file
660
src/api/trusteeApi.ts
Normal file
|
|
@ -0,0 +1,660 @@
|
||||||
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TYPES & INTERFACES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface TrusteeOrganisation {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
enabled: boolean;
|
||||||
|
mandateId?: string;
|
||||||
|
_createdAt?: number;
|
||||||
|
_modifiedAt?: number;
|
||||||
|
_createdBy?: string;
|
||||||
|
_modifiedBy?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrusteeRole {
|
||||||
|
id: string;
|
||||||
|
desc: string;
|
||||||
|
mandateId?: string;
|
||||||
|
_createdAt?: number;
|
||||||
|
_modifiedAt?: number;
|
||||||
|
_createdBy?: string;
|
||||||
|
_modifiedBy?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrusteeAccess {
|
||||||
|
id: string;
|
||||||
|
organisationId: string;
|
||||||
|
roleId: string;
|
||||||
|
userId: string;
|
||||||
|
contractId?: string | null;
|
||||||
|
mandateId?: string;
|
||||||
|
_createdAt?: number;
|
||||||
|
_modifiedAt?: number;
|
||||||
|
_createdBy?: string;
|
||||||
|
_modifiedBy?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrusteeContract {
|
||||||
|
id: string;
|
||||||
|
organisationId: string;
|
||||||
|
label: string;
|
||||||
|
enabled: boolean;
|
||||||
|
mandateId?: string;
|
||||||
|
_createdAt?: number;
|
||||||
|
_modifiedAt?: number;
|
||||||
|
_createdBy?: string;
|
||||||
|
_modifiedBy?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrusteeDocument {
|
||||||
|
id: string;
|
||||||
|
organisationId: string;
|
||||||
|
contractId: string;
|
||||||
|
documentName: string;
|
||||||
|
documentMimeType: string;
|
||||||
|
documentData?: any; // Binary data, typically not included in list responses
|
||||||
|
mandateId?: string;
|
||||||
|
_createdAt?: number;
|
||||||
|
_modifiedAt?: number;
|
||||||
|
_createdBy?: string;
|
||||||
|
_modifiedBy?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrusteePosition {
|
||||||
|
id: string;
|
||||||
|
organisationId: string;
|
||||||
|
contractId: string;
|
||||||
|
valuta?: string;
|
||||||
|
transactionDateTime?: number;
|
||||||
|
company: string;
|
||||||
|
desc: string;
|
||||||
|
tags: string;
|
||||||
|
bookingCurrency: string;
|
||||||
|
bookingAmount: number;
|
||||||
|
originalCurrency: string;
|
||||||
|
originalAmount: number;
|
||||||
|
vatPercentage: number;
|
||||||
|
vatAmount: number;
|
||||||
|
mandateId?: string;
|
||||||
|
_createdAt?: number;
|
||||||
|
_modifiedAt?: number;
|
||||||
|
_createdBy?: string;
|
||||||
|
_modifiedBy?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrusteePositionDocument {
|
||||||
|
id: string;
|
||||||
|
organisationId: string;
|
||||||
|
contractId: string;
|
||||||
|
documentId: string;
|
||||||
|
positionId: string;
|
||||||
|
mandateId?: string;
|
||||||
|
_createdAt?: number;
|
||||||
|
_modifiedAt?: number;
|
||||||
|
_createdBy?: string;
|
||||||
|
_modifiedBy?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<any>;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// HELPER FUNCTIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function _buildPaginationParams(params?: PaginationParams): Record<string, any> {
|
||||||
|
const requestParams: any = {};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ORGANISATION API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export async function fetchOrganisations(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
params?: PaginationParams
|
||||||
|
): Promise<PaginatedResponse<TrusteeOrganisation> | TrusteeOrganisation[]> {
|
||||||
|
return await request({
|
||||||
|
url: '/api/trustee/organisations',
|
||||||
|
method: 'get',
|
||||||
|
params: _buildPaginationParams(params)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchOrganisationById(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
orgId: string
|
||||||
|
): Promise<TrusteeOrganisation | null> {
|
||||||
|
try {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/organisations/${orgId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching organisation by ID:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createOrganisation(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
data: Partial<TrusteeOrganisation>
|
||||||
|
): Promise<TrusteeOrganisation> {
|
||||||
|
return await request({
|
||||||
|
url: '/api/trustee/organisations',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateOrganisation(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
orgId: string,
|
||||||
|
data: Partial<TrusteeOrganisation>
|
||||||
|
): Promise<TrusteeOrganisation> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/organisations/${orgId}`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteOrganisation(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
orgId: string
|
||||||
|
): Promise<void> {
|
||||||
|
await request({
|
||||||
|
url: `/api/trustee/organisations/${orgId}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ROLE API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export async function fetchRoles(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
params?: PaginationParams
|
||||||
|
): Promise<PaginatedResponse<TrusteeRole> | TrusteeRole[]> {
|
||||||
|
return await request({
|
||||||
|
url: '/api/trustee/roles',
|
||||||
|
method: 'get',
|
||||||
|
params: _buildPaginationParams(params)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRoleById(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
roleId: string
|
||||||
|
): Promise<TrusteeRole | null> {
|
||||||
|
try {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/roles/${roleId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching role by ID:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRole(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
data: Partial<TrusteeRole>
|
||||||
|
): Promise<TrusteeRole> {
|
||||||
|
return await request({
|
||||||
|
url: '/api/trustee/roles',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateRole(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
roleId: string,
|
||||||
|
data: Partial<TrusteeRole>
|
||||||
|
): Promise<TrusteeRole> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/roles/${roleId}`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteRole(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
roleId: string
|
||||||
|
): Promise<void> {
|
||||||
|
await request({
|
||||||
|
url: `/api/trustee/roles/${roleId}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ACCESS API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export async function fetchAccess(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
params?: PaginationParams
|
||||||
|
): Promise<PaginatedResponse<TrusteeAccess> | TrusteeAccess[]> {
|
||||||
|
return await request({
|
||||||
|
url: '/api/trustee/access',
|
||||||
|
method: 'get',
|
||||||
|
params: _buildPaginationParams(params)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAccessById(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
accessId: string
|
||||||
|
): Promise<TrusteeAccess | null> {
|
||||||
|
try {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/access/${accessId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching access by ID:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAccessByOrganisation(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
orgId: string
|
||||||
|
): Promise<TrusteeAccess[]> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/access/organisation/${orgId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAccessByUser(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
userId: string
|
||||||
|
): Promise<TrusteeAccess[]> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/access/user/${userId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAccess(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
data: Partial<TrusteeAccess>
|
||||||
|
): Promise<TrusteeAccess> {
|
||||||
|
return await request({
|
||||||
|
url: '/api/trustee/access',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAccess(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
accessId: string,
|
||||||
|
data: Partial<TrusteeAccess>
|
||||||
|
): Promise<TrusteeAccess> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/access/${accessId}`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAccess(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
accessId: string
|
||||||
|
): Promise<void> {
|
||||||
|
await request({
|
||||||
|
url: `/api/trustee/access/${accessId}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CONTRACT API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export async function fetchContracts(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
params?: PaginationParams
|
||||||
|
): Promise<PaginatedResponse<TrusteeContract> | TrusteeContract[]> {
|
||||||
|
return await request({
|
||||||
|
url: '/api/trustee/contracts',
|
||||||
|
method: 'get',
|
||||||
|
params: _buildPaginationParams(params)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchContractById(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
contractId: string
|
||||||
|
): Promise<TrusteeContract | null> {
|
||||||
|
try {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/contracts/${contractId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching contract by ID:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchContractsByOrganisation(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
orgId: string
|
||||||
|
): Promise<TrusteeContract[]> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/contracts/organisation/${orgId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createContract(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
data: Partial<TrusteeContract>
|
||||||
|
): Promise<TrusteeContract> {
|
||||||
|
return await request({
|
||||||
|
url: '/api/trustee/contracts',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateContract(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
contractId: string,
|
||||||
|
data: Partial<TrusteeContract>
|
||||||
|
): Promise<TrusteeContract> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/contracts/${contractId}`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteContract(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
contractId: string
|
||||||
|
): Promise<void> {
|
||||||
|
await request({
|
||||||
|
url: `/api/trustee/contracts/${contractId}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DOCUMENT API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export async function fetchDocuments(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
params?: PaginationParams
|
||||||
|
): Promise<PaginatedResponse<TrusteeDocument> | TrusteeDocument[]> {
|
||||||
|
return await request({
|
||||||
|
url: '/api/trustee/documents',
|
||||||
|
method: 'get',
|
||||||
|
params: _buildPaginationParams(params)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDocumentById(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
documentId: string
|
||||||
|
): Promise<TrusteeDocument | null> {
|
||||||
|
try {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/documents/${documentId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching document by ID:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDocumentsByContract(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
contractId: string
|
||||||
|
): Promise<TrusteeDocument[]> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/documents/contract/${contractId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createDocument(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
data: Partial<TrusteeDocument>
|
||||||
|
): Promise<TrusteeDocument> {
|
||||||
|
return await request({
|
||||||
|
url: '/api/trustee/documents',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateDocument(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
documentId: string,
|
||||||
|
data: Partial<TrusteeDocument>
|
||||||
|
): Promise<TrusteeDocument> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/documents/${documentId}`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteDocument(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
documentId: string
|
||||||
|
): Promise<void> {
|
||||||
|
await request({
|
||||||
|
url: `/api/trustee/documents/${documentId}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// POSITION API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export async function fetchPositions(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
params?: PaginationParams
|
||||||
|
): Promise<PaginatedResponse<TrusteePosition> | TrusteePosition[]> {
|
||||||
|
return await request({
|
||||||
|
url: '/api/trustee/positions',
|
||||||
|
method: 'get',
|
||||||
|
params: _buildPaginationParams(params)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPositionById(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
positionId: string
|
||||||
|
): Promise<TrusteePosition | null> {
|
||||||
|
try {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/positions/${positionId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching position by ID:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPositionsByContract(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
contractId: string
|
||||||
|
): Promise<TrusteePosition[]> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/positions/contract/${contractId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPositionsByOrganisation(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
orgId: string
|
||||||
|
): Promise<TrusteePosition[]> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/positions/organisation/${orgId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPosition(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
data: Partial<TrusteePosition>
|
||||||
|
): Promise<TrusteePosition> {
|
||||||
|
return await request({
|
||||||
|
url: '/api/trustee/positions',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePosition(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
positionId: string,
|
||||||
|
data: Partial<TrusteePosition>
|
||||||
|
): Promise<TrusteePosition> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/positions/${positionId}`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePosition(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
positionId: string
|
||||||
|
): Promise<void> {
|
||||||
|
await request({
|
||||||
|
url: `/api/trustee/positions/${positionId}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// POSITION-DOCUMENT API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export async function fetchPositionDocuments(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
params?: PaginationParams
|
||||||
|
): Promise<PaginatedResponse<TrusteePositionDocument> | TrusteePositionDocument[]> {
|
||||||
|
return await request({
|
||||||
|
url: '/api/trustee/position-documents',
|
||||||
|
method: 'get',
|
||||||
|
params: _buildPaginationParams(params)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPositionDocumentById(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
linkId: string
|
||||||
|
): Promise<TrusteePositionDocument | null> {
|
||||||
|
try {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/position-documents/${linkId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching position-document link by ID:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDocumentsForPosition(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
positionId: string
|
||||||
|
): Promise<TrusteePositionDocument[]> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/position-documents/position/${positionId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPositionsForDocument(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
documentId: string
|
||||||
|
): Promise<TrusteePositionDocument[]> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/trustee/position-documents/document/${documentId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPositionDocument(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
data: Partial<TrusteePositionDocument>
|
||||||
|
): Promise<TrusteePositionDocument> {
|
||||||
|
return await request({
|
||||||
|
url: '/api/trustee/position-documents',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePositionDocument(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
linkId: string
|
||||||
|
): Promise<void> {
|
||||||
|
await request({
|
||||||
|
url: `/api/trustee/position-documents/${linkId}`,
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -227,3 +227,19 @@ export async function deleteUser(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send password setup link to a user
|
||||||
|
* Endpoint: POST /api/users/{userId}/send-password-link
|
||||||
|
*/
|
||||||
|
export async function sendPasswordLink(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
userId: string,
|
||||||
|
frontendUrl: string
|
||||||
|
): Promise<{ message: string; userId: string; email: string }> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/users/${userId}/send-password-link`,
|
||||||
|
method: 'post',
|
||||||
|
data: { frontendUrl }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,36 @@
|
||||||
background: var(--color-secondary-hover);
|
background: var(--color-secondary-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Generic Custom Action Button */
|
||||||
|
.actionButton.custom {
|
||||||
|
background: var(--color-secondary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionButton.custom:hover {
|
||||||
|
background: var(--color-secondary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success State */
|
||||||
|
.actionButton.success {
|
||||||
|
background: #28a745 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionButton.success:hover {
|
||||||
|
background: #218838 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error State */
|
||||||
|
.actionButton.error {
|
||||||
|
background: #dc3545 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionButton.error:hover {
|
||||||
|
background: #c82333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive Design */
|
/* Responsive Design */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.actionButtons {
|
.actionButtons {
|
||||||
|
|
@ -274,4 +304,12 @@
|
||||||
.actionButton.refresh:hover {
|
.actionButton.refresh:hover {
|
||||||
background: var(--color-secondary-hover);
|
background: var(--color-secondary-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actionButton.custom {
|
||||||
|
background: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionButton.custom:hover {
|
||||||
|
background: var(--color-secondary-hover);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { IoIosLink } from 'react-icons/io';
|
|
||||||
import { IoIosRefresh } from 'react-icons/io';
|
|
||||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
|
||||||
import styles from '../ActionButton.module.css';
|
|
||||||
|
|
||||||
export interface ConnectActionButtonProps<T = any> {
|
|
||||||
row: T;
|
|
||||||
disabled?: boolean | { disabled: boolean; message?: string };
|
|
||||||
loading?: boolean;
|
|
||||||
className?: string;
|
|
||||||
title?: string;
|
|
||||||
connectTitle?: string;
|
|
||||||
refreshTitle?: string;
|
|
||||||
hookData: any; // REQUIRED: Contains all hook data including operations
|
|
||||||
// Field mappings
|
|
||||||
idField?: string; // Field name for the unique identifier
|
|
||||||
statusField?: string; // Field name for the status field
|
|
||||||
operationName?: string; // Name of the connect operation in hookData
|
|
||||||
loadingStateName?: string; // Name of the loading state in hookData
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ConnectActionButton<T = any>({
|
|
||||||
row,
|
|
||||||
disabled = false,
|
|
||||||
loading = false,
|
|
||||||
className = '',
|
|
||||||
title,
|
|
||||||
connectTitle,
|
|
||||||
refreshTitle,
|
|
||||||
hookData,
|
|
||||||
idField = 'id',
|
|
||||||
statusField = 'status',
|
|
||||||
operationName = 'connectWithPopup',
|
|
||||||
loadingStateName = 'isConnecting'
|
|
||||||
}: ConnectActionButtonProps<T>) {
|
|
||||||
const { t } = useLanguage();
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
|
||||||
|
|
||||||
// Extract disabled state and tooltip message
|
|
||||||
const isDisabled = typeof disabled === 'boolean' ? disabled : disabled?.disabled || false;
|
|
||||||
const disabledMessage = typeof disabled === 'object' ? disabled?.message : undefined;
|
|
||||||
|
|
||||||
// Validate that hookData is provided with required operations
|
|
||||||
if (!hookData) {
|
|
||||||
throw new Error('ConnectActionButton requires hookData to be provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the connection data from the row
|
|
||||||
const connectionStatus = (row as any)[statusField];
|
|
||||||
const connectionId = (row as any)[idField];
|
|
||||||
const isActive = connectionStatus === 'active';
|
|
||||||
|
|
||||||
// Extract operations from hookData
|
|
||||||
const handleConnect = hookData[operationName];
|
|
||||||
const refetch = hookData.refetch;
|
|
||||||
const loadingState = hookData[loadingStateName];
|
|
||||||
|
|
||||||
// Validate required operations exist
|
|
||||||
if (!handleConnect) {
|
|
||||||
throw new Error(`ConnectActionButton requires hookData.${operationName} to be defined`);
|
|
||||||
}
|
|
||||||
if (!refetch) {
|
|
||||||
throw new Error('ConnectActionButton requires hookData.refetch to be defined');
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClick = async (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!isDisabled && !loading && !isProcessing) {
|
|
||||||
setIsProcessing(true);
|
|
||||||
try {
|
|
||||||
// Always use the connect operation for both active and inactive connections
|
|
||||||
// The backend will handle refreshing tokens for active connections
|
|
||||||
if (handleConnect) {
|
|
||||||
await handleConnect(connectionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refetch to update the connection status
|
|
||||||
if (refetch) {
|
|
||||||
await refetch();
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Connection operation failed:', error);
|
|
||||||
} finally {
|
|
||||||
setIsProcessing(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Determine button title and icon based on connection status
|
|
||||||
const defaultTitle = isActive
|
|
||||||
? (refreshTitle || t('connections.action.refresh', 'Refresh'))
|
|
||||||
: (connectTitle || t('connections.action.connect', 'Connect'));
|
|
||||||
|
|
||||||
const buttonTitle = title || defaultTitle;
|
|
||||||
const buttonIcon = isActive ? <IoIosRefresh /> : <IoIosLink />;
|
|
||||||
|
|
||||||
// Check if this specific connection is being processed
|
|
||||||
const isLoadingState = loadingState === true || isProcessing;
|
|
||||||
|
|
||||||
// Determine the final button title (tooltip)
|
|
||||||
const finalTitle = isDisabled && disabledMessage ? disabledMessage : buttonTitle;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={handleClick}
|
|
||||||
className={`${styles.actionButton} ${isActive ? styles.refresh : styles.connect} ${isLoadingState ? styles.loading : ''} ${isDisabled ? styles.disabled : ''} ${className}`}
|
|
||||||
title={finalTitle}
|
|
||||||
disabled={isDisabled || loading || isLoadingState}
|
|
||||||
>
|
|
||||||
<span className={styles.actionIcon}>
|
|
||||||
{isLoadingState ? <IoIosRefresh /> : buttonIcon}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ConnectActionButton;
|
|
||||||
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export { default as ConnectActionButton } from './ConnectActionButton';
|
|
||||||
export type { ConnectActionButtonProps } from './ConnectActionButton';
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||||
|
import styles from '../ActionButton.module.css';
|
||||||
|
|
||||||
|
export interface CustomActionButtonProps<T = any> {
|
||||||
|
row: T;
|
||||||
|
id: string; // Unique identifier for the action
|
||||||
|
icon: React.ReactNode; // Icon component to display
|
||||||
|
onClick: (row: T, hookData?: any) => Promise<void> | void; // Handler function
|
||||||
|
visible?: (row: T, hookData?: any) => boolean; // Show/hide based on row data
|
||||||
|
disabled?: (row: T, hookData?: any) => boolean | { disabled: boolean; message?: string };
|
||||||
|
loading?: (row: T, hookData?: any) => boolean;
|
||||||
|
title?: string | ((row: T) => string); // Tooltip text or translation key
|
||||||
|
className?: string; // Optional custom CSS class
|
||||||
|
hookData?: any; // Hook data passed through for context
|
||||||
|
idField?: string; // Field name for unique identifier (default: 'id')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CustomActionButton<T = any>({
|
||||||
|
row,
|
||||||
|
id,
|
||||||
|
icon,
|
||||||
|
onClick,
|
||||||
|
visible,
|
||||||
|
disabled,
|
||||||
|
loading,
|
||||||
|
title,
|
||||||
|
className = '',
|
||||||
|
hookData,
|
||||||
|
idField: _idField = 'id' // Available for future use, kept for API consistency
|
||||||
|
}: CustomActionButtonProps<T>) {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const [internalLoading, setInternalLoading] = useState(false);
|
||||||
|
const [showSuccessFeedback, setShowSuccessFeedback] = useState(false);
|
||||||
|
const [showErrorFeedback, setShowErrorFeedback] = useState(false);
|
||||||
|
|
||||||
|
// Check visibility - if not visible, don't render
|
||||||
|
if (visible && !visible(row, hookData)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract disabled state and tooltip message
|
||||||
|
const disabledResult = disabled ? disabled(row, hookData) : false;
|
||||||
|
const isDisabled = typeof disabledResult === 'boolean' ? disabledResult : disabledResult?.disabled || false;
|
||||||
|
const disabledMessage = typeof disabledResult === 'object' ? disabledResult?.message : undefined;
|
||||||
|
|
||||||
|
// Check loading state
|
||||||
|
const isLoadingFromProp = loading ? loading(row, hookData) : false;
|
||||||
|
const isLoading = isLoadingFromProp || internalLoading;
|
||||||
|
|
||||||
|
// Resolve title
|
||||||
|
let buttonTitle = '';
|
||||||
|
if (typeof title === 'function') {
|
||||||
|
buttonTitle = title(row);
|
||||||
|
} else if (typeof title === 'string') {
|
||||||
|
buttonTitle = t(title, title); // Try to translate, fallback to original
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the final tooltip
|
||||||
|
const finalTitle = isDisabled && disabledMessage ? disabledMessage : buttonTitle;
|
||||||
|
|
||||||
|
const handleClick = async (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!isDisabled && !isLoading) {
|
||||||
|
setInternalLoading(true);
|
||||||
|
setShowErrorFeedback(false);
|
||||||
|
setShowSuccessFeedback(false);
|
||||||
|
try {
|
||||||
|
await onClick(row, hookData);
|
||||||
|
// Show brief success feedback
|
||||||
|
setShowSuccessFeedback(true);
|
||||||
|
setTimeout(() => setShowSuccessFeedback(false), 2000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`CustomActionButton (${id}): Action failed:`, error);
|
||||||
|
setShowErrorFeedback(true);
|
||||||
|
setTimeout(() => setShowErrorFeedback(false), 3000);
|
||||||
|
} finally {
|
||||||
|
setInternalLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine icon to show based on state
|
||||||
|
let displayIcon: React.ReactNode = icon;
|
||||||
|
if (isLoading) {
|
||||||
|
displayIcon = '⏳';
|
||||||
|
} else if (showSuccessFeedback) {
|
||||||
|
displayIcon = '✓';
|
||||||
|
} else if (showErrorFeedback) {
|
||||||
|
displayIcon = '✗';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleClick}
|
||||||
|
className={`${styles.actionButton} ${styles.custom} ${isLoading ? styles.loading : ''} ${isDisabled ? styles.disabled : ''} ${showSuccessFeedback ? styles.success : ''} ${showErrorFeedback ? styles.error : ''} ${className}`}
|
||||||
|
title={finalTitle}
|
||||||
|
disabled={isDisabled || isLoading}
|
||||||
|
data-action-id={id}
|
||||||
|
>
|
||||||
|
<span className={styles.actionIcon}>
|
||||||
|
{displayIcon}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomActionButton;
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { CustomActionButton } from './CustomActionButton';
|
||||||
|
export type { CustomActionButtonProps } from './CustomActionButton';
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { IoIosDownload } from 'react-icons/io';
|
|
||||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
|
||||||
import styles from '../ActionButton.module.css';
|
|
||||||
|
|
||||||
export interface DownloadActionButtonProps<T = any> {
|
|
||||||
row: T;
|
|
||||||
onDownload: (row: T) => Promise<void> | void;
|
|
||||||
disabled?: boolean | { disabled: boolean; message?: string };
|
|
||||||
loading?: boolean;
|
|
||||||
className?: string;
|
|
||||||
title?: string;
|
|
||||||
isDownloading?: boolean;
|
|
||||||
hookData?: any; // Contains all hook data including operations
|
|
||||||
// Field mappings
|
|
||||||
idField?: string; // Field name for the unique identifier
|
|
||||||
nameField?: string; // Field name for file name (with extension)
|
|
||||||
loadingStateName?: string; // Name of the loading state in hookData
|
|
||||||
operationName?: string; // Name of the operation function in hookData
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DownloadActionButton<T = any>({
|
|
||||||
row,
|
|
||||||
onDownload,
|
|
||||||
disabled = false,
|
|
||||||
loading = false,
|
|
||||||
className = '',
|
|
||||||
title,
|
|
||||||
isDownloading = false,
|
|
||||||
hookData,
|
|
||||||
idField = 'id',
|
|
||||||
nameField,
|
|
||||||
loadingStateName = 'downloadingFiles',
|
|
||||||
operationName
|
|
||||||
}: DownloadActionButtonProps<T>) {
|
|
||||||
const { t } = useLanguage();
|
|
||||||
const [internalLoading, setInternalLoading] = useState(false);
|
|
||||||
|
|
||||||
// Extract disabled state and tooltip message
|
|
||||||
const isDisabled = typeof disabled === 'boolean' ? disabled : disabled?.disabled || false;
|
|
||||||
const disabledMessage = typeof disabled === 'object' ? disabled?.message : undefined;
|
|
||||||
|
|
||||||
// Extract file name from row using nameField or fallback to common field names
|
|
||||||
const getFileName = (): string => {
|
|
||||||
const rowAny = row as any;
|
|
||||||
|
|
||||||
// If nameField is explicitly provided, use it
|
|
||||||
if (nameField && rowAny[nameField]) {
|
|
||||||
return rowAny[nameField];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try common field names in order of preference
|
|
||||||
if (rowAny.fileName) return rowAny.fileName;
|
|
||||||
if (rowAny.file_name) return rowAny.file_name;
|
|
||||||
if (rowAny.name) return rowAny.name;
|
|
||||||
|
|
||||||
// Fallback: try to find any field that might contain the file name
|
|
||||||
const possibleFields = ['fileName', 'file_name', 'name', 'filename'];
|
|
||||||
for (const field of possibleFields) {
|
|
||||||
if (rowAny[field]) {
|
|
||||||
return rowAny[field];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last resort: use id or a default
|
|
||||||
return rowAny[idField] || 'download';
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClick = async (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!isDisabled && !loading && !isDownloading && !internalLoading) {
|
|
||||||
setInternalLoading(true);
|
|
||||||
try {
|
|
||||||
// If operationName is provided and hookData is available, use the hook function
|
|
||||||
if (operationName && hookData && hookData[operationName]) {
|
|
||||||
const fileId = (row as any)[idField];
|
|
||||||
const fileName = getFileName();
|
|
||||||
await hookData[operationName](fileId, fileName);
|
|
||||||
} else if (onDownload) {
|
|
||||||
// Fallback to the provided onDownload function
|
|
||||||
await onDownload(row);
|
|
||||||
} else {
|
|
||||||
console.error('No download function available');
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setInternalLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttonTitle = title || t('files.action.download', 'Download');
|
|
||||||
// Use hookData downloading state if available, otherwise use passed isDownloading
|
|
||||||
const loadingState = hookData?.[loadingStateName];
|
|
||||||
const actualIsDownloading = loadingState?.has((row as any)[idField]) || isDownloading;
|
|
||||||
const isLoading = loading || actualIsDownloading || internalLoading;
|
|
||||||
|
|
||||||
// Determine the final button title (tooltip)
|
|
||||||
const finalTitle = isDisabled && disabledMessage ? disabledMessage : buttonTitle;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={handleClick}
|
|
||||||
className={`${styles.actionButton} ${styles.download} ${isLoading ? styles.loading : ''} ${isDisabled ? styles.disabled : ''} ${className}`}
|
|
||||||
title={finalTitle}
|
|
||||||
disabled={isDisabled || isLoading}
|
|
||||||
>
|
|
||||||
<span className={styles.actionIcon}>
|
|
||||||
{isLoading ? '⏳' : <IoIosDownload />}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DownloadActionButton;
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export { default as DownloadActionButton } from './DownloadActionButton';
|
|
||||||
export type { DownloadActionButtonProps } from './DownloadActionButton';
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { IoIosPlay } from 'react-icons/io';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
|
||||||
import { useWorkflowSelection } from '../../../../contexts/WorkflowSelectionContext';
|
|
||||||
import styles from '../ActionButton.module.css';
|
|
||||||
|
|
||||||
export interface PlayActionButtonProps<T = any> {
|
|
||||||
row: T;
|
|
||||||
onPlay?: (row: T) => Promise<void> | void;
|
|
||||||
disabled?: boolean | { disabled: boolean; message?: string };
|
|
||||||
loading?: boolean;
|
|
||||||
className?: string;
|
|
||||||
title?: string;
|
|
||||||
hookData?: any; // Contains all hook data including operations
|
|
||||||
// Field mappings
|
|
||||||
idField?: string; // Field name for the unique identifier
|
|
||||||
nameField?: string; // Field name for display name
|
|
||||||
contentField?: string; // Field name for content (e.g., 'content' for prompts, 'prompt' for workflows)
|
|
||||||
// Navigation
|
|
||||||
navigateTo?: string; // Path to navigate to after selection (default: 'start/dashboard')
|
|
||||||
// Behavior
|
|
||||||
mode?: 'workflow' | 'prompt'; // 'workflow' selects workflow, 'prompt' sets input value
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PlayActionButton<T = any>({
|
|
||||||
row,
|
|
||||||
onPlay,
|
|
||||||
disabled = false,
|
|
||||||
loading = false,
|
|
||||||
className = '',
|
|
||||||
title,
|
|
||||||
hookData: _hookData,
|
|
||||||
idField = 'id',
|
|
||||||
nameField: _nameField = 'name',
|
|
||||||
contentField = 'content',
|
|
||||||
navigateTo = 'start/dashboard',
|
|
||||||
mode = 'prompt'
|
|
||||||
}: PlayActionButtonProps<T>) {
|
|
||||||
const { t } = useLanguage();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { selectWorkflow } = useWorkflowSelection();
|
|
||||||
|
|
||||||
// Extract disabled state and tooltip message
|
|
||||||
const isDisabled = typeof disabled === 'boolean' ? disabled : disabled?.disabled || false;
|
|
||||||
const disabledMessage = typeof disabled === 'object' ? disabled?.message : undefined;
|
|
||||||
|
|
||||||
const handleClick = async (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!isDisabled && !loading) {
|
|
||||||
try {
|
|
||||||
// Call the onPlay callback if provided
|
|
||||||
if (onPlay) {
|
|
||||||
await onPlay(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === 'workflow') {
|
|
||||||
// Workflow mode: reset workflow state and select workflow
|
|
||||||
const workflowId = (row as any)[idField];
|
|
||||||
if (!workflowId) {
|
|
||||||
console.error('Workflow ID not found in row');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch event to reset workflow state before selecting new one
|
|
||||||
// This ensures the dashboard resets and loads the selected workflow
|
|
||||||
window.dispatchEvent(new CustomEvent('workflowCleared', {
|
|
||||||
detail: { workflowId: null }
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Select the workflow in context (this will trigger sync in dashboard)
|
|
||||||
selectWorkflow(workflowId);
|
|
||||||
|
|
||||||
// Also dispatch workflowSelected event for any other listeners
|
|
||||||
window.dispatchEvent(new CustomEvent('workflowSelected', {
|
|
||||||
detail: { workflowId }
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
// Prompt mode: set input value in dashboard
|
|
||||||
const content = (row as any)[contentField];
|
|
||||||
if (content && typeof content === 'string') {
|
|
||||||
// Dispatch event to set dashboard input value
|
|
||||||
window.dispatchEvent(new CustomEvent('dashboardSetInput', {
|
|
||||||
detail: { value: content }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to dashboard (or specified path)
|
|
||||||
navigate(`/${navigateTo}`);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error in PlayActionButton:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttonTitle = title || (mode === 'workflow'
|
|
||||||
? t('workflows.action.play', 'Play')
|
|
||||||
: t('prompts.action.start', 'Start Prompt'));
|
|
||||||
const isLoading = loading;
|
|
||||||
const finalTitle = isDisabled && disabledMessage ? disabledMessage : buttonTitle;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={handleClick}
|
|
||||||
className={`${styles.actionButton} ${styles.play} ${isLoading ? styles.loading : ''} ${isDisabled ? styles.disabled : ''} ${className}`}
|
|
||||||
title={finalTitle}
|
|
||||||
disabled={isDisabled || isLoading}
|
|
||||||
>
|
|
||||||
<span className={styles.actionIcon}>
|
|
||||||
{isLoading ? '⏳' : <IoIosPlay />}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PlayActionButton;
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export { PlayActionButton } from './PlayActionButton';
|
|
||||||
export type { PlayActionButtonProps } from './PlayActionButton';
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
// Action Button Components
|
// Standard Action Button Components (built-in)
|
||||||
export { EditActionButton } from './EditActionButton';
|
export { EditActionButton } from './EditActionButton';
|
||||||
export { DeleteActionButton } from './DeleteActionButton';
|
export { DeleteActionButton } from './DeleteActionButton';
|
||||||
export { DownloadActionButton } from './DownloadActionButton';
|
|
||||||
export { ViewActionButton } from './ViewActionButton';
|
export { ViewActionButton } from './ViewActionButton';
|
||||||
export { CopyActionButton } from './CopyActionButton';
|
export { CopyActionButton } from './CopyActionButton';
|
||||||
export { ConnectActionButton } from './ConnectActionButton';
|
|
||||||
export { PlayActionButton } from './PlayActionButton';
|
|
||||||
export { RemoveActionButton } from './RemoveActionButton';
|
export { RemoveActionButton } from './RemoveActionButton';
|
||||||
|
|
||||||
|
// Generic Custom Action Button (for entity-specific actions)
|
||||||
|
export { CustomActionButton } from './CustomActionButton';
|
||||||
|
|
||||||
// Action Button Types
|
// Action Button Types
|
||||||
export type { EditActionButtonProps } from './EditActionButton';
|
export type { EditActionButtonProps } from './EditActionButton';
|
||||||
export type { DeleteActionButtonProps } from './DeleteActionButton';
|
export type { DeleteActionButtonProps } from './DeleteActionButton';
|
||||||
export type { DownloadActionButtonProps } from './DownloadActionButton';
|
|
||||||
export type { ViewActionButtonProps } from './ViewActionButton';
|
export type { ViewActionButtonProps } from './ViewActionButton';
|
||||||
export type { CopyActionButtonProps } from './CopyActionButton';
|
export type { CopyActionButtonProps } from './CopyActionButton';
|
||||||
export type { ConnectActionButtonProps } from './ConnectActionButton';
|
|
||||||
export type { PlayActionButtonProps } from './PlayActionButton';
|
|
||||||
export type { RemoveActionButtonProps } from './RemoveActionButton';
|
export type { RemoveActionButtonProps } from './RemoveActionButton';
|
||||||
|
export type { CustomActionButtonProps } from './CustomActionButton';
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,15 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.activeFiltersCount {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-secondary);
|
||||||
|
background: rgba(var(--color-secondary-rgb), 0.1);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.refreshButton {
|
.refreshButton {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import styles from './FormGeneratorControls.module.css';
|
||||||
import { Button } from '../../UiComponents/Button';
|
import { Button } from '../../UiComponents/Button';
|
||||||
import { IoIosRefresh } from "react-icons/io";
|
import { IoIosRefresh } from "react-icons/io";
|
||||||
import { FaTrash } from "react-icons/fa";
|
import { FaTrash } from "react-icons/fa";
|
||||||
import { isCheckboxType } from '../../../utils/attributeTypeMapper';
|
|
||||||
import type { AttributeType } from '../../../utils/attributeTypeMapper';
|
import type { AttributeType } from '../../../utils/attributeTypeMapper';
|
||||||
|
|
||||||
// Generic field/column config interface
|
// Generic field/column config interface
|
||||||
|
|
@ -26,7 +25,7 @@ export interface FormGeneratorControlsProps {
|
||||||
searchFocused: boolean;
|
searchFocused: boolean;
|
||||||
onSearchFocus: (focused: boolean) => void;
|
onSearchFocus: (focused: boolean) => void;
|
||||||
|
|
||||||
// Filter state
|
// Filter state (kept for compatibility but not used in this component)
|
||||||
filters: Record<string, any>;
|
filters: Record<string, any>;
|
||||||
onFilterChange: (key: string, value: any) => void;
|
onFilterChange: (key: string, value: any) => void;
|
||||||
filterFocused: Record<string, boolean>;
|
filterFocused: Record<string, boolean>;
|
||||||
|
|
@ -49,113 +48,29 @@ export interface FormGeneratorControlsProps {
|
||||||
selectable?: boolean;
|
selectable?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
|
||||||
// Special date filter handler (for FormGenerator date formatting)
|
// Active filters count for display
|
||||||
onDateFilterChange?: (key: string, value: string) => void;
|
activeFiltersCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormGeneratorControls({
|
export function FormGeneratorControls({
|
||||||
fields,
|
|
||||||
searchTerm,
|
searchTerm,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
searchFocused,
|
searchFocused,
|
||||||
onSearchFocus,
|
onSearchFocus,
|
||||||
filters,
|
|
||||||
onFilterChange,
|
|
||||||
filterFocused,
|
|
||||||
onFilterFocus,
|
|
||||||
selectedCount,
|
selectedCount,
|
||||||
displayData,
|
displayData,
|
||||||
onDeleteSingle,
|
onDeleteSingle,
|
||||||
onDeleteMultiple,
|
onDeleteMultiple,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
searchable = true,
|
searchable = true,
|
||||||
filterable = true,
|
|
||||||
selectable = true,
|
selectable = true,
|
||||||
loading = false,
|
loading = false,
|
||||||
onDateFilterChange
|
activeFiltersCount = 0
|
||||||
}: FormGeneratorControlsProps) {
|
}: FormGeneratorControlsProps) {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
|
||||||
// Check if all items are selected
|
// Check if all items are selected
|
||||||
const allItemsSelected = selectedCount > 0 && displayData.length > 0 && selectedCount === displayData.length;
|
const allItemsSelected = selectedCount > 0 && displayData.length > 0 && selectedCount === displayData.length;
|
||||||
|
|
||||||
// Filter fields that are filterable
|
|
||||||
const filterableFields = fields.filter(field => {
|
|
||||||
if (field.type === 'readonly') return false;
|
|
||||||
return field.filterable !== false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle date filter with special formatting (for FormGenerator)
|
|
||||||
const handleDateFilterChange = (key: string, value: string) => {
|
|
||||||
if (onDateFilterChange) {
|
|
||||||
onDateFilterChange(key, value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Default behavior for FormGeneratorList
|
|
||||||
onFilterChange(key, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Date filter formatting logic (for FormGenerator)
|
|
||||||
const handleDateFilterInput = (key: string, e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
let value = e.target.value;
|
|
||||||
const currentValue = filters[key] || '';
|
|
||||||
|
|
||||||
// Check if user is deleting (new value is shorter)
|
|
||||||
const isDeleting = value.length < currentValue.length;
|
|
||||||
|
|
||||||
if (isDeleting) {
|
|
||||||
// When deleting, preserve the exact input without auto-formatting
|
|
||||||
handleDateFilterChange(key, value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-pad single digits followed by dot (e.g., "4." -> "04.")
|
|
||||||
value = value.replace(/^(\d)\./, '0$1.');
|
|
||||||
value = value.replace(/\.(\d)\./, '.0$1.');
|
|
||||||
|
|
||||||
// Allow typing and format as DD.MM.YYYY
|
|
||||||
const digitsOnly = value.replace(/\D/g, ''); // Remove non-digits
|
|
||||||
|
|
||||||
let formatted = '';
|
|
||||||
if (digitsOnly.length >= 8) {
|
|
||||||
// Full format: DDMMYYYY -> DD.MM.YYYY
|
|
||||||
const day = digitsOnly.slice(0, 2);
|
|
||||||
const month = digitsOnly.slice(2, 4);
|
|
||||||
const year = digitsOnly.slice(4, 8);
|
|
||||||
|
|
||||||
// Validate day (01-31) and month (01-12)
|
|
||||||
if (parseInt(day) > 31 || parseInt(month) > 12 || parseInt(day) === 0 || parseInt(month) === 0) {
|
|
||||||
return; // Don't update if invalid
|
|
||||||
}
|
|
||||||
formatted = `${day}.${month}.${year}`;
|
|
||||||
} else if (digitsOnly.length >= 4) {
|
|
||||||
// Partial format: DDMM -> DD.MM.
|
|
||||||
const day = digitsOnly.slice(0, 2);
|
|
||||||
const month = digitsOnly.slice(2, 4);
|
|
||||||
const remaining = digitsOnly.slice(4);
|
|
||||||
|
|
||||||
// Validate day (01-31) and month (01-12)
|
|
||||||
if (parseInt(day) > 31 || parseInt(month) > 12 || parseInt(day) === 0 || parseInt(month) === 0) {
|
|
||||||
return; // Don't update if invalid
|
|
||||||
}
|
|
||||||
formatted = `${day}.${month}.${remaining}`;
|
|
||||||
} else if (digitsOnly.length >= 2) {
|
|
||||||
// Start format: DD -> DD.
|
|
||||||
const day = digitsOnly.slice(0, 2);
|
|
||||||
const remaining = digitsOnly.slice(2);
|
|
||||||
|
|
||||||
// Validate day (01-31)
|
|
||||||
if (parseInt(day) > 31 || parseInt(day) === 0) {
|
|
||||||
return; // Don't update if invalid
|
|
||||||
}
|
|
||||||
formatted = `${day}.${remaining}`;
|
|
||||||
} else {
|
|
||||||
// Just digits
|
|
||||||
formatted = digitsOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDateFilterChange(key, formatted);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.controls}>
|
<div className={styles.controls}>
|
||||||
|
|
@ -204,6 +119,11 @@ export function FormGeneratorControls({
|
||||||
{t('formgen.search.placeholder')}
|
{t('formgen.search.placeholder')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
{activeFiltersCount > 0 && (
|
||||||
|
<span className={styles.activeFiltersCount}>
|
||||||
|
{activeFiltersCount} {t('formgen.filter.active', 'filter(s)')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{onRefresh && (
|
{onRefresh && (
|
||||||
<button
|
<button
|
||||||
onClick={onRefresh}
|
onClick={onRefresh}
|
||||||
|
|
@ -216,93 +136,6 @@ export function FormGeneratorControls({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Filters */}
|
|
||||||
{filterable && (
|
|
||||||
<div className={styles.filtersContainer}>
|
|
||||||
{filterableFields.map(field => (
|
|
||||||
<div key={field.key} className={styles.filterGroup}>
|
|
||||||
{field.type && isCheckboxType(field.type) ? (
|
|
||||||
<div className={styles.customSelectContainer}>
|
|
||||||
<select
|
|
||||||
value={filters[field.key] || ''}
|
|
||||||
onChange={(e) => onFilterChange(field.key, e.target.value === '' ? undefined : e.target.value === 'true')}
|
|
||||||
className={`${styles.filterSelect} ${filters[field.key] ? styles.hasValue : ''}`}
|
|
||||||
>
|
|
||||||
<option value="" disabled hidden>{field.label}</option>
|
|
||||||
<option value="true">{t('formgen.filter.yes')}</option>
|
|
||||||
<option value="false">{t('formgen.filter.no')}</option>
|
|
||||||
</select>
|
|
||||||
{filters[field.key] && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => onFilterChange(field.key, '')}
|
|
||||||
className={styles.clearFilterButton}
|
|
||||||
title={t('formgen.filter.clear')}
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : field.filterOptions ? (
|
|
||||||
<div className={styles.customSelectContainer}>
|
|
||||||
<select
|
|
||||||
value={filters[field.key] || ''}
|
|
||||||
onChange={(e) => onFilterChange(field.key, e.target.value)}
|
|
||||||
className={`${styles.filterSelect} ${filters[field.key] ? styles.hasValue : ''}`}
|
|
||||||
>
|
|
||||||
<option value="" disabled hidden>{field.label}</option>
|
|
||||||
{field.filterOptions.map(option => (
|
|
||||||
<option key={option} value={option}>{option}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{filters[field.key] && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => onFilterChange(field.key, '')}
|
|
||||||
className={styles.clearFilterButton}
|
|
||||||
title={t('formgen.filter.clear')}
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : field.type === 'date' ? (
|
|
||||||
<div className={styles.floatingLabelInput}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder=" "
|
|
||||||
value={filters[field.key] || ''}
|
|
||||||
onChange={(e) => handleDateFilterInput(field.key, e)}
|
|
||||||
onFocus={() => onFilterFocus(field.key, true)}
|
|
||||||
onBlur={() => onFilterFocus(field.key, false)}
|
|
||||||
className={`${styles.filterInput} ${filterFocused[field.key] || filters[field.key] ? styles.focused : ''}`}
|
|
||||||
maxLength={10}
|
|
||||||
/>
|
|
||||||
<label className={filterFocused[field.key] || filters[field.key] ? styles.focusedLabel : styles.label}>
|
|
||||||
{field.label}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={styles.floatingLabelInput}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder=" "
|
|
||||||
value={filters[field.key] || ''}
|
|
||||||
onChange={(e) => onFilterChange(field.key, e.target.value)}
|
|
||||||
onFocus={() => onFilterFocus(field.key, true)}
|
|
||||||
onBlur={() => onFilterFocus(field.key, false)}
|
|
||||||
className={`${styles.filterInput} ${filterFocused[field.key] || filters[field.key] ? styles.focused : ''}`}
|
|
||||||
/>
|
|
||||||
<label className={filterFocused[field.key] || filters[field.key] ? styles.focusedLabel : styles.label}>
|
|
||||||
{t('formgen.filter.placeholder').replace('{column}', field.label)}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
isTextareaType,
|
isTextareaType,
|
||||||
isSelectType,
|
isSelectType,
|
||||||
isMultiselectType,
|
isMultiselectType,
|
||||||
|
isMultilingualType,
|
||||||
isCheckboxType,
|
isCheckboxType,
|
||||||
isFileType,
|
isFileType,
|
||||||
isNumberType,
|
isNumberType,
|
||||||
|
|
@ -34,11 +35,11 @@ const isMultilingualFieldName = (fieldName: string): boolean => {
|
||||||
const exactMultilingualFields = ['description'];
|
const exactMultilingualFields = ['description'];
|
||||||
|
|
||||||
// Fields that end with these patterns (but not roleLabel, etc.)
|
// Fields that end with these patterns (but not roleLabel, etc.)
|
||||||
|
// Note: "name" is NOT multilingual - Mandate.name and other name fields are plain strings
|
||||||
const multilingualPatterns = [
|
const multilingualPatterns = [
|
||||||
/^description$/i,
|
/^description$/i,
|
||||||
/^label$/i, // Only exact "label", not "roleLabel"
|
/^label$/i, // Only exact "label", not "roleLabel"
|
||||||
/^title$/i, // Only exact "title"
|
/^title$/i // Only exact "title"
|
||||||
/^name$/i // Only exact "name", not field names containing "name"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Check exact matches first
|
// Check exact matches first
|
||||||
|
|
@ -220,12 +221,16 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
|
|
||||||
// Initialize form data with defaults
|
// Initialize form data with defaults
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Helper to check if a field should be treated as multilingual
|
||||||
|
const isMultilingual = (attr: AttributeDefinition) =>
|
||||||
|
isMultilingualType(attr.type as AttributeType) || isMultilingualFieldName(attr.name);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
// Ensure TextMultilingual fields are properly initialized
|
// Ensure TextMultilingual fields are properly initialized
|
||||||
const processedData: any = { ...data };
|
const processedData: any = { ...data };
|
||||||
const filteredAttrs = getFilteredAttributes();
|
const filteredAttrs = getFilteredAttributes();
|
||||||
filteredAttrs.forEach(attr => {
|
filteredAttrs.forEach(attr => {
|
||||||
if (isMultilingualFieldName(attr.name) && processedData[attr.name]) {
|
if (isMultilingual(attr) && processedData[attr.name]) {
|
||||||
// If it's already a TextMultilingual object, keep it
|
// If it's already a TextMultilingual object, keep it
|
||||||
if (!isTextMultilingual(processedData[attr.name])) {
|
if (!isTextMultilingual(processedData[attr.name])) {
|
||||||
// If it's a string, convert to TextMultilingual
|
// If it's a string, convert to TextMultilingual
|
||||||
|
|
@ -242,7 +247,7 @@ export function FormGeneratorForm<T extends Record<string, 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)) {
|
} else if (isMultilingual(attr)) {
|
||||||
// Initialize TextMultilingual fields with empty object
|
// Initialize TextMultilingual fields with empty object
|
||||||
initialData[attr.name] = { en: '' };
|
initialData[attr.name] = { en: '' };
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -380,8 +385,9 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
|
|
||||||
// Check required fields
|
// Check required fields
|
||||||
if (attr.required) {
|
if (attr.required) {
|
||||||
// Special handling for TextMultilingual fields
|
// Special handling for TextMultilingual fields (by type or field name)
|
||||||
if (isMultilingualFieldName(attr.name) && isTextMultilingual(value)) {
|
const isMultilingual = isMultilingualType(attr.type as AttributeType) || isMultilingualFieldName(attr.name);
|
||||||
|
if (isMultilingual && isTextMultilingual(value)) {
|
||||||
if (!value.en || typeof value.en !== 'string' || value.en.trim() === '') {
|
if (!value.en || typeof value.en !== 'string' || value.en.trim() === '') {
|
||||||
newErrors[attr.name] = t('formgen.form.required', `${attr.label} (English) is required`);
|
newErrors[attr.name] = t('formgen.form.required', `${attr.label} (English) is required`);
|
||||||
return;
|
return;
|
||||||
|
|
@ -631,8 +637,9 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
||||||
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
|
// Check if this is a multilingual field - either by type or by field name convention
|
||||||
if (isMultilingualFieldName(attr.name) && (isTextMultilingual(value) || value === undefined || value === null || value === '')) {
|
if ((isMultilingualType(attr.type as AttributeType) || isMultilingualFieldName(attr.name)) &&
|
||||||
|
(isTextMultilingual(value) || value === undefined || value === null || value === '')) {
|
||||||
return renderMultilingualField(attr);
|
return renderMultilingualField(attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,11 @@ import actionButtonStyles from '../ActionButtons/ActionButton.module.css';
|
||||||
import {
|
import {
|
||||||
EditActionButton,
|
EditActionButton,
|
||||||
DeleteActionButton,
|
DeleteActionButton,
|
||||||
DownloadActionButton,
|
|
||||||
ViewActionButton,
|
ViewActionButton,
|
||||||
CopyActionButton,
|
CopyActionButton,
|
||||||
ConnectActionButton,
|
CustomActionButton
|
||||||
PlayActionButton
|
|
||||||
} from '../ActionButtons';
|
} from '../ActionButtons';
|
||||||
|
import { FaDownload, FaLink, FaPlay } from 'react-icons/fa';
|
||||||
import { formatUnixTimestamp } from '../../../utils/time';
|
import { formatUnixTimestamp } from '../../../utils/time';
|
||||||
import TextField from '../../UiComponents/TextField/TextField';
|
import TextField from '../../UiComponents/TextField/TextField';
|
||||||
import { FormGeneratorControls } from '../FormGeneratorControls';
|
import { FormGeneratorControls } from '../FormGeneratorControls';
|
||||||
|
|
@ -884,22 +883,47 @@ export function FormGeneratorList<T extends Record<string, any>>({
|
||||||
case 'delete':
|
case 'delete':
|
||||||
return <DeleteActionButton key={actionIndex} {...baseProps} containerRef={{ current: actionButtonsRefs.current.get(index) || null }} hookData={hookData} />;
|
return <DeleteActionButton key={actionIndex} {...baseProps} containerRef={{ current: actionButtonsRefs.current.get(index) || null }} hookData={hookData} />;
|
||||||
case 'download':
|
case 'download':
|
||||||
return <DownloadActionButton key={actionIndex} {...baseProps} onDownload={actionButton.onAction || (() => {})} isDownloading={isProcessing} hookData={hookData} operationName={actionButton.operationName} />;
|
return <CustomActionButton
|
||||||
|
key={actionIndex}
|
||||||
|
row={row}
|
||||||
|
id="download"
|
||||||
|
icon={<FaDownload />}
|
||||||
|
onClick={actionButton.onAction || (() => {})}
|
||||||
|
disabled={() => disabledResult}
|
||||||
|
loading={() => isProcessing}
|
||||||
|
title={actionTitle}
|
||||||
|
className={actionButton.className}
|
||||||
|
hookData={hookData}
|
||||||
|
/>;
|
||||||
case 'view':
|
case 'view':
|
||||||
return <ViewActionButton key={actionIndex} {...baseProps} onView={actionButton.onAction || (() => {})} isViewing={isProcessing} hookData={hookData} />;
|
return <ViewActionButton key={actionIndex} {...baseProps} onView={actionButton.onAction || (() => {})} isViewing={isProcessing} hookData={hookData} />;
|
||||||
case 'copy':
|
case 'copy':
|
||||||
return <CopyActionButton key={actionIndex} {...baseProps} onCopy={actionButton.onAction} isCopying={isProcessing} contentField={actionButton.contentField} />;
|
return <CopyActionButton key={actionIndex} {...baseProps} onCopy={actionButton.onAction} isCopying={isProcessing} contentField={actionButton.contentField} />;
|
||||||
case 'connect':
|
case 'connect':
|
||||||
return <ConnectActionButton key={actionIndex} {...baseProps} hookData={hookData} />;
|
return <CustomActionButton
|
||||||
case 'play':
|
|
||||||
return <PlayActionButton
|
|
||||||
key={actionIndex}
|
key={actionIndex}
|
||||||
{...baseProps}
|
row={row}
|
||||||
onPlay={actionButton.onAction}
|
id="connect"
|
||||||
hookData={hookData}
|
icon={<FaLink />}
|
||||||
navigateTo={actionButton.navigateTo}
|
onClick={actionButton.onAction || (() => {})}
|
||||||
contentField={actionButton.contentField}
|
disabled={() => disabledResult}
|
||||||
mode={(actionButton as any).mode || 'prompt'}
|
loading={() => isLoading}
|
||||||
|
title={actionTitle}
|
||||||
|
className={actionButton.className}
|
||||||
|
hookData={hookData}
|
||||||
|
/>;
|
||||||
|
case 'play':
|
||||||
|
return <CustomActionButton
|
||||||
|
key={actionIndex}
|
||||||
|
row={row}
|
||||||
|
id="play"
|
||||||
|
icon={<FaPlay />}
|
||||||
|
onClick={actionButton.onAction || (() => {})}
|
||||||
|
disabled={() => disabledResult}
|
||||||
|
loading={() => isLoading}
|
||||||
|
title={actionTitle}
|
||||||
|
className={actionButton.className}
|
||||||
|
hookData={hookData}
|
||||||
/>;
|
/>;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
.formGeneratorTable {
|
.formGeneratorTable {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
/* Ensure proper height constraints for scrolling */
|
/* Ensure proper height constraints for scrolling */
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
max-height: 100%;
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|
@ -24,10 +25,11 @@
|
||||||
border: 1px solid var(--color-primary);
|
border: 1px solid var(--color-primary);
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
/* Use calc to account for controls, pagination, and spacing */
|
/* Fill available space in flex container */
|
||||||
max-height: calc(100vh - 400px);
|
flex: 1;
|
||||||
/* No min-height - let it shrink to fit content */
|
min-height: 0;
|
||||||
/* When empty, it will only show header */
|
/* Ensure scrolling within container */
|
||||||
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Empty table styling - no extra space, just header */
|
/* Empty table styling - no extra space, just header */
|
||||||
|
|
@ -104,7 +106,6 @@
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,27 +126,159 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: left;
|
justify-content: left;
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columnLabel {
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sortIcon {
|
.sortIcon {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
color: var(--color-text-secondary, #999);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortIcon:hover {
|
||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
opacity: 1.;
|
}
|
||||||
|
|
||||||
|
.sortIcon.sortActive {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortIcon sub {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter icon in column header */
|
||||||
|
.filterIcon {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-text-secondary, #999);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterIcon:hover {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
background: rgba(var(--color-secondary-rgb), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterIcon.filterActive {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
background: rgba(var(--color-secondary-rgb), 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter dropdown */
|
||||||
|
.filterDropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
min-width: 180px;
|
||||||
|
max-width: 300px;
|
||||||
|
background: var(--color-bg);
|
||||||
|
border: 1px solid var(--color-border, #ddd);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 100;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterDropdownHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--color-border, #ddd);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterClearBtn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterClearBtn:hover {
|
||||||
|
background: rgba(255, 0, 0, 0.1);
|
||||||
|
color: #c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterDropdownOptions {
|
||||||
|
max-height: 250px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterOption {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterOption:hover {
|
||||||
|
background: var(--color-gray-disabled, #f5f5f5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterOptionSelected {
|
||||||
|
background: rgba(var(--color-secondary-rgb), 0.15);
|
||||||
|
color: var(--color-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterOptionSelected:hover {
|
||||||
|
background: rgba(var(--color-secondary-rgb), 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterOptionMore {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resizeHandle {
|
.resizeHandle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: -3px;
|
||||||
width: 4px;
|
width: 8px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
z-index: 11;
|
z-index: 20;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resizeHandle:hover {
|
.resizeHandle:hover {
|
||||||
background: var(--color-secondary);
|
background: var(--color-secondary);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizeHandle:active {
|
||||||
|
background: var(--color-secondary);
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.td {
|
.td {
|
||||||
|
|
@ -312,15 +445,14 @@ tbody .actionsColumn {
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 15px;
|
padding: 8px 0;
|
||||||
border-top: 1px solid var(--color-primary);
|
|
||||||
/* Ensure pagination stays visible and doesn't get cut off */
|
/* Ensure pagination stays visible and doesn't get cut off */
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
border-radius: 0 0 8px 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pageSizeSelector {
|
.pageSizeSelector {
|
||||||
|
|
@ -388,11 +520,90 @@ tbody .actionsColumn {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Page numbers container */
|
||||||
|
.pageNumbers {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
max-width: 60vw;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Individual page number button */
|
||||||
|
.pageNumber {
|
||||||
|
min-width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border: 1px solid var(--color-border, #ddd);
|
||||||
|
background: var(--color-bg, #fff);
|
||||||
|
color: var(--color-text);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageNumber:hover:not(:disabled) {
|
||||||
|
background: var(--color-secondary);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageNumber:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active/current page number */
|
||||||
|
.pageNumberActive {
|
||||||
|
background: var(--color-secondary);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--color-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ellipsis indicator */
|
||||||
|
.pageEllipsis {
|
||||||
|
padding: 0 8px;
|
||||||
|
color: var(--color-text-secondary, #666);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading overlay */
|
||||||
|
.loadingOverlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingOverlay p {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: var(--color-text-secondary, #666);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive Design */
|
/* Responsive Design */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.tableContainer {
|
.tableContainer {
|
||||||
max-height: calc(100vh - 350px);
|
flex: 1;
|
||||||
/* No min-height on mobile - let it shrink to fit content */
|
min-height: 0;
|
||||||
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Empty table styling - no extra space */
|
/* Empty table styling - no extra space */
|
||||||
|
|
@ -433,6 +644,17 @@ tbody .actionsColumn {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pageNumbers {
|
||||||
|
max-width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageNumber {
|
||||||
|
min-width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme support */
|
/* Dark theme support */
|
||||||
|
|
@ -448,6 +670,15 @@ tbody .actionsColumn {
|
||||||
.tr.selected {
|
.tr.selected {
|
||||||
background: rgba(var(--color-secondary-rgb), 0.2);
|
background: rgba(var(--color-secondary-rgb), 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loadingOverlay {
|
||||||
|
background: rgba(30, 30, 30, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageNumber {
|
||||||
|
background: var(--color-bg, #2d2d2d);
|
||||||
|
border-color: var(--color-border, #444);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Accessibility */
|
/* Accessibility */
|
||||||
|
|
@ -502,3 +733,83 @@ tbody .actionsColumn {
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Inline Editable Boolean Cells */
|
||||||
|
.booleanCell {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booleanEditable {
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid var(--color-border, #dee2e6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booleanEditable:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booleanEditable:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booleanEditable.booleanTrue {
|
||||||
|
color: var(--color-success, #28a745);
|
||||||
|
border-color: var(--color-success, #28a745);
|
||||||
|
background: rgba(40, 167, 69, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booleanEditable.booleanTrue:hover {
|
||||||
|
background: rgba(40, 167, 69, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booleanEditable.booleanFalse {
|
||||||
|
color: var(--color-text-secondary, #6c757d);
|
||||||
|
border-color: var(--color-border, #dee2e6);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booleanEditable.booleanFalse:hover {
|
||||||
|
color: var(--color-danger, #dc3545);
|
||||||
|
border-color: var(--color-danger, #dc3545);
|
||||||
|
background: rgba(220, 53, 69, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booleanReadonly {
|
||||||
|
cursor: default;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booleanReadonly.booleanTrue {
|
||||||
|
color: var(--color-success, #28a745);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booleanReadonly.booleanFalse {
|
||||||
|
color: var(--color-text-secondary, #adb5bd);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booleanLoading {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
animation: booleanPulse 1s ease-in-out infinite;
|
||||||
|
color: var(--color-primary, #007bff);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes booleanPulse {
|
||||||
|
0%, 100% { opacity: 0.4; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -221,4 +221,43 @@
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
visibility: visible !important;
|
visibility: visible !important;
|
||||||
color: #181818 !important;
|
color: #181818 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active state for submenu items */
|
||||||
|
.submenuList li.active {
|
||||||
|
background-color: var(--color-secondary);
|
||||||
|
border-radius: 0 25px 25px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenuList li.active a,
|
||||||
|
.submenuList li.active a span,
|
||||||
|
.submenuList li a.activeLink {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenuList li.active .submenuIcon {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active state for horizontal (minimized) submenu items */
|
||||||
|
.submenuHorizontalItem.active .submenuHorizontalLink,
|
||||||
|
.submenuHorizontalLink.activeLink {
|
||||||
|
background-color: var(--color-secondary);
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenuHorizontalItem.active .submenuHorizontalIcon,
|
||||||
|
.submenuHorizontalLink.activeLink .submenuHorizontalIcon {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenuHorizontalItem.active .submenuHorizontalIcon svg,
|
||||||
|
.submenuHorizontalLink.activeLink .submenuHorizontalIcon svg {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenuHorizontalItem.active .submenuHorizontalIcon svg path,
|
||||||
|
.submenuHorizontalLink.activeLink .submenuHorizontalIcon svg path {
|
||||||
|
fill: white !important;
|
||||||
|
stroke: white !important;
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,95 @@
|
||||||
import styles from './SidebarStyles/SidebarSubmenu.module.css';
|
import styles from './SidebarStyles/SidebarSubmenu.module.css';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { useRef, useEffect, useState } from 'react';
|
import React, { useRef, useEffect, useState } from 'react';
|
||||||
import { SidebarSubmenuProps } from './sidebarTypes';
|
import { SidebarSubmenuProps, SidebarSubmenuItemData } from './sidebarTypes';
|
||||||
|
|
||||||
|
// Separate component for submenu item to properly use hooks
|
||||||
|
interface SubmenuItemProps {
|
||||||
|
subitem: SidebarSubmenuItemData;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SubmenuItem: React.FC<SubmenuItemProps> = ({ subitem, isActive }) => {
|
||||||
|
const textRef = useRef<HTMLSpanElement>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isOverflowing, setIsOverflowing] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkOverflow = () => {
|
||||||
|
if (textRef.current && containerRef.current) {
|
||||||
|
const textWidth = textRef.current.scrollWidth;
|
||||||
|
const containerWidth = containerRef.current.clientWidth;
|
||||||
|
setIsOverflowing(textWidth > containerWidth);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkOverflow();
|
||||||
|
// Also check on window resize
|
||||||
|
window.addEventListener('resize', checkOverflow);
|
||||||
|
return () => window.removeEventListener('resize', checkOverflow);
|
||||||
|
}, [subitem.name]);
|
||||||
|
|
||||||
|
const SubIcon = subitem.icon as React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={isActive ? styles.active : ''}>
|
||||||
|
<Link
|
||||||
|
to={subitem.link || '#'}
|
||||||
|
title={subitem.name}
|
||||||
|
className={isActive ? styles.activeLink : ''}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={styles.textContainer}
|
||||||
|
>
|
||||||
|
<motion.span
|
||||||
|
ref={textRef}
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
initial={{ x: 0 }}
|
||||||
|
animate={{ x: 0 }}
|
||||||
|
{...(isOverflowing && {
|
||||||
|
whileHover: {
|
||||||
|
x: -(textRef.current?.scrollWidth || 0) + (containerRef.current?.clientWidth || 153),
|
||||||
|
transition: {
|
||||||
|
duration: 2,
|
||||||
|
ease: "linear"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', paddingRight: '10px' }}>
|
||||||
|
{SubIcon && <SubIcon className={styles.submenuIcon} />}
|
||||||
|
<span style={{ marginLeft: SubIcon ? '8px' : '0' }}>
|
||||||
|
{subitem.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</motion.span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen, isMinimized = false }) => {
|
const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen, isMinimized = false }) => {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// Check if a submenu item is active
|
||||||
|
const isSubmenuItemActive = (itemPath?: string) => {
|
||||||
|
if (!itemPath) return false;
|
||||||
|
const currentPath = location.pathname;
|
||||||
|
// Exact match or prefix match at path segment boundary
|
||||||
|
if (currentPath === itemPath) return true;
|
||||||
|
if (currentPath.startsWith(itemPath)) {
|
||||||
|
const nextChar = currentPath[itemPath.length];
|
||||||
|
if (nextChar === '/' || nextChar === undefined) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
if (!item.submenu) return null;
|
if (!item.submenu) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -42,13 +127,14 @@ const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen, isMinimiz
|
||||||
<ul className={styles.submenuHorizontalList}>
|
<ul className={styles.submenuHorizontalList}>
|
||||||
{item.submenu.map(subitem => {
|
{item.submenu.map(subitem => {
|
||||||
const SubIcon = subitem.icon as React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
const SubIcon = subitem.icon as React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||||||
|
const isActive = isSubmenuItemActive(subitem.link);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={subitem.id} className={styles.submenuHorizontalItem}>
|
<li key={subitem.id} className={`${styles.submenuHorizontalItem} ${isActive ? styles.active : ''}`}>
|
||||||
<Link
|
<Link
|
||||||
to={subitem.link || '#'}
|
to={subitem.link || '#'}
|
||||||
title={subitem.name}
|
title={subitem.name}
|
||||||
className={styles.submenuHorizontalLink}
|
className={`${styles.submenuHorizontalLink} ${isActive ? styles.activeLink : ''}`}
|
||||||
>
|
>
|
||||||
{SubIcon && (
|
{SubIcon && (
|
||||||
<SubIcon
|
<SubIcon
|
||||||
|
|
@ -56,7 +142,7 @@ const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen, isMinimiz
|
||||||
style={{
|
style={{
|
||||||
width: '16px',
|
width: '16px',
|
||||||
height: '16px',
|
height: '16px',
|
||||||
color: '#181818',
|
color: isActive ? 'white' : '#181818',
|
||||||
display: 'block'
|
display: 'block'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -77,68 +163,13 @@ const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen, isMinimiz
|
||||||
exit={{ opacity: 0, transition: { duration: 0.25, delay: 0 } }}
|
exit={{ opacity: 0, transition: { duration: 0.25, delay: 0 } }}
|
||||||
>
|
>
|
||||||
<ul className={styles.submenuList}>
|
<ul className={styles.submenuList}>
|
||||||
{item.submenu.map(subitem => {
|
{item.submenu.map(subitem => (
|
||||||
const textRef = useRef<HTMLSpanElement>(null);
|
<SubmenuItem
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
key={subitem.id}
|
||||||
const [isOverflowing, setIsOverflowing] = useState(false);
|
subitem={subitem}
|
||||||
|
isActive={isSubmenuItemActive(subitem.link)}
|
||||||
useEffect(() => {
|
/>
|
||||||
const checkOverflow = () => {
|
))}
|
||||||
if (textRef.current && containerRef.current) {
|
|
||||||
const textWidth = textRef.current.scrollWidth;
|
|
||||||
const containerWidth = containerRef.current.clientWidth;
|
|
||||||
setIsOverflowing(textWidth > containerWidth);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkOverflow();
|
|
||||||
// Also check on window resize
|
|
||||||
window.addEventListener('resize', checkOverflow);
|
|
||||||
return () => window.removeEventListener('resize', checkOverflow);
|
|
||||||
}, [subitem.name]);
|
|
||||||
|
|
||||||
const SubIcon = subitem.icon as React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={subitem.id}>
|
|
||||||
<Link
|
|
||||||
to={subitem.link || '#'}
|
|
||||||
title={subitem.name}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
className={styles.textContainer}
|
|
||||||
>
|
|
||||||
<motion.span
|
|
||||||
ref={textRef}
|
|
||||||
style={{
|
|
||||||
display: 'block',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
initial={{ x: 0 }}
|
|
||||||
animate={{ x: 0 }}
|
|
||||||
{...(isOverflowing && {
|
|
||||||
whileHover: {
|
|
||||||
x: -(textRef.current?.scrollWidth || 0) + (containerRef.current?.clientWidth || 153),
|
|
||||||
transition: {
|
|
||||||
duration: 2,
|
|
||||||
ease: "linear"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', paddingRight: '10px' }}>
|
|
||||||
{SubIcon && <SubIcon className={styles.submenuIcon} />}
|
|
||||||
<span style={{ marginLeft: SubIcon ? '8px' : '0' }}>
|
|
||||||
{subitem.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</motion.span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
</ul>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,27 @@ export const useSidebarLogic = (): SidebarContextType => {
|
||||||
}, [state.openItemId]);
|
}, [state.openItemId]);
|
||||||
|
|
||||||
// Check if an item is the active route
|
// Check if an item is the active route
|
||||||
|
// Supports exact match and prefix match (for parent items when child route is active)
|
||||||
const isItemActive = useCallback((itemPath?: string) => {
|
const isItemActive = useCallback((itemPath?: string) => {
|
||||||
if (!itemPath) return false;
|
if (!itemPath) return false;
|
||||||
return location.pathname === itemPath;
|
|
||||||
|
const currentPath = location.pathname;
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (currentPath === itemPath) return true;
|
||||||
|
|
||||||
|
// Prefix match: check if current path starts with the item path
|
||||||
|
// This highlights parent items when a child/subpage is active
|
||||||
|
// Ensure we match at path segment boundaries (e.g., /admin matches /admin/users but not /administrator)
|
||||||
|
if (currentPath.startsWith(itemPath)) {
|
||||||
|
// Check if the next character is either '/' or end of string
|
||||||
|
const nextChar = currentPath[itemPath.length];
|
||||||
|
if (nextChar === '/' || nextChar === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
// Minimize sidebar
|
// Minimize sidebar
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,10 @@ import {
|
||||||
DeleteActionButton,
|
DeleteActionButton,
|
||||||
RemoveActionButton,
|
RemoveActionButton,
|
||||||
EditActionButton,
|
EditActionButton,
|
||||||
DownloadActionButton,
|
|
||||||
CopyActionButton,
|
CopyActionButton,
|
||||||
ConnectActionButton,
|
CustomActionButton
|
||||||
PlayActionButton
|
|
||||||
} from '../../FormGenerator/ActionButtons';
|
} from '../../FormGenerator/ActionButtons';
|
||||||
|
import { FaDownload, FaLink, FaPlay } from 'react-icons/fa';
|
||||||
import { WorkflowFile } from '../../../hooks/usePlayground';
|
import { WorkflowFile } from '../../../hooks/usePlayground';
|
||||||
import styles from './ConnectedFilesList.module.css';
|
import styles from './ConnectedFilesList.module.css';
|
||||||
|
|
||||||
|
|
@ -303,12 +302,16 @@ export function ConnectedFilesList({
|
||||||
{...baseProps}
|
{...baseProps}
|
||||||
/>;
|
/>;
|
||||||
case 'download':
|
case 'download':
|
||||||
return <DownloadActionButton
|
return <CustomActionButton
|
||||||
key={actionIndex}
|
key={actionIndex}
|
||||||
{...baseProps}
|
row={file}
|
||||||
onDownload={actionButton.onAction || (() => {})}
|
id="download"
|
||||||
isDownloading={isProcessing}
|
icon={<FaDownload />}
|
||||||
operationName={actionButton.operationName}
|
onClick={actionButton.onAction || (() => {})}
|
||||||
|
disabled={() => disabledResult}
|
||||||
|
loading={() => isProcessing}
|
||||||
|
title={actionTitle}
|
||||||
|
className={actionButton.className}
|
||||||
/>;
|
/>;
|
||||||
case 'view':
|
case 'view':
|
||||||
return <ViewActionButton
|
return <ViewActionButton
|
||||||
|
|
@ -326,18 +329,28 @@ export function ConnectedFilesList({
|
||||||
contentField={actionButton.contentField}
|
contentField={actionButton.contentField}
|
||||||
/>;
|
/>;
|
||||||
case 'connect':
|
case 'connect':
|
||||||
return <ConnectActionButton
|
return <CustomActionButton
|
||||||
key={actionIndex}
|
key={actionIndex}
|
||||||
{...baseProps}
|
row={file}
|
||||||
|
id="connect"
|
||||||
|
icon={<FaLink />}
|
||||||
|
onClick={actionButton.onAction || (() => {})}
|
||||||
|
disabled={() => disabledResult}
|
||||||
|
loading={() => isLoading}
|
||||||
|
title={actionTitle}
|
||||||
|
className={actionButton.className}
|
||||||
/>;
|
/>;
|
||||||
case 'play':
|
case 'play':
|
||||||
return <PlayActionButton
|
return <CustomActionButton
|
||||||
key={actionIndex}
|
key={actionIndex}
|
||||||
{...baseProps}
|
row={file}
|
||||||
onPlay={actionButton.onAction}
|
id="play"
|
||||||
navigateTo={actionButton.navigateTo}
|
icon={<FaPlay />}
|
||||||
contentField={actionButton.contentField}
|
onClick={actionButton.onAction || (() => {})}
|
||||||
mode={(actionButton as any).mode || 'prompt'}
|
disabled={() => disabledResult}
|
||||||
|
loading={() => isLoading}
|
||||||
|
title={actionTitle}
|
||||||
|
className={actionButton.className}
|
||||||
/>;
|
/>;
|
||||||
case 'remove':
|
case 'remove':
|
||||||
return <RemoveActionButton
|
return <RemoveActionButton
|
||||||
|
|
|
||||||
|
|
@ -924,7 +924,7 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
|
|
||||||
if (content.tableConfig && currentTableHookData) {
|
if (content.tableConfig && currentTableHookData) {
|
||||||
|
|
||||||
const { columns: configColumns, actionButtons, emptyMessage, ...tableProps } = content.tableConfig;
|
const { columns: configColumns, actionButtons, customActions, emptyMessage, ...tableProps } = content.tableConfig;
|
||||||
|
|
||||||
// Only show loading spinner on initial load (when there's no data yet)
|
// Only show loading spinner on initial load (when there's no data yet)
|
||||||
// During refetch, keep the existing data visible
|
// During refetch, keep the existing data visible
|
||||||
|
|
@ -972,8 +972,9 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine which permission to check based on button type
|
// Determine which permission to check based on button type
|
||||||
|
// Only standard action types: edit, delete, view, copy
|
||||||
let requiredPermission: 'read' | 'create' | 'update' | 'delete' | null = null;
|
let requiredPermission: 'read' | 'create' | 'update' | 'delete' | null = null;
|
||||||
if (action.type === 'view' || action.type === 'play') {
|
if (action.type === 'view') {
|
||||||
requiredPermission = 'read';
|
requiredPermission = 'read';
|
||||||
} else if (action.type === 'edit') {
|
} else if (action.type === 'edit') {
|
||||||
requiredPermission = 'update';
|
requiredPermission = 'update';
|
||||||
|
|
@ -1055,9 +1056,7 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
contentField: action.contentField,
|
contentField: action.contentField,
|
||||||
operationName: action.operationName,
|
operationName: action.operationName,
|
||||||
loadingStateName: action.loadingStateName,
|
loadingStateName: action.loadingStateName,
|
||||||
// Navigation and behavior (for play button)
|
fetchItemFunctionName: action.fetchItemFunctionName
|
||||||
navigateTo: action.navigateTo,
|
|
||||||
mode: action.mode
|
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
|
|
@ -1085,6 +1084,21 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
columns={resolvedColumns}
|
columns={resolvedColumns}
|
||||||
loading={showLoadingSpinner}
|
loading={showLoadingSpinner}
|
||||||
actionButtons={formGeneratorActions}
|
actionButtons={formGeneratorActions}
|
||||||
|
customActions={customActions?.map(action => {
|
||||||
|
// Resolve LanguageText in title to string
|
||||||
|
let resolvedTitle: string | ((row: any) => string) | undefined = undefined;
|
||||||
|
if (typeof action.title === 'function') {
|
||||||
|
resolvedTitle = action.title;
|
||||||
|
} else if (typeof action.title === 'string') {
|
||||||
|
resolvedTitle = resolveLanguageText(action.title, t);
|
||||||
|
} else if (action.title && typeof action.title === 'object') {
|
||||||
|
resolvedTitle = resolveLanguageText(action.title as any, t);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...action,
|
||||||
|
title: resolvedTitle
|
||||||
|
};
|
||||||
|
})}
|
||||||
hookData={currentTableHookData}
|
hookData={currentTableHookData}
|
||||||
onDelete={currentTableHookData.onDelete}
|
onDelete={currentTableHookData.onDelete}
|
||||||
onDeleteMultiple={currentTableHookData.onDeleteMultiple}
|
onDeleteMultiple={currentTableHookData.onDeleteMultiple}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ import { allPageData, SidebarItem } from './data';
|
||||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||||
import { resolveLanguageText } from './pageInterface';
|
import { resolveLanguageText } from './pageInterface';
|
||||||
import { usePermissions } from '../../hooks/usePermissions';
|
import { usePermissions } from '../../hooks/usePermissions';
|
||||||
import { getUserDataCache } from '../../utils/userCache';
|
import { FaHome, FaHatWizard, FaBriefcase } from 'react-icons/fa';
|
||||||
import { FaHome, FaHatWizard } from 'react-icons/fa';
|
|
||||||
import { RiFolderSettingsFill } from 'react-icons/ri';
|
import { RiFolderSettingsFill } from 'react-icons/ri';
|
||||||
|
|
||||||
// Configuration for parent groups that don't have a page definition
|
// Configuration for parent groups that don't have a page definition
|
||||||
|
|
@ -17,13 +16,17 @@ const parentGroupConfig: Record<string, {
|
||||||
icon: FaHome,
|
icon: FaHome,
|
||||||
defaultOrder: 1
|
defaultOrder: 1
|
||||||
},
|
},
|
||||||
|
'trustee': {
|
||||||
|
icon: FaBriefcase,
|
||||||
|
defaultOrder: 2
|
||||||
|
},
|
||||||
'administration': {
|
'administration': {
|
||||||
icon: RiFolderSettingsFill,
|
icon: RiFolderSettingsFill,
|
||||||
defaultOrder: 2
|
defaultOrder: 3
|
||||||
},
|
},
|
||||||
'admin': {
|
'admin': {
|
||||||
icon: FaHatWizard,
|
icon: FaHatWizard,
|
||||||
defaultOrder: 3
|
defaultOrder: 4
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -55,7 +58,7 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
|
||||||
|
|
||||||
// Get translation function from language context
|
// Get translation function from language context
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const { canView } = usePermissions();
|
const { canView, preloadUiPermissions } = usePermissions();
|
||||||
|
|
||||||
// Get sidebar items from page data
|
// Get sidebar items from page data
|
||||||
const getSidebarItems = async (): Promise<SidebarItem[]> => {
|
const getSidebarItems = async (): Promise<SidebarItem[]> => {
|
||||||
|
|
@ -181,34 +184,13 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
|
||||||
.filter(page => !page.parentPath && !page.hide && page.showInSidebar !== false)
|
.filter(page => !page.parentPath && !page.hide && page.showInSidebar !== false)
|
||||||
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||||
|
|
||||||
// Log user info for debugging
|
// Process each main page (permissions already bulk-loaded)
|
||||||
const cachedUser = getUserDataCache();
|
|
||||||
console.log('👤 SidebarProvider: Current user info:', {
|
|
||||||
username: cachedUser?.username,
|
|
||||||
roleLabels: cachedUser?.roleLabels,
|
|
||||||
roleLabelsLength: Array.isArray(cachedUser?.roleLabels) ? cachedUser.roleLabels.length : 0
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process each main page
|
|
||||||
console.log('📋 SidebarProvider: Processing pages, total:', mainPages.length, 'pages to check');
|
|
||||||
const pageAccessResults: Array<{ path: string; name: string; hasAccess: boolean }> = [];
|
|
||||||
for (const pageData of mainPages) {
|
for (const pageData of mainPages) {
|
||||||
console.log('🔍 SidebarProvider: Checking access for page:', {
|
// Check RBAC permissions (from cache - no API call)
|
||||||
path: pageData.path,
|
|
||||||
name: pageData.name,
|
|
||||||
hasSubpages: pageData.hasSubpages
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check RBAC permissions
|
|
||||||
try {
|
try {
|
||||||
const hasRBACAccess = await canView('UI', pageData.path);
|
const hasRBACAccess = await canView('UI', pageData.path);
|
||||||
console.log('🔍 SidebarProvider: RBAC check result:', {
|
|
||||||
path: pageData.path,
|
|
||||||
hasAccess: hasRBACAccess
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasRBACAccess) {
|
if (!hasRBACAccess) {
|
||||||
console.log('⛔ SidebarProvider: Page hidden due to RBAC:', pageData.path);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,16 +199,15 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
|
||||||
try {
|
try {
|
||||||
const hasPrivilege = await pageData.privilegeChecker();
|
const hasPrivilege = await pageData.privilegeChecker();
|
||||||
if (!hasPrivilege) {
|
if (!hasPrivilege) {
|
||||||
console.log('⛔ SidebarProvider: Page hidden due to privilegeChecker:', pageData.path);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ SidebarProvider: Error checking privilegeChecker for ${pageData.path}:`, error);
|
console.error(`Error checking privilegeChecker for ${pageData.path}:`, error);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ SidebarProvider: Error checking RBAC access for ${pageData.path}:`, error);
|
console.error(`Error checking RBAC access for ${pageData.path}:`, error);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -349,30 +330,6 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
|
||||||
// Sort all items by order
|
// Sort all items by order
|
||||||
const sortedItems = items.sort((a, b) => (a.order || 0) - (b.order || 0));
|
const sortedItems = items.sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||||
|
|
||||||
// Summary of page access checks
|
|
||||||
const accessiblePages = pageAccessResults.filter(r => r.hasAccess);
|
|
||||||
const deniedPages = pageAccessResults.filter(r => !r.hasAccess);
|
|
||||||
|
|
||||||
console.log('📊 SidebarProvider: Page access summary:', {
|
|
||||||
totalPagesChecked: pageAccessResults.length,
|
|
||||||
accessiblePages: accessiblePages.length,
|
|
||||||
deniedPages: deniedPages.length,
|
|
||||||
accessiblePagePaths: accessiblePages.map(p => p.path),
|
|
||||||
deniedPagePaths: deniedPages.map(p => p.path),
|
|
||||||
deniedPageDetails: deniedPages.map(p => ({ path: p.path, name: p.name }))
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('📊 SidebarProvider: Final sidebar items built and sorted:', {
|
|
||||||
totalItems: sortedItems.length,
|
|
||||||
sortedPaths: sortedItems.map(item => item.link),
|
|
||||||
items: sortedItems.map(item => ({
|
|
||||||
id: item.id,
|
|
||||||
link: item.link,
|
|
||||||
name: item.name,
|
|
||||||
hasSubmenu: !!item.submenu,
|
|
||||||
submenuCount: item.submenu?.length || 0
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
return sortedItems;
|
return sortedItems;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -383,6 +340,10 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Preload all UI permissions in a single API call
|
||||||
|
// This caches all permissions before iterating through pages
|
||||||
|
await preloadUiPermissions();
|
||||||
|
|
||||||
const items = await getSidebarItems();
|
const items = await getSidebarItems();
|
||||||
console.log('✅ SidebarProvider: Setting sidebar items:', {
|
console.log('✅ SidebarProvider: Setting sidebar items:', {
|
||||||
count: items.length,
|
count: items.length,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ const createMandatesHook = () => {
|
||||||
updateOptimistically,
|
updateOptimistically,
|
||||||
attributes,
|
attributes,
|
||||||
permissions,
|
permissions,
|
||||||
|
pagination,
|
||||||
fetchMandateById,
|
fetchMandateById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
generateCreateFieldsFromAttributes,
|
generateCreateFieldsFromAttributes,
|
||||||
|
|
@ -49,6 +50,7 @@ const createMandatesHook = () => {
|
||||||
handleMandateDelete,
|
handleMandateDelete,
|
||||||
handleMandateCreate,
|
handleMandateCreate,
|
||||||
handleMandateUpdate,
|
handleMandateUpdate,
|
||||||
|
handleInlineUpdate,
|
||||||
deletingMandates,
|
deletingMandates,
|
||||||
editingMandates,
|
editingMandates,
|
||||||
deleteError,
|
deleteError,
|
||||||
|
|
@ -93,6 +95,7 @@ const createMandatesHook = () => {
|
||||||
handleDelete: handleMandateDelete,
|
handleDelete: handleMandateDelete,
|
||||||
handleDeleteMultiple,
|
handleDeleteMultiple,
|
||||||
handleMandateUpdate,
|
handleMandateUpdate,
|
||||||
|
handleInlineUpdate, // For inline boolean editing in table
|
||||||
// FormGenerator specific handlers
|
// FormGenerator specific handlers
|
||||||
onDelete: handleDeleteSingle,
|
onDelete: handleDeleteSingle,
|
||||||
onDeleteMultiple: handleDeleteMultiple,
|
onDeleteMultiple: handleDeleteMultiple,
|
||||||
|
|
@ -105,6 +108,7 @@ const createMandatesHook = () => {
|
||||||
// Attributes and permissions for dynamic column/button generation
|
// Attributes and permissions for dynamic column/button generation
|
||||||
attributes,
|
attributes,
|
||||||
permissions,
|
permissions,
|
||||||
|
pagination, // Pagination metadata from backend
|
||||||
columns: generatedColumns,
|
columns: generatedColumns,
|
||||||
// Functions for EditActionButton
|
// Functions for EditActionButton
|
||||||
fetchMandateById,
|
fetchMandateById,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ const createRbacRolesHook = () => {
|
||||||
updateOptimistically,
|
updateOptimistically,
|
||||||
attributes,
|
attributes,
|
||||||
permissions,
|
permissions,
|
||||||
|
pagination,
|
||||||
fetchRoleById,
|
fetchRoleById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
generateCreateFieldsFromAttributes,
|
generateCreateFieldsFromAttributes,
|
||||||
|
|
@ -114,6 +115,7 @@ const createRbacRolesHook = () => {
|
||||||
// Attributes and permissions for dynamic column/button generation
|
// Attributes and permissions for dynamic column/button generation
|
||||||
attributes,
|
attributes,
|
||||||
permissions,
|
permissions,
|
||||||
|
pagination, // Pagination metadata from backend
|
||||||
columns: generatedColumns,
|
columns: generatedColumns,
|
||||||
// Functions for EditActionButton
|
// Functions for EditActionButton
|
||||||
fetchRoleById,
|
fetchRoleById,
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ const createRbacRulesHook = () => {
|
||||||
handleRbacRuleDelete,
|
handleRbacRuleDelete,
|
||||||
handleRbacRuleCreate,
|
handleRbacRuleCreate,
|
||||||
handleRbacRuleUpdate,
|
handleRbacRuleUpdate,
|
||||||
|
handleInlineUpdate,
|
||||||
deletingRbacRules,
|
deletingRbacRules,
|
||||||
editingRbacRules,
|
editingRbacRules,
|
||||||
deleteError,
|
deleteError,
|
||||||
|
|
@ -94,6 +95,7 @@ const createRbacRulesHook = () => {
|
||||||
handleDelete: handleRbacRuleDelete,
|
handleDelete: handleRbacRuleDelete,
|
||||||
handleDeleteMultiple,
|
handleDeleteMultiple,
|
||||||
handleRbacRuleUpdate,
|
handleRbacRuleUpdate,
|
||||||
|
handleInlineUpdate, // For inline boolean editing in table
|
||||||
// FormGenerator specific handlers
|
// FormGenerator specific handlers
|
||||||
onDelete: handleDeleteSingle,
|
onDelete: handleDeleteSingle,
|
||||||
onDeleteMultiple: handleDeleteMultiple,
|
onDeleteMultiple: handleDeleteMultiple,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { GenericPageData } from '../../../pageInterface';
|
import { GenericPageData } from '../../../pageInterface';
|
||||||
import { FaUsers, FaPlus } from 'react-icons/fa';
|
import { FaUsers, FaPlus } from 'react-icons/fa';
|
||||||
|
import { IoMailOutline } from 'react-icons/io5';
|
||||||
import { useOrgUsers, useUserOperations } from '../../../../../hooks/useUsers';
|
import { useOrgUsers, useUserOperations } from '../../../../../hooks/useUsers';
|
||||||
import { getUserDataCache } from '../../../../../utils/userCache';
|
import { getUserDataCache } from '../../../../../utils/userCache';
|
||||||
|
|
||||||
|
|
@ -39,20 +40,26 @@ const createUsersHook = () => {
|
||||||
updateOptimistically,
|
updateOptimistically,
|
||||||
attributes,
|
attributes,
|
||||||
permissions,
|
permissions,
|
||||||
|
pagination,
|
||||||
fetchUserById,
|
fetchUserById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
} = useOrgUsers();
|
} = useOrgUsers();
|
||||||
const {
|
const {
|
||||||
handleUserDelete,
|
handleUserDelete,
|
||||||
handleUserCreate,
|
handleUserCreate,
|
||||||
handleUserUpdate,
|
handleUserUpdate,
|
||||||
|
handleInlineUpdate,
|
||||||
|
handleSendPasswordLink,
|
||||||
deletingUsers,
|
deletingUsers,
|
||||||
editingUsers,
|
editingUsers,
|
||||||
|
sendingPasswordLink,
|
||||||
creatingUser,
|
creatingUser,
|
||||||
deleteError,
|
deleteError,
|
||||||
createError,
|
createError,
|
||||||
updateError
|
updateError,
|
||||||
|
passwordLinkError
|
||||||
} = useUserOperations();
|
} = useUserOperations();
|
||||||
|
|
||||||
const generatedColumns = attributes && attributes.length > 0
|
const generatedColumns = attributes && attributes.length > 0
|
||||||
|
|
@ -99,24 +106,30 @@ const createUsersHook = () => {
|
||||||
handleDeleteMultiple,
|
handleDeleteMultiple,
|
||||||
handleUserCreate: wrappedHandleUserCreate,
|
handleUserCreate: wrappedHandleUserCreate,
|
||||||
handleUserUpdate,
|
handleUserUpdate,
|
||||||
|
handleInlineUpdate, // For inline boolean editing in table
|
||||||
|
handleSendPasswordLink, // Send password setup link to user
|
||||||
// FormGenerator specific handlers
|
// FormGenerator specific handlers
|
||||||
onDelete: handleDeleteSingle,
|
onDelete: handleDeleteSingle,
|
||||||
onDeleteMultiple: handleDeleteMultiple,
|
onDeleteMultiple: handleDeleteMultiple,
|
||||||
// Loading states
|
// Loading states
|
||||||
deletingUsers,
|
deletingUsers,
|
||||||
editingUsers,
|
editingUsers,
|
||||||
|
sendingPasswordLink,
|
||||||
creatingUser,
|
creatingUser,
|
||||||
// Error states
|
// Error states
|
||||||
deleteError,
|
deleteError,
|
||||||
createError,
|
createError,
|
||||||
updateError,
|
updateError,
|
||||||
|
passwordLinkError,
|
||||||
// Attributes and permissions for dynamic column/button generation
|
// Attributes and permissions for dynamic column/button generation
|
||||||
attributes,
|
attributes,
|
||||||
permissions,
|
permissions,
|
||||||
|
pagination, // Pagination metadata from backend
|
||||||
columns: generatedColumns,
|
columns: generatedColumns,
|
||||||
// Functions for EditActionButton
|
// Functions for EditActionButton
|
||||||
fetchUserById,
|
fetchUserById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -145,72 +158,9 @@ export const teamMembersPageData: GenericPageData = {
|
||||||
size: 'md',
|
size: 'md',
|
||||||
icon: FaPlus,
|
icon: FaPlus,
|
||||||
formConfig: {
|
formConfig: {
|
||||||
fields: [
|
// Fields will be generated dynamically from attributes via generateCreateFieldsFromAttributes
|
||||||
{
|
// PageRenderer will use generateCreateFieldsFromAttributes if available, otherwise generateEditFieldsFromAttributes
|
||||||
key: 'username',
|
fields: [], // Empty array - fields will be generated dynamically from attributes
|
||||||
label: 'team-members.field.username',
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
placeholder: 'team-members.field.username',
|
|
||||||
validator: (value: string) => {
|
|
||||||
if (!value || value.trim() === '') {
|
|
||||||
return 'Username cannot be empty';
|
|
||||||
}
|
|
||||||
if (value.length > 100) {
|
|
||||||
return 'Username cannot exceed 100 characters';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'email',
|
|
||||||
label: 'team-members.field.email',
|
|
||||||
type: 'email',
|
|
||||||
required: true,
|
|
||||||
placeholder: 'team-members.field.email',
|
|
||||||
validator: (value: string) => {
|
|
||||||
if (!value || value.trim() === '') {
|
|
||||||
return 'Email cannot be empty';
|
|
||||||
}
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
||||||
if (!emailRegex.test(value)) {
|
|
||||||
return 'Invalid email format';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'password',
|
|
||||||
label: 'team-members.field.password',
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
placeholder: 'team-members.field.password',
|
|
||||||
validator: (value: string) => {
|
|
||||||
if (!value || value.trim() === '') {
|
|
||||||
return 'Password cannot be empty';
|
|
||||||
}
|
|
||||||
if (value.length < 8) {
|
|
||||||
return 'Password must be at least 8 characters';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'fullName',
|
|
||||||
label: 'team-members.field.fullName',
|
|
||||||
type: 'string',
|
|
||||||
required: false,
|
|
||||||
placeholder: 'team-members.field.fullName'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'privilege',
|
|
||||||
label: 'team-members.field.privilege',
|
|
||||||
type: 'multiselect',
|
|
||||||
required: false,
|
|
||||||
options: ['viewer', 'editor', 'admin', 'sysadmin'],
|
|
||||||
placeholder: 'team-members.field.privilege'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
popupTitle: 'team-members.modal.create.title',
|
popupTitle: 'team-members.modal.create.title',
|
||||||
popupSize: 'medium',
|
popupSize: 'medium',
|
||||||
createOperationName: 'handleUserCreate',
|
createOperationName: 'handleUserCreate',
|
||||||
|
|
@ -228,6 +178,7 @@ export const teamMembersPageData: GenericPageData = {
|
||||||
tableConfig: {
|
tableConfig: {
|
||||||
hookFactory: createUsersHook,
|
hookFactory: createUsersHook,
|
||||||
// Columns are generated dynamically from attributes via hookData.columns
|
// Columns are generated dynamically from attributes via hookData.columns
|
||||||
|
// Standard action buttons (built-in: edit, delete, view, copy)
|
||||||
actionButtons: [
|
actionButtons: [
|
||||||
{
|
{
|
||||||
type: 'edit',
|
type: 'edit',
|
||||||
|
|
@ -249,7 +200,6 @@ export const teamMembersPageData: GenericPageData = {
|
||||||
idField: 'id',
|
idField: 'id',
|
||||||
operationName: 'handleDelete',
|
operationName: 'handleDelete',
|
||||||
loadingStateName: 'deletingUsers',
|
loadingStateName: 'deletingUsers',
|
||||||
// Only show if user has delete permission (permissions.delete !== 'n')
|
|
||||||
disabled: (hookData: any) => {
|
disabled: (hookData: any) => {
|
||||||
if (!hookData?.permissions) return { disabled: false };
|
if (!hookData?.permissions) return { disabled: false };
|
||||||
const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view;
|
const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view;
|
||||||
|
|
@ -257,6 +207,27 @@ export const teamMembersPageData: GenericPageData = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
// Custom action buttons (entity-specific)
|
||||||
|
customActions: [
|
||||||
|
{
|
||||||
|
id: 'sendPasswordLink',
|
||||||
|
icon: React.createElement(IoMailOutline),
|
||||||
|
title: 'team-members.action.sendPasswordLink',
|
||||||
|
onClick: async (row: any, hookData: any) => {
|
||||||
|
if (hookData?.handleSendPasswordLink) {
|
||||||
|
await hookData.handleSendPasswordLink(row.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Only show for users with local authentication (not msft/google)
|
||||||
|
visible: (row: any) => row.authenticationAuthority === 'local',
|
||||||
|
disabled: (_row: any, hookData: any) => {
|
||||||
|
if (!hookData?.permissions) return { disabled: false, message: '' };
|
||||||
|
const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view;
|
||||||
|
return { disabled: !hasUpdate, message: 'No permission to send password link' };
|
||||||
|
},
|
||||||
|
loading: (row: any, hookData: any) => hookData?.sendingPasswordLink?.has(row.id) || false
|
||||||
|
}
|
||||||
|
],
|
||||||
searchable: true,
|
searchable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { GenericPageData } from '../../pageInterface';
|
import { GenericPageData } from '../../pageInterface';
|
||||||
import { FaRegFileAlt, FaUpload } from 'react-icons/fa';
|
import { FaRegFileAlt, FaUpload, FaDownload } from 'react-icons/fa';
|
||||||
import { useUserFiles, useFileOperations } from '../../../../hooks/useFiles';
|
import { useUserFiles, useFileOperations } from '../../../../hooks/useFiles';
|
||||||
|
|
||||||
// Helper function to convert attribute definitions to column config
|
// Helper function to convert attribute definitions to column config
|
||||||
|
|
@ -66,6 +66,7 @@ const createFilesHook = () => {
|
||||||
updateFileOptimistically,
|
updateFileOptimistically,
|
||||||
attributes,
|
attributes,
|
||||||
permissions,
|
permissions,
|
||||||
|
pagination,
|
||||||
fetchFileById,
|
fetchFileById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
|
|
@ -152,6 +153,7 @@ const createFilesHook = () => {
|
||||||
// Attributes and permissions for dynamic column/button generation
|
// Attributes and permissions for dynamic column/button generation
|
||||||
attributes,
|
attributes,
|
||||||
permissions,
|
permissions,
|
||||||
|
pagination, // Pagination metadata from backend
|
||||||
columns: generatedColumns, // Return generated columns
|
columns: generatedColumns, // Return generated columns
|
||||||
// Functions for EditActionButton
|
// Functions for EditActionButton
|
||||||
fetchFileById, // Fetch single file by ID
|
fetchFileById, // Fetch single file by ID
|
||||||
|
|
@ -201,18 +203,16 @@ export const filesPageData: GenericPageData = {
|
||||||
tableConfig: {
|
tableConfig: {
|
||||||
hookFactory: createFilesHook,
|
hookFactory: createFilesHook,
|
||||||
// Columns are generated dynamically from attributes via hookData.columns
|
// Columns are generated dynamically from attributes via hookData.columns
|
||||||
|
// Standard action buttons (built-in: edit, delete, view, copy)
|
||||||
actionButtons: [
|
actionButtons: [
|
||||||
{
|
{
|
||||||
type: 'view',
|
type: 'view',
|
||||||
title: 'files.action.preview',
|
title: 'files.action.preview',
|
||||||
idField: 'id',
|
idField: 'id',
|
||||||
// nameField and typeField will be determined from attributes dynamically
|
|
||||||
// For now, use common backend field names
|
|
||||||
nameField: 'fileName',
|
nameField: 'fileName',
|
||||||
typeField: 'mimeType',
|
typeField: 'mimeType',
|
||||||
operationName: 'handlePreview',
|
operationName: 'handlePreview',
|
||||||
loadingStateName: 'previewingFiles',
|
loadingStateName: 'previewingFiles',
|
||||||
// Only show if user has read permission (permissions.read !== 'n')
|
|
||||||
disabled: (hookData: any) => {
|
disabled: (hookData: any) => {
|
||||||
if (!hookData?.permissions) return { disabled: false };
|
if (!hookData?.permissions) return { disabled: false };
|
||||||
const hasRead = hookData.permissions.read !== 'n' && hookData.permissions.view;
|
const hasRead = hookData.permissions.read !== 'n' && hookData.permissions.view;
|
||||||
|
|
@ -233,26 +233,12 @@ export const filesPageData: GenericPageData = {
|
||||||
return { disabled: !hasUpdate, message: 'No permission to edit files' };
|
return { disabled: !hasUpdate, message: 'No permission to edit files' };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: 'download',
|
|
||||||
title: 'files.action.download',
|
|
||||||
idField: 'id',
|
|
||||||
operationName: 'handleDownload',
|
|
||||||
loadingStateName: 'downloadingFiles',
|
|
||||||
// Only show if user has read permission (permissions.read !== 'n')
|
|
||||||
disabled: (hookData: any) => {
|
|
||||||
if (!hookData?.permissions) return { disabled: false };
|
|
||||||
const hasRead = hookData.permissions.read !== 'n' && hookData.permissions.view;
|
|
||||||
return { disabled: !hasRead, message: 'No permission to download files' };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
title: 'files.action.delete',
|
title: 'files.action.delete',
|
||||||
idField: 'id',
|
idField: 'id',
|
||||||
operationName: 'handleDelete',
|
operationName: 'handleDelete',
|
||||||
loadingStateName: 'deletingFiles',
|
loadingStateName: 'deletingFiles',
|
||||||
// Only show if user has delete permission (permissions.delete !== 'n')
|
|
||||||
disabled: (hookData: any) => {
|
disabled: (hookData: any) => {
|
||||||
if (!hookData?.permissions) return { disabled: false };
|
if (!hookData?.permissions) return { disabled: false };
|
||||||
const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view;
|
const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view;
|
||||||
|
|
@ -260,6 +246,25 @@ export const filesPageData: GenericPageData = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
// Custom action buttons (entity-specific)
|
||||||
|
customActions: [
|
||||||
|
{
|
||||||
|
id: 'download',
|
||||||
|
icon: React.createElement(FaDownload),
|
||||||
|
title: 'files.action.download',
|
||||||
|
onClick: async (row: any, hookData: any) => {
|
||||||
|
if (hookData?.handleDownload) {
|
||||||
|
await hookData.handleDownload(row.id, row.fileName, row.mimeType);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disabled: (row: any, hookData: any) => {
|
||||||
|
if (!hookData?.permissions) return { disabled: false, message: '' };
|
||||||
|
const hasRead = hookData.permissions.read !== 'n' && hookData.permissions.view;
|
||||||
|
return { disabled: !hasRead, message: 'No permission to download files' };
|
||||||
|
},
|
||||||
|
loading: (row: any, hookData: any) => hookData?.downloadingFiles?.has(row.id) || false
|
||||||
|
}
|
||||||
|
],
|
||||||
searchable: true,
|
searchable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,16 @@ export { chatbotPageData } from './chatbot';
|
||||||
export { mandatesPageData } from './admin/mandates';
|
export { mandatesPageData } from './admin/mandates';
|
||||||
export { rbacRulesPageData } from './admin/rbac-rules';
|
export { rbacRulesPageData } from './admin/rbac-rules';
|
||||||
export { rbacRolePageData } from './admin/rbac-role';
|
export { rbacRolePageData } from './admin/rbac-role';
|
||||||
|
// Trustee pages (no container - SidebarProvider creates virtual parent group)
|
||||||
|
export {
|
||||||
|
trusteeOrganisationsPageData,
|
||||||
|
trusteeRolesPageData,
|
||||||
|
trusteeAccessPageData,
|
||||||
|
trusteeContractsPageData,
|
||||||
|
trusteeDocumentsPageData,
|
||||||
|
trusteePositionsPageData,
|
||||||
|
trusteePages
|
||||||
|
} from './trustee';
|
||||||
|
|
||||||
// Import all page data
|
// Import all page data
|
||||||
import { dashboardPageData } from './dashboard';
|
import { dashboardPageData } from './dashboard';
|
||||||
|
|
@ -29,6 +39,7 @@ import { chatbotPageData } from './chatbot';
|
||||||
import { mandatesPageData } from './admin/mandates';
|
import { mandatesPageData } from './admin/mandates';
|
||||||
import { rbacRulesPageData } from './admin/rbac-rules';
|
import { rbacRulesPageData } from './admin/rbac-rules';
|
||||||
import { rbacRolePageData } from './admin/rbac-role';
|
import { rbacRolePageData } from './admin/rbac-role';
|
||||||
|
import { trusteePages } from './trustee';
|
||||||
|
|
||||||
// Array of all page data
|
// Array of all page data
|
||||||
export const allPageData = [
|
export const allPageData = [
|
||||||
|
|
@ -36,17 +47,19 @@ export const allPageData = [
|
||||||
filesPageData,
|
filesPageData,
|
||||||
workflowsPageData,
|
workflowsPageData,
|
||||||
connectionsPageData,
|
connectionsPageData,
|
||||||
teamMembersPageData,
|
|
||||||
promptsPageData,
|
promptsPageData,
|
||||||
speechPageData,
|
speechPageData,
|
||||||
settingsPageData,
|
settingsPageData,
|
||||||
pekPageData,
|
pekPageData,
|
||||||
pekTablesPageData,
|
pekTablesPageData,
|
||||||
chatbotPageData,
|
chatbotPageData,
|
||||||
|
// Trustee pages (before Administration)
|
||||||
|
...trusteePages,
|
||||||
|
// Administration pages
|
||||||
|
teamMembersPageData,
|
||||||
mandatesPageData,
|
mandatesPageData,
|
||||||
rbacRulesPageData,
|
rbacRulesPageData,
|
||||||
rbacRolePageData,
|
rbacRolePageData,
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Helper function to get page data by path
|
// Helper function to get page data by path
|
||||||
|
|
|
||||||
|
|
@ -38,14 +38,17 @@ const createPromptsHook = () => {
|
||||||
updateOptimistically,
|
updateOptimistically,
|
||||||
attributes,
|
attributes,
|
||||||
permissions,
|
permissions,
|
||||||
|
pagination,
|
||||||
fetchPromptById,
|
fetchPromptById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
} = usePrompts();
|
} = usePrompts();
|
||||||
const {
|
const {
|
||||||
handlePromptDelete,
|
handlePromptDelete,
|
||||||
handlePromptCreate,
|
handlePromptCreate,
|
||||||
handlePromptUpdate,
|
handlePromptUpdate,
|
||||||
|
handleInlineUpdate,
|
||||||
deletingPrompts,
|
deletingPrompts,
|
||||||
creatingPrompt,
|
creatingPrompt,
|
||||||
deleteError,
|
deleteError,
|
||||||
|
|
@ -98,6 +101,7 @@ const createPromptsHook = () => {
|
||||||
handleDeleteMultiple,
|
handleDeleteMultiple,
|
||||||
handlePromptCreate: wrappedHandlePromptCreate, // Use wrapped version
|
handlePromptCreate: wrappedHandlePromptCreate, // Use wrapped version
|
||||||
handlePromptUpdate,
|
handlePromptUpdate,
|
||||||
|
handleInlineUpdate, // For inline boolean editing in table
|
||||||
// FormGenerator specific handlers
|
// FormGenerator specific handlers
|
||||||
onDelete: handleDeleteSingle,
|
onDelete: handleDeleteSingle,
|
||||||
onDeleteMultiple: handleDeleteMultiple,
|
onDeleteMultiple: handleDeleteMultiple,
|
||||||
|
|
@ -111,10 +115,12 @@ const createPromptsHook = () => {
|
||||||
// Attributes and permissions for dynamic column/button generation
|
// Attributes and permissions for dynamic column/button generation
|
||||||
attributes,
|
attributes,
|
||||||
permissions,
|
permissions,
|
||||||
|
pagination, // Pagination metadata from backend
|
||||||
columns: generatedColumns, // Return generated columns
|
columns: generatedColumns, // Return generated columns
|
||||||
// Functions for EditActionButton
|
// Functions for EditActionButton
|
||||||
fetchPromptById, // Fetch single prompt by ID
|
fetchPromptById, // Fetch single prompt by ID
|
||||||
generateEditFieldsFromAttributes, // Generate edit fields from attributes
|
generateEditFieldsFromAttributes, // Generate edit fields from attributes
|
||||||
|
generateCreateFieldsFromAttributes, // Generate create fields from attributes
|
||||||
ensureAttributesLoaded // Generic function to ensure attributes are loaded
|
ensureAttributesLoaded // Generic function to ensure attributes are loaded
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -143,42 +149,9 @@ export const promptsPageData: GenericPageData = {
|
||||||
icon: FaPlus,
|
icon: FaPlus,
|
||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
formConfig: {
|
formConfig: {
|
||||||
fields: [
|
// Fields will be generated dynamically from attributes via generateCreateFieldsFromAttributes
|
||||||
{
|
// PageRenderer will use generateCreateFieldsFromAttributes if available, otherwise generateEditFieldsFromAttributes
|
||||||
key: 'name',
|
fields: [], // Empty array - fields will be generated dynamically from attributes
|
||||||
label: 'prompts.field.name',
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
placeholder: 'prompts.field.name',
|
|
||||||
validator: (value: string) => {
|
|
||||||
if (!value || value.trim() === '') {
|
|
||||||
return 'Prompt name cannot be empty';
|
|
||||||
}
|
|
||||||
if (value.length > 100) {
|
|
||||||
return 'Prompt name cannot exceed 100 characters';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'content',
|
|
||||||
label: 'prompts.field.content',
|
|
||||||
type: 'textarea',
|
|
||||||
required: true,
|
|
||||||
placeholder: 'prompts.field.content',
|
|
||||||
minRows: 6,
|
|
||||||
maxRows: 12,
|
|
||||||
validator: (value: string) => {
|
|
||||||
if (!value || value.trim() === '') {
|
|
||||||
return 'Prompt content cannot be empty';
|
|
||||||
}
|
|
||||||
if (value.length > 10000) {
|
|
||||||
return 'Prompt content cannot exceed 10,000 characters';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
popupTitle: 'prompts.modal.create.title',
|
popupTitle: 'prompts.modal.create.title',
|
||||||
popupSize: 'medium',
|
popupSize: 'medium',
|
||||||
createOperationName: 'handlePromptCreate',
|
createOperationName: 'handlePromptCreate',
|
||||||
|
|
|
||||||
221
src/core/PageManager/data/pages/trustee/access.ts
Normal file
221
src/core/PageManager/data/pages/trustee/access.ts
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { GenericPageData } from '../../../pageInterface';
|
||||||
|
import { FaKey, FaPlus } from 'react-icons/fa';
|
||||||
|
import { useTrusteeAccess, useTrusteeAccessOperations } from '../../../../../hooks/useTrustee';
|
||||||
|
|
||||||
|
const attributesToColumns = (attributes: any[]) => {
|
||||||
|
return attributes.map(attr => {
|
||||||
|
const isDateField = attr.type === 'date' || attr.type === 'timestamp' ||
|
||||||
|
/(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name);
|
||||||
|
|
||||||
|
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,
|
||||||
|
filterable: isDateField ? false : (attr.filterable !== false),
|
||||||
|
searchable: attr.searchable !== false,
|
||||||
|
filterOptions: attr.filterOptions
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAccessHook = () => {
|
||||||
|
return () => {
|
||||||
|
const {
|
||||||
|
items: accessRecords,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
fetchById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
} = useTrusteeAccess();
|
||||||
|
const {
|
||||||
|
handleDelete,
|
||||||
|
handleCreate,
|
||||||
|
handleUpdate,
|
||||||
|
deletingItems,
|
||||||
|
creatingItem,
|
||||||
|
deleteError,
|
||||||
|
createError,
|
||||||
|
updateError
|
||||||
|
} = useTrusteeAccessOperations();
|
||||||
|
|
||||||
|
const generatedColumns = attributes && attributes.length > 0
|
||||||
|
? attributesToColumns(attributes)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const wrappedHandleCreate = useCallback(async (formData: any) => {
|
||||||
|
return await handleCreate(formData);
|
||||||
|
}, [handleCreate]);
|
||||||
|
|
||||||
|
const handleDeleteSingle = useCallback(async (item: any) => {
|
||||||
|
const success = await handleDelete(item.id);
|
||||||
|
if (success) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleDelete, refetch]);
|
||||||
|
|
||||||
|
const handleDeleteMultiple = useCallback(async (selectedItems: any[]) => {
|
||||||
|
const ids = selectedItems.map(item => item.id);
|
||||||
|
const results = await Promise.all(ids.map(id => handleDelete(id)));
|
||||||
|
const allSuccessful = results.every(result => result);
|
||||||
|
if (allSuccessful) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleDelete, refetch]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: accessRecords,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
handleDelete,
|
||||||
|
handleDeleteMultiple,
|
||||||
|
handleCreate: wrappedHandleCreate,
|
||||||
|
handleUpdate,
|
||||||
|
onDelete: handleDeleteSingle,
|
||||||
|
onDeleteMultiple: handleDeleteMultiple,
|
||||||
|
deletingItems,
|
||||||
|
creatingItem,
|
||||||
|
deleteError,
|
||||||
|
createError,
|
||||||
|
updateError,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
columns: generatedColumns,
|
||||||
|
fetchById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trusteeAccessPageData: GenericPageData = {
|
||||||
|
id: 'trustee-access',
|
||||||
|
path: 'trustee/access',
|
||||||
|
name: 'trustee.access.title',
|
||||||
|
description: 'trustee.access.description',
|
||||||
|
parentPath: 'trustee',
|
||||||
|
|
||||||
|
icon: FaKey,
|
||||||
|
title: 'trustee.access.title',
|
||||||
|
subtitle: 'trustee.access.subtitle',
|
||||||
|
|
||||||
|
headerButtons: [
|
||||||
|
{
|
||||||
|
id: 'new-access',
|
||||||
|
label: 'trustee.access.new_button',
|
||||||
|
icon: FaPlus,
|
||||||
|
variant: 'primary',
|
||||||
|
formConfig: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'organisationId',
|
||||||
|
label: 'trustee.access.field.organisationId',
|
||||||
|
type: 'enum',
|
||||||
|
required: true,
|
||||||
|
optionsReference: 'TrusteeOrganisation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'roleId',
|
||||||
|
label: 'trustee.access.field.roleId',
|
||||||
|
type: 'enum',
|
||||||
|
required: true,
|
||||||
|
optionsReference: 'TrusteeRole'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'userId',
|
||||||
|
label: 'trustee.access.field.userId',
|
||||||
|
type: 'enum',
|
||||||
|
required: true,
|
||||||
|
optionsReference: 'User'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'contractId',
|
||||||
|
label: 'trustee.access.field.contractId',
|
||||||
|
type: 'enum',
|
||||||
|
required: false,
|
||||||
|
optionsReference: 'TrusteeContract',
|
||||||
|
placeholder: 'trustee.access.field.contractId_placeholder'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
popupTitle: 'trustee.access.modal.create.title',
|
||||||
|
popupSize: 'medium',
|
||||||
|
createOperationName: 'handleCreate',
|
||||||
|
successMessage: 'trustee.access.create.success',
|
||||||
|
errorMessage: 'trustee.access.create.error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
id: 'access-table',
|
||||||
|
type: 'table',
|
||||||
|
tableConfig: {
|
||||||
|
hookFactory: createAccessHook,
|
||||||
|
actionButtons: [
|
||||||
|
{
|
||||||
|
type: 'edit',
|
||||||
|
title: 'trustee.access.action.edit',
|
||||||
|
idField: 'id',
|
||||||
|
nameField: 'id',
|
||||||
|
operationName: 'handleUpdate',
|
||||||
|
loadingStateName: 'updatingItems',
|
||||||
|
fetchItemFunctionName: 'fetchById',
|
||||||
|
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 access' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'delete',
|
||||||
|
title: 'trustee.access.action.delete',
|
||||||
|
idField: 'id',
|
||||||
|
operationName: 'handleDelete',
|
||||||
|
loadingStateName: 'deletingItems',
|
||||||
|
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 access' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
searchable: true,
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
resizable: true,
|
||||||
|
pagination: true,
|
||||||
|
pageSize: 10,
|
||||||
|
className: 'trustee-access-table'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
persistent: false,
|
||||||
|
preload: false,
|
||||||
|
preserveState: true,
|
||||||
|
moduleEnabled: true,
|
||||||
|
|
||||||
|
onActivate: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Access activated');
|
||||||
|
},
|
||||||
|
onLoad: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Access loaded');
|
||||||
|
},
|
||||||
|
onUnload: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Access unloaded');
|
||||||
|
}
|
||||||
|
};
|
||||||
212
src/core/PageManager/data/pages/trustee/contracts.ts
Normal file
212
src/core/PageManager/data/pages/trustee/contracts.ts
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { GenericPageData } from '../../../pageInterface';
|
||||||
|
import { FaFileContract, FaPlus } from 'react-icons/fa';
|
||||||
|
import { useTrusteeContracts, useTrusteeContractOperations } from '../../../../../hooks/useTrustee';
|
||||||
|
|
||||||
|
const attributesToColumns = (attributes: any[]) => {
|
||||||
|
return attributes.map(attr => {
|
||||||
|
const isDateField = attr.type === 'date' || attr.type === 'timestamp' ||
|
||||||
|
/(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name);
|
||||||
|
|
||||||
|
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,
|
||||||
|
filterable: isDateField ? false : (attr.filterable !== false),
|
||||||
|
searchable: attr.searchable !== false,
|
||||||
|
filterOptions: attr.filterOptions
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createContractsHook = () => {
|
||||||
|
return () => {
|
||||||
|
const {
|
||||||
|
items: contracts,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
fetchById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
} = useTrusteeContracts();
|
||||||
|
const {
|
||||||
|
handleDelete,
|
||||||
|
handleCreate,
|
||||||
|
handleUpdate,
|
||||||
|
deletingItems,
|
||||||
|
creatingItem,
|
||||||
|
deleteError,
|
||||||
|
createError,
|
||||||
|
updateError
|
||||||
|
} = useTrusteeContractOperations();
|
||||||
|
|
||||||
|
const generatedColumns = attributes && attributes.length > 0
|
||||||
|
? attributesToColumns(attributes)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const wrappedHandleCreate = useCallback(async (formData: any) => {
|
||||||
|
return await handleCreate(formData);
|
||||||
|
}, [handleCreate]);
|
||||||
|
|
||||||
|
const handleDeleteSingle = useCallback(async (item: any) => {
|
||||||
|
const success = await handleDelete(item.id);
|
||||||
|
if (success) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleDelete, refetch]);
|
||||||
|
|
||||||
|
const handleDeleteMultiple = useCallback(async (selectedItems: any[]) => {
|
||||||
|
const ids = selectedItems.map(item => item.id);
|
||||||
|
const results = await Promise.all(ids.map(id => handleDelete(id)));
|
||||||
|
const allSuccessful = results.every(result => result);
|
||||||
|
if (allSuccessful) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleDelete, refetch]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: contracts,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
handleDelete,
|
||||||
|
handleDeleteMultiple,
|
||||||
|
handleCreate: wrappedHandleCreate,
|
||||||
|
handleUpdate,
|
||||||
|
onDelete: handleDeleteSingle,
|
||||||
|
onDeleteMultiple: handleDeleteMultiple,
|
||||||
|
deletingItems,
|
||||||
|
creatingItem,
|
||||||
|
deleteError,
|
||||||
|
createError,
|
||||||
|
updateError,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
columns: generatedColumns,
|
||||||
|
fetchById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trusteeContractsPageData: GenericPageData = {
|
||||||
|
id: 'trustee-contracts',
|
||||||
|
path: 'trustee/contracts',
|
||||||
|
name: 'trustee.contracts.title',
|
||||||
|
description: 'trustee.contracts.description',
|
||||||
|
parentPath: 'trustee',
|
||||||
|
|
||||||
|
icon: FaFileContract,
|
||||||
|
title: 'trustee.contracts.title',
|
||||||
|
subtitle: 'trustee.contracts.subtitle',
|
||||||
|
|
||||||
|
headerButtons: [
|
||||||
|
{
|
||||||
|
id: 'new-contract',
|
||||||
|
label: 'trustee.contracts.new_button',
|
||||||
|
icon: FaPlus,
|
||||||
|
variant: 'primary',
|
||||||
|
formConfig: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'organisationId',
|
||||||
|
label: 'trustee.contracts.field.organisationId',
|
||||||
|
type: 'enum',
|
||||||
|
required: true,
|
||||||
|
optionsReference: 'TrusteeOrganisation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'label',
|
||||||
|
label: 'trustee.contracts.field.label',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'trustee.contracts.field.label_placeholder'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enabled',
|
||||||
|
label: 'trustee.contracts.field.enabled',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
popupTitle: 'trustee.contracts.modal.create.title',
|
||||||
|
popupSize: 'medium',
|
||||||
|
createOperationName: 'handleCreate',
|
||||||
|
successMessage: 'trustee.contracts.create.success',
|
||||||
|
errorMessage: 'trustee.contracts.create.error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
id: 'contracts-table',
|
||||||
|
type: 'table',
|
||||||
|
tableConfig: {
|
||||||
|
hookFactory: createContractsHook,
|
||||||
|
actionButtons: [
|
||||||
|
{
|
||||||
|
type: 'edit',
|
||||||
|
title: 'trustee.contracts.action.edit',
|
||||||
|
idField: 'id',
|
||||||
|
nameField: 'label',
|
||||||
|
operationName: 'handleUpdate',
|
||||||
|
loadingStateName: 'updatingItems',
|
||||||
|
fetchItemFunctionName: 'fetchById',
|
||||||
|
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 contracts' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'delete',
|
||||||
|
title: 'trustee.contracts.action.delete',
|
||||||
|
idField: 'id',
|
||||||
|
operationName: 'handleDelete',
|
||||||
|
loadingStateName: 'deletingItems',
|
||||||
|
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 contracts' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
searchable: true,
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
resizable: true,
|
||||||
|
pagination: true,
|
||||||
|
pageSize: 10,
|
||||||
|
className: 'trustee-contracts-table'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
persistent: false,
|
||||||
|
preload: false,
|
||||||
|
preserveState: true,
|
||||||
|
moduleEnabled: true,
|
||||||
|
|
||||||
|
onActivate: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Contracts activated');
|
||||||
|
},
|
||||||
|
onLoad: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Contracts loaded');
|
||||||
|
},
|
||||||
|
onUnload: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Contracts unloaded');
|
||||||
|
}
|
||||||
|
};
|
||||||
230
src/core/PageManager/data/pages/trustee/documents.ts
Normal file
230
src/core/PageManager/data/pages/trustee/documents.ts
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { GenericPageData } from '../../../pageInterface';
|
||||||
|
import { FaFile, FaPlus } from 'react-icons/fa';
|
||||||
|
import { useTrusteeDocuments, useTrusteeDocumentOperations } from '../../../../../hooks/useTrustee';
|
||||||
|
|
||||||
|
const attributesToColumns = (attributes: any[]) => {
|
||||||
|
return attributes.map(attr => {
|
||||||
|
const isDateField = attr.type === 'date' || attr.type === 'timestamp' ||
|
||||||
|
/(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name);
|
||||||
|
|
||||||
|
// Hide binary data column
|
||||||
|
if (attr.name === 'documentData') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
filterable: isDateField ? false : (attr.filterable !== false),
|
||||||
|
searchable: attr.searchable !== false,
|
||||||
|
filterOptions: attr.filterOptions
|
||||||
|
};
|
||||||
|
}).filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDocumentsHook = () => {
|
||||||
|
return () => {
|
||||||
|
const {
|
||||||
|
items: documents,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
fetchById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
} = useTrusteeDocuments();
|
||||||
|
const {
|
||||||
|
handleDelete,
|
||||||
|
handleCreate,
|
||||||
|
handleUpdate,
|
||||||
|
deletingItems,
|
||||||
|
creatingItem,
|
||||||
|
deleteError,
|
||||||
|
createError,
|
||||||
|
updateError
|
||||||
|
} = useTrusteeDocumentOperations();
|
||||||
|
|
||||||
|
const generatedColumns = attributes && attributes.length > 0
|
||||||
|
? attributesToColumns(attributes)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const wrappedHandleCreate = useCallback(async (formData: any) => {
|
||||||
|
return await handleCreate(formData);
|
||||||
|
}, [handleCreate]);
|
||||||
|
|
||||||
|
const handleDeleteSingle = useCallback(async (item: any) => {
|
||||||
|
const success = await handleDelete(item.id);
|
||||||
|
if (success) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleDelete, refetch]);
|
||||||
|
|
||||||
|
const handleDeleteMultiple = useCallback(async (selectedItems: any[]) => {
|
||||||
|
const ids = selectedItems.map(item => item.id);
|
||||||
|
const results = await Promise.all(ids.map(id => handleDelete(id)));
|
||||||
|
const allSuccessful = results.every(result => result);
|
||||||
|
if (allSuccessful) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleDelete, refetch]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: documents,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
handleDelete,
|
||||||
|
handleDeleteMultiple,
|
||||||
|
handleCreate: wrappedHandleCreate,
|
||||||
|
handleUpdate,
|
||||||
|
onDelete: handleDeleteSingle,
|
||||||
|
onDeleteMultiple: handleDeleteMultiple,
|
||||||
|
deletingItems,
|
||||||
|
creatingItem,
|
||||||
|
deleteError,
|
||||||
|
createError,
|
||||||
|
updateError,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
columns: generatedColumns,
|
||||||
|
fetchById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trusteeDocumentsPageData: GenericPageData = {
|
||||||
|
id: 'trustee-documents',
|
||||||
|
path: 'trustee/documents',
|
||||||
|
name: 'trustee.documents.title',
|
||||||
|
description: 'trustee.documents.description',
|
||||||
|
parentPath: 'trustee',
|
||||||
|
|
||||||
|
icon: FaFile,
|
||||||
|
title: 'trustee.documents.title',
|
||||||
|
subtitle: 'trustee.documents.subtitle',
|
||||||
|
|
||||||
|
headerButtons: [
|
||||||
|
{
|
||||||
|
id: 'new-document',
|
||||||
|
label: 'trustee.documents.new_button',
|
||||||
|
icon: FaPlus,
|
||||||
|
variant: 'primary',
|
||||||
|
formConfig: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'organisationId',
|
||||||
|
label: 'trustee.documents.field.organisationId',
|
||||||
|
type: 'enum',
|
||||||
|
required: true,
|
||||||
|
optionsReference: 'TrusteeOrganisation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'contractId',
|
||||||
|
label: 'trustee.documents.field.contractId',
|
||||||
|
type: 'enum',
|
||||||
|
required: true,
|
||||||
|
optionsReference: 'TrusteeContract'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'documentName',
|
||||||
|
label: 'trustee.documents.field.documentName',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'trustee.documents.field.documentName_placeholder'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'documentMimeType',
|
||||||
|
label: 'trustee.documents.field.documentMimeType',
|
||||||
|
type: 'enum',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: 'application/pdf', label: 'PDF' },
|
||||||
|
{ value: 'image/jpeg', label: 'JPEG' },
|
||||||
|
{ value: 'image/png', label: 'PNG' },
|
||||||
|
{ value: 'application/octet-stream', label: 'Other' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
popupTitle: 'trustee.documents.modal.create.title',
|
||||||
|
popupSize: 'medium',
|
||||||
|
createOperationName: 'handleCreate',
|
||||||
|
successMessage: 'trustee.documents.create.success',
|
||||||
|
errorMessage: 'trustee.documents.create.error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
id: 'documents-table',
|
||||||
|
type: 'table',
|
||||||
|
tableConfig: {
|
||||||
|
hookFactory: createDocumentsHook,
|
||||||
|
actionButtons: [
|
||||||
|
{
|
||||||
|
type: 'edit',
|
||||||
|
title: 'trustee.documents.action.edit',
|
||||||
|
idField: 'id',
|
||||||
|
nameField: 'documentName',
|
||||||
|
operationName: 'handleUpdate',
|
||||||
|
loadingStateName: 'updatingItems',
|
||||||
|
fetchItemFunctionName: 'fetchById',
|
||||||
|
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 documents' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'delete',
|
||||||
|
title: 'trustee.documents.action.delete',
|
||||||
|
idField: 'id',
|
||||||
|
operationName: 'handleDelete',
|
||||||
|
loadingStateName: 'deletingItems',
|
||||||
|
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 documents' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
searchable: true,
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
resizable: true,
|
||||||
|
pagination: true,
|
||||||
|
pageSize: 10,
|
||||||
|
className: 'trustee-documents-table'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
persistent: false,
|
||||||
|
preload: false,
|
||||||
|
preserveState: true,
|
||||||
|
moduleEnabled: true,
|
||||||
|
|
||||||
|
onActivate: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Documents activated');
|
||||||
|
},
|
||||||
|
onLoad: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Documents loaded');
|
||||||
|
},
|
||||||
|
onUnload: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Documents unloaded');
|
||||||
|
}
|
||||||
|
};
|
||||||
31
src/core/PageManager/data/pages/trustee/index.ts
Normal file
31
src/core/PageManager/data/pages/trustee/index.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { GenericPageData } from '../../../pageInterface';
|
||||||
|
|
||||||
|
// Import all trustee page configurations
|
||||||
|
import { trusteeOrganisationsPageData } from './organisations';
|
||||||
|
import { trusteeRolesPageData } from './roles';
|
||||||
|
import { trusteeAccessPageData } from './access';
|
||||||
|
import { trusteeContractsPageData } from './contracts';
|
||||||
|
import { trusteeDocumentsPageData } from './documents';
|
||||||
|
import { trusteePositionsPageData } from './positions';
|
||||||
|
|
||||||
|
// Export all trustee pages
|
||||||
|
export {
|
||||||
|
trusteeOrganisationsPageData,
|
||||||
|
trusteeRolesPageData,
|
||||||
|
trusteeAccessPageData,
|
||||||
|
trusteeContractsPageData,
|
||||||
|
trusteeDocumentsPageData,
|
||||||
|
trusteePositionsPageData
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export array of all trustee pages for registration
|
||||||
|
// No explicit container needed - SidebarProvider creates a virtual parent group
|
||||||
|
// based on parentPath: 'trustee' in the child pages
|
||||||
|
export const trusteePages: GenericPageData[] = [
|
||||||
|
trusteeOrganisationsPageData,
|
||||||
|
trusteeRolesPageData,
|
||||||
|
trusteeAccessPageData,
|
||||||
|
trusteeContractsPageData,
|
||||||
|
trusteeDocumentsPageData,
|
||||||
|
trusteePositionsPageData
|
||||||
|
];
|
||||||
226
src/core/PageManager/data/pages/trustee/organisations.ts
Normal file
226
src/core/PageManager/data/pages/trustee/organisations.ts
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { GenericPageData } from '../../../pageInterface';
|
||||||
|
import { FaBuilding, FaPlus } from 'react-icons/fa';
|
||||||
|
import { useTrusteeOrganisations, useTrusteeOrganisationOperations } from '../../../../../hooks/useTrustee';
|
||||||
|
|
||||||
|
// Helper function to convert attribute definitions to column config
|
||||||
|
const attributesToColumns = (attributes: any[]) => {
|
||||||
|
return attributes.map(attr => {
|
||||||
|
const isDateField = attr.type === 'date' || attr.type === 'timestamp' ||
|
||||||
|
/(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name);
|
||||||
|
|
||||||
|
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,
|
||||||
|
filterable: isDateField ? false : (attr.filterable !== false),
|
||||||
|
searchable: attr.searchable !== false,
|
||||||
|
filterOptions: attr.filterOptions
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook factory function for organisations data
|
||||||
|
const createOrganisationsHook = () => {
|
||||||
|
return () => {
|
||||||
|
const {
|
||||||
|
items: organisations,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
fetchById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
} = useTrusteeOrganisations();
|
||||||
|
const {
|
||||||
|
handleDelete,
|
||||||
|
handleCreate,
|
||||||
|
handleUpdate,
|
||||||
|
deletingItems,
|
||||||
|
creatingItem,
|
||||||
|
deleteError,
|
||||||
|
createError,
|
||||||
|
updateError
|
||||||
|
} = useTrusteeOrganisationOperations();
|
||||||
|
|
||||||
|
const generatedColumns = attributes && attributes.length > 0
|
||||||
|
? attributesToColumns(attributes)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const wrappedHandleCreate = useCallback(async (formData: any) => {
|
||||||
|
return await handleCreate(formData);
|
||||||
|
}, [handleCreate]);
|
||||||
|
|
||||||
|
const handleDeleteSingle = useCallback(async (item: any) => {
|
||||||
|
const success = await handleDelete(item.id);
|
||||||
|
if (success) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleDelete, refetch]);
|
||||||
|
|
||||||
|
const handleDeleteMultiple = useCallback(async (selectedItems: any[]) => {
|
||||||
|
const ids = selectedItems.map(item => item.id);
|
||||||
|
const results = await Promise.all(ids.map(id => handleDelete(id)));
|
||||||
|
const allSuccessful = results.every(result => result);
|
||||||
|
if (allSuccessful) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleDelete, refetch]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: organisations,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
handleDelete,
|
||||||
|
handleDeleteMultiple,
|
||||||
|
handleCreate: wrappedHandleCreate,
|
||||||
|
handleUpdate,
|
||||||
|
onDelete: handleDeleteSingle,
|
||||||
|
onDeleteMultiple: handleDeleteMultiple,
|
||||||
|
deletingItems,
|
||||||
|
creatingItem,
|
||||||
|
deleteError,
|
||||||
|
createError,
|
||||||
|
updateError,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
columns: generatedColumns,
|
||||||
|
fetchById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trusteeOrganisationsPageData: GenericPageData = {
|
||||||
|
id: 'trustee-organisations',
|
||||||
|
path: 'trustee/organisations',
|
||||||
|
name: 'trustee.organisations.title',
|
||||||
|
description: 'trustee.organisations.description',
|
||||||
|
parentPath: 'trustee',
|
||||||
|
|
||||||
|
icon: FaBuilding,
|
||||||
|
title: 'trustee.organisations.title',
|
||||||
|
subtitle: 'trustee.organisations.subtitle',
|
||||||
|
|
||||||
|
headerButtons: [
|
||||||
|
{
|
||||||
|
id: 'new-organisation',
|
||||||
|
label: 'trustee.organisations.new_button',
|
||||||
|
icon: FaPlus,
|
||||||
|
variant: 'primary',
|
||||||
|
formConfig: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'id',
|
||||||
|
label: 'trustee.organisations.field.id',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'trustee.organisations.field.id_placeholder',
|
||||||
|
validator: (value: string) => {
|
||||||
|
if (!value || value.trim() === '') {
|
||||||
|
return 'Organisation ID cannot be empty';
|
||||||
|
}
|
||||||
|
if (value.length < 3 || value.length > 50) {
|
||||||
|
return 'Organisation ID must be 3-50 characters';
|
||||||
|
}
|
||||||
|
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
|
||||||
|
return 'Organisation ID can only contain letters, numbers, hyphens, and underscores';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'label',
|
||||||
|
label: 'trustee.organisations.field.label',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'trustee.organisations.field.label_placeholder'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enabled',
|
||||||
|
label: 'trustee.organisations.field.enabled',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
popupTitle: 'trustee.organisations.modal.create.title',
|
||||||
|
popupSize: 'medium',
|
||||||
|
createOperationName: 'handleCreate',
|
||||||
|
successMessage: 'trustee.organisations.create.success',
|
||||||
|
errorMessage: 'trustee.organisations.create.error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
id: 'organisations-table',
|
||||||
|
type: 'table',
|
||||||
|
tableConfig: {
|
||||||
|
hookFactory: createOrganisationsHook,
|
||||||
|
actionButtons: [
|
||||||
|
{
|
||||||
|
type: 'edit',
|
||||||
|
title: 'trustee.organisations.action.edit',
|
||||||
|
idField: 'id',
|
||||||
|
nameField: 'label',
|
||||||
|
operationName: 'handleUpdate',
|
||||||
|
loadingStateName: 'updatingItems',
|
||||||
|
fetchItemFunctionName: 'fetchById',
|
||||||
|
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 organisations' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'delete',
|
||||||
|
title: 'trustee.organisations.action.delete',
|
||||||
|
idField: 'id',
|
||||||
|
operationName: 'handleDelete',
|
||||||
|
loadingStateName: 'deletingItems',
|
||||||
|
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 organisations' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
searchable: true,
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
resizable: true,
|
||||||
|
pagination: true,
|
||||||
|
pageSize: 10,
|
||||||
|
className: 'trustee-organisations-table'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
persistent: false,
|
||||||
|
preload: false,
|
||||||
|
preserveState: true,
|
||||||
|
moduleEnabled: true,
|
||||||
|
|
||||||
|
onActivate: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Organisations activated');
|
||||||
|
},
|
||||||
|
onLoad: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Organisations loaded');
|
||||||
|
},
|
||||||
|
onUnload: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Organisations unloaded');
|
||||||
|
}
|
||||||
|
};
|
||||||
279
src/core/PageManager/data/pages/trustee/positions.ts
Normal file
279
src/core/PageManager/data/pages/trustee/positions.ts
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { GenericPageData } from '../../../pageInterface';
|
||||||
|
import { FaReceipt, FaPlus } from 'react-icons/fa';
|
||||||
|
import { useTrusteePositions, useTrusteePositionOperations } from '../../../../../hooks/useTrustee';
|
||||||
|
|
||||||
|
const attributesToColumns = (attributes: any[]) => {
|
||||||
|
return attributes.map(attr => {
|
||||||
|
const isDateField = attr.type === 'date' || attr.type === 'timestamp' ||
|
||||||
|
/(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name);
|
||||||
|
|
||||||
|
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,
|
||||||
|
filterable: isDateField ? false : (attr.filterable !== false),
|
||||||
|
searchable: attr.searchable !== false,
|
||||||
|
filterOptions: attr.filterOptions
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPositionsHook = () => {
|
||||||
|
return () => {
|
||||||
|
const {
|
||||||
|
items: positions,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
fetchById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
} = useTrusteePositions();
|
||||||
|
const {
|
||||||
|
handleDelete,
|
||||||
|
handleCreate,
|
||||||
|
handleUpdate,
|
||||||
|
deletingItems,
|
||||||
|
creatingItem,
|
||||||
|
deleteError,
|
||||||
|
createError,
|
||||||
|
updateError
|
||||||
|
} = useTrusteePositionOperations();
|
||||||
|
|
||||||
|
const generatedColumns = attributes && attributes.length > 0
|
||||||
|
? attributesToColumns(attributes)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const wrappedHandleCreate = useCallback(async (formData: any) => {
|
||||||
|
// Auto-calculate VAT amount if not provided
|
||||||
|
if (formData.bookingAmount && formData.vatPercentage && !formData.vatAmount) {
|
||||||
|
formData.vatAmount = formData.bookingAmount * formData.vatPercentage / 100;
|
||||||
|
}
|
||||||
|
return await handleCreate(formData);
|
||||||
|
}, [handleCreate]);
|
||||||
|
|
||||||
|
const handleDeleteSingle = useCallback(async (item: any) => {
|
||||||
|
const success = await handleDelete(item.id);
|
||||||
|
if (success) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleDelete, refetch]);
|
||||||
|
|
||||||
|
const handleDeleteMultiple = useCallback(async (selectedItems: any[]) => {
|
||||||
|
const ids = selectedItems.map(item => item.id);
|
||||||
|
const results = await Promise.all(ids.map(id => handleDelete(id)));
|
||||||
|
const allSuccessful = results.every(result => result);
|
||||||
|
if (allSuccessful) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleDelete, refetch]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: positions,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
handleDelete,
|
||||||
|
handleDeleteMultiple,
|
||||||
|
handleCreate: wrappedHandleCreate,
|
||||||
|
handleUpdate,
|
||||||
|
onDelete: handleDeleteSingle,
|
||||||
|
onDeleteMultiple: handleDeleteMultiple,
|
||||||
|
deletingItems,
|
||||||
|
creatingItem,
|
||||||
|
deleteError,
|
||||||
|
createError,
|
||||||
|
updateError,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
columns: generatedColumns,
|
||||||
|
fetchById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trusteePositionsPageData: GenericPageData = {
|
||||||
|
id: 'trustee-positions',
|
||||||
|
path: 'trustee/positions',
|
||||||
|
name: 'trustee.positions.title',
|
||||||
|
description: 'trustee.positions.description',
|
||||||
|
parentPath: 'trustee',
|
||||||
|
|
||||||
|
icon: FaReceipt,
|
||||||
|
title: 'trustee.positions.title',
|
||||||
|
subtitle: 'trustee.positions.subtitle',
|
||||||
|
|
||||||
|
headerButtons: [
|
||||||
|
{
|
||||||
|
id: 'new-position',
|
||||||
|
label: 'trustee.positions.new_button',
|
||||||
|
icon: FaPlus,
|
||||||
|
variant: 'primary',
|
||||||
|
formConfig: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'organisationId',
|
||||||
|
label: 'trustee.positions.field.organisationId',
|
||||||
|
type: 'enum',
|
||||||
|
required: true,
|
||||||
|
optionsReference: 'TrusteeOrganisation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'contractId',
|
||||||
|
label: 'trustee.positions.field.contractId',
|
||||||
|
type: 'enum',
|
||||||
|
required: true,
|
||||||
|
optionsReference: 'TrusteeContract'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'valuta',
|
||||||
|
label: 'trustee.positions.field.valuta',
|
||||||
|
type: 'date',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'company',
|
||||||
|
label: 'trustee.positions.field.company',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
placeholder: 'trustee.positions.field.company_placeholder'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'desc',
|
||||||
|
label: 'trustee.positions.field.desc',
|
||||||
|
type: 'textarea',
|
||||||
|
required: false,
|
||||||
|
minRows: 2,
|
||||||
|
maxRows: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'bookingCurrency',
|
||||||
|
label: 'trustee.positions.field.bookingCurrency',
|
||||||
|
type: 'enum',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: 'CHF', label: 'CHF' },
|
||||||
|
{ value: 'EUR', label: 'EUR' },
|
||||||
|
{ value: 'USD', label: 'USD' },
|
||||||
|
{ value: 'GBP', label: 'GBP' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'bookingAmount',
|
||||||
|
label: 'trustee.positions.field.bookingAmount',
|
||||||
|
type: 'number',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'originalCurrency',
|
||||||
|
label: 'trustee.positions.field.originalCurrency',
|
||||||
|
type: 'enum',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: 'CHF', label: 'CHF' },
|
||||||
|
{ value: 'EUR', label: 'EUR' },
|
||||||
|
{ value: 'USD', label: 'USD' },
|
||||||
|
{ value: 'GBP', label: 'GBP' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'originalAmount',
|
||||||
|
label: 'trustee.positions.field.originalAmount',
|
||||||
|
type: 'number',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vatPercentage',
|
||||||
|
label: 'trustee.positions.field.vatPercentage',
|
||||||
|
type: 'number',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vatAmount',
|
||||||
|
label: 'trustee.positions.field.vatAmount',
|
||||||
|
type: 'number',
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
popupTitle: 'trustee.positions.modal.create.title',
|
||||||
|
popupSize: 'large',
|
||||||
|
createOperationName: 'handleCreate',
|
||||||
|
successMessage: 'trustee.positions.create.success',
|
||||||
|
errorMessage: 'trustee.positions.create.error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
id: 'positions-table',
|
||||||
|
type: 'table',
|
||||||
|
tableConfig: {
|
||||||
|
hookFactory: createPositionsHook,
|
||||||
|
actionButtons: [
|
||||||
|
{
|
||||||
|
type: 'edit',
|
||||||
|
title: 'trustee.positions.action.edit',
|
||||||
|
idField: 'id',
|
||||||
|
nameField: 'desc',
|
||||||
|
operationName: 'handleUpdate',
|
||||||
|
loadingStateName: 'updatingItems',
|
||||||
|
fetchItemFunctionName: 'fetchById',
|
||||||
|
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 positions' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'delete',
|
||||||
|
title: 'trustee.positions.action.delete',
|
||||||
|
idField: 'id',
|
||||||
|
operationName: 'handleDelete',
|
||||||
|
loadingStateName: 'deletingItems',
|
||||||
|
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 positions' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
searchable: true,
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
resizable: true,
|
||||||
|
pagination: true,
|
||||||
|
pageSize: 10,
|
||||||
|
className: 'trustee-positions-table'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
persistent: false,
|
||||||
|
preload: false,
|
||||||
|
preserveState: true,
|
||||||
|
moduleEnabled: true,
|
||||||
|
|
||||||
|
onActivate: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Positions activated');
|
||||||
|
},
|
||||||
|
onLoad: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Positions loaded');
|
||||||
|
},
|
||||||
|
onUnload: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Positions unloaded');
|
||||||
|
}
|
||||||
|
};
|
||||||
208
src/core/PageManager/data/pages/trustee/roles.ts
Normal file
208
src/core/PageManager/data/pages/trustee/roles.ts
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { GenericPageData } from '../../../pageInterface';
|
||||||
|
import { FaUserTag, FaPlus } from 'react-icons/fa';
|
||||||
|
import { useTrusteeRoles, useTrusteeRoleOperations } from '../../../../../hooks/useTrustee';
|
||||||
|
|
||||||
|
const attributesToColumns = (attributes: any[]) => {
|
||||||
|
return attributes.map(attr => {
|
||||||
|
const isDateField = attr.type === 'date' || attr.type === 'timestamp' ||
|
||||||
|
/(date|Date|timestamp|Timestamp|created|Created|updated|Updated|at|At)$/i.test(attr.name);
|
||||||
|
|
||||||
|
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,
|
||||||
|
filterable: isDateField ? false : (attr.filterable !== false),
|
||||||
|
searchable: attr.searchable !== false,
|
||||||
|
filterOptions: attr.filterOptions
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRolesHook = () => {
|
||||||
|
return () => {
|
||||||
|
const {
|
||||||
|
items: roles,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
fetchById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
} = useTrusteeRoles();
|
||||||
|
const {
|
||||||
|
handleDelete,
|
||||||
|
handleCreate,
|
||||||
|
handleUpdate,
|
||||||
|
deletingItems,
|
||||||
|
creatingItem,
|
||||||
|
deleteError,
|
||||||
|
createError,
|
||||||
|
updateError
|
||||||
|
} = useTrusteeRoleOperations();
|
||||||
|
|
||||||
|
const generatedColumns = attributes && attributes.length > 0
|
||||||
|
? attributesToColumns(attributes)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const wrappedHandleCreate = useCallback(async (formData: any) => {
|
||||||
|
return await handleCreate(formData);
|
||||||
|
}, [handleCreate]);
|
||||||
|
|
||||||
|
const handleDeleteSingle = useCallback(async (item: any) => {
|
||||||
|
const success = await handleDelete(item.id);
|
||||||
|
if (success) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleDelete, refetch]);
|
||||||
|
|
||||||
|
const handleDeleteMultiple = useCallback(async (selectedItems: any[]) => {
|
||||||
|
const ids = selectedItems.map(item => item.id);
|
||||||
|
const results = await Promise.all(ids.map(id => handleDelete(id)));
|
||||||
|
const allSuccessful = results.every(result => result);
|
||||||
|
if (allSuccessful) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [handleDelete, refetch]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: roles,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
handleDelete,
|
||||||
|
handleDeleteMultiple,
|
||||||
|
handleCreate: wrappedHandleCreate,
|
||||||
|
handleUpdate,
|
||||||
|
onDelete: handleDeleteSingle,
|
||||||
|
onDeleteMultiple: handleDeleteMultiple,
|
||||||
|
deletingItems,
|
||||||
|
creatingItem,
|
||||||
|
deleteError,
|
||||||
|
createError,
|
||||||
|
updateError,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
columns: generatedColumns,
|
||||||
|
fetchById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trusteeRolesPageData: GenericPageData = {
|
||||||
|
id: 'trustee-roles',
|
||||||
|
path: 'trustee/roles',
|
||||||
|
name: 'trustee.roles.title',
|
||||||
|
description: 'trustee.roles.description',
|
||||||
|
parentPath: 'trustee',
|
||||||
|
|
||||||
|
icon: FaUserTag,
|
||||||
|
title: 'trustee.roles.title',
|
||||||
|
subtitle: 'trustee.roles.subtitle',
|
||||||
|
|
||||||
|
headerButtons: [
|
||||||
|
{
|
||||||
|
id: 'new-role',
|
||||||
|
label: 'trustee.roles.new_button',
|
||||||
|
icon: FaPlus,
|
||||||
|
variant: 'primary',
|
||||||
|
formConfig: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'id',
|
||||||
|
label: 'trustee.roles.field.id',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'trustee.roles.field.id_placeholder'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'desc',
|
||||||
|
label: 'trustee.roles.field.desc',
|
||||||
|
type: 'textarea',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'trustee.roles.field.desc_placeholder',
|
||||||
|
minRows: 3,
|
||||||
|
maxRows: 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
popupTitle: 'trustee.roles.modal.create.title',
|
||||||
|
popupSize: 'medium',
|
||||||
|
createOperationName: 'handleCreate',
|
||||||
|
successMessage: 'trustee.roles.create.success',
|
||||||
|
errorMessage: 'trustee.roles.create.error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
id: 'roles-table',
|
||||||
|
type: 'table',
|
||||||
|
tableConfig: {
|
||||||
|
hookFactory: createRolesHook,
|
||||||
|
actionButtons: [
|
||||||
|
{
|
||||||
|
type: 'edit',
|
||||||
|
title: 'trustee.roles.action.edit',
|
||||||
|
idField: 'id',
|
||||||
|
nameField: 'id',
|
||||||
|
operationName: 'handleUpdate',
|
||||||
|
loadingStateName: 'updatingItems',
|
||||||
|
fetchItemFunctionName: 'fetchById',
|
||||||
|
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: 'trustee.roles.action.delete',
|
||||||
|
idField: 'id',
|
||||||
|
operationName: 'handleDelete',
|
||||||
|
loadingStateName: 'deletingItems',
|
||||||
|
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: 'trustee-roles-table'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
persistent: false,
|
||||||
|
preload: false,
|
||||||
|
preserveState: true,
|
||||||
|
moduleEnabled: true,
|
||||||
|
|
||||||
|
onActivate: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Roles activated');
|
||||||
|
},
|
||||||
|
onLoad: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Roles loaded');
|
||||||
|
},
|
||||||
|
onUnload: async () => {
|
||||||
|
if (import.meta.env.DEV) console.log('Trustee Roles unloaded');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -78,6 +78,7 @@ const createWorkflowsHook = () => {
|
||||||
updateOptimistically,
|
updateOptimistically,
|
||||||
attributes,
|
attributes,
|
||||||
permissions,
|
permissions,
|
||||||
|
pagination,
|
||||||
fetchWorkflowById,
|
fetchWorkflowById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
|
|
@ -133,6 +134,7 @@ const createWorkflowsHook = () => {
|
||||||
// Attributes and permissions for dynamic column/button generation
|
// Attributes and permissions for dynamic column/button generation
|
||||||
attributes,
|
attributes,
|
||||||
permissions,
|
permissions,
|
||||||
|
pagination, // Pagination metadata from backend
|
||||||
columns: generatedColumns, // Return generated columns
|
columns: generatedColumns, // Return generated columns
|
||||||
// Functions for EditActionButton
|
// Functions for EditActionButton
|
||||||
fetchWorkflowById, // Fetch single workflow by ID
|
fetchWorkflowById, // Fetch single workflow by ID
|
||||||
|
|
|
||||||
|
|
@ -262,9 +262,9 @@ export interface GenericDataHook {
|
||||||
[key: string]: any; // Allow additional properties for dynamic data sources
|
[key: string]: any; // Allow additional properties for dynamic data sources
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action button configuration
|
// Standard action button configuration (built-in actions: edit, delete, view, copy)
|
||||||
export interface ActionButtonConfig {
|
export interface ActionButtonConfig {
|
||||||
type: 'view' | 'edit' | 'download' | 'delete' | 'copy' | 'connect' | 'play';
|
type: 'view' | 'edit' | 'delete' | 'copy';
|
||||||
onAction?: (row: any) => Promise<void> | void; // Optional for delete buttons since they handle their own logic
|
onAction?: (row: any) => Promise<void> | void; // Optional for delete buttons since they handle their own logic
|
||||||
title?: string | LanguageText;
|
title?: string | LanguageText;
|
||||||
disabled?: (row: any) => boolean | { disabled: boolean; message?: string };
|
disabled?: (row: any) => boolean | { disabled: boolean; message?: string };
|
||||||
|
|
@ -274,24 +274,32 @@ export interface ActionButtonConfig {
|
||||||
nameField?: string; // Field name for display name (default: 'name' or 'file_name')
|
nameField?: string; // Field name for display name (default: 'name' or 'file_name')
|
||||||
typeField?: string; // Field name for type/mime type (default: 'type' or 'mime_type')
|
typeField?: string; // Field name for type/mime type (default: 'type' or 'mime_type')
|
||||||
contentField?: string; // Field name for content (default: 'content')
|
contentField?: string; // Field name for content (default: 'content')
|
||||||
statusField?: string; // Field name for status (default: 'status')
|
|
||||||
authorityField?: string; // Field name for authority (msft/google) (default: 'authority')
|
|
||||||
// Operation and loading state names
|
// Operation and loading state names
|
||||||
operationName?: string; // Name of the operation function in hookData
|
operationName?: string; // Name of the operation function in hookData
|
||||||
disconnectOperationName?: string; // Name of the disconnect operation function in hookData (for connect button)
|
|
||||||
refreshOperationName?: string; // Name of the refresh operation function in hookData (for connect button)
|
|
||||||
loadingStateName?: string; // Name of the loading state in hookData
|
loadingStateName?: string; // Name of the loading state in hookData
|
||||||
fetchItemFunctionName?: string; // Name of the function in hookData to fetch a single item by ID (for edit button)
|
fetchItemFunctionName?: string; // Name of the function in hookData to fetch a single item by ID (for edit button)
|
||||||
// Navigation and behavior (for play button)
|
}
|
||||||
navigateTo?: string; // Path to navigate to after action (default: 'start/dashboard')
|
|
||||||
mode?: 'workflow' | 'prompt'; // Behavior mode for play button: 'workflow' selects workflow, 'prompt' sets input value (default: 'prompt')
|
// Custom action button configuration (for entity-specific actions like download, connect, play, sendPasswordLink)
|
||||||
|
export interface CustomActionConfig {
|
||||||
|
id: string; // Unique identifier for the action
|
||||||
|
icon: React.ReactNode; // Icon component to display
|
||||||
|
onClick: (row: any, hookData?: any) => Promise<void> | void; // Handler function
|
||||||
|
visible?: (row: any, hookData?: any) => boolean; // Show/hide based on row data (default: true)
|
||||||
|
disabled?: (row: any, hookData?: any) => boolean | { disabled: boolean; message?: string }; // Disable based on row data
|
||||||
|
loading?: (row: any, hookData?: any) => boolean; // Loading state based on row data
|
||||||
|
title?: string | LanguageText | ((row: any) => string); // Tooltip text
|
||||||
|
className?: string; // Optional custom CSS class
|
||||||
|
// Field mappings (optional, for convenience)
|
||||||
|
idField?: string; // Field name for the unique identifier (default: 'id')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table content configuration
|
// Table content configuration
|
||||||
export interface TableContentConfig {
|
export interface TableContentConfig {
|
||||||
hookFactory: () => () => GenericDataHook; // Hook factory that returns a hook function
|
hookFactory: () => () => GenericDataHook; // Hook factory that returns a hook function
|
||||||
columns?: any[]; // Column configuration (optional - can be generated dynamically from attributes via hookData.columns)
|
columns?: any[]; // Column configuration (optional - can be generated dynamically from attributes via hookData.columns)
|
||||||
actionButtons?: ActionButtonConfig[]; // Action buttons configuration
|
actionButtons?: ActionButtonConfig[]; // Standard action buttons configuration (edit, delete, view, copy)
|
||||||
|
customActions?: CustomActionConfig[]; // Custom action buttons (download, connect, play, sendPasswordLink, etc.)
|
||||||
searchable?: boolean;
|
searchable?: boolean;
|
||||||
filterable?: boolean;
|
filterable?: boolean;
|
||||||
sortable?: boolean;
|
sortable?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -235,11 +235,11 @@ export function useMandates() {
|
||||||
|
|
||||||
// Email validation
|
// Email validation
|
||||||
if (fieldType === 'email') {
|
if (fieldType === 'email') {
|
||||||
validator = (value: string) => {
|
validator = (value: any) => {
|
||||||
if (required && (!value || value.trim() === '')) {
|
if (required && (!value || (typeof value === 'string' && value.trim() === ''))) {
|
||||||
return 'Email cannot be empty';
|
return 'Email cannot be empty';
|
||||||
}
|
}
|
||||||
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
if (value && typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
||||||
return 'Invalid email format';
|
return 'Invalid email format';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -369,11 +369,11 @@ export function useMandates() {
|
||||||
|
|
||||||
// Email validation
|
// Email validation
|
||||||
if (fieldType === 'email') {
|
if (fieldType === 'email') {
|
||||||
validator = (value: string) => {
|
validator = (value: any) => {
|
||||||
if (required && (!value || value.trim() === '')) {
|
if (required && (!value || (typeof value === 'string' && value.trim() === ''))) {
|
||||||
return 'Email cannot be empty';
|
return 'Email cannot be empty';
|
||||||
}
|
}
|
||||||
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
if (value && typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
||||||
return 'Invalid email format';
|
return 'Invalid email format';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -399,8 +399,8 @@ export function useMandates() {
|
||||||
}
|
}
|
||||||
// String validation for required fields
|
// String validation for required fields
|
||||||
else if (fieldType === 'string' && required) {
|
else if (fieldType === 'string' && required) {
|
||||||
validator = (value: string) => {
|
validator = (value: any) => {
|
||||||
if (!value || value.trim() === '') {
|
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
||||||
return `${attr.label} is required`;
|
return `${attr.label} is required`;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -548,6 +548,15 @@ export function useMandateOperations() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Generic inline update handler for FormGeneratorTable
|
||||||
|
const handleInlineUpdate = async (mandateId: string, changes: Partial<MandateUpdateData>) => {
|
||||||
|
const result = await handleMandateUpdate(mandateId, changes as MandateUpdateData);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Failed to update');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deletingMandates,
|
deletingMandates,
|
||||||
editingMandates,
|
editingMandates,
|
||||||
|
|
@ -558,6 +567,7 @@ export function useMandateOperations() {
|
||||||
handleMandateDelete,
|
handleMandateDelete,
|
||||||
handleMandateCreate,
|
handleMandateCreate,
|
||||||
handleMandateUpdate,
|
handleMandateUpdate,
|
||||||
|
handleInlineUpdate,
|
||||||
isLoading
|
isLoading
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -276,11 +276,11 @@ export function useRbacRoles() {
|
||||||
|
|
||||||
// Email validation
|
// Email validation
|
||||||
if (fieldType === 'email') {
|
if (fieldType === 'email') {
|
||||||
validator = (value: string) => {
|
validator = (value: any) => {
|
||||||
if (required && (!value || value.trim() === '')) {
|
if (required && (!value || (typeof value === 'string' && value.trim() === ''))) {
|
||||||
return 'Email cannot be empty';
|
return 'Email cannot be empty';
|
||||||
}
|
}
|
||||||
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
if (value && typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
||||||
return 'Invalid email format';
|
return 'Invalid email format';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -410,11 +410,11 @@ export function useRbacRoles() {
|
||||||
|
|
||||||
// Email validation
|
// Email validation
|
||||||
if (fieldType === 'email') {
|
if (fieldType === 'email') {
|
||||||
validator = (value: string) => {
|
validator = (value: any) => {
|
||||||
if (required && (!value || value.trim() === '')) {
|
if (required && (!value || (typeof value === 'string' && value.trim() === ''))) {
|
||||||
return 'Email cannot be empty';
|
return 'Email cannot be empty';
|
||||||
}
|
}
|
||||||
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
if (value && typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
||||||
return 'Invalid email format';
|
return 'Invalid email format';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -650,6 +650,15 @@ export function useRbacRoleOperations() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Generic inline update handler for FormGeneratorTable
|
||||||
|
const handleInlineUpdate = async (roleId: string, changes: Partial<RoleUpdateData>) => {
|
||||||
|
const result = await handleRoleUpdate(roleId, changes as RoleUpdateData);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Failed to update');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deletingRoles,
|
deletingRoles,
|
||||||
editingRoles,
|
editingRoles,
|
||||||
|
|
@ -660,6 +669,7 @@ export function useRbacRoleOperations() {
|
||||||
handleRoleDelete,
|
handleRoleDelete,
|
||||||
handleRoleCreate,
|
handleRoleCreate,
|
||||||
handleRoleUpdate,
|
handleRoleUpdate,
|
||||||
|
handleInlineUpdate,
|
||||||
isLoading
|
isLoading
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -252,11 +252,11 @@ export function useRbacRules() {
|
||||||
|
|
||||||
// Email validation
|
// Email validation
|
||||||
if (fieldType === 'email') {
|
if (fieldType === 'email') {
|
||||||
validator = (value: string) => {
|
validator = (value: any) => {
|
||||||
if (required && (!value || value.trim() === '')) {
|
if (required && (!value || (typeof value === 'string' && value.trim() === ''))) {
|
||||||
return 'Email cannot be empty';
|
return 'Email cannot be empty';
|
||||||
}
|
}
|
||||||
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
if (value && typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
||||||
return 'Invalid email format';
|
return 'Invalid email format';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -386,11 +386,11 @@ export function useRbacRules() {
|
||||||
|
|
||||||
// Email validation
|
// Email validation
|
||||||
if (fieldType === 'email') {
|
if (fieldType === 'email') {
|
||||||
validator = (value: string) => {
|
validator = (value: any) => {
|
||||||
if (required && (!value || value.trim() === '')) {
|
if (required && (!value || (typeof value === 'string' && value.trim() === ''))) {
|
||||||
return 'Email cannot be empty';
|
return 'Email cannot be empty';
|
||||||
}
|
}
|
||||||
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
if (value && typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
||||||
return 'Invalid email format';
|
return 'Invalid email format';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -416,8 +416,8 @@ export function useRbacRules() {
|
||||||
}
|
}
|
||||||
// String validation for required fields
|
// String validation for required fields
|
||||||
else if (fieldType === 'string' && required) {
|
else if (fieldType === 'string' && required) {
|
||||||
validator = (value: string) => {
|
validator = (value: any) => {
|
||||||
if (!value || value.trim() === '') {
|
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
||||||
return `${attr.label} is required`;
|
return `${attr.label} is required`;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -565,6 +565,15 @@ export function useRbacRuleOperations() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Generic inline update handler for FormGeneratorTable
|
||||||
|
const handleInlineUpdate = async (ruleId: string, changes: Partial<RbacRuleUpdateData>) => {
|
||||||
|
const result = await handleRbacRuleUpdate(ruleId, changes as RbacRuleUpdateData);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Failed to update');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deletingRbacRules,
|
deletingRbacRules,
|
||||||
editingRbacRules,
|
editingRbacRules,
|
||||||
|
|
@ -575,6 +584,7 @@ export function useRbacRuleOperations() {
|
||||||
handleRbacRuleDelete,
|
handleRbacRuleDelete,
|
||||||
handleRbacRuleCreate,
|
handleRbacRuleCreate,
|
||||||
handleRbacRuleUpdate,
|
handleRbacRuleUpdate,
|
||||||
|
handleInlineUpdate,
|
||||||
isLoading
|
isLoading
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -247,11 +247,11 @@ export function useUserFiles() {
|
||||||
|
|
||||||
if (attr.name === 'fileName' || attr.name === 'file_name') {
|
if (attr.name === 'fileName' || attr.name === 'file_name') {
|
||||||
required = true;
|
required = true;
|
||||||
validator = (value: string) => {
|
validator = (value: any) => {
|
||||||
if (!value || value.trim() === '') {
|
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
||||||
return 'File name cannot be empty';
|
return 'File name cannot be empty';
|
||||||
}
|
}
|
||||||
if (value.length > 255) {
|
if (typeof value === 'string' && value.length > 255) {
|
||||||
return 'File name cannot exceed 255 characters';
|
return 'File name cannot exceed 255 characters';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -284,16 +284,12 @@ export function useUserFiles() {
|
||||||
}, [attributes, fetchAttributes]);
|
}, [attributes, fetchAttributes]);
|
||||||
|
|
||||||
// Fetch attributes and permissions on mount
|
// Fetch attributes and permissions on mount
|
||||||
|
// Note: Do NOT fetch files here - let the table component control pagination
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAttributes();
|
fetchAttributes();
|
||||||
fetchPermissions();
|
fetchPermissions();
|
||||||
}, [fetchAttributes, fetchPermissions]);
|
}, [fetchAttributes, fetchPermissions]);
|
||||||
|
|
||||||
// Initial fetch
|
|
||||||
useEffect(() => {
|
|
||||||
fetchFiles();
|
|
||||||
}, [fetchFiles]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: files,
|
data: files,
|
||||||
loading,
|
loading,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useCallback, useRef } from 'react';
|
||||||
import { useApiRequest } from './useApi';
|
import { useApiRequest } from './useApi';
|
||||||
import {
|
import {
|
||||||
fetchPermissions as fetchPermissionsApi,
|
fetchPermissions as fetchPermissionsApi,
|
||||||
|
fetchAllPermissions as fetchAllPermissionsApi,
|
||||||
type PermissionLevel,
|
type PermissionLevel,
|
||||||
type UserPermissions,
|
type UserPermissions,
|
||||||
type PermissionContext
|
type PermissionContext
|
||||||
|
|
@ -18,22 +19,30 @@ interface PermissionCache {
|
||||||
// Operation type for permission checks
|
// Operation type for permission checks
|
||||||
export type PermissionOperation = 'read' | 'create' | 'update' | 'delete';
|
export type PermissionOperation = 'read' | 'create' | 'update' | 'delete';
|
||||||
|
|
||||||
|
// Default permission (no access)
|
||||||
|
const DEFAULT_NO_ACCESS: UserPermissions = {
|
||||||
|
view: false,
|
||||||
|
read: 'n' as PermissionLevel,
|
||||||
|
create: 'n' as PermissionLevel,
|
||||||
|
update: 'n' as PermissionLevel,
|
||||||
|
delete: 'n' as PermissionLevel,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for managing RBAC permissions
|
* Hook for managing RBAC permissions
|
||||||
* Provides centralized permission checking with caching
|
* Provides centralized permission checking with caching
|
||||||
|
*
|
||||||
|
* Optimized to fetch all UI permissions in a single API call on first use,
|
||||||
|
* then serve subsequent requests from cache.
|
||||||
*/
|
*/
|
||||||
export const usePermissions = () => {
|
export const usePermissions = () => {
|
||||||
const [cache, setCache] = useState<PermissionCache>({});
|
|
||||||
const cacheRef = useRef<PermissionCache>({});
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const cacheRef = useRef<PermissionCache>({});
|
||||||
|
const bulkLoadPromiseRef = useRef<Promise<void> | null>(null);
|
||||||
|
const bulkLoadedContextsRef = useRef<Set<string>>(new Set());
|
||||||
const pendingRequests = useRef<Map<string, Promise<UserPermissions>>>(new Map());
|
const pendingRequests = useRef<Map<string, Promise<UserPermissions>>>(new Map());
|
||||||
const { request } = useApiRequest();
|
const { request } = useApiRequest();
|
||||||
|
|
||||||
// Keep cacheRef in sync with cache state
|
|
||||||
useEffect(() => {
|
|
||||||
cacheRef.current = cache;
|
|
||||||
}, [cache]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a cache key for a permission check
|
* Generate a cache key for a permission check
|
||||||
*/
|
*/
|
||||||
|
|
@ -42,39 +51,67 @@ export const usePermissions = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retry function with exponential backoff for 429 errors
|
* Load all permissions for a context (UI or RESOURCE) in bulk
|
||||||
|
* This is called once per context and caches all permissions
|
||||||
*/
|
*/
|
||||||
const retryWithBackoff = async (
|
const loadBulkPermissions = useCallback(async (context: 'UI' | 'RESOURCE'): Promise<void> => {
|
||||||
fn: () => Promise<any>,
|
// Skip if already loaded for this context
|
||||||
maxRetries = 3,
|
if (bulkLoadedContextsRef.current.has(context)) {
|
||||||
baseDelay = 1000
|
return;
|
||||||
): Promise<any> => {
|
|
||||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
||||||
try {
|
|
||||||
return await fn();
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.response?.status === 429 && attempt < maxRetries - 1) {
|
|
||||||
const delay = baseDelay * Math.pow(2, attempt);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
// Check if there's already a pending bulk load
|
||||||
|
if (bulkLoadPromiseRef.current) {
|
||||||
|
await bulkLoadPromiseRef.current;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the bulk load promise
|
||||||
|
bulkLoadPromiseRef.current = (async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
console.log(`🔐 usePermissions: Bulk loading all ${context} permissions...`);
|
||||||
|
const response = await fetchAllPermissionsApi(request, context);
|
||||||
|
|
||||||
|
// Cache all permissions from the response
|
||||||
|
const contextKey = context.toLowerCase() as 'ui' | 'resource';
|
||||||
|
const permissions = response[contextKey] || {};
|
||||||
|
|
||||||
|
const newCache: PermissionCache = { ...cacheRef.current };
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const [item, perm] of Object.entries(permissions)) {
|
||||||
|
const key = getPermissionKey(context, item);
|
||||||
|
newCache[key] = perm;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheRef.current = newCache;
|
||||||
|
bulkLoadedContextsRef.current.add(context);
|
||||||
|
|
||||||
|
console.log(`✅ usePermissions: Bulk loaded ${count} ${context} permissions`);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`❌ usePermissions: Error bulk loading ${context} permissions:`, error);
|
||||||
|
// Don't mark as loaded on error - allow retry
|
||||||
|
} finally {
|
||||||
|
bulkLoadPromiseRef.current = null;
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
await bulkLoadPromiseRef.current;
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check permissions for a given context and item
|
* Fetch individual permission (used for DATA context and fallback)
|
||||||
* Returns full UserPermissions object
|
|
||||||
* Checks cache first, then fetches from backend if not cached
|
|
||||||
*/
|
*/
|
||||||
const checkPermission = useCallback(async (
|
const fetchIndividualPermission = useCallback(async (
|
||||||
context: PermissionContext,
|
context: PermissionContext,
|
||||||
item?: string
|
item?: string
|
||||||
): Promise<UserPermissions> => {
|
): Promise<UserPermissions> => {
|
||||||
const key = getPermissionKey(context, item);
|
const key = getPermissionKey(context, item);
|
||||||
|
|
||||||
// Check cache first using ref to avoid stale closures
|
// Check cache first
|
||||||
if (cacheRef.current[key]) {
|
if (cacheRef.current[key]) {
|
||||||
return cacheRef.current[key];
|
return cacheRef.current[key];
|
||||||
}
|
}
|
||||||
|
|
@ -84,76 +121,21 @@ export const usePermissions = () => {
|
||||||
return pendingRequests.current.get(key)!;
|
return pendingRequests.current.get(key)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new request
|
// Fetch individual permission
|
||||||
const requestPromise = (async () => {
|
const requestPromise = (async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// Use retry logic for 429 errors
|
const permissions = await fetchPermissionsApi(request, context, item);
|
||||||
// Note: We wrap the API call in retry logic since useApiRequest doesn't handle 429 retries
|
|
||||||
console.log('🔐 usePermissions: Checking permissions for:', { context, item, cacheKey: key });
|
// Update cache
|
||||||
|
cacheRef.current = { ...cacheRef.current, [key]: permissions };
|
||||||
|
|
||||||
const permissions = await retryWithBackoff(async () => {
|
|
||||||
try {
|
|
||||||
const result = await fetchPermissionsApi(request, context, item);
|
|
||||||
console.log('✅ usePermissions: Received permissions response:', {
|
|
||||||
context,
|
|
||||||
item,
|
|
||||||
permissions: result,
|
|
||||||
view: result?.view,
|
|
||||||
viewType: typeof result?.view,
|
|
||||||
viewValue: result?.view,
|
|
||||||
read: result?.read,
|
|
||||||
create: result?.create,
|
|
||||||
update: result?.update,
|
|
||||||
delete: result?.delete,
|
|
||||||
isArray: Array.isArray(result),
|
|
||||||
keys: result ? Object.keys(result) : [],
|
|
||||||
fullResponse: JSON.stringify(result, null, 2)
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('❌ usePermissions: Error fetching permissions:', {
|
|
||||||
context,
|
|
||||||
item,
|
|
||||||
error: error.message,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
fullError: error
|
|
||||||
});
|
|
||||||
// If useApiRequest throws, we need to check if it's a 429
|
|
||||||
// For now, we'll let the retry logic handle it
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update cache after fetching from backend
|
|
||||||
setCache(prev => {
|
|
||||||
const newCache = { ...prev, [key]: permissions };
|
|
||||||
cacheRef.current = newCache;
|
|
||||||
console.log('💾 usePermissions: Cached permissions:', { context, item, permissions });
|
|
||||||
return newCache;
|
|
||||||
});
|
|
||||||
|
|
||||||
return permissions;
|
return permissions;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Only log non-429 errors to avoid spam
|
console.error('Error checking permissions:', error);
|
||||||
if (error.response?.status !== 429) {
|
|
||||||
console.error('Error checking permissions:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return cached value if available, otherwise default (no access)
|
// Return cached value if available, otherwise default (no access)
|
||||||
const cached = cacheRef.current[key];
|
return cacheRef.current[key] || DEFAULT_NO_ACCESS;
|
||||||
if (cached) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
view: false,
|
|
||||||
read: 'n' as PermissionLevel,
|
|
||||||
create: 'n' as PermissionLevel,
|
|
||||||
update: 'n' as PermissionLevel,
|
|
||||||
delete: 'n' as PermissionLevel,
|
|
||||||
};
|
|
||||||
} finally {
|
} finally {
|
||||||
pendingRequests.current.delete(key);
|
pendingRequests.current.delete(key);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -166,6 +148,39 @@ export const usePermissions = () => {
|
||||||
return requestPromise;
|
return requestPromise;
|
||||||
}, [request]);
|
}, [request]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check permissions for a given context and item
|
||||||
|
* Returns full UserPermissions object
|
||||||
|
*
|
||||||
|
* For UI/RESOURCE contexts: Uses bulk-loaded cache, falls back to individual fetch
|
||||||
|
* For DATA context: Fetches individually (as items are dynamic)
|
||||||
|
*/
|
||||||
|
const checkPermission = useCallback(async (
|
||||||
|
context: PermissionContext,
|
||||||
|
item?: string
|
||||||
|
): Promise<UserPermissions> => {
|
||||||
|
const key = getPermissionKey(context, item);
|
||||||
|
|
||||||
|
// For UI and RESOURCE contexts, try bulk loading first
|
||||||
|
if (context === 'UI' || context === 'RESOURCE') {
|
||||||
|
// Ensure bulk permissions are loaded
|
||||||
|
await loadBulkPermissions(context);
|
||||||
|
|
||||||
|
// Check cache after bulk load
|
||||||
|
if (cacheRef.current[key]) {
|
||||||
|
return cacheRef.current[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not in bulk cache, fall back to individual fetch
|
||||||
|
// (item may not have explicit rule, but backend will calculate effective permissions)
|
||||||
|
console.log(`⚠️ usePermissions: ${context}:${item} not in bulk cache, fetching individually`);
|
||||||
|
return fetchIndividualPermission(context, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For DATA context, fetch individually
|
||||||
|
return fetchIndividualPermission(context, item);
|
||||||
|
}, [loadBulkPermissions, fetchIndividualPermission]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if user has permission for a specific operation
|
* Check if user has permission for a specific operation
|
||||||
* Returns true if user has any level of permission (not 'n')
|
* Returns true if user has any level of permission (not 'n')
|
||||||
|
|
@ -197,35 +212,25 @@ export const usePermissions = () => {
|
||||||
context: PermissionContext,
|
context: PermissionContext,
|
||||||
item: string
|
item: string
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
console.log('👁️ canView: Checking view access for:', { context, item });
|
|
||||||
const permissions = await checkPermission(context, item);
|
const permissions = await checkPermission(context, item);
|
||||||
const hasAccess = permissions.view === true;
|
return permissions.view === true;
|
||||||
console.log('👁️ canView: Result:', {
|
|
||||||
context,
|
|
||||||
item,
|
|
||||||
hasAccess,
|
|
||||||
viewPermission: permissions.view,
|
|
||||||
viewPermissionType: typeof permissions.view,
|
|
||||||
viewPermissionValue: permissions.view,
|
|
||||||
allPermissions: {
|
|
||||||
view: permissions.view,
|
|
||||||
read: permissions.read,
|
|
||||||
create: permissions.create,
|
|
||||||
update: permissions.update,
|
|
||||||
delete: permissions.delete
|
|
||||||
},
|
|
||||||
fullPermissionsObject: JSON.stringify(permissions, null, 2)
|
|
||||||
});
|
|
||||||
return hasAccess;
|
|
||||||
}, [checkPermission]);
|
}, [checkPermission]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preload all permissions for UI context
|
||||||
|
* Call this early in the app lifecycle to warm the cache
|
||||||
|
*/
|
||||||
|
const preloadUiPermissions = useCallback(async (): Promise<void> => {
|
||||||
|
await loadBulkPermissions('UI');
|
||||||
|
}, [loadBulkPermissions]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the permission cache
|
* Clear the permission cache
|
||||||
* Useful when user permissions change or after logout
|
* Useful when user permissions change or after logout
|
||||||
*/
|
*/
|
||||||
const clearCache = useCallback(() => {
|
const clearCache = useCallback(() => {
|
||||||
setCache({});
|
|
||||||
cacheRef.current = {};
|
cacheRef.current = {};
|
||||||
|
bulkLoadedContextsRef.current.clear();
|
||||||
pendingRequests.current.clear();
|
pendingRequests.current.clear();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -233,6 +238,7 @@ export const usePermissions = () => {
|
||||||
checkPermission,
|
checkPermission,
|
||||||
hasPermission,
|
hasPermission,
|
||||||
canView,
|
canView,
|
||||||
|
preloadUiPermissions,
|
||||||
loading,
|
loading,
|
||||||
clearCache,
|
clearCache,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -235,11 +235,11 @@ export function usePrompts() {
|
||||||
// Match create button configuration for prompts
|
// Match create button configuration for prompts
|
||||||
if (attr.name === 'name') {
|
if (attr.name === 'name') {
|
||||||
required = true;
|
required = true;
|
||||||
validator = (value: string) => {
|
validator = (value: any) => {
|
||||||
if (!value || value.trim() === '') {
|
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
||||||
return 'Prompt name cannot be empty';
|
return 'Prompt name cannot be empty';
|
||||||
}
|
}
|
||||||
if (value.length > 100) {
|
if (typeof value === 'string' && value.length > 100) {
|
||||||
return 'Prompt name cannot exceed 100 characters';
|
return 'Prompt name cannot exceed 100 characters';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -248,11 +248,11 @@ export function usePrompts() {
|
||||||
required = true;
|
required = true;
|
||||||
minRows = 6; // Match create button: minRows: 6
|
minRows = 6; // Match create button: minRows: 6
|
||||||
maxRows = 12; // Match create button: maxRows: 12
|
maxRows = 12; // Match create button: maxRows: 12
|
||||||
validator = (value: string) => {
|
validator = (value: any) => {
|
||||||
if (!value || value.trim() === '') {
|
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
||||||
return 'Prompt content cannot be empty';
|
return 'Prompt content cannot be empty';
|
||||||
}
|
}
|
||||||
if (value.length > 10000) {
|
if (typeof value === 'string' && value.length > 10000) {
|
||||||
return 'Prompt content cannot exceed 10,000 characters';
|
return 'Prompt content cannot exceed 10,000 characters';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -302,11 +302,11 @@ export function usePrompts() {
|
||||||
|
|
||||||
if (attr.name === 'name') {
|
if (attr.name === 'name') {
|
||||||
required = true;
|
required = true;
|
||||||
validator = (value: string) => {
|
validator = (value: any) => {
|
||||||
if (!value || value.trim() === '') {
|
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
||||||
return 'Prompt name cannot be empty';
|
return 'Prompt name cannot be empty';
|
||||||
}
|
}
|
||||||
if (value.length > 100) {
|
if (typeof value === 'string' && value.length > 100) {
|
||||||
return 'Prompt name cannot exceed 100 characters';
|
return 'Prompt name cannot exceed 100 characters';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -315,11 +315,11 @@ export function usePrompts() {
|
||||||
required = true;
|
required = true;
|
||||||
minRows = 6; // Match create button
|
minRows = 6; // Match create button
|
||||||
maxRows = 12; // Match create button
|
maxRows = 12; // Match create button
|
||||||
validator = (value: string) => {
|
validator = (value: any) => {
|
||||||
if (!value || value.trim() === '') {
|
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
||||||
return 'Prompt content cannot be empty';
|
return 'Prompt content cannot be empty';
|
||||||
}
|
}
|
||||||
if (value.length > 10000) {
|
if (typeof value === 'string' && value.length > 10000) {
|
||||||
return 'Prompt content cannot exceed 10,000 characters';
|
return 'Prompt content cannot exceed 10,000 characters';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -342,6 +342,95 @@ export function usePrompts() {
|
||||||
return editableFields;
|
return editableFields;
|
||||||
}, [attributes]);
|
}, [attributes]);
|
||||||
|
|
||||||
|
// Generate create fields from attributes dynamically
|
||||||
|
// For prompts, the create form is essentially the same as edit form
|
||||||
|
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
|
||||||
|
let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string';
|
||||||
|
let minRows: number | undefined = undefined;
|
||||||
|
let maxRows: number | undefined = undefined;
|
||||||
|
|
||||||
|
// Map backend types to form field types
|
||||||
|
// Cast to string to handle all possible backend type values
|
||||||
|
const attrType = attr.type as string;
|
||||||
|
if (attrType === 'checkbox' || attrType === 'boolean') {
|
||||||
|
fieldType = 'boolean';
|
||||||
|
} else if (attrType === 'email') {
|
||||||
|
fieldType = 'email';
|
||||||
|
} else if (attrType === 'timestamp' || attrType === 'date' || attrType === 'time') {
|
||||||
|
fieldType = 'date';
|
||||||
|
} else if (attrType === 'textarea') {
|
||||||
|
fieldType = 'textarea';
|
||||||
|
// Set default rows for textarea fields
|
||||||
|
minRows = 6;
|
||||||
|
maxRows = 12;
|
||||||
|
} else if (attr.name === 'content' || attr.name.toLowerCase().includes('content')) {
|
||||||
|
// Content fields should be textarea
|
||||||
|
fieldType = 'textarea';
|
||||||
|
minRows = 6;
|
||||||
|
maxRows = 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if required and build validator
|
||||||
|
const required = attr.required === true;
|
||||||
|
let validator: ((value: any) => string | null) | undefined = undefined;
|
||||||
|
|
||||||
|
// Required string validation
|
||||||
|
if (required && (fieldType === 'string' || fieldType === 'textarea')) {
|
||||||
|
validator = (value: any) => {
|
||||||
|
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
||||||
|
return `${attr.label} is required`;
|
||||||
|
}
|
||||||
|
if (attr.name === 'name' && typeof value === 'string' && value.length > 100) {
|
||||||
|
return 'Prompt name cannot exceed 100 characters';
|
||||||
|
}
|
||||||
|
if (attr.name === 'content' && typeof value === 'string' && value.length > 10000) {
|
||||||
|
return 'Prompt content cannot exceed 10,000 characters';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: attr.name,
|
||||||
|
label: attr.label || attr.name,
|
||||||
|
type: fieldType,
|
||||||
|
required,
|
||||||
|
validator,
|
||||||
|
minRows,
|
||||||
|
maxRows
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return createFields;
|
||||||
|
}, [attributes]);
|
||||||
|
|
||||||
// Ensure attributes are loaded - can be called by EditActionButton
|
// Ensure attributes are loaded - can be called by EditActionButton
|
||||||
const ensureAttributesLoaded = useCallback(async () => {
|
const ensureAttributesLoaded = useCallback(async () => {
|
||||||
// If attributes are already loaded, return them
|
// If attributes are already loaded, return them
|
||||||
|
|
@ -377,6 +466,7 @@ export function usePrompts() {
|
||||||
pagination,
|
pagination,
|
||||||
fetchPromptById,
|
fetchPromptById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded // Generic function to ensure attributes are loaded
|
ensureAttributesLoaded // Generic function to ensure attributes are loaded
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -474,6 +564,15 @@ export function usePromptOperations() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Generic inline update handler for FormGeneratorTable
|
||||||
|
const handleInlineUpdate = async (promptId: string, changes: Partial<{ name: string; content: string }>) => {
|
||||||
|
const result = await handlePromptUpdate(promptId, changes as { name: string; content: string });
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Failed to update');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deletingPrompts,
|
deletingPrompts,
|
||||||
creatingPrompt,
|
creatingPrompt,
|
||||||
|
|
@ -483,6 +582,7 @@ export function usePromptOperations() {
|
||||||
handlePromptDelete,
|
handlePromptDelete,
|
||||||
handlePromptCreate,
|
handlePromptCreate,
|
||||||
handlePromptUpdate,
|
handlePromptUpdate,
|
||||||
|
handleInlineUpdate,
|
||||||
isLoading
|
isLoading
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
484
src/hooks/useTrustee.ts
Normal file
484
src/hooks/useTrustee.ts
Normal file
|
|
@ -0,0 +1,484 @@
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useApiRequest } from './useApi';
|
||||||
|
import { getUserDataCache } from '../utils/userCache';
|
||||||
|
import api from '../api';
|
||||||
|
import { usePermissions, type UserPermissions } from './usePermissions';
|
||||||
|
import {
|
||||||
|
// Types
|
||||||
|
type TrusteeOrganisation,
|
||||||
|
type TrusteeRole,
|
||||||
|
type TrusteeAccess,
|
||||||
|
type TrusteeContract,
|
||||||
|
type TrusteeDocument,
|
||||||
|
type TrusteePosition,
|
||||||
|
type TrusteePositionDocument,
|
||||||
|
type PaginationParams,
|
||||||
|
// Organisation API
|
||||||
|
fetchOrganisations as fetchOrganisationsApi,
|
||||||
|
fetchOrganisationById as fetchOrganisationByIdApi,
|
||||||
|
createOrganisation as createOrganisationApi,
|
||||||
|
updateOrganisation as updateOrganisationApi,
|
||||||
|
deleteOrganisation as deleteOrganisationApi,
|
||||||
|
// Role API
|
||||||
|
fetchRoles as fetchRolesApi,
|
||||||
|
fetchRoleById as fetchRoleByIdApi,
|
||||||
|
createRole as createRoleApi,
|
||||||
|
updateRole as updateRoleApi,
|
||||||
|
deleteRole as deleteRoleApi,
|
||||||
|
// Access API
|
||||||
|
fetchAccess as fetchAccessApi,
|
||||||
|
fetchAccessById as fetchAccessByIdApi,
|
||||||
|
createAccess as createAccessApi,
|
||||||
|
updateAccess as updateAccessApi,
|
||||||
|
deleteAccess as deleteAccessApi,
|
||||||
|
// Contract API
|
||||||
|
fetchContracts as fetchContractsApi,
|
||||||
|
fetchContractById as fetchContractByIdApi,
|
||||||
|
createContract as createContractApi,
|
||||||
|
updateContract as updateContractApi,
|
||||||
|
deleteContract as deleteContractApi,
|
||||||
|
// Document API
|
||||||
|
fetchDocuments as fetchDocumentsApi,
|
||||||
|
fetchDocumentById as fetchDocumentByIdApi,
|
||||||
|
createDocument as createDocumentApi,
|
||||||
|
updateDocument as updateDocumentApi,
|
||||||
|
deleteDocument as deleteDocumentApi,
|
||||||
|
// Position API
|
||||||
|
fetchPositions as fetchPositionsApi,
|
||||||
|
fetchPositionById as fetchPositionByIdApi,
|
||||||
|
createPosition as createPositionApi,
|
||||||
|
updatePosition as updatePositionApi,
|
||||||
|
deletePosition as deletePositionApi,
|
||||||
|
// Position-Document API
|
||||||
|
fetchPositionDocuments as fetchPositionDocumentsApi,
|
||||||
|
createPositionDocument as createPositionDocumentApi,
|
||||||
|
deletePositionDocument as deletePositionDocumentApi,
|
||||||
|
} from '../api/trusteeApi';
|
||||||
|
|
||||||
|
// Re-export types
|
||||||
|
export type {
|
||||||
|
TrusteeOrganisation,
|
||||||
|
TrusteeRole,
|
||||||
|
TrusteeAccess,
|
||||||
|
TrusteeContract,
|
||||||
|
TrusteeDocument,
|
||||||
|
TrusteePosition,
|
||||||
|
TrusteePositionDocument,
|
||||||
|
PaginationParams
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AttributeDefinition {
|
||||||
|
name: string;
|
||||||
|
type: 'text' | 'email' | 'date' | 'checkbox' | 'select' | 'multiselect' | 'number' | 'textarea' | 'timestamp' | 'file';
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
required?: boolean;
|
||||||
|
default?: any;
|
||||||
|
options?: any[] | string;
|
||||||
|
readonly?: boolean;
|
||||||
|
editable?: boolean;
|
||||||
|
visible?: boolean;
|
||||||
|
order?: number;
|
||||||
|
sortable?: boolean;
|
||||||
|
filterable?: boolean;
|
||||||
|
searchable?: boolean;
|
||||||
|
width?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
maxWidth?: number;
|
||||||
|
filterOptions?: string[];
|
||||||
|
dependsOn?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GENERIC TRUSTEE ENTITY HOOK FACTORY
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface TrusteeEntityConfig<T> {
|
||||||
|
entityName: string;
|
||||||
|
fetchAll: (request: any, params?: PaginationParams) => Promise<any>;
|
||||||
|
fetchById: (request: any, id: string) => Promise<T | null>;
|
||||||
|
create: (request: any, data: Partial<T>) => Promise<T>;
|
||||||
|
update: (request: any, id: string, data: Partial<T>) => Promise<T>;
|
||||||
|
deleteItem: (request: any, id: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntityConfig<T>) {
|
||||||
|
return function useTrusteeEntity() {
|
||||||
|
const [items, setItems] = useState<T[]>([]);
|
||||||
|
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, T[]>();
|
||||||
|
const { checkPermission } = usePermissions();
|
||||||
|
|
||||||
|
const fetchAttributes = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/api/attributes/${config.entityName}`);
|
||||||
|
let attrs: AttributeDefinition[] = [];
|
||||||
|
if (response.data?.attributes && Array.isArray(response.data.attributes)) {
|
||||||
|
attrs = response.data.attributes;
|
||||||
|
} else if (Array.isArray(response.data)) {
|
||||||
|
attrs = response.data;
|
||||||
|
}
|
||||||
|
setAttributes(attrs);
|
||||||
|
return attrs;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Error fetching ${config.entityName} attributes:`, error);
|
||||||
|
setAttributes([]);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchPermissions = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const perms = await checkPermission('DATA', config.entityName);
|
||||||
|
setPermissions(perms);
|
||||||
|
return perms;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Error fetching ${config.entityName} permissions:`, error);
|
||||||
|
const defaultPerms: UserPermissions = {
|
||||||
|
view: false,
|
||||||
|
read: 'n',
|
||||||
|
create: 'n',
|
||||||
|
update: 'n',
|
||||||
|
delete: 'n',
|
||||||
|
};
|
||||||
|
setPermissions(defaultPerms);
|
||||||
|
return defaultPerms;
|
||||||
|
}
|
||||||
|
}, [checkPermission]);
|
||||||
|
|
||||||
|
const fetchItems = useCallback(async (params?: PaginationParams) => {
|
||||||
|
try {
|
||||||
|
const data = await config.fetchAll(request, params);
|
||||||
|
|
||||||
|
if (data && typeof data === 'object' && 'items' in data) {
|
||||||
|
const fetchedItems = Array.isArray(data.items) ? data.items : [];
|
||||||
|
setItems(fetchedItems);
|
||||||
|
if (data.pagination) {
|
||||||
|
setPagination(data.pagination);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const fetchedItems = Array.isArray(data) ? data : [];
|
||||||
|
setItems(fetchedItems);
|
||||||
|
setPagination(null);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
setItems([]);
|
||||||
|
setPagination(null);
|
||||||
|
}
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const removeOptimistically = (itemId: string) => {
|
||||||
|
setItems(prev => prev.filter(item => item.id !== itemId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateOptimistically = (itemId: string, updateData: Partial<T>) => {
|
||||||
|
setItems(prev =>
|
||||||
|
prev.map(item =>
|
||||||
|
item.id === itemId
|
||||||
|
? { ...item, ...updateData }
|
||||||
|
: item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchById = useCallback(async (itemId: string): Promise<T | null> => {
|
||||||
|
return await config.fetchById(request, itemId);
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const generateEditFieldsFromAttributes = useCallback(() => {
|
||||||
|
if (!attributes || attributes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
.filter(attr => {
|
||||||
|
if (attr.readonly === true || attr.editable === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const nonEditableFields = ['_createdBy', '_createdAt', '_modifiedBy', '_modifiedAt'];
|
||||||
|
return !nonEditableFields.includes(attr.name);
|
||||||
|
})
|
||||||
|
.map(attr => {
|
||||||
|
let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' | 'number' = 'string';
|
||||||
|
let options: Array<{ value: string | number; label: string }> | undefined = undefined;
|
||||||
|
let optionsReference: string | undefined = undefined;
|
||||||
|
|
||||||
|
if (attr.type === 'checkbox') {
|
||||||
|
fieldType = 'boolean';
|
||||||
|
} else if (attr.type === 'email') {
|
||||||
|
fieldType = 'email';
|
||||||
|
} else if (attr.type === 'date') {
|
||||||
|
fieldType = 'date';
|
||||||
|
} else if (attr.type === 'number') {
|
||||||
|
fieldType = 'number';
|
||||||
|
} else if (attr.type === 'select') {
|
||||||
|
fieldType = 'enum';
|
||||||
|
if (Array.isArray(attr.options)) {
|
||||||
|
options = attr.options.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 attr.options === 'string') {
|
||||||
|
optionsReference = attr.options;
|
||||||
|
}
|
||||||
|
} else if (attr.type === 'multiselect') {
|
||||||
|
fieldType = 'multiselect';
|
||||||
|
if (Array.isArray(attr.options)) {
|
||||||
|
options = attr.options.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 attr.options === 'string') {
|
||||||
|
optionsReference = attr.options;
|
||||||
|
}
|
||||||
|
} else if (attr.type === 'textarea') {
|
||||||
|
fieldType = 'textarea';
|
||||||
|
} else if (attr.type === 'timestamp') {
|
||||||
|
fieldType = 'readonly';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: attr.name,
|
||||||
|
label: attr.label || attr.name,
|
||||||
|
type: fieldType,
|
||||||
|
editable: attr.editable !== false && attr.readonly !== true,
|
||||||
|
required: attr.required === true,
|
||||||
|
options,
|
||||||
|
optionsReference,
|
||||||
|
dependsOn: attr.dependsOn
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [attributes]);
|
||||||
|
|
||||||
|
const ensureAttributesLoaded = useCallback(async () => {
|
||||||
|
if (attributes && attributes.length > 0) {
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
return await fetchAttributes();
|
||||||
|
}, [attributes, fetchAttributes]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAttributes();
|
||||||
|
fetchPermissions();
|
||||||
|
}, [fetchAttributes, fetchPermissions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchItems();
|
||||||
|
}, [fetchItems]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch: fetchItems,
|
||||||
|
removeOptimistically,
|
||||||
|
updateOptimistically,
|
||||||
|
attributes,
|
||||||
|
permissions,
|
||||||
|
pagination,
|
||||||
|
fetchById,
|
||||||
|
generateEditFieldsFromAttributes,
|
||||||
|
ensureAttributesLoaded
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function _createTrusteeOperationsHook<T extends { id: string }>(config: TrusteeEntityConfig<T>) {
|
||||||
|
return function useTrusteeEntityOperations() {
|
||||||
|
const [deletingItems, setDeletingItems] = useState<Set<string>>(new Set());
|
||||||
|
const [creatingItem, setCreatingItem] = 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 handleDelete = async (itemId: string) => {
|
||||||
|
setDeleteError(null);
|
||||||
|
setDeletingItems(prev => new Set(prev).add(itemId));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await config.deleteItem(request, itemId);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
setDeleteError(error.message);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setDeletingItems(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(itemId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = async (itemData: Partial<T>) => {
|
||||||
|
setCreateError(null);
|
||||||
|
setCreatingItem(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newItem = await config.create(request, itemData);
|
||||||
|
return { success: true, data: newItem };
|
||||||
|
} catch (error: any) {
|
||||||
|
setCreateError(error.message);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
} finally {
|
||||||
|
setCreatingItem(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = async (itemId: string, updateData: Partial<T>) => {
|
||||||
|
setUpdateError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedItem = await config.update(request, itemId, updateData);
|
||||||
|
return { success: true, data: updatedItem };
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMessage = error.response?.data?.message || error.message || 'Failed to update';
|
||||||
|
setUpdateError(errorMessage);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
statusCode: error.response?.status,
|
||||||
|
isPermissionError: error.response?.status === 403,
|
||||||
|
isValidationError: error.response?.status === 400
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletingItems,
|
||||||
|
creatingItem,
|
||||||
|
deleteError,
|
||||||
|
createError,
|
||||||
|
updateError,
|
||||||
|
handleDelete,
|
||||||
|
handleCreate,
|
||||||
|
handleUpdate,
|
||||||
|
isLoading
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ORGANISATION HOOKS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const organisationConfig: TrusteeEntityConfig<TrusteeOrganisation> = {
|
||||||
|
entityName: 'TrusteeOrganisation',
|
||||||
|
fetchAll: fetchOrganisationsApi,
|
||||||
|
fetchById: fetchOrganisationByIdApi,
|
||||||
|
create: createOrganisationApi,
|
||||||
|
update: updateOrganisationApi,
|
||||||
|
deleteItem: deleteOrganisationApi
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTrusteeOrganisations = _createTrusteeEntityHook(organisationConfig);
|
||||||
|
export const useTrusteeOrganisationOperations = _createTrusteeOperationsHook(organisationConfig);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ROLE HOOKS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const roleConfig: TrusteeEntityConfig<TrusteeRole> = {
|
||||||
|
entityName: 'TrusteeRole',
|
||||||
|
fetchAll: fetchRolesApi,
|
||||||
|
fetchById: fetchRoleByIdApi,
|
||||||
|
create: createRoleApi,
|
||||||
|
update: updateRoleApi,
|
||||||
|
deleteItem: deleteRoleApi
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTrusteeRoles = _createTrusteeEntityHook(roleConfig);
|
||||||
|
export const useTrusteeRoleOperations = _createTrusteeOperationsHook(roleConfig);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ACCESS HOOKS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const accessConfig: TrusteeEntityConfig<TrusteeAccess> = {
|
||||||
|
entityName: 'TrusteeAccess',
|
||||||
|
fetchAll: fetchAccessApi,
|
||||||
|
fetchById: fetchAccessByIdApi,
|
||||||
|
create: createAccessApi,
|
||||||
|
update: updateAccessApi,
|
||||||
|
deleteItem: deleteAccessApi
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTrusteeAccess = _createTrusteeEntityHook(accessConfig);
|
||||||
|
export const useTrusteeAccessOperations = _createTrusteeOperationsHook(accessConfig);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CONTRACT HOOKS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const contractConfig: TrusteeEntityConfig<TrusteeContract> = {
|
||||||
|
entityName: 'TrusteeContract',
|
||||||
|
fetchAll: fetchContractsApi,
|
||||||
|
fetchById: fetchContractByIdApi,
|
||||||
|
create: createContractApi,
|
||||||
|
update: updateContractApi,
|
||||||
|
deleteItem: deleteContractApi
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTrusteeContracts = _createTrusteeEntityHook(contractConfig);
|
||||||
|
export const useTrusteeContractOperations = _createTrusteeOperationsHook(contractConfig);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DOCUMENT HOOKS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const documentConfig: TrusteeEntityConfig<TrusteeDocument> = {
|
||||||
|
entityName: 'TrusteeDocument',
|
||||||
|
fetchAll: fetchDocumentsApi,
|
||||||
|
fetchById: fetchDocumentByIdApi,
|
||||||
|
create: createDocumentApi,
|
||||||
|
update: updateDocumentApi,
|
||||||
|
deleteItem: deleteDocumentApi
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTrusteeDocuments = _createTrusteeEntityHook(documentConfig);
|
||||||
|
export const useTrusteeDocumentOperations = _createTrusteeOperationsHook(documentConfig);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// POSITION HOOKS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const positionConfig: TrusteeEntityConfig<TrusteePosition> = {
|
||||||
|
entityName: 'TrusteePosition',
|
||||||
|
fetchAll: fetchPositionsApi,
|
||||||
|
fetchById: fetchPositionByIdApi,
|
||||||
|
create: createPositionApi,
|
||||||
|
update: updatePositionApi,
|
||||||
|
deleteItem: deletePositionApi
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTrusteePositions = _createTrusteeEntityHook(positionConfig);
|
||||||
|
export const useTrusteePositionOperations = _createTrusteeOperationsHook(positionConfig);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// POSITION-DOCUMENT HOOKS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const positionDocumentConfig: TrusteeEntityConfig<TrusteePositionDocument> = {
|
||||||
|
entityName: 'TrusteePositionDocument',
|
||||||
|
fetchAll: fetchPositionDocumentsApi,
|
||||||
|
fetchById: async () => null, // Not typically needed
|
||||||
|
create: createPositionDocumentApi,
|
||||||
|
update: async () => { throw new Error('Update not supported for position-document links'); },
|
||||||
|
deleteItem: deletePositionDocumentApi
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTrusteePositionDocuments = _createTrusteeEntityHook(positionDocumentConfig);
|
||||||
|
export const useTrusteePositionDocumentOperations = _createTrusteeOperationsHook(positionDocumentConfig);
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
createUser as createUserApi,
|
createUser as createUserApi,
|
||||||
updateUser as updateUserApi,
|
updateUser as updateUserApi,
|
||||||
deleteUser as deleteUserApi,
|
deleteUser as deleteUserApi,
|
||||||
|
sendPasswordLink as sendPasswordLinkApi,
|
||||||
type User,
|
type User,
|
||||||
type UserUpdateData,
|
type UserUpdateData,
|
||||||
type AttributeDefinition,
|
type AttributeDefinition,
|
||||||
|
|
@ -579,11 +580,11 @@ export function useOrgUsers() {
|
||||||
|
|
||||||
// Email validation
|
// Email validation
|
||||||
if (fieldType === 'email') {
|
if (fieldType === 'email') {
|
||||||
validator = (value: string) => {
|
validator = (value: any) => {
|
||||||
if (required && (!value || value.trim() === '')) {
|
if (required && (!value || (typeof value === 'string' && value.trim() === ''))) {
|
||||||
return 'Email cannot be empty';
|
return 'Email cannot be empty';
|
||||||
}
|
}
|
||||||
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
if (value && typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
||||||
return 'Invalid email format';
|
return 'Invalid email format';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -625,6 +626,135 @@ export function useOrgUsers() {
|
||||||
return editableFields;
|
return editableFields;
|
||||||
}, [attributes]);
|
}, [attributes]);
|
||||||
|
|
||||||
|
// Generate create fields from attributes dynamically
|
||||||
|
// For users, we add a password field that's not in the backend attributes (since passwords are hashed)
|
||||||
|
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', 'authenticationAuthority'];
|
||||||
|
return !nonEditableFields.includes(attr.name);
|
||||||
|
})
|
||||||
|
.map(attr => {
|
||||||
|
// Map backend attribute type to form field type
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Map backend types to form field types
|
||||||
|
// Cast to string to handle all possible backend type values
|
||||||
|
const attrType = attr.type as string;
|
||||||
|
if (attrType === 'checkbox' || attrType === 'boolean') {
|
||||||
|
fieldType = 'boolean';
|
||||||
|
} else if (attrType === 'email') {
|
||||||
|
fieldType = 'email';
|
||||||
|
} else if (attrType === 'timestamp' || attrType === 'date' || attrType === 'time') {
|
||||||
|
fieldType = 'date';
|
||||||
|
} else if (attrType === 'select' || attrType === 'enum') {
|
||||||
|
fieldType = 'enum';
|
||||||
|
if (Array.isArray(attr.options)) {
|
||||||
|
options = attr.options.map(opt => {
|
||||||
|
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 attr.options === 'string') {
|
||||||
|
optionsReference = attr.options;
|
||||||
|
}
|
||||||
|
} else if (attrType === 'multiselect') {
|
||||||
|
fieldType = 'multiselect';
|
||||||
|
if (Array.isArray(attr.options)) {
|
||||||
|
options = attr.options.map(opt => {
|
||||||
|
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 attr.options === 'string') {
|
||||||
|
optionsReference = attr.options;
|
||||||
|
}
|
||||||
|
} else if (attrType === 'textarea') {
|
||||||
|
fieldType = 'textarea';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if required and build validator
|
||||||
|
const required = attr.required === true;
|
||||||
|
let validator: ((value: any) => string | null) | undefined = undefined;
|
||||||
|
|
||||||
|
// Email validation
|
||||||
|
if (attr.type === 'email') {
|
||||||
|
validator = (value: any) => {
|
||||||
|
if (required && (!value || (typeof value === 'string' && value.trim() === ''))) {
|
||||||
|
return 'Email cannot be empty';
|
||||||
|
}
|
||||||
|
if (value && typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
||||||
|
return 'Invalid email format';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Required string validation
|
||||||
|
else if (required && fieldType === 'string') {
|
||||||
|
validator = (value: any) => {
|
||||||
|
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
||||||
|
return `${attr.label} is required`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 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,
|
||||||
|
required,
|
||||||
|
validator,
|
||||||
|
options,
|
||||||
|
optionsReference
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: Password field removed - users are created without password
|
||||||
|
// Admin can send password setup link after user creation using handleSendPasswordLink
|
||||||
|
|
||||||
|
return createFields;
|
||||||
|
}, [attributes]);
|
||||||
|
|
||||||
// Ensure attributes are loaded - can be called by EditActionButton
|
// Ensure attributes are loaded - can be called by EditActionButton
|
||||||
const ensureAttributesLoaded = useCallback(async () => {
|
const ensureAttributesLoaded = useCallback(async () => {
|
||||||
// Don't fetch attributes if user is not authenticated (prevents 401 errors)
|
// Don't fetch attributes if user is not authenticated (prevents 401 errors)
|
||||||
|
|
@ -667,6 +797,7 @@ export function useOrgUsers() {
|
||||||
pagination,
|
pagination,
|
||||||
fetchUserById,
|
fetchUserById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -675,11 +806,13 @@ export function useOrgUsers() {
|
||||||
export function useUserOperations() {
|
export function useUserOperations() {
|
||||||
const [deletingUsers, setDeletingUsers] = useState<Set<string>>(new Set());
|
const [deletingUsers, setDeletingUsers] = useState<Set<string>>(new Set());
|
||||||
const [editingUsers, setEditingUsers] = useState<Set<string>>(new Set());
|
const [editingUsers, setEditingUsers] = useState<Set<string>>(new Set());
|
||||||
|
const [sendingPasswordLink, setSendingPasswordLink] = useState<Set<string>>(new Set());
|
||||||
const [creatingUser, setCreatingUser] = useState(false);
|
const [creatingUser, setCreatingUser] = useState(false);
|
||||||
const { request, isLoading } = useApiRequest();
|
const { request, isLoading } = useApiRequest();
|
||||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||||
const [createError, setCreateError] = useState<string | null>(null);
|
const [createError, setCreateError] = useState<string | null>(null);
|
||||||
const [updateError, setUpdateError] = useState<string | null>(null);
|
const [updateError, setUpdateError] = useState<string | null>(null);
|
||||||
|
const [passwordLinkError, setPasswordLinkError] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleUserDelete = async (userId: string) => {
|
const handleUserDelete = async (userId: string) => {
|
||||||
setDeleteError(null);
|
setDeleteError(null);
|
||||||
|
|
@ -702,7 +835,7 @@ export function useUserOperations() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUserCreate = async (userData: Omit<User, 'id' | 'mandateId'> & { password: string }) => {
|
const handleUserCreate = async (userData: Omit<User, 'id' | 'mandateId'>) => {
|
||||||
setCreateError(null);
|
setCreateError(null);
|
||||||
setCreatingUser(true);
|
setCreatingUser(true);
|
||||||
|
|
||||||
|
|
@ -726,6 +859,31 @@ export function useUserOperations() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Send password setup link to a user (admin function)
|
||||||
|
const handleSendPasswordLink = async (userId: string) => {
|
||||||
|
setPasswordLinkError(null);
|
||||||
|
setSendingPasswordLink(prev => new Set(prev).add(userId));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get frontend URL from current window location
|
||||||
|
const frontendUrl = `${window.location.protocol}//${window.location.host}`;
|
||||||
|
|
||||||
|
const result = await sendPasswordLinkApi(request, userId, frontendUrl);
|
||||||
|
|
||||||
|
return { success: true, message: result.message, email: result.email };
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMessage = error.response?.data?.detail || error.message || 'Failed to send password link';
|
||||||
|
setPasswordLinkError(errorMessage);
|
||||||
|
return { success: false, error: errorMessage };
|
||||||
|
} finally {
|
||||||
|
setSendingPasswordLink(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(userId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleUserUpdate = async (userId: string, updateData: UserUpdateData, _originalData?: any) => {
|
const handleUserUpdate = async (userId: string, updateData: UserUpdateData, _originalData?: any) => {
|
||||||
setUpdateError(null);
|
setUpdateError(null);
|
||||||
setEditingUsers(prev => new Set(prev).add(userId));
|
setEditingUsers(prev => new Set(prev).add(userId));
|
||||||
|
|
@ -764,16 +922,29 @@ export function useUserOperations() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Generic inline update handler for FormGeneratorTable
|
||||||
|
const handleInlineUpdate = async (userId: string, changes: Partial<UserUpdateData>) => {
|
||||||
|
const result = await handleUserUpdate(userId, changes as UserUpdateData);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Failed to update');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deletingUsers,
|
deletingUsers,
|
||||||
editingUsers,
|
editingUsers,
|
||||||
|
sendingPasswordLink,
|
||||||
creatingUser,
|
creatingUser,
|
||||||
deleteError,
|
deleteError,
|
||||||
createError,
|
createError,
|
||||||
updateError,
|
updateError,
|
||||||
|
passwordLinkError,
|
||||||
handleUserDelete,
|
handleUserDelete,
|
||||||
handleUserCreate,
|
handleUserCreate,
|
||||||
handleUserUpdate,
|
handleUserUpdate,
|
||||||
|
handleInlineUpdate,
|
||||||
|
handleSendPasswordLink,
|
||||||
isLoading
|
isLoading
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -290,11 +290,11 @@ export function useUserWorkflows() {
|
||||||
|
|
||||||
if (attr.name === 'name') {
|
if (attr.name === 'name') {
|
||||||
required = true;
|
required = true;
|
||||||
validator = (value: string) => {
|
validator = (value: any) => {
|
||||||
if (!value || value.trim() === '') {
|
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
||||||
return 'Workflow name cannot be empty';
|
return 'Workflow name cannot be empty';
|
||||||
}
|
}
|
||||||
if (value.length > 100) {
|
if (typeof value === 'string' && value.length > 100) {
|
||||||
return 'Workflow name cannot exceed 100 characters';
|
return 'Workflow name cannot exceed 100 characters';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -343,16 +343,12 @@ export function useUserWorkflows() {
|
||||||
}, [attributes, fetchAttributes]);
|
}, [attributes, fetchAttributes]);
|
||||||
|
|
||||||
// Fetch attributes and permissions on mount
|
// Fetch attributes and permissions on mount
|
||||||
|
// Note: Do NOT fetch workflows here - let the table component control pagination
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAttributes();
|
fetchAttributes();
|
||||||
fetchPermissions();
|
fetchPermissions();
|
||||||
}, [fetchAttributes, fetchPermissions]);
|
}, [fetchAttributes, fetchPermissions]);
|
||||||
|
|
||||||
// Initial fetch
|
|
||||||
useEffect(() => {
|
|
||||||
fetchWorkflowsData();
|
|
||||||
}, [fetchWorkflowsData]);
|
|
||||||
|
|
||||||
// Listen for workflow creation events to refetch workflows list
|
// Listen for workflow creation events to refetch workflows list
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleWorkflowCreated = (_event: CustomEvent<{ workflow: UserWorkflow }>) => {
|
const handleWorkflowCreated = (_event: CustomEvent<{ workflow: UserWorkflow }>) => {
|
||||||
|
|
|
||||||
|
|
@ -535,6 +535,9 @@ export default {
|
||||||
'team-members.new_button': 'Mitglied hinzufügen',
|
'team-members.new_button': 'Mitglied hinzufügen',
|
||||||
'team-members.action.edit': 'Bearbeiten',
|
'team-members.action.edit': 'Bearbeiten',
|
||||||
'team-members.action.delete': 'Löschen',
|
'team-members.action.delete': 'Löschen',
|
||||||
|
'team-members.action.sendPasswordLink': 'Passwort-Link senden',
|
||||||
|
'team-members.action.passwordLinkSent': 'Passwort-Link gesendet!',
|
||||||
|
'team-members.action.passwordLinkFailed': 'Link konnte nicht gesendet werden',
|
||||||
'team-members.field.username': 'Benutzername',
|
'team-members.field.username': 'Benutzername',
|
||||||
'team-members.field.email': 'E-Mail',
|
'team-members.field.email': 'E-Mail',
|
||||||
'team-members.field.password': 'Passwort',
|
'team-members.field.password': 'Passwort',
|
||||||
|
|
@ -716,8 +719,8 @@ export default {
|
||||||
'warning.duplicate_file.message': 'Die Datei "{fileName}" existiert bereits mit identischem Inhalt. Die vorhandene Datei wird wiederverwendet.',
|
'warning.duplicate_file.message': 'Die Datei "{fileName}" existiert bereits mit identischem Inhalt. Die vorhandene Datei wird wiederverwendet.',
|
||||||
|
|
||||||
// Administration
|
// Administration
|
||||||
'administration.title': 'Verwaltung',
|
'administration.title': 'Werkzeuge',
|
||||||
'administration.description': 'Verwaltungs- und Management-Tools',
|
'administration.description': 'Werkzeuge und Hilfsmittel',
|
||||||
'administration.subtitle': '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.intro.description': 'Dieser Bereich enthält alle Verwaltungs- und Management-Tools für Ihren Arbeitsbereich.',
|
||||||
'administration.features.title': 'Verfügbare Tools',
|
'administration.features.title': 'Verfügbare Tools',
|
||||||
|
|
@ -799,4 +802,111 @@ export default {
|
||||||
'dragdrop.overlay.default_subtext': 'Sie können auch auf den Upload-Button klicken',
|
'dragdrop.overlay.default_subtext': 'Sie können auch auf den Upload-Button klicken',
|
||||||
'dragdrop.overlay.processing': 'Dateien werden verarbeitet...',
|
'dragdrop.overlay.processing': 'Dateien werden verarbeitet...',
|
||||||
'dragdrop.overlay.error': 'Fehler beim Verarbeiten der Dateien',
|
'dragdrop.overlay.error': 'Fehler beim Verarbeiten der Dateien',
|
||||||
|
|
||||||
|
// Trustee Feature
|
||||||
|
'trustee.title': 'Treuhand',
|
||||||
|
'trustee.subtitle': 'Treuhandverwaltung',
|
||||||
|
'trustee.description': 'Verwaltung von Treuhand-Organisationen, Verträgen und Buchungen',
|
||||||
|
|
||||||
|
// Trustee Organisations
|
||||||
|
'trustee.organisations.title': 'Organisationen',
|
||||||
|
'trustee.organisations.subtitle': 'Trustee-Organisationen verwalten',
|
||||||
|
'trustee.organisations.description': 'Verwaltung der Treuhand-Organisationen',
|
||||||
|
'trustee.organisations.new_button': 'Neue Organisation',
|
||||||
|
'trustee.organisations.field.id': 'ID',
|
||||||
|
'trustee.organisations.field.id_placeholder': 'z.B. treuhand-ag-zuerich',
|
||||||
|
'trustee.organisations.field.label': 'Bezeichnung',
|
||||||
|
'trustee.organisations.field.label_placeholder': 'z.B. Treuhand AG Zürich',
|
||||||
|
'trustee.organisations.field.enabled': 'Aktiviert',
|
||||||
|
'trustee.organisations.modal.create.title': 'Neue Organisation erstellen',
|
||||||
|
'trustee.organisations.create.success': 'Organisation erfolgreich erstellt',
|
||||||
|
'trustee.organisations.create.error': 'Fehler beim Erstellen der Organisation',
|
||||||
|
'trustee.organisations.action.edit': 'Bearbeiten',
|
||||||
|
'trustee.organisations.action.delete': 'Löschen',
|
||||||
|
|
||||||
|
// Trustee Roles
|
||||||
|
'trustee.roles.title': 'Rollen',
|
||||||
|
'trustee.roles.subtitle': 'Trustee-Rollen verwalten',
|
||||||
|
'trustee.roles.description': 'Verwaltung der Feature-spezifischen Rollen',
|
||||||
|
'trustee.roles.new_button': 'Neue Rolle',
|
||||||
|
'trustee.roles.field.id': 'Rollen-ID',
|
||||||
|
'trustee.roles.field.id_placeholder': 'z.B. admin, operate, userreport',
|
||||||
|
'trustee.roles.field.desc': 'Beschreibung',
|
||||||
|
'trustee.roles.field.desc_placeholder': 'Beschreibung der Rolle',
|
||||||
|
'trustee.roles.modal.create.title': 'Neue Rolle erstellen',
|
||||||
|
'trustee.roles.create.success': 'Rolle erfolgreich erstellt',
|
||||||
|
'trustee.roles.create.error': 'Fehler beim Erstellen der Rolle',
|
||||||
|
'trustee.roles.action.edit': 'Bearbeiten',
|
||||||
|
'trustee.roles.action.delete': 'Löschen',
|
||||||
|
|
||||||
|
// Trustee Access
|
||||||
|
'trustee.access.title': 'Zugriff',
|
||||||
|
'trustee.access.subtitle': 'Benutzer-Zugriff verwalten',
|
||||||
|
'trustee.access.description': 'Verwaltung der Benutzerzugriffe auf Organisationen',
|
||||||
|
'trustee.access.new_button': 'Neuer Zugriff',
|
||||||
|
'trustee.access.field.organisationId': 'Organisation',
|
||||||
|
'trustee.access.field.roleId': 'Rolle',
|
||||||
|
'trustee.access.field.userId': 'Benutzer',
|
||||||
|
'trustee.access.field.contractId': 'Vertrag (optional)',
|
||||||
|
'trustee.access.field.contractId_placeholder': 'Leer = Zugriff auf alle Verträge',
|
||||||
|
'trustee.access.modal.create.title': 'Neuen Zugriff erstellen',
|
||||||
|
'trustee.access.create.success': 'Zugriff erfolgreich erstellt',
|
||||||
|
'trustee.access.create.error': 'Fehler beim Erstellen des Zugriffs',
|
||||||
|
'trustee.access.action.edit': 'Bearbeiten',
|
||||||
|
'trustee.access.action.delete': 'Löschen',
|
||||||
|
|
||||||
|
// Trustee Contracts
|
||||||
|
'trustee.contracts.title': 'Verträge',
|
||||||
|
'trustee.contracts.subtitle': 'Kundenverträge verwalten',
|
||||||
|
'trustee.contracts.description': 'Verwaltung der Kundenverträge',
|
||||||
|
'trustee.contracts.new_button': 'Neuer Vertrag',
|
||||||
|
'trustee.contracts.field.organisationId': 'Organisation',
|
||||||
|
'trustee.contracts.field.label': 'Bezeichnung',
|
||||||
|
'trustee.contracts.field.label_placeholder': 'z.B. Muster AG 2026',
|
||||||
|
'trustee.contracts.field.enabled': 'Aktiviert',
|
||||||
|
'trustee.contracts.modal.create.title': 'Neuen Vertrag erstellen',
|
||||||
|
'trustee.contracts.create.success': 'Vertrag erfolgreich erstellt',
|
||||||
|
'trustee.contracts.create.error': 'Fehler beim Erstellen des Vertrags',
|
||||||
|
'trustee.contracts.action.edit': 'Bearbeiten',
|
||||||
|
'trustee.contracts.action.delete': 'Löschen',
|
||||||
|
|
||||||
|
// Trustee Documents
|
||||||
|
'trustee.documents.title': 'Dokumente',
|
||||||
|
'trustee.documents.subtitle': 'Belege verwalten',
|
||||||
|
'trustee.documents.description': 'Verwaltung der Dokumente und Belege',
|
||||||
|
'trustee.documents.new_button': 'Neues Dokument',
|
||||||
|
'trustee.documents.field.organisationId': 'Organisation',
|
||||||
|
'trustee.documents.field.contractId': 'Vertrag',
|
||||||
|
'trustee.documents.field.documentName': 'Dateiname',
|
||||||
|
'trustee.documents.field.documentName_placeholder': 'z.B. Beleg.pdf',
|
||||||
|
'trustee.documents.field.documentMimeType': 'Dateityp',
|
||||||
|
'trustee.documents.modal.create.title': 'Neues Dokument erstellen',
|
||||||
|
'trustee.documents.create.success': 'Dokument erfolgreich erstellt',
|
||||||
|
'trustee.documents.create.error': 'Fehler beim Erstellen des Dokuments',
|
||||||
|
'trustee.documents.action.edit': 'Bearbeiten',
|
||||||
|
'trustee.documents.action.delete': 'Löschen',
|
||||||
|
'trustee.documents.action.download': 'Herunterladen',
|
||||||
|
|
||||||
|
// Trustee Positions
|
||||||
|
'trustee.positions.title': 'Positionen',
|
||||||
|
'trustee.positions.subtitle': 'Buchungspositionen verwalten',
|
||||||
|
'trustee.positions.description': 'Verwaltung der Buchungspositionen (Speseneinträge)',
|
||||||
|
'trustee.positions.new_button': 'Neue Position',
|
||||||
|
'trustee.positions.field.organisationId': 'Organisation',
|
||||||
|
'trustee.positions.field.contractId': 'Vertrag',
|
||||||
|
'trustee.positions.field.valuta': 'Valutadatum',
|
||||||
|
'trustee.positions.field.company': 'Firma',
|
||||||
|
'trustee.positions.field.company_placeholder': 'Name des Unternehmens',
|
||||||
|
'trustee.positions.field.desc': 'Beschreibung',
|
||||||
|
'trustee.positions.field.bookingCurrency': 'Buchungswährung',
|
||||||
|
'trustee.positions.field.bookingAmount': 'Buchungsbetrag',
|
||||||
|
'trustee.positions.field.originalCurrency': 'Originalwährung',
|
||||||
|
'trustee.positions.field.originalAmount': 'Originalbetrag',
|
||||||
|
'trustee.positions.field.vatPercentage': 'MwSt %',
|
||||||
|
'trustee.positions.field.vatAmount': 'MwSt Betrag',
|
||||||
|
'trustee.positions.modal.create.title': 'Neue Position erstellen',
|
||||||
|
'trustee.positions.create.success': 'Position erfolgreich erstellt',
|
||||||
|
'trustee.positions.create.error': 'Fehler beim Erstellen der Position',
|
||||||
|
'trustee.positions.action.edit': 'Bearbeiten',
|
||||||
|
'trustee.positions.action.delete': 'Löschen',
|
||||||
};
|
};
|
||||||
|
|
@ -535,6 +535,9 @@ export default {
|
||||||
'team-members.new_button': 'Add Member',
|
'team-members.new_button': 'Add Member',
|
||||||
'team-members.action.edit': 'Edit',
|
'team-members.action.edit': 'Edit',
|
||||||
'team-members.action.delete': 'Delete',
|
'team-members.action.delete': 'Delete',
|
||||||
|
'team-members.action.sendPasswordLink': 'Send password setup link',
|
||||||
|
'team-members.action.passwordLinkSent': 'Password link sent!',
|
||||||
|
'team-members.action.passwordLinkFailed': 'Failed to send link',
|
||||||
'team-members.field.username': 'Username',
|
'team-members.field.username': 'Username',
|
||||||
'team-members.field.email': 'Email',
|
'team-members.field.email': 'Email',
|
||||||
'team-members.field.password': 'Password',
|
'team-members.field.password': 'Password',
|
||||||
|
|
@ -716,8 +719,8 @@ export default {
|
||||||
'warning.duplicate_file.message': 'The file "{fileName}" already exists with identical content. The existing file will be reused.',
|
'warning.duplicate_file.message': 'The file "{fileName}" already exists with identical content. The existing file will be reused.',
|
||||||
|
|
||||||
// Administration
|
// Administration
|
||||||
'administration.title': 'Administration',
|
'administration.title': 'Utils',
|
||||||
'administration.description': 'Administration and management tools',
|
'administration.description': 'Utilities and tools',
|
||||||
'administration.subtitle': '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.intro.description': 'This section contains all administration and management tools for your workspace.',
|
||||||
'administration.features.title': 'Available Tools',
|
'administration.features.title': 'Available Tools',
|
||||||
|
|
@ -799,4 +802,111 @@ export default {
|
||||||
'dragdrop.overlay.default_subtext': 'You can also click the upload button',
|
'dragdrop.overlay.default_subtext': 'You can also click the upload button',
|
||||||
'dragdrop.overlay.processing': 'Processing files...',
|
'dragdrop.overlay.processing': 'Processing files...',
|
||||||
'dragdrop.overlay.error': 'Error processing files',
|
'dragdrop.overlay.error': 'Error processing files',
|
||||||
|
|
||||||
|
// Trustee Feature
|
||||||
|
'trustee.title': 'Trustee',
|
||||||
|
'trustee.subtitle': 'Trustee Management',
|
||||||
|
'trustee.description': 'Manage trustee organisations, contracts, and bookings',
|
||||||
|
|
||||||
|
// Trustee Organisations
|
||||||
|
'trustee.organisations.title': 'Organisations',
|
||||||
|
'trustee.organisations.subtitle': 'Manage trustee organisations',
|
||||||
|
'trustee.organisations.description': 'Management of trustee organisations',
|
||||||
|
'trustee.organisations.new_button': 'New Organisation',
|
||||||
|
'trustee.organisations.field.id': 'ID',
|
||||||
|
'trustee.organisations.field.id_placeholder': 'e.g. trustee-ag-zurich',
|
||||||
|
'trustee.organisations.field.label': 'Label',
|
||||||
|
'trustee.organisations.field.label_placeholder': 'e.g. Trustee AG Zurich',
|
||||||
|
'trustee.organisations.field.enabled': 'Enabled',
|
||||||
|
'trustee.organisations.modal.create.title': 'Create New Organisation',
|
||||||
|
'trustee.organisations.create.success': 'Organisation created successfully',
|
||||||
|
'trustee.organisations.create.error': 'Error creating organisation',
|
||||||
|
'trustee.organisations.action.edit': 'Edit',
|
||||||
|
'trustee.organisations.action.delete': 'Delete',
|
||||||
|
|
||||||
|
// Trustee Roles
|
||||||
|
'trustee.roles.title': 'Roles',
|
||||||
|
'trustee.roles.subtitle': 'Manage trustee roles',
|
||||||
|
'trustee.roles.description': 'Management of feature-specific roles',
|
||||||
|
'trustee.roles.new_button': 'New Role',
|
||||||
|
'trustee.roles.field.id': 'Role ID',
|
||||||
|
'trustee.roles.field.id_placeholder': 'e.g. admin, operate, userreport',
|
||||||
|
'trustee.roles.field.desc': 'Description',
|
||||||
|
'trustee.roles.field.desc_placeholder': 'Role description',
|
||||||
|
'trustee.roles.modal.create.title': 'Create New Role',
|
||||||
|
'trustee.roles.create.success': 'Role created successfully',
|
||||||
|
'trustee.roles.create.error': 'Error creating role',
|
||||||
|
'trustee.roles.action.edit': 'Edit',
|
||||||
|
'trustee.roles.action.delete': 'Delete',
|
||||||
|
|
||||||
|
// Trustee Access
|
||||||
|
'trustee.access.title': 'Access',
|
||||||
|
'trustee.access.subtitle': 'Manage user access',
|
||||||
|
'trustee.access.description': 'Management of user access to organisations',
|
||||||
|
'trustee.access.new_button': 'New Access',
|
||||||
|
'trustee.access.field.organisationId': 'Organisation',
|
||||||
|
'trustee.access.field.roleId': 'Role',
|
||||||
|
'trustee.access.field.userId': 'User',
|
||||||
|
'trustee.access.field.contractId': 'Contract (optional)',
|
||||||
|
'trustee.access.field.contractId_placeholder': 'Empty = Access to all contracts',
|
||||||
|
'trustee.access.modal.create.title': 'Create New Access',
|
||||||
|
'trustee.access.create.success': 'Access created successfully',
|
||||||
|
'trustee.access.create.error': 'Error creating access',
|
||||||
|
'trustee.access.action.edit': 'Edit',
|
||||||
|
'trustee.access.action.delete': 'Delete',
|
||||||
|
|
||||||
|
// Trustee Contracts
|
||||||
|
'trustee.contracts.title': 'Contracts',
|
||||||
|
'trustee.contracts.subtitle': 'Manage customer contracts',
|
||||||
|
'trustee.contracts.description': 'Management of customer contracts',
|
||||||
|
'trustee.contracts.new_button': 'New Contract',
|
||||||
|
'trustee.contracts.field.organisationId': 'Organisation',
|
||||||
|
'trustee.contracts.field.label': 'Label',
|
||||||
|
'trustee.contracts.field.label_placeholder': 'e.g. Muster AG 2026',
|
||||||
|
'trustee.contracts.field.enabled': 'Enabled',
|
||||||
|
'trustee.contracts.modal.create.title': 'Create New Contract',
|
||||||
|
'trustee.contracts.create.success': 'Contract created successfully',
|
||||||
|
'trustee.contracts.create.error': 'Error creating contract',
|
||||||
|
'trustee.contracts.action.edit': 'Edit',
|
||||||
|
'trustee.contracts.action.delete': 'Delete',
|
||||||
|
|
||||||
|
// Trustee Documents
|
||||||
|
'trustee.documents.title': 'Documents',
|
||||||
|
'trustee.documents.subtitle': 'Manage receipts',
|
||||||
|
'trustee.documents.description': 'Management of documents and receipts',
|
||||||
|
'trustee.documents.new_button': 'New Document',
|
||||||
|
'trustee.documents.field.organisationId': 'Organisation',
|
||||||
|
'trustee.documents.field.contractId': 'Contract',
|
||||||
|
'trustee.documents.field.documentName': 'File Name',
|
||||||
|
'trustee.documents.field.documentName_placeholder': 'e.g. Receipt.pdf',
|
||||||
|
'trustee.documents.field.documentMimeType': 'File Type',
|
||||||
|
'trustee.documents.modal.create.title': 'Create New Document',
|
||||||
|
'trustee.documents.create.success': 'Document created successfully',
|
||||||
|
'trustee.documents.create.error': 'Error creating document',
|
||||||
|
'trustee.documents.action.edit': 'Edit',
|
||||||
|
'trustee.documents.action.delete': 'Delete',
|
||||||
|
'trustee.documents.action.download': 'Download',
|
||||||
|
|
||||||
|
// Trustee Positions
|
||||||
|
'trustee.positions.title': 'Positions',
|
||||||
|
'trustee.positions.subtitle': 'Manage booking positions',
|
||||||
|
'trustee.positions.description': 'Management of booking positions (expense entries)',
|
||||||
|
'trustee.positions.new_button': 'New Position',
|
||||||
|
'trustee.positions.field.organisationId': 'Organisation',
|
||||||
|
'trustee.positions.field.contractId': 'Contract',
|
||||||
|
'trustee.positions.field.valuta': 'Value Date',
|
||||||
|
'trustee.positions.field.company': 'Company',
|
||||||
|
'trustee.positions.field.company_placeholder': 'Company name',
|
||||||
|
'trustee.positions.field.desc': 'Description',
|
||||||
|
'trustee.positions.field.bookingCurrency': 'Booking Currency',
|
||||||
|
'trustee.positions.field.bookingAmount': 'Booking Amount',
|
||||||
|
'trustee.positions.field.originalCurrency': 'Original Currency',
|
||||||
|
'trustee.positions.field.originalAmount': 'Original Amount',
|
||||||
|
'trustee.positions.field.vatPercentage': 'VAT %',
|
||||||
|
'trustee.positions.field.vatAmount': 'VAT Amount',
|
||||||
|
'trustee.positions.modal.create.title': 'Create New Position',
|
||||||
|
'trustee.positions.create.success': 'Position created successfully',
|
||||||
|
'trustee.positions.create.error': 'Error creating position',
|
||||||
|
'trustee.positions.action.edit': 'Edit',
|
||||||
|
'trustee.positions.action.delete': 'Delete',
|
||||||
};
|
};
|
||||||
|
|
@ -535,6 +535,9 @@ export default {
|
||||||
'team-members.new_button': 'Ajouter un membre',
|
'team-members.new_button': 'Ajouter un membre',
|
||||||
'team-members.action.edit': 'Modifier',
|
'team-members.action.edit': 'Modifier',
|
||||||
'team-members.action.delete': 'Supprimer',
|
'team-members.action.delete': 'Supprimer',
|
||||||
|
'team-members.action.sendPasswordLink': 'Envoyer le lien de mot de passe',
|
||||||
|
'team-members.action.passwordLinkSent': 'Lien de mot de passe envoyé!',
|
||||||
|
'team-members.action.passwordLinkFailed': 'Échec de l\'envoi du lien',
|
||||||
'team-members.field.username': 'Nom d\'utilisateur',
|
'team-members.field.username': 'Nom d\'utilisateur',
|
||||||
'team-members.field.email': 'E-mail',
|
'team-members.field.email': 'E-mail',
|
||||||
'team-members.field.password': 'Mot de passe',
|
'team-members.field.password': 'Mot de passe',
|
||||||
|
|
@ -716,8 +719,8 @@ export default {
|
||||||
'warning.duplicate_file.message': 'Le fichier "{fileName}" existe déjà avec un contenu identique. Le fichier existant sera réutilisé.',
|
'warning.duplicate_file.message': 'Le fichier "{fileName}" existe déjà avec un contenu identique. Le fichier existant sera réutilisé.',
|
||||||
|
|
||||||
// Administration
|
// Administration
|
||||||
'administration.title': 'Administration',
|
'administration.title': 'Outils',
|
||||||
'administration.description': 'Outils d\'administration et de gestion',
|
'administration.description': 'Outils et utilitaires',
|
||||||
'administration.subtitle': '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.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.title': 'Outils Disponibles',
|
||||||
|
|
@ -799,4 +802,111 @@ export default {
|
||||||
'dragdrop.overlay.default_subtext': 'Vous pouvez aussi cliquer sur le bouton de téléchargement',
|
'dragdrop.overlay.default_subtext': 'Vous pouvez aussi cliquer sur le bouton de téléchargement',
|
||||||
'dragdrop.overlay.processing': 'Traitement des fichiers...',
|
'dragdrop.overlay.processing': 'Traitement des fichiers...',
|
||||||
'dragdrop.overlay.error': 'Erreur lors du traitement des fichiers',
|
'dragdrop.overlay.error': 'Erreur lors du traitement des fichiers',
|
||||||
|
|
||||||
|
// Trustee Feature
|
||||||
|
'trustee.title': 'Fiduciaire',
|
||||||
|
'trustee.subtitle': 'Gestion Fiduciaire',
|
||||||
|
'trustee.description': 'Gestion des organisations fiduciaires, contrats et réservations',
|
||||||
|
|
||||||
|
// Trustee Organisations
|
||||||
|
'trustee.organisations.title': 'Organisations',
|
||||||
|
'trustee.organisations.subtitle': 'Gérer les organisations fiduciaires',
|
||||||
|
'trustee.organisations.description': 'Gestion des organisations fiduciaires',
|
||||||
|
'trustee.organisations.new_button': 'Nouvelle Organisation',
|
||||||
|
'trustee.organisations.field.id': 'ID',
|
||||||
|
'trustee.organisations.field.id_placeholder': 'ex. fiduciaire-ag-zurich',
|
||||||
|
'trustee.organisations.field.label': 'Libellé',
|
||||||
|
'trustee.organisations.field.label_placeholder': 'ex. Fiduciaire AG Zurich',
|
||||||
|
'trustee.organisations.field.enabled': 'Activé',
|
||||||
|
'trustee.organisations.modal.create.title': 'Créer une nouvelle organisation',
|
||||||
|
'trustee.organisations.create.success': 'Organisation créée avec succès',
|
||||||
|
'trustee.organisations.create.error': 'Erreur lors de la création de l\'organisation',
|
||||||
|
'trustee.organisations.action.edit': 'Modifier',
|
||||||
|
'trustee.organisations.action.delete': 'Supprimer',
|
||||||
|
|
||||||
|
// Trustee Roles
|
||||||
|
'trustee.roles.title': 'Rôles',
|
||||||
|
'trustee.roles.subtitle': 'Gérer les rôles fiduciaires',
|
||||||
|
'trustee.roles.description': 'Gestion des rôles spécifiques à la fonctionnalité',
|
||||||
|
'trustee.roles.new_button': 'Nouveau Rôle',
|
||||||
|
'trustee.roles.field.id': 'ID du rôle',
|
||||||
|
'trustee.roles.field.id_placeholder': 'ex. admin, operate, userreport',
|
||||||
|
'trustee.roles.field.desc': 'Description',
|
||||||
|
'trustee.roles.field.desc_placeholder': 'Description du rôle',
|
||||||
|
'trustee.roles.modal.create.title': 'Créer un nouveau rôle',
|
||||||
|
'trustee.roles.create.success': 'Rôle créé avec succès',
|
||||||
|
'trustee.roles.create.error': 'Erreur lors de la création du rôle',
|
||||||
|
'trustee.roles.action.edit': 'Modifier',
|
||||||
|
'trustee.roles.action.delete': 'Supprimer',
|
||||||
|
|
||||||
|
// Trustee Access
|
||||||
|
'trustee.access.title': 'Accès',
|
||||||
|
'trustee.access.subtitle': 'Gérer les accès utilisateurs',
|
||||||
|
'trustee.access.description': 'Gestion des accès utilisateurs aux organisations',
|
||||||
|
'trustee.access.new_button': 'Nouvel Accès',
|
||||||
|
'trustee.access.field.organisationId': 'Organisation',
|
||||||
|
'trustee.access.field.roleId': 'Rôle',
|
||||||
|
'trustee.access.field.userId': 'Utilisateur',
|
||||||
|
'trustee.access.field.contractId': 'Contrat (optionnel)',
|
||||||
|
'trustee.access.field.contractId_placeholder': 'Vide = Accès à tous les contrats',
|
||||||
|
'trustee.access.modal.create.title': 'Créer un nouvel accès',
|
||||||
|
'trustee.access.create.success': 'Accès créé avec succès',
|
||||||
|
'trustee.access.create.error': 'Erreur lors de la création de l\'accès',
|
||||||
|
'trustee.access.action.edit': 'Modifier',
|
||||||
|
'trustee.access.action.delete': 'Supprimer',
|
||||||
|
|
||||||
|
// Trustee Contracts
|
||||||
|
'trustee.contracts.title': 'Contrats',
|
||||||
|
'trustee.contracts.subtitle': 'Gérer les contrats clients',
|
||||||
|
'trustee.contracts.description': 'Gestion des contrats clients',
|
||||||
|
'trustee.contracts.new_button': 'Nouveau Contrat',
|
||||||
|
'trustee.contracts.field.organisationId': 'Organisation',
|
||||||
|
'trustee.contracts.field.label': 'Libellé',
|
||||||
|
'trustee.contracts.field.label_placeholder': 'ex. Muster AG 2026',
|
||||||
|
'trustee.contracts.field.enabled': 'Activé',
|
||||||
|
'trustee.contracts.modal.create.title': 'Créer un nouveau contrat',
|
||||||
|
'trustee.contracts.create.success': 'Contrat créé avec succès',
|
||||||
|
'trustee.contracts.create.error': 'Erreur lors de la création du contrat',
|
||||||
|
'trustee.contracts.action.edit': 'Modifier',
|
||||||
|
'trustee.contracts.action.delete': 'Supprimer',
|
||||||
|
|
||||||
|
// Trustee Documents
|
||||||
|
'trustee.documents.title': 'Documents',
|
||||||
|
'trustee.documents.subtitle': 'Gérer les pièces justificatives',
|
||||||
|
'trustee.documents.description': 'Gestion des documents et pièces justificatives',
|
||||||
|
'trustee.documents.new_button': 'Nouveau Document',
|
||||||
|
'trustee.documents.field.organisationId': 'Organisation',
|
||||||
|
'trustee.documents.field.contractId': 'Contrat',
|
||||||
|
'trustee.documents.field.documentName': 'Nom du fichier',
|
||||||
|
'trustee.documents.field.documentName_placeholder': 'ex. Justificatif.pdf',
|
||||||
|
'trustee.documents.field.documentMimeType': 'Type de fichier',
|
||||||
|
'trustee.documents.modal.create.title': 'Créer un nouveau document',
|
||||||
|
'trustee.documents.create.success': 'Document créé avec succès',
|
||||||
|
'trustee.documents.create.error': 'Erreur lors de la création du document',
|
||||||
|
'trustee.documents.action.edit': 'Modifier',
|
||||||
|
'trustee.documents.action.delete': 'Supprimer',
|
||||||
|
'trustee.documents.action.download': 'Télécharger',
|
||||||
|
|
||||||
|
// Trustee Positions
|
||||||
|
'trustee.positions.title': 'Positions',
|
||||||
|
'trustee.positions.subtitle': 'Gérer les positions de réservation',
|
||||||
|
'trustee.positions.description': 'Gestion des positions de réservation (entrées de dépenses)',
|
||||||
|
'trustee.positions.new_button': 'Nouvelle Position',
|
||||||
|
'trustee.positions.field.organisationId': 'Organisation',
|
||||||
|
'trustee.positions.field.contractId': 'Contrat',
|
||||||
|
'trustee.positions.field.valuta': 'Date de valeur',
|
||||||
|
'trustee.positions.field.company': 'Entreprise',
|
||||||
|
'trustee.positions.field.company_placeholder': 'Nom de l\'entreprise',
|
||||||
|
'trustee.positions.field.desc': 'Description',
|
||||||
|
'trustee.positions.field.bookingCurrency': 'Devise de comptabilisation',
|
||||||
|
'trustee.positions.field.bookingAmount': 'Montant de comptabilisation',
|
||||||
|
'trustee.positions.field.originalCurrency': 'Devise d\'origine',
|
||||||
|
'trustee.positions.field.originalAmount': 'Montant d\'origine',
|
||||||
|
'trustee.positions.field.vatPercentage': 'TVA %',
|
||||||
|
'trustee.positions.field.vatAmount': 'Montant TVA',
|
||||||
|
'trustee.positions.modal.create.title': 'Créer une nouvelle position',
|
||||||
|
'trustee.positions.create.success': 'Position créée avec succès',
|
||||||
|
'trustee.positions.create.error': 'Erreur lors de la création de la position',
|
||||||
|
'trustee.positions.action.edit': 'Modifier',
|
||||||
|
'trustee.positions.action.delete': 'Supprimer',
|
||||||
};
|
};
|
||||||
|
|
@ -17,11 +17,11 @@
|
||||||
/* Card-style container with background and shadow */
|
/* Card-style container with background and shadow */
|
||||||
.pageCard {
|
.pageCard {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 25px 25px 0 25px;
|
padding: 25px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-self: top;
|
align-self: top;
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
gap: 20px;
|
gap: 15px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden; /* Prevent card from expanding beyond viewport */
|
overflow: hidden; /* Prevent card from expanding beyond viewport */
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
@ -149,9 +149,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableContainer {
|
.tableContainer {
|
||||||
margin: 1.5rem 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.refetchingIndicator {
|
.refetchingIndicator {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export type AttributeType =
|
||||||
| 'textarea'
|
| 'textarea'
|
||||||
| 'select'
|
| 'select'
|
||||||
| 'multiselect'
|
| 'multiselect'
|
||||||
|
| 'multilingual'
|
||||||
| 'integer'
|
| 'integer'
|
||||||
| 'float'
|
| 'float'
|
||||||
| 'number'
|
| 'number'
|
||||||
|
|
@ -28,6 +29,7 @@ export type InputComponentType =
|
||||||
| 'textarea'
|
| 'textarea'
|
||||||
| 'select'
|
| 'select'
|
||||||
| 'multiselect'
|
| 'multiselect'
|
||||||
|
| 'multilingual'
|
||||||
| 'checkbox'
|
| 'checkbox'
|
||||||
| 'file'
|
| 'file'
|
||||||
| 'email'
|
| 'email'
|
||||||
|
|
@ -136,6 +138,13 @@ export function isMultiselectType(attributeType: AttributeType): boolean {
|
||||||
return attributeType === 'multiselect';
|
return attributeType === 'multiselect';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if an attribute type should render as a multilingual field
|
||||||
|
*/
|
||||||
|
export function isMultilingualType(attributeType: AttributeType): boolean {
|
||||||
|
return attributeType === 'multilingual';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if an attribute type should render as a checkbox
|
* Determines if an attribute type should render as a checkbox
|
||||||
*/
|
*/
|
||||||
|
|
@ -174,6 +183,9 @@ export function getDefaultValueForType(attributeType: AttributeType): any {
|
||||||
if (isMultiselectType(attributeType)) {
|
if (isMultiselectType(attributeType)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
if (isMultilingualType(attributeType)) {
|
||||||
|
return { en: '' };
|
||||||
|
}
|
||||||
if (isNumberType(attributeType)) {
|
if (isNumberType(attributeType)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue