198 lines
5.9 KiB
TypeScript
198 lines
5.9 KiB
TypeScript
import { useEffect } from 'react';
|
|
import type { ReactElement } from 'react';
|
|
import { useForm } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { z } from 'zod';
|
|
import { Modal, FormField, FormSelect, PrimaryButton, SecondaryButton } from '@/components/shared';
|
|
import type { CreateRoleRequest } from '@/types/role';
|
|
|
|
// Validation schema
|
|
const newRoleSchema = z.object({
|
|
name: z.string().min(1, 'Role name is required'),
|
|
code: z.enum(['super_admin', 'tenant_admin', 'quality_manager', 'developer', 'viewer'], {
|
|
message: 'Role code is required',
|
|
}),
|
|
description: z.string().min(1, 'Description is required'),
|
|
scope: z.enum(['platform', 'tenant', 'module'], {
|
|
message: 'Scope is required',
|
|
}),
|
|
});
|
|
|
|
type NewRoleFormData = z.infer<typeof newRoleSchema>;
|
|
|
|
interface NewRoleModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSubmit: (data: CreateRoleRequest) => Promise<void>;
|
|
isLoading?: boolean;
|
|
}
|
|
|
|
const scopeOptions = [
|
|
{ value: 'platform', label: 'Platform' },
|
|
{ value: 'tenant', label: 'Tenant' },
|
|
{ value: 'module', label: 'Module' },
|
|
];
|
|
|
|
const roleCodeOptions = [
|
|
{ value: 'super_admin', label: 'Super Admin' },
|
|
{ value: 'tenant_admin', label: 'Tenant Admin' },
|
|
{ value: 'quality_manager', label: 'Quality Manager' },
|
|
{ value: 'developer', label: 'Developer' },
|
|
{ value: 'viewer', label: 'Viewer' },
|
|
];
|
|
|
|
export const NewRoleModal = ({
|
|
isOpen,
|
|
onClose,
|
|
onSubmit,
|
|
isLoading = false,
|
|
}: NewRoleModalProps): ReactElement | null => {
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
setValue,
|
|
watch,
|
|
reset,
|
|
setError,
|
|
clearErrors,
|
|
formState: { errors },
|
|
} = useForm<NewRoleFormData>({
|
|
resolver: zodResolver(newRoleSchema),
|
|
defaultValues: {
|
|
scope: 'platform',
|
|
code: undefined,
|
|
},
|
|
});
|
|
|
|
const scopeValue = watch('scope');
|
|
const codeValue = watch('code');
|
|
|
|
// Reset form when modal closes
|
|
useEffect(() => {
|
|
if (!isOpen) {
|
|
reset({
|
|
name: '',
|
|
code: undefined,
|
|
description: '',
|
|
scope: 'platform',
|
|
});
|
|
clearErrors();
|
|
}
|
|
}, [isOpen, reset, clearErrors]);
|
|
|
|
const handleFormSubmit = async (data: NewRoleFormData): Promise<void> => {
|
|
clearErrors();
|
|
try {
|
|
await onSubmit(data);
|
|
} catch (error: any) {
|
|
// Handle validation errors from API
|
|
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 === 'code' || detail.path === 'description' || detail.path === 'scope') {
|
|
setError(detail.path as keyof NewRoleFormData, {
|
|
type: 'server',
|
|
message: detail.message,
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
// Handle general errors
|
|
// Check for nested error object with message property
|
|
const errorObj = error?.response?.data?.error;
|
|
const errorMessage =
|
|
(typeof errorObj === 'object' && errorObj !== null && 'message' in errorObj ? errorObj.message : null) ||
|
|
(typeof errorObj === 'string' ? errorObj : null) ||
|
|
error?.response?.data?.message ||
|
|
error?.message ||
|
|
'Failed to create role. Please try again.';
|
|
setError('root', {
|
|
type: 'server',
|
|
message: typeof errorMessage === 'string' ? errorMessage : 'Failed to create role. Please try again.',
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen}
|
|
onClose={onClose}
|
|
title="Create Role"
|
|
description="Define a new role by setting permissions and role type."
|
|
maxWidth="md"
|
|
footer={
|
|
<>
|
|
<SecondaryButton
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={isLoading}
|
|
className="px-4 py-2.5 text-sm"
|
|
>
|
|
Cancel
|
|
</SecondaryButton>
|
|
<PrimaryButton
|
|
type="button"
|
|
onClick={handleSubmit(handleFormSubmit)}
|
|
disabled={isLoading}
|
|
size="default"
|
|
className="px-4 py-2.5 text-sm"
|
|
>
|
|
{isLoading ? 'Creating...' : 'Create Role'}
|
|
</PrimaryButton>
|
|
</>
|
|
}
|
|
>
|
|
<form onSubmit={handleSubmit(handleFormSubmit)} className="p-5 flex flex-col gap-0">
|
|
{/* General Error Display */}
|
|
{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]">{errors.root.message}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Role Name and Role Code Row */}
|
|
<div className="grid grid-cols-2 gap-5 pb-4">
|
|
<FormField
|
|
label="Role Name"
|
|
required
|
|
placeholder="Enter Text Here"
|
|
error={errors.name?.message}
|
|
{...register('name')}
|
|
/>
|
|
|
|
<FormSelect
|
|
label="Role Code"
|
|
required
|
|
placeholder="Select Role Code"
|
|
options={roleCodeOptions}
|
|
value={codeValue}
|
|
onValueChange={(value) => setValue('code', value as 'super_admin' | 'tenant_admin' | 'quality_manager' | 'developer' | 'viewer')}
|
|
error={errors.code?.message}
|
|
/>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<FormField
|
|
label="Description"
|
|
required
|
|
placeholder="Enter Text Here"
|
|
error={errors.description?.message}
|
|
{...register('description')}
|
|
/>
|
|
|
|
{/* Scope */}
|
|
<FormSelect
|
|
label="Scope"
|
|
required
|
|
placeholder="Select Scope"
|
|
options={scopeOptions}
|
|
value={scopeValue}
|
|
onValueChange={(value) => setValue('scope', value as 'platform' | 'tenant' | 'module')}
|
|
error={errors.scope?.message}
|
|
/>
|
|
</form>
|
|
</Modal>
|
|
);
|
|
};
|