diff --git a/apps/admin-ui/app/(drawer)/dashboard/complaints/index.tsx b/apps/admin-ui/app/(drawer)/dashboard/complaints/index.tsx
index 1fc927e..c02924b 100644
--- a/apps/admin-ui/app/(drawer)/dashboard/complaints/index.tsx
+++ b/apps/admin-ui/app/(drawer)/dashboard/complaints/index.tsx
@@ -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}
+ {item.response && (
+
+
+
+
+ Admin Reply
+
+
+
+ {item.response}
+
+
+ )}
+
{item.images && item.images.length > 0 && (
diff --git a/apps/admin-ui/app/(drawer)/dashboard/coupons/create.tsx b/apps/admin-ui/app/(drawer)/dashboard/coupons/create.tsx
index ba3bbac..5474054 100644
--- a/apps/admin-ui/app/(drawer)/dashboard/coupons/create.tsx
+++ b/apps/admin-ui/app/(drawer)/dashboard/coupons/create.tsx
@@ -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 () => {
- await refetchCoupons()
- await refetchReservedCoupons()
- Alert.alert('Success', `${isReservedCoupon ? 'Reserved coupon' : 'Coupon'} created successfully`, [
- { text: 'OK', onPress: () => router.back() }
- ]);
- },
- onError: (error: any) => {
- Alert.alert('Error', error.message || 'Failed to create coupon');
- },
- });
+ try {
+ await mutation.mutateAsync({ ...payload, couponCodes: codes });
+ await refetchCoupons()
+ await refetchReservedCoupons()
+ Alert.alert('Success', `${codes.length} coupon${codes.length > 1 ? 's' : ''} created successfully`, [
+ { text: 'OK', onPress: () => router.back() }
+ ]);
+ } catch (error: any) {
+ Alert.alert('Error', error.message || 'Failed to create coupons');
+ }
};
return (
diff --git a/apps/admin-ui/app/(drawer)/dashboard/coupons/edit/[id].tsx b/apps/admin-ui/app/(drawer)/dashboard/coupons/edit/[id].tsx
index 1320290..cfb8dd4 100644
--- a/apps/admin-ui/app/(drawer)/dashboard/coupons/edit/[id].tsx
+++ b/apps/admin-ui/app/(drawer)/dashboard/coupons/edit/[id].tsx
@@ -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 = {
- couponCode: coupon.couponCode,
+ couponCodes: coupon.couponCode ? [coupon.couponCode] : [''],
isUserBased: coupon.isUserBased,
isApplyForAll: coupon.isApplyForAll,
targetUsers: coupon.targetUser ? [coupon.targetUser.id] : [],
diff --git a/apps/admin-ui/src/components/CouponForm.tsx b/apps/admin-ui/src/components/CouponForm.tsx
index 020f922..e39be7c 100644
--- a/apps/admin-ui/src/components/CouponForm.tsx
+++ b/apps/admin-ui/src/components/CouponForm.tsx
@@ -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,20 +57,16 @@ 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) {
- 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' });
- }
+}).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) {
- 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' });
- }
+}).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,70 +163,56 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
Is Reserved Coupon
- {/* Coupon Code (single, non-reserved) OR Coupon Codes (array, reserved) */}
- {!isReserved ? (
-
- setFieldValue('couponCode', text.toUpperCase())}
- keyboardType="default"
- autoCapitalize="characters"
- error={!!(touched.couponCode && errors.couponCode)}
- />
-
- ) : (
-
- Coupon Codes
-
- Each code creates a separate reserved 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);
+ {/* 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);
}}
- style={tw`ml-2 p-2`}
- >
- ✕
-
+ keyboardType="default"
+ autoCapitalize="characters"
+ error={!!codeError}
+ />
+ {codeError && {codeError}}
- );
- })}
- {(errors.couponCodes as any)?.message && (
- {(errors.couponCodes as any).message}
- )}
- setFieldValue('couponCodes', [...(values.couponCodes || []), ''])}
- style={tw`mt-2 py-2 px-3 bg-blue-50 rounded-lg border border-blue-200 self-start`}
- >
- + Add Code
-
-
- )}
+ {
+ 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`mt-2 py-2 px-3 bg-blue-50 rounded-lg border border-blue-200 self-start`}
+ >
+ + Add Code
+
+
{/* Discount Type Selection */}
@@ -308,17 +284,19 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
/>
- {/* Maximum Discount Value */}
-
- setFieldValue('maxValue', parseFloat(text) || undefined)}
- keyboardType="numeric"
- error={!!(touched.maxValue && errors.maxValue)}
- />
-
+ {/* Maximum Discount Value - only for percentage discounts */}
+ {values.flatDiscount === undefined && (
+
+ setFieldValue('maxValue', parseFloat(text) || undefined)}
+ keyboardType="numeric"
+ error={!!(touched.maxValue && errors.maxValue)}
+ />
+
+ )}
{/* Validity Period */}
diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/complaint.ts b/apps/backend/src/trpc/apis/admin-apis/apis/complaint.ts
index f4463ec..bca4d06 100644
--- a/apps/backend/src/trpc/apis/admin-apis/apis/complaint.ts
+++ b/apps/backend/src/trpc/apis/admin-apis/apis/complaint.ts
@@ -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,
};
})
);
diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts b/apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts
index e8e9bfd..703cd2a 100644
--- a/apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts
+++ b/apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts
@@ -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 => {
- const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, skuIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input;
+ .mutation(async ({ input, ctx }): Promise => {
+ 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,66 +92,37 @@ export const couponRouter = router({
}
}
- const coupon = await createCouponWithRelations(
- {
- 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,
- },
- applicableUsers,
- applicableProducts
- );
+ 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}`);
+ }
- /*
- // 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,
- }))
+ const coupon = await createCouponWithRelations(
+ {
+ 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,
+ },
+ applicableUsers,
+ applicableProducts
);
+
+ 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 => {
- 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,62 +388,46 @@ 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()}`;
-
- // Using dbService helper (new implementation)
- const codeExists = await checkReservedCouponExists(secretCode);
- if (codeExists) {
- throw new Error("Secret code already exists");
+ // 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}`];
}
- const coupon = await createReservedCouponWithProducts(
- {
- 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,
- },
- applicableProducts
- );
+ 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: ${secretCode}`);
+ }
- /*
- // 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,
- }))
+ const coupon = await createReservedCouponWithProducts(
+ {
+ secretCode,
+ couponCode: secretCode,
+ 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,
+ },
+ applicableProducts
);
- }
- */
- return coupon;
+ createdCoupons.push(coupon);
+ }
+
+ return createdCoupons;
}),
getUsersMiniInfo: protectedProcedure
diff --git a/apps/user-ui/components/SlotSpecificView.tsx b/apps/user-ui/components/SlotSpecificView.tsx
index 2e1fd4e..7880df4 100644
--- a/apps/user-ui/components/SlotSpecificView.tsx
+++ b/apps/user-ui/components/SlotSpecificView.tsx
@@ -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 (
+
+
+ }
renderItem={({ item }) => (