apply discount

This commit is contained in:
hamid zarghami
2025-12-07 12:23:15 +03:30
parent 88088c2ed5
commit fea922a2fa
6 changed files with 144 additions and 43 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -16,6 +16,12 @@ export interface CartItem {
totalPrice: number;
}
export interface Coupon {
couponId: string;
couponName: string;
couponCode: string;
}
export interface CartData {
userId: string;
restaurantId: string;
@@ -34,6 +40,7 @@ export interface CartData {
deliveryMethodId?: string;
addressId?: string;
paymentMethodId?: string;
coupon?: Coupon;
}
export interface CartCategory {
@@ -1,10 +1,15 @@
'use client';
import Combobox, { ComboboxOption } from '@/components/combobox/Combobox';
import InputField from '@/components/input/InputField';
import clsx from 'clsx';
import { Cup, Icon, Ticket, TicketDiscount } from 'iconsax-react';
import { Cup, Icon, TickCircle, Ticket, Trash } from 'iconsax-react';
import { useTranslation } from 'react-i18next';
import { useApplyCoupon, useRemoveCoupon } from '../../hooks/useOrderData';
import Button from '@/components/button/PrimaryButton';
import { extractErrorMessage } from '@/lib/func';
import { toast } from '@/components/Toast';
import { useEffect, useState } from 'react';
import { useGetCartItems } from '@/app/[name]/(Dialogs)/cart/hooks/useCartData';
type CouponSectionProps = {
couponType: string;
@@ -21,26 +26,58 @@ export const CouponSection = ({
onCouponTypeChange,
onCouponCodeChange,
}: CouponSectionProps) => {
const { t } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
const { t } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
const { mutate: applyCoupon, isPending } = useApplyCoupon();
const { mutate: removeCoupon, isPending: isRemoving } = useRemoveCoupon();
const { data: cartItems } = useGetCartItems();
const [isApplied, setIsApplied] = useState<boolean>(false);
const couponOptions: Array<{ id: string, label: string, icon: Icon }> = [
{ id: '0', label: t("SectionCoupon.InputCouponType.Options.Discount"), icon: Cup },
{ id: '1', label: t("SectionCoupon.InputCouponType.Options.GiftCard"), icon: Ticket },
];
const couponDropdownOptions: Array<ComboboxOption> = [
{ id: '0', title: t("SectionCoupon.InputCouponCode.Options.Default") },
{ id: '1', title: "Something" },
];
const handleCouponCodeChange = (id: string) => {
if (id === '0') {
onCouponCodeChange('');
return;
}
onCouponCodeChange(id);
const handleApplyCoupon = () => {
applyCoupon(couponCode, {
onSuccess: () => {
toast('کوپن با موفقیت اعمال شد', 'success');
setIsApplied(true);
},
onError: (error) => {
toast(extractErrorMessage(error), 'error');
setIsApplied(false);
}
});
};
const handleCouponCodeChange = (value: string) => {
onCouponCodeChange(value);
if (isApplied) {
setIsApplied(false);
}
};
const handleRemoveCoupon = () => {
removeCoupon(undefined, {
onSuccess: () => {
toast('کوپن با موفقیت حذف شد', 'success');
setIsApplied(false);
onCouponCodeChange('');
},
onError: (error) => {
toast(extractErrorMessage(error), 'error');
}
});
};
useEffect(() => {
const hasCoupon = (cartItems?.data?.couponDiscount ?? 0) > 0;
setIsApplied(hasCoupon);
if (hasCoupon && cartItems?.data?.coupon?.couponCode) {
onCouponCodeChange(cartItems.data.coupon.couponCode);
}
}, [cartItems, onCouponCodeChange]);
return (
<section className="bg-container rounded-container box-shadow-normal p-4">
<div className="py-3">
@@ -69,33 +106,50 @@ export const CouponSection = ({
))}
</div>
<Combobox
className={clsx(
'relative mt-6',
couponType === '0' ? 'block' : 'hidden',
couponCode.length === 0 && 'text-border **:stroke-border dark:text-neutral-500 dark:**:stroke-neutral-500'
)}
icon={TicketDiscount}
placeholder={t("SectionCoupon.InputCouponCode.Placeholder")}
title=''
searchable={false}
options={couponDropdownOptions}
selectedId={couponCode}
onSelectionChange={(e, i) => handleCouponCodeChange(String(i))}
/>
{isApplied ? (
<div className='mt-6 flex items-center gap-3'>
<div className='flex-1 flex items-center gap-2 px-4 py-2.5 bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 rounded-normal'>
<TickCircle size={20} color='#10b981' variant='Bold' />
<span className='text-xs text-emerald-700 dark:text-emerald-400 font-medium'>کد تخفیف اعمال شده</span>
<span className='text-xs text-emerald-600 dark:text-emerald-500 font-medium mr-auto'>{couponCode}</span>
</div>
<button
onClick={handleRemoveCoupon}
disabled={isRemoving}
className='p-2.5 hover:bg-gray-100 dark:hover:bg-neutral-800 rounded-normal transition-colors disabled:opacity-50'
type='button'
>
<Trash color='currentColor' size={20} className='text-gray-600 dark:text-gray-400' />
</button>
</div>
) : (
<div className='flex gap-4 items-end'>
<InputField
autoComplete='off'
className={clsx(
'relative mt-6',
couponCodeError.trim() === 'valid' && 'text-emerald-400 **:stroke-emerald-400 ring ring-emerald-400'
)}
placeholder={t("SectionCoupon.InputCouponCode.Placeholder")}
htmlFor={'couponCode'}
onChange={(e) => handleCouponCodeChange(e.target.value)}
value={couponCode}
/>
<Button
disabled={couponCode.length === 0 || isPending}
className='w-fit h-11'
onClick={handleApplyCoupon}
pending={isPending}
>
<div className='flex items-center gap-2'>
<TickCircle size={16} color='currentColor' variant='Outline' />
<span className='text-xs whitespace-nowrap'>اعمال</span>
</div>
</Button>
</div>
)}
<InputField
autoComplete='off'
className={clsx(
'relative mt-6',
couponType === '1' ? '' : 'hidden!',
couponCodeError.trim() === 'valid' && 'text-emerald-400 **:stroke-emerald-400 ring ring-emerald-400'
)}
placeholder={t("SectionCoupon.InputCouponCode.Placeholder")}
htmlFor={'couponCode'}
onChange={(e) => onCouponCodeChange(e.target.value)}
value={couponCode}
/>
</div>
</section>
);
@@ -79,6 +79,12 @@ function OrderDetailInex() {
}
}, [cartItems?.data?.addressId, setAddressCart, setIsSelectedAddress, setSelectedAddressId]);
useEffect(() => {
if (cartItems?.data?.coupon?.couponCode && cartItems.data.couponDiscount > 0) {
setCouponCode(cartItems.data.coupon.couponCode);
}
}, [cartItems?.data?.coupon?.couponCode, cartItems?.data?.couponDiscount]);
const selectedDeliveryMethod = useMemo(() => {
if (!shipmentMethod?.data || !deliveryType) return null;
return shipmentMethod.data.find((item) => item.id === deliveryType);
@@ -1,4 +1,4 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
getPaymentMethod,
getShipmentMethod,
@@ -6,6 +6,8 @@ import {
setDeliveryMethod,
setPaymentMethod,
createOrder,
applyCoupon,
removeCoupon,
} from "../service/OrderService";
export const useGetShipmentMethod = () => {
@@ -45,3 +47,25 @@ export const useCreateOrder = () => {
mutationFn: createOrder,
});
};
export const useApplyCoupon = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: applyCoupon,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["cart-items"] });
queryClient.invalidateQueries({ queryKey: ["cart-total"] });
},
});
};
export const useRemoveCoupon = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: removeCoupon,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["cart-items"] });
queryClient.invalidateQueries({ queryKey: ["cart-total"] });
},
});
};
@@ -42,3 +42,13 @@ export const createOrder = async (): Promise<CreateOrderResponse> => {
const { data } = await api.post<CreateOrderResponse>("/public/checkout");
return data;
};
export const applyCoupon = async (code: string) => {
const { data } = await api.post("/public/cart/apply-coupon", { code });
return data;
};
export const removeCoupon = async () => {
const { data } = await api.delete("/public/cart/coupon");
return data;
};