import React, { useState, useEffect } from 'react'; import { View, Text, TouchableOpacity, FlatList } from 'react-native'; import { Formik } from 'formik'; import * as Yup from 'yup'; import { MyTextInput, MyButton, tw, AppContainer , DateTimePickerMod, Checkbox, BottomDialog, MyText } from 'common-ui'; import { trpc } from '../trpc-client'; import { CreateCouponPayload } from 'common-ui/shared-types'; 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(), isFirstOrderOnly: 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, isFirstOrderOnly: 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] = text.toUpperCase(); setFieldValue('couponCodes', next); }} keyboardType="default" autoCapitalize="characters" error={!!codeError} /> {codeError && {codeError}} { const next = (values.couponCodes || []).filter((_: string, i: number) => i !== index); setFieldValue('couponCodes', next.length > 0 ? next : ['']); }} style={tw`ml-2 p-2`} > ); })} {(errors.couponCodes as any)?.message && ( {(errors.couponCodes as any).message} )} setFieldValue('couponCodes', [...(values.couponCodes || []), ''])} style={tw`py-2 px-3 bg-blue-50 rounded-lg border border-blue-200`} > + Add Code {patternCode && ( setMultiDialogOpen(true)} style={tw`ml-2 py-2 px-3 bg-green-50 rounded-lg border border-green-200`} > Add Multiple )} {/* Discount Type Selection */} Discount Type * { setFieldValue('discountPercent', values.discountPercent || 0); setFieldValue('flatDiscount', undefined); }} style={tw`flex-1 p-3 border rounded-lg mr-2 ${ values.discountPercent !== undefined ? 'border-blue-500' : 'border-gray-300' }`} > Percentage { setFieldValue('flatDiscount', values.flatDiscount || 0); setFieldValue('discountPercent', undefined); }} style={tw`flex-1 p-3 border rounded-lg ${ values.flatDiscount !== undefined ? 'border-blue-500' : 'border-gray-300' }`} > Flat Amount {/* Discount Value */} {values.discountPercent !== undefined && ( setFieldValue('discountPercent', parseFloat(text) || 0)} keyboardType="numeric" error={!!(touched.discountPercent && errors.discountPercent)} /> )} {values.flatDiscount !== undefined && ( setFieldValue('flatDiscount', parseFloat(text) || 0)} keyboardType="numeric" error={!!(touched.flatDiscount && errors.flatDiscount)} /> )} {/* Minimum Order */} setFieldValue('minOrder', parseFloat(text) || undefined)} keyboardType="numeric" error={!!(touched.minOrder && errors.minOrder)} /> {/* Maximum Discount Value - only for percentage discounts */} {values.flatDiscount === undefined && ( setFieldValue('maxValue', parseFloat(text) || undefined)} keyboardType="numeric" error={!!(touched.maxValue && errors.maxValue)} /> )} {/* Validity Period */} Valid Till { setFieldValue('validTill', date?.toISOString()) }} /> {/* Usage Limit */} setFieldValue('maxLimitForUser', parseInt(text) || undefined)} keyboardType="numeric" error={!!(touched.maxLimitForUser && errors.maxLimitForUser)} /> {/* Exclusive Apply */} Exclusive Apply setFieldValue('exclusiveApply', !values.exclusiveApply)} style={tw`flex-row items-center`} > {values.exclusiveApply && } Exclusive coupon (cannot be combined with other coupons) {/* First Order Only */} First Order Only setFieldValue('isFirstOrderOnly', !values.isFirstOrderOnly)} style={tw`flex-row items-center`} > {values.isFirstOrderOnly && } Valid only on the customer's first order {/* Target Audience */} Target Audience {isReserved ? '(Disabled for Reserved Coupons)' : ''} { setFieldValue('isApplyForAll', true); setFieldValue('isUserBased', false); setFieldValue('targetUsers', []); }} style={tw`flex-1 p-3 border rounded-lg mr-2 ${ values.isApplyForAll ? 'border-blue-500' : 'border-gray-300' } ${isReserved ? 'opacity-50' : ''}`} > All Users { setFieldValue('isUserBased', true); setFieldValue('isApplyForAll', false); }} style={tw`flex-1 p-3 border rounded-lg ${ values.isUserBased ? 'border-blue-500' : 'border-gray-300' } ${isReserved ? 'opacity-50' : ''}`} > Specific User {/* Applicable User Selection */} Applicable Users (Optional) setUsersDropdownOpen(true)} style={tw`border border-gray-300 rounded p-3 bg-white ${isReserved ? 'opacity-50' : ''}`} > {values.applicableUsers?.length ? `${values.applicableUsers.length} users selected` : 'Select users...'} setUsersDropdownOpen(false)}> Select Applicable Users item.id.toString()} renderItem={({ item }) => ( toggleUserSelection(item.id)} style={tw`flex-row items-center p-3 border-b border-gray-200`} > toggleUserSelection(item.id)} /> {item.mobile} - {item.name} )} onEndReached={() => { if (hasMoreUsers && !isFetchingUsers) { setUserOffset(prev => prev + USERS_PAGE_SIZE); } }} onEndReachedThreshold={0.5} ListFooterComponent={isFetchingUsers ? Loading... : null} style={tw`max-h-60`} /> setUsersDropdownOpen(false)} style={tw`mt-4 bg-blue-500 p-3 rounded`} > Done {/* Add Multiple Coupons Dialog */} setMultiDialogOpen(false)}> Add Multiple Coupons Generate {patternCode ? `${patternPrefix}${patternStart + 1}, ${patternPrefix}${patternStart + 2}, ...` : 'sequential'} codes from your pattern { setMultiDialogOpen(false); setMultiCount(''); }} style={tw`flex-1 p-3 rounded bg-gray-100 mr-2`} > Cancel Generate {/* Submit Button */} handleSubmit()} loading={isLoading} disabled={isLoading} > Create Coupon ); }} ); }