import { useState, useEffect, type ChangeEvent } from 'react' import { Formik } from 'formik' import * as Yup from 'yup' import { pInput as PInput, MyButton, AppContainer, Checkbox, BottomDialog, p as P, MyFlatList } from 'web-components' import DateTimeInput from './DateTimeInput' import { trpc } from '@/lib/trpc-client' // Local copy of common-ui/shared-types CreateCouponPayload (web cannot import // the RN common-ui package; @packages/shared does not export this type). export interface CreateCouponPayload { couponCodes: string[] discountPercent?: number flatDiscount?: number minOrder?: number maxValue?: number validTill?: string maxLimitForUser?: number isApplyForAll: boolean isUserBased: boolean targetUsers?: number[] productIds?: number[] skuIds?: number[] applicableUsers?: number[] applicableProducts?: number[] exclusiveApply?: boolean } const USERS_PAGE_SIZE = 10 interface CouponFormValues extends CreateCouponPayload { isReservedCoupon?: boolean couponCodes: string[] skuIds?: number[] } interface CouponFormProps { onSubmit: (values: CouponFormValues) => void isLoading: boolean initialValues?: Partial } const couponValidationSchema = Yup.object().shape({ isReservedCoupon: Yup.boolean().optional(), couponCodes: Yup.array().of( Yup.string() .required('Code is required') .min(3, 'Code must be at least 3 characters') .max(50, 'Code cannot exceed 50 characters') .matches(/^[A-Z0-9_-]+$/, 'Code can only contain uppercase letters, numbers, underscores, and hyphens') ), discountPercent: Yup.number() .min(0, 'Must be positive') .max(100, 'Cannot exceed 100%') .optional(), flatDiscount: Yup.number() .min(0, 'Must be positive') .optional(), minOrder: Yup.number().min(0, 'Must be positive').optional(), maxValue: Yup.number().min(0, 'Must be positive').optional(), validTill: Yup.date().optional(), maxLimitForUser: Yup.number().min(1, 'Must be at least 1').optional(), exclusiveApply: Yup.boolean().optional(), isUserBased: Yup.boolean(), isApplyForAll: Yup.boolean(), applicableUsers: Yup.array().of(Yup.number()).optional(), }).test('discount-validation', 'Must provide exactly one discount type with valid value', function(value) { const { discountPercent, flatDiscount } = value const hasPercent = discountPercent !== undefined && discountPercent > 0 const hasFlat = flatDiscount !== undefined && flatDiscount > 0 if (hasPercent && hasFlat) { return this.createError({ message: 'Cannot have both percentage and flat discount' }) } if (!hasPercent && !hasFlat) { return this.createError({ message: 'Must provide either percentage or flat discount' }) } return true }).test('codes-required', 'At least one coupon code is required', function(value: any) { const codes = (value.couponCodes || []).filter((c: string) => c && c.trim()) if (codes.length === 0) { return this.createError({ path: 'couponCodes', message: 'Add at least one coupon code' }) } return true }).test('codes-unique', 'Coupon codes must be unique', function(value: any) { const codes = (value.couponCodes || []).map((c: string) => c.trim()).filter(Boolean) if (new Set(codes).size !== codes.length) { return this.createError({ path: 'couponCodes', message: 'Coupon codes must be unique' }) } return true }) export default function CouponForm({ onSubmit, isLoading, initialValues }: CouponFormProps) { // User dropdown states const [userSearchQuery, setUserSearchQuery] = useState('') const [userOffset, setUserOffset] = useState(0) const [allUsers, setAllUsers] = useState<{ id: number; name: string; mobile: string | null }[]>([]) const [hasMoreUsers, setHasMoreUsers] = useState(true) const [usersDropdownOpen, setUsersDropdownOpen] = useState(false) // Add-multiple dialog states (for pattern codes like ABC12) const [multiDialogOpen, setMultiDialogOpen] = useState(false) const [multiCount, setMultiCount] = useState('') const { data: usersData, isFetching: isFetchingUsers } = trpc.admin.coupon.getUsersMiniInfo.useQuery( { search: userSearchQuery, limit: USERS_PAGE_SIZE, offset: userOffset }, { enabled: usersDropdownOpen } ) useEffect(() => { if (usersData?.users) { if (userOffset === 0) { setAllUsers(usersData.users) } else { setAllUsers(prev => [...prev, ...usersData.users]) } setHasMoreUsers(usersData.users.length === USERS_PAGE_SIZE) } }, [usersData, userOffset]) useEffect(() => { setUserOffset(0) setHasMoreUsers(true) }, [userSearchQuery]) useEffect(() => { if (usersDropdownOpen) { setUserOffset(0) setAllUsers([]) setHasMoreUsers(true) setUserSearchQuery('') } }, [usersDropdownOpen]) // User search functionality will be inside Formik const defaultValues: CouponFormValues = { couponCodes: [''], isUserBased: false, isApplyForAll: false, targetUsers: [], discountPercent: undefined, flatDiscount: undefined, minOrder: undefined, maxValue: undefined, validTill: undefined, maxLimitForUser: undefined, skuIds: undefined, applicableUsers: [], applicableProducts: [], exclusiveApply: false, isReservedCoupon: false, } return ( initialValues={(initialValues || defaultValues) as CouponFormValues} validationSchema={couponValidationSchema} onSubmit={onSubmit} > {({ values, errors, touched, setFieldValue, handleSubmit }) => { const toggleUserSelection = (userId: number) => { const current = values.applicableUsers || [] const newSelection = current.includes(userId) ? current.filter(id => id !== userId) : [...current, userId] setFieldValue('applicableUsers', newSelection) } const isReserved = (values as any).isReservedCoupon // Detect pattern codes like ABC12 (ends in digits) for bulk generation. // Continue numbering from the highest code with the same prefix. const patternCode = (values.couponCodes || []).find( (c: string) => /^[A-Z0-9_-]*\d+$/.test(c.trim()) ) const patternPrefix = patternCode ? patternCode.replace(/\d+$/, '') : '' const patternStart = (values.couponCodes || []).reduce((max, c) => { const trimmed = c.trim() if (!patternPrefix || !trimmed.startsWith(patternPrefix)) return max const m = trimmed.match(/\d+$/) if (!m) return max return Math.max(max, parseInt(m[0], 10)) }, patternCode ? parseInt((patternCode.match(/\d+$/) || ['0'])[0], 10) : 0) const handleAddMultiple = () => { const count = parseInt(multiCount, 10) if (!count || count < 1 || count > 100) return // Start from the next number after the highest existing code const newCodes = Array.from( { length: count }, (_, i) => `${patternPrefix}${patternStart + 1 + i}` ) const existing = (values.couponCodes || []).filter((c: string) => c && c.trim()) setFieldValue('couponCodes', [...existing, ...newCodes]) setMultiDialogOpen(false) setMultiCount('') } return ( {/* Is Reserved Coupon Checkbox */}
setFieldValue('isReservedCoupon', !(values as any).isReservedCoupon)} />

Is Reserved Coupon

{/* Coupon Codes */}

Coupon Codes

Each code creates a separate coupon with the same settings.

{(values.couponCodes || []).map((code: string, index: number) => { const codeError = Array.isArray(errors.couponCodes) ? (errors.couponCodes as any)[index] : errors.couponCodes return (
) => { const next = [...(values.couponCodes || [])] next[index] = e.target.value.toUpperCase() setFieldValue('couponCodes', next) }} error={!!codeError} /> {codeError &&

{codeError}

}
) })} {(errors.couponCodes as any)?.message && (

{(errors.couponCodes as any).message}

)}
{patternCode && ( )}
{/* Discount Type Selection */}

Discount Type *

{/* Discount Value */} {values.discountPercent !== undefined && (
setFieldValue('discountPercent', parseFloat(e.target.value) || 0)} type="number" inputMode="numeric" error={!!(touched.discountPercent && errors.discountPercent)} />
)} {values.flatDiscount !== undefined && (
setFieldValue('flatDiscount', parseFloat(e.target.value) || 0)} type="number" inputMode="numeric" error={!!(touched.flatDiscount && errors.flatDiscount)} />
)} {/* Minimum Order */}
setFieldValue('minOrder', parseFloat(e.target.value) || undefined)} type="number" inputMode="numeric" error={!!(touched.minOrder && errors.minOrder)} />
{/* Maximum Discount Value - only for percentage discounts */} {values.flatDiscount === undefined && (
setFieldValue('maxValue', parseFloat(e.target.value) || undefined)} type="number" inputMode="numeric" error={!!(touched.maxValue && errors.maxValue)} />
)} {/* Validity Period */}

Valid Till

{ setFieldValue('validTill', date?.toISOString()) }} />
{/* Usage Limit */}
setFieldValue('maxLimitForUser', parseInt(e.target.value) || undefined)} type="number" inputMode="numeric" error={!!(touched.maxLimitForUser && errors.maxLimitForUser)} />
{/* Exclusive Apply */}

Exclusive Apply

{/* Target Audience */}

Target Audience {isReserved ? '(Disabled for Reserved Coupons)' : ''}

{/* Applicable User Selection */}

Applicable Users (Optional)

setUsersDropdownOpen(false)}>

Select Applicable Users

setUserSearchQuery(e.target.value)} />
item.id.toString()} renderItem={(item) => ( )} ListFooterComponent={ <> {isFetchingUsers ?

Loading...

: null} {hasMoreUsers && !isFetchingUsers ? ( ) : null} } />
{/* Add Multiple Coupons Dialog */} setMultiDialogOpen(false)}>

Add Multiple Coupons

Generate {patternCode ? `${patternPrefix}${patternStart + 1}, ${patternPrefix}${patternStart + 2}, ...` : 'sequential'} codes from your pattern

setMultiCount(e.target.value)} type="number" inputMode="numeric" />
{/* Submit Button */} handleSubmit()} disabled={isLoading} textContent={isLoading ? 'Creating...' : 'Create Coupon'} />
) }} ) }