canina/frontend/application/components/CheckoutPage.tsx

526 lines
28 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

"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
} 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 { Product, PRODUCTS } from "../lib/data/products";
import { productService } from "../lib/services/productService";
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>('online');
const [selectedAddressId, setSelectedAddressId] = useState<string>('');
const [showNewAddressForm, setShowNewAddressForm] = useState(false);
// Custom manual address state if guest or not using stored addresses
const [manualName, setManualName] = useState('');
const [manualPhone, setManualPhone] = useState('');
const [manualAddress, setManualAddress] = useState('');
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 subtotal = getSubtotal();
const remainder = subtotal % 10000;
const roundUpDiff = remainder === 0 ? 10000 : 10000 - 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);
const shippingAddressStr = selectedAddr
? `${selectedAddr.province}، ${selectedAddr.city}، ${selectedAddr.detail} (گیرنده: ${selectedAddr.receptorName} - ${toPersian(selectedAddr.phone)})`
: manualAddress || undefined;
const orderId = await addOrder({
items: [...items],
total: totalAmount,
charityDonation: charityDonation,
petId: activePet?.id,
shippingAddress: shippingAddressStr
});
// 3. Update user charity total
if (charityDonation > 0) {
useUserStore.setState((state) => ({
profile: {
...state.profile,
charityDonationTotal: (state.profile.charityDonationTotal || 0) + charityDonation
}
}));
}
// 4. Update pet consumptions and charity contributions for active pet
if (activePet) {
const newConsumptions = [...(activePet.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(activePet.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 && !showNewAddressForm ? (
<div className="space-y-4">
<p className="text-xs font-bold text-medical-gray-500 mb-2">انتخاب از بین آدرسهای ذخیرهشده شما:</p>
<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>
<button
onClick={() => setShowNewAddressForm(true)}
className="mt-4 flex items-center gap-2 text-canina-blue font-black text-xs hover:underline"
>
<PlusCircle className="w-4 h-4" />
<span>استفاده از آدرس جدید</span>
</button>
</div>
) : (
<div className="space-y-4">
{profile.addresses && profile.addresses.length > 0 && (
<button
onClick={() => setShowNewAddressForm(false)}
className="mb-4 text-xs font-bold text-canina-blue hover:underline"
>
بازگشت به آدرسهای ذخیرهشده
</button>
)}
<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"
placeholder="۰۹۱۲XXXXXXX"
/>
</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-28 sm:h-32"
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-3 gap-3 sm:gap-4">
{[
{ id: 'online', 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" />
},
{ id: 'cod', label: 'پرداخت در محل', desc: 'فقط تهران', icon: <Truck 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", 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>
<p className={cn("text-[10px] font-bold truncate", pay.id === 'wallet' ? "text-canina-blue" : "text-medical-gray-400")}>{pay.desc}</p>
</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"> مابقی مبلغ تا ۱۰,۰۰۰ تومان بعدی صرف تامین غذا و دارو برای حیوانات بی-سرپرست میشود.</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-sm font-bold text-green-400">
<span>تخفیف اشتراک (۵٪-)</span>
<span className="font-vazir text-green-400">{toPersian(getDiscount().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>
</div>
);
}