- Fix seed-products.ts TS error (implicit any) and BOM handling - Re-run full seed to restore correct Persian encoding in DB - Fix productService query→search param mismatch - Fix IngredientWiki hardcoded port 4000→use settingsStore - Restore seed-products-data.json from git after accidental corruption
392 lines
21 KiB
TypeScript
392 lines
21 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
|
||
} 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 } 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, 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');
|
||
|
||
useEffect(() => {
|
||
productService.getProducts().then(data => setDbProducts(data));
|
||
}, []);
|
||
|
||
const subtotal = getSubtotal();
|
||
const roundedAmount = Math.ceil(subtotal / 10000) * 10000;
|
||
const roundUpDiff = roundedAmount - subtotal;
|
||
|
||
const handleCharityToggle = () => {
|
||
if (!useRoundUp) {
|
||
setCharityDonation(Math.max(roundUpDiff, Math.round(subtotal * 0.01)));
|
||
setUseRoundUp(true);
|
||
} else {
|
||
setCharityDonation(0);
|
||
setUseRoundUp(false);
|
||
}
|
||
};
|
||
|
||
const handleFinalize = async () => {
|
||
setLoading(true);
|
||
const activePet = getActivePet();
|
||
|
||
try {
|
||
// 1. Register the order in the backend
|
||
const orderId = await addOrder({
|
||
items: [...items],
|
||
total: getTotal(),
|
||
charityDonation: charityDonation,
|
||
petId: activePet?.id
|
||
});
|
||
|
||
// 2. Update user charity total
|
||
if (charityDonation > 0) {
|
||
updateProfile({
|
||
charityDonationTotal: (profile.charityDonationTotal || 0) + charityDonation
|
||
});
|
||
}
|
||
|
||
// 3. Update pet consumptions for the refill logic
|
||
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('/dashboard');
|
||
} 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-12">
|
||
{/* Main Form */}
|
||
<div className="lg:col-span-8 space-y-8">
|
||
{/* Step 1: Shipping */}
|
||
<section className="bg-white rounded-[3rem] border border-medical-gray-200 p-10 overflow-hidden relative">
|
||
<div className="absolute top-0 right-0 w-3 h-full bg-canina-blue opacity-20" />
|
||
<div className="flex items-center gap-4 mb-10">
|
||
<div className="w-12 h-12 bg-medical-gray-900 text-white rounded-2xl flex items-center justify-center font-black">۱</div>
|
||
<h2 className="text-2xl font-black text-medical-gray-900 italic">اطلاعات ارسال</h2>
|
||
</div>
|
||
|
||
<div className="grid md:grid-cols-2 gap-6">
|
||
<div className="space-y-2">
|
||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">نام و نام خانوادگی</label>
|
||
<input type="text" className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 px-6 text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20" placeholder="مثلاً: پارسا آقایی" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">شماره تماس</label>
|
||
<input type="text" className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 px-6 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 className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">آدرس دقیق پستی</label>
|
||
<textarea className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 px-6 text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20 h-32" placeholder="استان، شهر، خیابان..." />
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Step 2: Payment */}
|
||
<section className="bg-white rounded-[3rem] border border-medical-gray-200 p-10 overflow-hidden relative">
|
||
<div className="absolute top-0 right-0 w-3 h-full bg-medical-gray-900 opacity-20" />
|
||
<div className="flex items-center gap-4 mb-10">
|
||
<div className="w-12 h-12 bg-medical-gray-900 text-white rounded-2xl flex items-center justify-center font-black">۲</div>
|
||
<h2 className="text-2xl font-black text-medical-gray-900 italic">شیوه پرداخت</h2>
|
||
</div>
|
||
|
||
<div className="grid md:grid-cols-3 gap-4">
|
||
{[
|
||
{ id: 'online', label: 'پرداخت آنلاین', desc: 'کارتهای بانکی', icon: <CreditCard className="w-6 h-6" /> },
|
||
{ id: 'wallet', label: 'اعتبار حساب', desc: 'شارژ شده اختصاصی', icon: <Wallet className="w-6 h-6" /> },
|
||
{ id: 'cod', label: 'پرداخت در محل', desc: 'فقط تهران', icon: <Truck className="w-6 h-6" /> }
|
||
].map((pay) => (
|
||
<div
|
||
key={pay.id}
|
||
onClick={async () => {
|
||
setPaymentMethod(pay.id);
|
||
try {
|
||
// In a real scenario, this would call api.post('/payment/init', { method: pay.id, amount: getTotal() })
|
||
// and redirect to the returned gatewayUrl.
|
||
} catch (err) {
|
||
console.error("Payment init failed");
|
||
}
|
||
}}
|
||
className={cn("p-6 rounded-3xl border-2 cursor-pointer transition-all group", paymentMethod === pay.id ? "border-canina-blue bg-canina-blue/5" : "border-medical-gray-100 hover:border-canina-blue")}
|
||
>
|
||
<div className="w-12 h-12 bg-medical-gray-50 rounded-2xl flex items-center justify-center text-medical-gray-400 group-hover:text-canina-blue group-hover:bg-canina-blue/10 mb-4 transition-all">
|
||
{pay.icon}
|
||
</div>
|
||
<h4 className="font-black text-sm text-medical-gray-900 mb-1">{pay.label}</h4>
|
||
<p className="text-[10px] text-medical-gray-400 font-bold">{pay.desc}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Step 3: Charity Section */}
|
||
<section className="bg-white rounded-[3rem] border border-medical-gray-200 p-10 overflow-hidden relative">
|
||
<div className="absolute top-0 right-0 w-3 h-full bg-pink-500 opacity-20" />
|
||
<div className="flex items-center justify-between mb-8">
|
||
<div className="flex items-center gap-4">
|
||
<div className="w-12 h-12 bg-pink-500 text-white rounded-2xl flex items-center justify-center">
|
||
<Heart className="w-6 h-6 fill-white" />
|
||
</div>
|
||
<div>
|
||
<h2 className="text-2xl font-black text-medical-gray-900 italic">ردپای مهربانی</h2>
|
||
<p className="text-xs font-bold text-pink-500 mt-1">سهم شما در حمایت از حیوانات بیپناه</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-pink-50 rounded-[2.5rem] p-8 border border-pink-100">
|
||
<div className="flex flex-wrap items-center justify-between gap-6">
|
||
<div className="flex-1 min-w-[280px]">
|
||
<h4 className="text-lg font-black text-medical-gray-900 italic mb-2">رند کردن مبلغ و کمک به پناهگاه</h4>
|
||
<p className="text-sm text-medical-gray-600 leading-relaxed font-medium"> با انتخاب این گزینه، مابقی مبلغ تا ده هزار تومان بعدی یا ۱٪ از خرید شما (هر کدام بیشتر باشد) صرف تامین غذا و دارو برای حیوانات بی-سرپرست تحت حمایت کانینا میشود.</p>
|
||
<div className="mt-4 flex items-center gap-3 text-pink-600">
|
||
<ShieldCheck className="w-4 h-4" />
|
||
<span className="text-[10px] font-black uppercase tracking-widest">گزارش شفاف هزینهکرد در داشبورد شما</span>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleCharityToggle}
|
||
className={cn(
|
||
"w-20 h-20 rounded-full flex items-center justify-center transition-all shadow-xl hover:scale-110 active:scale-95",
|
||
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-10 h-10", useRoundUp ? "rotate-45" : "rotate-0")} />
|
||
</button>
|
||
</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>
|
||
)}
|
||
</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/10 pb-4">خلاصه سفارش</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/90 leading-tight">{item.product.name}</p>
|
||
<p className="text-[10px] text-white/40 font-bold mt-1">تعداد: {toPersian(item.quantity)} بسته</p>
|
||
</div>
|
||
<span className="text-xs font-mono font-bold font-vazir">{toPersian((item.product.priceValue * item.quantity).toLocaleString())}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="space-y-4 pt-8 border-t border-white/10">
|
||
<div className="flex justify-between text-sm font-bold text-white/60">
|
||
<span>مجموع اقلام</span>
|
||
<span className="font-vazir">{toPersian(getSubtotal().toLocaleString())} تومان</span>
|
||
</div>
|
||
{isSubscribed && (
|
||
<div className="flex justify-between text-sm font-bold text-green-400">
|
||
<span>تخفیف اشتراک (۵٪-)</span>
|
||
<span className="font-vazir">{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">{toPersian(charityDonation.toLocaleString())} تومان+</span>
|
||
</div>
|
||
)}
|
||
<div className="flex justify-between text-sm font-bold text-white/60">
|
||
<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">مبلغ نهایی</span>
|
||
<div className="text-left">
|
||
<p className="text-3xl font-black text-canina-blue leading-none font-vazir">{toPersian(getTotal().toLocaleString())}</p>
|
||
<p className="text-[10px] text-white/30 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
|
||
name="coupon"
|
||
type="text"
|
||
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
|
||
const fullProduct = dbProducts.find(p => p.id === item.product.id) || item.product;
|
||
const dose = activePet ? fullProduct.calculateDosage(activePet.weight, activePet.age <= 1) : { quantity: 1 };
|
||
const days = Math.floor(fullProduct.packageSize / (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>
|
||
);
|
||
}
|