card to card

This commit is contained in:
hamid zarghami
2026-06-15 11:02:05 +03:30
parent da896b8772
commit 2229c13784
8 changed files with 208 additions and 30 deletions
@@ -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 = {
+2 -1
View File
@@ -17,7 +17,8 @@
"paymentMethod": {
"Cash": "پرداخت نقدی",
"Online": "پرداخت آنلاین",
"Wallet": "پرداخت با کیف پول"
"Wallet": "پرداخت با کیف پول",
"CreditCard": "کارت به کارت"
},
"status": {
"new": "سفارش جدید",
+9
View File
@@ -67,6 +67,15 @@
},
"SectionPayment": {
"Title": "انتخاب شیوه پرداخت",
"CreditCard": {
"CardNumberLabel": "شماره کارت",
"PaymentDescLabel": "توضیحات پرداخت",
"PaymentDescPlaceholder": "مثال: واریز از حساب با شماره ۱۲۳۴",
"ReceiptLabel": "تصویر رسید پرداخت",
"UploadReceipt": "آپلود رسید",
"Uploading": "در حال آپلود...",
"UploadSuccess": "رسید با موفقیت آپلود شد"
},
"InputPaymentType": {
"Label": "",
"Options": {