import FormFooter from "@/components/formFooter"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import NumberAndSelect from "@/components/ui/numberAndSelect"; import { resetDurationOptions } from "@/lib/constants/governance"; import { getErrorMessage, useCreateCustomerMutation, useUpdateCustomerMutation } from "@/lib/store"; import { CreateCustomerRequest, Customer, UpdateCustomerRequest } from "@/lib/types/governance"; import { formatCurrency } from "@/lib/utils/governance"; import { Validator } from "@/lib/utils/validation"; import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib"; import { formatDistanceToNow } from "date-fns"; import isEqual from "lodash.isequal"; import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; interface CustomerDialogProps { customer?: Customer | null; onSave: () => void; onCancel: () => void; } interface CustomerFormData { name: string; // Budget budgetMaxLimit: number | undefined; budgetResetDuration: string; // Rate Limit tokenMaxLimit: number | undefined; tokenResetDuration: string; requestMaxLimit: number | undefined; requestResetDuration: string; isDirty: boolean; } // Helper function to create initial state const createInitialState = (customer?: Customer | null): Omit => { return { name: customer?.name || "", // Budget budgetMaxLimit: customer?.budget?.max_limit ?? undefined, budgetResetDuration: customer?.budget?.reset_duration || "1M", // Rate Limit tokenMaxLimit: customer?.rate_limit?.token_max_limit ?? undefined, tokenResetDuration: customer?.rate_limit?.token_reset_duration || "1h", requestMaxLimit: customer?.rate_limit?.request_max_limit ?? undefined, requestResetDuration: customer?.rate_limit?.request_reset_duration || "1h", }; }; export default function CustomerDialog({ customer, onSave, onCancel }: CustomerDialogProps) { const isEditing = !!customer; const [initialState] = useState>(createInitialState(customer)); const [formData, setFormData] = useState({ ...initialState, isDirty: false, }); const hasCreateAccess = useRbac(RbacResource.Customers, RbacOperation.Create); const hasUpdateAccess = useRbac(RbacResource.Customers, RbacOperation.Update); const hasPermission = isEditing ? hasUpdateAccess : hasCreateAccess; // RTK Query hooks const [createCustomer, { isLoading: isCreating }] = useCreateCustomerMutation(); const [updateCustomer, { isLoading: isUpdating }] = useUpdateCustomerMutation(); const loading = isCreating || isUpdating; // Track isDirty state useEffect(() => { const currentData = { name: formData.name, budgetMaxLimit: formData.budgetMaxLimit, budgetResetDuration: formData.budgetResetDuration, tokenMaxLimit: formData.tokenMaxLimit, tokenResetDuration: formData.tokenResetDuration, requestMaxLimit: formData.requestMaxLimit, requestResetDuration: formData.requestResetDuration, }; setFormData((prev) => ({ ...prev, isDirty: !isEqual(initialState, currentData), })); }, [ formData.name, formData.budgetMaxLimit, formData.budgetResetDuration, formData.tokenMaxLimit, formData.tokenResetDuration, formData.requestMaxLimit, formData.requestResetDuration, initialState, ]); // Values for validation and submission (already numbers) const budgetMaxLimitNum = formData.budgetMaxLimit; const tokenMaxLimitNum = formData.tokenMaxLimit; const requestMaxLimitNum = formData.requestMaxLimit; // Validation const validator = useMemo( () => new Validator([ // Basic validation Validator.required(formData.name.trim(), "Customer name is required"), // Check if anything is dirty Validator.custom(formData.isDirty, "No changes to save"), // Budget validation ...(formData.budgetMaxLimit !== undefined && formData.budgetMaxLimit !== null ? [ Validator.minValue(budgetMaxLimitNum ?? 0, 0.01, "Budget max limit must be greater than $0.01"), Validator.required(formData.budgetResetDuration, "Budget reset duration is required"), ] : []), // Rate limit validation - token limits ...(formData.tokenMaxLimit !== undefined && formData.tokenMaxLimit !== null ? [ Validator.minValue(tokenMaxLimitNum ?? 0, 1, "Token max limit must be at least 1"), Validator.required(formData.tokenResetDuration, "Token reset duration is required"), ] : []), // Rate limit validation - request limits ...(formData.requestMaxLimit !== undefined && formData.requestMaxLimit !== null ? [ Validator.minValue(requestMaxLimitNum ?? 0, 1, "Request max limit must be at least 1"), Validator.required(formData.requestResetDuration, "Request reset duration is required"), ] : []), ]), [formData, budgetMaxLimitNum, tokenMaxLimitNum, requestMaxLimitNum], ); const updateField = (field: K, value: CustomerFormData[K]) => { setFormData((prev) => ({ ...prev, [field]: value })); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!validator.isValid()) { toast.error(validator.getFirstError()); return; } try { if (isEditing && customer) { // Update existing customer const updateData: UpdateCustomerRequest = { name: formData.name, }; // Detect budget changes using had/has pattern const hadBudget = !!customer.budget; const hasBudget = budgetMaxLimitNum !== undefined && budgetMaxLimitNum !== null; if (hasBudget) { updateData.budget = { max_limit: budgetMaxLimitNum, reset_duration: formData.budgetResetDuration, }; } else if (hadBudget) { updateData.budget = {} as UpdateCustomerRequest["budget"]; } // Detect rate limit changes using had/has pattern const hadRateLimit = !!customer.rate_limit; const hasRateLimit = (tokenMaxLimitNum !== undefined && tokenMaxLimitNum !== null) || (requestMaxLimitNum !== undefined && requestMaxLimitNum !== null); if (hasRateLimit) { updateData.rate_limit = { token_max_limit: tokenMaxLimitNum, token_reset_duration: tokenMaxLimitNum !== undefined && tokenMaxLimitNum !== null ? formData.tokenResetDuration : undefined, request_max_limit: requestMaxLimitNum, request_reset_duration: requestMaxLimitNum !== undefined && requestMaxLimitNum !== null ? formData.requestResetDuration : undefined, }; } else if (hadRateLimit) { updateData.rate_limit = {} as UpdateCustomerRequest["rate_limit"]; } await updateCustomer({ customerId: customer.id, data: updateData }).unwrap(); toast.success("Customer updated successfully"); } else { // Create new customer const createData: CreateCustomerRequest = { name: formData.name, }; // Add budget if enabled if (budgetMaxLimitNum !== undefined && budgetMaxLimitNum !== null) { createData.budget = { max_limit: budgetMaxLimitNum, reset_duration: formData.budgetResetDuration, }; } // Add rate limit if enabled (token or request limits) if ( (tokenMaxLimitNum !== undefined && tokenMaxLimitNum !== null) || (requestMaxLimitNum !== undefined && requestMaxLimitNum !== null) ) { createData.rate_limit = { token_max_limit: tokenMaxLimitNum, token_reset_duration: tokenMaxLimitNum !== undefined && tokenMaxLimitNum !== null ? formData.tokenResetDuration : undefined, request_max_limit: requestMaxLimitNum, request_reset_duration: requestMaxLimitNum !== undefined && requestMaxLimitNum !== null ? formData.requestResetDuration : undefined, }; } await createCustomer(createData).unwrap(); toast.success("Customer created successfully"); } onSave(); } catch (error) { toast.error(getErrorMessage(error)); } }; return ( {isEditing ? "Edit Customer" : "Create Customer"} {isEditing ? "Update the customer information and settings." : "Create a new customer account to organize teams and manage resources."}
{/* Basic Information */}
updateField("name", e.target.value)} />

This name will be used to identify the customer account.

{/* Budget Configuration */} updateField("budgetMaxLimit", value)} onChangeSelect={(value) => updateField("budgetResetDuration", value)} options={resetDurationOptions} dataTestId="budget-max-limit-input" /> {/* Rate Limit Configuration - Token Limits */} updateField("tokenMaxLimit", value)} onChangeSelect={(value) => updateField("tokenResetDuration", value)} options={resetDurationOptions} /> {/* Rate Limit Configuration - Request Limits */} updateField("requestMaxLimit", value)} onChangeSelect={(value) => updateField("requestResetDuration", value)} options={resetDurationOptions} /> {/* Current Usage Section (only shown when editing with existing limits) */} {isEditing && (customer?.budget || customer?.rate_limit) && (

Current Usage

{customer?.budget && (

Budget

{formatCurrency(customer.budget.current_usage)} / {formatCurrency(customer.budget.max_limit)} = customer.budget.max_limit ? "destructive" : "default"} className="text-xs" > {Math.round((customer.budget.current_usage / customer.budget.max_limit) * 100)}%

Last Reset: {formatDistanceToNow(new Date(customer.budget.last_reset), { addSuffix: true })}

)} {customer?.rate_limit?.token_max_limit && (

Tokens

{customer.rate_limit.token_current_usage.toLocaleString()} /{" "} {customer.rate_limit.token_max_limit.toLocaleString()} = customer.rate_limit.token_max_limit ? "destructive" : "default" } className="text-xs" > {Math.round((customer.rate_limit.token_current_usage / customer.rate_limit.token_max_limit) * 100)}%

Last Reset: {formatDistanceToNow(new Date(customer.rate_limit.token_last_reset), { addSuffix: true })}

)} {customer?.rate_limit?.request_max_limit && (

Requests

{customer.rate_limit.request_current_usage.toLocaleString()} /{" "} {customer.rate_limit.request_max_limit.toLocaleString()} = customer.rate_limit.request_max_limit ? "destructive" : "default" } className="text-xs" > {Math.round((customer.rate_limit.request_current_usage / customer.rate_limit.request_max_limit) * 100)}%

Last Reset: {formatDistanceToNow(new Date(customer.rate_limit.request_last_reset), { addSuffix: true })}

)}
)}
); }