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

View file

@ -37,6 +37,11 @@ export default function EditCoupon() {
};
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 }, {
onSuccess: async () => {
await refetch()
@ -74,7 +79,7 @@ export default function EditCoupon() {
// Transform coupon data for form (targetUser to targetUsers array)
const initialValues: Partial<CreateCouponPayload & { isReservedCoupon?: boolean }> = {
couponCode: coupon.couponCode,
couponCodes: coupon.couponCode ? [coupon.couponCode] : [''],
isUserBased: coupon.isUserBased,
isApplyForAll: coupon.isApplyForAll,
targetUsers: coupon.targetUser ? [coupon.targetUser.id] : [],

View file

@ -11,7 +11,7 @@ const USERS_PAGE_SIZE = 10;
interface CouponFormValues extends CreateCouponPayload {
isReservedCoupon?: boolean;
couponCodes?: string[];
couponCodes: string[];
skuIds?: number[];
}
@ -23,11 +23,6 @@ interface CouponFormProps {
const couponValidationSchema = Yup.object().shape({
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(
Yup.string()
.required('Code is required')
@ -62,21 +57,17 @@ const couponValidationSchema = Yup.object().shape({
return this.createError({ message: 'Must provide either percentage or flat discount' });
}
return true;
}).test('reserved-codes-required', 'At least one coupon code is required', function(value: any) {
if (value.isReservedCoupon) {
}).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('reserved-codes-unique', 'Coupon codes must be unique', function(value: any) {
if (value.isReservedCoupon) {
}).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;
});
@ -124,8 +115,7 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
// User search functionality will be inside Formik
const defaultValues: CouponFormValues = {
couponCode: '',
couponCodes: [],
couponCodes: [''],
isUserBased: false,
isApplyForAll: false,
targetUsers: [],
@ -173,24 +163,11 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
<MyText style={tw`ml-2 text-sm font-medium text-gray-700`}>Is Reserved Coupon</MyText>
</View>
{/* Coupon Code (single, non-reserved) OR Coupon Codes (array, reserved) */}
{!isReserved ? (
<View style={tw`mb-4`}>
<MyTextInput
topLabel="Coupon Code"
placeholder="e.g., SAVE20"
value={values.couponCode || ''}
onChangeText={(text: string) => setFieldValue('couponCode', text.toUpperCase())}
keyboardType="default"
autoCapitalize="characters"
error={!!(touched.couponCode && errors.couponCode)}
/>
</View>
) : (
{/* Coupon Codes */}
<View style={tw`mb-4`}>
<Text style={tw`text-base font-bold mb-1`}>Coupon Codes</Text>
<Text style={tw`text-xs text-gray-500 mb-2`}>
Each code creates a separate reserved coupon with the same settings.
Each code creates a separate coupon with the same settings.
</Text>
{(values.couponCodes || []).map((code: string, index: number) => {
const codeError = Array.isArray(errors.couponCodes)
@ -217,7 +194,7 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
<TouchableOpacity
onPress={() => {
const next = (values.couponCodes || []).filter((_: string, i: number) => i !== index);
setFieldValue('couponCodes', next);
setFieldValue('couponCodes', next.length > 0 ? next : ['']);
}}
style={tw`ml-2 p-2`}
>
@ -236,7 +213,6 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
<Text style={tw`text-blue-600 font-medium`}>+ Add Code</Text>
</TouchableOpacity>
</View>
)}
{/* Discount Type Selection */}
<Text style={tw`text-base font-bold mb-2`}>
@ -308,7 +284,8 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
/>
</View>
{/* Maximum Discount Value */}
{/* Maximum Discount Value - only for percentage discounts */}
{values.flatDiscount === undefined && (
<View style={tw`mb-4`}>
<MyTextInput
topLabel="Maximum Discount Value"
@ -319,6 +296,7 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
error={!!(touched.maxValue && errors.maxValue)}
/>
</View>
)}
{/* Validity Period */}
<View style={tw`mb-4`}>

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import { View, ScrollView, Alert, Dimensions } from 'react-native';
import { Image } from 'expo-image';
import { useRouter, usePathname } from 'expo-router';
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 { useAllProducts, useStores, useSlots } from '@/src/hooks/prominent-api-hooks';
import { AllProductsApiType } from '@backend/trpc/router';
@ -445,6 +445,7 @@ interface FlashDeliveryProductsProps {
export function FlashDeliveryProducts({ storeId:storeIdParent, baseUrl, onProductPress }: FlashDeliveryProductsProps) {
useHideTabNav('quick_delivery');
const [isLoadingDialogOpen, setIsLoadingDialogOpen] = React.useState(false);
const [searchQuery, setSearchQuery] = useState('');
const router = useRouter();
const storeId = storeIdParent;
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 (
<View style={tw`flex-1`}>
<MyFlatList
data={flashProducts}
numColumns={2}
ListHeaderComponent={
<View style={tw`px-4 pt-2`}>
<SearchBar
value={searchQuery}
onChangeText={setSearchQuery}
placeholder="Search products in this store..."
/>
</View>
}
renderItem={({ item }) => (
<CompactProductCard
item={item}

View file

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