fix: standardize admin form inputs, validation, and sync with backend constraints

- Unified all inputs to use .input CSS class, consistent label styles, and btn-primary/btn-secondary buttons
- Fixed number inputs to use number | '' pattern allowing field clearing without default replacement
- Added field-level validation with inline error borders and messages
- Synced frontend constraints with backend Pydantic schemas (maxLength, min/max values)
- Fixed toggle switch dimensions across all admin forms
This commit is contained in:
c0mrade
2026-02-06 19:37:07 +03:00
parent 88d9377adb
commit 6e7eb36f76
13 changed files with 364 additions and 227 deletions

View File

@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
import { adminPaymentMethodsApi } from '../api/adminPaymentMethods';
import type { PromoGroupSimple } from '../types';
import { usePlatform } from '../platform/hooks/usePlatform';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
const BackIcon = () => (
<svg
@@ -89,8 +90,8 @@ export default function AdminPaymentMethodEdit() {
const [isEnabled, setIsEnabled] = useState(false);
const [customName, setCustomName] = useState('');
const [subOptions, setSubOptions] = useState<Record<string, boolean>>({});
const [minAmount, setMinAmount] = useState('');
const [maxAmount, setMaxAmount] = useState('');
const [minAmount, setMinAmount] = useState<number | ''>('');
const [maxAmount, setMaxAmount] = useState<number | ''>('');
const [userTypeFilter, setUserTypeFilter] = useState<'all' | 'telegram' | 'email'>('all');
const [firstTopupFilter, setFirstTopupFilter] = useState<'any' | 'yes' | 'no'>('any');
const [promoGroupFilterMode, setPromoGroupFilterMode] = useState<'all' | 'selected'>('all');
@@ -102,8 +103,8 @@ export default function AdminPaymentMethodEdit() {
setIsEnabled(config.is_enabled);
setCustomName(config.display_name || '');
setSubOptions(config.sub_options || {});
setMinAmount(config.min_amount_kopeks?.toString() || '');
setMaxAmount(config.max_amount_kopeks?.toString() || '');
setMinAmount(config.min_amount_kopeks ?? '');
setMaxAmount(config.max_amount_kopeks ?? '');
setUserTypeFilter(config.user_type_filter);
setFirstTopupFilter(config.first_topup_filter);
setPromoGroupFilterMode(config.promo_group_filter_mode);
@@ -144,13 +145,13 @@ export default function AdminPaymentMethodEdit() {
}
// Amounts
if (minAmount.trim()) {
data.min_amount_kopeks = parseInt(minAmount, 10) || null;
if (minAmount !== '') {
data.min_amount_kopeks = toNumber(minAmount) || null;
} else {
data.reset_min_amount = true;
}
if (maxAmount.trim()) {
data.max_amount_kopeks = parseInt(maxAmount, 10) || null;
if (maxAmount !== '') {
data.max_amount_kopeks = toNumber(maxAmount) || null;
} else {
data.reset_max_amount = true;
}
@@ -236,13 +237,13 @@ export default function AdminPaymentMethodEdit() {
</div>
<button
onClick={() => setIsEnabled(!isEnabled)}
className={`relative h-7 w-12 rounded-full transition-colors ${
className={`relative h-6 w-11 rounded-full transition-colors ${
isEnabled ? 'bg-accent-500' : 'bg-dark-600'
}`}
>
<span
className={`absolute top-0.5 h-6 w-6 rounded-full bg-white shadow transition-transform ${
isEnabled ? 'left-[calc(100%-1.625rem)]' : 'left-0.5'
className={`absolute top-1 h-4 w-4 rounded-full bg-white transition-transform ${
isEnabled ? 'left-6' : 'left-1'
}`}
/>
</button>
@@ -250,7 +251,7 @@ export default function AdminPaymentMethodEdit() {
{/* Display name */}
<div>
<label className="mb-2 block text-sm font-medium text-dark-200">
<label className="mb-2 block text-sm font-medium text-dark-300">
{t('admin.paymentMethods.displayName')}
</label>
<input
@@ -268,7 +269,7 @@ export default function AdminPaymentMethodEdit() {
{/* Sub-options */}
{config.available_sub_options && config.available_sub_options.length > 0 && (
<div>
<label className="mb-2 block text-sm font-medium text-dark-200">
<label className="mb-2 block text-sm font-medium text-dark-300">
{t('admin.paymentMethods.subOptions')}
</label>
<div className="space-y-2">
@@ -302,25 +303,25 @@ export default function AdminPaymentMethodEdit() {
{/* Min/Max amounts */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-2 block text-sm font-medium text-dark-200">
<label className="mb-2 block text-sm font-medium text-dark-300">
{t('admin.paymentMethods.minAmount')}
</label>
<input
type="number"
value={minAmount}
onChange={(e) => setMinAmount(e.target.value)}
onChange={createNumberInputHandler(setMinAmount, 0)}
placeholder={config.default_min_amount_kopeks.toString()}
className="input"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-dark-200">
<label className="mb-2 block text-sm font-medium text-dark-300">
{t('admin.paymentMethods.maxAmount')}
</label>
<input
type="number"
value={maxAmount}
onChange={(e) => setMaxAmount(e.target.value)}
onChange={createNumberInputHandler(setMaxAmount, 0)}
placeholder={config.default_max_amount_kopeks.toString()}
className="input"
/>