This commit is contained in:
shafi54 2026-08-18 23:56:47 +05:30
parent 7291bfe7ce
commit 496faffe0a
8 changed files with 209 additions and 249 deletions

View file

@ -117,6 +117,10 @@ export default function Complaints() {
contentContainerStyle={tw`px-4 py-4`} contentContainerStyle={tw`px-4 py-4`}
data={complaints} data={complaints}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={10}
removeClippedSubviews
onEndReached={handleLoadMore} onEndReached={handleLoadMore}
onEndReachedThreshold={0.5} onEndReachedThreshold={0.5}
renderItem={({ item }) => ( renderItem={({ item }) => (
@ -146,6 +150,20 @@ export default function Complaints() {
{item.text} {item.text}
</MyText> </MyText>
{item.response && (
<View style={tw`mb-3 bg-blue-50 border border-blue-100 rounded-xl p-3`}>
<View style={tw`flex-row items-center mb-1`}>
<MaterialIcons name="reply" size={16} color="#2563EB" />
<MyText style={tw`ml-1 text-xs font-bold text-blue-700`}>
Admin Reply
</MyText>
</View>
<MyText style={tw`text-sm text-gray-700 leading-5`}>
{item.response}
</MyText>
</View>
)}
{item.images && item.images.length > 0 && ( {item.images && item.images.length > 0 && (
<View style={tw`mb-3`}> <View style={tw`mb-3`}>
<MyText style={tw`text-sm font-semibold text-gray-700 mb-2`}> <MyText style={tw`text-sm font-semibold text-gray-700 mb-2`}>

View file

@ -25,7 +25,6 @@ export default function CreateCoupon() {
) )
const handleCreateCoupon = async (values: any) => { const handleCreateCoupon = async (values: any) => {
console.log('Form values:', values); // Debug log
const { isReservedCoupon, couponCodes, ...rest } = values; const { isReservedCoupon, couponCodes, ...rest } = values;
// Transform targetUsers array to targetUser for backend compatibility // Transform targetUsers array to targetUser for backend compatibility
const payload = { const payload = {
@ -33,43 +32,29 @@ export default function CreateCoupon() {
targetUser: rest.targetUsers?.[0] || undefined, targetUser: rest.targetUsers?.[0] || undefined,
}; };
delete payload.targetUsers; delete payload.targetUsers;
console.log('Payload:', payload); // Debug log
if (isReservedCoupon && Array.isArray(couponCodes) && couponCodes.length > 0) { // For flat discounts, send the discount value as maxValue too
// Create one reserved coupon per code, same config if (payload.flatDiscount !== undefined) {
const codes = couponCodes.map((c: string) => c.trim()).filter(Boolean); payload.maxValue = payload.flatDiscount;
try { }
for (const code of codes) {
await createReservedCoupon.mutateAsync({ ...payload, couponCode: code }); const codes = (couponCodes || []).map((c: string) => c.trim()).filter(Boolean);
} if (codes.length === 0) {
await refetchCoupons() Alert.alert('Error', 'At least one coupon code is required');
await refetchReservedCoupons()
Alert.alert('Success', `${codes.length} reserved coupon${codes.length > 1 ? 's' : ''} created successfully`, [
{ text: 'OK', onPress: () => router.back() }
]);
} catch (error: any) {
Alert.alert('Error', error.message || 'Failed to create reserved coupons');
}
return; return;
} }
const mutation = isReservedCoupon ? createReservedCoupon : createCoupon; const mutation = isReservedCoupon ? createReservedCoupon : createCoupon;
const isLoading = isReservedCoupon ? createReservedCoupon.isPending : createCoupon.isPending; try {
await mutation.mutateAsync({ ...payload, couponCodes: codes });
if (isLoading) return; // Prevent double submission await refetchCoupons()
await refetchReservedCoupons()
mutation.mutate(payload, { Alert.alert('Success', `${codes.length} coupon${codes.length > 1 ? 's' : ''} created successfully`, [
onSuccess: async () => { { text: 'OK', onPress: () => router.back() }
await refetchCoupons() ]);
await refetchReservedCoupons() } catch (error: any) {
Alert.alert('Success', `${isReservedCoupon ? 'Reserved coupon' : 'Coupon'} created successfully`, [ Alert.alert('Error', error.message || 'Failed to create coupons');
{ text: 'OK', onPress: () => router.back() } }
]);
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to create coupon');
},
});
}; };
return ( return (

View file

@ -37,6 +37,11 @@ export default function EditCoupon() {
}; };
delete updates.targetUsers; delete updates.targetUsers;
// For flat discounts, send the discount value as maxValue too
if (updates.flatDiscount !== undefined) {
updates.maxValue = updates.flatDiscount;
}
updateCoupon.mutate({ id: couponId, updates }, { updateCoupon.mutate({ id: couponId, updates }, {
onSuccess: async () => { onSuccess: async () => {
await refetch() await refetch()
@ -74,7 +79,7 @@ export default function EditCoupon() {
// Transform coupon data for form (targetUser to targetUsers array) // Transform coupon data for form (targetUser to targetUsers array)
const initialValues: Partial<CreateCouponPayload & { isReservedCoupon?: boolean }> = { const initialValues: Partial<CreateCouponPayload & { isReservedCoupon?: boolean }> = {
couponCode: coupon.couponCode, couponCodes: coupon.couponCode ? [coupon.couponCode] : [''],
isUserBased: coupon.isUserBased, isUserBased: coupon.isUserBased,
isApplyForAll: coupon.isApplyForAll, isApplyForAll: coupon.isApplyForAll,
targetUsers: coupon.targetUser ? [coupon.targetUser.id] : [], targetUsers: coupon.targetUser ? [coupon.targetUser.id] : [],

View file

@ -11,7 +11,7 @@ const USERS_PAGE_SIZE = 10;
interface CouponFormValues extends CreateCouponPayload { interface CouponFormValues extends CreateCouponPayload {
isReservedCoupon?: boolean; isReservedCoupon?: boolean;
couponCodes?: string[]; couponCodes: string[];
skuIds?: number[]; skuIds?: number[];
} }
@ -23,11 +23,6 @@ interface CouponFormProps {
const couponValidationSchema = Yup.object().shape({ const couponValidationSchema = Yup.object().shape({
isReservedCoupon: Yup.boolean().optional(), isReservedCoupon: Yup.boolean().optional(),
couponCode: Yup.string()
.required('Coupon code is required')
.min(3, 'Coupon code must be at least 3 characters')
.max(50, 'Coupon code cannot exceed 50 characters')
.matches(/^[A-Z0-9_-]+$/, 'Coupon code can only contain uppercase letters, numbers, underscores, and hyphens'),
couponCodes: Yup.array().of( couponCodes: Yup.array().of(
Yup.string() Yup.string()
.required('Code is required') .required('Code is required')
@ -62,20 +57,16 @@ const couponValidationSchema = Yup.object().shape({
return this.createError({ message: 'Must provide either percentage or flat discount' }); return this.createError({ message: 'Must provide either percentage or flat discount' });
} }
return true; return true;
}).test('reserved-codes-required', 'At least one coupon code is required', function(value: any) { }).test('codes-required', 'At least one coupon code is required', function(value: any) {
if (value.isReservedCoupon) { const codes = (value.couponCodes || []).filter((c: string) => c && c.trim());
const codes = (value.couponCodes || []).filter((c: string) => c && c.trim()); if (codes.length === 0) {
if (codes.length === 0) { return this.createError({ path: 'couponCodes', message: 'Add at least one coupon code' });
return this.createError({ path: 'couponCodes', message: 'Add at least one coupon code' });
}
} }
return true; return true;
}).test('reserved-codes-unique', 'Coupon codes must be unique', function(value: any) { }).test('codes-unique', 'Coupon codes must be unique', function(value: any) {
if (value.isReservedCoupon) { const codes = (value.couponCodes || []).map((c: string) => c.trim()).filter(Boolean);
const codes = (value.couponCodes || []).map((c: string) => c.trim()).filter(Boolean); if (new Set(codes).size !== codes.length) {
if (new Set(codes).size !== codes.length) { return this.createError({ path: 'couponCodes', message: 'Coupon codes must be unique' });
return this.createError({ path: 'couponCodes', message: 'Coupon codes must be unique' });
}
} }
return true; return true;
}); });
@ -124,8 +115,7 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
// User search functionality will be inside Formik // User search functionality will be inside Formik
const defaultValues: CouponFormValues = { const defaultValues: CouponFormValues = {
couponCode: '', couponCodes: [''],
couponCodes: [],
isUserBased: false, isUserBased: false,
isApplyForAll: false, isApplyForAll: false,
targetUsers: [], targetUsers: [],
@ -173,70 +163,56 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
<MyText style={tw`ml-2 text-sm font-medium text-gray-700`}>Is Reserved Coupon</MyText> <MyText style={tw`ml-2 text-sm font-medium text-gray-700`}>Is Reserved Coupon</MyText>
</View> </View>
{/* Coupon Code (single, non-reserved) OR Coupon Codes (array, reserved) */} {/* Coupon Codes */}
{!isReserved ? ( <View style={tw`mb-4`}>
<View style={tw`mb-4`}> <Text style={tw`text-base font-bold mb-1`}>Coupon Codes</Text>
<MyTextInput <Text style={tw`text-xs text-gray-500 mb-2`}>
topLabel="Coupon Code" Each code creates a separate coupon with the same settings.
placeholder="e.g., SAVE20" </Text>
value={values.couponCode || ''} {(values.couponCodes || []).map((code: string, index: number) => {
onChangeText={(text: string) => setFieldValue('couponCode', text.toUpperCase())} const codeError = Array.isArray(errors.couponCodes)
keyboardType="default" ? (errors.couponCodes as any)[index]
autoCapitalize="characters" : errors.couponCodes;
error={!!(touched.couponCode && errors.couponCode)} return (
/> <View key={index} style={tw`flex-row items-center mb-2`}>
</View> <View style={tw`flex-1`}>
) : ( <MyTextInput
<View style={tw`mb-4`}> topLabel={`Code ${index + 1}`}
<Text style={tw`text-base font-bold mb-1`}>Coupon Codes</Text> placeholder="e.g., SECRET10"
<Text style={tw`text-xs text-gray-500 mb-2`}> value={code}
Each code creates a separate reserved coupon with the same settings. onChangeText={(text: string) => {
</Text> const next = [...(values.couponCodes || [])];
{(values.couponCodes || []).map((code: string, index: number) => { next[index] = text.toUpperCase();
const codeError = Array.isArray(errors.couponCodes)
? (errors.couponCodes as any)[index]
: errors.couponCodes;
return (
<View key={index} style={tw`flex-row items-center mb-2`}>
<View style={tw`flex-1`}>
<MyTextInput
topLabel={`Code ${index + 1}`}
placeholder="e.g., SECRET10"
value={code}
onChangeText={(text: string) => {
const next = [...(values.couponCodes || [])];
next[index] = text.toUpperCase();
setFieldValue('couponCodes', next);
}}
keyboardType="default"
autoCapitalize="characters"
error={!!codeError}
/>
{codeError && <Text style={tw`text-red-500 text-xs mt-1`}>{codeError}</Text>}
</View>
<TouchableOpacity
onPress={() => {
const next = (values.couponCodes || []).filter((_: string, i: number) => i !== index);
setFieldValue('couponCodes', next); setFieldValue('couponCodes', next);
}} }}
style={tw`ml-2 p-2`} keyboardType="default"
> autoCapitalize="characters"
<Text style={tw`text-red-500 font-bold text-lg`}></Text> error={!!codeError}
</TouchableOpacity> />
{codeError && <Text style={tw`text-red-500 text-xs mt-1`}>{codeError}</Text>}
</View> </View>
); <TouchableOpacity
})} onPress={() => {
{(errors.couponCodes as any)?.message && ( const next = (values.couponCodes || []).filter((_: string, i: number) => i !== index);
<Text style={tw`text-red-500 text-xs mb-2`}>{(errors.couponCodes as any).message}</Text> setFieldValue('couponCodes', next.length > 0 ? next : ['']);
)} }}
<TouchableOpacity style={tw`ml-2 p-2`}
onPress={() => setFieldValue('couponCodes', [...(values.couponCodes || []), ''])} >
style={tw`mt-2 py-2 px-3 bg-blue-50 rounded-lg border border-blue-200 self-start`} <Text style={tw`text-red-500 font-bold text-lg`}></Text>
> </TouchableOpacity>
<Text style={tw`text-blue-600 font-medium`}>+ Add Code</Text> </View>
</TouchableOpacity> );
</View> })}
)} {(errors.couponCodes as any)?.message && (
<Text style={tw`text-red-500 text-xs mb-2`}>{(errors.couponCodes as any).message}</Text>
)}
<TouchableOpacity
onPress={() => setFieldValue('couponCodes', [...(values.couponCodes || []), ''])}
style={tw`mt-2 py-2 px-3 bg-blue-50 rounded-lg border border-blue-200 self-start`}
>
<Text style={tw`text-blue-600 font-medium`}>+ Add Code</Text>
</TouchableOpacity>
</View>
{/* Discount Type Selection */} {/* Discount Type Selection */}
<Text style={tw`text-base font-bold mb-2`}> <Text style={tw`text-base font-bold mb-2`}>
@ -308,17 +284,19 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
/> />
</View> </View>
{/* Maximum Discount Value */} {/* Maximum Discount Value - only for percentage discounts */}
<View style={tw`mb-4`}> {values.flatDiscount === undefined && (
<MyTextInput <View style={tw`mb-4`}>
topLabel="Maximum Discount Value" <MyTextInput
placeholder="e.g., 200" topLabel="Maximum Discount Value"
value={values.maxValue?.toString() || ''} placeholder="e.g., 200"
onChangeText={(text: string) => setFieldValue('maxValue', parseFloat(text) || undefined)} value={values.maxValue?.toString() || ''}
keyboardType="numeric" onChangeText={(text: string) => setFieldValue('maxValue', parseFloat(text) || undefined)}
error={!!(touched.maxValue && errors.maxValue)} keyboardType="numeric"
/> error={!!(touched.maxValue && errors.maxValue)}
</View> />
</View>
)}
{/* Validity Period */} {/* Validity Period */}
<View style={tw`mb-4`}> <View style={tw`mb-4`}>

View file

@ -27,6 +27,7 @@ export const complaintRouter = router({
status: string; status: string;
createdAt: Date; createdAt: Date;
images: string[]; images: string[];
response: string | null;
}>; }>;
nextCursor?: number; nextCursor?: number;
}> => { }> => {
@ -83,6 +84,7 @@ export const complaintRouter = router({
status: c.isResolved ? 'resolved' : 'pending', status: c.isResolved ? 'resolved' : 'pending',
createdAt: c.createdAt, createdAt: c.createdAt,
images: signedImages, images: signedImages,
response: c.response,
}; };
}) })
); );

View file

@ -24,6 +24,7 @@ import type { Coupon, CouponValidationResult, UserMiniInfo } from '@packages/sha
const createCouponBodySchema = z.object({ const createCouponBodySchema = z.object({
couponCode: z.string().optional(), couponCode: z.string().optional(),
couponCodes: z.array(z.string()).optional(),
isUserBased: z.boolean().optional(), isUserBased: z.boolean().optional(),
discountPercent: z.number().optional(), discountPercent: z.number().optional(),
flatDiscount: z.number().optional(), flatDiscount: z.number().optional(),
@ -48,8 +49,8 @@ const validateCouponBodySchema = z.object({
export const couponRouter = router({ export const couponRouter = router({
create: protectedProcedure create: protectedProcedure
.input(createCouponBodySchema) .input(createCouponBodySchema)
.mutation(async ({ input, ctx }): Promise<Coupon> => { .mutation(async ({ input, ctx }): Promise<Coupon[]> => {
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, skuIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input; const { couponCode, couponCodes, isUserBased, discountPercent, flatDiscount, minOrder, skuIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input;
// Validation: ensure at least one discount type is provided // Validation: ensure at least one discount type is provided
if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) { if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) {
@ -72,18 +73,15 @@ export const couponRouter = router({
throw new Error("Unauthorized"); throw new Error("Unauthorized");
} }
// Generate coupon code if not provided // Support both single code (legacy) and array of codes
let finalCouponCode = couponCode; let codes: string[] = couponCodes?.filter(Boolean) || [];
if (!finalCouponCode) { if (codes.length === 0 && couponCode) {
codes = [couponCode];
}
if (codes.length === 0) {
const timestamp = Date.now().toString().slice(-6); const timestamp = Date.now().toString().slice(-6);
const random = Math.random().toString(36).substring(2, 8).toUpperCase(); const random = Math.random().toString(36).substring(2, 8).toUpperCase();
finalCouponCode = `MF${timestamp}${random}`; codes = [`MF${timestamp}${random}`];
}
// Using dbService helper (new implementation)
const codeExists = await checkCouponExists(finalCouponCode);
if (codeExists) {
throw new Error("Coupon code already exists");
} }
// If applicableUsers is provided, verify users exist // If applicableUsers is provided, verify users exist
@ -94,66 +92,37 @@ export const couponRouter = router({
} }
} }
const coupon = await createCouponWithRelations( const createdCoupons: Coupon[] = [];
{ for (const code of codes) {
couponCode: finalCouponCode, const finalCouponCode = code.trim().toUpperCase();
isUserBased: isUserBased || false, const codeExists = await checkCouponExists(finalCouponCode);
discountPercent: discountPercent?.toString(), if (codeExists) {
flatDiscount: flatDiscount?.toString(), throw new Error(`Coupon code already exists: ${finalCouponCode}`);
minOrder: minOrder?.toString(), }
skuIds: skuIds || null,
createdBy: staffUserId,
maxValue: maxValue?.toString(),
isApplyForAll: isApplyForAll || false,
validTill: validTill ? dayjs(validTill).toDate() : undefined,
maxLimitForUser,
exclusiveApply: exclusiveApply || false,
},
applicableUsers,
applicableProducts
);
/* const coupon = await createCouponWithRelations(
// Old implementation - direct DB query with transaction: {
const result = await db.insert(coupons).values({ couponCode: finalCouponCode,
couponCode: finalCouponCode, isUserBased: isUserBased || false,
isUserBased: isUserBased || false, discountPercent: discountPercent?.toString(),
discountPercent: discountPercent?.toString(), flatDiscount: flatDiscount?.toString(),
flatDiscount: flatDiscount?.toString(), minOrder: minOrder?.toString(),
minOrder: minOrder?.toString(), skuIds: skuIds || null,
skuIds: skuIds || null, createdBy: staffUserId,
createdBy: staffUserId, maxValue: maxValue?.toString(),
maxValue: maxValue?.toString(), isApplyForAll: isApplyForAll || false,
isApplyForAll: isApplyForAll || false, validTill: validTill ? dayjs(validTill).toDate() : undefined,
validTill: validTill ? dayjs(validTill).toDate() : undefined, maxLimitForUser,
maxLimitForUser, exclusiveApply: exclusiveApply || false,
exclusiveApply: exclusiveApply || false, },
}).returning(); applicableUsers,
applicableProducts
const coupon = result[0];
// Insert applicable users
if (applicableUsers && applicableUsers.length > 0) {
await db.insert(couponApplicableUsers).values(
applicableUsers.map(userId => ({
couponId: coupon.id,
userId,
}))
); );
createdCoupons.push(coupon);
} }
// Insert applicable products return createdCoupons;
if (applicableProducts && applicableProducts.length > 0) {
await db.insert(couponApplicableProducts).values(
applicableProducts.map(skuId => ({
couponId: coupon.id,
skuId,
}))
);
}
*/
return coupon;
}), }),
getAll: protectedProcedure getAll: protectedProcedure
@ -210,7 +179,8 @@ export const couponRouter = router({
// Prepare update data // Prepare update data
const updateData: any = {}; const updateData: any = {};
if (updates.couponCode !== undefined) updateData.couponCode = updates.couponCode; const updatedCode = updates.couponCodes?.[0]?.trim() || updates.couponCode;
if (updatedCode !== undefined && updatedCode !== '') updateData.couponCode = updatedCode.trim().toUpperCase();
if (updates.isUserBased !== undefined) updateData.isUserBased = updates.isUserBased; if (updates.isUserBased !== undefined) updateData.isUserBased = updates.isUserBased;
if (updates.discountPercent !== undefined) updateData.discountPercent = updates.discountPercent?.toString(); if (updates.discountPercent !== undefined) updateData.discountPercent = updates.discountPercent?.toString();
if (updates.flatDiscount !== undefined) updateData.flatDiscount = updates.flatDiscount?.toString(); if (updates.flatDiscount !== undefined) updateData.flatDiscount = updates.flatDiscount?.toString();
@ -405,7 +375,7 @@ export const couponRouter = router({
createReservedCoupon: protectedProcedure createReservedCoupon: protectedProcedure
.input(createCouponBodySchema) .input(createCouponBodySchema)
.mutation(async ({ input, ctx }): Promise<any> => { .mutation(async ({ input, ctx }): Promise<any> => {
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, skuIds, applicableProducts, maxValue, validTill, maxLimitForUser, exclusiveApply } = input; const { couponCode, couponCodes, isUserBased, discountPercent, flatDiscount, minOrder, skuIds, applicableProducts, maxValue, validTill, maxLimitForUser, exclusiveApply } = input;
// Validation: ensure at least one discount type is provided // Validation: ensure at least one discount type is provided
if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) { if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) {
@ -418,62 +388,46 @@ export const couponRouter = router({
throw new Error("Unauthorized"); throw new Error("Unauthorized");
} }
// Generate secret code if not provided // Support both single code (legacy) and array of codes
let secretCode = couponCode || `SECRET${Date.now().toString().slice(-6)}${Math.random().toString(36).substring(2, 8).toUpperCase()}`; let codes: string[] = couponCodes?.filter(Boolean) || [];
if (codes.length === 0 && couponCode) {
// Using dbService helper (new implementation) codes = [couponCode];
const codeExists = await checkReservedCouponExists(secretCode); }
if (codeExists) { if (codes.length === 0) {
throw new Error("Secret code already exists"); const timestamp = Date.now().toString().slice(-6);
const random = Math.random().toString(36).substring(2, 8).toUpperCase();
codes = [`SECRET${timestamp}${random}`];
} }
const coupon = await createReservedCouponWithProducts( const createdCoupons = [];
{ for (const code of codes) {
secretCode, const secretCode = code.trim().toUpperCase();
couponCode: couponCode || `RESERVED${Date.now().toString().slice(-6)}`, const codeExists = await checkReservedCouponExists(secretCode);
discountPercent: discountPercent?.toString(), if (codeExists) {
flatDiscount: flatDiscount?.toString(), throw new Error(`Secret code already exists: ${secretCode}`);
minOrder: minOrder?.toString(), }
skuIds,
maxValue: maxValue?.toString(),
validTill: validTill ? dayjs(validTill).toDate() : undefined,
maxLimitForUser,
exclusiveApply: exclusiveApply || false,
createdBy: staffUserId,
},
applicableProducts
);
/* const coupon = await createReservedCouponWithProducts(
// Old implementation - direct DB query: {
const result = await db.insert(reservedCoupons).values({ secretCode,
secretCode, couponCode: secretCode,
couponCode: couponCode || RESERVED${Date.now().toString().slice(-6)}, discountPercent: discountPercent?.toString(),
discountPercent: discountPercent?.toString(), flatDiscount: flatDiscount?.toString(),
flatDiscount: flatDiscount?.toString(), minOrder: minOrder?.toString(),
minOrder: minOrder?.toString(), skuIds,
skuIds, maxValue: maxValue?.toString(),
maxValue: maxValue?.toString(), validTill: validTill ? dayjs(validTill).toDate() : undefined,
validTill: validTill ? dayjs(validTill).toDate() : undefined, maxLimitForUser,
maxLimitForUser, exclusiveApply: exclusiveApply || false,
exclusiveApply: exclusiveApply || false, createdBy: staffUserId,
createdBy: staffUserId, },
}).returning(); applicableProducts
const coupon = result[0];
// Insert applicable products if provided
if (applicableProducts && applicableProducts.length > 0) {
await db.insert(couponApplicableProducts).values(
applicableProducts.map(skuId => ({
couponId: coupon.id,
skuId,
}))
); );
}
*/
return coupon; createdCoupons.push(coupon);
}
return createdCoupons;
}), }),
getUsersMiniInfo: protectedProcedure getUsersMiniInfo: protectedProcedure

View file

@ -5,7 +5,7 @@ import { View, ScrollView, Alert, Dimensions } from 'react-native';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { useRouter, usePathname } from 'expo-router'; import { useRouter, usePathname } from 'expo-router';
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { tw, theme, MyText, MyTouchableOpacity, MyFlatList, AppContainer, MiniQuantifier } from 'common-ui'; import { tw, theme, MyText, MyTouchableOpacity, MyFlatList, AppContainer, MiniQuantifier, SearchBar } from 'common-ui';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { useAllProducts, useStores, useSlots } from '@/src/hooks/prominent-api-hooks'; import { useAllProducts, useStores, useSlots } from '@/src/hooks/prominent-api-hooks';
import { AllProductsApiType } from '@backend/trpc/router'; import { AllProductsApiType } from '@backend/trpc/router';
@ -445,6 +445,7 @@ interface FlashDeliveryProductsProps {
export function FlashDeliveryProducts({ storeId:storeIdParent, baseUrl, onProductPress }: FlashDeliveryProductsProps) { export function FlashDeliveryProducts({ storeId:storeIdParent, baseUrl, onProductPress }: FlashDeliveryProductsProps) {
useHideTabNav('quick_delivery'); useHideTabNav('quick_delivery');
const [isLoadingDialogOpen, setIsLoadingDialogOpen] = React.useState(false); const [isLoadingDialogOpen, setIsLoadingDialogOpen] = React.useState(false);
const [searchQuery, setSearchQuery] = useState('');
const router = useRouter(); const router = useRouter();
const storeId = storeIdParent; const storeId = storeIdParent;
const storeIdNum = storeId; const storeIdNum = storeId;
@ -506,11 +507,28 @@ export function FlashDeliveryProducts({ storeId:storeIdParent, baseUrl, onProduc
}) || []; }) || [];
} }
// Apply search filter (searches within this store's products only)
const trimmedQuery = searchQuery.trim().toLowerCase();
if (trimmedQuery) {
flashProducts = flashProducts.filter(p =>
p.name?.toLowerCase().includes(trimmedQuery)
);
}
return ( return (
<View style={tw`flex-1`}> <View style={tw`flex-1`}>
<MyFlatList <MyFlatList
data={flashProducts} data={flashProducts}
numColumns={2} numColumns={2}
ListHeaderComponent={
<View style={tw`px-4 pt-2`}>
<SearchBar
value={searchQuery}
onChangeText={setSearchQuery}
placeholder="Search products in this store..."
/>
</View>
}
renderItem={({ item }) => ( renderItem={({ item }) => (
<CompactProductCard <CompactProductCard
item={item} item={item}

View file

@ -271,7 +271,7 @@ export interface Coupon {
} }
export interface CreateCouponPayload { export interface CreateCouponPayload {
couponCode: string; couponCodes: string[];
discountPercent?: number; discountPercent?: number;
flatDiscount?: number; flatDiscount?: number;
minOrder?: number; minOrder?: number;