canina/frontend/application/components/CheckoutPage.tsx
parsa aghaei 7615aa0e51 fix: resolve seed encoding, search param, and wiki data source issues
- 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
2026-07-11 15:40:49 +03:30

392 lines
21 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 } 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>
);
}