692 lines
38 KiB
TypeScript
692 lines
38 KiB
TypeScript
"use client";
|
||
import React, { useState, useEffect } from "react";
|
||
import { motion } from "motion/react";
|
||
import {
|
||
ChevronRight,
|
||
ChevronLeft,
|
||
MapPin,
|
||
CreditCard,
|
||
Truck,
|
||
ShieldCheck,
|
||
Calendar,
|
||
Wallet,
|
||
Clock,
|
||
ArrowRight,
|
||
CheckCircle2,
|
||
Heart,
|
||
PlusCircle,
|
||
Copy
|
||
} from "lucide-react";
|
||
import { toPersian, cn } from "../lib/utils";
|
||
import { useCartStore } from "../lib/store/cartStore";
|
||
import { usePetStore } from "../lib/store/usePetStore";
|
||
import { useUserStore } from "../lib/store/userStore";
|
||
import TopUpModal from "./TopUpModal";
|
||
import AddressModal from "./AddressModal";
|
||
import { IRAN_PROVINCES, PROVINCE_CITIES } from "../lib/data/provinces";
|
||
import { toast } from "sonner";
|
||
import { useRouter } from 'next/navigation';
|
||
|
||
export default function CheckoutPage() {
|
||
const router = useRouter();
|
||
const { items, getTotal, getSubtotal, getDiscount, isSubscribed, charityDonation, setCharityDonation, addOrder, clearCart } = useCartStore();
|
||
const { getActivePet, updatePet } = usePetStore();
|
||
const { profile, isLoggedIn, updateProfile } = useUserStore();
|
||
const [step, setStep] = useState(1);
|
||
const [loading, setLoading] = useState(false);
|
||
const [useRoundUp, setUseRoundUp] = useState(false);
|
||
const [dbProducts, setDbProducts] = useState<Product[]>([]);
|
||
const [paymentMethod, setPaymentMethod] = useState<string>('card');
|
||
const [showTopUpModal, setShowTopUpModal] = useState(false);
|
||
const [showAddressModal, setShowAddressModal] = useState(false);
|
||
|
||
const [selectedAddressId, setSelectedAddressId] = useState<string>('');
|
||
|
||
// Custom manual address state if guest or not using stored addresses
|
||
const [manualName, setManualName] = useState('');
|
||
const [manualPhone, setManualPhone] = useState('');
|
||
const [manualProvince, setManualProvince] = useState('');
|
||
const [manualCity, setManualCity] = useState('');
|
||
const [manualAddress, setManualAddress] = useState('');
|
||
|
||
const handleCopy = (text: string, title: string) => {
|
||
navigator.clipboard.writeText(text);
|
||
toast.success(`${title} با موفقیت کپی شد!`);
|
||
};
|
||
|
||
useEffect(() => {
|
||
productService.getProducts({ limit: 999 }).then(res => setDbProducts(res.data));
|
||
|
||
// Set default selected address if logged in
|
||
if (isLoggedIn && profile.addresses && profile.addresses.length > 0) {
|
||
const def = profile.addresses.find(a => a.isDefault) || profile.addresses[0];
|
||
setSelectedAddressId(def.id);
|
||
} else if (isLoggedIn && profile.firstName) {
|
||
// Pre-fill profile info for manual fields
|
||
setManualName(`${profile.firstName} ${profile.lastName}`.trim());
|
||
setManualPhone(profile.mobile || '');
|
||
} else {
|
||
setSelectedAddressId('');
|
||
}
|
||
}, [profile, isLoggedIn]);
|
||
|
||
const roundStep = Number(useSettingsStore((state) => state.getText('CHARITY_ROUND_STEP', '10000'))) || 10000;
|
||
const subtotal = getSubtotal();
|
||
const remainder = subtotal % roundStep;
|
||
const roundUpDiff = remainder === 0 ? roundStep : roundStep - remainder;
|
||
|
||
const handleCharityToggle = () => {
|
||
if (!useRoundUp) {
|
||
setCharityDonation(roundUpDiff);
|
||
setUseRoundUp(true);
|
||
} else {
|
||
setCharityDonation(0);
|
||
setUseRoundUp(false);
|
||
}
|
||
};
|
||
|
||
const handleFinalize = async () => {
|
||
const totalAmount = getTotal();
|
||
setLoading(true);
|
||
const activePet = getActivePet();
|
||
|
||
try {
|
||
// 1. Deduct wallet balance if payment method is wallet
|
||
if (paymentMethod === 'wallet') {
|
||
const walletDeducted = Math.min(profile.walletBalance || 0, totalAmount);
|
||
if (walletDeducted > 0) {
|
||
const newBalance = (profile.walletBalance || 0) - walletDeducted;
|
||
const newTransaction = {
|
||
id: `TR-${Math.floor(Math.random() * 90000) + 10000}`,
|
||
type: 'purchase' as const,
|
||
amount: walletDeducted,
|
||
date: new Date().toISOString(),
|
||
status: 'success' as const
|
||
};
|
||
useUserStore.setState((state) => ({
|
||
profile: {
|
||
...state.profile,
|
||
walletBalance: newBalance,
|
||
transactions: [newTransaction, ...(state.profile.transactions || [])]
|
||
}
|
||
}));
|
||
}
|
||
}
|
||
|
||
// 2. Register the order in the backend
|
||
const selectedAddr = profile.addresses?.find(a => a.id === selectedAddressId);
|
||
let shippingAddressStr = '';
|
||
if (selectedAddr) {
|
||
shippingAddressStr = `${selectedAddr.province}، ${selectedAddr.city}، ${selectedAddr.detail} (گیرنده: ${selectedAddr.receptorName} - ${toPersian(selectedAddr.phone)})`;
|
||
} else if (manualAddress) {
|
||
const provCity = [manualProvince, manualCity].filter(Boolean).join('، ');
|
||
shippingAddressStr = `${provCity ? provCity + '، ' : ''}${manualAddress}${manualName ? ` (گیرنده: ${manualName}${manualPhone ? ` - ${toPersian(manualPhone)}` : ''})` : ''}`;
|
||
}
|
||
|
||
const orderId = await addOrder({
|
||
items: [...items],
|
||
total: totalAmount,
|
||
charityDonation: charityDonation,
|
||
paymentMethod: paymentMethod,
|
||
isRefill: isSubscribed,
|
||
petId: activePet?.id,
|
||
shippingAddress: shippingAddressStr
|
||
});
|
||
|
||
// Re-fetch user profile from backend to sync PostgreSQL wallet balance and charity donation total
|
||
const { fetchProfile } = useUserStore.getState();
|
||
await fetchProfile();
|
||
|
||
// 4. Update pet consumptions and charity contributions for active pet
|
||
const targetPet = activePet || usePetStore.getState().pets[0];
|
||
if (targetPet) {
|
||
const newConsumptions = [...(targetPet.consumptions || [])];
|
||
|
||
items.forEach(item => {
|
||
const existing = newConsumptions.find(c => c.productId === item.product.id);
|
||
if (existing) {
|
||
existing.remaining += item.product.packageSize * item.quantity;
|
||
existing.packageSize = item.product.packageSize;
|
||
} else {
|
||
newConsumptions.push({
|
||
productId: item.product.id,
|
||
packageSize: item.product.packageSize,
|
||
remaining: item.product.packageSize * item.quantity
|
||
});
|
||
}
|
||
});
|
||
|
||
updatePet(targetPet.id, {
|
||
consumptions: newConsumptions
|
||
});
|
||
}
|
||
|
||
clearCart();
|
||
toast.success("سفارش شما با موفقیت ثبت شد!");
|
||
router.push(`/checkout/success/${orderId}`);
|
||
} catch (err: any) {
|
||
toast.error(err.message || "خطا در ثبت سفارش. لطفاً مجدداً تلاش کنید.");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
if (items.length === 0) {
|
||
return (
|
||
<div className="min-h-screen bg-medical-gray-50 flex items-center justify-center p-6" dir="rtl">
|
||
<div className="text-center space-y-6">
|
||
<div className="w-24 h-24 bg-white rounded-full flex items-center justify-center mx-auto text-medical-gray-200">
|
||
<ShieldCheck className="w-12 h-12" />
|
||
</div>
|
||
<h2 className="text-2xl font-black text-medical-gray-900 italic">سبد خرید شما خالی است</h2>
|
||
<button onClick={() => router.push('/shop')} className="text-canina-blue font-black underline underline-offset-8">بازگشت به فروشگاه</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-medical-gray-50 py-12 px-6 font-vazir" dir="rtl">
|
||
<div className="max-w-7xl mx-auto">
|
||
|
||
{/* Mobile Navigation & Breadcrumbs */}
|
||
<div className="lg:hidden mb-10 space-y-4">
|
||
<button
|
||
onClick={() => router.back()}
|
||
className="flex items-center gap-2 text-medical-gray-500 font-bold hover:text-canina-blue transition-colors"
|
||
>
|
||
<ChevronRight className="w-5 h-5" />
|
||
<span>بازگشت به سبد خرید</span>
|
||
</button>
|
||
|
||
<div className="flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest overflow-x-auto whitespace-nowrap pb-2">
|
||
<span className="cursor-pointer hover:text-canina-blue" onClick={() => router.push('/shop')}>سبد خرید</span>
|
||
<ChevronLeft className="w-2.5 h-2.5" />
|
||
<span className="text-canina-blue">نهاییسازی سفارش</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Desktop Breadcrumbs (Hidden on Mobile) */}
|
||
<div className="hidden lg:flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-10">
|
||
<span className="cursor-pointer hover:text-canina-blue" onClick={() => router.push('/')}>خانه</span>
|
||
<ChevronLeft className="w-2.5 h-2.5" />
|
||
<span className="text-canina-blue">درگاه پرداخت امن و نهاییسازی</span>
|
||
</div>
|
||
|
||
<div className="grid lg:grid-cols-12 gap-8 lg:gap-12">
|
||
{/* Main Form */}
|
||
<div className="lg:col-span-8 space-y-6 sm:space-y-8">
|
||
{/* Step 1: Shipping */}
|
||
<section className="bg-white rounded-[2rem] sm:rounded-[3rem] border border-medical-gray-200 p-5 sm:p-10 overflow-hidden relative">
|
||
<div className="absolute top-0 right-0 w-2.5 sm:w-3 h-full bg-canina-blue opacity-20" />
|
||
<div className="flex items-center gap-3 sm:gap-4 mb-6 sm:mb-10">
|
||
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-medical-gray-900 text-white rounded-2xl flex items-center justify-center font-black text-sm sm:text-base">۱</div>
|
||
<h2 className="text-xl sm:text-2xl font-black text-medical-gray-900 italic">اطلاعات ارسال</h2>
|
||
</div>
|
||
|
||
{isLoggedIn && profile.addresses && profile.addresses.length > 0 ? (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<p className="text-xs font-bold text-medical-gray-500">انتخاب آدرس جهت تحویل سفارش:</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowAddressModal(true)}
|
||
className="flex items-center gap-1.5 text-canina-blue font-black text-xs hover:underline bg-canina-blue/5 border border-canina-blue/20 px-3 py-1.5 rounded-xl transition-all"
|
||
>
|
||
<PlusCircle className="w-4 h-4" />
|
||
<span>+ افزودن آدرس جدید</span>
|
||
</button>
|
||
</div>
|
||
<div className="grid gap-3">
|
||
{profile.addresses.map((addr) => (
|
||
<div
|
||
key={addr.id}
|
||
onClick={() => setSelectedAddressId(addr.id)}
|
||
className={cn(
|
||
"p-4 rounded-2xl border-2 cursor-pointer transition-all flex items-start gap-3",
|
||
selectedAddressId === addr.id ? "border-canina-blue bg-canina-blue/5" : "border-medical-gray-100 hover:border-medical-gray-200"
|
||
)}
|
||
>
|
||
<input
|
||
type="radio"
|
||
name="selectedAddress"
|
||
checked={selectedAddressId === addr.id}
|
||
onChange={() => setSelectedAddressId(addr.id)}
|
||
className="mt-1"
|
||
/>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<span className="font-black text-sm text-medical-gray-900">{addr.title}</span>
|
||
{addr.isDefault && (
|
||
<span className="bg-canina-blue/10 text-canina-blue text-[9px] font-black px-2 py-0.5 rounded-full">پیشفرض</span>
|
||
)}
|
||
</div>
|
||
<p className="text-xs text-medical-gray-600 leading-relaxed truncate">{addr.province}، {addr.city}، {addr.detail}</p>
|
||
<p className="text-[10px] text-medical-gray-400 font-bold mt-1">گیرنده: {addr.receptorName} | همراه: {toPersian(addr.phone)}</p>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
{isLoggedIn && (
|
||
<div className="flex justify-end mb-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowAddressModal(true)}
|
||
className="flex items-center gap-1.5 text-canina-blue font-black text-xs hover:underline bg-canina-blue/5 border border-canina-blue/20 px-3 py-1.5 rounded-xl transition-all"
|
||
>
|
||
<PlusCircle className="w-4 h-4" />
|
||
<span>افزودن و ذخیره آدرس جدید</span>
|
||
</button>
|
||
</div>
|
||
)}
|
||
<div className="grid md:grid-cols-2 gap-4 sm:gap-6">
|
||
<div className="space-y-2">
|
||
<label htmlFor="chk-fullname" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">نام و نام خانوادگی گیرنده *</label>
|
||
<input
|
||
id="chk-fullname"
|
||
name="name"
|
||
type="text"
|
||
autoComplete="name"
|
||
value={manualName}
|
||
onChange={(e) => setManualName(e.target.value)}
|
||
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-3 sm:py-4 px-4 sm:px-6 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
|
||
placeholder="مثلاً: پارسا آقایی"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label htmlFor="chk-tel" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">شماره تماس گیرنده *</label>
|
||
<input
|
||
id="chk-tel"
|
||
name="tel"
|
||
type="tel"
|
||
autoComplete="tel"
|
||
value={manualPhone}
|
||
onChange={(e) => setManualPhone(e.target.value.replace(/[^0-9]/g, ''))}
|
||
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-3 sm:py-4 px-4 sm:px-6 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20 dir-ltr text-right"
|
||
placeholder="۰۹۱۲XXXXXXX"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label htmlFor="chk-province" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">استان *</label>
|
||
<select
|
||
id="chk-province"
|
||
value={manualProvince}
|
||
onChange={(e) => {
|
||
const prov = e.target.value;
|
||
const cities = PROVINCE_CITIES[prov] || [];
|
||
setManualProvince(prov);
|
||
setManualCity(cities[0] || '');
|
||
}}
|
||
className="w-full bg-white border border-medical-gray-200 rounded-2xl py-3 sm:py-4 px-4 sm:px-6 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20 font-bold"
|
||
>
|
||
<option value="">انتخاب استان...</option>
|
||
{IRAN_PROVINCES.map((prov) => (
|
||
<option key={prov} value={prov}>{prov}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label htmlFor="chk-city" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">شهر *</label>
|
||
{manualProvince && PROVINCE_CITIES[manualProvince] ? (
|
||
<select
|
||
id="chk-city"
|
||
value={manualCity}
|
||
onChange={(e) => setManualCity(e.target.value)}
|
||
className="w-full bg-white border border-medical-gray-200 rounded-2xl py-3 sm:py-4 px-4 sm:px-6 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20 font-bold"
|
||
>
|
||
<option value="">انتخاب شهر...</option>
|
||
{PROVINCE_CITIES[manualProvince].map((c) => (
|
||
<option key={c} value={c}>{c}</option>
|
||
))}
|
||
</select>
|
||
) : (
|
||
<input
|
||
id="chk-city"
|
||
type="text"
|
||
value={manualCity}
|
||
onChange={(e) => setManualCity(e.target.value)}
|
||
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-3 sm:py-4 px-4 sm:px-6 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
|
||
placeholder="نام شهر..."
|
||
/>
|
||
)}
|
||
</div>
|
||
<div className="md:col-span-2 space-y-2">
|
||
<label htmlFor="chk-street" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">آدرس دقیق پستی *</label>
|
||
<textarea
|
||
id="chk-street"
|
||
name="street-address"
|
||
autoComplete="street-address"
|
||
value={manualAddress}
|
||
onChange={(e) => setManualAddress(e.target.value)}
|
||
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-3 sm:py-4 px-4 sm:px-6 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20 h-24 sm:h-28"
|
||
placeholder="خیابان، کوچه، پلاک، واحد..."
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
{/* Step 2: Payment */}
|
||
<section className="bg-white rounded-[2rem] sm:rounded-[3rem] border border-medical-gray-200 p-5 sm:p-10 overflow-hidden relative">
|
||
<div className="absolute top-0 right-0 w-2.5 sm:w-3 h-full bg-medical-gray-900 opacity-20" />
|
||
<div className="flex items-center gap-3 sm:gap-4 mb-6 sm:mb-10">
|
||
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-medical-gray-900 text-white rounded-2xl flex items-center justify-center font-black text-sm sm:text-base">۲</div>
|
||
<h2 className="text-xl sm:text-2xl font-black text-medical-gray-900 italic">شیوه پرداخت</h2>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
|
||
{[
|
||
{
|
||
id: 'card',
|
||
label: 'کارت به کارت (واریز مستقیم)',
|
||
desc: 'واریز به شماره کارت بانک سامان',
|
||
icon: <CreditCard className="w-5 h-5 sm:w-6 sm:h-6" />
|
||
},
|
||
{
|
||
id: 'wallet',
|
||
label: 'اعتبار حساب (کیف پول)',
|
||
desc: `موجودی: ${toPersian((profile.walletBalance || 0).toLocaleString())} تومان`,
|
||
icon: <Wallet className="w-5 h-5 sm:w-6 sm:h-6" />
|
||
}
|
||
].map((pay) => (
|
||
<div
|
||
key={pay.id}
|
||
onClick={() => setPaymentMethod(pay.id)}
|
||
className={cn("p-4 sm:p-6 rounded-2xl sm:rounded-3xl border-2 cursor-pointer transition-all group flex sm:flex-col items-center sm:items-start gap-4 sm:gap-0 relative", paymentMethod === pay.id ? "border-canina-blue bg-canina-blue/5" : "border-medical-gray-100 hover:border-canina-blue")}
|
||
>
|
||
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-medical-gray-50 rounded-xl sm:rounded-2xl flex items-center justify-center text-medical-gray-400 group-hover:text-canina-blue group-hover:bg-canina-blue/10 sm:mb-4 transition-all flex-shrink-0">
|
||
{pay.icon}
|
||
</div>
|
||
<div className="min-w-0 flex-1">
|
||
<h4 className="font-black text-xs sm:text-sm text-medical-gray-900 mb-0.5 sm:mb-1 truncate">{pay.label}</h4>
|
||
<div className="flex items-center justify-between gap-2">
|
||
<p className={cn("text-[10px] font-bold truncate", pay.id === 'wallet' ? "text-canina-blue" : "text-medical-gray-400")}>{pay.desc}</p>
|
||
{pay.id === 'wallet' && (profile.walletBalance || 0) === 0 && (
|
||
<button
|
||
type="button"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowTopUpModal(true);
|
||
}}
|
||
className="text-[10px] bg-canina-blue text-white px-2 py-0.5 rounded-lg font-bold hover:bg-blue-700 transition-colors flex items-center gap-1"
|
||
>
|
||
<PlusCircle className="w-3 h-3" />
|
||
<span>افزایش موجودی</span>
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{paymentMethod === 'card' && (
|
||
<div className="mt-6 bg-blue-50/80 border border-blue-200 rounded-2xl p-5 space-y-3 font-vazir">
|
||
<div className="flex items-center gap-2 text-canina-blue font-bold text-sm">
|
||
<CreditCard className="w-5 h-5" />
|
||
<span>اطلاعات کارت جهت واریز وجه سفارش:</span>
|
||
</div>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs bg-white p-4 rounded-xl border border-blue-100 shadow-sm">
|
||
|
||
{/* Card Number Box */}
|
||
<div className="bg-gray-50/80 p-3 rounded-xl border border-gray-200/60 flex items-center justify-between group hover:border-canina-blue transition-all">
|
||
<div className="space-y-1">
|
||
<span className="text-[11px] text-gray-500 font-bold block">شماره کارت (بانک سامان):</span>
|
||
<span
|
||
onClick={() => handleCopy("6219861974012071", "شماره کارت")}
|
||
className="font-mono text-sm sm:text-base font-extrabold text-gray-900 tracking-wider dir-ltr block cursor-pointer hover:text-canina-blue transition-colors"
|
||
>
|
||
6219 - 8619 - 7401 - 2071
|
||
</span>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleCopy("6219861974012071", "شماره کارت")}
|
||
className="p-2 rounded-lg bg-white border border-gray-200 text-gray-500 hover:text-canina-blue hover:border-canina-blue transition-all"
|
||
title="کپی شماره کارت"
|
||
>
|
||
<Copy className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Sheba Box */}
|
||
<div className="bg-gray-50/80 p-3 rounded-xl border border-gray-200/60 flex items-center justify-between group hover:border-canina-blue transition-all">
|
||
<div className="space-y-1">
|
||
<span className="text-[11px] text-gray-500 font-bold block">شماره شبا:</span>
|
||
<span
|
||
onClick={() => handleCopy("IR650560611828005725101501", "شماره شبا")}
|
||
className="font-mono text-xs sm:text-sm font-extrabold text-gray-900 tracking-wider dir-ltr block cursor-pointer hover:text-canina-blue transition-colors"
|
||
>
|
||
IR-65 0560 6118 2800 5725 1015 01
|
||
</span>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleCopy("IR650560611828005725101501", "شماره شبا")}
|
||
className="p-2 rounded-lg bg-white border border-gray-200 text-gray-500 hover:text-canina-blue hover:border-canina-blue transition-all"
|
||
title="کپی شماره شبا"
|
||
>
|
||
<Copy className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="md:col-span-2 pt-2 border-t border-gray-100 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 text-gray-700 font-bold">
|
||
<span className="text-xs">به نام: <strong className="text-gray-900">پارسا آقایی</strong></span>
|
||
<span className="text-canina-blue text-[11px]">پس از ثبت سفارش، کارشناسان فروش جهت تایید فیش با شما تماس میگیرند.</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
{/* Step 3: Charity Section */}
|
||
<section className="bg-white rounded-[2rem] sm:rounded-[3rem] border border-medical-gray-200 p-5 sm:p-10 overflow-hidden relative">
|
||
<div className="absolute top-0 right-0 w-2.5 sm:w-3 h-full bg-pink-500 opacity-20" />
|
||
<div className="flex items-center justify-between mb-6 sm:mb-8">
|
||
<div className="flex items-center gap-3 sm:gap-4">
|
||
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-pink-500 text-white rounded-2xl flex items-center justify-center flex-shrink-0">
|
||
<Heart className="w-5 h-5 sm:w-6 sm:h-6 fill-white" />
|
||
</div>
|
||
<div>
|
||
<h2 className="text-xl sm:text-2xl font-black text-medical-gray-900 italic">ردپای مهربانی</h2>
|
||
<p className="text-[10px] sm:text-xs font-bold text-pink-500 mt-0.5">سهم شما در حمایت از حیوانات بیپناه</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-pink-50 rounded-[2rem] sm:rounded-[2.5rem] p-5 sm:p-8 border border-pink-100">
|
||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 sm:gap-6">
|
||
<div className="flex-1">
|
||
<h4 className="text-base sm:text-lg font-black text-medical-gray-900 italic mb-1 sm:mb-2">رند کردن مبلغ و کمک به پناهگاه</h4>
|
||
<p className="text-xs sm:text-sm text-medical-gray-600 leading-relaxed font-medium"> مابقی مبلغ تا {toPersian(roundStep.toLocaleString())} تومان بعدی صرف تامین غذا و دارو برای حیوانات بیسرپرست میشود.</p>
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleCharityToggle}
|
||
className={cn(
|
||
"w-14 h-14 sm:w-20 sm:h-20 rounded-full flex items-center justify-center transition-all shadow-lg self-end sm:self-center",
|
||
useRoundUp ? "bg-pink-500 text-white shadow-pink-200" : "bg-white text-medical-gray-300 border-2 border-medical-gray-100"
|
||
)}
|
||
>
|
||
<PlusCircle className={cn("w-7 h-7 sm:w-10 sm:h-10", useRoundUp ? "rotate-45" : "rotate-0")} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{useRoundUp && (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
className="mt-6 pt-6 border-t border-pink-100 flex items-center justify-between"
|
||
>
|
||
<span className="text-sm font-black text-pink-600 italic">مبلغ اهدایی شما:</span>
|
||
<span className="text-lg font-black font-vazir text-pink-500">{toPersian(charityDonation.toLocaleString())} تومان</span>
|
||
</motion.div>
|
||
)}
|
||
|
||
<p className="mt-6 text-[10px] text-center text-medical-gray-400 font-bold">«با خرید این محصول، شما هم در درمان یک حیوان پناهگاهی سهیم شدید.»</p>
|
||
</section>
|
||
</div>
|
||
|
||
{/* Sidebar Summary */}
|
||
<div className="lg:col-span-4 space-y-8">
|
||
<div className="bg-medical-gray-900 rounded-[3rem] p-10 text-white shadow-2xl sticky top-32">
|
||
<h3 className="text-2xl font-black italic mb-8 border-b border-white/20 pb-4 text-white">خلاصه سفارش</h3>
|
||
|
||
<div className="space-y-6 mb-10 overflow-y-auto max-h-60 pr-2">
|
||
{items.map((item) => (
|
||
<div key={item.product.id} className="flex justify-between items-start gap-4">
|
||
<div className="flex-1">
|
||
<p className="text-sm font-black text-white leading-tight">{item.product.name}</p>
|
||
<p className="text-[10px] text-white/60 font-bold mt-1">تعداد: {toPersian(item.quantity)} بسته</p>
|
||
</div>
|
||
<span className="text-xs font-mono font-bold font-vazir text-white">{toPersian((item.product.priceValue * item.quantity).toLocaleString())} تومان</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="space-y-4 pt-8 border-t border-white/20">
|
||
<div className="flex justify-between text-sm font-bold text-white/80">
|
||
<span>مجموع اقلام</span>
|
||
<span className="font-vazir text-white">{toPersian(getSubtotal().toLocaleString())} تومان</span>
|
||
</div>
|
||
{isSubscribed && (
|
||
<div className="flex justify-between text-xs font-bold text-green-400 bg-green-500/10 p-2.5 rounded-xl border border-green-500/20">
|
||
<span>رزرو هوشمند تمدید (پاداش ۵٪ خرید بعدی):</span>
|
||
<span className="font-vazir font-black text-green-400">رزرو شد ({toPersian(Math.round(getSubtotal() * 0.05).toLocaleString())} تومان)</span>
|
||
</div>
|
||
)}
|
||
{charityDonation > 0 && (
|
||
<div className="flex justify-between text-sm font-bold text-pink-400">
|
||
<span>ردپای مهربانی</span>
|
||
<span className="font-vazir text-pink-400">{toPersian(charityDonation.toLocaleString())} تومان+</span>
|
||
</div>
|
||
)}
|
||
<div className="flex justify-between text-sm font-bold text-white/80">
|
||
<span>هزینه ارسال</span>
|
||
<span className="text-green-400 uppercase tracking-widest text-[10px] font-black">رایگان</span>
|
||
</div>
|
||
<div className="flex justify-between items-end pt-4">
|
||
<span className="text-xl font-black italic text-white">مبلغ نهایی</span>
|
||
<div className="text-left">
|
||
<p className="text-3xl font-black text-canina-gold leading-none font-vazir">{toPersian(getTotal().toLocaleString())}</p>
|
||
<p className="text-[10px] text-white/50 font-bold mt-1">تومان</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Discount Code */}
|
||
<div className="mt-8 pt-8 border-t border-white/10">
|
||
<form
|
||
className="flex gap-2"
|
||
onSubmit={async (e) => {
|
||
e.preventDefault();
|
||
const code = (e.currentTarget.elements.namedItem('coupon') as HTMLInputElement).value;
|
||
const success = await useCartStore.getState().applyCoupon(code);
|
||
if (success) {
|
||
toast.success("تبریک! کد تخفیف با موفقیت اعمال شد.");
|
||
} else {
|
||
toast.error("کد تخفیف معتبر نیست.");
|
||
}
|
||
}}
|
||
>
|
||
<input
|
||
id="chk-coupon"
|
||
name="coupon"
|
||
type="text"
|
||
autoComplete="off"
|
||
placeholder="کد تخفیف (مثلاً: CANINO2024)"
|
||
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs focus:ring-1 focus:ring-canina-blue outline-none"
|
||
/>
|
||
<button type="submit" className="bg-white/10 px-4 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-white/20 transition-all">اعمال</button>
|
||
</form>
|
||
</div>
|
||
|
||
{/* Refill Estimates */}
|
||
<div className="mt-10 p-6 bg-white/5 rounded-3xl border border-white/10 space-y-4">
|
||
<div className="flex items-center gap-3">
|
||
<Clock className="w-5 h-5 text-canina-blue" />
|
||
<h4 className="text-xs font-black uppercase tracking-widest font-vazir">تخمین اتمام مصرف (هوش مصنوعی)</h4>
|
||
</div>
|
||
<div className="space-y-2">
|
||
{items.map(item => {
|
||
const activePet = usePetStore.getState().getActivePet();
|
||
// Re-hydrate product to ensure methods like calculateDosage exist from static config
|
||
const staticProduct = PRODUCTS.find(p => p.id === item.product.id || p.artNo === item.product.artNo);
|
||
const calculateFn = staticProduct?.calculateDosage || item.product.calculateDosage;
|
||
|
||
const dose = activePet && typeof calculateFn === 'function'
|
||
? calculateFn(activePet.weight, activePet.age <= 1)
|
||
: { quantity: 1 };
|
||
|
||
const days = Math.floor((staticProduct?.packageSize || item.product.packageSize || 50) / (dose.quantity || 1));
|
||
|
||
return (
|
||
<div key={item.product.id} className="flex items-center justify-between text-[10px] font-medium font-vazir">
|
||
<span className="text-white/40">{item.product.name}</span>
|
||
<span className="text-canina-blue">~ {toPersian(days)} روز دیگر</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleFinalize}
|
||
disabled={loading}
|
||
className="w-full bg-canina-blue text-white py-6 rounded-2xl font-black text-xl mt-10 hover:scale-[1.02] transition-all shadow-xl shadow-canina-blue/20 flex items-center justify-center gap-3 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{loading ? (
|
||
<>
|
||
<div className="w-6 h-6 border-4 border-white/20 border-t-white rounded-full animate-spin" />
|
||
در حال ثبت...
|
||
</>
|
||
) : (
|
||
<>
|
||
پرداخت و تکمیل {step === 1 ? 'سفارش' : 'مرحله'}
|
||
<ArrowRight className="w-6 h-6 rotate-180" />
|
||
</>
|
||
)}
|
||
</button>
|
||
|
||
<div className="mt-6 flex items-center justify-center gap-2 text-[10px] font-black text-white/20 tracking-widest uppercase">
|
||
<ShieldCheck className="w-4 h-4" />
|
||
تضمین امنیت تراکنش بانکی
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<TopUpModal
|
||
isOpen={showTopUpModal}
|
||
onClose={() => setShowTopUpModal(false)}
|
||
onConfirm={(amt) => {
|
||
useUserStore.setState(state => ({
|
||
profile: {
|
||
...state.profile,
|
||
walletBalance: (state.profile.walletBalance || 0) + amt
|
||
}
|
||
}));
|
||
}}
|
||
/>
|
||
|
||
<AddressModal
|
||
isOpen={showAddressModal}
|
||
onClose={() => setShowAddressModal(false)}
|
||
onSave={(newAddr) => {
|
||
const currentAddresses = profile.addresses || [];
|
||
const updatedAddresses = [newAddr, ...currentAddresses];
|
||
updateProfile({ addresses: updatedAddresses });
|
||
setSelectedAddressId(newAddr.id);
|
||
setShowAddressModal(false);
|
||
toast.success('آدرس جدید اضافه شد و به عنوان آدرس تحویل انتخاب گردید.');
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|