Files
chaos-api/web/default/src/features/system-settings/general/quota-settings-section.tsx
T
CaIon 0526a22643 feat: require compliance confirmation for paid features
Gate payment, redemption, subscription, and invitation reward flows behind an audited compliance acknowledgement.
2026-05-13 22:18:46 +08:00

289 lines
9.3 KiB
TypeScript
Vendored

/*
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 <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { ChangeEvent } from 'react'
import * as z from 'zod'
import type { Resolver } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next'
import { Alert, AlertDescription } from '@/components/ui/alert'
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 { Switch } from '@/components/ui/switch'
import { FormDirtyIndicator } from '../components/form-dirty-indicator'
import { FormNavigationGuard } from '../components/form-navigation-guard'
import { SettingsSection } from '../components/settings-section'
import { useSettingsForm } from '../hooks/use-settings-form'
import { useUpdateOption } from '../hooks/use-update-option'
const quotaSchema = z.object({
QuotaForNewUser: z.coerce.number().min(0),
PreConsumedQuota: z.coerce.number().min(0),
QuotaForInviter: z.coerce.number().min(0),
QuotaForInvitee: z.coerce.number().min(0),
TopUpLink: z.string(),
general_setting: z.object({
docs_link: z.string(),
}),
quota_setting: z.object({
enable_free_model_pre_consume: z.boolean(),
}),
})
type QuotaFormValues = z.infer<typeof quotaSchema>
type QuotaSettingsSectionProps = {
defaultValues: QuotaFormValues
complianceConfirmed?: boolean
}
export function QuotaSettingsSection({
defaultValues,
complianceConfirmed = true,
}: QuotaSettingsSectionProps) {
const { t } = useTranslation()
const updateOption = useUpdateOption()
const handleNumberChange =
(onChange: (value: number | string) => void) =>
(event: ChangeEvent<HTMLInputElement>) => {
onChange(
event.target.value === '' ? '' : event.currentTarget.valueAsNumber
)
}
const { form, handleSubmit, isDirty, isSubmitting } =
useSettingsForm<QuotaFormValues>({
resolver: zodResolver(quotaSchema) as Resolver<
QuotaFormValues,
unknown,
QuotaFormValues
>,
defaultValues,
onSubmit: async (_data, changedFields) => {
for (const [key, value] of Object.entries(changedFields)) {
await updateOption.mutateAsync({
key,
value: value as string | number | boolean,
})
}
},
})
return (
<SettingsSection
title={t('Quota Settings')}
description={t('Configure user quota allocation and rewards')}
>
<FormNavigationGuard when={isDirty} />
{!complianceConfirmed ? (
<Alert variant='destructive'>
<AlertDescription>
{t(
'Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.'
)}
</AlertDescription>
</Alert>
) : null}
<Form {...form}>
<form onSubmit={handleSubmit} className='space-y-6'>
<FormDirtyIndicator isDirty={isDirty} />
<FormField
control={form.control}
name='QuotaForNewUser'
render={({ field }) => (
<FormItem>
<FormLabel>{t('New User Quota')}</FormLabel>
<FormControl>
<Input
type='number'
value={field.value ?? ''}
onChange={handleNumberChange(field.onChange)}
name={field.name}
onBlur={field.onBlur}
ref={field.ref}
/>
</FormControl>
<FormDescription>
{t('Initial quota given to new users')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='PreConsumedQuota'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Pre-Consumed Quota')}</FormLabel>
<FormControl>
<Input
type='number'
value={field.value ?? ''}
onChange={handleNumberChange(field.onChange)}
name={field.name}
onBlur={field.onBlur}
ref={field.ref}
/>
</FormControl>
<FormDescription>
{t('Quota consumed before charging users')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='QuotaForInviter'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Inviter Reward')}</FormLabel>
<FormControl>
<Input
type='number'
value={field.value ?? ''}
onChange={handleNumberChange(field.onChange)}
name={field.name}
onBlur={field.onBlur}
ref={field.ref}
/>
</FormControl>
<FormDescription>
{t('Quota given to users who invite others')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='QuotaForInvitee'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Invitee Reward')}</FormLabel>
<FormControl>
<Input
type='number'
value={field.value ?? ''}
onChange={handleNumberChange(field.onChange)}
name={field.name}
onBlur={field.onBlur}
ref={field.ref}
/>
</FormControl>
<FormDescription>
{t('Quota given to invited users')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='quota_setting.enable_free_model_pre_consume'
render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
<div className='space-y-0.5'>
<FormLabel className='text-base'>
{t('Pre-Consume for Free Models')}
</FormLabel>
<FormDescription>
{t(
'When enabled, zero-cost models also pre-consume quota before final settlement.'
)}
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={updateOption.isPending}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name='TopUpLink'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Top-Up Link')}</FormLabel>
<FormControl>
<Input
placeholder={t('https://example.com/topup')}
{...field}
/>
</FormControl>
<FormDescription>
{t('External link for users to purchase quota')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='general_setting.docs_link'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Documentation Link')}</FormLabel>
<FormControl>
<Input
placeholder={t('https://docs.example.com')}
{...field}
/>
</FormControl>
<FormDescription>
{t('Link to your documentation site')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button
type='submit'
disabled={updateOption.isPending || isSubmitting}
>
{updateOption.isPending ? t('Saving...') : t('Save Changes')}
</Button>
</form>
</Form>
</SettingsSection>
)
}