241 lines
7.3 KiB
TypeScript
241 lines
7.3 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import type { ReactElement } from 'react';
|
|
import { useForm } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { z } from 'zod';
|
|
import { Loader2 } from 'lucide-react';
|
|
import { Modal, FormField, FormSelect, PrimaryButton, SecondaryButton } from '@/components/shared';
|
|
import type { Tenant } from '@/types/tenant';
|
|
|
|
// Validation schema - matches backend validation
|
|
const editTenantSchema = z.object({
|
|
name: z
|
|
.string()
|
|
.min(1, 'name is required')
|
|
.min(3, 'name must be at least 3 characters')
|
|
.max(100, 'name must be at most 100 characters'),
|
|
slug: z
|
|
.string()
|
|
.min(1, 'slug is required')
|
|
.min(3, 'slug must be at least 3 characters')
|
|
.max(100, 'slug must be at most 100 characters')
|
|
.regex(/^[a-z0-9-]+$/, 'slug format is invalid'),
|
|
status: z.enum(['active', 'suspended', 'deleted'], {
|
|
message: 'Status is required',
|
|
}),
|
|
settings: z.any().optional().nullable(),
|
|
subscription_tier: z.string().max(50, 'subscription_tier must be at most 50 characters').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(),
|
|
});
|
|
|
|
type EditTenantFormData = z.infer<typeof editTenantSchema>;
|
|
|
|
interface EditTenantModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
tenantId: string | null;
|
|
onLoadTenant: (id: string) => Promise<Tenant>;
|
|
onSubmit: (id: string, data: EditTenantFormData) => Promise<void>;
|
|
isLoading?: boolean;
|
|
}
|
|
|
|
const statusOptions = [
|
|
{ value: 'active', label: 'Active' },
|
|
{ value: 'suspended', label: 'Suspended' },
|
|
{ value: 'deleted', label: 'Deleted' },
|
|
];
|
|
|
|
export const EditTenantModal = ({
|
|
isOpen,
|
|
onClose,
|
|
tenantId,
|
|
onLoadTenant,
|
|
onSubmit,
|
|
isLoading = false,
|
|
}: EditTenantModalProps): ReactElement | null => {
|
|
const [isLoadingTenant, setIsLoadingTenant] = useState<boolean>(false);
|
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
setValue,
|
|
watch,
|
|
reset,
|
|
setError,
|
|
clearErrors,
|
|
formState: { errors },
|
|
} = useForm<EditTenantFormData>({
|
|
resolver: zodResolver(editTenantSchema),
|
|
});
|
|
|
|
const statusValue = watch('status');
|
|
|
|
// Load tenant data when modal opens
|
|
useEffect(() => {
|
|
if (isOpen && tenantId) {
|
|
const loadTenant = async (): Promise<void> => {
|
|
try {
|
|
setIsLoadingTenant(true);
|
|
setLoadError(null);
|
|
clearErrors();
|
|
const tenant = await onLoadTenant(tenantId);
|
|
reset({
|
|
name: tenant.name,
|
|
slug: tenant.slug,
|
|
status: tenant.status,
|
|
settings: tenant.settings,
|
|
subscription_tier: tenant.subscription_tier,
|
|
max_users: tenant.max_users,
|
|
max_modules: tenant.max_modules,
|
|
});
|
|
} catch (err: any) {
|
|
setLoadError(err?.response?.data?.error?.message || 'Failed to load tenant details');
|
|
} finally {
|
|
setIsLoadingTenant(false);
|
|
}
|
|
};
|
|
loadTenant();
|
|
} else {
|
|
reset({
|
|
name: '',
|
|
slug: '',
|
|
status: 'active',
|
|
settings: null,
|
|
subscription_tier: null,
|
|
max_users: null,
|
|
max_modules: null,
|
|
});
|
|
setLoadError(null);
|
|
clearErrors();
|
|
}
|
|
}, [isOpen, tenantId, onLoadTenant, reset, clearErrors]);
|
|
|
|
const handleFormSubmit = async (data: EditTenantFormData): Promise<void> => {
|
|
if (!tenantId) return;
|
|
|
|
clearErrors();
|
|
try {
|
|
await onSubmit(tenantId, 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 === 'slug' ||
|
|
detail.path === 'status' ||
|
|
detail.path === 'settings' ||
|
|
detail.path === 'subscription_tier' ||
|
|
detail.path === 'max_users' ||
|
|
detail.path === 'max_modules'
|
|
) {
|
|
setError(detail.path as keyof EditTenantFormData, {
|
|
type: 'server',
|
|
message: detail.message,
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
// Handle general errors
|
|
const errorMessage =
|
|
error?.response?.data?.error ||
|
|
error?.response?.data?.message ||
|
|
error?.message ||
|
|
'Failed to update tenant. Please try again.';
|
|
setError('root', {
|
|
type: 'server',
|
|
message: typeof errorMessage === 'string' ? errorMessage : 'Failed to update tenant. Please try again.',
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen}
|
|
onClose={onClose}
|
|
title="Edit Tenant"
|
|
description="Update tenant information"
|
|
maxWidth="md"
|
|
footer={
|
|
<>
|
|
<SecondaryButton
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={isLoading || isLoadingTenant}
|
|
className="px-4 py-2.5 text-sm"
|
|
>
|
|
Cancel
|
|
</SecondaryButton>
|
|
<PrimaryButton
|
|
type="button"
|
|
onClick={handleSubmit(handleFormSubmit)}
|
|
disabled={isLoading || isLoadingTenant}
|
|
size="default"
|
|
className="px-4 py-2.5 text-sm"
|
|
>
|
|
{isLoading ? 'Updating...' : 'Update Tenant'}
|
|
</PrimaryButton>
|
|
</>
|
|
}
|
|
>
|
|
<form onSubmit={handleSubmit(handleFormSubmit)} className="p-5">
|
|
{isLoadingTenant && (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
|
</div>
|
|
)}
|
|
|
|
{loadError && (
|
|
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">
|
|
<p className="text-sm text-[#ef4444]">{loadError}</p>
|
|
</div>
|
|
)}
|
|
|
|
{!isLoadingTenant && (
|
|
<div className="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>
|
|
)}
|
|
|
|
{/* Tenant Name */}
|
|
<FormField
|
|
label="Tenant Name"
|
|
required
|
|
placeholder="Enter tenant name"
|
|
error={errors.name?.message}
|
|
{...register('name')}
|
|
/>
|
|
|
|
{/* Slug */}
|
|
<FormField
|
|
label="Slug"
|
|
required
|
|
placeholder="Enter slug (lowercase, numbers, hyphens only)"
|
|
error={errors.slug?.message}
|
|
{...register('slug')}
|
|
/>
|
|
|
|
{/* 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>
|
|
)}
|
|
</form>
|
|
</Modal>
|
|
);
|
|
};
|