enh
This commit is contained in:
parent
7291bfe7ce
commit
496faffe0a
8 changed files with 209 additions and 249 deletions
|
|
@ -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`}>
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
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
|
|
||||||
|
|
||||||
mutation.mutate(payload, {
|
|
||||||
onSuccess: async () => {
|
|
||||||
await refetchCoupons()
|
await refetchCoupons()
|
||||||
await refetchReservedCoupons()
|
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() }
|
{ text: 'OK', onPress: () => router.back() }
|
||||||
]);
|
]);
|
||||||
},
|
} catch (error: any) {
|
||||||
onError: (error: any) => {
|
Alert.alert('Error', error.message || 'Failed to create coupons');
|
||||||
Alert.alert('Error', error.message || 'Failed to create coupon');
|
}
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -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] : [],
|
||||||
|
|
|
||||||
|
|
@ -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,21 +57,17 @@ 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,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>
|
<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`}>
|
|
||||||
<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>
|
|
||||||
) : (
|
|
||||||
<View style={tw`mb-4`}>
|
<View style={tw`mb-4`}>
|
||||||
<Text style={tw`text-base font-bold mb-1`}>Coupon Codes</Text>
|
<Text style={tw`text-base font-bold mb-1`}>Coupon Codes</Text>
|
||||||
<Text style={tw`text-xs text-gray-500 mb-2`}>
|
<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>
|
</Text>
|
||||||
{(values.couponCodes || []).map((code: string, index: number) => {
|
{(values.couponCodes || []).map((code: string, index: number) => {
|
||||||
const codeError = Array.isArray(errors.couponCodes)
|
const codeError = Array.isArray(errors.couponCodes)
|
||||||
|
|
@ -217,7 +194,7 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
const next = (values.couponCodes || []).filter((_: string, i: number) => i !== index);
|
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`}
|
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>
|
<Text style={tw`text-blue-600 font-medium`}>+ Add Code</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</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,7 +284,8 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Maximum Discount Value */}
|
{/* Maximum Discount Value - only for percentage discounts */}
|
||||||
|
{values.flatDiscount === undefined && (
|
||||||
<View style={tw`mb-4`}>
|
<View style={tw`mb-4`}>
|
||||||
<MyTextInput
|
<MyTextInput
|
||||||
topLabel="Maximum Discount Value"
|
topLabel="Maximum Discount Value"
|
||||||
|
|
@ -319,6 +296,7 @@ export default function CouponForm({ onSubmit, isLoading, initialValues }: Coupo
|
||||||
error={!!(touched.maxValue && errors.maxValue)}
|
error={!!(touched.maxValue && errors.maxValue)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Validity Period */}
|
{/* Validity Period */}
|
||||||
<View style={tw`mb-4`}>
|
<View style={tw`mb-4`}>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,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(
|
const coupon = await createCouponWithRelations(
|
||||||
{
|
{
|
||||||
couponCode: finalCouponCode,
|
couponCode: finalCouponCode,
|
||||||
|
|
@ -113,47 +119,10 @@ export const couponRouter = router({
|
||||||
applicableProducts
|
applicableProducts
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
createdCoupons.push(coupon);
|
||||||
// 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,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,19 +388,29 @@ 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) {
|
||||||
|
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);
|
const codeExists = await checkReservedCouponExists(secretCode);
|
||||||
if (codeExists) {
|
if (codeExists) {
|
||||||
throw new Error("Secret code already exists");
|
throw new Error(`Secret code already exists: ${secretCode}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const coupon = await createReservedCouponWithProducts(
|
const coupon = await createReservedCouponWithProducts(
|
||||||
{
|
{
|
||||||
secretCode,
|
secretCode,
|
||||||
couponCode: couponCode || `RESERVED${Date.now().toString().slice(-6)}`,
|
couponCode: secretCode,
|
||||||
discountPercent: discountPercent?.toString(),
|
discountPercent: discountPercent?.toString(),
|
||||||
flatDiscount: flatDiscount?.toString(),
|
flatDiscount: flatDiscount?.toString(),
|
||||||
minOrder: minOrder?.toString(),
|
minOrder: minOrder?.toString(),
|
||||||
|
|
@ -444,36 +424,10 @@ export const couponRouter = router({
|
||||||
applicableProducts
|
applicableProducts
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
createdCoupons.push(coupon);
|
||||||
// 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,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
return coupon;
|
return createdCoupons;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getUsersMiniInfo: protectedProcedure
|
getUsersMiniInfo: protectedProcedure
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue