card to card
This commit is contained in:
@@ -1,44 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import { Card, Icon, Wallet2 } from 'iconsax-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { useGetPaymentMethod } from '../../hooks/useOrderData';
|
||||
import { useCheckoutStore } from '../../store/Store';
|
||||
import { useGetUserWallet } from '@/app/[name]/(Main)/transactions/hooks/useTransactionData';
|
||||
import { useGetCartItems } from '@/app/[name]/(Main)/cart/hooks/useCartData';
|
||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||
import { useGetProfile, useSingleUpload } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||
import { PaymentMethodEnum } from '../../enum/Enum';
|
||||
import { toast } from '@/components/Toast';
|
||||
import { extractErrorMessage } from '@/lib/func';
|
||||
import Image from 'next/image';
|
||||
|
||||
type PaymentSectionProps = {
|
||||
paymentType: string;
|
||||
onPaymentTypeChange: (id: string) => void;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const getIconByMethod = (method: "CardOnDelivery" | "Cash" | "Online", _gateway: string | null): Icon => {
|
||||
if (method === "Online" || method === "CardOnDelivery") {
|
||||
const getIconByMethod = (method: PaymentMethodEnum): Icon => {
|
||||
if (method === PaymentMethodEnum.Online || method === PaymentMethodEnum.CreditCard) {
|
||||
return Card;
|
||||
}
|
||||
if (method === "Cash") {
|
||||
if (method === PaymentMethodEnum.Cash || method === PaymentMethodEnum.Wallet) {
|
||||
return Wallet2;
|
||||
}
|
||||
return Card;
|
||||
};
|
||||
|
||||
const isWalletPayment = (method: string, gateway: string | null): boolean => {
|
||||
return gateway?.toLowerCase() === 'wallet' || method.toLowerCase() === 'wallet';
|
||||
};
|
||||
|
||||
export const PaymentSection = ({ paymentType, onPaymentTypeChange }: PaymentSectionProps) => {
|
||||
|
||||
const { t: tOrderDetail } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
|
||||
const { t: tOrders } = useTranslation('orders');
|
||||
const { data: paymentMethod } = useGetPaymentMethod();
|
||||
const { setIsSelectedPayment } = useCheckoutStore();
|
||||
const {
|
||||
setIsSelectedPayment,
|
||||
paymentDesc,
|
||||
setPaymentDesc,
|
||||
paymentAttachments,
|
||||
setPaymentAttachments,
|
||||
} = useCheckoutStore();
|
||||
const { data: userWallet } = useGetUserWallet();
|
||||
const { isSuccess } = useGetProfile();
|
||||
const { data: cartData } = useGetCartItems(isSuccess);
|
||||
const { mutateAsync: singleUpload, isPending: isUploading } = useSingleUpload();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const walletAmount = userWallet?.data?.balance ?? 0;
|
||||
const totalAmount = cartData?.data?.total ?? 0;
|
||||
@@ -63,14 +70,50 @@ export const PaymentSection = ({ paymentType, onPaymentTypeChange }: PaymentSect
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
label: getPaymentMethodTranslation(item.method, item.title),
|
||||
icon: getIconByMethod(item.method, item.gateway),
|
||||
label: getPaymentMethodTranslation(item.method, item.description),
|
||||
icon: getIconByMethod(item.method),
|
||||
method: item.method,
|
||||
gateway: item.gateway,
|
||||
isWallet: isWalletPayment(item.method, item.gateway),
|
||||
cardNumber: item.cardNumber,
|
||||
description: item.description,
|
||||
isWallet: item.method === PaymentMethodEnum.Wallet,
|
||||
}));
|
||||
}, [paymentMethod, getPaymentMethodTranslation]);
|
||||
|
||||
const selectedCreditCard = useMemo(() => {
|
||||
const selected = paymentOptions.find(opt => opt.id === paymentType);
|
||||
if (selected?.method === PaymentMethodEnum.CreditCard) return selected;
|
||||
return null;
|
||||
}, [paymentOptions, paymentType]);
|
||||
|
||||
const handlePaymentTypeChange = (value: string) => {
|
||||
const selected = paymentOptions.find(opt => opt.id === value);
|
||||
if (selected?.method !== PaymentMethodEnum.CreditCard) {
|
||||
setPaymentDesc('');
|
||||
setPaymentAttachments([]);
|
||||
}
|
||||
onPaymentTypeChange(value);
|
||||
setIsSelectedPayment(true);
|
||||
};
|
||||
|
||||
const handleReceiptUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const response = await singleUpload(file);
|
||||
if (response?.data?.url) {
|
||||
setPaymentAttachments([response.data.url]);
|
||||
toast(tOrderDetail('SectionPayment.CreditCard.UploadSuccess'), 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
toast(extractErrorMessage(error), 'error');
|
||||
} finally {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="bg-container rounded-container box-shadow-normal p-4">
|
||||
<div className="py-3">
|
||||
@@ -78,15 +121,11 @@ export const PaymentSection = ({ paymentType, onPaymentTypeChange }: PaymentSect
|
||||
|
||||
<RadioGroup
|
||||
value={paymentType}
|
||||
onValueChange={(value) => {
|
||||
onPaymentTypeChange(value);
|
||||
setIsSelectedPayment(true);
|
||||
}}
|
||||
onValueChange={handlePaymentTypeChange}
|
||||
className='flex flex-col gap-6 mt-6'
|
||||
>
|
||||
{paymentOptions.map(({ id, label, icon: Icon, isWallet }) => {
|
||||
const isDisabled = isWallet && isWalletInsufficient;
|
||||
const walletOption = paymentOptions.find(opt => opt.id === id && opt.isWallet);
|
||||
|
||||
return (
|
||||
<div key={id} className='flex items-center justify-between'>
|
||||
@@ -100,12 +139,11 @@ export const PaymentSection = ({ paymentType, onPaymentTypeChange }: PaymentSect
|
||||
htmlFor={`paymentMethod${id}`}
|
||||
>
|
||||
<div className='flex gap-1'>
|
||||
{walletOption && (
|
||||
{isWallet && (
|
||||
<span className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
({formatPrice(walletAmount)} T)
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className='text-xs mt-0.5'>{label}</span>
|
||||
</div>
|
||||
<Icon
|
||||
@@ -118,8 +156,83 @@ export const PaymentSection = ({ paymentType, onPaymentTypeChange }: PaymentSect
|
||||
);
|
||||
})}
|
||||
</RadioGroup>
|
||||
|
||||
{selectedCreditCard && (
|
||||
<div className='mt-6 pt-4 border-t border-border flex flex-col gap-4'>
|
||||
{selectedCreditCard.cardNumber && (
|
||||
<div>
|
||||
<span className='text-xs text-gray-500 dark:text-gray-400'>
|
||||
{tOrderDetail('SectionPayment.CreditCard.CardNumberLabel')}
|
||||
</span>
|
||||
<p className='text-sm font-medium mt-1' dir='ltr'>
|
||||
{selectedCreditCard.cardNumber}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedCreditCard.description && (
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 leading-5'>
|
||||
{selectedCreditCard.description}
|
||||
</p>
|
||||
)}
|
||||
<div>
|
||||
<label className='text-xs text-gray-500 dark:text-gray-400' htmlFor='paymentDesc'>
|
||||
{tOrderDetail('SectionPayment.CreditCard.PaymentDescLabel')}
|
||||
</label>
|
||||
<textarea
|
||||
id='paymentDesc'
|
||||
className='w-full px-4 py-2.5 mt-2 text-sm2 leading-6 outline-0 border border-border rounded-normal resize-none focus:ring-0'
|
||||
placeholder={tOrderDetail('SectionPayment.CreditCard.PaymentDescPlaceholder')}
|
||||
value={paymentDesc}
|
||||
onChange={(e) => setPaymentDesc(e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span className='text-xs text-gray-500 dark:text-gray-400'>
|
||||
{tOrderDetail('SectionPayment.CreditCard.ReceiptLabel')}
|
||||
</span>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type='file'
|
||||
accept='image/*'
|
||||
className='hidden'
|
||||
onChange={handleReceiptUpload}
|
||||
/>
|
||||
{paymentAttachments[0] ? (
|
||||
<div className='mt-2 relative w-full h-32 rounded-normal overflow-hidden border border-border'>
|
||||
<Image
|
||||
src={paymentAttachments[0]}
|
||||
alt='receipt'
|
||||
fill
|
||||
className='object-contain'
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
className='absolute top-2 left-2 text-xs bg-white/90 dark:bg-background/90 px-2 py-1 rounded-normal'
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isUploading}
|
||||
>
|
||||
{isUploading
|
||||
? tOrderDetail('SectionPayment.CreditCard.Uploading')
|
||||
: tOrderDetail('SectionPayment.CreditCard.UploadReceipt')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type='button'
|
||||
className='w-full mt-2 py-3 text-sm2 border border-dashed border-border rounded-normal text-gray-500 dark:text-gray-400 hover:border-primary transition-colors'
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isUploading}
|
||||
>
|
||||
{isUploading
|
||||
? tOrderDetail('SectionPayment.CreditCard.Uploading')
|
||||
: tOrderDetail('SectionPayment.CreditCard.UploadReceipt')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -12,9 +12,10 @@ import { useParams } from 'next/navigation';
|
||||
import { useGetCartItems, useSaveAllMethod } from '@/app/[name]/(Main)/cart/hooks/useCartData';
|
||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useGetShipmentMethod } from '../../hooks/useOrderData';
|
||||
import { useGetShipmentMethod, useGetPaymentMethod } from '../../hooks/useOrderData';
|
||||
import CartItemsList from '@/app/[name]/(Main)/cart/components/CartItemsList';
|
||||
import { useCartStore } from '@/app/[name]/(Main)/cart/store/Store';
|
||||
import { PaymentMethodEnum } from '../../enum/Enum';
|
||||
|
||||
type SummarySectionProps = {
|
||||
cartModal: boolean;
|
||||
@@ -25,7 +26,7 @@ type SummarySectionProps = {
|
||||
export const SummarySection = ({ cartModal, onToggleCartModal, deliveryType }: SummarySectionProps) => {
|
||||
|
||||
const { t } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
|
||||
const { isSelectedShipment, isSelectedPayment, deliveryType: storeDeliveryType, paymentType, selectedAddressId, tableNumber, carAddress, clear } = useCheckoutStore();
|
||||
const { isSelectedShipment, isSelectedPayment, deliveryType: storeDeliveryType, paymentType, selectedAddressId, tableNumber, carAddress, paymentDesc, paymentAttachments, clear } = useCheckoutStore();
|
||||
const { description, reset: resetCart } = useCartStore();
|
||||
const { mutate: createOrder, isPending: isCreateOrderPending } = useCreateOrder();
|
||||
const { mutate: saveAllMethod, isPending: isSaveAllMethodPending } = useSaveAllMethod()
|
||||
@@ -34,6 +35,7 @@ export const SummarySection = ({ cartModal, onToggleCartModal, deliveryType }: S
|
||||
const { isSuccess } = useGetProfile();
|
||||
const { data: cartData } = useGetCartItems(isSuccess);
|
||||
const { data: shipmentMethod } = useGetShipmentMethod();
|
||||
const { data: paymentMethod } = useGetPaymentMethod();
|
||||
|
||||
const selectedDeliveryMethod = useMemo(() => {
|
||||
if (!shipmentMethod?.data || !deliveryType) return null;
|
||||
@@ -52,6 +54,17 @@ export const SummarySection = ({ cartModal, onToggleCartModal, deliveryType }: S
|
||||
return selectedDeliveryMethod?.method === 'dineIn';
|
||||
}, [selectedDeliveryMethod]);
|
||||
|
||||
const selectedPaymentMethod = useMemo(() => {
|
||||
if (!paymentMethod?.data || !paymentType) return null;
|
||||
return paymentMethod.data.find((item) => item.id === paymentType);
|
||||
}, [paymentMethod?.data, paymentType]);
|
||||
|
||||
const isCreditCardPayment = selectedPaymentMethod?.method === PaymentMethodEnum.CreditCard;
|
||||
|
||||
const isCreditCardIncomplete = isCreditCardPayment && (
|
||||
!paymentDesc.trim() || paymentAttachments.length === 0
|
||||
);
|
||||
|
||||
const formatPrice = useCallback(
|
||||
(value: number) => value.toLocaleString('fa-IR'),
|
||||
[]
|
||||
@@ -91,6 +104,17 @@ export const SummarySection = ({ cartModal, onToggleCartModal, deliveryType }: S
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCreditCardPayment) {
|
||||
if (!paymentDesc.trim()) {
|
||||
toast('لطفا توضیحات پرداخت را وارد کنید', 'error');
|
||||
return;
|
||||
}
|
||||
if (paymentAttachments.length === 0) {
|
||||
toast('لطفا تصویر رسید پرداخت را آپلود کنید', 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// if (!isDeliveryCar && !isDineIn && !selectedAddressId) {
|
||||
// toast('لطفا آدرس را انتخاب کنید', 'error');
|
||||
// return;
|
||||
@@ -103,6 +127,10 @@ export const SummarySection = ({ cartModal, onToggleCartModal, deliveryType }: S
|
||||
tableNumber: isDineIn ? tableNumber.toString() : undefined,
|
||||
...(isCourierDelivery && selectedAddressId && { addressId: selectedAddressId }),
|
||||
...(isDeliveryCar && carAddress && { carAddress }),
|
||||
...(isCreditCardPayment && {
|
||||
attachments: paymentAttachments,
|
||||
paymentDesc: paymentDesc.trim(),
|
||||
}),
|
||||
};
|
||||
saveAllMethod(saveAllMethodData, {
|
||||
onSuccess: () => {
|
||||
@@ -192,7 +220,12 @@ export const SummarySection = ({ cartModal, onToggleCartModal, deliveryType }: S
|
||||
<div className='text-xs text-gray-400 dark:text-gray-500'>{t("PayableAmountLabel")}</div>
|
||||
<div className='text-sm mt-2 font-semibold'>{formatPrice(total)} تومان</div>
|
||||
</div>
|
||||
<Button pending={isCreateOrderPending || isSaveAllMethodPending} onClick={handleSubmit} className='px-10 w-fit dark:bg-white dark:text-black! dark:hover:bg-gray-100'>
|
||||
<Button
|
||||
pending={isCreateOrderPending || isSaveAllMethodPending}
|
||||
disabled={isCreditCardIncomplete}
|
||||
onClick={handleSubmit}
|
||||
className='px-10 w-fit dark:bg-white dark:text-black! dark:hover:bg-gray-100'
|
||||
>
|
||||
{t("ButtonSubmit")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
export enum PaymentMethodEnum {
|
||||
Online = "Online",
|
||||
Cash = "Cash",
|
||||
Wallet = "Wallet",
|
||||
CreditCard = "CreditCard",
|
||||
}
|
||||
|
||||
export const enum OrderStatus {
|
||||
NEW = "new",
|
||||
PENDING_PAYMENT = "pendingPayment",
|
||||
|
||||
@@ -15,6 +15,8 @@ const initialState = {
|
||||
carColor: string;
|
||||
plateNumber: string;
|
||||
} | null,
|
||||
paymentDesc: "",
|
||||
paymentAttachments: [] as string[],
|
||||
lastUpdated: null as number | null,
|
||||
};
|
||||
|
||||
@@ -38,6 +40,10 @@ export const useCheckoutStore = create<CheckoutStoreType>()(
|
||||
set({ tableNumber: value, lastUpdated: Date.now() }),
|
||||
setCarAddress: (value) =>
|
||||
set({ carAddress: value, lastUpdated: Date.now() }),
|
||||
setPaymentDesc: (value) =>
|
||||
set({ paymentDesc: value, lastUpdated: Date.now() }),
|
||||
setPaymentAttachments: (value) =>
|
||||
set({ paymentAttachments: value, lastUpdated: Date.now() }),
|
||||
setLastUpdated: (value) => set({ lastUpdated: value }),
|
||||
clear: () => set({ ...initialState }),
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BaseResponse } from "@/app/[name]/(Main)/types/Types";
|
||||
import { PaymentMethodEnum } from "../enum/Enum";
|
||||
|
||||
export interface ShipmentMethod {
|
||||
id: string;
|
||||
@@ -107,10 +108,10 @@ export interface PaymentMethodResponse {
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
restaurant: Restaurant;
|
||||
title: string;
|
||||
method: "CardOnDelivery" | "Cash" | "Online";
|
||||
method: PaymentMethodEnum;
|
||||
gateway: string | null;
|
||||
description: string;
|
||||
cardNumber: string | null;
|
||||
enabled: boolean;
|
||||
order: number;
|
||||
merchantId: string | null;
|
||||
@@ -145,6 +146,10 @@ export type CheckoutStoreType = {
|
||||
plateNumber: string;
|
||||
} | null
|
||||
) => void;
|
||||
paymentDesc: string;
|
||||
setPaymentDesc: (value: string) => void;
|
||||
paymentAttachments: string[];
|
||||
setPaymentAttachments: (value: string[]) => void;
|
||||
lastUpdated: number | null;
|
||||
setLastUpdated: (value: number | null) => void;
|
||||
clear: () => void;
|
||||
@@ -353,9 +358,10 @@ export interface OrderDetailPaymentMethod {
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
restaurant: string;
|
||||
method: "CardOnDelivery" | "Cash" | "Online";
|
||||
method: PaymentMethodEnum;
|
||||
gateway: string | null;
|
||||
description: string;
|
||||
cardNumber: string | null;
|
||||
enabled: boolean;
|
||||
order: number;
|
||||
merchantId: string | null;
|
||||
|
||||
@@ -91,11 +91,14 @@ export type SaveAllMethod = {
|
||||
deliveryMethodId: string;
|
||||
addressId?: string;
|
||||
paymentMethodId: string;
|
||||
tableNumber?: string;
|
||||
carAddress?: {
|
||||
carModel: string;
|
||||
carColor: string;
|
||||
plateNumber: string;
|
||||
};
|
||||
attachments?: string[];
|
||||
paymentDesc?: string;
|
||||
};
|
||||
|
||||
export type CartStoreType = {
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"paymentMethod": {
|
||||
"Cash": "پرداخت نقدی",
|
||||
"Online": "پرداخت آنلاین",
|
||||
"Wallet": "پرداخت با کیف پول"
|
||||
"Wallet": "پرداخت با کیف پول",
|
||||
"CreditCard": "کارت به کارت"
|
||||
},
|
||||
"status": {
|
||||
"new": "سفارش جدید",
|
||||
|
||||
@@ -67,6 +67,15 @@
|
||||
},
|
||||
"SectionPayment": {
|
||||
"Title": "انتخاب شیوه پرداخت",
|
||||
"CreditCard": {
|
||||
"CardNumberLabel": "شماره کارت",
|
||||
"PaymentDescLabel": "توضیحات پرداخت",
|
||||
"PaymentDescPlaceholder": "مثال: واریز از حساب با شماره ۱۲۳۴",
|
||||
"ReceiptLabel": "تصویر رسید پرداخت",
|
||||
"UploadReceipt": "آپلود رسید",
|
||||
"Uploading": "در حال آپلود...",
|
||||
"UploadSuccess": "رسید با موفقیت آپلود شد"
|
||||
},
|
||||
"InputPaymentType": {
|
||||
"Label": "",
|
||||
"Options": {
|
||||
|
||||
Reference in New Issue
Block a user