apply discount
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
@@ -16,6 +16,12 @@ export interface CartItem {
|
|||||||
totalPrice: number;
|
totalPrice: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Coupon {
|
||||||
|
couponId: string;
|
||||||
|
couponName: string;
|
||||||
|
couponCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CartData {
|
export interface CartData {
|
||||||
userId: string;
|
userId: string;
|
||||||
restaurantId: string;
|
restaurantId: string;
|
||||||
@@ -34,6 +40,7 @@ export interface CartData {
|
|||||||
deliveryMethodId?: string;
|
deliveryMethodId?: string;
|
||||||
addressId?: string;
|
addressId?: string;
|
||||||
paymentMethodId?: string;
|
paymentMethodId?: string;
|
||||||
|
coupon?: Coupon;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CartCategory {
|
export interface CartCategory {
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Combobox, { ComboboxOption } from '@/components/combobox/Combobox';
|
|
||||||
import InputField from '@/components/input/InputField';
|
import InputField from '@/components/input/InputField';
|
||||||
import clsx from 'clsx';
|
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 { 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 = {
|
type CouponSectionProps = {
|
||||||
couponType: string;
|
couponType: string;
|
||||||
@@ -21,26 +26,58 @@ export const CouponSection = ({
|
|||||||
onCouponTypeChange,
|
onCouponTypeChange,
|
||||||
onCouponCodeChange,
|
onCouponCodeChange,
|
||||||
}: CouponSectionProps) => {
|
}: 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 }> = [
|
const couponOptions: Array<{ id: string, label: string, icon: Icon }> = [
|
||||||
{ id: '0', label: t("SectionCoupon.InputCouponType.Options.Discount"), icon: Cup },
|
{ id: '0', label: t("SectionCoupon.InputCouponType.Options.Discount"), icon: Cup },
|
||||||
{ id: '1', label: t("SectionCoupon.InputCouponType.Options.GiftCard"), icon: Ticket },
|
{ id: '1', label: t("SectionCoupon.InputCouponType.Options.GiftCard"), icon: Ticket },
|
||||||
];
|
];
|
||||||
|
|
||||||
const couponDropdownOptions: Array<ComboboxOption> = [
|
const handleApplyCoupon = () => {
|
||||||
{ id: '0', title: t("SectionCoupon.InputCouponCode.Options.Default") },
|
applyCoupon(couponCode, {
|
||||||
{ id: '1', title: "Something" },
|
onSuccess: () => {
|
||||||
];
|
toast('کوپن با موفقیت اعمال شد', 'success');
|
||||||
|
setIsApplied(true);
|
||||||
const handleCouponCodeChange = (id: string) => {
|
},
|
||||||
if (id === '0') {
|
onError: (error) => {
|
||||||
onCouponCodeChange('');
|
toast(extractErrorMessage(error), 'error');
|
||||||
return;
|
setIsApplied(false);
|
||||||
}
|
}
|
||||||
onCouponCodeChange(id);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<section className="bg-container rounded-container box-shadow-normal p-4">
|
<section className="bg-container rounded-container box-shadow-normal p-4">
|
||||||
<div className="py-3">
|
<div className="py-3">
|
||||||
@@ -57,7 +94,7 @@ export const CouponSection = ({
|
|||||||
/>
|
/>
|
||||||
<span className='text-xs mt-0.5'>{label}</span>
|
<span className='text-xs mt-0.5'>{label}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
checked={couponType === id}
|
checked={couponType === id}
|
||||||
onChange={() => onCouponTypeChange(id)}
|
onChange={() => onCouponTypeChange(id)}
|
||||||
@@ -69,33 +106,50 @@ export const CouponSection = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Combobox
|
{isApplied ? (
|
||||||
className={clsx(
|
<div className='mt-6 flex items-center gap-3'>
|
||||||
'relative mt-6',
|
<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'>
|
||||||
couponType === '0' ? 'block' : 'hidden',
|
<TickCircle size={20} color='#10b981' variant='Bold' />
|
||||||
couponCode.length === 0 && 'text-border **:stroke-border dark:text-neutral-500 dark:**:stroke-neutral-500'
|
<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>
|
||||||
icon={TicketDiscount}
|
</div>
|
||||||
placeholder={t("SectionCoupon.InputCouponCode.Placeholder")}
|
<button
|
||||||
title=''
|
onClick={handleRemoveCoupon}
|
||||||
searchable={false}
|
disabled={isRemoving}
|
||||||
options={couponDropdownOptions}
|
className='p-2.5 hover:bg-gray-100 dark:hover:bg-neutral-800 rounded-normal transition-colors disabled:opacity-50'
|
||||||
selectedId={couponCode}
|
type='button'
|
||||||
onSelectionChange={(e, i) => handleCouponCodeChange(String(i))}
|
>
|
||||||
/>
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -79,6 +79,12 @@ function OrderDetailInex() {
|
|||||||
}
|
}
|
||||||
}, [cartItems?.data?.addressId, setAddressCart, setIsSelectedAddress, setSelectedAddressId]);
|
}, [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(() => {
|
const selectedDeliveryMethod = useMemo(() => {
|
||||||
if (!shipmentMethod?.data || !deliveryType) return null;
|
if (!shipmentMethod?.data || !deliveryType) return null;
|
||||||
return shipmentMethod.data.find((item) => item.id === deliveryType);
|
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 {
|
import {
|
||||||
getPaymentMethod,
|
getPaymentMethod,
|
||||||
getShipmentMethod,
|
getShipmentMethod,
|
||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
setDeliveryMethod,
|
setDeliveryMethod,
|
||||||
setPaymentMethod,
|
setPaymentMethod,
|
||||||
createOrder,
|
createOrder,
|
||||||
|
applyCoupon,
|
||||||
|
removeCoupon,
|
||||||
} from "../service/OrderService";
|
} from "../service/OrderService";
|
||||||
|
|
||||||
export const useGetShipmentMethod = () => {
|
export const useGetShipmentMethod = () => {
|
||||||
@@ -45,3 +47,25 @@ export const useCreateOrder = () => {
|
|||||||
mutationFn: createOrder,
|
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");
|
const { data } = await api.post<CreateOrderResponse>("/public/checkout");
|
||||||
return data;
|
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;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user