diff --git a/index.html b/index.html index 92fd9fe..5d9620b 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + qassure-frontend diff --git a/public/LTTS.svg b/public/LTTS.svg new file mode 100644 index 0000000..adf1455 --- /dev/null +++ b/public/LTTS.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 06ae6e8..ec2ab8f 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -78,6 +78,35 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { const isSuperAdmin = isSuperAdminCheck(); + // Get role name for display + const getRoleName = (): string => { + if (isSuperAdmin) { + return 'Super Admin'; + } + let rolesArray: string[] = []; + if (Array.isArray(roles)) { + rolesArray = roles; + } else if (typeof roles === 'string') { + try { + rolesArray = JSON.parse(roles); + } catch { + rolesArray = []; + } + } + // Get the first role and format it + if (rolesArray.length > 0) { + const role = rolesArray[0]; + // Convert snake_case to Title Case + return role + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' '); + } + return 'User'; + }; + + const roleName = getRoleName(); + // Fetch theme if tenant admin if (!isSuperAdmin) { useTenantTheme(); @@ -212,8 +241,20 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { > -
- {(!isSuperAdmin && logoUrl) ? '' : 'QAssure'} +
+
+ {(!isSuperAdmin && logoUrl) ? '' : 'QAssure'} +
+ {(!isSuperAdmin && logoUrl) ? null : ( +
+ {roleName} +
+ )}
+ + {/* Powered by LTTS */} +
+

+ Powered by +

+ L&T Technology Services +
{/* Desktop Sidebar */} @@ -266,8 +319,20 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { > -
- {(!isSuperAdmin && logoUrl) ? '' : 'QAssure'} +
+
+ {(!isSuperAdmin && logoUrl) ? '' : 'QAssure'} +
+ {(!isSuperAdmin && logoUrl) ? null : ( +
+ {roleName} +
+ )}
@@ -289,6 +354,18 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { Support Center + + {/* Powered by LTTS */} +
+

+ Powered by +

+ L&T Technology Services +
); diff --git a/src/components/shared/DataTable.tsx b/src/components/shared/DataTable.tsx index 866db1a..8b4e07f 100644 --- a/src/components/shared/DataTable.tsx +++ b/src/components/shared/DataTable.tsx @@ -65,7 +65,7 @@ export const DataTable = ({ return ( {column.label} @@ -75,7 +75,7 @@ export const DataTable = ({ - + {emptyMessage} @@ -106,14 +106,14 @@ export const DataTable = ({ : column.align === 'center' ? 'text-center' : 'text-left'; - return ( - - {column.label} - - ); + return ( + + {column.label} + + ); })} @@ -131,7 +131,7 @@ export const DataTable = ({ ? 'text-center' : 'text-left'; return ( - + {column.render ? column.render(item) : String((item as any)[column.key])} ); diff --git a/src/components/shared/EditRoleModal.tsx b/src/components/shared/EditRoleModal.tsx index 9244ebf..ff3ca5b 100644 --- a/src/components/shared/EditRoleModal.tsx +++ b/src/components/shared/EditRoleModal.tsx @@ -69,7 +69,7 @@ const editRoleSchema = z.object({ "Code must be lowercase and use '_' for separation (e.g. abc_def)" ), description: z.string().min(1, 'Description is required'), - modules: z.array(z.string().uuid()).optional().nullable(), + modules: z.array(z.uuid()).optional().nullable(), permissions: z.array(z.object({ resource: z.string(), action: z.string(), diff --git a/src/components/shared/EditUserModal.tsx b/src/components/shared/EditUserModal.tsx index 469ae4b..b15b941 100644 --- a/src/components/shared/EditUserModal.tsx +++ b/src/components/shared/EditUserModal.tsx @@ -12,13 +12,13 @@ import { PrimaryButton, SecondaryButton, } from '@/components/shared'; -import { tenantService } from '@/services/tenant-service'; +// import { tenantService } from '@/services/tenant-service'; import { roleService } from '@/services/role-service'; import type { User } from '@/types/user'; // Validation schema const editUserSchema = z.object({ - email: z.string().min(1, 'Email is required').email('Please enter a valid email address'), + email: z.email({ message: 'Please enter a valid email address' }), first_name: z.string().min(1, 'First name is required'), last_name: z.string().min(1, 'Last name is required'), status: z.enum(['active', 'suspended', 'deleted'], { @@ -58,7 +58,7 @@ export const EditUserModal = ({ const [isLoadingUser, setIsLoadingUser] = useState(false); const [loadError, setLoadError] = useState(null); const loadedUserIdRef = useRef(null); - const [selectedTenantId, setSelectedTenantId] = useState(''); + // const [selectedTenantId, setSelectedTenantId] = useState(''); const [selectedRoleId, setSelectedRoleId] = useState(''); const { @@ -75,71 +75,74 @@ export const EditUserModal = ({ }); const statusValue = watch('status'); - const tenantIdValue = watch('tenant_id'); + // const tenantIdValue = watch('tenant_id'); const roleIdValue = watch('role_id'); // Store tenant and role names from user response - const [currentTenantName, setCurrentTenantName] = useState(''); + // const [currentTenantName, setCurrentTenantName] = useState(''); const [currentRoleName, setCurrentRoleName] = useState(''); // Store initial options for immediate display - const [initialTenantOption, setInitialTenantOption] = useState<{ value: string; label: string } | null>(null); + // const [initialTenantOption, setInitialTenantOption] = useState<{ value: string; label: string } | null>(null); const [initialRoleOption, setInitialRoleOption] = useState<{ value: string; label: string } | null>(null); + console.log('roleIdValue', roleIdValue); + console.log('initialRoleOption', initialRoleOption); + // Load tenants for dropdown - ensure selected tenant is included - const loadTenants = async (page: number, limit: number) => { - const response = await tenantService.getAll(page, limit); - let options = response.data.map((tenant) => ({ - value: tenant.id, - label: tenant.name, - })); + // const loadTenants = async (page: number, limit: number) => { + // const response = await tenantService.getAll(page, limit); + // let options = response.data.map((tenant) => ({ + // value: tenant.id, + // label: tenant.name, + // })); - // Always include initial option if it exists and matches the selected value - if (initialTenantOption && page === 1) { - const exists = options.find((opt) => opt.value === initialTenantOption.value); - if (!exists) { - options = [initialTenantOption, ...options]; - } - } + // // Always include initial option if it exists and matches the selected value + // if (initialTenantOption && page === 1) { + // const exists = options.find((opt) => opt.value === initialTenantOption.value); + // if (!exists) { + // options = [initialTenantOption, ...options]; + // } + // } - // If we have a selected tenant ID and it's not in the current options, add it with stored name - if (selectedTenantId && page === 1 && !initialTenantOption) { - const existingOption = options.find((opt) => opt.value === selectedTenantId); - if (!existingOption) { - // If we have the name from user response, use it; otherwise fetch - if (currentTenantName) { - options = [ - { - value: selectedTenantId, - label: currentTenantName, - }, - ...options, - ]; - } else { - try { - const tenantResponse = await tenantService.getById(selectedTenantId); - if (tenantResponse.success) { - // Prepend the selected tenant to the options - options = [ - { - value: tenantResponse.data.id, - label: tenantResponse.data.name, - }, - ...options, - ]; - } - } catch (err) { - // If fetching fails, just continue with existing options - console.warn('Failed to fetch selected tenant:', err); - } - } - } - } + // // If we have a selected tenant ID and it's not in the current options, add it with stored name + // if (selectedTenantId && page === 1 && !initialTenantOption) { + // const existingOption = options.find((opt) => opt.value === selectedTenantId); + // if (!existingOption) { + // // If we have the name from user response, use it; otherwise fetch + // if (currentTenantName) { + // options = [ + // { + // value: selectedTenantId, + // label: currentTenantName, + // }, + // ...options, + // ]; + // } else { + // try { + // const tenantResponse = await tenantService.getById(selectedTenantId); + // if (tenantResponse.success) { + // // Prepend the selected tenant to the options + // options = [ + // { + // value: tenantResponse.data.id, + // label: tenantResponse.data.name, + // }, + // ...options, + // ]; + // } + // } catch (err) { + // // If fetching fails, just continue with existing options + // console.warn('Failed to fetch selected tenant:', err); + // } + // } + // } + // } - return { - options, - pagination: response.pagination, - }; - }; + // return { + // options, + // pagination: response.pagination, + // }; + // }; // Load roles for dropdown - ensure selected role is included const loadRoles = async (page: number, limit: number) => { @@ -174,21 +177,21 @@ export const EditUserModal = ({ ...options, ]; } else { - try { - const roleResponse = await roleService.getById(selectedRoleId); - if (roleResponse.success) { - // Prepend the selected role to the options - options = [ - { - value: roleResponse.data.id, - label: roleResponse.data.name, - }, - ...options, - ]; - } - } catch (err) { - // If fetching fails, just continue with existing options - console.warn('Failed to fetch selected role:', err); + try { + const roleResponse = await roleService.getById(selectedRoleId); + if (roleResponse.success) { + // Prepend the selected role to the options + options = [ + { + value: roleResponse.data.id, + label: roleResponse.data.name, + }, + ...options, + ]; + } + } catch (err) { + // If fetching fails, just continue with existing options + console.warn('Failed to fetch selected role:', err); } } } @@ -205,62 +208,62 @@ export const EditUserModal = ({ if (isOpen && userId) { // Only load if this is a new userId or modal was closed and reopened if (loadedUserIdRef.current !== userId) { - const loadUser = async (): Promise => { - try { - setIsLoadingUser(true); - setLoadError(null); - clearErrors(); - const user = await onLoadUser(userId); + const loadUser = async (): Promise => { + try { + setIsLoadingUser(true); + setLoadError(null); + clearErrors(); + const user = await onLoadUser(userId); loadedUserIdRef.current = userId; - + // Extract tenant and role IDs from nested objects or fallback to direct properties const tenantId = user.tenant?.id || user.tenant_id || ''; const roleId = user.role?.id || user.role_id || ''; - const tenantName = user.tenant?.name || ''; + // const tenantName = user.tenant?.name || ''; const roleName = user.role?.name || ''; - - setSelectedTenantId(tenantId); - setSelectedRoleId(roleId); - setCurrentTenantName(tenantName); + + // setSelectedTenantId(tenantId); + setSelectedRoleId(roleId); + // setCurrentTenantName(tenantName); setCurrentRoleName(roleName); - + // Set initial options for immediate display using names from user response - if (tenantId && tenantName) { - setInitialTenantOption({ value: tenantId, label: tenantName }); - } + // if (tenantId && tenantName) { + // setInitialTenantOption({ value: tenantId, label: tenantName }); + // } if (roleId && roleName) { setInitialRoleOption({ value: roleId, label: roleName }); } - - reset({ - email: user.email, - first_name: user.first_name, - last_name: user.last_name, - status: user.status, - tenant_id: defaultTenantId || tenantId, - role_id: roleId, - }); - - // If defaultTenantId is provided, override tenant_id - if (defaultTenantId) { - setValue('tenant_id', defaultTenantId, { shouldValidate: true }); + + reset({ + email: user.email, + first_name: user.first_name, + last_name: user.last_name, + status: user.status, + tenant_id: defaultTenantId || tenantId, + role_id: roleId, + }); + + // If defaultTenantId is provided, override tenant_id + if (defaultTenantId) { + setValue('tenant_id', defaultTenantId, { shouldValidate: true }); + } + } catch (err: any) { + setLoadError(err?.response?.data?.error?.message || 'Failed to load user details'); + } finally { + setIsLoadingUser(false); } - } catch (err: any) { - setLoadError(err?.response?.data?.error?.message || 'Failed to load user details'); - } finally { - setIsLoadingUser(false); - } - }; - loadUser(); + }; + loadUser(); } } else if (!isOpen) { // Only reset when modal is closed loadedUserIdRef.current = null; - setSelectedTenantId(''); + // setSelectedTenantId(''); setSelectedRoleId(''); - setCurrentTenantName(''); + // setCurrentTenantName(''); setCurrentRoleName(''); - setInitialTenantOption(null); + // setInitialTenantOption(null); setInitialRoleOption(null); reset({ email: '', @@ -404,8 +407,8 @@ export const EditUserModal = ({ {/* Tenant and Role Row */} -
- {!defaultTenantId && ( +
+ {/* {!defaultTenantId && ( + )} */} + {currentRoleName !== 'Tenant Admin' && ( + setValue('role_id', value)} + onLoadOptions={loadRoles} + initialOption={initialRoleOption || undefined} + error={errors.role_id?.message} + /> )} - - setValue('role_id', value)} - onLoadOptions={loadRoles} - initialOption={initialRoleOption || undefined} - error={errors.role_id?.message} + placeholder="Select Status" + options={statusOptions} + value={statusValue} + onValueChange={(value) => setValue('status', value as 'active' | 'suspended' | 'deleted')} + error={errors.status?.message} />
- {/* Status */} - setValue('status', value as 'active' | 'suspended' | 'deleted')} - error={errors.status?.message} - />
)} diff --git a/src/components/shared/NewRoleModal.tsx b/src/components/shared/NewRoleModal.tsx index b07cc09..eece6e6 100644 --- a/src/components/shared/NewRoleModal.tsx +++ b/src/components/shared/NewRoleModal.tsx @@ -69,7 +69,7 @@ const newRoleSchema = z.object({ "Code must be lowercase and use '_' for separation (e.g. abc_def)" ), description: z.string().min(1, 'Description is required'), - modules: z.array(z.string().uuid()).optional().nullable(), + modules: z.array(z.uuid()).optional().nullable(), permissions: z.array(z.object({ resource: z.string(), action: z.string(), diff --git a/src/components/shared/NewUserModal.tsx b/src/components/shared/NewUserModal.tsx index 9f22907..1761408 100644 --- a/src/components/shared/NewUserModal.tsx +++ b/src/components/shared/NewUserModal.tsx @@ -16,7 +16,7 @@ import { roleService } from '@/services/role-service'; // Validation schema const newUserSchema = z .object({ - email: z.string().min(1, 'Email is required').email('Please enter a valid email address'), + email: z.email({ message: 'Please enter a valid email address' }), password: z.string().min(1, 'Password is required').min(6, 'Password must be at least 6 characters'), confirmPassword: z.string().min(1, 'Confirm password is required'), first_name: z.string().min(1, 'First name is required'), diff --git a/src/components/superadmin/NewModuleModal.tsx b/src/components/superadmin/NewModuleModal.tsx index 8c7fb7b..d7d4739 100644 --- a/src/components/superadmin/NewModuleModal.tsx +++ b/src/components/superadmin/NewModuleModal.tsx @@ -31,10 +31,15 @@ const newModuleSchema = z.object({ .max(50, 'runtime_language must be at most 50 characters'), framework: z.string().max(50, 'framework must be at most 50 characters').optional().nullable(), webhookurl: z.string().max(500, "webhookurl must be at most 500 characters").url("Invalid URL format").nullable(), - base_url: z + frontend_base_url: z .string() - .min(1, 'base_url is required') - .max(255, 'base_url must be at most 255 characters') + .min(1, 'frontend_base_url is required') + .max(255, 'frontend_base_url must be at most 255 characters') + .url('Invalid URL format'), + backend_base_url: z + .string() + .min(1, 'backend_base_url is required') + .max(255, 'backend_base_url must be at most 255 characters') .url('Invalid URL format'), health_endpoint: z .string() @@ -50,8 +55,8 @@ const newModuleSchema = z.object({ max_replicas: z.number().int().min(1, 'max_replicas must be at least 1').max(50, 'max_replicas must be at most 50').optional().nullable(), last_health_check: z.string().optional().nullable(), consecutive_failures: z.number().int().optional().nullable(), - registered_by: z.string().uuid().optional().nullable(), - tenant_id: z.string().uuid().optional().nullable(), + registered_by: z.uuid().optional().nullable(), + tenant_id: z.uuid().optional().nullable(), metadata: z.any().optional().nullable(), }); @@ -113,7 +118,8 @@ export const NewModuleModal = ({ runtime_language: '', framework: null, webhookurl: null, - base_url: '', + frontend_base_url: '', + backend_base_url: '', health_endpoint: '', endpoints: null, kafka_topics: null, @@ -155,7 +161,7 @@ export const NewModuleModal = ({ if (error?.response?.data?.details && Array.isArray(error.response.data.details)) { const validationErrors = error.response.data.details; validationErrors.forEach((detail: { path: string; message: string }) => { - if (detail.path === 'name' || detail.path === 'module_id' || detail.path === 'description' || detail.path === 'version' || detail.path === 'runtime_language' || detail.path === 'framework' || detail.path === 'webhookurl' || detail.path === 'base_url' || detail.path === 'health_endpoint' || detail.path === 'endpoints' || detail.path === 'kafka_topics' || detail.path === 'cpu_request' || detail.path === 'cpu_limit' || detail.path === 'memory_request' || detail.path === 'memory_limit' || detail.path === 'min_replicas' || detail.path === 'max_replicas' || detail.path === 'last_health_check' || detail.path === 'consecutive_failures' || detail.path === 'registered_by' || detail.path === 'tenant_id' || detail.path === 'metadata') { + if (detail.path === 'name' || detail.path === 'module_id' || detail.path === 'description' || detail.path === 'version' || detail.path === 'runtime_language' || detail.path === 'framework' || detail.path === 'webhookurl' || detail.path === 'frontend_base_url' || detail.path === 'backend_base_url' || detail.path === 'health_endpoint' || detail.path === 'endpoints' || detail.path === 'kafka_topics' || detail.path === 'cpu_request' || detail.path === 'cpu_limit' || detail.path === 'memory_request' || detail.path === 'memory_limit' || detail.path === 'min_replicas' || detail.path === 'max_replicas' || detail.path === 'last_health_check' || detail.path === 'consecutive_failures' || detail.path === 'registered_by' || detail.path === 'tenant_id' || detail.path === 'metadata') { setError(detail.path as keyof NewModuleFormData, { type: 'server', message: detail.message, @@ -370,14 +376,28 @@ export const NewModuleModal = ({

URL Configuration

- +
+
+ +
+
+ +
+
{module.framework || 'N/A'}

- -

{module.base_url}

+ +

{module.frontend_base_url || 'N/A'}

+
+
+ +

{module.backend_base_url || 'N/A'}

diff --git a/src/pages/superadmin/CreateTenantWizard.tsx b/src/pages/superadmin/CreateTenantWizard.tsx index b5370a8..bd7aa26 100644 --- a/src/pages/superadmin/CreateTenantWizard.tsx +++ b/src/pages/superadmin/CreateTenantWizard.tsx @@ -34,13 +34,14 @@ const tenantDetailsSchema = z.object({ }).optional().nullable(), max_users: z.number().int().min(1, 'max_users must be at least 1').optional().nullable(), max_modules: z.number().int().min(1, 'max_modules must be at least 1').optional().nullable(), - modules: z.array(z.string().uuid()).optional().nullable(), + modules: z.array(z.uuid()).optional().nullable(), }); // Step 2: Contact Details Schema - user creation + organization address const contactDetailsSchema = z .object({ - email: z.string().min(1, 'Email is required').email('Please enter a valid email address'), + email: z + .email({ message: 'Please enter a valid email address' }), password: z.string().min(1, 'Password is required').min(6, 'Password must be at least 6 characters'), confirmPassword: z.string().min(1, 'Confirm password is required'), first_name: z.string().min(1, 'First name is required'), @@ -113,6 +114,8 @@ const CreateTenantWizard = (): ReactElement => { // Form instances for each step const tenantDetailsForm = useForm({ resolver: zodResolver(tenantDetailsSchema), + mode: 'onChange', + reValidateMode: 'onChange', defaultValues: { name: '', slug: '', @@ -139,6 +142,8 @@ const CreateTenantWizard = (): ReactElement => { const contactDetailsForm = useForm({ resolver: zodResolver(contactDetailsSchema), + mode: 'onChange', + reValidateMode: 'onChange', defaultValues: { email: '', password: '', @@ -157,6 +162,8 @@ const CreateTenantWizard = (): ReactElement => { const settingsForm = useForm({ resolver: zodResolver(settingsSchema), + mode: 'onChange', + reValidateMode: 'onChange', defaultValues: { enable_sso: false, enable_2fa: false, @@ -331,14 +338,18 @@ const CreateTenantWizard = (): ReactElement => { setLogoError(null); setFaviconError(null); - if (!logoFileUrl && !logoFile) { + const isLogoMissing = !logoFileUrl && !logoFile; + const isFaviconMissing = !faviconFileUrl && !faviconFile; + + if (isLogoMissing) { setLogoError('Logo is required'); - setCurrentStep(3); // Go to settings step where logo/favicon are - return; } - if (!faviconFileUrl && !faviconFile) { + if (isFaviconMissing) { setFaviconError('Favicon is required'); + } + + if (isLogoMissing || isFaviconMissing) { setCurrentStep(3); // Go to settings step where logo/favicon are return; } @@ -379,40 +390,122 @@ const CreateTenantWizard = (): ReactElement => { showToast.success(message); navigate('/tenants'); } catch (err: any) { - // Handle validation errors from API - same as NewTenantModal + // Clear previous errors + tenantDetailsForm.clearErrors(); + contactDetailsForm.clearErrors(); + settingsForm.clearErrors(); + + // Handle validation errors from API if (err?.response?.data?.details && Array.isArray(err.response.data.details)) { const validationErrors = err.response.data.details; + let hasTenantErrors = false; + let hasContactErrors = false; + let hasSettingsErrors = false; + validationErrors.forEach((detail: { path: string; message: string }) => { - // Handle tenant details errors - if ( - detail.path === 'name' || - detail.path === 'slug' || - detail.path === 'status' || - detail.path === 'subscription_tier' || - detail.path === 'max_users' || - detail.path === 'max_modules' || - detail.path === 'module_ids' + const path = detail.path; + + // Handle nested paths first + if (path.startsWith('settings.contact.')) { + // Contact details errors from nested path + hasContactErrors = true; + const fieldName = path.replace('settings.contact.', ''); + if (fieldName === 'confirmPassword') { + // Skip confirmPassword as it's not in the form schema + return; + } + contactDetailsForm.setError(fieldName as keyof ContactDetailsForm, { + type: 'server', + message: detail.message, + }); + } else if (path.startsWith('settings.branding.')) { + // Settings/branding errors from nested path + hasSettingsErrors = true; + const fieldName = path.replace('settings.branding.', ''); + // Map file_path fields to form fields + if (fieldName === 'logo_file_path') { + setLogoError(detail.message); + } else if (fieldName === 'favicon_file_path') { + setFaviconError(detail.message); + } else { + settingsForm.setError(fieldName as keyof SettingsForm, { + type: 'server', + message: detail.message, + }); + } + } else if (path.startsWith('settings.')) { + // Other settings errors + hasSettingsErrors = true; + const fieldName = path.replace('settings.', ''); + settingsForm.setError(fieldName as keyof SettingsForm, { + type: 'server', + message: detail.message, + }); + } + // Handle tenant details errors (step 1) + else if ( + path === 'name' || + path === 'slug' || + path === 'domain' || + path === 'status' || + path === 'subscription_tier' || + path === 'max_users' || + path === 'max_modules' || + path === 'module_ids' ) { - const fieldPath = detail.path === 'module_ids' ? 'modules' : detail.path; + hasTenantErrors = true; + const fieldPath = path === 'module_ids' ? 'modules' : path; tenantDetailsForm.setError(fieldPath as keyof TenantDetailsForm, { type: 'server', message: detail.message, }); } - // Handle contact details errors + // Handle contact details errors (step 2) - direct paths else if ( - detail.path === 'email' || - detail.path === 'password' || - detail.path === 'first_name' || - detail.path === 'last_name' + path === 'email' || + path === 'password' || + path === 'first_name' || + path === 'last_name' || + path === 'contact_phone' || + path === 'address_line1' || + path === 'address_line2' || + path === 'city' || + path === 'state' || + path === 'postal_code' || + path === 'country' ) { - contactDetailsForm.setError(detail.path as keyof ContactDetailsForm, { + hasContactErrors = true; + contactDetailsForm.setError(path as keyof ContactDetailsForm, { + type: 'server', + message: detail.message, + }); + } + // Handle settings errors (step 3) - direct paths + else if ( + path === 'enable_sso' || + path === 'enable_2fa' || + path === 'primary_color' || + path === 'secondary_color' || + path === 'accent_color' + ) { + hasSettingsErrors = true; + settingsForm.setError(path as keyof SettingsForm, { type: 'server', message: detail.message, }); } }); + + // Navigate to the step with errors + if (hasTenantErrors) { + setCurrentStep(1); + } else if (hasContactErrors) { + setCurrentStep(2); + } else if (hasSettingsErrors) { + setCurrentStep(3); + } } else { + // Handle general errors const errorObj = err?.response?.data?.error; const errorMessage = (typeof errorObj === 'object' && errorObj !== null && 'message' in errorObj ? errorObj.message : null) || @@ -425,6 +518,7 @@ const CreateTenantWizard = (): ReactElement => { message: typeof errorMessage === 'string' ? errorMessage : 'Failed to create tenant. Please try again.', }); showToast.error(typeof errorMessage === 'string' ? errorMessage : 'Failed to create tenant. Please try again.'); + setCurrentStep(1); // Navigate to first step for general errors } } finally { setIsSubmitting(false); @@ -778,6 +872,12 @@ const CreateTenantWizard = (): ReactElement => { Set resource limits and security preferences for this tenant.

+ {/* General Error Display */} + {settingsForm.formState.errors.root && ( +
+

{settingsForm.formState.errors.root.message}

+
+ )} {/* Branding Section */}
diff --git a/src/pages/superadmin/EditTenant.tsx b/src/pages/superadmin/EditTenant.tsx index 8004cc3..0b5507e 100644 --- a/src/pages/superadmin/EditTenant.tsx +++ b/src/pages/superadmin/EditTenant.tsx @@ -34,12 +34,12 @@ const tenantDetailsSchema = z.object({ }).optional().nullable(), max_users: z.number().int().min(1, 'max_users must be at least 1').optional().nullable(), max_modules: z.number().int().min(1, 'max_modules must be at least 1').optional().nullable(), - modules: z.array(z.string().uuid()).optional().nullable(), + modules: z.array(z.uuid()).optional().nullable(), }); // Step 2: Contact Details Schema - NO password fields const contactDetailsSchema = z.object({ - email: z.string().min(1, 'Email is required').email('Please enter a valid email address'), + email: z.email({ message: 'Please enter a valid email address' }), first_name: z.string().min(1, 'First name is required'), last_name: z.string().min(1, 'Last name is required'), contact_phone: z @@ -122,6 +122,8 @@ const EditTenant = (): ReactElement => { // Form instances for each step const tenantDetailsForm = useForm({ resolver: zodResolver(tenantDetailsSchema), + mode: 'onChange', + reValidateMode: 'onChange', defaultValues: { name: '', slug: '', @@ -136,6 +138,8 @@ const EditTenant = (): ReactElement => { const contactDetailsForm = useForm({ resolver: zodResolver(contactDetailsSchema), + mode: 'onChange', + reValidateMode: 'onChange', defaultValues: { email: '', first_name: '', @@ -152,6 +156,8 @@ const EditTenant = (): ReactElement => { const settingsForm = useForm({ resolver: zodResolver(settingsSchema), + mode: 'onChange', + reValidateMode: 'onChange', defaultValues: { enable_sso: false, enable_2fa: false, @@ -433,14 +439,18 @@ const EditTenant = (): ReactElement => { setLogoError(null); setFaviconError(null); - if (!logoFilePath) { + const isLogoMissing = !logoFilePath; + const isFaviconMissing = !faviconFilePath; + + if (isLogoMissing) { setLogoError('Logo is required'); - setCurrentStep(3); // Go to settings step where logo/favicon are - return; } - if (!faviconFilePath) { + if (isFaviconMissing) { setFaviconError('Favicon is required'); + } + + if (isLogoMissing || isFaviconMissing) { setCurrentStep(3); // Go to settings step where logo/favicon are return; } @@ -476,47 +486,130 @@ const EditTenant = (): ReactElement => { showToast.success(message); navigate('/tenants'); } catch (err: any) { + // Clear previous errors + tenantDetailsForm.clearErrors(); + contactDetailsForm.clearErrors(); + settingsForm.clearErrors(); + + // Handle validation errors from API if (err?.response?.data?.details && Array.isArray(err.response.data.details)) { const validationErrors = err.response.data.details; + let hasTenantErrors = false; + let hasContactErrors = false; + let hasSettingsErrors = false; + validationErrors.forEach((detail: { path: string; message: string }) => { - if ( - detail.path === 'name' || - detail.path === 'slug' || - detail.path === 'status' || - detail.path === 'subscription_tier' || - detail.path === 'max_users' || - detail.path === 'max_modules' || - detail.path === 'module_ids' + const path = detail.path; + + // Handle nested paths first + if (path.startsWith('settings.contact.')) { + // Contact details errors from nested path + hasContactErrors = true; + const fieldName = path.replace('settings.contact.', ''); + contactDetailsForm.setError(fieldName as keyof ContactDetailsForm, { + type: 'server', + message: detail.message, + }); + } else if (path.startsWith('settings.branding.')) { + // Settings/branding errors from nested path + hasSettingsErrors = true; + const fieldName = path.replace('settings.branding.', ''); + // Map file_path fields to form fields + if (fieldName === 'logo_file_path') { + setLogoError(detail.message); + } else if (fieldName === 'favicon_file_path') { + setFaviconError(detail.message); + } else { + settingsForm.setError(fieldName as keyof SettingsForm, { + type: 'server', + message: detail.message, + }); + } + } else if (path.startsWith('settings.')) { + // Other settings errors + hasSettingsErrors = true; + const fieldName = path.replace('settings.', ''); + settingsForm.setError(fieldName as keyof SettingsForm, { + type: 'server', + message: detail.message, + }); + } + // Handle tenant details errors (step 1) + else if ( + path === 'name' || + path === 'slug' || + path === 'domain' || + path === 'status' || + path === 'subscription_tier' || + path === 'max_users' || + path === 'max_modules' || + path === 'module_ids' ) { - const fieldPath = detail.path === 'module_ids' ? 'modules' : detail.path; + hasTenantErrors = true; + const fieldPath = path === 'module_ids' ? 'modules' : path; tenantDetailsForm.setError(fieldPath as keyof TenantDetailsForm, { type: 'server', message: detail.message, }); - } else if ( - detail.path === 'email' || - detail.path === 'first_name' || - detail.path === 'last_name' || - detail.path === 'contact_phone' || - detail.path === 'address_line1' || - detail.path === 'city' || - detail.path === 'state' || - detail.path === 'postal_code' || - detail.path === 'country' + } + // Handle contact details errors (step 2) - direct paths + else if ( + path === 'email' || + path === 'first_name' || + path === 'last_name' || + path === 'contact_phone' || + path === 'address_line1' || + path === 'address_line2' || + path === 'city' || + path === 'state' || + path === 'postal_code' || + path === 'country' ) { - contactDetailsForm.setError(detail.path as keyof ContactDetailsForm, { + hasContactErrors = true; + contactDetailsForm.setError(path as keyof ContactDetailsForm, { + type: 'server', + message: detail.message, + }); + } + // Handle settings errors (step 3) - direct paths + else if ( + path === 'enable_sso' || + path === 'enable_2fa' || + path === 'primary_color' || + path === 'secondary_color' || + path === 'accent_color' + ) { + hasSettingsErrors = true; + settingsForm.setError(path as keyof SettingsForm, { type: 'server', message: detail.message, }); } }); + + // Navigate to the step with errors + if (hasTenantErrors) { + setCurrentStep(1); + } else if (hasContactErrors) { + setCurrentStep(2); + } else if (hasSettingsErrors) { + setCurrentStep(3); + } } else { + // Handle general errors + const errorObj = err?.response?.data?.error; const errorMessage = - err?.response?.data?.error?.message || + (typeof errorObj === 'object' && errorObj !== null && 'message' in errorObj ? errorObj.message : null) || + (typeof errorObj === 'string' ? errorObj : null) || err?.response?.data?.message || err?.message || 'Failed to update tenant. Please try again.'; - showToast.error(errorMessage); + tenantDetailsForm.setError('root', { + type: 'server', + message: typeof errorMessage === 'string' ? errorMessage : 'Failed to update tenant. Please try again.', + }); + showToast.error(typeof errorMessage === 'string' ? errorMessage : 'Failed to update tenant. Please try again.'); + setCurrentStep(1); // Navigate to first step for general errors } } finally { setIsSubmitting(false); @@ -878,6 +971,12 @@ const EditTenant = (): ReactElement => {

Configuration & Limits

Set resource limits and security preferences for this tenant.

+ {/* General Error Display */} + {settingsForm.formState.errors.root && ( +
+

{settingsForm.formState.errors.root.message}

+
+ )} {/* Branding Section - Same as CreateTenantWizard */}
diff --git a/src/pages/tenant/Settings.tsx b/src/pages/tenant/Settings.tsx index 051e83f..05ad0f1 100644 --- a/src/pages/tenant/Settings.tsx +++ b/src/pages/tenant/Settings.tsx @@ -245,13 +245,18 @@ const Settings = (): ReactElement => { setLogoError(null); setFaviconError(null); - if (!logoFilePath) { + const isLogoMissing = !logoFilePath; + const isFaviconMissing = !faviconFilePath; + + if (isLogoMissing) { setLogoError('Logo is required'); - return; } - if (!faviconFilePath) { + if (isFaviconMissing) { setFaviconError('Favicon is required'); + } + + if (isLogoMissing || isFaviconMissing) { return; } diff --git a/src/types/module.ts b/src/types/module.ts index ca5a88f..519b07b 100644 --- a/src/types/module.ts +++ b/src/types/module.ts @@ -8,7 +8,8 @@ export interface Module { runtime_language: string | null; framework: string | null; webhookurl: string | null; - base_url: string; + frontend_base_url: string; + backend_base_url: string; health_endpoint: string; endpoints: string[] | null; kafka_topics: string[] | null;