feat: Add LTTS logo and update various UI components and pages across superadmin and tenant sections.
This commit is contained in:
parent
6efcb90f3e
commit
757a9f216b
@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/LTTS.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>qassure-frontend</title>
|
<title>qassure-frontend</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
4
public/LTTS.svg
Normal file
4
public/LTTS.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 24 KiB |
@ -78,6 +78,35 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
|||||||
|
|
||||||
const isSuperAdmin = isSuperAdminCheck();
|
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
|
// Fetch theme if tenant admin
|
||||||
if (!isSuperAdmin) {
|
if (!isSuperAdmin) {
|
||||||
useTenantTheme();
|
useTenantTheme();
|
||||||
@ -212,8 +241,20 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
|||||||
>
|
>
|
||||||
<Shield className="w-6 h-6 text-white" strokeWidth={1.67} />
|
<Shield className="w-6 h-6 text-white" strokeWidth={1.67} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[18px] font-bold text-[#0f1724] tracking-[-0.36px]">
|
<div className="flex flex-col">
|
||||||
{(!isSuperAdmin && logoUrl) ? '' : 'QAssure'}
|
<div className="text-[18px] font-bold text-[#0f1724] tracking-[-0.36px]">
|
||||||
|
{(!isSuperAdmin && logoUrl) ? '' : 'QAssure'}
|
||||||
|
</div>
|
||||||
|
{(!isSuperAdmin && logoUrl) ? null : (
|
||||||
|
<div
|
||||||
|
className="text-[12px] font-semibold capitalize mt-[7px] -translate-y-1/2"
|
||||||
|
style={{
|
||||||
|
color: !isSuperAdmin && theme?.accent_color ? theme.accent_color : '#084cc8',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{roleName}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@ -238,6 +279,18 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
|||||||
<span className="text-[13px] font-medium text-[#0f1724]">Support Center</span>
|
<span className="text-[13px] font-medium text-[#0f1724]">Support Center</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Powered by LTTS */}
|
||||||
|
<div className="w-full flex flex-col items-center justify-center gap-0 px-2">
|
||||||
|
<p className="text-[10px] font-medium text-[#9ca3af] capitalize leading-normal">
|
||||||
|
Powered by
|
||||||
|
</p>
|
||||||
|
<img
|
||||||
|
src="/LTTS.svg"
|
||||||
|
alt="L&T Technology Services"
|
||||||
|
className="h-6 w-auto max-w-[150px] object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Desktop Sidebar */}
|
{/* Desktop Sidebar */}
|
||||||
@ -266,8 +319,20 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
|||||||
>
|
>
|
||||||
<Shield className="w-6 h-6 text-white" strokeWidth={1.67} />
|
<Shield className="w-6 h-6 text-white" strokeWidth={1.67} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-base md:text-base lg:text-base xl:text-[18px] font-bold text-[#0f1724] tracking-[-0.36px]">
|
<div className="flex flex-col">
|
||||||
{(!isSuperAdmin && logoUrl) ? '' : 'QAssure'}
|
<div className="text-base md:text-base lg:text-base xl:text-[18px] font-bold text-[#0f1724] tracking-[-0.36px]">
|
||||||
|
{(!isSuperAdmin && logoUrl) ? '' : 'QAssure'}
|
||||||
|
</div>
|
||||||
|
{(!isSuperAdmin && logoUrl) ? null : (
|
||||||
|
<div
|
||||||
|
className="text-[12px] font-semibold capitalize mt-[7px] -translate-y-1/2"
|
||||||
|
style={{
|
||||||
|
color: !isSuperAdmin && theme?.accent_color ? theme.accent_color : '#084cc8',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{roleName}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -289,6 +354,18 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
|||||||
<span className="text-xs md:text-xs lg:text-[13px] font-medium text-[#0f1724]">Support Center</span>
|
<span className="text-xs md:text-xs lg:text-[13px] font-medium text-[#0f1724]">Support Center</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Powered by LTTS */}
|
||||||
|
<div className="w-full flex flex-col items-center justify-center gap-0 px-2">
|
||||||
|
<p className="text-[10px] font-medium text-[#9ca3af] capitalize leading-normal">
|
||||||
|
Powered by
|
||||||
|
</p>
|
||||||
|
<img
|
||||||
|
src="/LTTS.svg"
|
||||||
|
alt="L&T Technology Services"
|
||||||
|
className="h-6 w-auto max-w-[150px] object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export const DataTable = <T,>({
|
|||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
key={column.key}
|
key={column.key}
|
||||||
className={`px-3 md:px-4 lg:px-5 py-2 md:py-2.5 lg:py-3 ${alignClass} text-[10px] md:text-xs font-medium text-[#9aa6b2] uppercase`}
|
className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-xs font-medium text-[#9aa6b2] uppercase`}
|
||||||
>
|
>
|
||||||
{column.label}
|
{column.label}
|
||||||
</th>
|
</th>
|
||||||
@ -75,7 +75,7 @@ export const DataTable = <T,>({
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={columns.length} className="px-3 md:px-4 lg:px-5 py-6 md:py-7 lg:py-8 text-center text-xs md:text-sm text-[#6b7280]">
|
<td colSpan={columns.length} className="px-3 md:px-2 lg:px-4 xl:px-5 py-6 md:py-3 lg:py-7 xl:py-8 text-center text-xs md:text-xs lg:text-sm text-[#6b7280]">
|
||||||
{emptyMessage}
|
{emptyMessage}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -106,14 +106,14 @@ export const DataTable = <T,>({
|
|||||||
: column.align === 'center'
|
: column.align === 'center'
|
||||||
? 'text-center'
|
? 'text-center'
|
||||||
: 'text-left';
|
: 'text-left';
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
key={column.key}
|
key={column.key}
|
||||||
className={`px-3 md:px-4 lg:px-5 py-2 md:py-2.5 lg:py-3 ${alignClass} text-[10px] md:text-xs font-medium text-[#9aa6b2] uppercase`}
|
className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-xs font-medium text-[#9aa6b2] uppercase`}
|
||||||
>
|
>
|
||||||
{column.label}
|
{column.label}
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -131,7 +131,7 @@ export const DataTable = <T,>({
|
|||||||
? 'text-center'
|
? 'text-center'
|
||||||
: 'text-left';
|
: 'text-left';
|
||||||
return (
|
return (
|
||||||
<td key={column.key} className={`px-3 md:px-4 lg:px-5 py-2.5 md:py-3 lg:py-4 ${alignClass} text-xs md:text-sm`}>
|
<td key={column.key} className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2.5 md:py-1.5 lg:py-3 xl:py-4 ${alignClass} text-xs md:text-[10px] lg:text-sm`}>
|
||||||
{column.render ? column.render(item) : String((item as any)[column.key])}
|
{column.render ? column.render(item) : String((item as any)[column.key])}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -69,7 +69,7 @@ const editRoleSchema = z.object({
|
|||||||
"Code must be lowercase and use '_' for separation (e.g. abc_def)"
|
"Code must be lowercase and use '_' for separation (e.g. abc_def)"
|
||||||
),
|
),
|
||||||
description: z.string().min(1, 'Description is required'),
|
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({
|
permissions: z.array(z.object({
|
||||||
resource: z.string(),
|
resource: z.string(),
|
||||||
action: z.string(),
|
action: z.string(),
|
||||||
|
|||||||
@ -12,13 +12,13 @@ import {
|
|||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
SecondaryButton,
|
SecondaryButton,
|
||||||
} from '@/components/shared';
|
} from '@/components/shared';
|
||||||
import { tenantService } from '@/services/tenant-service';
|
// import { tenantService } from '@/services/tenant-service';
|
||||||
import { roleService } from '@/services/role-service';
|
import { roleService } from '@/services/role-service';
|
||||||
import type { User } from '@/types/user';
|
import type { User } from '@/types/user';
|
||||||
|
|
||||||
// Validation schema
|
// Validation schema
|
||||||
const editUserSchema = z.object({
|
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'),
|
first_name: z.string().min(1, 'First name is required'),
|
||||||
last_name: z.string().min(1, 'Last name is required'),
|
last_name: z.string().min(1, 'Last name is required'),
|
||||||
status: z.enum(['active', 'suspended', 'deleted'], {
|
status: z.enum(['active', 'suspended', 'deleted'], {
|
||||||
@ -58,7 +58,7 @@ export const EditUserModal = ({
|
|||||||
const [isLoadingUser, setIsLoadingUser] = useState<boolean>(false);
|
const [isLoadingUser, setIsLoadingUser] = useState<boolean>(false);
|
||||||
const [loadError, setLoadError] = useState<string | null>(null);
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
const loadedUserIdRef = useRef<string | null>(null);
|
const loadedUserIdRef = useRef<string | null>(null);
|
||||||
const [selectedTenantId, setSelectedTenantId] = useState<string>('');
|
// const [selectedTenantId, setSelectedTenantId] = useState<string>('');
|
||||||
const [selectedRoleId, setSelectedRoleId] = useState<string>('');
|
const [selectedRoleId, setSelectedRoleId] = useState<string>('');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -75,71 +75,74 @@ export const EditUserModal = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const statusValue = watch('status');
|
const statusValue = watch('status');
|
||||||
const tenantIdValue = watch('tenant_id');
|
// const tenantIdValue = watch('tenant_id');
|
||||||
const roleIdValue = watch('role_id');
|
const roleIdValue = watch('role_id');
|
||||||
|
|
||||||
// Store tenant and role names from user response
|
// Store tenant and role names from user response
|
||||||
const [currentTenantName, setCurrentTenantName] = useState<string>('');
|
// const [currentTenantName, setCurrentTenantName] = useState<string>('');
|
||||||
const [currentRoleName, setCurrentRoleName] = useState<string>('');
|
const [currentRoleName, setCurrentRoleName] = useState<string>('');
|
||||||
// Store initial options for immediate display
|
// 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);
|
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
|
// Load tenants for dropdown - ensure selected tenant is included
|
||||||
const loadTenants = async (page: number, limit: number) => {
|
// const loadTenants = async (page: number, limit: number) => {
|
||||||
const response = await tenantService.getAll(page, limit);
|
// const response = await tenantService.getAll(page, limit);
|
||||||
let options = response.data.map((tenant) => ({
|
// let options = response.data.map((tenant) => ({
|
||||||
value: tenant.id,
|
// value: tenant.id,
|
||||||
label: tenant.name,
|
// label: tenant.name,
|
||||||
}));
|
// }));
|
||||||
|
|
||||||
// Always include initial option if it exists and matches the selected value
|
// // Always include initial option if it exists and matches the selected value
|
||||||
if (initialTenantOption && page === 1) {
|
// if (initialTenantOption && page === 1) {
|
||||||
const exists = options.find((opt) => opt.value === initialTenantOption.value);
|
// const exists = options.find((opt) => opt.value === initialTenantOption.value);
|
||||||
if (!exists) {
|
// if (!exists) {
|
||||||
options = [initialTenantOption, ...options];
|
// options = [initialTenantOption, ...options];
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// If we have a selected tenant ID and it's not in the current options, add it with stored name
|
// // 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) {
|
// if (selectedTenantId && page === 1 && !initialTenantOption) {
|
||||||
const existingOption = options.find((opt) => opt.value === selectedTenantId);
|
// const existingOption = options.find((opt) => opt.value === selectedTenantId);
|
||||||
if (!existingOption) {
|
// if (!existingOption) {
|
||||||
// If we have the name from user response, use it; otherwise fetch
|
// // If we have the name from user response, use it; otherwise fetch
|
||||||
if (currentTenantName) {
|
// if (currentTenantName) {
|
||||||
options = [
|
// options = [
|
||||||
{
|
// {
|
||||||
value: selectedTenantId,
|
// value: selectedTenantId,
|
||||||
label: currentTenantName,
|
// label: currentTenantName,
|
||||||
},
|
// },
|
||||||
...options,
|
// ...options,
|
||||||
];
|
// ];
|
||||||
} else {
|
// } else {
|
||||||
try {
|
// try {
|
||||||
const tenantResponse = await tenantService.getById(selectedTenantId);
|
// const tenantResponse = await tenantService.getById(selectedTenantId);
|
||||||
if (tenantResponse.success) {
|
// if (tenantResponse.success) {
|
||||||
// Prepend the selected tenant to the options
|
// // Prepend the selected tenant to the options
|
||||||
options = [
|
// options = [
|
||||||
{
|
// {
|
||||||
value: tenantResponse.data.id,
|
// value: tenantResponse.data.id,
|
||||||
label: tenantResponse.data.name,
|
// label: tenantResponse.data.name,
|
||||||
},
|
// },
|
||||||
...options,
|
// ...options,
|
||||||
];
|
// ];
|
||||||
}
|
// }
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
// If fetching fails, just continue with existing options
|
// // If fetching fails, just continue with existing options
|
||||||
console.warn('Failed to fetch selected tenant:', err);
|
// console.warn('Failed to fetch selected tenant:', err);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return {
|
// return {
|
||||||
options,
|
// options,
|
||||||
pagination: response.pagination,
|
// pagination: response.pagination,
|
||||||
};
|
// };
|
||||||
};
|
// };
|
||||||
|
|
||||||
// Load roles for dropdown - ensure selected role is included
|
// Load roles for dropdown - ensure selected role is included
|
||||||
const loadRoles = async (page: number, limit: number) => {
|
const loadRoles = async (page: number, limit: number) => {
|
||||||
@ -174,21 +177,21 @@ export const EditUserModal = ({
|
|||||||
...options,
|
...options,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const roleResponse = await roleService.getById(selectedRoleId);
|
const roleResponse = await roleService.getById(selectedRoleId);
|
||||||
if (roleResponse.success) {
|
if (roleResponse.success) {
|
||||||
// Prepend the selected role to the options
|
// Prepend the selected role to the options
|
||||||
options = [
|
options = [
|
||||||
{
|
{
|
||||||
value: roleResponse.data.id,
|
value: roleResponse.data.id,
|
||||||
label: roleResponse.data.name,
|
label: roleResponse.data.name,
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If fetching fails, just continue with existing options
|
// If fetching fails, just continue with existing options
|
||||||
console.warn('Failed to fetch selected role:', err);
|
console.warn('Failed to fetch selected role:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -205,62 +208,62 @@ export const EditUserModal = ({
|
|||||||
if (isOpen && userId) {
|
if (isOpen && userId) {
|
||||||
// Only load if this is a new userId or modal was closed and reopened
|
// Only load if this is a new userId or modal was closed and reopened
|
||||||
if (loadedUserIdRef.current !== userId) {
|
if (loadedUserIdRef.current !== userId) {
|
||||||
const loadUser = async (): Promise<void> => {
|
const loadUser = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
setIsLoadingUser(true);
|
setIsLoadingUser(true);
|
||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
clearErrors();
|
clearErrors();
|
||||||
const user = await onLoadUser(userId);
|
const user = await onLoadUser(userId);
|
||||||
loadedUserIdRef.current = userId;
|
loadedUserIdRef.current = userId;
|
||||||
|
|
||||||
// Extract tenant and role IDs from nested objects or fallback to direct properties
|
// Extract tenant and role IDs from nested objects or fallback to direct properties
|
||||||
const tenantId = user.tenant?.id || user.tenant_id || '';
|
const tenantId = user.tenant?.id || user.tenant_id || '';
|
||||||
const roleId = user.role?.id || user.role_id || '';
|
const roleId = user.role?.id || user.role_id || '';
|
||||||
const tenantName = user.tenant?.name || '';
|
// const tenantName = user.tenant?.name || '';
|
||||||
const roleName = user.role?.name || '';
|
const roleName = user.role?.name || '';
|
||||||
|
|
||||||
setSelectedTenantId(tenantId);
|
// setSelectedTenantId(tenantId);
|
||||||
setSelectedRoleId(roleId);
|
setSelectedRoleId(roleId);
|
||||||
setCurrentTenantName(tenantName);
|
// setCurrentTenantName(tenantName);
|
||||||
setCurrentRoleName(roleName);
|
setCurrentRoleName(roleName);
|
||||||
|
|
||||||
// Set initial options for immediate display using names from user response
|
// Set initial options for immediate display using names from user response
|
||||||
if (tenantId && tenantName) {
|
// if (tenantId && tenantName) {
|
||||||
setInitialTenantOption({ value: tenantId, label: tenantName });
|
// setInitialTenantOption({ value: tenantId, label: tenantName });
|
||||||
}
|
// }
|
||||||
if (roleId && roleName) {
|
if (roleId && roleName) {
|
||||||
setInitialRoleOption({ value: roleId, label: roleName });
|
setInitialRoleOption({ value: roleId, label: roleName });
|
||||||
}
|
}
|
||||||
|
|
||||||
reset({
|
reset({
|
||||||
email: user.email,
|
email: user.email,
|
||||||
first_name: user.first_name,
|
first_name: user.first_name,
|
||||||
last_name: user.last_name,
|
last_name: user.last_name,
|
||||||
status: user.status,
|
status: user.status,
|
||||||
tenant_id: defaultTenantId || tenantId,
|
tenant_id: defaultTenantId || tenantId,
|
||||||
role_id: roleId,
|
role_id: roleId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If defaultTenantId is provided, override tenant_id
|
// If defaultTenantId is provided, override tenant_id
|
||||||
if (defaultTenantId) {
|
if (defaultTenantId) {
|
||||||
setValue('tenant_id', defaultTenantId, { shouldValidate: true });
|
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');
|
loadUser();
|
||||||
} finally {
|
|
||||||
setIsLoadingUser(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadUser();
|
|
||||||
}
|
}
|
||||||
} else if (!isOpen) {
|
} else if (!isOpen) {
|
||||||
// Only reset when modal is closed
|
// Only reset when modal is closed
|
||||||
loadedUserIdRef.current = null;
|
loadedUserIdRef.current = null;
|
||||||
setSelectedTenantId('');
|
// setSelectedTenantId('');
|
||||||
setSelectedRoleId('');
|
setSelectedRoleId('');
|
||||||
setCurrentTenantName('');
|
// setCurrentTenantName('');
|
||||||
setCurrentRoleName('');
|
setCurrentRoleName('');
|
||||||
setInitialTenantOption(null);
|
// setInitialTenantOption(null);
|
||||||
setInitialRoleOption(null);
|
setInitialRoleOption(null);
|
||||||
reset({
|
reset({
|
||||||
email: '',
|
email: '',
|
||||||
@ -404,8 +407,8 @@ export const EditUserModal = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tenant and Role Row */}
|
{/* Tenant and Role Row */}
|
||||||
<div className={`grid ${defaultTenantId ? 'grid-cols-1' : 'grid-cols-2'} gap-5 pb-4`}>
|
<div className={`grid grid-cols-2 gap-5 pb-4`}>
|
||||||
{!defaultTenantId && (
|
{/* {!defaultTenantId && (
|
||||||
<PaginatedSelect
|
<PaginatedSelect
|
||||||
label="Assign Tenant"
|
label="Assign Tenant"
|
||||||
required
|
required
|
||||||
@ -416,30 +419,31 @@ export const EditUserModal = ({
|
|||||||
initialOption={initialTenantOption || undefined}
|
initialOption={initialTenantOption || undefined}
|
||||||
error={errors.tenant_id?.message}
|
error={errors.tenant_id?.message}
|
||||||
/>
|
/>
|
||||||
|
)} */}
|
||||||
|
{currentRoleName !== 'Tenant Admin' && (
|
||||||
|
<PaginatedSelect
|
||||||
|
label="Assign Role"
|
||||||
|
required
|
||||||
|
placeholder="Select Role"
|
||||||
|
value={roleIdValue || ''}
|
||||||
|
onValueChange={(value) => setValue('role_id', value)}
|
||||||
|
onLoadOptions={loadRoles}
|
||||||
|
initialOption={initialRoleOption || undefined}
|
||||||
|
error={errors.role_id?.message}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
{/* Status */}
|
||||||
<PaginatedSelect
|
<FormSelect
|
||||||
label="Assign Role"
|
label="Status"
|
||||||
required
|
required
|
||||||
placeholder="Select Role"
|
placeholder="Select Status"
|
||||||
value={roleIdValue || ''}
|
options={statusOptions}
|
||||||
onValueChange={(value) => setValue('role_id', value)}
|
value={statusValue}
|
||||||
onLoadOptions={loadRoles}
|
onValueChange={(value) => setValue('status', value as 'active' | 'suspended' | 'deleted')}
|
||||||
initialOption={initialRoleOption || undefined}
|
error={errors.status?.message}
|
||||||
error={errors.role_id?.message}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status */}
|
|
||||||
<FormSelect
|
|
||||||
label="Status"
|
|
||||||
required
|
|
||||||
placeholder="Select Status"
|
|
||||||
options={statusOptions}
|
|
||||||
value={statusValue}
|
|
||||||
onValueChange={(value) => setValue('status', value as 'active' | 'suspended' | 'deleted')}
|
|
||||||
error={errors.status?.message}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -69,7 +69,7 @@ const newRoleSchema = z.object({
|
|||||||
"Code must be lowercase and use '_' for separation (e.g. abc_def)"
|
"Code must be lowercase and use '_' for separation (e.g. abc_def)"
|
||||||
),
|
),
|
||||||
description: z.string().min(1, 'Description is required'),
|
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({
|
permissions: z.array(z.object({
|
||||||
resource: z.string(),
|
resource: z.string(),
|
||||||
action: z.string(),
|
action: z.string(),
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import { roleService } from '@/services/role-service';
|
|||||||
// Validation schema
|
// Validation schema
|
||||||
const newUserSchema = z
|
const newUserSchema = z
|
||||||
.object({
|
.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'),
|
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'),
|
confirmPassword: z.string().min(1, 'Confirm password is required'),
|
||||||
first_name: z.string().min(1, 'First name is required'),
|
first_name: z.string().min(1, 'First name is required'),
|
||||||
|
|||||||
@ -31,10 +31,15 @@ const newModuleSchema = z.object({
|
|||||||
.max(50, 'runtime_language must be at most 50 characters'),
|
.max(50, 'runtime_language must be at most 50 characters'),
|
||||||
framework: z.string().max(50, 'framework must be at most 50 characters').optional().nullable(),
|
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(),
|
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()
|
.string()
|
||||||
.min(1, 'base_url is required')
|
.min(1, 'frontend_base_url is required')
|
||||||
.max(255, 'base_url must be at most 255 characters')
|
.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'),
|
.url('Invalid URL format'),
|
||||||
health_endpoint: z
|
health_endpoint: z
|
||||||
.string()
|
.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(),
|
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(),
|
last_health_check: z.string().optional().nullable(),
|
||||||
consecutive_failures: z.number().int().optional().nullable(),
|
consecutive_failures: z.number().int().optional().nullable(),
|
||||||
registered_by: z.string().uuid().optional().nullable(),
|
registered_by: z.uuid().optional().nullable(),
|
||||||
tenant_id: z.string().uuid().optional().nullable(),
|
tenant_id: z.uuid().optional().nullable(),
|
||||||
metadata: z.any().optional().nullable(),
|
metadata: z.any().optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -113,7 +118,8 @@ export const NewModuleModal = ({
|
|||||||
runtime_language: '',
|
runtime_language: '',
|
||||||
framework: null,
|
framework: null,
|
||||||
webhookurl: null,
|
webhookurl: null,
|
||||||
base_url: '',
|
frontend_base_url: '',
|
||||||
|
backend_base_url: '',
|
||||||
health_endpoint: '',
|
health_endpoint: '',
|
||||||
endpoints: null,
|
endpoints: null,
|
||||||
kafka_topics: null,
|
kafka_topics: null,
|
||||||
@ -155,7 +161,7 @@ export const NewModuleModal = ({
|
|||||||
if (error?.response?.data?.details && Array.isArray(error.response.data.details)) {
|
if (error?.response?.data?.details && Array.isArray(error.response.data.details)) {
|
||||||
const validationErrors = error.response.data.details;
|
const validationErrors = error.response.data.details;
|
||||||
validationErrors.forEach((detail: { path: string; message: string }) => {
|
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, {
|
setError(detail.path as keyof NewModuleFormData, {
|
||||||
type: 'server',
|
type: 'server',
|
||||||
message: detail.message,
|
message: detail.message,
|
||||||
@ -370,14 +376,28 @@ export const NewModuleModal = ({
|
|||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-sm font-semibold text-[#0f1724] mb-3">URL Configuration</h3>
|
<h3 className="text-sm font-semibold text-[#0f1724] mb-3">URL Configuration</h3>
|
||||||
<div className="flex flex-col gap-0">
|
<div className="flex flex-col gap-0">
|
||||||
<FormField
|
<div className="flex gap-5">
|
||||||
label="Base URL"
|
<div className="flex-1">
|
||||||
required
|
<FormField
|
||||||
type="url"
|
label="Frontend Base URL"
|
||||||
placeholder="https://example.com"
|
required
|
||||||
error={errors.base_url?.message}
|
type="url"
|
||||||
{...register('base_url')}
|
placeholder="https://frontend.example.com"
|
||||||
/>
|
error={errors.frontend_base_url?.message}
|
||||||
|
{...register('frontend_base_url')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<FormField
|
||||||
|
label="Backend Base URL"
|
||||||
|
required
|
||||||
|
type="url"
|
||||||
|
placeholder="https://backend.example.com"
|
||||||
|
error={errors.backend_base_url?.message}
|
||||||
|
{...register('backend_base_url')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="Health Endpoint"
|
label="Health Endpoint"
|
||||||
|
|||||||
@ -145,8 +145,12 @@ export const ViewModuleModal = ({
|
|||||||
<p className="text-sm font-medium text-[#0e1b2a]">{module.framework || 'N/A'}</p>
|
<p className="text-sm font-medium text-[#0e1b2a]">{module.framework || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Base URL</label>
|
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Frontend Base URL</label>
|
||||||
<p className="text-sm font-medium text-[#0e1b2a] font-mono break-all">{module.base_url}</p>
|
<p className="text-sm font-medium text-[#0e1b2a] font-mono break-all">{module.frontend_base_url || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Backend Base URL</label>
|
||||||
|
<p className="text-sm font-medium text-[#0e1b2a] font-mono break-all">{module.backend_base_url || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Health Endpoint</label>
|
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Health Endpoint</label>
|
||||||
|
|||||||
@ -34,13 +34,14 @@ const tenantDetailsSchema = z.object({
|
|||||||
}).optional().nullable(),
|
}).optional().nullable(),
|
||||||
max_users: z.number().int().min(1, 'max_users must be at least 1').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(),
|
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
|
// Step 2: Contact Details Schema - user creation + organization address
|
||||||
const contactDetailsSchema = z
|
const contactDetailsSchema = z
|
||||||
.object({
|
.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'),
|
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'),
|
confirmPassword: z.string().min(1, 'Confirm password is required'),
|
||||||
first_name: z.string().min(1, 'First name is required'),
|
first_name: z.string().min(1, 'First name is required'),
|
||||||
@ -113,6 +114,8 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
// Form instances for each step
|
// Form instances for each step
|
||||||
const tenantDetailsForm = useForm<TenantDetailsForm>({
|
const tenantDetailsForm = useForm<TenantDetailsForm>({
|
||||||
resolver: zodResolver(tenantDetailsSchema),
|
resolver: zodResolver(tenantDetailsSchema),
|
||||||
|
mode: 'onChange',
|
||||||
|
reValidateMode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: '',
|
name: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
@ -139,6 +142,8 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
|
|
||||||
const contactDetailsForm = useForm<ContactDetailsForm>({
|
const contactDetailsForm = useForm<ContactDetailsForm>({
|
||||||
resolver: zodResolver(contactDetailsSchema),
|
resolver: zodResolver(contactDetailsSchema),
|
||||||
|
mode: 'onChange',
|
||||||
|
reValidateMode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
@ -157,6 +162,8 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
|
|
||||||
const settingsForm = useForm<SettingsForm>({
|
const settingsForm = useForm<SettingsForm>({
|
||||||
resolver: zodResolver(settingsSchema),
|
resolver: zodResolver(settingsSchema),
|
||||||
|
mode: 'onChange',
|
||||||
|
reValidateMode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
enable_sso: false,
|
enable_sso: false,
|
||||||
enable_2fa: false,
|
enable_2fa: false,
|
||||||
@ -331,14 +338,18 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
setLogoError(null);
|
setLogoError(null);
|
||||||
setFaviconError(null);
|
setFaviconError(null);
|
||||||
|
|
||||||
if (!logoFileUrl && !logoFile) {
|
const isLogoMissing = !logoFileUrl && !logoFile;
|
||||||
|
const isFaviconMissing = !faviconFileUrl && !faviconFile;
|
||||||
|
|
||||||
|
if (isLogoMissing) {
|
||||||
setLogoError('Logo is required');
|
setLogoError('Logo is required');
|
||||||
setCurrentStep(3); // Go to settings step where logo/favicon are
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!faviconFileUrl && !faviconFile) {
|
if (isFaviconMissing) {
|
||||||
setFaviconError('Favicon is required');
|
setFaviconError('Favicon is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLogoMissing || isFaviconMissing) {
|
||||||
setCurrentStep(3); // Go to settings step where logo/favicon are
|
setCurrentStep(3); // Go to settings step where logo/favicon are
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -379,40 +390,122 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
showToast.success(message);
|
showToast.success(message);
|
||||||
navigate('/tenants');
|
navigate('/tenants');
|
||||||
} catch (err: any) {
|
} 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)) {
|
if (err?.response?.data?.details && Array.isArray(err.response.data.details)) {
|
||||||
const validationErrors = 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 }) => {
|
validationErrors.forEach((detail: { path: string; message: string }) => {
|
||||||
// Handle tenant details errors
|
const path = detail.path;
|
||||||
if (
|
|
||||||
detail.path === 'name' ||
|
// Handle nested paths first
|
||||||
detail.path === 'slug' ||
|
if (path.startsWith('settings.contact.')) {
|
||||||
detail.path === 'status' ||
|
// Contact details errors from nested path
|
||||||
detail.path === 'subscription_tier' ||
|
hasContactErrors = true;
|
||||||
detail.path === 'max_users' ||
|
const fieldName = path.replace('settings.contact.', '');
|
||||||
detail.path === 'max_modules' ||
|
if (fieldName === 'confirmPassword') {
|
||||||
detail.path === 'module_ids'
|
// 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, {
|
tenantDetailsForm.setError(fieldPath as keyof TenantDetailsForm, {
|
||||||
type: 'server',
|
type: 'server',
|
||||||
message: detail.message,
|
message: detail.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Handle contact details errors
|
// Handle contact details errors (step 2) - direct paths
|
||||||
else if (
|
else if (
|
||||||
detail.path === 'email' ||
|
path === 'email' ||
|
||||||
detail.path === 'password' ||
|
path === 'password' ||
|
||||||
detail.path === 'first_name' ||
|
path === 'first_name' ||
|
||||||
detail.path === 'last_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',
|
type: 'server',
|
||||||
message: detail.message,
|
message: detail.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Navigate to the step with errors
|
||||||
|
if (hasTenantErrors) {
|
||||||
|
setCurrentStep(1);
|
||||||
|
} else if (hasContactErrors) {
|
||||||
|
setCurrentStep(2);
|
||||||
|
} else if (hasSettingsErrors) {
|
||||||
|
setCurrentStep(3);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Handle general errors
|
||||||
const errorObj = err?.response?.data?.error;
|
const errorObj = err?.response?.data?.error;
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
(typeof errorObj === 'object' && errorObj !== null && 'message' in errorObj ? errorObj.message : null) ||
|
(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.',
|
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.');
|
showToast.error(typeof errorMessage === 'string' ? errorMessage : 'Failed to create tenant. Please try again.');
|
||||||
|
setCurrentStep(1); // Navigate to first step for general errors
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
@ -778,6 +872,12 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
Set resource limits and security preferences for this tenant.
|
Set resource limits and security preferences for this tenant.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{/* General Error Display */}
|
||||||
|
{settingsForm.formState.errors.root && (
|
||||||
|
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">
|
||||||
|
<p className="text-sm text-[#ef4444]">{settingsForm.formState.errors.root.message}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Branding Section */}
|
{/* Branding Section */}
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg p-4 md:p-6 flex flex-col gap-4">
|
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg p-4 md:p-6 flex flex-col gap-4">
|
||||||
|
|||||||
@ -34,12 +34,12 @@ const tenantDetailsSchema = z.object({
|
|||||||
}).optional().nullable(),
|
}).optional().nullable(),
|
||||||
max_users: z.number().int().min(1, 'max_users must be at least 1').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(),
|
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
|
// Step 2: Contact Details Schema - NO password fields
|
||||||
const contactDetailsSchema = z.object({
|
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'),
|
first_name: z.string().min(1, 'First name is required'),
|
||||||
last_name: z.string().min(1, 'Last name is required'),
|
last_name: z.string().min(1, 'Last name is required'),
|
||||||
contact_phone: z
|
contact_phone: z
|
||||||
@ -122,6 +122,8 @@ const EditTenant = (): ReactElement => {
|
|||||||
// Form instances for each step
|
// Form instances for each step
|
||||||
const tenantDetailsForm = useForm<TenantDetailsForm>({
|
const tenantDetailsForm = useForm<TenantDetailsForm>({
|
||||||
resolver: zodResolver(tenantDetailsSchema),
|
resolver: zodResolver(tenantDetailsSchema),
|
||||||
|
mode: 'onChange',
|
||||||
|
reValidateMode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: '',
|
name: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
@ -136,6 +138,8 @@ const EditTenant = (): ReactElement => {
|
|||||||
|
|
||||||
const contactDetailsForm = useForm<ContactDetailsForm>({
|
const contactDetailsForm = useForm<ContactDetailsForm>({
|
||||||
resolver: zodResolver(contactDetailsSchema),
|
resolver: zodResolver(contactDetailsSchema),
|
||||||
|
mode: 'onChange',
|
||||||
|
reValidateMode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: '',
|
email: '',
|
||||||
first_name: '',
|
first_name: '',
|
||||||
@ -152,6 +156,8 @@ const EditTenant = (): ReactElement => {
|
|||||||
|
|
||||||
const settingsForm = useForm<SettingsForm>({
|
const settingsForm = useForm<SettingsForm>({
|
||||||
resolver: zodResolver(settingsSchema),
|
resolver: zodResolver(settingsSchema),
|
||||||
|
mode: 'onChange',
|
||||||
|
reValidateMode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
enable_sso: false,
|
enable_sso: false,
|
||||||
enable_2fa: false,
|
enable_2fa: false,
|
||||||
@ -433,14 +439,18 @@ const EditTenant = (): ReactElement => {
|
|||||||
setLogoError(null);
|
setLogoError(null);
|
||||||
setFaviconError(null);
|
setFaviconError(null);
|
||||||
|
|
||||||
if (!logoFilePath) {
|
const isLogoMissing = !logoFilePath;
|
||||||
|
const isFaviconMissing = !faviconFilePath;
|
||||||
|
|
||||||
|
if (isLogoMissing) {
|
||||||
setLogoError('Logo is required');
|
setLogoError('Logo is required');
|
||||||
setCurrentStep(3); // Go to settings step where logo/favicon are
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!faviconFilePath) {
|
if (isFaviconMissing) {
|
||||||
setFaviconError('Favicon is required');
|
setFaviconError('Favicon is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLogoMissing || isFaviconMissing) {
|
||||||
setCurrentStep(3); // Go to settings step where logo/favicon are
|
setCurrentStep(3); // Go to settings step where logo/favicon are
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -476,47 +486,130 @@ const EditTenant = (): ReactElement => {
|
|||||||
showToast.success(message);
|
showToast.success(message);
|
||||||
navigate('/tenants');
|
navigate('/tenants');
|
||||||
} catch (err: any) {
|
} 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)) {
|
if (err?.response?.data?.details && Array.isArray(err.response.data.details)) {
|
||||||
const validationErrors = 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 }) => {
|
validationErrors.forEach((detail: { path: string; message: string }) => {
|
||||||
if (
|
const path = detail.path;
|
||||||
detail.path === 'name' ||
|
|
||||||
detail.path === 'slug' ||
|
// Handle nested paths first
|
||||||
detail.path === 'status' ||
|
if (path.startsWith('settings.contact.')) {
|
||||||
detail.path === 'subscription_tier' ||
|
// Contact details errors from nested path
|
||||||
detail.path === 'max_users' ||
|
hasContactErrors = true;
|
||||||
detail.path === 'max_modules' ||
|
const fieldName = path.replace('settings.contact.', '');
|
||||||
detail.path === 'module_ids'
|
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, {
|
tenantDetailsForm.setError(fieldPath as keyof TenantDetailsForm, {
|
||||||
type: 'server',
|
type: 'server',
|
||||||
message: detail.message,
|
message: detail.message,
|
||||||
});
|
});
|
||||||
} else if (
|
}
|
||||||
detail.path === 'email' ||
|
// Handle contact details errors (step 2) - direct paths
|
||||||
detail.path === 'first_name' ||
|
else if (
|
||||||
detail.path === 'last_name' ||
|
path === 'email' ||
|
||||||
detail.path === 'contact_phone' ||
|
path === 'first_name' ||
|
||||||
detail.path === 'address_line1' ||
|
path === 'last_name' ||
|
||||||
detail.path === 'city' ||
|
path === 'contact_phone' ||
|
||||||
detail.path === 'state' ||
|
path === 'address_line1' ||
|
||||||
detail.path === 'postal_code' ||
|
path === 'address_line2' ||
|
||||||
detail.path === 'country'
|
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',
|
type: 'server',
|
||||||
message: detail.message,
|
message: detail.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Navigate to the step with errors
|
||||||
|
if (hasTenantErrors) {
|
||||||
|
setCurrentStep(1);
|
||||||
|
} else if (hasContactErrors) {
|
||||||
|
setCurrentStep(2);
|
||||||
|
} else if (hasSettingsErrors) {
|
||||||
|
setCurrentStep(3);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Handle general errors
|
||||||
|
const errorObj = err?.response?.data?.error;
|
||||||
const errorMessage =
|
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?.response?.data?.message ||
|
||||||
err?.message ||
|
err?.message ||
|
||||||
'Failed to update tenant. Please try again.';
|
'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 {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
@ -878,6 +971,12 @@ const EditTenant = (): ReactElement => {
|
|||||||
<h2 className="text-lg font-semibold text-[#0f1724]">Configuration & Limits</h2>
|
<h2 className="text-lg font-semibold text-[#0f1724]">Configuration & Limits</h2>
|
||||||
<p className="text-sm text-[#6b7280] mt-1">Set resource limits and security preferences for this tenant.</p>
|
<p className="text-sm text-[#6b7280] mt-1">Set resource limits and security preferences for this tenant.</p>
|
||||||
</div>
|
</div>
|
||||||
|
{/* General Error Display */}
|
||||||
|
{settingsForm.formState.errors.root && (
|
||||||
|
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">
|
||||||
|
<p className="text-sm text-[#ef4444]">{settingsForm.formState.errors.root.message}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Branding Section - Same as CreateTenantWizard */}
|
{/* Branding Section - Same as CreateTenantWizard */}
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg p-4 md:p-6 flex flex-col gap-4">
|
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg p-4 md:p-6 flex flex-col gap-4">
|
||||||
|
|||||||
@ -245,13 +245,18 @@ const Settings = (): ReactElement => {
|
|||||||
setLogoError(null);
|
setLogoError(null);
|
||||||
setFaviconError(null);
|
setFaviconError(null);
|
||||||
|
|
||||||
if (!logoFilePath) {
|
const isLogoMissing = !logoFilePath;
|
||||||
|
const isFaviconMissing = !faviconFilePath;
|
||||||
|
|
||||||
|
if (isLogoMissing) {
|
||||||
setLogoError('Logo is required');
|
setLogoError('Logo is required');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!faviconFilePath) {
|
if (isFaviconMissing) {
|
||||||
setFaviconError('Favicon is required');
|
setFaviconError('Favicon is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLogoMissing || isFaviconMissing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,8 @@ export interface Module {
|
|||||||
runtime_language: string | null;
|
runtime_language: string | null;
|
||||||
framework: string | null;
|
framework: string | null;
|
||||||
webhookurl: string | null;
|
webhookurl: string | null;
|
||||||
base_url: string;
|
frontend_base_url: string;
|
||||||
|
backend_base_url: string;
|
||||||
health_endpoint: string;
|
health_endpoint: string;
|
||||||
endpoints: string[] | null;
|
endpoints: string[] | null;
|
||||||
kafka_topics: string[] | null;
|
kafka_topics: string[] | null;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user