/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useQuery } from '@tanstack/react-query' import { Pencil } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { getCurrencyDisplay, getCurrencyLabel } from '@/lib/currency' import { formatQuota, parseQuotaFromDollars } from '@/lib/format' import { Button } from '@/components/ui/button' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from '@/components/ui/sheet' import { Textarea } from '@/components/ui/textarea' import { SideDrawerSection, sideDrawerContentClassName, sideDrawerFooterClassName, sideDrawerFormClassName, sideDrawerHeaderClassName, } from '@/components/drawer-layout' import { createUser, updateUser, getUser, getGroups } from '../api' import { BINDING_FIELDS, ERROR_MESSAGES, SUCCESS_MESSAGES } from '../constants' import { userFormSchema, type UserFormValues, USER_FORM_DEFAULT_VALUES, transformFormDataToPayload, transformUserToFormDefaults, } from '../lib' import { type User } from '../types' import { UserQuotaDialog } from './user-quota-dialog' import { useUsers } from './users-provider' type UsersMutateDrawerProps = { open: boolean onOpenChange: (open: boolean) => void currentRow?: User } export function UsersMutateDrawer({ open, onOpenChange, currentRow, }: UsersMutateDrawerProps) { const { t } = useTranslation() const isUpdate = !!currentRow const { triggerRefresh } = useUsers() const [isSubmitting, setIsSubmitting] = useState(false) const [quotaDialogOpen, setQuotaDialogOpen] = useState(false) // Fetch groups const { data: groupsData } = useQuery({ queryKey: ['groups'], queryFn: getGroups, staleTime: 5 * 60 * 1000, }) const groups = groupsData?.data || [] const form = useForm({ resolver: zodResolver(userFormSchema), defaultValues: USER_FORM_DEFAULT_VALUES, }) // Load existing data when updating useEffect(() => { if (open && isUpdate && currentRow) { // For update, fetch fresh data getUser(currentRow.id).then((result) => { if (result.success && result.data) { form.reset(transformUserToFormDefaults(result.data)) } }) } else if (open && !isUpdate) { // For create, reset to defaults form.reset(USER_FORM_DEFAULT_VALUES) } }, [open, isUpdate, currentRow, form]) const { meta: currencyMeta } = getCurrencyDisplay() const currencyLabel = getCurrencyLabel() const tokensOnly = currencyMeta.kind === 'tokens' const currentQuotaRaw = form.watch('quota_dollars') || 0 const onSubmit = async (data: UserFormValues) => { if (!isUpdate) { const passwordLength = data.password?.length || 0 if (passwordLength < 8 || passwordLength > 20) { form.setError('password', { type: 'manual', message: t('Password must be between 8 and 20 characters'), }) return } } setIsSubmitting(true) try { const payload = transformFormDataToPayload(data, currentRow?.id) const result = isUpdate ? await updateUser(payload as typeof payload & { id: number }) : await createUser(payload) if (result.success) { toast.success( isUpdate ? t(SUCCESS_MESSAGES.USER_UPDATED) : t(SUCCESS_MESSAGES.USER_CREATED) ) onOpenChange(false) triggerRefresh() } else { toast.error( result.message || (isUpdate ? t(ERROR_MESSAGES.UPDATE_FAILED) : t(ERROR_MESSAGES.CREATE_FAILED)) ) } } catch (_error) { toast.error(t(ERROR_MESSAGES.UNEXPECTED)) } finally { setIsSubmitting(false) } } const refreshUserData = async () => { if (!currentRow) return const result = await getUser(currentRow.id) if (result.success && result.data) { form.reset(transformUserToFormDefaults(result.data)) } triggerRefresh() } return ( <> { onOpenChange(v) if (!v) { form.reset() } }} > {isUpdate ? t('Update') : t('Create')} {t('User')} {isUpdate ? t('Update the user by providing necessary info.') : t('Add a new user by providing necessary info.')}
{/* Basic Information */}

{t('Basic Information')}

( {t('Username')} )} /> {!isUpdate && ( ( {t('Role')} {t("Set the user's role (cannot be Root)")} )} /> )} ( {t('Display Name')} {t('Leave empty to use username')} )} /> ( {t('Password')} )} />
{/* Group & Quota Settings (Update only) */} {isUpdate && (

{t('Group & Quota')}

( {t('Group')} )} /> ( {t('Remaining Quota ({{currency}})', { currency: currencyLabel, })}
{formatQuota(parseQuotaFromDollars(field.value || 0))}
)} /> ( {t('Remark')}