erp-platform/ui/src/components/orders/PaymentForm.tsx
2025-11-10 22:50:51 +03:00

470 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState } from 'react'
import {
FaCreditCard,
FaLock,
FaArrowLeft,
FaUser,
FaBuilding,
FaMailBulk,
FaPhone,
FaMapMarkerAlt,
FaMap,
FaGlobe,
FaMoneyBillWave,
FaDollarSign,
FaUserPlus,
} from 'react-icons/fa'
import {
BillingCycle,
BasketItem,
InstallmentOptionDto,
PaymentMethodDto,
} from '@/proxy/order/models'
import { OrderService } from '@/services/order.service'
import { CustomTenantDto } from '@/proxy/config/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
interface CartState {
items: BasketItem[]
total: number
globalBillingCycle: BillingCycle
globalPeriod: number
}
interface PaymentFormProps {
tenant: CustomTenantDto
onBack: () => void
onComplete: (paymentData: Record<string, unknown>) => void
cartState: CartState
}
export const PaymentForm: React.FC<PaymentFormProps> = ({
tenant,
onBack,
onComplete,
cartState,
}) => {
const defaultPaymentMethod = 'Kredi Kartı'
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>(defaultPaymentMethod)
const [selectedInstallment, setSelectedInstallment] = useState<InstallmentOptionDto>()
const [paymentData, setPaymentData] = useState({
cardNumber: '',
expiryDate: '',
cvv: '',
cardName: '',
})
const { translate } = useLocalization()
const [paymentMethods, setPaymentMethods] = useState<PaymentMethodDto[]>([])
const [installmentOptions, setInstallmentOptions] = useState<InstallmentOptionDto[]>([])
const [loading, setLoading] = useState<boolean>(true)
useEffect(() => {
const fetchData = async () => {
const orderService = new OrderService()
try {
const paymentResponse = await orderService.getPaymentMethodList()
setPaymentMethods(paymentResponse.data)
if (paymentResponse.data.length > 0) {
setSelectedPaymentMethod(paymentResponse.data[1].name)
}
const installmentResponse = await orderService.getInstallmentOptionList()
setInstallmentOptions(installmentResponse.data)
} catch (err) {
console.error('Ödeme şekilleri ve Komisyon Bilgileri alınamadı', err)
} finally {
setLoading(false)
}
}
fetchData()
}, [])
const selectedMethod = paymentMethods.find((m) => m.name === selectedPaymentMethod)
let commission = 0
if (selectedPaymentMethod === defaultPaymentMethod && selectedInstallment) {
commission = cartState.total * selectedInstallment.commission
} else if (selectedMethod) {
commission = cartState.total * selectedMethod.commission
}
const finalTotal = cartState.total + commission
const formatPrice = (price: number) =>
new Intl.NumberFormat('tr-TR', {
style: 'currency',
currency: 'TRY',
minimumFractionDigits: 2,
}).format(price)
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onComplete({
...paymentData,
paymentMethodId: selectedMethod?.id,
installment:
selectedPaymentMethod === defaultPaymentMethod
? (selectedInstallment?.installment ?? 1)
: 1,
commission,
total: finalTotal,
})
}
const handleInputChange = (field: string, value: string) => {
setPaymentData((prev) => ({ ...prev, [field]: value }))
}
return (
<div className="mx-auto px-4 lg:px-8 mb-6">
{loading ? (
<div className="text-center py-12 text-gray-500">
{translate('::App.Loading')}
</div>
) : (
<form onSubmit={handleSubmit} className="flex flex-col lg:flex-row gap-6">
{/* 3 Sütun: Ödeme Yöntemi | Taksit Seçenekleri | Sipariş Özeti */}
<div className="w-full lg:w-1/3 flex flex-col gap-6">
<div className="bg-white rounded-xl shadow border p-6">
{tenant && (
<>
<h2 className="text-lg font-semibold mb-4 flex items-center">
<FaUser className="w-5 h-5 text-green-600 mr-2" />{' '}
{translate('::Public.payment.customerInfo')}
</h2>
<div className="pt-4 border-t border-gray-200">
<div className="grid grid-cols-1 gap-y-3 text-sm text-gray-700">
<div className="flex items-center gap-2">
<FaUser className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.code')}
</span>
<span>{tenant.name}</span>
</div>
<div className="flex items-center gap-2">
<FaUser className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::LoginPanel.Profil')}
</span>
<span>{tenant.founder}</span>
</div>
<div className="flex items-center gap-2">
<FaBuilding className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.company')}
</span>
<span>{tenant.organizationName}</span>
</div>
<div className="flex items-center gap-2">
<FaMailBulk className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Abp.Account.EmailAddress')}
</span>
<span>{tenant.email}</span>
</div>
<div className="flex items-center gap-2">
<FaPhone className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Abp.Identity.User.UserInformation.PhoneNumber')}
</span>
<span>{tenant.phoneNumber}</span>
</div>
<div className="flex items-start gap-2">
<FaMapMarkerAlt className="w-4 h-4 text-gray-500 mt-0.5" />
<div>
<span className="font-medium">
{translate('::App.Address')}:
</span>
<div>{tenant.address1}</div>
</div>
</div>
<div className="flex items-center gap-2">
<FaGlobe className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.country')}
</span>
<span>{tenant.country}</span>
</div>
<div className="flex items-center gap-2">
<FaGlobe className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.common.city')}
</span>
<span>{tenant.city}</span>
</div>
<div className="flex items-center gap-2">
<FaMap className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.district')}
</span>
<span>{tenant.district}</span>
</div>
<div className="flex items-center gap-2">
<FaMapMarkerAlt className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.postalCode')}:
</span>
<span>{tenant.postalCode}</span>
</div>
<div className="flex items-center gap-2">
<FaMoneyBillWave className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.contact.taxOffice')}:
</span>
<span>{tenant.taxOffice}</span>
</div>
<div className="flex items-center gap-2">
<FaDollarSign className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.contact.taxNumber')}:
</span>
<span>{tenant.vknTckn}</span>
</div>
{tenant.reference && (
<div className="flex items-center gap-2">
<FaUserPlus className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.reference')}:
</span>
<span>{tenant.reference}</span>
</div>
)}
</div>
</div>
</>
)}
</div>
</div>
<div className="w-full lg:w-1/3 flex flex-col gap-6">
{/* 1. Sütun: Ödeme Yöntemi */}
<div className="bg-white rounded-xl shadow border p-6">
<h2 className="text-lg font-semibold mb-4 flex items-center">
<FaLock className="w-5 h-5 text-green-600 mr-2" />{' '}
{translate('::Public.payment.method.title')}
</h2>
<div className="space-y-2">
{paymentMethods.map((method) => (
<label
key={method.id}
className={`flex items-center p-3 border-2 rounded-lg cursor-pointer transition-all ${
selectedPaymentMethod === method.name
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<input
type="radio"
name="paymentMethodId"
value={method.name}
checked={selectedPaymentMethod === method.name}
onChange={(e) => {
setSelectedPaymentMethod(e.target.value)
}}
className="sr-only"
/>
<span className="text-2xl mr-3">{method.logo}</span>
<div>
<div className="font-medium text-gray-900">{method.name}</div>
<div className="text-sm text-gray-600">
{method.name === defaultPaymentMethod
? translate('::Public.payment.method.installmentsAvailable')
: translate('::Public.payment.method.noCommission')}
</div>
</div>
</label>
))}
</div>
</div>
{/* Taksit Seçenekleri */}
{selectedPaymentMethod === defaultPaymentMethod && (
<div className="bg-white rounded-xl shadow border p-4">
<h3 className="text-md font-medium text-gray-800 mb-2">
{translate('::App.Orders.InstallmentOptions')}
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{installmentOptions.map((option) => (
<label
key={option.id}
className={`flex flex-col items-center justify-center h-full p-4 border-2 rounded-xl cursor-pointer transition-all text-xs md:text-sm text-center select-none ${
selectedInstallment?.installment === option.installment
? 'border-blue-500 bg-blue-50 shadow-md scale-105'
: 'border-gray-200 hover:border-blue-200'
}`}
>
<input
type="radio"
name="installment"
value={option.installment}
checked={selectedInstallment?.installment === option.installment}
onChange={() => setSelectedInstallment(option)}
className="sr-only"
/>
<div className="font-semibold text-base mb-1">{option.name}</div>
<div className="text-gray-500 mb-1">
{translate('::Public.payment.installments.commission')}{' '}
<span className="font-semibold">
%{(option.commission * 100).toFixed(1)}
</span>
</div>
<div className="text-blue-700 font-bold mb-1">
{option.installment > 1
? `${option.installment} ${translate('::Public.payment.installments.monthly')}`
: translate('::Public.payment.installments.single')}
</div>
<div className="font-extrabold text-lg text-gray-900 mt-1">
{formatPrice(
(cartState.total + cartState.total * option.commission) /
option.installment,
)}
</div>
</label>
))}
</div>
</div>
)}
</div>
{/* 3. Sütun: Kart Bilgileri + Sipariş Özeti ve Butonlar */}
<div className="w-full lg:w-1/3 flex flex-col gap-6">
{/* Kart Bilgileri */}
{selectedPaymentMethod !== defaultPaymentMethod && (
<div className="bg-white rounded-xl shadow border p-6 space-y-3">
<h3 className="text-md font-medium text-gray-800 mb-3">
{translate('::Public.payment.card.title')}
</h3>
<input
type="text"
required
value={paymentData.cardName}
onChange={(e) => handleInputChange('cardName', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
placeholder={translate('::Public.payment.card.name')}
/>
<input
type="text"
required
value={paymentData.cardNumber}
onChange={(e) => handleInputChange('cardNumber', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
placeholder={translate('::Public.payment.card.number')}
maxLength={19}
/>
<div className="grid grid-cols-2 gap-3">
<input
type="text"
required
value={paymentData.expiryDate}
onChange={(e) => handleInputChange('expiryDate', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
placeholder="MM/YY"
maxLength={5}
/>
<input
type="text"
required
value={paymentData.cvv}
onChange={(e) => handleInputChange('cvv', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
placeholder="CVV"
maxLength={4}
/>
</div>
</div>
)}
{/* Sipariş Özeti ve Butonlar */}
<div className="bg-white rounded-xl shadow border p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
{translate('::Public.payment.summary.title')}
</h3>
<div className="space-y-4 mb-4">
{cartState.items.map((item, index) => (
<div key={`${item.product.id}-${index}`} className="flex justify-between text-sm">
<div>
<div className="font-medium">{item.product.name}</div>
<div className="text-gray-500">
{item.quantity}x -{' '}
{item.billingCycle === 'monthly'
? 'Aylık'
: item.billingCycle === 'yearly'
? 'Yıllık'
: 'Aylık'}{' '}
{cartState.globalPeriod > 1 &&
`(${cartState.globalPeriod} ${item.billingCycle === 'monthly' ? 'Ay' : 'Yıl'})`}
</div>
</div>
<div className="font-medium">{formatPrice(item.totalPrice)}</div>
</div>
))}
</div>
<div className="space-y-1 text-sm border-t border-gray-200 pt-4 text-gray-600">
<div className="flex justify-between">
<span>{translate('::Public.payment.summary.subtotal')}</span>
<span>{formatPrice(cartState.total)}</span>
</div>
<div className="flex justify-between">
<span>{translate('::Public.payment.installments.commission')}</span>
<span>{formatPrice(commission)}</span>
</div>
{selectedPaymentMethod === defaultPaymentMethod &&
selectedInstallment?.installment &&
selectedInstallment.installment > 1 && (
<div className="flex justify-between text-blue-600">
<span>{translate('::Public.payment.summary.monthlyInstallment')}: </span>
<span>
{formatPrice(finalTotal / selectedInstallment.installment)} x{' '}
{selectedInstallment.installment}
</span>
</div>
)}
<div className="flex justify-between text-base font-bold pt-2 text-gray-900">
<span>{translate('::Public.payment.summary.total')}</span>
<span className="text-blue-600">{formatPrice(finalTotal)}</span>
</div>
</div>
{/* Butonlar */}
<div className="flex justify-between items-center mt-6">
<button
type="button"
onClick={onBack}
className="flex items-center px-6 py-3 border border-gray-300 text-gray-700 rounded-lg"
>
<FaArrowLeft className="w-4 h-4 mr-2" />
{translate('::Public.payment.buttons.back')}
</button>
<button
type="submit"
className="flex items-center justify-center px-6 py-3 bg-green-600 text-white rounded-lg"
>
<FaCreditCard className="w-5 h-5 mr-2" />
{selectedPaymentMethod === 'bank-transfer'
? translate('::Public.payment.buttons.completeOrder')
: translate('::Public.payment.buttons.pay')}
</button>
</div>
</div>
</div>
</form>
)}
</div>
)
}