canina/frontend/application/components/CheckoutPage.tsx

769 lines
43 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,
CreditCard,
Truck,
ShieldCheck,
Wallet,
Clock,
ArrowRight,
Heart,
PlusCircle,
Copy
} from "lucide-react";
import { toPersian, cn } from "../lib/utils";
import { PRODUCTS } from "../lib/data/products";
import { useCartStore } from "../lib/store/cartStore";
import { usePetStore } from "../lib/store/usePetStore";
import { useUserStore } from "../lib/store/userStore";
import { useSettingsStore } from "../lib/store/settingsStore";
import TopUpModal from "./TopUpModal";
import AddressModal from "./AddressModal";
import SearchableSelect from "./SearchableSelect";
import { IRAN_PROVINCES, PROVINCE_CITIES } from "../lib/data/provinces";
import { toast } from "sonner";
import { useRouter } from 'next/navigation';
import api from "../lib/services/api";
export default function CheckoutPage() {
const router = useRouter();
const { items, getTotal, getSubtotal, isSubscribed, charityDonation, setCharityDonation, addOrder, clearCart } = useCartStore();
const { getActivePet, updatePet } = usePetStore();
const { profile, isLoggedIn } = useUserStore();
const [loading, setLoading] = useState(false);
const [useRoundUp, setUseRoundUp] = useState(false);
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 [manualZipCode, setManualZipCode] = useState('');
const [manualAddress, setManualAddress] = useState('');
const handleCopy = (text: string, title: string) => {
navigator.clipboard.writeText(text);
toast.success(`${title} با موفقیت کپی شد!`);
};
useEffect(() => {
Promise.resolve().then(() => {
// 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 || '');
}
});
}, [isLoggedIn, profile]);
const roundStep = Number(useSettingsStore((state) => state.getText('CHARITY_ROUND_STEP', '10000'))) || 10000;
const shippingFeeSetting = useSettingsStore((state) => state.getText('shipping_fee', '0'));
const shippingFee = Number(shippingFeeSetting) || 0;
const isCardToCardEnabled = useSettingsStore((state) =>
state.getBoolean('PAY_GATEWAY_CARD_ENABLE', true) &&
state.getBoolean('cardToCardEnabled', true) &&
state.getBoolean('payment_card_to_card_enabled', true)
);
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() + shippingFee;
const activePet = getActivePet();
// 1. Validate shipping address
const selectedAddr = profile.addresses?.find(a => a.id === selectedAddressId);
let shippingAddressStr = '';
if (selectedAddr) {
shippingAddressStr = `${selectedAddr.province}، ${selectedAddr.city}، ${selectedAddr.detail}${selectedAddr.zipCode ? ` (کد پستی: ${selectedAddr.zipCode})` : ''} (گیرنده: ${selectedAddr.receptorName} - ${toPersian(selectedAddr.phone)})`;
} else if (manualAddress) {
const provCity = [manualProvince, manualCity].filter(Boolean).join('، ');
shippingAddressStr = `${provCity ? provCity + '، ' : ''}${manualAddress}${manualZipCode ? ` (کد پستی: ${manualZipCode})` : ''}${manualName ? ` (گیرنده: ${manualName}${manualPhone ? ` - ${toPersian(manualPhone)}` : ''})` : ''}`;
}
if (!selectedAddressId && !manualAddress) {
toast.error("لطفاً یک آدرس برای ارسال سفارش انتخاب کرده یا آدرس جدید وارد کنید.");
return;
}
// 2. Validate wallet balance if payment method is wallet
if (paymentMethod === 'wallet' && (profile.walletBalance || 0) < totalAmount) {
toast.error(`موجودی کیف پول شما (${(profile.walletBalance || 0).toLocaleString('fa-IR')} تومان) کمتر از مبلغ نهایی سفارش (${totalAmount.toLocaleString('fa-IR')} تومان) است. لطفاً ابتدا کیف پول خود را شارژ کنید.`);
return;
}
setLoading(true);
try {
// 3. Register the order in the backend
const orderId = await addOrder({
items: [...items],
total: totalAmount,
charityDonation: charityDonation,
paymentMethod: paymentMethod,
isRefill: isSubscribed,
petId: activePet?.id,
shippingAddress: shippingAddressStr
});
// 4. Handle Online Payment via Zibal IPG
if (paymentMethod === 'online') {
try {
const res = await api.post('/payment/zibal/initiate', { orderId });
if (res.data?.paymentUrl) {
toast.success("در حال انتقال به درگاه پرداخت زیبال...");
window.location.href = res.data.paymentUrl;
return;
} else {
toast.error("خطا در ایجاد تراکنش پرداخت زیبال");
}
} catch (gatewayErr: unknown) {
const gErr = gatewayErr as { response?: { data?: { message?: string } }; message?: string };
const errMsg = gErr.response?.data?.message || gErr.message || "خطا در اتصال به درگاه پرداخت زیبال";
toast.error(errMsg);
router.push(`/checkout/success/${orderId}`);
return;
}
}
// 5. Re-fetch user profile from backend to sync PostgreSQL wallet balance and charity donation total
const { fetchProfile } = useUserStore.getState();
await fetchProfile();
// 6. 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: unknown) {
// Re-fetch profile to guarantee local state is always identical to PostgreSQL
useUserStore.getState().fetchProfile().catch(() => {});
const errorObj = err as { response?: { data?: { message?: string; details?: Array<{ message?: string }> } }; message?: string };
const backendMessage = errorObj.response?.data?.message || errorObj.response?.data?.details?.[0]?.message || errorObj.message;
// Only toast if message exists and is not a generic duplicate
if (backendMessage && !backendMessage.includes("Request failed") && !backendMessage.includes("خطای غیرمنتظره")) {
toast.error(`خطا در ثبت سفارش: ${backendMessage}`);
}
} 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>
<SearchableSelect
id="chk-province"
options={IRAN_PROVINCES}
value={manualProvince}
placeholder="انتخاب استان..."
searchPlaceholder="جستجوی استان..."
onChange={(prov) => {
const cities = PROVINCE_CITIES[prov] || [];
setManualProvince(prov);
setManualCity(cities[0] || '');
}}
/>
</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] ? (
<SearchableSelect
id="chk-city"
options={PROVINCE_CITIES[manualProvince]}
value={manualCity}
placeholder="انتخاب شهر..."
searchPlaceholder="جستجوی شهر..."
onChange={(c) => setManualCity(c)}
/>
) : (
<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 className="md:col-span-2 space-y-2">
<label htmlFor="chk-zipcode" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">کد پستی ۱۰ رقمی</label>
<input
id="chk-zipcode"
type="text"
maxLength={10}
value={manualZipCode}
onChange={(e) => setManualZipCode(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="۱۲۳۴۵۶۷۸۹۰"
/>
</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">
{(() => {
const getText = useSettingsStore.getState().getText;
const allGateways = [
{
id: 'card',
label: getText('PAY_GATEWAY_CARD_LABEL', 'کارت به کارت (واریز مستقیم)'),
desc: getText('PAY_GATEWAY_CARD_DESC', 'واریز به شماره کارت بانک سامان'),
icon: <CreditCard className="w-5 h-5 sm:w-6 sm:h-6" />,
enabled: isCardToCardEnabled
},
{
id: 'wallet',
label: getText('PAY_GATEWAY_WALLET_LABEL', 'اعتبار حساب (کیف پول)'),
desc: `موجودی: ${toPersian((profile.walletBalance || 0).toLocaleString())} تومان`,
icon: <Wallet className="w-5 h-5 sm:w-6 sm:h-6" />,
enabled: getText('PAY_GATEWAY_WALLET_ENABLE', 'true') === 'true'
},
{
id: 'online',
label: getText('PAY_GATEWAY_ONLINE_LABEL', 'درگاه پرداخت اینترنتی (زیبال / شتاب)'),
desc: getText('PAY_GATEWAY_ONLINE_DESC', 'پرداخت امن و آنی با کلیه کارت‌های عضو شتاب'),
icon: <ShieldCheck className="w-5 h-5 sm:w-6 sm:h-6" />,
enabled: getText('PAY_GATEWAY_ONLINE_ENABLE', 'true') === 'true'
},
{
id: 'cod',
label: getText('PAY_GATEWAY_COD_LABEL', 'پرداخت در محل (کارتخوان)'),
desc: getText('PAY_GATEWAY_COD_DESC', 'تحویل کالا و پرداخت درب منزل'),
icon: <Truck className="w-5 h-5 sm:w-6 sm:h-6" />,
enabled: getText('PAY_GATEWAY_COD_ENABLE', 'false') === 'true'
}
];
const sortedGateways = [...allGateways].sort((a, b) => (b.enabled ? 1 : 0) - (a.enabled ? 1 : 0));
return sortedGateways.map((pay) => (
<div
key={pay.id}
onClick={() => {
if (pay.enabled) {
setPaymentMethod(pay.id);
} else {
toast.info("این روش پرداخت به زودی فعال خواهد شد.");
}
}}
className={cn(
"p-4 sm:p-6 rounded-2xl sm:rounded-3xl border-2 transition-all group flex sm:flex-col items-center sm:items-start gap-4 sm:gap-0 relative",
pay.enabled
? (paymentMethod === pay.id ? "border-canina-blue bg-canina-blue/5 cursor-pointer" : "border-medical-gray-100 hover:border-canina-blue cursor-pointer")
: "border-medical-gray-100 bg-medical-gray-50/60 opacity-60 cursor-not-allowed"
)}
>
<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 w-full">
<div className="flex items-center justify-between gap-2 mb-0.5 sm:mb-1">
<h4 className="font-black text-xs sm:text-sm text-medical-gray-900 truncate">{pay.label}</h4>
{!pay.enabled && (
<span className="text-[9px] font-black bg-amber-100 text-amber-800 px-2 py-0.5 rounded-full shrink-0 font-vazir">
به زودی
</span>
)}
</div>
<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' && pay.enabled && (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 shrink-0 font-vazir"
>
<PlusCircle className="w-3 h-3" />
<span>افزایش موجودی</span>
</button>
)}
</div>
</div>
</div>
));
})()}
</div>
{isCardToCardEnabled && 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>
{shippingFee > 0 ? (
<span className="font-vazir font-bold text-white">{toPersian(shippingFee.toLocaleString())} تومان</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() + shippingFee).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" />
در حال ثبت...
</>
) : (
<>
پرداخت و تکمیل سفارش
<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={async (newAddr) => {
try {
await useUserStore.getState().addAddress(newAddr);
const updatedProfile = useUserStore.getState().profile;
if (updatedProfile.addresses && updatedProfile.addresses.length > 0) {
setSelectedAddressId(updatedProfile.addresses[0].id);
}
setShowAddressModal(false);
} catch (e) {
console.error("Failed to add address", e);
}
}}
/>
</div>
);
}